#-*- coding: utf-8 -*-

# Copyright 2008-2013 Calculate Ltd. http://www.calculate-linux.org
#
#  Licensed under the Apache License, Version 2.0 (the "License");
#  you may not use this file except in compliance with the License.
#  You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
#  Unless required by applicable law or agreed to in writing, software
#  distributed under the License is distributed on an "AS IS" BASIS,
#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#  See the License for the specific language governing permissions and
#  limitations under the License.

import os
import sys
from os import path
import re
from calculate.lib.datavars import (Variable,VariableError,ReadonlyVariable,
                                    ReadonlyTableVariable,FieldValue)
from calculate.lib.cl_template import iniParser
from calculate.lib.utils.files import (readLinesFile,isMount,readFile, find,
    FindFileType)
from calculate.lib.utils.common import getValueFromCmdLine,cmpVersion
from calculate.lib.utils.portage import isPkgInstalled
from calculate.lib.variables.user import VariableUrLogin
from calculate.lib.convertenv import convertEnv
from calculate.lib.utils.ip import isOpenPort
import time
import ldap
from socket import gethostbyname
from calculate.lib.cl_ldap import ldapUser
from calculate.lib.variables.user import LdapHelper
import pwd
from calculate.client.client import Client

from calculate.lib.cl_lang import setLocalTranslate
setLocalTranslate('cl_client3',sys.modules[__name__])

class VariableClRemoteHost(Variable):
    """
    IP or domain name of CDS
    """
    value = ""

class VariableClRemoteHostNew(Variable):
    """
    IP or domain name of CDS
    """
    opt = ["cl_remote_host_new"]
    metavalue = "DOMAIN"
    type = "choiceedit"
    value = ""
    untrusted = True

    def init(self):
        self.label = _("Domain IP")
        self.help = _("domain")

    def check(self,value):
        if self.Get('cl_client_mount_set') == 'off':
            if self.Get('cl_localhost_set') == 'off':
                if self.Get('cl_remote_host') == '':
                    if not value:
                        raise VariableError(_("Please specify the domain"))
                    elif not isOpenPort(value,445):
                        raise VariableError(
                            _("The specified address is not available"))

class VariableClRemoteHostLive(ReadonlyVariable):
    """
    Remote host from /proc/cmdline param domain
    """
    def get(self):
        return getValueFromCmdLine("calculate","domain") or ""


class VariableOsRemoteAuth(Variable):
    """
    Client work mode (local or hostname)
    """


class VariableOsRemoteClient(Variable):
    """
    Version which apply templates
    """
    pass


class VariableClRemotePw(Variable):
    """
    Client password
    """
    type = "onepassword"
    opt = ["--domain-password"]

    def init(self):
        self.label = _("Domain password")
        self.help = _("specify the domain password")

    def get(self):
        return getValueFromCmdLine("calculate","domain_pw") or ""

class VariableClMovedSkipPath(Variable):
    """
    Skip "Moved" path
    """
    type = "list"
    value = ['Disks','Home','Moved','FTP','Desktop', 'Share']

class VariableClSyncSkipPath(Variable):
    """
    Skip sync path
    """
    type = "list"
    value = [".googleearth", "Home", "Disks", "FTP",
        'Share', ".local/share/akonadi/db_data", ".VirtualBox",
        ".mozilla/firefox/calculate.default/urlclassifier3.sqlite",
        ".local/share/mime/mime.cache", ".gvfs",
        ".kde4/share/apps/nepomuk/repository/main/data", ".logout",
        ".Xauthority", ".thumbnails", ".mozilla/firefox/*/Cache",
        ".kde4/socket-*", ".cache/", ".local/share/Trash"]

class VariableClSyncDelPath(Variable):
    """
    Removed path on sync
    """
    type = "list"
    value = [".kde4/share/config/phonondevicesrc",
             ".kde4/cache-*", ".kde4/tmp-*"]

class VariableClProfileAllSet(Variable):
    type = "bool"
    value = "off"

class VariableClClientSync(Variable):
    type = "bool"
    value = "on"
    metavalue = "ON/OFF"

    opt = ["--sync"]

    def init(self):
        self.label = _("Synchronize the user profile")
        self.help = _("synchronize user preferences")

class VariableClLocalhostSet(Variable):
    """
    Using autopartition
    """
    type = "bool"
    element = "radio"
    value = "off"
    opt = ["-r"]
    metavalue = "ON/OFF"
    untrusted = True

    def init(self):
        self.label = _("Workstation role")
        self.help = _("remove the domain connection settings")

    def choice(self):
        return [("off",_("Domain workstation")),
                ("on",_("Local workstation"))]

    def check(self,value):
        if self.Get('cl_client_mount_set') == 'off':
            if self.Get('cl_remote_host') == '' and value == "on":
                raise VariableError(_("The workstation is not in the domain"))
            if self.Get('cl_remote_host') != '' and value == "off":
                raise VariableError(_("The workstation is already in the domain %s")
                                %self.Get('cl_remote_host') + "\n" + 
                                _("Before joining the domain, "
                                "you need to remove it from the previous domain"))

    #def get(self):
    #    if self.Get('cl_remote_host') == '':
    #        return "on"
    #    else:
    #        return "off"

class VariableClClientMountSet(Variable):
    """
    Mount remote by configured domain information
    """
    type = "bool"
    value = "off"
    metavalue = "ON/OFF"
    opt = ["--mount"]

    def init(self):
        self.label = _("Only mount the domain resource")
        self.help = _("only mount the [remote] domain resource")

class VariableUrUserPw(Variable,LdapHelper):
    """
    Current user password
    """
    type = "need-onepassword"
    opt = ["--old-password"]
    metavalue = "OLDPASSWORD"
    value = ""
    untrusted = True

    def init(self):
        self.label = _("Current password")
        self.help = _("current user password")

    def checkUserPwdLDAP(self, server, userDN, password):
        """Check unix user password on server"""
        ldapInit = ldap.initialize("ldap://%s"%server)
        errMessage = ""
        try: 
            ldapInit.bind_s(userDN, password)
        except ldap.INVALID_CREDENTIALS:
            raise VariableError(_("Wrong password"))
        except ldap.LDAPError, e:
            errMessage = e[0]['desc']
            raise VariableError(errMessage)
        return True

    def check(self,value):
        if not value:
            raise VariableError(_("Empty password"))
        # читаем os_remote_auth, так как при смене пароля
        # чтение должно выполняться от пользователя,
        # cl_remote_host не может быть прочитан пользователем
        server = self.Get('os_remote_auth')
        ldapObj = self.getLdapUserObject()
        if ldapObj:
            usersDN = ldapObj.getUsersDN()
            userDN = ldapObj.addDN("uid=%s"%self.Get('ur_login'),
                                            usersDN)
            self.checkUserPwdLDAP(server,userDN,value)

class VariableUrUserNewPw(Variable):
    """
    New user password
    """
    type = "need-password"
    opt = ["--new-password"]
    metavalue = "NEWPASSWORD"
    value = ""
    untrusted = True

    def init(self):
        self.label = _("New password")
        self.help = _("new user password")

    def check(self,value):
        if not value:
            raise VariableError(_("Empty password"))

class VariableClClientLogin(VariableUrLogin):
    """
    User Login
    """
    opt = ["cl_client_login"]
    alias = "ur_login"

    def choice(self):
        loginChoice = VariableUrLogin.choice(self)
        if self.Get('cl_action') == 'passwd':
            return filter(lambda x:x != "root",loginChoice)
        else:
            return loginChoice

    def check(self,value):
        """Does user exist"""
        if not value in self.choice() and self.Get('cl_action') == 'logout':
            raise VariableError(_("X session users not found"))
        if value == "":
            raise VariableError(_("Please specify the user"))
        if value == "root" and self.Get('cl_action') == 'passwd':
            raise VariableError(\
                _("This action can be executed by a non-root user only"))
        try:
            pwd.getpwnam(value).pw_gid
        except:
            raise VariableError(_("User %s does not exist")%value)

    def get(self):
        if (self.Get('cl_action') == 'passwd' and 
            self.Get('ur_login') != 'root'):
            return self.Get('ur_login')
        return ""

class VariableClClientRelevanceSet(ReadonlyVariable):
    """
    Актуальны ли сейчас выполненные шаблоны
    """
    type = "bool"

    def get(self):
        # если происходят действия ввода или вывода из домена
        if (self.Get('cl_action') in ("domain","undomain") and
            self.Get('cl_client_mount_set') == 'off'):
            return "off"
        # если изменился домен
        if self.Get('cl_remote_host') != self.Get("os_remote_auth"):
            return "off"
        if (self.Get('cl_remote_host') and
            not isMount(self.Get('cl_client_remote_path'))):
            return "off"
        return "on"

class VariableClClientRemotePath(Variable):
    """
    Путь для монитрования //domain/remote
    """
    value = "/var/calculate/remote"

class VariableClClientProfileName(Variable):
    """
    Название удаленного профиля (CLD,CLDX,all)
    """
    def get(self):
        return ("all" if self.Get('cl_profile_all_set') == 'on'
                else self.Get('os_linux_shortname'))

class VariableClLdapData(ldapUser,ReadonlyVariable):
    """
    Внутренняя переменная, содержащая объект для доступа к данным LDAP
    """
    type = "object"

    def get(self):
        return self

    def getReplDN(self):
        """
        Получить из LDAP домен на котором находится актуальный профиль
        Get from LDAP last domain server

        Branch has ((username,systemname,host))
        """
        usersDN = self.getUsersDN()
        if usersDN:
            partDN = "ou=Worked,ou=Replication,ou=LDAP"
            servicesDN = "ou=Services"
            baseDN = usersDN.rpartition(servicesDN+",")[2]
            replDN = self.addDN(partDN, servicesDN, baseDN)
            return replDN
        return False

    def searchPrevHost(self, userName, osLinuxShort):
        """Find server which user use"""
        connectData = self.getBindConnectData()
        if connectData:
            bindDn, bindPw, host = connectData
            replDN = self.getReplDN()
            # find string for service replication branch
            userAndOsName = "%s@%s"%(userName,osLinuxShort)
            findAttr = "uid=%s"%userAndOsName
            # connect to LDAP
            if not self.ldapConnect(bindDn, bindPw, host):
                return False
            resSearch = self.ldapObj.ldapSearch(replDN, ldap.SCOPE_ONELEVEL,
                                                findAttr, ["host"])
            return resSearch
        return False

    def _gethostbyname(self,hostname):
        try:
            return gethostbyname(hostname)
        except:
            pass
        return None

    def getNameRemoteServer(self,userName, osLinuxShort, curHost):
        """
        Get remote domain hostname or empty if profile is keeped on 
        current server
        """
        searchPrevHost = self.searchPrevHost(userName, osLinuxShort)
        if searchPrevHost and searchPrevHost[0][0][1].has_key('host'):
            prevHost = searchPrevHost[0][0][1]['host'][0]
        else:
            prevHost = None
        # get ip address of previous server and current server
        prevIp = self._gethostbyname(prevHost)
        curIp = self._gethostbyname(curHost)
        # if actual profile not found or on local
        if not prevHost or curIp and prevIp == curIp:
            return ""
        else:
            return prevHost

    def isRepl(self):
        """
        Is on or off replication on server
        """
        connectData = self.getBindConnectData()
        if connectData:
            bindDn, bindPw, host = connectData
            usersDN = self.getUsersDN()
            partDN = "ou=Replication,ou=LDAP"
            servicesDN = "ou=Services"
            baseDN = usersDN.rpartition(servicesDN+",")[2]
            replDN = self.addDN(partDN, servicesDN, baseDN)
            findAttr = "ou=Worked"
            # connect to LDAP
            if not self.ldapConnect(bindDn, bindPw, host):
                return False
            resSearch = self.ldapObj.ldapSearch(replDN, ldap.SCOPE_ONELEVEL,
                                                findAttr,
                                                [findAttr.partition("=")[0]])
            if resSearch:
                return True
        return False


class VariableClReplicationHost(ReadonlyVariable):
    """
    Удаленный сервер при репликации, который содержит актуальный профиль
    """
    def get(self):
        return self.Get('cl_ldap_data').getNameRemoteServer(
            self.Get('ur_login'),self.Get('cl_client_profile_name'),
            self.Get('cl_remote_host'))

class VariableClClientUserMountData(ReadonlyTableVariable):
    """
    Таблица монтирования ресурсов
    """
    source = ['cl_client_user_mount_name',
              'cl_client_user_mount_resource',
              'cl_client_user_mount_path',
              'cl_client_user_mount_host']

    def get(self):
        home = path.split(self.Get('ur_home_path'))[0]
        envFile = self.Get('cl_env_server_path')
        def generate():
            yield ("share","share",path.join(self.Get('ur_home_path'),"Share"),
                   self.Get('cl_remote_host'))
            yield ("unix","unix",path.join(home,".%s"%self.Get('ur_login')),
                   self.Get('cl_remote_host'))
            yield ("homes","homes",path.join(self.Get('ur_home_path'),"Home"),
                   self.Get('cl_remote_host'))
            if convertEnv().getVar("ftp","host"):
                yield ("ftp","ftp",path.join(self.Get('ur_home_path'),"FTP"),
                   self.Get('cl_remote_host'))
            else:
                yield ("ftp",'','','')
            if self.Get('cl_replication_host'):
                yield ("remote_profile","unix",
                       path.join(home,".%s.remote"%self.Get('ur_login')),
                       self.Get('cl_replication_host'))
            else:
                yield ("remote_profile",'unix','','')
        return list(generate())

class VariableClClientUserMountName(FieldValue,ReadonlyVariable):
    """
    Название удаленного ресурса
    """
    type = "list"
    source_variable = "cl_client_user_mount_data"
    column = 0


class VariableClClientUserMountResource(FieldValue,ReadonlyVariable):
    """
    Название удаленного ресурса
    """
    type = "list"
    source_variable = "cl_client_user_mount_data"
    column = 1

class VariableClClientUserMountPath(FieldValue,ReadonlyVariable):
    """
    Путь подключения удаленного ресурса
    """
    type = "list"
    source_variable = "cl_client_user_mount_data"
    column = 2

class VariableClClientUserMountHost(FieldValue,ReadonlyVariable):
    """
    Удаленный сервер
    """
    type = "list"
    source_variable = "cl_client_user_mount_data"
    column = 3

class SyncHelper:
    """
    Вспомогательный объект для определения статуса синхронизации и времени
    по конфигурационным файлам
    """
    def getSyncStatus(self,rpath):
        """
        Получить  status_sync из desktop файла
        """
        fileConfig = path.join(rpath, Client.configFileDesktop)
        # get data from desktop config on success run get by archive
        if os.path.exists(fileConfig):
            objConfig = iniParser(fileConfig)
            data = self.getDataInConfig("main", ["status_sync"],
                                                 objConfig)
            if data:
                return data.get("status_sync","")
        return ""

    def getDataInConfig(self, section, listVars, objConfig):
        """
        Прочитать список переменных из области конфигурационного файла
        """
        varsConfig = {}
        for varName in listVars:
            varsConfig[varName] = objConfig.getVar(section,varName)
        if objConfig.getError():
            return False
        return varsConfig


    def convertDate(self,strdate,dateformat="%Y-%m-%d %H:%M:%S"):
        """
        Convert date from string format (dateformat) to stuct or None
        """
        if strdate:
            try:
                return time.strptime(strdate,dateformat)
            except ValueError:
                pass
        return ""

    def getDateObjClientConf(self, rpath):
        """
        Получить время синхронизации из .calculate/desktop.env
        """
        fileConfig = path.join(rpath, Client.configFileDesktop)
        if os.path.exists(fileConfig):
            objConfig = iniParser(fileConfig)
            data = self.getDataInConfig("main", ["date", "date_logout"],
                                        objConfig)
            timeLogout = data["date_logout"]
            timeConfig = data["date"]
            dates = filter(None,
                    [self.convertDate(timeLogout),
                     self.convertDate(timeConfig)])
            if dates:
                return dates[0]
        return ""

    def checkNeedSync(self,homeDir,rpath,curTimeObj,curStatusSync,osLinuxShort):
        """
        Проверить необходимость синхронизации текущего профиля с удаленным
        """
        # profile directory
        #fileConfig = os.path.join(homeDir, Client.configFileServer)
        pathProfile = os.path.join(rpath, osLinuxShort)
        #if readFile(fileConfig).strip():
        #    return True
        fileSoftConfigThis = os.path.join(pathProfile,
                                      Client.configFileSoft)
        fileSoftConfigCur = os.path.join(homeDir,
                                      Client.configFileSoft)
        xSessionCur = iniParser(fileSoftConfigCur).getVar('main','xsession')
        xSessionThis = iniParser(fileSoftConfigThis).getVar('main','xsession')
        # check profile date on current server
        #fileConfigThis = os.path.join(pathProfile, Client.configFileDesktop)
        #if iniParser(fileConfigThis).getVar('main','status_sync') == "success":
        #    self.setVarToConfig("main", {"status_sync":"success_mount"},
        #                        fileConfigThis)
        thisTimeObj = self.getDateObjClientConf(pathProfile)
        if curStatusSync == "success_logout" and \
            xSessionCur == xSessionThis and \
            thisTimeObj and curTimeObj and \
            curTimeObj >= thisTimeObj:
            return False
        return True

class VariableClClientSyncTime(SyncHelper,ReadonlyVariable):
    """
    Текущее время синхронизации профиля
    """
    def get(self):
        return self.getDateObjClientConf(self.Get('ur_home_path'))

class VariableClClientPackTime(SyncHelper,ReadonlyVariable):
    """
    Время комады упаковки профиля
    """
    def get(self):
        return str(float(time.time()))

class VariableClClientSyncStatus(SyncHelper,ReadonlyVariable):
    """
    Текущий статус синхронизации профиля
    """
    def get(self):
        return self.getSyncStatus(self.Get('ur_home_path'))

class VariableClClientLocalSyncTime(SyncHelper,ReadonlyVariable):
    """
    Текущий статус синхронизации профиля
    """
    def get(self):
        return self.getDateObjClientConf(
                path.join(
                self.Select('cl_client_user_mount_path',
                    where='cl_client_user_mount_name',eq='unix',
                    limit=1),self.Get('cl_client_profile_name')))

class VariableClClientSyncReplicationSet(SyncHelper,ReadonlyVariable):
    """
    Нужно ли синхронизировать текущий профиль с удаленным доменом
    """
    type = "bool"

    def get(self):
        if not self.Get('cl_replication_host'):
            return "off"
        profilePath = self.Select('cl_client_user_mount_path',
                        where='cl_client_user_mount_name',
                        eq='remote_profile',limit=1)
        if self.Get('cl_action') == 'login' and not isMount(profilePath):
            raise VariableError(_("Remote profile not mounted"))
        return "on" if self.checkNeedSync(self.Get('ur_home_path'),profilePath,
                    self.Get('cl_client_sync_time'),
                    self.Get('cl_client_sync_status'),
                    self.Get('cl_client_profile_name')) else "off"

class VariableClClientSyncLocalSet(SyncHelper,ReadonlyVariable):
    """
    Нужно ли синхронизировать текущий профиль с локальным доменом
    """
    type = "bool"

    def get(self):
        if not self.Get('cl_remote_host'):
            return "off"
        profilePath = self.Select('cl_client_user_mount_path',
                        where='cl_client_user_mount_name',
                        eq='unix',limit=1)
        if self.Get('cl_action') == 'login' and not isMount(profilePath):
            raise VariableError(_("Remote profile not mounted"))
        return "on" if self.checkNeedSync(self.Get('ur_home_path'),profilePath,
                    self.Get('cl_client_sync_time'),
                    self.Get('cl_client_sync_status'),
                    self.Get('cl_client_profile_name')) else "off"

class VariableClClientSymlinks(ReadonlyVariable):
    """
    Список симлинков в пользовательском профиле
    """
    def get(self):
        skipFiles = (self.Get('cl_sync_del_path') +
                    self.Get('cl_sync_skip_path'))
        reSkip = re.compile("|".join(map(lambda x:x.replace("*",".*"),
            skipFiles))).search
        return filter(lambda x:not reSkip(x),
               find(self.Get('ur_home_path'),onefilesystem=True,
               filetype=FindFileType.SymbolicLink))

class VariableClClientNscdCache(Variable):
    """
    Частота обновления кэша nscd при работе в домене в часах
    """

class VariableClCifsVer(ReadonlyVariable):
    """
    Версия модуля CIFS
    """
    def get(self):
        return readFile("/sys/module/cifs/version")

class VariableClRsyncVer(ReadonlyVariable):
    """
    Версия rsync
    """
    def get(self):
        data = isPkgInstalled('net-misc/rsync')
        if data:
            return data[0]['PVR']
        return ""
