#!/usr/libexec/platform-python

"""
    workspace++

    ws_list

    python version of ws_list command, no privileges necessary

    list workspaces for all or selected users sorted or unsorted
    with different output formats.
    Reads new YAML configuration files and new YAML workspace database.

    differences to old workspace version
    - usage if YAML file format
    - more options for administrator (configured and root)
        + list expired/moved workspaces

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

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

    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/>.

"""

from __future__ import print_function

import os, os.path, pwd, grp, sys, stat
import glob, time
from optparse import OptionParser


class struct: pass
space2fs={}
spaces=[]

# 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.insert(0,value)
                    break
                else:
                    print("Warning: Invalid pythonpath in ws.conf", file=sys.stderr)

read_python_conf()

import yaml


# print a entry
def printentry(entry, admin, terse, verbose):
    if verbose: terse = False
    if admin:
        print("id:", os.path.basename(entry.name))
    else:
        print("id:", os.path.basename(entry.name)[os.path.basename(entry.name).find('-')+1:])
    print(4*' ','workspace directory  :', entry.workspace)
    print(4*' ','remaining time       :', end=' ')
    if time.time() > entry.expiration:
        print('expired')
    else:
        remaining = entry.expiration-time.time()
        print('%d days %d hours' % (remaining/(24*3600),(remaining%(24*3600))/3600))
    if not terse:
        if entry.comment != "":
            print(4*' ','comment              :', entry.comment)
        print(4*' ','creation time        :', time.ctime(entry.creation))
        print(4*' ','expiration date      :', time.ctime(entry.expiration))
        for s in spaces:
            if entry.workspace.startswith(s):
                print(4*' ','filesystem name      :', space2fs[s])
    print(4*' ','available extensions :', entry.extensions)
    if verbose:
        print(4*' ','acctcode             :', entry.acctcode)
        print(4*' ','reminder             :', time.ctime(entry.expiration-entry.reminder*(24*3600)))
        print(4*' ','mailaddress          :', entry.mailaddress)

# we have to find out if the calling user is admin before we can process commandline,
# so we have to determine user and read config first, and parse commandline last

# who are we?
uid = os.getuid()
gid = os.getgid()
user = pwd.getpwuid(uid)[0]
group = grp.getgrgid(gid)[0]
groups = [grp.getgrgid(gid_tmp)[0] for gid_tmp in os.getgroups()]

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

# check admin state
admin = False
if 'admins' in config:
    if user in config['admins']:
        admin = True

# make root always admin, seeing admin options and admin output
# FIXME: eid or uid or both?
if os.geteuid()==0 or os.getuid()==0:
    admin = True

# option configuration
parser = OptionParser(usage="Usage: %prog [options] [pattern]")
parser.add_option('-F', '--filesystem', dest='filesystem', help='filesystem to list workspaces from')
parser.add_option('-g', action="store_true", dest='groupws', default=False, help='also list group workspaces')
parser.add_option('-l', action="store_true", dest='filesystemlist', default=False, help='list available filesystems')
parser.add_option('-s', action="store_true", dest='short', default=False, help='only show names of workspaces')
if admin:
    parser.add_option('-u', action="store", dest='user', help='only show workspaces of selected user')
    parser.add_option('-e', action="store_true", dest='expired', default=False,
            help='show expired workspaces, can be combined with -F and -u to reduce output, no sorting supported')
parser.add_option('-N', action="store_true", dest='sort_name', default=False, help='sort by name')
parser.add_option('-C', action="store_true", dest='sort_creation', default=False, help='sort by creation')
parser.add_option('-R', action="store_true", dest='sort_remaining', default=False, help='sort by remaining time')
parser.add_option('-r', action="store_true", dest='sort_revert', default=False, help='revert sorting order')
parser.add_option('-t', action="store_true", dest='terse', default=False, help='terse output format')
parser.add_option('-v', action="store_true", dest='verbose', default=False, help='verbose output format')
parser.add_option('-a', action="store_true", dest='all', default=False, help='for compatibility')
(options, args) = parser.parse_args()

# all filesystems or a selected one?
if(options.filesystem):
    if options.filesystem in config['workspaces']:
        filesystems = [options.filesystem]
    else:
        print("Error: no such filesystem.",file=sys.stderr)
        sys.exit(-1)
else:
    filesystems = config['workspaces'].keys()

# reduce list to allowed filesystems
legal=[]
for f in filesystems:
    userok=True
    if (('user_acl' in config['workspaces'][f] and len(config['workspaces'][f]['user_acl'])>0 ) or
        ('group_acl' in config['workspaces'][f] and len(config['workspaces'][f]['group_acl'])>0)):
        userok=False
    if 'group_acl' in config['workspaces'][f]:
        for g in groups:
            if g in config['workspaces'][f]['group_acl']:
                userok=True
                break
        if group in config['workspaces'][f]['group_acl']:
            userok=True
    if 'user_acl' in config['workspaces'][f]:
        if user in config['workspaces'][f]['user_acl']:
            userok=True
     # admin can see workspaces from anywhere, but can also restrict
    if userok or admin:
        legal.append(f)

# list workspaces (only allowed ones), exit after done
if options.filesystemlist:
    print('available filesystems:')
    check_default=True
    mydefault=""
    for i in legal:
        if check_default:
            if (('userdefault' in config['workspaces'][i] and user in config['workspaces'][i]['userdefault']) or
               ('groupdefault' in config['workspaces'][i] and group in config['workspaces'][i]['groupdefault'])):
                mydefault=i

    if mydefault=="":
        if 'default' in config: mydefault=config['default']

    for i in legal:
        print(i,end='')
        if i==mydefault:
            print(" (default)")
        else:
            print()
    sys.exit(0)

# create mapping from spaces to filesystems
for f in legal:
    for s in config['workspaces'][f]["spaces"]:
        space2fs[s]=f
        spaces.append(s)


# list workspaces, the normal case
sort = options.sort_remaining or options.sort_creation or options.sort_name
entrylist = []
for fs in legal:

    if len(args) == 1:
        filepattern = args[0]
    else:
        filepattern = "*"

    if admin:
        if not options.expired:
            if options.user:
                pattern = os.path.join(config['workspaces'][fs]['database'],options.user+'-'+filepattern)
            else:
                pattern = os.path.join(config['workspaces'][fs]['database'],'*-'+filepattern)
        else:
            if options.user:
                pattern = os.path.join(config['workspaces'][fs]['database'],config['workspaces'][fs]['deleted'],
                                                                                                options.user+'-'+filepattern)
            else:
                pattern = os.path.join(config['workspaces'][fs]['database'],config['workspaces'][fs]['deleted'],'*-'+filepattern)
    else:
        if options.groupws:
            pattern = os.path.join(config['workspaces'][fs]['database'],'*-'+filepattern)
        else:
            pattern = os.path.join(config['workspaces'][fs]['database'],user+'-'+filepattern)

    for ws in glob.glob(pattern):
        if options.groupws:
            if not os.path.basename(ws).startswith(user+"-"):
                mode = os.stat(ws).st_mode
                if not mode & stat.S_IXUSR:
                    continue
                else:
                    try:
                        content = yaml.load(open(ws))
                    except IOError:
                        continue
                    wsgroup = content['group']
                    if group != wsgroup:
                        continue

        if options.short:
            print(os.path.basename(ws)[os.path.basename(ws).find('-')+1:])
        else:
            entry = struct()
            entry.name = ws
            try:
                content = yaml.load(open(ws))
            except IOError:
                continue
            entry.creation = os.path.getctime(ws)

            if content:
                try:
                    entry.expiration = int(content['expiration'])
                    entry.extensions = content['extensions']
                    entry.acctcode = content['acctcode']
                    entry.workspace = content['workspace']
                    entry.reminder = int(content['reminder'])
                    entry.mailaddress = content['mailaddress']
                    try:
                        entry.comment = content['comment']
                    except:
                        entry.comment = ""
                except TypeError:
                    # fallback to old file format
                    f=open(ws)
                    entry.expiration = int(f.readline())
                    entry.workspace = f.readline()[:-1]
                    entry.acctcode = f.readline()[:-1].split(":")[1]
                    entry.extensions = int(f.readline()[:-1].split(":")[1])
                    entry.reminder = 0
                    entry.mailaddress = ""
                    f.close()
                if sort:
                    # store for sorting
                    entrylist.append(entry)
                else:
                    # this is the quick way, unsorted output if fastest, data printed as it comes in
                    printentry(entry, admin, options.terse, options.verbose)
            else:
                print("ws-db file is empty:", ws ,  file=sys.stderr)

# list sorted
if sort:
    if options.sort_name: sortedentrylist = sorted(entrylist,key=lambda x: x.name, reverse=options.sort_revert)
    if options.sort_creation: sortedentrylist = sorted(entrylist,key=lambda x: x.creation, reverse=options.sort_revert)
    if options.sort_remaining: sortedentrylist = sorted(entrylist,key=lambda x: x.expiration, reverse=options.sort_revert)

    for entry in sortedentrylist:
        printentry(entry, admin, options.terse, options.verbose)
