#!/usr/bin/python

# Copyright (c) 2010 Peter Palfrader
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.


# The farm_journal database table keeps a journal of which files got
# added to the farm recently.  This script outputs a tarball of the files
# referenced in the journal to stdout.

import errno
import optparse
import os
import pwd
import simplejson
import subprocess
import sys
import tempfile
import time
import urllib2
import yaml
sys.path.append(os.path.abspath(os.path.dirname(sys.argv[0]))+'/../lib')
from dbhelper import DBHelper

parser = optparse.OptionParser()
parser.set_usage("%prog [<options>] <sourcepackage>\n" +
          "Usage: %prog --apply=<REMOVAL_LOG_ID>\n" +
          "Usage: %prog --undo=<REMOVAL_LOG_ID>")
parser.add_option("-c", "--config", dest="conffile", metavar="CONFFILE",
  help="Config file location.")
parser.add_option("-u", "--url", dest="baseurl", metavar="BASEURL",
  help="Base URL to snapshot machine readable interface.")
parser.add_option("-a", "--apply", dest="apply_id", metavar="REMOVAL_LOG_ID",
  help="Re-apply chmods for removal log entry.")
parser.add_option("-U", "--undo", dest="undo_id", metavar="REMOVAL_LOG_ID",
  help="Undo chmods for removal log entry and remote entry.")

(options, args) = parser.parse_args()

if options.baseurl is None:
    options.baseurl = 'http://snapshot.debian.org/'
if options.conffile is None and 'SNAPSHOT_CONF' in os.environ:
    options.conffile = os.environ['SNAPSHOT_CONF']

if options.conffile is None:
    sys.stderr.write("No --config option is given and environment variable SNAPSHOT_CONF is not set.\n")
    parser.print_help()
    sys.exit(1)
numactions = 0
if len(args) == 1:
    numactions += 1
if not options.apply_id is None:
    numactions += 1
if not options.undo_id is None:
    numactions += 1
if not numactions == 1:
    parser.print_help()
    sys.exit(1)

config = yaml.load(open(options.conffile).read())
db = DBHelper(config['db']['connectstring'])

def make_path(digest):
    prefix1 = digest[0:2]
    prefix2 = digest[2:4]
    return os.path.join(config['snapshot']['farmpath'], prefix1, prefix2, digest)

def get_versions(package):
    url = options.baseurl + 'mr/package/' + urllib2.quote(package) +'/'
    f = urllib2.urlopen(url)
    s = simplejson.load(f)
    versions = map(lambda l: l['version'], s['result'])
    return versions

def get_files(package, version):
    url = options.baseurl + 'mr/package/' + urllib2.quote(package) +'/' + urllib2.quote(version) +'/allfiles?fileinfo=1'
    try:
        f = urllib2.urlopen(url)
    except urllib2.HTTPError, e:
        sys.stderr.write("Could not fetch %s: %s\n"%(url, e))
        sys.exit(1)
    s = simplejson.load(f)
    return s

def extract_info(h, json, seen):
    info = None
    try:
        info = json['fileinfo'][h][0]['name']
    except (IndexError, KeyError):
        info = '(unknown)'

    result = None
    if h in seen:
        result = '# %s (dup) %s'%(h, info)
    else:
        result = '%s %s'%(h, info)
        seen[h] = True
    return result

def prepare_editbuffer(package):
    versions = get_versions(package)
    pwinfo = pwd.getpwuid(os.getuid())
    if 'SUDO_USER' in os.environ:
        pwinfo = pwd.getpwnam(os.environ['SUDO_USER'])
    editbuffer = [ '%s - unredistributable'%(package),
                   '',
                   '',
                   ' -- %s (%s) on host %s at %s'%(
                        pwinfo[4].rstrip(','),
                        pwinfo[0],
                        open('/etc/hostname').read().rstrip(),
                        time.strftime("%a, %d %b %Y %H:%M:%S +0000", time.gmtime()),
                     ),
                   '# Please give a reason for removing this package.',
                   '# (Comment lines starting with "#" will be ignored.)',
                   '',
                   '=files=',
                   '# Comment out any files you do not wish to remove/make unreadable:',
                   ]
    seen = {}
    for v in versions:
        editbuffer.append('')
        editbuffer.append("# %s (%s)"%(package,v))
        s = get_files(package, v)
        for sourcefile in map(lambda x: x['hash'], s['result']['source']):
            editbuffer.append(extract_info(sourcefile, s, seen))
        for b in s['result']['binaries']:
            editbuffer.append('# binary %s %s'%(b['name'], b['version']))
            for f in b['files']:
                editbuffer.append(extract_info(f['hash'], s, seen))

    return editbuffer

def edit_buffer(buf):
    tmp = tempfile.NamedTemporaryFile()
    for l in buf:
        tmp.write(l)
        tmp.write("\n")
    tmp.flush()

    try:
        retcode = subprocess.call(['sensible-editor', tmp.name])
        if retcode < 0:
            sys.stderr.write("Editor was terminated by signal %d.\n"%(-retcode))
            sys.exit(1)
        elif retcode != 0:
            sys.stderr.write("Editor returned non-zero exit code %d.\n"%(retcode))
            sys.exit(1)
    except OSError, e:
        sys.stderr.write("Failed to exec editor (%s).\n"%(e))
        sys.exit(1)

    tmp.seek(0)
    buf = []
    for l in tmp:
        l = l.rstrip()
        buf.append(l)
    tmp.close()
    return buf

def parse_editbuffer(buf):
    msg = []
    files = []
    in_files = False
    for l in buf:
        if l.startswith('#'): continue

        if l == '=files=':
            in_files = True
            continue
        if in_files:
            if l == '':
                continue
            x = l.split(' ', 1)
            x = x[0]
            if len(x) != 40:
                sys.stderr.write("Cannot parse line '%s'.\n"%(l))
                sys.exit(1)
            files.append(x)
        else:
            msg.append(l)

    if not in_files or len(files) == 0:
        sys.stderr.write("No files mentioned?\n")
        sys.exit(1)

    while len(msg) > 0 and msg[-1] == '':
        msg.pop()

    if len(msg) == 0:
        sys.stderr.write("No reason given.\n")
        sys.exit(1)

    return (msg, files)

def insert_into_db(db, msg, files):
    c = db.execute('BEGIN')
    c.close()

    row = db.query_one('INSERT INTO removal_log (reason) VALUES (%(reason)s) RETURNING removal_log_id',
                       {'reason': "\n".join(msg)+"\n"})
    id = row['removal_log_id']
    for h in files:
        c = db.execute('INSERT INTO removal_affects (removal_log_id, hash) VALUES (%(removal_log_id)s, %(hash)s)',
                       {'removal_log_id': id,
                        'hash': h})
        c.close()
    c = db.execute('COMMIT')
    c.close()

    return id

def fschange(db, id, mode=0400):
    hashes = db.query_firsts('SELECT hash FROM removal_affects WHERE removal_log_id=%(removal_log_id)s', {'removal_log_id': id})
    for h in hashes:
        p = make_path(h)
        try:
            os.chmod(p, mode)
        except OSError, ex:
            if ex.errno in [errno.ENOENT]:
                sys.stderr.write("Warning: File does not exist: %s\n"%(p))
                continue
            else:
                raise

def remove_from_db(db, id):
    c = db.execute('BEGIN')
    c.close()
    c = db.execute('DELETE FROM removal_affects WHERE removal_log_id=%(removal_log_id)s', {'removal_log_id': id})
    c.close()
    c = db.execute('DELETE FROM removal_log WHERE removal_log_id=%(removal_log_id)s', {'removal_log_id': id})
    c.close()
    c = db.execute('COMMIT')
    c.close()



if not options.undo_id is None:
    fschange(db, options.undo_id, 0644)
    remove_from_db(db, options.undo_id)
    sys.exit(0)

if options.apply_id is None:
    package = args[0]
    buf = prepare_editbuffer(package)
    newbuf = edit_buffer(buf)
    if buf == newbuf:
        sys.stdout.write("Edit buffer not changed.  Proceed anyway? [y/N] ")
        sys.stdout.flush()
        line = sys.stdin.readline().strip()
        if not line == "y":
            sys.exit(0)

    msg, files = parse_editbuffer(newbuf)
    id = insert_into_db(db, msg, files)
    print "Committed to database, applying changes to filesystem.  If this fails"
    print "for some reason please fix what is necessary then rerun this script"
    print "with just --apply %d"%(id)
    options.apply_id = id

fschange(db, options.apply_id)

# vim:set et:
# vim:set ts=4:
# vim:set shiftwidth=4:
