#!/usr/bin/env python

#
# Copyright (c) 2009-2011 VMware, Inc.  All rights reserved.
# 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.
#

"""Provides service management command line utility

Usage: vamimanager <command>
One of the following <command> must be specified first:
        --install       [--target <targetfolder>] <servicetarfile>
                        install the service by extracting to target folder and registering the service
        --uninstall     [--target <targetfolder>] <servicetarfile>
                        uninstall the service by deleting from target folder and unregistering the service
        --extract       [--target <targetfolder>] <servicetarfile>
                        extract the service to target folder
        --register    servicename
                        register the service
        --unregister    servicename
                        unregister the service

Default targetfolder is /opt/vmware

This is also used by pkgservice to register services as post install process for installing service as a package

"""

from __future__ import print_function
import tempfile
import tarfile
import libxml2
import re
import sys
import subprocess
import os
import os.path
import time
import fileinput
import shutil

import gettext
__t = gettext.translation('vamimanager', '/opt/vmware/lib/locale', fallback=True)
_ = __t.ugettext if sys.version_info[0] < 3 else __t.gettext

VADKOUT = os.path.join('/opt', 'vmware')
targetdir = VADKOUT
log = None
FAILUREMSG = _("Failure: ")
DEBUGMSG = "Debug: "
debugflag = False


#
# maintain a log file
#
class logfile:
   def message(self, msg):
      msg = msg.strip()
      print (msg)


def debug(msg):
   if debugflag:
      log.message(DEBUGMSG + msg)
#
# Modify a passed message to indicate a failure
#
def failure(msg):
   log.message(FAILUREMSG + msg)
#
#  Curry lets you pass in default function arguments when passing functions as arguments to other functions
#  Default arguments are passed from left to right
#   refer http://www.python.org/dev/peps/pep-0309/
def curry(fn, *cargs, **ckwargs):
   def call_fn(*fargs, **fkwargs):
      d = ckwargs.copy()
      d.update(fkwargs)
      return fn(*(cargs + fargs), **d)
   return call_fn


def extract(servicetar, target, filter, targetfile=None):
   flist = []
   f = re.compile(filter)
   for path in servicetar.namelist():
      if not f.match(path) :
         continue
      if targetfile:
         tgt = targetfile(target, path, servicetar)
      else:
         tgt = os.path.join(target, path)
      tgtdir = os.path.dirname(tgt)
      if not os.path.exists(tgtdir):
         os.makedirs(tgtdir)
      if os.path.isdir(tgt):
         continue
      flist.append(tgt)
      debug("Copying to "+tgt)
      servicetar.copy(path,tgt)
   return flist


def delete(servicetar, target, filter, targetfile):
   flist = []
   f = re.compile(filter)
   for path in servicetar.namelist():
      if not f.match(path) :
         continue

      if targetfile:
         tgt = targetfile(target, path, servicetar)
      else:
         tgt = os.path.join(target, path)
      if os.path.isfile(tgt):
         debug("Removing file "+tgt)
         os.remove(tgt)
   return flist

def delete_error(fc, path, excinfo):
   failure(_("Unable to delete ") + path + "\n" + excinfo)

def rm_tree(path):
   if os.path.exists(path):
      shutil.rmtree(path, delete_error)

def striptargetfile(strip, target, path, servicetar):
   tpath = re.sub(strip, '', path)
   return os.path.join(target, tpath)

def namespacefile(target, path, servicetar):
   filename = os.path.basename(path)
   rens = re.compile(r"^#pragma +namespace *\(\"(.+)\"\).*")
   namespace = "root/cimv2"
   content = servicetar.read(path)
   for line in content.splitlines():
      match = rens.match(line)
      if match:
         namespace = match.group(1)
         break
   #if name space starts with / os.path.join will interpret as absolute path and ignore target
   if namespace.startswith('/'):
      namespace = namespace[1:]
   tgt = os.path.join(target, namespace, filename)
   return tgt


def add_service_name(xml_file, service_name):
   doc = libxml2.parseFile(xml_file)
   xp = doc.xpathNewContext()
   service = xp.xpathEval('/vami/services/service[@name="' + service_name + '"]')
   if len(service) == 0:
      services = xp.xpathEval('/vami/services')[0]
      service = services.newChild(None, 'service', None)
      service.setProp('name', service_name)
      doc.saveFormatFile(xml_file, 1)
   doc.freeDoc()

def remove_service_name(xml_file, service_name):
   doc = libxml2.parseFile(xml_file)
   xp = doc.xpathNewContext()
   service = xp.xpathEval('/vami/services/service[@name="' + service_name + '"]')
   if len(service) == 0:
      doc.freeDoc()
      return
   for s in service:
      s.unlinkNode()
      s.freeNode()
   doc.saveFormatFile(xml_file, 1)
   doc.freeDoc()

def createServicesXML(dir, filename):
   xml_file = os.path.join(dir , filename)
   if os.path.exists(xml_file):
      return
   if not os.path.exists(dir):
      os.makedirs(dir)
   xml = "<vami><services></services></vami>"
   doc = libxml2.parseDoc(xml)
   doc.saveFormatFile(xml_file, 1)

def register_service(service_name):
   httpdocs = os.path.join(targetdir, 'share', 'htdocs')
   #Update VAMI.xml file
   vami_file = os.path.join(httpdocs, 'VAMI.xml')
   createServicesXML(httpdocs, 'VAMI.xml')
   debug("Adding "+service_name+" to "+vami_file)
   add_service_name(vami_file, service_name)
   #Update services.xml file
   vamiloc = os.path.join(targetdir, 'var', 'lib', 'vami')
   services_xml = os.path.join(vamiloc, 'services.xml')
   createServicesXML(vamiloc, 'services.xml')
   debug("Adding "+service_name+" to "+services_xml)
   add_service_name(services_xml, service_name)
   #Update lighttpd.conf
   #lighttpd only takes include file relative to lighttpd.conf
   relserviceloc = os.path.join("..", "..", 'var', 'lib', 'vami', service_name)
   conffile = os.path.join(relserviceloc, 'conf', 'lighttpd.conf')
   conffileloc = os.path.join(vamiloc, service_name, 'conf', 'lighttpd.conf')
   lighttpdconf = os.path.join(targetdir , 'etc', 'lighttpd', 'lighttpd.conf')
   if os.path.exists(conffileloc):
      fp = open (lighttpdconf, 'a')
      try:
         debug("Adding include "+conffile+" to "+lighttpdconf)
         fp.write('include "'  +conffile + '"\n')
      finally:
         fp.close()

def unregister_service(service_name):
   httpdocs = os.path.join(targetdir, 'share', 'htdocs')

   #Remove entry from VAMI.xml file
   vami_file = os.path.join(httpdocs , 'VAMI.xml')
   debug("Removing "+service_name+" from "+vami_file)
   remove_service_name(vami_file, service_name)

   #Remove entry from services.xml file
   vamiloc = os.path.join(targetdir, 'var', 'lib', 'vami')
   services_xml = os.path.join(vamiloc, 'services.xml')
   debug("Removing "+service_name+" from "+services_xml)
   remove_service_name(services_xml, service_name)

   #Remove entry from lighttpd.conf
   relserviceloc = os.path.join("..", "..", 'var', 'lib', 'vami', service_name)
   conffile = os.path.join(relserviceloc, 'conf', 'lighttpd.conf')
   lighttpdconf = os.path.join(targetdir , 'etc', 'lighttpd', 'lighttpd.conf')
   # Skip removing service entries if lighttpd is not installed on system.
   if os.path.exists(lighttpdconf):
      includeline = 'include "' + conffile + '"\n'
      debug("Removing "+includeline+" from "+lighttpdconf)
      for line in fileinput.input(lighttpdconf, inplace=1):
         if not line == includeline:
            sys.stdout.write(line)
   return 0


def extract_view(service_name, servicetar):
   httpdocs = os.path.join(targetdir, 'share', 'htdocs')

   #Extract view files and web-root
   serviceloc = os.path.join(httpdocs, 'service', service_name)
   extract(servicetar, serviceloc, r'^(\./)?view/.*' , curry(striptargetfile, r'^(\./)?view/'))
   extract(servicetar, httpdocs, r'^(\./)?web\-root/.*' , curry(striptargetfile, r'^(\./)?web\-root/'))

   # extract lighttpd.conf file
   vamiloc = os.path.join(targetdir, 'var', 'lib', 'vami')
   serviceloc = os.path.join(vamiloc, service_name)
   extract(servicetar, serviceloc, r'^(\./)?conf/.*')


def extract_provider(service_name, servicetar):
   #Extract .so and .reg files by normalizing the directory structure
   libloc = os.path.join(targetdir , 'lib')
   # TODO: Do we need to change the mode to 755 here?
   extract(servicetar, libloc, r'(\./)?provider/.*\.so', curry(striptargetfile, r'^(\./)?provider(/)?.*/'))

   stageloc = os.path.join(targetdir , 'var', 'lib', 'sfcb', 'stage')

   stageregs = os.path.join(stageloc, 'regs')
   extract(servicetar, stageregs, r'(\./)?provider/.*\.reg', curry(striptargetfile, r'^(\./)?provider(/)?.*/'))

   #Process .mof files by placing them according to their namespace
   stagemofs = os.path.join(stageloc, 'mofs')
   extract(servicetar, stagemofs, r'(\./)?provider/.*\.mof', curry(namespacefile))


   #Extract system-root
   sysroot='/'
   #Cut /opt/vmware from targetdir to find the sysroot
   if not targetdir==VADKOUT:sysroot=targetdir[0:len(targetdir)-len(VADKOUT)]
   extract(servicetar, sysroot, r'^(\./)?system-root/.*', curry(striptargetfile, r'^(\./)?system-root/'))

      #Extract provider.xml if present
   vamiloc = os.path.join(targetdir, 'var', 'lib', 'vami')
   serviceloc = os.path.join(vamiloc, service_name)
   extract(servicetar, serviceloc, r'(\./)?provider/provider.*\.xml')
   #Extract service.xml
   extract(servicetar, serviceloc, r'(\./)?service.xml')


def delete_view(service_name, servicetar):
   httpdocs = os.path.join(targetdir, 'share', 'htdocs')

   #delete view files
   serviceloc = os.path.join(httpdocs, 'service', service_name)
   rm_tree(serviceloc)

   #delete only files present in tar file in  web-root
   delete(servicetar, httpdocs, r'^(\./)?web-root/.*' , curry(striptargetfile, r'^(\./)?web-root/'))

   #delete lighttpd.conf file for the serivce
   vamiloc = os.path.join(targetdir, 'var', 'lib', 'vami')
   serviceconf = os.path.join(vamiloc, service_name, 'conf')
   rm_tree(serviceconf)

def delete_provider(service_name, servicetar):
   #Delete .so and .reg files only those present in tarfile
   libloc = os.path.join(targetdir , 'lib')
   delete(servicetar, libloc, r'(\./)?provider/.*\.so', curry(striptargetfile, r'^(\./)?provider(/)?.*/'))

   stageloc = os.path.join(targetdir , 'var', 'lib', 'sfcb', 'stage')
   delete(servicetar, stageloc + '/regs', r'(\./)?provider/.*\.reg', curry(striptargetfile, r'^(\./)?provider(/)?.*/'))

   #Process .mof files to find namespaces, then delete only those present in tarfile
   delete(servicetar, stageloc + '/mofs', r'(\./)?provider/.*\.mof', curry(namespacefile))
   sysroot='/'
   if not targetdir==VADKOUT:sysroot=targetdir[0:targetdir.find(VADKOUT)]
   delete(servicetar, sysroot, r'^(\./)?system-root/.*', curry(striptargetfile, r'^(\./)?system-root/'))

   #Remove provider folder
   vamiloc = os.path.join(targetdir, 'var', 'lib', 'vami')
   serviceloc = os.path.join(vamiloc, service_name)
   rm_tree(serviceloc)

def start_servers():
   stop_servers()
   cmd = ['/opt/vmware/bin/sfcbrepos', '-f']
   ret = runprocess(cmd)
   if ret>0: return ret
   cmd = ['/etc/init.d/vami-sfcb', 'start']
   ret = runprocess(cmd)
   if ret>0: return ret
   cmd = ['/etc/init.d/vami-lighttp', 'start']
   ret = runprocess(cmd)
   if ret>0: return ret

def stop_servers():
   cmd = ['/etc/init.d/vami-sfcb', 'stop']
   ret = runprocess(cmd)
   if ret>0: return ret
   cmd = ['/etc/init.d/vami-lighttp', 'stop']
   ret = runprocess(cmd)
   if ret>0: return ret


def runprocess(cmd):
   pobj = subprocess.Popen(' '.join(cmd), bufsize=1, shell=True,
                     stdout=subprocess.PIPE,
                     stderr=subprocess.STDOUT)
   while True:
      line = pobj.stdout.readline()
      if not line:
         break
      debug(line.strip())
   return pobj.wait()

def install_service(service_name, servicetar):
   global debugflag
   debugflag=True
   extract_service(service_name, servicetar)
   register_service(service_name)
   ret = start_servers()
   if ret>0:
      failure(_("Unable to start lighttpd and sfcbd servers"))

def uninstall_service(service_name, servicetar):
   global debugflag
   debugflag=True
   ret = stop_servers()
   if ret>0:
        failure(_("Unable to stop  lighttpd and sfcbd servers"))
   delete_service(service_name, servicetar)
   unregister_service(service_name)
   ret = start_servers()
   if ret>0:
      failure(_("Unable to start lighttpd and sfcbd servers"))

def extract_service(service_name, servicetar):
   extract_view(service_name, servicetar)
   extract_provider(service_name, servicetar)

def delete_service(service_name, servicetar):
   delete_view(service_name, servicetar)
   delete_provider(service_name, servicetar)


def usage():
   print (_("\nUsage:"), os.path.basename(sys.argv[0]), "<command>")
   print (_("One of the following <command> must be specified first:"))
   for long, cmd, options, usagemsg, arglen in servicecommandhelp:
      print ("\t", long, "\t", options, "\n\t\t\t", _(usagemsg))
   print (_("\nDefault targetfolder is /opt/vmware\n"))

def commandusage(long, options, usagemsg):
   print (_("\nUsage:"), os.path.basename(sys.argv[0]), long, "\t", options)
   print ("\t\t\t", _(usagemsg))

#
# Define a dummy function so we can use gettext on the usagemsgs.
# Remember to tell xgettext to look for an N_ prefix!
#
def N_(message): return message

servicecommandhelp = [
   ("--install", install_service, "[--target <targetfolder>] <servicetarfile>", N_("install the service by extracting to target folder and registering the service"), 5),
   ("--uninstall", uninstall_service, "[--target <targetfolder>] <servicetarfile>", N_("uninstall the service by deleting from target folder and unregistering the service"), 5),
   ("--extract", extract_service, "[--target <targetfolder>] <servicetarfile>", N_("extract the service to target folder"), 5),
   ("--register", register_service, "servicename", N_("register the service "), 3),
   ("--unregister", unregister_service, "servicename", N_("unregister the service"), 3)
]

def validate_invoke(command, tfile):
   ret = 1

   if not os.path.exists(tfile):
      failure(_('Can not find "') + tfile + '"')
      return ret

   clean_temp = False
   dir_path = None
   if os.path.isdir(tfile) :
      dir_path=tfile
   elif tarfile.is_tarfile(tfile):
      dir_path = tempfile.mkdtemp()
      tf = tarfile.open(tfile,'r')
      extractall(tf,dir_path)
      tf.close()
      clean_temp = True
   else:
      failure(_('Given file "') + tfile + _('" is not a service tar file or a service directory.'))
      return ret

   servicetar = DirWrapper(dir_path)

   try:
      service_text = servicetar.read('service.xml')
   except:
      failure('"' + tfile + _('" is missing service.xml '))
      return ret
   #   print service_text
   doc = libxml2.parseDoc(service_text)
   xp = doc.xpathNewContext()

   sn = xp.xpathEval('//service/name')
   if not sn:
      failure(_('Missing service name in service.xml file in "') + tfile + '"')
      return 1
   service_name = sn[0].content.lower()
   doc.freeDoc()

   ret = command(service_name, servicetar)
   if clean_temp:
      rm_tree(dir_path)
   return 0


def extractall(tf, path=".", members=None):
        directories = []

        if members is None:
            members = tf

        for tarinfo in members:
            if tarinfo.isdir():
                # Extract directory with a safe mode, so that
                # all files below can be extracted as well.
                try:
                    os.makedirs(os.path.join(path, tarinfo.name), 0o777)
                except EnvironmentError:
                    pass
                directories.append(tarinfo)
            else:
                tf.extract(tarinfo, path)

        # Reverse sort directories.
        directories.sort(lambda a, b: cmp(a.name, b.name))
        directories.reverse()

        # Set correct owner, mtime and filemode on directories.
        for tarinfo in directories:
            path = os.path.join(path, tarinfo.name)
            try:
                tf.chown(tarinfo, path)
                tf.utime(tarinfo, path)
                tf.chmod(tarinfo, path)
            except:
                if tf.errorlevel > 1:
                    raise

class DirWrapper:
   def __init__(self, dirpath):
      self.dir = dirpath

   def read(self, filepath):
      f = open(os.path.join(self.dir, filepath), 'r')
      data = f.read()
      f.close()
      return data

   def copy(self,src,dst):
      shutil.copy(os.path.join(self.dir, src), dst)

#   Return list of the absolute file paths recursively
   def namelist(self):
      namelist = []
      l = len(self.dir) + 1
      for root, dirs, files in os.walk(self.dir):
         for name in files:
            filename = os.path.join(root, name)
            namelist.append(filename[l:])
      return namelist


def main():
   global targetdir
   global log
   ret = 1
   if len(sys.argv) <= 1:
      usage()
      return ret

   cmd = sys.argv[1]
   tfile = None
   log = logfile()

   for long, command, options, usagemsg, arglen in servicecommandhelp:
      if cmd == long:
         if len(sys.argv) == 3 or len(sys.argv) == arglen:
            tfile = sys.argv[2]
            if len(sys.argv) > 3 and sys.argv[2] == "--target":
               targetdir = os.path.join(sys.argv[3], 'opt', 'vmware')
               tfile = sys.argv[4]
            if not os.path.exists(targetdir): os.makedirs(targetdir)
            if sys.argv[1] == "--register" or sys.argv[1] == "--unregister":
               service_name = sys.argv[2].lower()
               command(service_name)
               return 0
            else:
               return validate_invoke(command, tfile)
         else:
            commandusage(long, options, usagemsg)
            return ret
         break
   else:
      usage()
      return ret

if __name__ == "__main__":
   sys.exit(main())
