#!/usr/bin/env python

import stat, os, sys, pwd
import traceback
from errno import *
from stat import *
# fuse stuff
try:
    from fuse import Fuse
except:
    print "ERROR:", "cannot import fuse. Please make sure python-fuse is installed"
    print "Chelonia is not mounted"
    sys.exit(-1)
import fuse
import random

if not hasattr(fuse, '__version__'):
    raise RuntimeError, \
        "your fuse-py does not know fuse.__version__, probably it's too old."

fuse.fuse_python_api = (0,2)

# to avoid fuse to look in /lib on host fs
if os.environ.has_key('LD_LIBRARY_PATH'):
    LD_LIBRARY_PATH = os.environ['LD_LIBRARY_PATH']
    if LD_LIBRARY_PATH[0] == ':' or LD_LIBRARY_PATH[-1] == ':':
        print "LD_LIBRARY_PATH=%s ends and/or starts with :"%LD_LIBRARY_PATH
        print "Please fix your LD_LIBRARY_PATH!"

#fuse.feature_assert('stateful_files', 'has_init')

# arc storage stuff
try:
    import arc
except:
    print "ERROR:", "Cannot import arc. Please make sure ARC python bindings are installed"
    print "Chelonia is not mounted"
    sys.exit(-1)

from storage.client import BartenderClient
from storage.client import ISISClient
from storage.common import create_checksum
from arcom.service import false, true
from storage.common import upload_to_turl, download_from_turl

logfile = open("messagesfromarc",'w')
root_logger = arc.Logger_getRootLogger()
root_logger.addDestination(arc.LogStream(logfile))
root_logger.setThreshold(arc.ERROR)

import time

try:
    # MNT is not really used, but needs to be in sys.argv[1] for
    # initializing FUSE
    MNT = sys.argv[1]
except:
    raise RuntimeError, 'Usage: ./arcfs.py LOCAL_MOUNTPOINT'

verbose = False
print_xml = False
configfilename = ''
bartender_url_from_argument = ''
allowed_to_run_without_arc = False

user_config = None
ssl_config = {}
BartenderURL = ''
problem_with_userconfig = None
try:
    import arc
    user_config = arc.UserConfig(configfilename)
    key_file = user_config.KeyPath()
    cert_file = user_config.CertificatePath()
    proxy_file = user_config.ProxyPath()
    ca_file = user_config.CACertificatePath()
    ca_dir = user_config.CACertificatesDirectory()
    bartender_urls = [url.fullstr() for url in user_config.Bartender()]
    # the UserConfig currently does not support having ISIS services
    isis_urls = []
except:
    problem_with_userconfig = traceback.format_exc()
    key_file = None
    cert_file = None
    proxy_file = None
    ca_file = None
    ca_dir = None
    bartender_urls = []
    isis_urls = []

ssl_config = {}
key_file = os.environ.get('ARC_KEY_FILE', key_file)
cert_file = os.environ.get('ARC_CERT_FILE', cert_file)
proxy_file = os.environ.get('ARC_PROXY_FILE', proxy_file)
ca_file = os.environ.get('ARC_CA_FILE', ca_file)
ca_dir = os.environ.get('ARC_CA_DIR', ca_dir)
if proxy_file:
    ssl_config['proxy_file'] = proxy_file
    if verbose:
        print '- The proxy certificate file:', ssl_config['proxy_file']
else:
    if key_file and cert_file:
        ssl_config['key_file'] = key_file
        ssl_config['cert_file'] = cert_file
        if verbose:
            print '- The key file:', ssl_config['key_file']
            print '- The cert file:', ssl_config['cert_file']
if ca_file:
    ssl_config['ca_file'] = ca_file
    if verbose:
        print '- The CA file:', ssl_config['ca_file']
elif ca_dir:
    ssl_config['ca_dir'] = ca_dir
    if verbose:
        print '- The CA dir:', ssl_config['ca_dir']

no_bartender_message = "No Bartender URL found."

bartender_env_url = os.environ.get('ARC_BARTENDER_URL', '')
if bartender_url_from_argument:
    bartender_urls = [bartender_url_from_argument]
    if verbose:
        print '- Bartender URL specified as an argument, will use only this:', bartender_urls[0]
elif bartender_env_url:
    bartender_urls = [bartender_env_url]
    if verbose:
        print '- Bartender URL found in the ARC_BARTENDER_URL environment variable, will use only this:', bartender_urls[0]
if not bartender_urls:
    if verbose:
        print '- No Bartender URL found in the config or in the environment, try to get one from ISIS:'
    isis_env_url = os.environ.get('ARC_ISIS_URL', '')
    if isis_env_url:
        isis_urls = [isis_env_url]
        if verbose:
            print '  - ISIS URL found in the ARC_ISIS_URL environment variable:', isis_urls[0]
    if not isis_urls:
        if verbose:
            print '  - No ISIS URL found in the config or in the environment, giving up.'
        no_bartender_message = 'No Bartender URL and no ISIS URL found - cannot figure out where to connect.'
    else:
        bartender_found = False
        isis_connected = False
        while not bartender_found and len(isis_urls) > 0:
            isis_url = isis_urls.pop()
            isis = ISISClient(isis_url, print_xml, ssl_config = ssl_config)
            try:
                bartender_urls = isis.getServiceURLs('org.nordugrid.storage.bartender')
                isis_connected = True
                if not bartender_urls:
                    if verbose:
                        print '  -', isis_url, '- This ISIS did not return any Bartender URL.'
                else:
                    bartender_found = True
                    if verbose:
                        print '  -', isis_url, '- Got Bartender URL from ISIS:', ', '.join(bartender_urls)
            except:
                if verbose:
                    print '  -', isis_url, '- Failed to connect to ISIS.'
        if not bartender_found:
            if isis_connected:
                no_bartender_message = 'Cannot get Bartender URL from the ISIS. Try again later.'
            else:
                no_bartender_message = 'Cannot connect to any ISIS to get Bartender URL.'
if bartender_urls:
    if verbose:
        print '- The URL of the Bartender(s):', ', '.join(bartender_urls)        
else:
    if problem_with_userconfig and verbose:
        print '- There was a problem reading the user\'s config. Reason:', str(problem_with_userconfig)
    # bartender_urls = ['http://localhost:60000/Bartender']
    # print '- The URL of the Bartender is not given, using', bartender_urls[0]
    print 'ERROR:', no_bartender_message
    sys.exit(-1)
try:
    needed_replicas = os.environ['ARC_NEEDED_REPLICAS']
except:
    needed_replicas = 3

random.shuffle(bartender_urls)
bartender = None
for bartender_url in bartender_urls:
    try:
        bartender = BartenderClient(bartender_urls, False, ssl_config)
        break
    except:
        pass

if bartender == None:
    print 'ERROR:', "No valid bartender found"
    sys.exit(-1)

PROTOCOL = 'http'

FUSETRANSFER = os.path.join(os.getcwd(),'fuse_transfer')
if not os.path.isdir(FUSETRANSFER):
    os.mkdir(FUSETRANSFER)

VERBOSE = 0

debugfile = open('arcfsmessages','a')


def debug_msg(msg, msg2=''):
    """
    Function to get readable debug output to file
    """
    if VERBOSE:
        print >> debugfile, BartenderURL+":", msg, msg2
        debugfile.flush()

debug_msg(sys.argv)

def flag2mode(flags):
    """
    Function to get correct mode from flags for os.fdopen
    """
    md = {os.O_RDONLY: 'r', os.O_WRONLY: 'w', os.O_RDWR: 'w+'}
    m = md[flags & (os.O_RDONLY | os.O_WRONLY | os.O_RDWR)]

    if flags | os.O_APPEND:
        m = m.replace('w', 'a', 1)

    return m


def sym2oct(symmode, symlink=False):
    """
    Hackish function to get octal mode bits
    Example
    input: '-rw-r--r--'
    output: 0644
    """
    if len(symmode)!=10:
        print "bad symmode",symmode
    type=symmode[0]
    owner=symmode[1:4]
    grp=symmode[4:7]
    all=symmode[7:10]
    o=g=a=s=0
    if 'r' in owner: o+=4
    if 'w' in owner: o+=2
    if 'x' in owner: o+=1
    if 'S' in owner: s+=4
    if 's' in owner: s+=4;o+=1
    if 'r' in grp: g+=4
    if 'w' in grp: g+=2
    if 'x' in grp: g+=1
    if 'S' in grp: s+=4
    if 's' in grp: s+=4;g+=1
    if 'r' in all: a+=4
    if 'w' in all: a+=2
    if 'x' in all: a+=1
    if 'T' in all: s+=4
    if 't' in all: s+=4;a+=1
    f=0;sl=0
    if type=='d': f=4
    elif type=='c': f=2
    elif type=='l': f=2; sl=1
    else:
        sl=1
        if symlink:
            f=2
    return sl*8**5+f*8**4+s*8**3+o*8**2+g*8+a


class GenStat(fuse.Stat):
    """
    Default class for stat attributes
    """
    def __init__(self):
        self.st_mode  = 0
        self.st_ino   = 0
        self.st_dev   = 0
        self.st_nlink = 0
        self.st_uid   = 0
        self.st_gid   = 0
        self.st_size  = 0
        self.st_atime = 0
        self.st_mtime = 0
        self.st_ctime = 0


class ARCInode:
    """
    Class used to store ARCFS inode
    """

    def __init__(self, mode, nlink, uid, gid, size, mtime,
                 metadata, atime=0, ctime=0, closed='no', entries=[]):
        debug_msg("called ARCInode.__init__")
        self.st = GenStat()
        self.st.st_mode  = mode
        self.st.st_nlink = nlink
        self.st.st_uid   = uid
        self.st.st_gid   = gid
        self.st.st_size  = size
        self.st.st_mtime = mtime
        self.st.st_atime = atime
        self.st.st_ctime = ctime
        self.closed      = closed
        self.entries     = entries
        self.metadata    = metadata
        debug_msg("left ARCInode.__init__")


class ARCFS(Fuse):
    """
    Class for ARC FUSE file system. Interface to FUSE and ARC Storage System
    Usage:

    - instantiate

    - add options to `parser` attribute (an instance of `FuseOptParse`)

    - call `parse`

    - call `main`
    """

    def __init__(self, *args, **kw):
        """
        init Fuse, bartender is BartenderClient(BartenderURL, False)
        """
        Fuse.__init__(self, *args, **kw)
        debug_msg('fuse initiated')
        self.bartender = bartender
        debug_msg('left ARCFS.__init__')
        if sys.platform == 'darwin':
            #self.fuse_args.add("noappledouble", True)
            #self.fuse_args.add("noapplexattr", True)
            self.fuse_args.add("volname", "Chelonia")
            self.fuse_args.add("fsname", "ARCFS")


    def getattr(self, path):
        """
        get stat from path
        gets inode and returns inode.stat
        """
        debug_msg('called getattr',path)
        inod = self.getinode(path)
        if inod:
            return inod.st
        else: return -ENOENT


    def mkinod(self, path):
        """
        Function to get metadata of path from Bartender and parse it to ARCInode
        Returns ARCInode
        """
        debug_msg('mkinod called:', (path))
        request = {'0':path}
        metadata = self.bartender.stat(request)['0']
        if not isinstance(metadata,dict):
            # no metadata
            debug_msg('mkinod left, no metadata', (path))
            return None
        is_dir = metadata[('entry', 'type')] == 'collection' or metadata[('entry', 'type')] == 'mountpoint'
        is_file = metadata[('entry', 'type')] == 'file'
        if is_file:
            nlink = 1
            size = long(metadata[('states','size')])
            if len([status for (section, location), status in
                    metadata.items() if
                    section == 'locations' and status == 'alive']) == \
                    int(metadata[('states', 'neededReplicas')]):
                status = 'alive'
            else:
                status = 'not ready'
            # we don't know the mode so we set something
            mode = sym2oct('-rw-r--r--')
            closed = 'no'
            entries = []
        elif is_dir:
            # + 2 for . and ..

            nlink = len([guid for (section, file), guid in
                         metadata.items() if section == 'entries'])+2
            closed = metadata.get(('states', 'closed'),'no')
            mode = sym2oct('drwxr-xr-x',False)
            debug_msg("here")
            size = 0
            entries = [fname for (section, fname), guid in metadata.items()
                       if section == 'entries']
        else:
            debug_msg('mkinod left, nothing', (path))
            # neither file nor collection
            return None
        if metadata.has_key(('timestamps', 'created')):
            ctime = float(metadata[('timestamps', 'created')])
        else:
            ctime = 0
        if metadata.has_key(('timestamps', 'modified')):
            mtime = float(metadata[('timestamps', 'modified')])
        else:
            mtime = ctime
        atime = mtime
        # no way to get uid and gid in ARC storage (yet) so we use users uid
        uid = os.getuid()
        gid = os.getgid()
        debug_msg('mkinod left:', (path))

        return ARCInode(mode, nlink, uid, gid, size, mtime,
                        metadata, atime, ctime, closed, entries)


    def readdir(self, path, offset):
        """
        function yielding Direntries
        calls getinode once for directory, then uses entries list
        in dir inode
        """
        debug_msg('readdir called:', path)
        for r in '.','..':
            yield fuse.Direntry(r)
        dirnode = self.getinode(path)
        for fname in dirnode.entries:
            yield fuse.Direntry(fname)


    def mknod(self, path, mode, dev):
        """
        Function called if new resource will be created
        Check if mode is reasonable and if it already exists
        else put path in self.linked
        """
        debug_msg('mknod called:', (path, mode, dev))
        if not (S_ISREG(mode) | S_ISFIFO(mode) | S_ISSOCK(mode)):
            return -EINVAL
        if self.bartender.stat({'0':path})['0']:
            return -EEXIST
        debug_msg('mknod left', (path, mode, dev))


    def unlink(self, path):
        """
        Function called when removing file/link
        """
        debug_msg('unlink called:', path)
        response = self.bartender.delFile({'0':path})
        debug_msg('delFile responded', response['0'])
        debug_msg('unlink left:', path)


    def link(self, path, path1):
        debug_msg('link called:', (path, path1))
        request = {'0' : (path, path1, True)}
        response = self.bartender.move(request)
        debug_msg('link left', response)


    def rename(self, path, path1):
        debug_msg('rename called:', (path, path1))
        request = {'0' : (path, path1, False)}
        response = self.bartender.move(request)
        debug_msg('rename left', response)


    def mkdir(self, path, mode):
        debug_msg('mkdir called:', path)
        request = {'0': (path, {('states', 'closed') : 'no'})}
        response = self.bartender.makeCollection(request)
        debug_msg('mkdir left:', path)


    def rmdir(self, path):
        debug_msg('rmdir called:', path)
        request = {'0': path}
        status = self.bartender.unmakeCollection(request)['0']
        if status == 'collection is not empty':
            return -ENOTEMPTY
        debug_msg('rmdir left:', path)


    def getinode(self,path):
        debug_msg('getinode called:', path)
        try:
            inod = self.mkinod(path)
            return inod
        except:
            return None

    def statfs(self):
        """
        Cut'n'paste from fuse python example xmp.py:

        Should return an object with statvfs attributes (f_bsize, f_frsize...).
        Eg., the return value of os.statvfs() is such a thing (since py 2.2).
        If you are not reusing an existing statvfs object, start with
        fuse.StatVFS(), and define the attributes.

        To provide usable information (ie., you want sensible df(1)
        output, you are suggested to specify the following attributes:

            - f_bsize - preferred size of file blocks, in bytes
            - f_frsize - fundamental size of file blcoks, in bytes
                [if you have no idea, use the same as blocksize]
            - f_blocks - total number of blocks in the filesystem
            - f_bfree - number of free blocks
            - f_files - total number of file inodes
            - f_ffree - nunber of free file inodes
        """

        res = fuse.StatVfs()
        loc = os.statvfs(".")
        # currently we have no data of available disk space, so pretending 1TB
        available = 1024**4/loc.f_bsize
        res.f_bsize = loc.f_bsize
        res.f_frsize = res.f_bsize
        res.f_blocks = available
        res.f_bfree = available
        res.f_bavail = available
        # cannot have more inodes than the local system permits
        res.f_files = loc.f_files
        res.f_ffree = loc.f_ffree
        res.f_favail = loc.f_favail
        res.f_namemax = loc.f_namemax
        return res


    def fsinit(self):
        """
        Does whatever needed to get the FS up and running
        """
        debug_msg('fsinit called')
        # make sure we have root collection
        self.bartender.makeCollection({'0':('/', {('states', 'closed'):'no'})})
        debug_msg('fsinit left')


    class ARCFSFile(object):
        """
        Class taking care of file spesific stuff like write and read,
        lock and unlock (or rather __init__ and release)
        """

        def __init__(self, path, flags, *mode):
            """
            Initiate file type object
            asks bartender to get file, and if not found open a new file
            """
            debug_msg('Called ARCFSFile.__init__',(path,flags,mode))
            self.bartender = bartender
            self.transfer = FUSETRANSFER
            self.path = path
            self.tmp_path = os.path.join(self.transfer, arc.UUID())
            request = {'0' : (path, [PROTOCOL])}
            success, turl, protocol = self.bartender.getFile(request)['0']
            if success == 'not found':
                self.creating = True
                # to make sure the file is unique:
                self.file = os.fdopen(os.open(self.tmp_path, flags, *mode),
                                      flag2mode(flags))
                self.fd = self.file.fileno()
                # notify storage about file
                # set size and neededReplicas to 0, checksum to ''
                # correct values will be set in release
                metadata = {('states', 'size') : 0, ('states', 'checksum') : '',
                            ('states', 'checksumType') : 'md5', ('states', 'neededReplicas') : 0}
                request = {'0': (self.path, metadata, [PROTOCOL])}
                response = self.bartender.putFile(request)
                success, turl, protocol = response['0']
                debug_msg('__init__',response['0'])
            else:
                debug_msg('success', success)
                self.creating = False
                # read method needs read/write mode
                self.file = file(self.tmp_path,'wb+')
                self.fd = self.file.fileno()
            self.turl = turl
            self.success = success
            self.direct_io = True
            debug_msg('left ARCFSFile.__init__',success)


        def read(self, size, offset):
            """
            read file
            gets file from bartender. If file has no valid replica,
            we'll ask bartender again till replica is ready
            read will read entire file on first block,
            then write to local file, which is used for the rest of the
            life of this ARCFSFile object
            """
            debug_msg('read called:', (size, offset))

            if self.file.tell() > 0:
                # file is already downloaded
                self.file.seek(offset)
                debug_msg('read left fancy:', (size, offset))
                return self.file.read(size)

            success = self.success
            turl = self.turl
            request = {'0' : (self.path, [PROTOCOL])}
            while success == 'file has no valid replica':
                # try again
                time.sleep(0.1)
                success, turl, _ = self.bartender.getFile(request)['0']
            self.turl = turl
            self.success = success
            if success == 'is not a file':
                return -EISDIR
            if success != 'done':
                return -ENOENT
            download_from_turl(turl, PROTOCOL, self.file, ssl_config = ssl_config)
            self.file.seek(offset)

            return self.file.read(size)

        def write(self, buf, offset):
            """
            write to self.file
            note that write to storage is done only on call to release
            """
            debug_msg('write called:', (len(buf), offset))
            self.file.seek(offset)
            self.file.write(buf)
            debug_msg('write left:', (len(buf), offset))
            return len(buf)


        def release(self, flags):
            """
            release file and write to storage
            """
            debug_msg('release called:', (flags))
            size = self.file.tell()
            self.file.close()
            if self.creating:
                f = file(self.tmp_path,'rb')
                checksum = create_checksum(f, 'md5')
                f.close()
                # set size and checksum now that we have it
                request = {'size':(self.path, 'set', 'states', 'size', size),
                           'checksum':(self.path, 'set', 'states', 'checksum', checksum),
                           'replicas':(self.path, 'set', 'states', 'neededReplicas', needed_replicas)}
                modify_success = self.bartender.modify(request)
                if modify_success['size'] == 'set' and \
                   modify_success['replicas'] == 'set' and \
                   modify_success['checksum'] == 'set':
                    f = file(self.tmp_path,'rb')
                    parent = os.path.dirname(self.path)
                    child = os.path.basename(self.path)
                    GUID = self.bartender.list({'0':parent})['0'][0][child][0]
                    response = self.bartender.addReplica({'release': GUID}, [PROTOCOL])
                    success, turl, protocol = response['release']
                    if success == 'done':
                        upload_to_turl(turl, PROTOCOL, f, ssl_config = ssl_config)

                f.close()
            os.remove(self.tmp_path)
            debug_msg('release left:', (flags))


        # stolen from fuse-python xmp.py
        def _fflush(self):
            debug_msg('_fflush called')
            if 'w' in self.file.mode or 'a' in self.file.mode:
                self.file.flush()
            debug_msg('_fflush left')


        # stolen from fuse-python xmp.py
        def fsync(self, isfsyncfile):
            debug_msg('fsync called')
            self._fflush()
            if isfsyncfile and hasattr(os, 'fdatasync'):
                os.fdatasync(self.fd)
            else:
                os.fsync(self.fd)
            debug_msg('fsync left')


        # stolen from fuse-python xmp.py
        def flush(self):
            debug_msg('flush called')
            self._fflush()
            # cf. xmp_flush() in fusexmp_fh.c
            os.close(os.dup(self.fd))
            debug_msg('flush left')


        def fakeinod(self):
            """
            Function to fake inode before write is released
            """
            debug_msg('fakeinod called')
            mode = sym2oct('-rw-r--r--')
            nlink = 1
            uid = os.getuid()
            gid = os.getgid()
            size = 0
            mtime = atime = ctime = time.time()
            metadata = {}

            debug_msg('leaving fakeinod')
            return ARCInode(mode, nlink, uid, gid, size, mtime,
                            metadata, atime=atime, ctime=ctime)

        def mkfinod(self):
            """
            Function to get metadata from Bartender and parse it to ARCInode
            Returns ARCInode
            """
            debug_msg('mkfinod called')
            request = {'0':self.path}
            metadata = self.bartender.stat(request)['0']
            if not isinstance(metadata,dict):
                debug_msg('mkfinod left, no metadata')
                return None
            if not metadata[('entry', 'type')] == 'file':
                debug_msg('mkfinod left, not a file')
                return None
            nlink = 1
            size = long(metadata[('states','size')])
            mode = sym2oct('-rw-r--r--')
            closed = 'no'

            if metadata.has_key(('timestamps', 'created')):
                ctime = float(metadata[('timestamps', 'created')])
            else:
                ctime = 0
            if metadata.has_key(('timestamps', 'modified')):
                mtime = float(metadata[('timestamps', 'modified')])
            else:
                mtime = ctime
            atime = mtime
            # no way to get uid and gid in ARC storage (yet) so we use users uid
            uid = os.getuid()
            gid = os.getgid()

            debug_msg('mkfinod left')
            return ARCInode(mode, nlink, uid, gid, size, mtime, metadata,
                            atime, ctime)

        def fgetinode(self):
            """
            Functio to get file inode. If self.creating is true
            bartender does not know about this file yet ('cause it's not
            written to system yet), so we return a fake inode instead
            """
            debug_msg('fgetinode called')
            if self.creating:
                return self.fakeinod()
            try:
                return self.mkfinod()
            except:
                return None


        def fgetattr(self):
            """
            get file attributes
            """
            debug_msg('fgetattr called')
            inod = self.fgetinode()
            if inod:
                return inod.st
            else:
                return -ENOENT


    def main(self, *a, **kw):

        self.file_class = self.ARCFSFile

        return Fuse.main(self, *a, **kw)


def main():
    usage = """
    Userspace mount of ARC Storage.

    """ + Fuse.fusage

    server = ARCFS(version="%prog " + fuse.__version__,
                  usage=usage,
                  dash_s_do='setsingle')

    server.parser.add_option(mountopt="root", metavar="PATH",
                             default=BartenderURL,
                             help="mirror arc filesystem from under PATH [default: %default]")

    server.parse(values=server, errex=1)
    server.main()

if __name__ == '__main__':
    main()



debugfile.close()
