# vim: tabstop=4 shiftwidth=4 softtabstop=4

# Copyright 2021 Lenovo
#
# 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 glob
import os
import shutil
import tempfile
import confluent.sshutil as sshutil
import confluent.util as util
import confluent.noderange as noderange
import eventlet
import pwd
import grp

def mkdirp(path):
    try:
        os.makedirs(path)
    except OSError as e:
        if e.errno != 17:
            raise

def get_entries(filename):
    secname = 'REPLACE:'
    filename = filename.strip()
    if filename[-1] == '>':
        filename = filename[:-1]
    with open(filename, 'r') as slfile:
        slist = slfile.read()
    entries = slist.split('\n')
    for ent in entries:
        ent = ent.split('#', 1)[0].strip()
        if not ent:
            continue
        if ent in ('APPENDONCE:', 'MERGE:', 'REPLACE:'):
            secname = ent
        if ent[0] == '<':
            subfilename = ent[1:]
            if subfilename[-1] == '>':
                subfilename = subfilename[:-1]
            if subfilename[0] != '/':
                subfilename = os.path.join(os.path.dirname(filename), subfilename)
            for subent in get_entries(subfilename):
                yield subent
            yield secname
        else:
            yield ent

class SyncList(object):
    def __init__(self, filename, nodename, cfg):
        slist = None
        self.replacemap = {}
        self.appendmap = {}
        self.appendoncemap = {}
        self.mergemap = {}
        self.optmap = {}
        entries = get_entries(filename)
        currmap = self.replacemap
        for ent in entries:
            try:
                cmtidx = ent.index('#')
                ent = ent[:cmtidx]
            except ValueError:
                pass
            for special in '$%^&|{}':
                if special in ent:
                    raise Exception(
                        'Special character "{}" reserved for future use'.format(special))
            ent = ent.strip()
            if not ent:
                continue
            if ent[-1] == ':':
                if ent == 'MERGE:':
                    currmap = self.mergemap
                elif ent == 'APPENDONCE:':
                    currmap = self.appendoncemap
                elif ent == 'REPLACE:':
                    currmap = self.replacemap
                else:
                    raise Exception(
                        'Section "{}" is not currently supported in syncfiles'.format(ent[:-1]))
                continue
            if '->' in ent:
                k, v = ent.split('->')
                k = k.strip()
                v = v.strip()
                if ':' in v:
                    nr, v = v.split(':', 1)
                    for candidate in noderange.NodeRange(nr, cfg).nodes:
                        if candidate == nodename:
                            break
                    else:
                        continue
                optparts = v.split()
                v = optparts[0]
                optparts = optparts[1:]
            else:
                kparts = []
                optparts = []
                currparts = kparts
                for part in ent.split():
                    if part[0] == '(':
                        currparts = optparts
                    currparts.append(part)
                k = ' '.join(kparts)
                v = None
            entopts = {}
            if optparts:
                if optparts[0][0] != '(' or optparts[-1][-1] != ')':
                    raise Exception("Unsupported syntax in syncfile: " + ent)
                opts = ','.join(optparts)
                opts = opts[1:-1]
                for opt in opts.split(','):
                    optname, optval = opt.split('=')
                    if optname == 'owner':
                        try:
                            uid = pwd.getpwnam(optval).pw_uid
                        except KeyError:
                            uid = None
                        optval = {'name': optval, 'id': uid}
                    elif optname == 'group':
                        try:
                            gid = grp.getgrnam(optval).gr_gid
                        except KeyError:
                            gid = None
                        optval = {'name': optval, 'id': gid}
                    entopts[optname] = optval
            currmap[k] = v
            targ = v if v else k
            for f in targ.split():
                self.optmap[f] = entopts


def sync_list_to_node(sl, node, suffixes):
    targdir = tempfile.mkdtemp('.syncto{}'.format(node))
    output = ''
    try:
        for ent in sl.replacemap:
            stage_ent(sl.replacemap, ent, targdir)
        if 'append' in suffixes:
            while suffixes['append'] and suffixes['append'][0] == '/':
                suffixes['append'] = suffixes['append'][1:]
            for ent in sl.appendmap:
                stage_ent(sl.appendmap, ent,
                          os.path.join(targdir, suffixes['append']))
        if 'merge' in suffixes:
            while suffixes['merge'] and suffixes['merge'][0] == '/':
                suffixes['merge'] = suffixes['merge'][1:]
            for ent in sl.mergemap:
                stage_ent(sl.mergemap, ent,
                          os.path.join(targdir, suffixes['merge']), True)
        if 'appendonce' in suffixes:
            while suffixes['appendonce'] and suffixes['appendonce'][0] == '/':
                suffixes['appendonce'] = suffixes['appendonce'][1:]
            for ent in sl.appendoncemap:
                stage_ent(sl.appendoncemap, ent,
                          os.path.join(targdir, suffixes['appendonce']), True)
        sshutil.prep_ssh_key('/etc/confluent/ssh/automation')
        output = util.run(
            ['rsync', '-rvLD', targdir + '/', 'root@{}:/'.format(node)])[0]
    except Exception as e:
        if 'CalledProcessError' not in repr(e):
            # https://github.com/eventlet/eventlet/issues/413
            # for some reason, can't catch the calledprocesserror normally
            # for this exception, implement a hack workaround
            raise
        unreadablefiles = []
        for root, dirnames, filenames in os.walk(targdir):
            for filename in filenames:
                filename = os.path.join(root, filename)
                try:
                    with open(filename, 'r') as _:
                        pass
                except OSError as e:
                    unreadablefiles.append(filename.replace(targdir, ''))
        if unreadablefiles:
            raise Exception("Syncing failed due to unreadable files: " + ','.join(unreadablefiles))
        else:
            raise
    finally:
        shutil.rmtree(targdir)
    if not isinstance(output, str):
        output = output.decode('utf8')
    retval = {
        'options': sl.optmap,
        'output': output,
    }
    return retval # need dictionary with output and options

def stage_ent(currmap, ent, targdir, appendexist=False):
    dst = currmap[ent]
    everyfent = []
    allfents = ent.split()
    for tmpent in allfents:
        fents = glob.glob(tmpent)
        everyfent.extend(fents)
    if not everyfent:
        raise Exception('No matching files for "{}"'.format(ent))
    if dst is None:  # this is to indicate source and destination as one
        dst = os.path.dirname(everyfent[0]) + '/'
    while dst and dst[0] == '/':
        dst = dst[1:]
    if len(everyfent) > 1 and dst[-1] != '/':
        raise Exception(
            'Multiple files match {}, {} needs a trailing slash to indicate a directory'.format(ent, dst))
    fulltarg = os.path.join(targdir, dst)
    for targ in everyfent:
        mkpathorlink(targ, fulltarg, appendexist)

def mkpathorlink(source, destination, appendexist=False):
    if os.path.isdir(source):
        mkdirp(destination)
        for ent in os.listdir(source):
            currsrc = os.path.join(source, ent)
            currdst = os.path.join(destination, ent)
            mkpathorlink(currsrc, currdst)
    else:
        if destination[-1] == '/':
            mkdirp(destination)
            destination = os.path.join(destination, os.path.basename(source))
        else:
            mkdirp(os.path.dirname(destination))
        if appendexist and os.path.exists(destination):
            tmpnam = tempfile.mktemp()
            shutil.copy(destination, tmpnam)
            os.remove(destination)
            with open(destination, 'w') as realdest:
                with open(tmpnam) as olddest:
                    realdest.write(olddest.read())
                with open(source) as sourcedata:
                    realdest.write(sourcedata.read())
            os.remove(tmpnam)
        else:
            os.symlink(source, destination)


syncrunners = {}


def start_syncfiles(nodename, cfg, suffixes):
    deployinfo = cfg.get_node_attributes(
        nodename, ('deployment.*',))
    deployinfo = deployinfo.get(nodename, {})
    profile = deployinfo.get(
        'deployment.pendingprofile', {}).get('value', '')
    if not profile:
        profile = deployinfo.get(
        'deployment.stagedprofile', {}).get('value', '')
    if not profile:
        profile = deployinfo.get(
        'deployment.profile', {}).get('value', '')
    if not profile:
        raise Exception('Cannot perform syncfiles without profile assigned')
    synclist = '/var/lib/confluent/public/os/{}/syncfiles'.format(profile)
    if not os.path.exists(synclist):
        return '200 OK'  # not running
    sl = SyncList(synclist, nodename, cfg)
    if not (sl.appendmap or sl.mergemap or sl.replacemap or sl.appendoncemap):
        return '200 OK'  # the synclist has no actual entries
    syncrunners[nodename] = eventlet.spawn(
        sync_list_to_node, sl, nodename, suffixes)
    return '202 Queued' # backgrounded

def get_syncresult(nodename):
    if nodename not in syncrunners:
        return ('204 Not Running', '')
    if not syncrunners[nodename].dead:
        return ('200 OK', '')
    result = syncrunners[nodename].wait()
    del syncrunners[nodename]
    return ('200 OK', result)

[((218, 15, 218, 41), 'os.path.join', 'os.path.join', ({(218, 28, 218, 35): 'targdir', (218, 37, 218, 40): 'dst'}, {}), '(targdir, dst)', False, 'import os\n'), ((223, 7, 223, 28), 'os.path.isdir', 'os.path.isdir', ({(223, 21, 223, 27): 'source'}, {}), '(source)', False, 'import os\n'), ((272, 28, 273, 50), 'eventlet.spawn', 'eventlet.spawn', ({(273, 8, 273, 25): 'sync_list_to_node', (273, 27, 273, 29): 'sl', (273, 31, 273, 39): 'nodename', (273, 41, 273, 49): 'suffixes'}, {}), '(sync_list_to_node, sl, nodename, suffixes)', False, 'import eventlet\n'), ((30, 8, 30, 25), 'os.makedirs', 'os.makedirs', ({(30, 20, 30, 24): 'path'}, {}), '(path)', False, 'import os\n'), ((170, 8, 170, 61), 'confluent.sshutil.prep_ssh_key', 'sshutil.prep_ssh_key', ({(170, 29, 170, 60): '"""/etc/confluent/ssh/automation"""'}, {}), "('/etc/confluent/ssh/automation')", True, 'import confluent.sshutil as sshutil\n'), ((193, 8, 193, 30), 'shutil.rmtree', 'shutil.rmtree', ({(193, 22, 193, 29): 'targdir'}, {}), '(targdir)', False, 'import shutil\n'), ((207, 16, 207, 33), 'glob.glob', 'glob.glob', ({(207, 26, 207, 32): 'tmpent'}, {}), '(tmpent)', False, 'import glob\n'), ((225, 19, 225, 37), 'os.listdir', 'os.listdir', ({(225, 30, 225, 36): 'source'}, {}), '(source)', False, 'import os\n'), ((267, 11, 267, 35), 'os.path.exists', 'os.path.exists', ({(267, 26, 267, 34): 'synclist'}, {}), '(synclist)', False, 'import os\n'), ((180, 41, 180, 57), 'os.walk', 'os.walk', ({(180, 49, 180, 56): 'targdir'}, {}), '(targdir)', False, 'import os\n'), ((212, 14, 212, 43), 'os.path.dirname', 'os.path.dirname', ({(212, 30, 212, 42): 'everyfent[0]'}, {}), '(everyfent[0])', False, 'import os\n'), ((226, 22, 226, 47), 'os.path.join', 'os.path.join', ({(226, 35, 226, 41): 'source', (226, 43, 226, 46): 'ent'}, {}), '(source, ent)', False, 'import os\n'), ((227, 22, 227, 52), 'os.path.join', 'os.path.join', ({(227, 35, 227, 46): 'destination', (227, 48, 227, 51): 'ent'}, {}), '(destination, ent)', False, 'import os\n'), ((235, 27, 235, 54), 'os.path.exists', 'os.path.exists', ({(235, 42, 235, 53): 'destination'}, {}), '(destination)', False, 'import os\n'), ((236, 21, 236, 38), 'tempfile.mktemp', 'tempfile.mktemp', ({}, {}), '()', False, 'import tempfile\n'), ((237, 12, 237, 44), 'shutil.copy', 'shutil.copy', ({(237, 24, 237, 35): 'destination', (237, 37, 237, 43): 'tmpnam'}, {}), '(destination, tmpnam)', False, 'import shutil\n'), ((238, 12, 238, 34), 'os.remove', 'os.remove', ({(238, 22, 238, 33): 'destination'}, {}), '(destination)', False, 'import os\n'), ((244, 12, 244, 29), 'os.remove', 'os.remove', ({(244, 22, 244, 28): 'tmpnam'}, {}), '(tmpnam)', False, 'import os\n'), ((246, 12, 246, 43), 'os.symlink', 'os.symlink', ({(246, 23, 246, 29): 'source', (246, 31, 246, 42): 'destination'}, {}), '(source, destination)', False, 'import os\n'), ((232, 52, 232, 76), 'os.path.basename', 'os.path.basename', ({(232, 69, 232, 75): 'source'}, {}), '(source)', False, 'import os\n'), ((234, 19, 234, 47), 'os.path.dirname', 'os.path.dirname', ({(234, 35, 234, 46): 'destination'}, {}), '(destination)', False, 'import os\n'), ((54, 43, 54, 68), 'os.path.dirname', 'os.path.dirname', ({(54, 59, 54, 67): 'filename'}, {}), '(filename)', False, 'import os\n'), ((157, 26, 157, 67), 'os.path.join', 'os.path.join', ({(157, 39, 157, 46): 'targdir', (157, 48, 157, 66): "suffixes['append']"}, {}), "(targdir, suffixes['append'])", False, 'import os\n'), ((163, 26, 163, 66), 'os.path.join', 'os.path.join', ({(163, 39, 163, 46): 'targdir', (163, 48, 163, 65): "suffixes['merge']"}, {}), "(targdir, suffixes['merge'])", False, 'import os\n'), ((169, 26, 169, 71), 'os.path.join', 'os.path.join', ({(169, 39, 169, 46): 'targdir', (169, 48, 169, 70): "suffixes['appendonce']"}, {}), "(targdir, suffixes['appendonce'])", False, 'import os\n'), ((182, 27, 182, 55), 'os.path.join', 'os.path.join', ({(182, 40, 182, 44): 'root', (182, 46, 182, 54): 'filename'}, {}), '(root, filename)', False, 'import os\n'), ((101, 37, 101, 65), 'confluent.noderange.NodeRange', 'noderange.NodeRange', ({(101, 57, 101, 59): 'nr', (101, 61, 101, 64): 'cfg'}, {}), '(nr, cfg)', True, 'import confluent.noderange as noderange\n'), ((129, 34, 129, 54), 'pwd.getpwnam', 'pwd.getpwnam', ({(129, 47, 129, 53): 'optval'}, {}), '(optval)', False, 'import pwd\n'), ((135, 34, 135, 54), 'grp.getgrnam', 'grp.getgrnam', ({(135, 47, 135, 53): 'optval'}, {}), '(optval)', False, 'import grp\n')]