#!/usr/bin/env python2

"""
    workspace++

    ws_expirer

    python version of ws_expirer command, only for root

    to be called from a cronjob to expire workspaces, does delete the data as well
    Reads new YAML configuration files and new YAML workspace database.

    (c) Holger Berger 2013, 2014, 2015, 2016, 2017, 2018, 2019
    (c) Bernd Krischok 2017

    workspace++ is based on workspace by Holger Berger, Thomas Beisel and Martin Hecht
    and Adrian Reber

    workspace++ is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    workspace++ is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with workspace++.  If not, see <http://www.gnu.org/licenses/>.

"""

import os,sys
import glob
import time
import smtplib
import os.path
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart

# read a single line from ws.conf of the form: pythonpath: /path/to/python
def read_python_conf():
    for l in open("/etc/ws.conf","r"):
        if 'pythonpath' in l:
            key=l.split(":")[0].strip()
            value=l.split(":")[1].strip()
            if key == 'pythonpath':
                if os.path.isdir(value):
                    sys.path.append(value)
                    break
                else:
                    print >> sys.stderr, "Warning: Invalid pythonpath in ws.conf"

read_python_conf()
import yaml


count = 0

# send a reminder email
def send_reminder(smtphost, clustername, wsname, expiration, mailaddress):
    text = """ 
        Your workspace %s on system %s will expire at %s.
    """ % (wsname, clustername, time.ctime(expiration))
    msg = MIMEMultipart('mixed')
    recipients = [ mailaddress ]
    try:
        sender = config[mail_from]
    except:
        sender = "wsadmin" 
    msg['From'] = sender
    msg['To'] = mailaddress
    msg['Subject'] = 'Workspace %s will expire at %s' % (wsname, time.ctime(expiration))
    msg.preamble = text

    msg.attach(MIMEText(text,'html'))

#    now=time.strftime('%Y%m%dT%H%M%S', time.localtime())
#    then=time.strftime('%Y%m%dT%H%M%S', time.localtime(expiration))
#    ical="""
#BEGIN:VCALENDAR
#PRODID:-//HLRS Cluster Team//Workspace V2.1//EN
#VERSION:2.0
#BEGIN:VEVENT
#UID:%d
#DESCRIPTION:Workspace %s will be deleted on system %s
#DTEND:%s
#DTSTAMP:%s
#DTSTART:%s
#SUMMARY:Workspace %s expires
#CLASS:PRIVATE
#END:VEVENT
#END:VCALENDAR
#    """ % (time.time(), wsname, clustername,then, now, then, wsname)
#
#    att = MIMEText(ical,"calendar")
#    msg.attach(att)

    try:
        s = smtplib.SMTP(host=smtphost)
        s.sendmail(sender, recipients, msg.as_string())
        s.quit()
    except:  # something went wrong 
        print "could not send reminder email, may be invalid address?", recipients

# slow recursive deleter, to avoid high meta data pressure on servers
def deldir(dir):
    global count
    print "   deldir",dir
    if not os.path.exists(dir): return
    for obj in os.listdir(dir):
        fullname=os.path.join(dir,obj)
        if os.path.isdir(fullname) and not os.path.islink(fullname):
            deldir(fullname)
            time.sleep(0.01)
            os.rmdir(fullname)
            time.sleep(0.01)
        else:
            if os.path.isfile(fullname):
                size=os.path.getsize(fullname)
            else:
                size=0.001
            os.unlink(fullname)
            time.sleep(max(0.001,size*0.00000000001))
            count+=1
            if count%10==0:
                time.sleep(0.05)
            if count%100==0:
                time.sleep(0.1)
                count=0

# getting old workspace database informations (path and expiration date)
def get_old_db_entry_informations(dbfile):
    D={}
    f = open(dbfile,"r")
    d = f.readlines()
    D={'expiration': d[0].split('\n')[0], 'workspace': d[1].split('\n')[0]}
    f.close()
    return D

# collect all the workspace paths of all db entries
def get_dbentriesws(dbfullpathlist):
    W=[]
    for dbentryfilename in dbfullpathlist:
       try:
          dbentry = yaml.load(open(dbentryfilename))
          workspace = dbentry['workspace']
       except:
          dbentry = get_old_db_entry_informations(dbentryfilename)
          workspace = dbentry['workspace']
       W.append(workspace)
    return W

# Options Parsing ...
def vararg_callback(option, opt_str, value, parser):
    assert value is None
    done = 0
    value = []
    rargs = parser.rargs
    while rargs:
        arg = rargs[0]
        # Stop if we hit an arg like "--foo", "-a", "-fx", "--file=f",
        # etc.  Note that this also stops on "-3" or "-3.0", so if
        # your option takes numeric values, you will need to handle
        # this.
        if ((arg[:2] == "--" and len(arg) > 2) or
            (arg[:1] == "-" and len(arg) > 1 and arg[1] != "-")):
            break
        else:
            value.append(arg)
            del rargs[0]
    setattr(parser.values, option.dest, value)

def processOpts(): 
    import optparse

    parser = optparse.OptionParser()
    parser.add_option("-w", "--workspaces", action="callback", dest="fslist",
                          callback=vararg_callback,
                          help="list of workspace filesystems")
    parser.add_option("-c", "--cleaner", dest="cleaner", action='store_const', const=1,
                          help="dryrun or really clean (default is dryrun)")
    (options, args) = parser.parse_args()
    if not options:
       print "*** FATAL: No options defined. ***"
       sys.exit(1)

    return options



if os.getuid()!=0:
    print >> sys.stderr, "Error: you are not root."
    sys.exit(-1)

# load config file
config = yaml.load(open('/etc/ws.conf'))


smtphost = config['smtphost']
clustername = config['clustername']

start = time.time()

print "start of expirer run", time.ctime()

dryrun = True


# Get the command options
opts={}
opts=processOpts()
fslist=[]
if not opts.fslist:
   for fs in config["workspaces"]:
       fslist.append(fs)
else:
   fslist=opts.fslist

if fslist == []:
   print "Error: no workspace defined"
   sys.exit(2)

if not opts.cleaner:
   dryrun = True
else:
   dryrun = False

if not dryrun:
   print "really cleaning ..."
else:
   print "simulate cleaning ... (dryrun)"


#dryrun = True

# cleanup stray directories, this removes stuff that was released (no DB entry any more)
# from spaces, and checks if anything is left over in removed state for whatever reasons
for fs in fslist:
        # first for visible workspaces
        dbdir = config["workspaces"][fs]["database"]
        spaces = config["workspaces"][fs]["spaces"]	
        dbentries = glob.glob(os.path.join(dbdir,"*-*"))
        dbentrynames = map(os.path.basename, dbentries)
        dbentriesws=get_dbentriesws(dbentries)
        dbentryworkspaces=map(os.path.basename, dbentriesws)
        workspacedelprefix = config["workspaces"][fs]["deleted"]
        print " checking for stray workspaces for", fs, dbdir, spaces
        for space in spaces:
                for ws in glob.glob(os.path.join(space,"*-*")):
                        #if os.path.basename(ws) not in dbentrynames:
                        if os.path.basename(ws) not in dbentryworkspaces:
                                print "  stray workspace", ws
                                # FIXME delete it
                                # FIXME this could fail on scatefs, should fallback to 'mv'
                                timestamp=str(int(time.time()))
                                if not dryrun:
                                    os.rename(ws, os.path.join(os.path.dirname(ws), workspacedelprefix,os.path.basename(ws)+"-"+timestamp))
                                    print "  OS.RENAME", ws, os.path.join(os.path.dirname(ws), workspacedelprefix,os.path.basename(ws)+"-"+timestamp)
                                else:
                                    print "  MV" , ws, os.path.join(os.path.dirname(ws), workspacedelprefix,os.path.basename(ws)+"-"+timestamp)
                        else:
                                print "  valid workspace", ws

        # second for removed workspaces
        dbdelentries = glob.glob(os.path.join(dbdir,config["workspaces"][fs]["deleted"],"*-*"))
        dbdelentrynames = map(os.path.basename, dbdelentries)
        for space in spaces:
                for ws in glob.glob(os.path.join(space,config["workspaces"][fs]["deleted"],"*-*")):	
                        if os.path.basename(ws) not in dbdelentrynames:
                                print "  stray removed workspace", ws
                                if not dryrun:
                                    deldir(os.path.join(os.path.dirname(ws), workspacedelprefix, os.path.basename(ws)))
                                    print "  DELDIR", os.path.join(os.path.dirname(ws), workspacedelprefix, os.path.basename(ws))
                                else:
                                    print "  RM", os.path.join(os.path.dirname(ws), workspacedelprefix, os.path.basename(ws))
                        else:
                                print "  valid removed workspace", ws


# expire the workspaces by moving them into deleted spaces, dbentry + workspace itself
# this searches over db
for fs in fslist:
    spaces = config["workspaces"][fs]["spaces"]
    dbdeldir = os.path.join(config["workspaces"][fs]["database"], config["workspaces"][fs]["deleted"])
    workspacedelprefix = config["workspaces"][fs]["deleted"]
    dbdir = config["workspaces"][fs]["database"]
    print " checking for workspaces to be expired for", fs, dbdir, spaces
    for dbentryfilename in glob.glob(os.path.join(dbdir,"*-*")):
        reminder = 0
        mailaddress = ""
        workspace = ""
        expiration = 0
        try:
           dbentry = yaml.load(open(dbentryfilename))
           reminder = int(dbentry['reminder'])
           mailaddress = dbentry['mailaddress']
        except:
           dbentry = get_old_db_entry_informations(dbentryfilename)
           reminder = 0
           mailaddress = ""
        expiration = int(dbentry['expiration'])
        workspace = dbentry['workspace']
        if workspace == "" or expiration == 0:
            print "  FAILED", dbentryfilename
            continue
        if time.time() > expiration:
            print "  expiring", dbentryfilename
            timestamp=str(int(time.time()))
            if not dryrun:
                os.rename(dbentryfilename, os.path.join(dbdeldir, os.path.basename(dbentryfilename))+"-"+timestamp)
                print "  OS.RENAME", dbentryfilename, os.path.join(dbdeldir, os.path.basename(dbentryfilename))+"-"+timestamp
            else:
                print "  MV", dbentryfilename, os.path.join(dbdeldir, os.path.basename(dbentryfilename))+"-"+timestamp

            # FIXME this could fail on scatefs, should fallback to 'mv'
            if not dryrun:
                try:
                    os.rename(workspace, os.path.join(os.path.dirname(workspace),
                                    workspacedelprefix,os.path.basename(dbentryfilename)+"-"+timestamp))
                    print "  OS.RENAME", workspace, os.path.join(os.path.dirname(workspace), workspacedelprefix,os.path.basename(dbentryfilename)+"-"+timestamp)
                except:  
                    print "  OS.RENAME FAILED", workspace, os.path.join(os.path.dirname(workspace), workspacedelprefix,os.path.basename(dbentryfilename)+"-"+timestamp)
            else:
                print "  MV", workspace, os.path.join(os.path.dirname(workspace), workspacedelprefix,os.path.basename(dbentryfilename)+"-"+timestamp)

        else:
            print "  keeping", dbentryfilename
            if time.time() > (expiration - (reminder*(24*3600))):
                #print "  mail needed"
                swsname = os.path.basename(dbentryfilename)[os.path.basename(dbentryfilename).find('-')+1:]
                if not dryrun:
                    if mailaddress != "":
                       send_reminder(smtphost, clustername, swsname, expiration, mailaddress)
                       print "  SEND_REMINDER", swsname, expiration, mailaddress
                else:
                    print "  MAIL", swsname, expiration, mailaddress



# delete the already expired workspaces which are over "keeptime" days old
# this searches over DB
for fs in fslist:
    spaces = config["workspaces"][fs]["spaces"]
    dbdir = config["workspaces"][fs]["database"]
    print " checking for expired workspaces for", fs, dbdir, spaces
    dbdeldir = os.path.join(config["workspaces"][fs]["database"], config["workspaces"][fs]["deleted"])
    keeptime = config["workspaces"][fs]["keeptime"]
    workspacedelprefix = config["workspaces"][fs]["deleted"]
    for dbentryfilename in glob.glob(os.path.join(dbdeldir,"*-*-*")):
        workspace = ""
        expiration = 0
        try:
           dbentry = yaml.load(open(dbentryfilename))
           expiration = int(dbentry['expiration'])
           workspace = dbentry['workspace']
        except:
           dbentry = get_old_db_entry_informations(dbentryfilename)
           expiration = int(dbentry['expiration'])
           workspace = dbentry['workspace']
        if workspace == "" or expiration == 0:
            print "  FAILED", dbentryfilename
            continue
        # take time of release from filename
        released = dbentryfilename.split("-")[-1]
        try:
            expiration = int(released)
        except ValueError:
            pass 
        if time.time() > expiration + keeptime*24*3600:
            print "  deleting", dbentryfilename
            # remove the DB entry
            if not dryrun:
                os.unlink(dbentryfilename)
                print " OS.UNLINK", dbentryfilename
            else:
                print "  RM",dbentryfilename
            # remove the workspace directory
            if not dryrun:
                deldir(os.path.join(os.path.dirname(workspace), workspacedelprefix,os.path.basename(dbentryfilename)))
                print "  DELDIR", os.path.join(os.path.dirname(workspace), workspacedelprefix,os.path.basename(dbentryfilename))
            else:
                print "  RM", os.path.join(os.path.dirname(workspace), workspacedelprefix,os.path.basename(dbentryfilename))
            try:
                if not dryrun:
                    os.rmdir(os.path.join(os.path.dirname(workspace), workspacedelprefix,os.path.basename(dbentryfilename)))
                    print "  OS.RMDIR", os.path.join(os.path.dirname(workspace), workspacedelprefix,os.path.basename(dbentryfilename))
                else:
                    print "  RM", os.path.join(os.path.dirname(workspace), workspacedelprefix,os.path.basename(dbentryfilename))
            except:
              print "  does not exist"
        else:
            print "  moved but restorable",dbentryfilename



end = time.time()
print "end of expirer run after ",end-start,"seconds at",time.ctime()
