#!/usr/bin/python
# @package      hubzero-mw2-file-service
# @file         maxwell_fs
# @copyright    Copyright (c) 2016-2020 The Regents of the University of California.
# @license      http://opensource.org/licenses/MIT MIT
#
# Based on prior work by Richard L. Kennell and Nicholas Kisseberth
#
# Copyright (c) 2016-2020 The Regents of the University of California.
#
# 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.
#
# HUBzero is a registered trademark of The Regents of the University of California.
#

"""
Maxwell script, to be run on file servers.
This script does not access the SQL database, which simplifies the handling of forks and child
 exits.

Files used by this script:
FILE_CONFIG_FILE : the path to the configuration information.
              Note that fixed paths are set in error.py.


log files for an application:
SERVICE_LOG_PATH/<session number>.err
SERVICE_LOG_PATH/<session number>.out

Configuration information for a particular session and application are stored
inside a user's directory:
resource file = homedir + "/data/sessions/<session number>/resources"
"""

import os
import sys
import pwd
import re
import stat
import subprocess
from hubzero.mw.log import log, setup_log, log_exc, ttyprint, save_out
from hubzero.mw.support import check_rundir
from hubzero.mw.user_account import User_account
from hubzero.mw.constants import HOST_K, FILE_CONFIG_FILE, SERVICE_LOG, \
    VERBOSE, USER_REGEXP, SESSNAME_REGEXP, PATH_REGEXP
from hubzero.mw.errors import  MaxwellError, InputError

CONTAINER_CONF = {}

#=============================================================================
# Check the user's directory
#=============================================================================
def setup_dir(user, session, params=None):
  """
    Command-level function. Note that this happens on the fileserver, not the
    execution host.
  """
  account = User_account(user)
  os.umask(0077) # only user has permissions
  account.home_quota(CONTAINER_CONF)
  account.relinquish()

  try:
    session_existed = True
    try:
      os.lstat(account.session_dir(session))
    except OSError:
      os.makedirs(account.session_dir(session))
      session_existed = False
    if session_existed:
      log("session directory already exists (%s, %s)" % (user, session))
      sys.exit(1)

    resources = account.session_dir(session) + "/resources"

    rfile = open(resources, "w")
    rfile.write("sessionid %s\n" % session)
    rfile.write("results_directory %s/data/results/%s\n" % (account.homedir, session))
    rfile.close()
    if VERBOSE:
      log("setup session directory and resources for user '%s' in session %s" % (user, session))
  except OSError, re:
    raise MaxwellError("Unable to setup user '%s' in session %s because %s" % (user, session, re))

  if params is not None and params != "":
    import urllib2
    params_path = account.params_path(session)
    pfile = open(params_path, "w")
    pfile.write(urllib2.unquote(params).decode("utf8"))

def update_quota(user, block_soft, block_hard):
  """
    Change the quota for a user.
    This happens on a fileserver.
  """
  if block_soft < 0 or block_hard < 0:
    raise InputError("Invalid quotas")
  # don't create home directory because setquota is invoked at the time of user creation
  # don't want empty home directories. There were also LDAP propagation delays
  # account = User_account(user)
  # account.home_quota(CONTAINER_CONF)
  subprocess.check_call(["setquota", '-a', user, "%d" % block_soft, "%d" % block_hard, '0', '0'])

def get_quota(user):
  """
    Get the quotas (soft and hard) for a user.
    This happens on a fileserver.
  """
  process = subprocess.Popen(
    ["quota", "-v", "--no-wrap", user],
    stdin= None,
    stdout= subprocess.PIPE,
    stderr= subprocess.PIPE,
    shell = False
  )
  (stdout, stderr) = process.communicate()
  if process.returncode == 0:
    for line in str(stdout).split("\n"):
      if line[0:20] == "Disk quotas for user":
        continue
      elif line[0:15] == "     Filesystem":
        continue
      else:
        parts = line.split()
        if len(parts) > 2:
          print parts[2]
          print parts[3]
  else:
    raise MaxwellError("Quota for user '%s' error: '%s'" % (user, stderr))

def update_resources(user, session, dispnum):
  """
    Update the resource file for the session.
    Command-level function.  Note that this happens on the fileserver, not the
    execution host.
    dispnum is unused and is there for compatibility with anonymous sessions
    (see maxwell_service)
  """
  os.umask(0077)
  account = User_account(user)
  resources = account.session_dir(session) + "/resources"

  account.relinquish()

  try:
    rfile = open(resources,"a+")
    # Read data from command line and write to file, until an empty string is found
    while 1:
      line = sys.stdin.readline()
      if line == "":
        break
      rfile.write(line)

    rfile.close()
  except OSError:
    raise MaxwellError("Unable to append to resource file.")

def erase_session_dir(user, session_id):
  """Used for aborting session creation after the session files were setup.

    Command-level function.
  """
  account = User_account(user)

  account.relinquish()

  args = ['/bin/rm', '-rf', account.session_dir(session_id)]
  subprocess.check_call(args)

def erase_userhome(user):
  account = User_account(user)
  if str(user).find("zqkg") == 0:
    # OK, this is a test user
    args = ['/bin/rm', '-rf', account.homedir]
    subprocess.check_call(args)
  else:
    raise MaxwellError("Requested deleting the home directory of a user who isn't a test user")

def move_userhome(userfrom, userto):
  accountfrom = User_account(userfrom)
  accountto = User_account(userto)
  if os.path.isdir(accountto.homedir):
    raise MaxwellError("Home directory %s already exists" % accountto.homedir)
  args = ['/bin/mv', accountfrom.homedir, accountto.homedir]
  subprocess.check_call(args)

def create_userhome(user):
  account = User_account(user)
  account.create_home()

def proxy_path(account):
  # return account.homedir + "/x509up_u" + str(account.uid)
  return "/apps/globus/proxies/x509up_u" + str(account.uid)

def store_proxy(user, proxy):
  """ Springboard functionality to keep proxy credentials in /apps.
      Why not in the user's home directory?  Because there's no username yet when this is called sometimes.
      We're assuming that there won't be race conditions writing here.  Make
      /apps/globus owned by root, readable by others: we're writing as root, that's not safe if others can write.
  """
  if VERBOSE:
    log("Installing grid proxy credential for '%s'." % (user))

  try:
    if (str.isdigit(user)):
      info = pwd.getpwuid(int(user))
    else:
      info = pwd.getpwnam(user)
    uid = info[2]
    gid = info[3]
    homedir = info[5]
  except KeyError:
    raise MaxwellError("Unable to find account information for '%s'." % user)

  proxyfile = "/apps/globus/proxies/x509up_u" + str(uid)

  #account = User_account(user)
  #account.home_quota()
  os.umask(0077)
  #account.relinquish()
  try:
    rfile = open(proxyfile, "w")
    rfile.write(proxy)
    rfile.close()
    os.chown(proxyfile, uid, gid)
  except OSError:
    raise MaxwellError("Unable to store grid proxy credential")

def grid_proxy_info_timeleft(user):
  """Retrieve the proxy timeleft.  Run as the user
  """
  MYPROXY_SERVER = "myproxy.teragrid.org"
  account = User_account(user)
  cmd = "export X509_USER_PROXY=%s; export MYPROXY_SERVER=%s; . /apps/globus/setup.sh; grid-proxy-info -timeleft" % \
    (proxy_path(account), MYPROXY_SERVER)
  args = ['/bin/su', str(user), "-c", cmd]
  subprocess.check_call(args)
  p = subprocess.Popen(args, stderr=subprocess.PIPE, stdout=subprocess.PIPE)
  (stdout, stderr) = p.communicate()
  if len(stdout) > 0:
    ttyprint(stdout)
  if len(stderr) > 0:
    log("error:" + stderr)
    ttyprint(stderr)

def setfacl(facl, toolpath):
  try:
    os.lstat(toolpath)
  except OSError:
    log("tool '%s' doesn't seem to be installed" % toolpath)
    # if tool isn't installed, do nothing
    return
  subprocess.call(['setfacl', "--set", facl, toolpath])

#=============================================================================
# INPUT VALIDATION
#=============================================================================
def validate_user(i):
  """input username validation"""
  m = re.match(USER_REGEXP, sys.argv[i])
  if m is None:
    raise InputError("Bad user ID '%s'" % sys.argv[i])
  return m.group()

def validate_path(i):
  """input path validation"""
  m = re.match(PATH_REGEXP, sys.argv[i])
  if m is None:
    raise InputError("Bad path '%s'" % sys.argv[i])
  return m.group()

def validate_facl(i):
  """input path validation"""
  m = re.match(r'\A[a-zA-Z0-9._\-:,\'\"]+\Z', sys.argv[i])
  if m is None:
    raise InputError("Bad path '%s'" % sys.argv[i])
  return m.group()

def check_nargs(minarg, maxarg=0):
  """Check the number of arguments"""
  if len(sys.argv) < minarg:
    raise InputError("Incomplete command: %s" % " ".join(sys.argv))
  if len(sys.argv) > maxarg and len(sys.argv) > minarg:
    raise InputError("Too many arguments: %s" % " ".join(sys.argv))

def validate_session(i):
  """Validate a session identifier, number + suffix
  suffix: d for development, p for production """
  m = re.match(SESSNAME_REGEXP, sys.argv[i])
  if m is None:
    raise InputError("Bad session ID '%s'" % sys.argv[i])
  return m.group()

#=============================================================================
#=============================================================================
# Main program...
# by session name, we mean an integer followed by a letter
# by sessnum, we mean just the integer.
#
# We recognize these commands on the fileserver:
#  setup_dir <user> <session name>
#  update_resources <user> <session name>
#  erase_sessdir  <user> <session name>
#=============================================================================
#=============================================================================


#=============================================================================
# Configuration and Safety
# This program receives commands from maxwell
# it runs on a different host and so has different rules.
#
# note that hosts have varying capabilities (roles) assigned to them
# if it's a fileserver, it will respond to setup_dir, etc...
#=============================================================================

check_rundir()

# Load the configuration and override the default variables.
# First check that it is safe to do so
try:
  mode = os.lstat(FILE_CONFIG_FILE)[stat.ST_MODE]
  if mode & stat.S_IWOTH:
    print "configuration file is writable by others; exiting.\n"
    sys.exit(1)
  try:
    execfile(FILE_CONFIG_FILE)
  except EnvironmentError:
    print "Unable to read configuration file, exiting."
    print "The configuration file '%s' needs to exist" % FILE_CONFIG_FILE
    sys.exit(1)

except OSError:
  # no configuration file is present, use defaults
  pass


# check that user is correct
login = pwd.getpwuid(os.geteuid())[0]
if login != HOST_K["SVC_HOST_USER"]:
  print "maxwell: access denied to %s. Must be run as %s (see %s)" \
        % (login, HOST_K["SVC_HOST_USER"], FILE_CONFIG_FILE)
  sys.exit(1)

#=============================================================================
# Input parsing
#=============================================================================

if sys.argv[1] == "check":
  # the only place in this script where we need to print to standard out.
  # However we want to capture stdout and stderr.  If the logging redirection is setup
  # before this point, we can't print unless we call save_out().  Then later startvnc hangs
  # because save_out() prevents the dissociation of processes.
  # One solution is to setup logging after this.  Alternatively, call save_out() and then discard_out().
  print("OK")
  sys.exit(0)

if sys.argv[1] == "get_quota":
  # done before setting up logging because we need to print to stdout
  username = validate_user(2)
  get_quota(username)
  exit(0)

save_out() # this script needs ttyprint functionality
setup_log(SERVICE_LOG, None) # no log id because the log file is unique to this script
if VERBOSE:
  log("received command %s" % " ".join(sys.argv))
try:
  inputs = {}


  if sys.argv[1] == "setup_dir":
    check_nargs(4, 5)
    inputs["user"] = validate_user(2)
    inputs["session"] = validate_session(3)
    if len(sys.argv) > 4:
      inputs["params"] = sys.argv[4]
    else:
      inputs["params"] = None
    setup_dir(**inputs)

  elif sys.argv[1] == "update_resources":
    # dispnum is unused so we don't validate it
    check_nargs(5)
    inputs["user"] = validate_user(2)
    inputs["session"] = validate_session(3)
    inputs["dispnum"] = 0
    update_resources(**inputs)

  elif sys.argv[1] == "erase_sessdir":
    check_nargs(4)
    user_param = validate_user(2)
    sess_param = validate_session(3)
    erase_session_dir(user_param, sess_param)

  elif sys.argv[1] == "erase_userhome":
    check_nargs(3)
    user_param = validate_user(2)
    erase_userhome(user_param)

  elif sys.argv[1] == "create_userhome":
    check_nargs(3)
    user_param = validate_user(2)
    create_userhome(user_param)

  elif sys.argv[1] == "move_userhome":
    check_nargs(4)
    userfrom = validate_user(2)
    userto = validate_user(3)
    move_userhome(userfrom, userto)

  elif sys.argv[1] == "store_proxy":
    check_nargs(3)
    user = sys.argv[2]
    proxy = sys.stdin.read()
    store_proxy(user, proxy)

  elif sys.argv[1] == "grid_proxy_info_timeleft":
    check_nargs(3)
    user = validate_user(2)
    grid_proxy_info_timeleft(user)

  elif sys.argv[1] == "setfacl":
    check_nargs(4)
    facl = sys.argv[2]
    toolpath = validate_path(3)
    setfacl(facl, toolpath)

  elif sys.argv[1] == "update_quota":
    check_nargs(5)
    inputs["block_soft"] = int(sys.argv[3])
    inputs["block_hard"] = int(sys.argv[4])
    users = sys.argv[2].split(",")
    prog = re.compile(USER_REGEXP)
    try:
      validated_users = map(lambda x:prog.match(x).group(0), users)
    except AttributeError:
      raise InputError("Invalid user name passed to update_quota: '%s'" % "' '".join(users))

    for user in validated_users:
      inputs["user"] = user
      update_quota(**inputs)

  else:
    raise InputError("Unknown command: %s" % " ".join(sys.argv))

# attempted conversion of alpha chars to int results in ValueError
except ValueError, e:
  log("Integer input expected for this command: '%s'" % " ".join(sys.argv))
  sys.exit(1)

except InputError, e:
  log("%s" % e)
  if VERBOSE:
    log_exc(e)
  sys.exit(1)

except MaxwellError, e:
  log("%s" % e)
  if VERBOSE:
    log_exc(e)
  sys.exit(2)

except Exception, e:
  log("%s" % e)
  if VERBOSE:
    log_exc(e)
  sys.exit(5)

if VERBOSE:
  log("done")
sys.exit(0)
