#!/usr/local/bin/python
#-
# Copyright (c) 2015 iXsystems, Inc.
# All rights reserved.
# This file is a part of TrueNAS
# and may not be copied and/or distributed
# without the express permission of iXsystems.

from socketserver import ThreadingMixIn
from xmlrpc.server import (
    SimpleXMLRPCRequestHandler,
    SimpleXMLRPCServer,
    resolve_dotted_attribute,
)
import base64
import fcntl
import logging
import logging.config
import os
import resource
import sys
import xmlrpc.client

from setproctitle import setproctitle
import daemon

LOG_FILE = '/var/log/hasyncd.log'
SYNC_FILE = '/var/tmp/sync_failed'


class PidFile(object):

    def __init__(self, path):
        self.path = path
        self.pidfile = None

    def __enter__(self):
        self.pidfile = open(self.path, 'a+')
        try:
            fcntl.flock(self.pidfile.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
        except IOError:
            raise SystemExit('Already running according to ' + self.path)
        self.pidfile.seek(0)
        self.pidfile.truncate()
        self.pidfile.write(str(os.getpid()))
        self.pidfile.flush()
        self.pidfile.seek(0)
        return self.pidfile

    def __exit__(self, *args, **kwargs):
        try:
            if os.path.exists(self.path):
                os.unlink(self.path)
            self.pidfile.close()
        except IOError:
            pass


class Request(object):

    def __init__(self, **kwargs):
        self.client_address = kwargs.pop('client_address')
        self.connection = kwargs.pop('connection')
        self.headers = kwargs.pop('headers')


class HASyncRequestHandler(SimpleXMLRPCRequestHandler):

    protocol_version = 'HTTP/1.1'

    def log_message(self, format, *args):
        return None

    def _dispatch(self, method, params):
        """
        Method based on SimpleXMLRPCDispatcher to pass the request object
        as argument.
        """
        func = None
        try:
            # check to see if a matching function has been registered
            func = self.server.funcs[method]
        except KeyError:
            if self.server.instance is not None:
                # check for a _dispatch method
                if hasattr(self.server.instance, '_dispatch'):
                    return self.server.instance._dispatch(method, params)
                else:
                    # call instance method directly
                    try:
                        func = resolve_dotted_attribute(
                            self.server.instance,
                            method,
                            self.server.allow_dotted_names
                            )
                    except AttributeError:
                        pass

        request = Request(
            client_address=self.client_address,
            connection=self.connection,
            headers=self.headers,
        )
        if func is not None:
            try:
                return func(request, *params)
            except xmlrpc.client.Fault:
                raise
            except:
                log.exception('Failed to run method "%s"', method)
                raise
        else:
            raise Exception('method "%s" is not supported' % method)


class HASyncServer(ThreadingMixIn, SimpleXMLRPCServer):

    _shutdown = False

    def shutdown(self):
        self._shutdown = True

    def serve_forever(self):
        while not self._shutdown: self.handle_request()


class Funcs:

    def __init__(self, server, *args, **kwargs):
        from django.db import connection
        from freenasUI.common.system import get_sw_version
        self._conn = connection
        self._server = server
        self._version = get_sw_version()

    def _ebRender(self, failure):
        return xmlrpc.client.Fault(self.FAILURE, str(failure))

    def _authenticated(self, request):
        from freenasUI.failover.models import Failover
        client_addr, client_port = request.connection.getpeername()
        # Connection is only allowed from privileged port and IPs
        if client_port > 1024 or client_addr not in (
            '169.254.10.1',
            '169.254.10.2',
            '169.254.10.20',
            '169.254.10.80',
        ):
            raise xmlrpc.client.Fault(5, 'Access Denied')
        try:
            return Failover.objects.order_by('-id')[0]
        except IndexError:
            return Failover.objects.create()

    def _check_version(self, request):
        rversion = request.headers.get('Version')
        if rversion != self._version:
            raise xmlrpc.client.Fault(55, 'Versions mismatch (%s != %s' % (
                self._version, rversion
            ))

    def enc_getkey(self, request):
        from freenasUI.failover.enc_helper import LocalEscrowCtl
        self._authenticated(request)
        escrowctl = LocalEscrowCtl()
        return escrowctl.getkey()

    def enc_setkey(self, request, passphrase):
        from freenasUI.failover.enc_helper import LocalEscrowCtl
        self._authenticated(request)
        escrowctl = LocalEscrowCtl()
        return escrowctl.setkey(passphrase)

    def listdir(self, request, path):
        self._authenticated(request)
        if not os.path.exists(path):
            return []
        if not os.path.isdir(path):
            return []

        files = []
        for f in os.listdir(path):
            fullpath = os.path.join(path, f)
            if not os.path.isfile(fullpath):
                continue
            files.append(fullpath)
        return files

    def ping(self, request):
        return 'pong'

    def boot_ready(self, request):
        return os.path.exists('/tmp/.bootready')

    def pool_available(self, request):
        from freenasUI.middleware.notifier import notifier
        self._authenticated(request)
        p1 = notifier()._pipeopen('zpool list -H | grep -v freenas-boot')
        data = p1.communicate()[0].strip()
        return len(data) > 0

    def file_recv(self, request, path):
        self._authenticated(request)
        if not os.path.exists(path):
            return None
        with open(path, 'rb') as f:
            data = base64.b64encode(f.read()).decode()
        mode = os.stat(path).st_mode
        return data, mode

    def file_send(self, request, path, content, mode, chunk=False):
        self._authenticated(request)
        dirname = os.path.dirname(path)
        if not os.path.exists(dirname):
            os.makedirs(dirname)
        if chunk:
            openmode = 'ab'
        else:
            openmode = 'wb+'
        with open(path, openmode) as f:
            f.write(base64.b64decode(content))
        os.chmod(path, mode)
        return True

    def file_delete(self, request, path):
        self._authenticated(request)
        if not os.path.exists(path):
            return False
        os.unlink(path)
        return True

    def run_sql(self, request, query, params):
        self._authenticated(request)
        self._check_version(request)
        cursor = self._conn.cursor()
        if params is None:
            cursor.executelocal(query)
        else:
            cursor.executelocal(query, params)
        cursor.close()

    def service(self, request, verbs):
        from freenasUI.middleware.notifier import notifier
        self._authenticated(request)
        rvs = []
        _n = notifier()
        for verb, service, timeout, onetime in verbs:
            if verb not in (
                'start', 'stop', 'restart', 'reload'
            ):
                continue
            rvs.append(getattr(_n, verb)(service, timeout=timeout, onetime=onetime))
        return rvs

    def sync_to(self, request, query):
        self._check_version(request)
        self._authenticated(request)
        return self._conn.dump_recv(query)

    def sync_from(self, request):
        failover = self._authenticated(request)
        self._check_version(request)
        return self._conn.dump_send()

    def node_specific_config(self, request):
        from collections import defaultdict
        from freenasUI.middleware.notifier import notifier
        from freenasUI.network.models import (
            Alias, GlobalConfiguration, Interfaces
        )
        from freenasUI.services.models import CIFS
        from freenasUI.system.models import SystemDataset

        self._check_version(request)
        self._authenticated(request)

        data = {
            'cifs': {},
            'alias': defaultdict(dict),
            'interfaces': {},
            'globalconfiguration': {},
            'systemdataset': {},
        }

        node = notifier().failover_node()
        if node not in ('A', 'B'):
            return data

        if node == 'A':
            aliasv4 = 'alias_v4address'
            aliasv6 = 'alias_v6address'
            netbios = 'cifs_srv_netbiosname'
            ipv4 = 'int_ipv4address'
            uuid = 'sys_uuid'
            hostname = 'gc_hostname'
        else:
            aliasv4 = 'alias_v4address_b'
            aliasv6 = 'alias_v6address_b'
            netbios = 'cifs_srv_netbiosname_b'
            ipv4 = 'int_ipv4address_b'
            uuid = 'sys_uuid_b'
            hostname = 'gc_hostname_b'

        cifs = CIFS.objects.order_by('-id')
        if cifs.exists():
            cifs = cifs[0]
            data['cifs'][netbios] = getattr(cifs, netbios)

        gc = GlobalConfiguration.objects.order_by('-id')
        if gc.exists():
            gc = gc[0]
            data['globalconfiguration'][hostname] = getattr(gc, hostname)

        for iface in Interfaces.objects.all():
            data['interfaces'][str(iface.id)] = {
                ipv4: str(getattr(iface, ipv4)),
            }

        for alias in Alias.objects.all():
            key = str(alias.id)
            ipv4 = getattr(alias, aliasv4)
            if ipv4:
                data['alias'][key][aliasv4] = str(ipv4)
            ipv6 = getattr(alias, aliasv6)
            if ipv6:
                data['alias'][key][aliasv6] = str(ipv6)

        # RPC can't marshall defaultdict object
        data['alias'] = dict(data['alias'])

        sys = SystemDataset.objects.order_by('-id')
        if sys.exists():
            sys = sys[0]
            data['systemdataset'][uuid] = getattr(sys, uuid)

        return data

    def version(self, request):
        self._authenticated(request)
        return self._version

    def reboot(self, request):
        from freenasUI.middleware.notifier import notifier
        self._authenticated(request)
        self._server.shutdown()
        notifier().restart("system")

    def notifier(self, request, method, args, kwargs):
        if not args:
            args = []
        if not kwargs:
            kwargs = {}
        from freenasUI.middleware.notifier import notifier
        self._authenticated(request)
        return getattr(notifier(), method)(*args, **kwargs)

    def hactl(self, request, action, args=None):
        from freenasUI.middleware.notifier import notifier
        self._check_version(request)
        failover = self._authenticated(request)

        if args is None:
            args = []

        if action == 'enable':
            if failover.disabled is False:
                return False
            failover.disabled = False
            failover.master = False
            failover.save()
            notifier().start("ix-devd")
        elif action == 'disable':
            if failover.disabled is True:
                return False
            failover.disabled = True
            if 'active' in args:
                failover.master = True
            elif 'passive' in args:
                failover.master = False
            failover.save()
            notifier().start("ix-devd")

    def updated(self, request, download=True, apply=False):
        from freenasUI.middleware.notifier import notifier
        from freenasUI.system.models import Update as mUpdate
        from freenasUI.system.utils import run_updated

        self._authenticated(request)

        # Create a memory device filesystem
        # Since standby node has no disks
        notifier().create_upload_location()

        try:
            updateobj = mUpdate.objects.order_by('-id')[0]
        except IndexError:
            updateobj = mUpdate.objects.create()

        returncode, uuid = run_updated(
            str(updateobj.get_train()),
            '/var/tmp/firmware/update',
            download=download,
            apply=apply,
        )
        if returncode != 0:
            return False
        return uuid

    def updated_handler(self, request, uuid):
        from freenasUI.system.utils import (
            UpdateHandler,
        )
        self._authenticated(request)
        handler = UpdateHandler(uuid=uuid)
        if handler.finished and not handler.error:
            handler.exit()
        return {
            'uuid': handler.uuid,
            'apply': handler.apply,
            'data': handler.load(),
            'error': handler.error,
            'finished': handler.finished,
            'reboot': handler.reboot,
        }

    def update_check(self, request):
        from freenasOS import Configuration
        from freenasOS.Exceptions import UpdateManifestNotFound
        from freenasOS.Update import CheckForUpdates
        from freenasUI.system.models import Update as mUpdate
        from middlewared.plugins.update import (
            CheckUpdateHandler, get_changelog
        )

        self._authenticated(request)

        try:
            updateobj = mUpdate.objects.order_by('-id')[0]
        except IndexError:
            updateobj = mUpdate.objects.create()

        handler = CheckUpdateHandler()
        try:
            update = CheckForUpdates(
                diff_handler=handler.diff_call,
                handler=handler.call,
                train=updateobj.get_train(),
            )
            network = True
        except UpdateManifestNotFound:
            network = False
            update = False
        if update:
            conf = Configuration.Configuration()
            sys_mani = conf.SystemManifest()
            if sys_mani:
                sequence = sys_mani.Sequence()
            else:
                sequence = ''
            changelog = get_changelog(
                updateobj.get_train(),
                start=sequence,
                end=update.Sequence(),
            )
        else:
            changelog = None

        return {
            'update': {
                'Notice': update.Notice,
                'Notes': update.Notes,
            } if update else update,
            'network': network,
            'handler': {
                'output': handler.output,
                'reboot': handler.reboot,
            },
            'changelog': changelog,
        }

    def update_pending(self, request):
        from freenasUI.middleware.client import client
        self._authenticated(request)
        with client as c:
            return c.call('update.get_pending', '/var/tmp/firmware/update')

    def update_manual(self, request, path):
        from freenasUI.system.utils import manual_update
        self._authenticated(request)
        return manual_update(path)


if __name__ == '__main__':

    """
    python-daemon uses rlimit to determine how many file descriptors to close
    before forking.
    This limit might be really high and take a long time in TrueNAS, so lets set it
    to a small value.
    """
    resource.setrlimit(resource.RLIMIT_NOFILE, (2048, 2048))

    pidfile = PidFile('/var/run/hasyncd.pid')

    logging.config.dictConfig({
        'version': 1,
        'disable_existing_loggers': False,
        'formatters': {
            'simple': {
                'format': '%(levelname)s %(asctime)s: %(message)s'
            },
        },
        'handlers': {
            'file': {
                'level': 'DEBUG',
                'filters': [],
                'class': 'logging.handlers.RotatingFileHandler',
                'maxBytes': 1024 * 1024 * 10,
                'backupCount': 5,
                'filename': LOG_FILE,
                'formatter': 'simple',
            },
        },
        'loggers': {
            'hasyncd': {
                'handlers': ['file'],
                'level': 'DEBUG',
                'propagate': True,
            },
        }
    })

    context = daemon.DaemonContext(
        working_directory='/root',
        umask=0o002,
        pidfile=pidfile,
        stdout=logging._handlers['file'].stream,
        stderr=logging._handlers['file'].stream,
        detach_process=True,
        files_preserve=[logging._handlers['file'].stream, sys.stdin.fileno()],
    )

    with context:

        if os.path.exists(LOG_FILE):
            os.chmod(LOG_FILE, 0o660)

        log = logging.getLogger('hasyncd')

        setproctitle('hasyncd')
        sys.path.extend([
            '/usr/local/lib',
            '/usr/local/www',
            '/usr/local/www/freenasUI'
        ])

        os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'freenasUI.settings')

        import django
        django.setup()

        from freenasUI.freeadmin.utils import set_language
        set_language()

        log.debug('Starting Journal')

        server = HASyncServer(
            ('0.0.0.0', 8000),
            HASyncRequestHandler,
            allow_none=True,
        )
        server.register_instance(Funcs(server))
        server.serve_forever()
