# 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.

import base64
import logging
import os
import re
import signal
import socket
import threading
import time
import xmlrpclib

log = logging.getLogger('freenasUI.failover.notifier')


class ServiceRemote(threading.Thread):

    def __init__(self, notifier, verb, name, *args, **kwargs):
        self._notifier = notifier
        self._verb = verb
        self._name = name
        threading.Thread.__init__(self, *args, **kwargs)

    def run(self):
        s = self._notifier.failover_rpc()
        try:
            s.service([(self._verb, self._name)])
        except (xmlrpclib.Fault, socket.error) as e:
            log.debug('Failed to run %s(%s): %s', self._verb, self._name, e)


class FailoverNotifier(object):

    BLACKLIST = (
        'backup',
        'hostname',
        'netif',
        'network',
        'system',
        'webshell',
    )

    def _service_sync(self, action, what):
        if what not in self.BLACKLIST and self.failover_status() == 'MASTER':
            sr = ServiceRemote(self, action, what)
            sr.start()
        from freenasUI.failover.models import Failover
        if Failover.objects.all().exists():
            if what == 'network' and self.failover_status() == 'MASTER' and Failover.objects.all()[0].disabled is True:
                sr = ServiceRemote(self, action, what)
                sr.start()


    def reload(self, what):
        self._service_sync('reload', what)
        return super(FailoverNotifier, self).reload(what)

    def restart(self, what):
        self._service_sync('restart', what)
        return super(FailoverNotifier, self).restart(what)

    def start(self, what):
        self._service_sync('start', what)
        return super(FailoverNotifier, self).start(what)

    def stop(self, what):
        self._service_sync('stop', what)
        return super(FailoverNotifier, self).stop(what)

    def _restart_hasyncd(self):
        try:
            with open('/var/run/hasyncd.pid', 'r') as f:
                pid = f.read()
            os.kill(int(pid), signal.SIGTERM)
            time.sleep(0.2)
            os.kill(int(pid), signal.SIGKILL)
        except:
            pass
        self._system_nolog(
            "/usr/local/bin/python /usr/local/www/freenasUI/tools/hasyncd.py"
        )

    def get_carp_states(self):
        p1 = self._pipeopen("/sbin/ifconfig -l")
        res = p1.communicate()[0]
        if p1.returncode != 0:
            raise ValueError('ifconfig failed')
        numbers = re.findall(r'carp([0-9]+)', res)
        numbers = filter(lambda y: y not in ('1', '2'), numbers)

        masters = []
        backups = []
        for number in numbers:
            p1 = self._pipeopen("/sbin/ifconfig carp%s" % number, logger=None)
            res = p1.communicate()[0]
            if p1.returncode != 0:
                raise ValueError('ifconfig failed')
            reg = re.search(r'\bcarp: (?P<status>\w+)\s', res, re.S)
            if not reg:
                raise ValueError('ifconfig failed')
            status = reg.group("status")
            if status == 'MASTER':
                masters.append(number)
            else:
                backups.append(number)
        return masters, backups

    def failover_licensed(self):
        from freenasUI.support.utils import get_license
        license, error = get_license()
        if license is None or not license.system_serial_ha:
            return False
        return True

    def failover_status(self):
        """
        Return the current status of this node in the failover

        Returns:
            MASTER
            BACKUP
            ELECTING
            IMPORTING
            ERROR
            SINGLE
        """

        c, conn = self._open_db(ret_conn=True)
        c.execute(
            'SELECT '
            '   COUNT(*) '
            'FROM network_interfaces '
            'WHERE int_vip != "" AND int_vip IS NOT NULL'
        )
        vipcount = c.fetchone()[0]
        c.execute(
            'SELECT '
            '   COUNT(*) '
            'FROM storage_volume'
        )
        volcount = c.fetchone()[0]
        c.close()
        conn.close()

        if 0 in (vipcount, volcount) or not self.failover_licensed():
            return 'SINGLE'

        ipaddress = self.failover_pair_ip()

        try:
            masters, backups = self.get_carp_states()
        except:
            return 'ERROR'

        if len(masters) > 0:
            p1 = self._pipeopen("/sbin/zpool list -H | grep -v freenas-boot")
            data = p1.communicate()[0].strip()
            if len(data) > 0:
                return 'MASTER'
            else:
                if os.path.exists('/tmp/.failover_electing'):
                    return 'ELECTING'
                elif os.path.exists('/tmp/.failover_importing'):
                    return 'IMPORTING'
                elif os.path.exists('/tmp/.failover_failed'):
                    return 'ERROR'

        s = self.failover_rpc(ip=ipaddress)
        if s is None:
            return 'UNKNOWN'
        try:
            rv = s.pool_available()
            # Other node has the pool
            if rv is True or volcount == 0:
                # check for carp MASTER (any) in remote?
                return 'BACKUP'
            # Other node has no pool
            elif rv is False:
                # check for carp MASTER (none) in remote?
                return 'ERROR'
            # We couldn't contact the other node
            else:
                return 'UNKNOWN'
        except (socket.error, xmlrpclib.Fault) as e:
            return 'UNKNOWN'

    def failover_hardware(self):
        from freenasUI.failover.detect import ha_hardware
        return ha_hardware()

    def failover_internal_interfaces(self):
        # XXX: Use in migrations:
        # failover/migrations/0014
        # XXX: DO NOT REMOVE/CHANGE
        hardware = self.failover_hardware()
        if hardware == 'ECHOSTREAM':
            proc = self._pipeopen('ifconfig -l')
            ifaces = proc.communicate()[0].strip().split(' ')
            ifaces = filter(lambda y: y.startswith('em'), ifaces)
            ifaces = sorted(ifaces, reverse=True)[:1]
            return ifaces
        elif hardware == 'SBB':
            return ['ix0']
        elif hardware == 'ULTIMATE':
            return ['igb1']
        return []

    def failover_node(self):
        from freenasUI.failover.detect import ha_node
        # XXX: Use in migrations:
        # failover/migrations/0016
        # XXX: DO NOT REMOVE/CHANGE
        node = ha_node()
        if node is None:
            return 'MANUAL'
        return node

    def failover_pair_ip(self):
        node = self.failover_node()
        if node == 'MANUAL':
            #c, conn = self._open_db(ret_conn=True)
            #c.execute(
            #    'SELECT '
            #    '   ipaddress '
            #    'FROM system_failover '
            #    'WHERE disabled = 0 '
            #    'ORDER BY -id LIMIT 1'
            #)
            #ipaddress = c.fetchone()
            #c.close()
            #conn.close()
            return None
        elif node == 'A':
            return '169.254.10.2'
        elif node == 'B':
            return '169.254.10.1'
        else:
            return None

    def failover_force_master(self):
        ret = self._system('/usr/local/sbin/fenced force')
        if not ret:
            from freenasUI.failover.models import Failover
            if Failover.objects.count() == 1:
                if Failover.objects.all()[0].disabled is True:
                    self._system('/usr/local/bin/python '
                                 '/usr/local/libexec/truenas/carp-state-change-hook.py '
                                 'carp0 forcetakeover')
        else:
            return False

    def failover_rpc(self, ip=None, timeout=3):
        from freenasUI.failover.utils import TimeoutTransport, ServerProxy
        if ip is None:
            ip = self.failover_pair_ip()
        if ip is None:
            return None
        s = ServerProxy(
            'http://%s:8000' % ip,
            allow_none=True,
            transport=TimeoutTransport(conntimeout=timeout),
        )
        return s

    def sync_carp_ips(self):
        from freenasUI.network.models import Interfaces, Alias
        from django.db.models import Q
        for iface in Interfaces.objects.exclude(Q(int_vip=None) | Q(int_vip='')):
            carp_ips = [str(iface.int_vip)]
            for alias in iface.alias_set.all():
                if not alias.alias_vip:
                    continue
                carp_ips.append(str(alias.alias_vip))
            proc = self._pipeopen('ifconfig carp%d inet' % iface.int_carp)
            output = proc.communicate()[0]
            existing_ips = re.findall(r'inet (\S+)', output)
            for ip in existing_ips:
                if ip in carp_ips:
                    carp_ips.remove(ip)
                else:
                    self._pipeopen('ifconfig carp%d %s delete' % (iface.int_carp, ip))

            for ip in carp_ips:
                self._pipeopen('ifconfig carp%d alias %s/32' % (iface.int_carp, ip))

    def sync_file_recv(self, s, path, dst=None):
        data = s.file_recv(path)
        if data is None:
            return None
        if dst is None:
            dst = path
        with open(dst, 'wb+') as f:
            f.write(base64.b64decode(data[0]))
        os.chmod(dst, data[1])

    def sync_file_send(self, s, path, dest=None):
        if dest is None:
            dest = path
        if os.path.exists(path):
            mode = os.stat(path).st_mode
            with open(path, 'rb') as f:
                first = True
                while True:
                    read = f.read(1024 * 1024 * 20)
                    if not read:
                        break
                    s.file_send(dest, base64.b64encode(read), mode, not first)
                    first = False

    def sync_specific_fields(self, s=None):
        from django.db import connection
        from freenasUI.directoryservice.models import ActiveDirectory
        from freenasUI.network.models import Alias, GlobalConfiguration, Interfaces
        from freenasUI.system.models import SystemDataset

        node = self.failover_node()
        if node not in ('A', 'B'):
            return None

        if s is None:
            s = self.failover_rpc()

        data = s.node_specific_config()

        # Load all the node specific config from other peer
        ad = ActiveDirectory.objects.order_by('-id')
        if ad.exists():
            ad = ad[0]
            for var, val in data['activedirectory'].items():
                setattr(ad, var, val)
            ad.save()

        for ifid, ifdata in data['interfaces'].items():
            iface = Interfaces.objects.filter(id=ifid)
            if not iface.exists():
                continue
            iface = iface[0]
            for var, val in ifdata.items():
                setattr(iface, var, val)
            iface.save()

        for aid, adata in data['alias'].items():
            alias = Alias.objects.filter(id=aid)
            if not alias.exists():
                continue
            alias = alias[0]
            for var, val in adata.items():
                setattr(alias, var, val)
            alias.save()

        sys = SystemDataset.objects.order_by('-id')
        if sys.exists():
            sys = sys[0]
            for var, val in data['systemdataset'].items():
                setattr(sys, var, val)
            sys.save()

        gc = GlobalConfiguration.objects.order_by('-id')
        if gc.exists():
            gc = gc[0]
            for var, val in data['globalconfiguration'].items():
                setattr(gc, var, val)
            gc.save()

        # Delete all sentinel files asking to make this operation
        from_sentinel = '/data/sync_fields_from_peer'
        to_sentinel = '/data/sync_fields_to_peer'
        if os.path.exists(from_sentinel):
            try:
                os.unlink(from_sentinel)
            except:
                pass
        s.file_delete(to_sentinel)

        # Be safe and send a copy of the database
        connection.dump_send()

    def sync_specific_fields_remote(self):
        """
        Helper to call sync_specific_fields in the other node
        """
        s = self.failover_rpc()
        s.notifier('sync_specific_fields', [], {})

    def failover_sync_peer(self, fromto='to'):
        '''
        Runs synctopeer script

        Returns:
            False - No failover configured
            Failover object - Sync success

        Raises:
            ValueError
        '''
        from django.db import connection
        from freenasUI.middleware.notifier import (
            GELI_KEYPATH,
            PWENC_FILE_SECRET,
        )
        from freenasUI.support.utils import LICENSE_FILE

        s = self.failover_rpc()
        ssh_rep_path = '/data/ssh'
        if fromto == 'to':
            log.debug("Dump the database to remote side")
            rv = connection.dump_send()
            log.debug("Send license and pwenc files")
            self.sync_file_send(s, LICENSE_FILE)
            self.sync_file_send(s, PWENC_FILE_SECRET)
            if os.path.exists('/root/.ssh/authorized_keys'):
                self.sync_file_send(s, '/root/.ssh/authorized_keys')

            if os.path.exists(GELI_KEYPATH) and os.path.isdir(GELI_KEYPATH):
                for f in os.listdir(GELI_KEYPATH):
                    path = os.path.join(GELI_KEYPATH, f)
                    if not os.path.isfile(path):
                        continue
                    self.sync_file_send(s, path)

            if os.path.exists(ssh_rep_path) and os.path.isdir(ssh_rep_path):
                for f in os.listdir(ssh_rep_path):
                    path = os.path.join(ssh_rep_path, f)
                    if not os.path.isfile(path):
                        continue
                    self.sync_file_send(s, path)

        else:
            log.debug("Receive the database from remote side")
            rv = s.sync_from()
            log.debug("Receive license and pwenc files")
            self.sync_file_recv(s, LICENSE_FILE)
            self.sync_file_recv(s, PWENC_FILE_SECRET)
            self.sync_file_recv(s, '/root/.ssh/authorized_keys')

            for f in s.listdir(GELI_KEYPATH):
                self.sync_file_recv(s, f)

            for f in s.listdir(ssh_rep_path):
                self.sync_file_recv(s, f)

        s.service([('restart', 'ix-devd')])

        if not rv:
            raise ValueError('Sync %s peer failed' % (fromto, ))
        # Disable HA Repl sync for now
        if False and fromto == 'to':
            remote, wantinit = self.harepl_get()
            if remote != '':
                rv = connection.dump_send()
                if not rv:
                    raise ValueError('Sync %s peer %s failed' % (
                        fromto,
                        remote,
                    ))
        return True

    def failover_public_ips(self):
        from freenasUI.network.models import Interfaces
        from django.db.models import Q
        node = self.failover_node() or 'A'
        ips = []
        for iface in Interfaces.objects.exclude(Q(int_vip=None) | Q(int_vip='')):
            if node == 'A' and iface.int_ipv4address:
                ips.append(str(iface.int_ipv4address))
            elif node == 'B' and iface.int_ipv4address_b:
                ips.append(str(iface.int_ipv4address_b))
        return ips

    def harepl_get(self):
        p = self._pipeopen('zpool list -H -o name | grep -v freenas-boot')
        if p.wait() == 0:
            pool = p.communicate()[0].split('\n')[0].strip()
            p = self._pipeopen(
                '/sbin/zfs list -H -o com.ixsystems:replremote %s' % pool
            )
            remote = p.communicate()[0].split('\n')[0].strip()
            if remote == '-':
                remote = ''
            p = self._pipeopen(
                '/sbin/zfs list -H -o com.ixsystems:replinit %s' % pool
            )
            wantinit = p.communicate()[0].split('\n')[0].strip()
            wantinit = (wantinit == 'True')
            return (remote, wantinit)
        else:
            log.warn('Can not get pool list')
        return None, None

    def harepl_set(self, remote, wantinit):
        p = self._pipeopen('zpool list -H -o name | grep -v freenas-boot')
        if p.wait() == 0:
            pool = p.communicate()[0].split('\n')[0].strip()
            if remote == '':
                cmd = '/sbin/zfs inherit com.ixsystems:replremote %s' % pool
                self._system(cmd)
                cmd = '/sbin/zfs inherit com.ixsystems:replinit %s' % pool
                self._system(cmd)
            else:
                cmd = '/sbin/zfs set com.ixsystems:replremote=%s %s' % (
                    remote,
                    pool
                )
                self._system(cmd)
                if wantinit:
                    cmd = '/sbin/zfs set com.ixsystems:replinit=True %s' % pool
                else:
                    cmd = '/sbin/zfs inherit com.ixsystems:replinit %s' % pool
                self._system(cmd)
            self._system('/usr/sbin/service ix-crontab quietstart')
        else:
            log.warn('Can not get pool list')

