#!/usr/bin/python

# -*- coding: utf-8 -*-

#
# opsi-pkg, command line tools for OPSI
# 
# Copyright 2012, Mathieu Souchaud.
# 
# This is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation; either version 2.1 of
# the License, or (at your option) any later version.
# 
# This software 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
# Lesser General Public License for more details.
# 
# You should have received a copy of the GNU Lesser General Public
# License along with this software; if not, write to the Free
# Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
# 02110-1301 USA, or see the FSF site: http://www.fsf.org.
#
# Version 0.2

# TODO: 
#   set product properties
#   add: set status manually
#   list only one host
#   ping using ip
#   follow Python coding guidelines
#   check if make-product-package remove defined group?


import argparse
import re
import time
import datetime
import string
import socket

from subprocess import Popen, PIPE

## to adapt to your network:

# default domain added to host name
#defaultDomain = '.domain.lan'
# guess it
defaultDomain = '.' + '.'.join(socket.getfqdn().split('.')[1:])

# number of day after which the host is shown in warning color (i.e. if lastSeen > warnSince)
warnSince = 30
# number of day after which the host is shown in error color
errorSince = 200


debug = False
def logDebug(msg):
    global debug
    if debug: 
        print grey + "DBG: " + str(msg) + reinitColor


def logWarning(msg):
    print red + "WRN: " + str(msg) + reinitColor


info = True
def logInfo(msg):
    global info
    if info: 
        print blue + str(msg) + reinitColor


verbose = False
def logVerbose(msg):
    global verbose
    if verbose: 
        print grey + str(msg) + reinitColor

# by default: ask and print default answer
default = False
yes = False
no = False

force = False

ping = False


grey = '\033[1;30m'
red = '\033[1;31m'
green = '\033[1;32m'
blue = '\033[1;34m'
reinitColor = '\033[1;m'


def ask(msg, defaultAnswer):
    ret = defaultAnswer

    if defaultAnswer == True:
        yesno = 'Y/n'
    else:
        yesno = 'y/N'
    question = msg + ' (' + yesno + '): '

    # unattended
    if yes or no or default:
        if yes:
            ret = True
        if no:
            ret = False

        if ret:
            answer = 'y'
        else:
            answer = 'n'

        logInfo(question + answer)

    # ask the user
    else:
        answer = raw_input(question)
        if answer == '':
            pass
        elif answer == 'y' or answer == 'Y':
            ret = True
        elif answer == 'n' or answer == 'N':
            ret = False
        else:
            logWarning('answer not understood (' + answer + '). Abort.')
            exit(1)

    return ret
            



def callOpsiAdmin(cmd):
    logDebug('launch command: ' + cmd)
    p = Popen(cmd.split(), stdout=PIPE, stderr=PIPE)

    out = p.communicate()
    if p.returncode != 0:
        logWarning('command "' + cmd + '" failed with status ' + str(p.returncode) + ' !')

    if p.returncode != 0 or debug:
        if out[0] != '':
            logDebug('command stdout:\n' + str(out[0]))
        if out[1] != '':
            logDebug('command stderr:\n' + str(out[1]))

    return out, p.returncode


# opsi return python var that are not defined
defVar = "false = False; true = True; null = None\n"

# return the var of the opsi cmd
def callOpsiAdminVar(cmd):
    out, returncode = callOpsiAdmin(cmd)
    exec(defVar + "var=" + out[0])
    return var


# store hosts results
# { 'machine1.domain.lan': {'lastSeen': '2012-01-10 08:42:42', 'up': 'off'}, ... }
hostsBuf = None

# getHosts() must be called instead of using hostsBuf in order to avoid calling slow opsi-admin too often
def getHosts():
    global hostsBuf

    if hostsBuf == None:
        hostsBuf = {}
        cmd = "opsi-admin -d method host_getObjects"
        hosts_tmp = callOpsiAdminVar(cmd)

        for h in hosts_tmp:
            if h['type'] == 'OpsiClient':
                hostsBuf[h['id']] = { 'lastSeen' : h['lastSeen'], 'up' : 'off', 'hardwareAddress' : h['hardwareAddress'], 'ip' : h['ipAddress'] }

        if ping:
            pingHosts(hostsBuf.keys())

    return hostsBuf


# set 'up' attribute to each pinggable host
def pingHosts(hosts):
    cmd = "oping -c 1 " + ' '.join(hosts)
    logDebug(cmd)
    
    try:
        out, returncode = callOpsiAdmin(cmd)
    except:
        logWarning('"oping" command need to be installed!')
        logDebug("command launched: " + cmd)
        return

    for line in out[0].splitlines():
        m = re.match('56 bytes from ([a-zA-Z0-9-.]+) .*icmp_seq=1', line)
        # host is up
        if m != None:
            hostname = m.group(1).lower()
            if getHosts().has_key(hostname):
                getHosts()[hostname]['up'] = 'on'


def deltaTimeToNow(timeStr):
        t =  datetime.datetime.fromtimestamp(time.mktime(time.strptime(timeStr, '%Y-%m-%d %H:%M:%S')))
        now =  datetime.datetime.now()
        diffDate = t - now
        if diffDate.days < 0:
            diffDateStr = diffDate.days * -1
        else:
            diffDateStr = 0

        return diffDateStr


def printHostHeader():
    if ping == True:
        print 'host'.ljust(32) + 'ip'.ljust(15) + 'up'.ljust(6) + 'lastSeen'.ljust(22) + 'since'.ljust(8) + 'mac'
    else:
        print 'host'.ljust(32) + 'ip'.ljust(15) + 'lastSeen'.ljust(22) + 'since'.ljust(8) + 'mac'


def printHostRow(host, pad = 0):
    if host.endswith(defaultDomain):
        hostStr = host[0 : len(host) - len(defaultDomain)]
    else:
        hostStr = host
    hostStr = hostStr.ljust(32)

    ip = getHosts()[host]['ip']

    upStr = ''
    if ping:
        up = getHosts()[host]['up']
        upStr = up.ljust(6)
        if up == 'on':
            upStr = blue + upStr + reinitColor

    lastSeen = getHosts()[host]['lastSeen']
    nbOfDay = deltaTimeToNow(lastSeen)
    lastSeenStr = lastSeen.ljust(22) + (str(nbOfDay).ljust(3) + 'd').ljust(8)
    if nbOfDay > errorSince:
        lastSeenStr = red + lastSeenStr + reinitColor
        hostStr = red + hostStr + reinitColor
    elif nbOfDay > warnSince:
        lastSeenStr = grey + lastSeenStr + reinitColor
        hostStr = grey + hostStr + reinitColor

    mac = getHosts()[host]['hardwareAddress']

    print ''.ljust(pad) + hostStr + ip.ljust(15) + upStr + lastSeenStr + str(mac)



def printHosts():
    printHostHeader()
    print ''
    for k in sorted(getHosts()):
        printHostRow(k)


def actionHosts(call, hosts):
    msg = ''
    action = call[0]
    if action == 'popup' and len(call) > 1:
        msg = ' "' + call[1] + '"'

    if hosts == []:
        hosts = getHosts().keys()
    else:
        hosts = checkHosts(addDefaultDomain(hosts))

    # don't understand how use hostsId with opsi :-(
    #hostsId = '\'["'
    #hostsId = hostsId + '", "'.join(hosts) + '"]\''

    if len(hosts) == 0:
        logWarning('No valid hosts found!. Abort.')
        return

    answer = ask('Launch ' + action + msg + ' on hosts ' + str(hosts) + '?', True)
    if not answer:
        logInfo('Abort.')
        return

    for h in hosts:
        if action == 'reboot':
            cmd = "opsi-admin -d method hostControl_reboot " + h
            callOpsiAdmin(cmd)

        elif action == 'shutdown':
            cmd = "opsi-admin -d method hostControl_shutdown " + h
            callOpsiAdmin(cmd)

        elif action == 'wakeup':
            cmd = "opsi-admin -d method powerOnHost " + h
            callOpsiAdmin(cmd)

        elif action == 'delete':
            cmd = "opsi-admin -d method deleteClient " + h
            callOpsiAdmin(cmd)

        elif action == 'fire':
            cmd = 'opsi-admin -d method hostControl_fireEvent "on_demand" ' + h
            callOpsiAdmin(cmd)

        elif action == 'popup' and msg != '':
            cmd = 'opsi-admin -d method hostControl_showPopup ' + msg + ' ' + h
            callOpsiAdmin(cmd)

        else:
            parser.print_help()
            exit(1)

    logInfo('Done.')


# available request with OPSI
requestsName = ['setup', 'uninstall', 'once', 'update', 'custom', 'userLogin', 'always', 'none']

# store packages results
# {'opsi-client-agent': {'packageVersion': '12', 'productVersion': '4.0.1'}, ... }
packagesBuf = None

def getPackages():
    global packagesBuf

    if packagesBuf == None:
        packagesBuf = {}
        cmd = "opsi-admin -d method product_getObjects"
        packages_tmp = callOpsiAdminVar(cmd)

        for p in packages_tmp:
            if p['type'] == 'LocalbootProduct':
                packagesBuf[p['id']] = { 'packageVersion' : p['packageVersion'], 
                                         'productVersion' : p['productVersion'],
                                         'requests' : [] }
                for request in requestsName:
                    if request != 'none' and p[request + 'Script'] != "":
                        packagesBuf[p['id']]['requests'].append(request)

    return packagesBuf


# store packages possible properties 
# {"config-win": [{'propertyId': "flag_rdp",  'defaultValues' : "admins",  'possibleValues' : ["admins","off","users"]}, ... ], ... }
packagesPropertiesBuf = None

def getPackagesProperties():
    global packagesPropertiesBuf

    if packagesPropertiesBuf == None:
        packagesPropertiesBuf = {}
        cmd = "opsi-admin -d method productProperty_getObjects"
        props = callOpsiAdminVar(cmd)

        for prop in props:
            #if prop['type'] == 'ProductPropertyState':
            content = { 'propertyId' : prop['propertyId'], 'defaultValues' : prop['defaultValues'], 'possibleValues' : prop['possibleValues'] }
            if packagesPropertiesBuf.has_key(prop['productId']):
                packagesPropertiesBuf[prop['productId']].append(content)
            else:
                packagesPropertiesBuf[prop['productId']] = [content]

    return packagesPropertiesBuf


# store packages properties state
# {"pc.domain.lan" : {"config-win" : [ {'propertyId' : "flag_rdp",  'values' : ["users" ]}, ... ], ... }, ... }
packagesPropertiesStateBuf = None

def getPackagesPropertiesState():
    global packagesPropertiesStateBuf

    if packagesPropertiesStateBuf == None:
        packagesPropertiesStateBuf = {}
        cmd = "opsi-admin -d method productPropertyState_getObjects"
        props = callOpsiAdminVar(cmd)

        for prop in props:
            if prop['type'] == 'ProductPropertyState':
                content = { 'propertyId' : prop['propertyId'], 'values' : prop['values'] }
                if packagesPropertiesStateBuf.has_key(prop['objectId']):
                    if packagesPropertiesStateBuf[prop['objectId']].has_key(prop['productId']):
                        packagesPropertiesStateBuf[prop['objectId']][prop['productId']].append(content)
                    else:
                        packagesPropertiesStateBuf[prop['objectId']][prop['productId']] = [content]
                else:
                    packagesPropertiesStateBuf[prop['objectId']] = { prop['productId'] : [content] }

    return packagesPropertiesStateBuf



# store packages dependancies 
# {"join_domain": [{'requiredProductId': "samba-prerequisite",  'productAction' : "setup"}, ... ], ... }
packagesDependenciesBuf = None

def getPackagesDependencies():
    global packagesDependenciesBuf

    if packagesDependenciesBuf == None:
        packagesDependenciesBuf = {}
        cmd = "opsi-admin -d method productDependency_getObjects"
        deps = callOpsiAdminVar(cmd)

        for dep in deps:
            #if dep['type'] == 'ProductPropertyState':
            content = { 'requiredProductId' : dep['requiredProductId'], 'productAction' : dep['productAction'] }
            if packagesDependenciesBuf.has_key(dep['productId']):
                packagesDependenciesBuf[dep['productId']].append(content)
            else:
                packagesDependenciesBuf[dep['productId']] = [content]

    return packagesDependenciesBuf


def printPackageHeader():
    print 'package'.ljust(32) + 'version'.ljust(16) + 'available_requests'.ljust(20)

def printPackageRow(package, pad = 0):
    allowedScripts = ''
    for request in getPackages()[package]['requests']:
        allowedScripts += request + ' '
    print ''.ljust(pad) + package.ljust(32) + green + getProductVersionFull(getPackages()[package]).ljust(16) + reinitColor + grey + allowedScripts.ljust(20) + reinitColor


def printPackageProperties(package, pad = 0):
    if getPackagesProperties().has_key(package):
        for prop in getPackagesProperties()[package]:
            print ''.ljust(pad) + grey + prop['propertyId'].ljust(32-pad) + str(prop['possibleValues']).ljust(16) + '  d:' + str(prop['defaultValues']) + reinitColor
    

def printPackageDependencies(package, pad = 0):
    if getPackagesDependencies().has_key(package):
        for dep in getPackagesDependencies()[package]:
          print ''.ljust(pad) + 'depends: ' + dep['requiredProductId'] + ' (' + str(dep['productAction']) + ')'
    

# return last product version
# if host != None, return installed product version of the package on the host
# on any error, return None
def getPackageVersion(package, host = None):
    if host == None:
        if getPackages().has_key(package):
            return getProductVersionFull(getPackages()[package])
        else:
            return None
    else:
        for pv in getPackagesFromHosts():
            if pv['clientId'] == host and \
                    pv['productId'] == package and \
                    pv['productType'] == 'LocalbootProduct' and \
                    pv['installationStatus'] == 'installed':
                        return getProductVersionFull(pv)
        return None
        

def printPackages():
    printPackageHeader()
    print ''
    for package in sorted(getPackages()):
        printPackageRow(package)
        printPackageDependencies(package, 2)
        printPackageProperties(package, 2)


# store package host association
# [{'clientId': 'machine1.domain.lan', 'productId': 'opsi-winst', 'productVersion': '4.11.1.1', 'actionRequest': 'none', 'installationStatus': 'installed', 'productType': 'LocalbootProduct', 'packageVersion': '2', 'type': 'ProductOnClient'}, ...]
packagesHostsBuf = None

def getPackagesFromHosts():
    global packagesHostsBuf

    if packagesHostsBuf == None:
        packagesHostsBuf = []
        cmd = "opsi-admin -d method productOnClient_getObjects"
        packages_tmp = callOpsiAdminVar(cmd)

        for p in packages_tmp:
            packagesHostsBuf.append(p)

    return packagesHostsBuf


def addDefaultDomain(hosts):
    hostsFqdn = []

    for h in hosts:
        if h.endswith(defaultDomain):
            hostsFqdn.append(h)
        else:
            hostsFqdn.append(h + defaultDomain)

    return hostsFqdn


def checkHosts(hosts):
    goodHosts = []
    for h in hosts:
        if not getHosts().has_key(h):
            logWarning('host ' + h + ' does not exists!')
        else:
            goodHosts.append(h)

    return goodHosts


def printPackageHostHeader():
    print "  " + 'package'.ljust(30) + \
            'version'.ljust(10) + \
            'action'.ljust(10)
#            'status'.ljust(16) + \
#            'last'.ljust(16)


def getProductVersionFull(productDict):
    productVersion = productDict['productVersion']
    packageVersion = productDict['packageVersion']
    if productVersion == None:
        productVersion = ''
    if packageVersion == None:
        packageVersion = ''

    return productVersion + '-' + packageVersion


def printPackagesFromHosts(hosts, packages):
        hosts = checkHosts(hosts)

        for k in sorted(getHosts()):
            if hosts == [] or k in hosts:
                printHostRow(k)
                hostPackageHeaderSent = 0

                for pv in getPackagesFromHosts():
                    if pv['clientId'] == k and pv['productType'] == 'LocalbootProduct':
                        # do not print not selected packages
                        if packages and len(packages) > 0 and pv['productId'] not in packages:
                            continue
                        logDebug(str(pv['productId']) + ' ' + str(pv['productVersion']) + ' ' + \
                                str(pv['actionRequest']) + ' ' + str(pv['installationStatus']) + ' ' + \
                                str(pv['lastAction']) + ' '  + str(pv['actionResult']))
                        if pv['installationStatus'] == 'installed' or \
                                pv['actionRequest'] != 'none' or \
                                pv['actionResult'] == 'failed':
                            # colorize
                            actionStr = ''
                            if packageUpToDate(pv['clientId'], pv['productId']):
                                versionColor = green
                                if pv['actionRequest'] != 'none':
                                    actionStr = actionStr + pv['actionRequest']
                            else:
                                versionColor = red
                                if pv['actionRequest'] != 'none':
                                    actionStr = green + pv['actionRequest']
                                else:
                                    actionStr = red + 'nosetup'
                                actionStr =  actionStr.ljust(16) + reinitColor
                            if pv['actionResult'] == 'failed':
                                actionStr = actionStr + red + ' Last action "' + pv['lastAction'] + '" failed!' + reinitColor

                            print '  ' + pv['productId'].ljust(30) + \
                                    versionColor + getProductVersionFull(pv).ljust(16) + reinitColor + \
                                    actionStr
                            if getPackagesPropertiesState().has_key(k) and getPackagesPropertiesState()[k].has_key(pv['productId']):
                                packageProps = getPackagesPropertiesState()[k][pv['productId']]
                                for packageProp in packageProps:
                                    print '    ' + grey + str(packageProp['propertyId']) + ': ' + str(packageProp['values']) + reinitColor

                print ''


# store groups results
# {'HostGroup': {'domain.lan': {'description': 'Every pc of domain.lan', 'parentGroupId': None}}, ...}
groupsBuf = None

# store group children
# {'HostGroup': {'root': ['domain.lan']}, 
#  'ProductGroup': {'root': [base, ...], 'base': ['bureautique'], ...}}
childrenGroupsBuf = None

def getGroups():
    global groupsBuf
    global childrenGroupsBuf

    if groupsBuf == None:
        groupsBuf = {}
        childrenGroupsBuf = {}
        cmd = "opsi-admin -d method group_getObjects"
        groups_tmp = callOpsiAdminVar(cmd)

        groupsBuf[ 'ProductGroup' ] = {}
        groupsBuf[ 'HostGroup' ] = {}
        childrenGroupsBuf[ 'ProductGroup' ] = {}
        childrenGroupsBuf[ 'HostGroup' ] = {}
        for g in groups_tmp:
            # fills in groupsBuf
            groupsBuf[ g['type'] ][ g['id'] ] = {'description' : g['description'], 'parentGroupId' : g['parentGroupId']}

            # fills in childrenGroupsBuf
            if g['parentGroupId'] == None:
                parentGroup = 'root'
            else:
                parentGroup = g['parentGroupId'] 
            if childrenGroupsBuf[ g['type'] ].has_key( parentGroup ):
                childrenGroupsBuf[ g['type'] ][ parentGroup ].append(g['id'])
            else:
                childrenGroupsBuf[ g['type'] ][ parentGroup ] = [g['id']]

    return groupsBuf


def getChildrenGroups():
    global childrenGroupsBuf

    if childrenGroupsBuf == None:
        getGroups()

    return childrenGroupsBuf


# store object to group association, improve :  {'groupType': {'groupid' : [...]}, ...}
# [{'groupType': 'ProductGroup', 'groupId': 'base', 'objectId': 'firefox'},
#  {'groupType': 'HostGroup', 'groupId': 'base', 'objectId': 'pc1.domain.lan'}, ...]
objectsGroupsBuf = None

def getObjectsGroups():
    global objectsGroupsBuf

    if objectsGroupsBuf == None:
        objectsGroupsBuf = []
        cmd = "opsi-admin -d method objectToGroup_getObjects"
        objectsGroups_tmp = callOpsiAdminVar(cmd)

        for og in objectsGroups_tmp:
            objectsGroupsBuf.append(og)

    return objectsGroupsBuf


def getHostsOfGroup(group, recursive=True):
    return getObjectsOfGroup('HostGroup', group, recursive)


def getPackagesOfGroup(group, recursive=True):
    return getObjectsOfGroup('ProductGroup', group, recursive)


def getObjectsOfGroup(groupType, group, recursive=True):
    """ 
    get objects of given group
    if recursive = True: give objects that belongs to children group too.
    if group = None, give objects that does not belong to a group
    """
    # print objects of the group
    objects = []

    if group != None:

        for og in getObjectsGroups():
            if og['groupType'] == groupType and og['groupId'] == group:
                objects.append(og['objectId'])

        # print children group
        if recursive and getChildrenGroups()[groupType].has_key(group):
            objects += getObjectsOfGroup(groupType, group, recursive)

    else:

        if groupType == 'HostGroup':
            allObjects = getHosts().keys()
        else:
            allObjects = getPackages().keys()

        for o in allObjects:
            objectHasGroup = False
            for og in getObjectsGroups():
                if og['groupType'] == groupType and og['objectId'] == o:
                    objectHasGroup = True
                    break
            if not objectHasGroup:
                objects.append(o)

    return objects


def printGroupsType(groupType, groups, level = 0):
    for group in groups:
        # print group
        print ''.ljust(level) + blue + group.ljust(24 - level) + getGroups()[groupType][group]['description'] + reinitColor

        # print objects of the group
        objects = getObjectsOfGroup(groupType, group, recursive=False)
        for o in sorted(objects):
            if groupType == 'HostGroup':
                printHostRow(o, 24)
            else:
                printPackageRow(o, 24)
        print ''

        # print children group
        if getChildrenGroups()[groupType].has_key(group):
            printGroupsType(groupType, getChildrenGroups()[groupType][group], level + 2)


def printGroups(groupType):
    # print object that belong to a group
    printGroupsType(groupType, sorted(getChildrenGroups()[groupType]['root']))

    # print lonely objects
    nogroupObjects = getObjectsOfGroup(groupType, None, recursive=False)
    if nogroupObjects != []:
        print blue + 'nogroup'.ljust(24) + 'Objects that does not belong to a group.' + reinitColor
        for o in sorted(nogroupObjects):
            if groupType == 'HostGroup':
                printHostRow(o, 24)
            else:
                printPackageRow(o, 24)


def requestPackageDependencies(installDic, package, host):
    # search recursively for dependencies and add them for install if needed
    # return True if a dep has to be install
    has_deps = False
    if getPackagesDependencies().has_key(package):
        for dep in getPackagesDependencies()[package]:
            dep_package = dep['requiredProductId']
            dep_action = dep['productAction']
            if not installationStatusMatchRequest(dep_action, host, dep_package):
                if not requestAlreadyRequested(dep_action, host, dep_package):
                    logWarning(package + ' need dependency ' + dep_package + ' (' + dep_action + ')')
                    installDic[host].append([dep_package, dep_action])
                    requestPackageDependencies(installDic, dep_package, host)
                    has_deps = True
    return has_deps

 
def requestPackage(request, packages, hosts, fire):

    if hosts == []:
        okHosts = getHosts().keys()
    else:
        okHosts = checkHosts(addDefaultDomain(hosts))

    warn = False

    okPackages = []
    for package in packages:
        if package not in getPackages():
            logWarning("Package " + package + " does not exists!")
            warn = True
        else:
            okPackages.append(package)

    installDic = {}
    for host in okHosts:
        installDic[host] = []
        for package in okPackages:
            warnDetails = None
            adjustedRequest = request
            if requestAlreadyRequested(request, host, package):
                warnDetails = 'request already required'
                adjustedRequest = None
            elif installationStatusMatchRequest(request, host, package):
                if request == 'setup':
                    if packageUpToDate(host, package):
                        warnDetails = 'package already up to date'
                        if requestAlreadyRequested('none', host, package):
                            adjustedRequest = None
                        else:
                            adjustedRequest = 'none'
                elif request == 'uninstall':
                    warnDetails = 'status already match request'
                    if requestAlreadyRequested('none', host, package):
                        adjustedRequest = None
                    else:
                        # reset previous request
                        adjustedRequest = 'none'
            
            if requestPackageDependencies(installDic, package, host) == True:
                warn = True

            if warnDetails != None:
                warnMsg = 'Package ' + package
                warnMsg += ' does not need request "' + request
                warnMsg += '" to be done on host ' + host.ljust(32)
                warnMsg += ' (' + warnDetails + ')'
                if force:
                    warnMsg += ' : request asked will still be done'
                    installDic[host].append([package, request])
                elif adjustedRequest == 'none':
                    installDic[host].append([package, adjustedRequest])
                    warnMsg += ' : request set to none'
                else:
                    warnMsg += ' : request skipped'
                logWarning(warnMsg)
                warn = True
            else:
                installDic[host].append([package, request])

    # log
    if warn and not force:
        # print detailed installation status
        for host in installDic.keys():
            if len(installDic[host]) > 0:
                logInfo('  request on host ' + host.ljust(32) + ' packages ' + str(installDic[host]))
    else:
        # print factorized installation status
        logInfo('  request "' + request + '" on hosts ' + str(okHosts)  + '\n        packages ' + str(okPackages))

    nbPackagesToInstall = 0
    for host in installDic.keys():
        nbPackagesToInstall += len(installDic[host])
    if nbPackagesToInstall == 0:
        logInfo('No package to request "' + request + '".')
    else:
        if warn:
            answer = ask('Errors appear, launch request anyway?', defaultAnswer=False)
        else:
            answer = ask('Launch request?', defaultAnswer=True)

        if answer:
            for host in installDic.keys():
                for package, packageRequest in installDic[host]:
                    cmd = 'opsi-admin -d method setProductActionRequest ' + package + ' ' + host + ' ' + packageRequest
                    callOpsiAdmin(cmd)
                    requestMsg = '  request "' + packageRequest + '" on ' + package + ' on host ' + host
                    # do it now
                    if packageRequest != 'none' and fire:
                        cmd = 'opsi-admin -d method hostControl_fireEvent "on_demand" ' + host
                        callOpsiAdmin(cmd)
                        requestMsg += ' now'
                    logInfo(requestMsg)
        else:
            logInfo('Abort.')
            return


# return True if the installed package on the host is already in the last version
def packageUpToDate(host, package):
    return getPackageVersion(package) == getPackageVersion(package, host)


def requestAlreadyRequested(request, host, package):
    alreadyRequired = False

    for pv in getPackagesFromHosts():
        if pv['clientId'] == host and \
                pv['productId'] == package and \
                pv['productType'] == 'LocalbootProduct' and \
                pv['actionRequest'] == request:
            alreadyRequired = True
            break
    return alreadyRequired


def installationStatusMatchRequest(request, host, package):
    # only setup and installed has an installation status(?)
    if request != 'setup' and request != 'uninstall':
        return False  

    installed = False

    for pv in getPackagesFromHosts():
        if pv['clientId'] == host and \
                pv['productId'] == package and \
                pv['productType'] == 'LocalbootProduct' and \
                pv['installationStatus'] == 'installed':
            installed = True
            break

    if request == 'setup':
        return installed
    else:
        return not installed


# create/update a group
def addGroup(groupType, group, description, parent = None):
    if parent != None and parent not in getGroups()[groupType]:
        logWarning('parent group ' + parent + ' does not exists. abort.')
        return 

    already_exists = False
    for g in getGroups()[groupType]:
        if g == group:
            already_exists = True
            break

    if already_exists:
        logInfo('group ' + group + ' already exists')

    if not already_exists or force:
        
        if ask('create group ' + group + '?', defaultAnswer=True):
            cmd = 'opsi-admin -d method group_create' + groupType + ' ' + group + ' ' + '"' + description + '"'
            callOpsiAdmin(cmd)
            
            if parent != None:
                parentStr = '"' + parent + '"'
            else:
                parentStr = 'null'
            cmd = 'opsi-admin -d method group_updateObject \'{"ident" : "' + group + '", "description" : "' + description + \
                    '", "parentGroupId" : ' + parentStr + ', "type" : "' + groupType + '", "id" : "' + group + '"}\''
            callOpsiAdmin(cmd)

            logInfo('group ' + group + ' added')

        else:
            logInfo('Abort.')
            return

        

# opsi cannot have same name in product group and host group
# so we can guess the group type from the group name
def getGroupType(group):
    groupType = None

    if group in getGroups()['HostGroup']:
        groupType = 'HostGroup'
    elif group in getGroups()['ProductGroup']:
        groupType = 'ProductGroup'

    return groupType


# add objects to a group
def addGroupObjects(group, objects = []):
    groupType = getGroupType(group)
    if groupType == None:
        logWarning('no group ' + group + ' found. abort.')
        return
    elif groupType == 'HostGroup':
        if len(objects) == 0:
            objects = getHosts().keys()
        objects = addDefaultDomain(objects)
    elif groupType == 'ProductGroup' and len(objects) == 0:
        objects = getPackages().keys()

    ok_objects = []
    warn = False
    for o in objects:
        if (groupType == 'HostGroup' and o in getHosts()) or \
           (groupType == 'ProductGroup' and o in getPackages()):
            ok_objects.append(o)
        else:
            logWarning('object ' + o + ' does not exists. Removed from wanted objects.')
    if warn:
        answer = ask('Errors appear. Add ' + str(ok_objects) + ' to group ' + group + ' anyway?', defaultAnswer=False)
    else:
        answer = ask('Add ' + str(ok_objects) + ' to group ' + group + '?', defaultAnswer=True)
    if answer:
        for o in ok_objects:
            cmd = 'opsi-admin -d method objectToGroup_create ' + groupType + ' ' + group + ' ' + o
            callOpsiAdmin(cmd)
            logInfo('object ' + o + ' added to group ' + group)

    else:
        logInfo('Abort.')
        return
    #print ''
    #if info:
    #    printGroups(groupType) 


# if no objects given : remove the group and every objects
# if objects given : remove only the objects, not the group
def deleteGroup(group, objects = []):
    groupType = getGroupType(group)
    if groupType == None:
        logWarning('no group ' + group + ' found. abort.')
        return
    elif groupType == 'HostGroup':
        objects = addDefaultDomain(objects)

    if objects != []:
        answer = ask('Delete ' + str(objects) + ' from group ' + group + '?', 
                     defaultAnswer=True)
        if answer:
            for og in objects:
                cmd = 'opsi-admin -d method objectToGroup_delete ' + groupType + ' ' + group + ' ' + og
                callOpsiAdmin(cmd)
                logInfo('  object ' + og + ' of ' + groupType + ' ' + group + ' deleted')
        else:
            logInfo('Abort.')
            return


    else:
        answer = ask('Delete group ' + group + '?', defaultAnswer=True)
        if answer:
            # (opsi delete group objects on group deletion)
            cmd = 'opsi-admin -d method group_delete ' + group
            callOpsiAdmin(cmd)
            logInfo(groupType + ' ' + group + ' deleted')
        else:
            logInfo('Abort.')
            return



# speed test
def testBuf(trucs):
    getGroups()
    getHosts()
    getObjectsGroups()
    getPackages()
    getPackagesFromHosts()


# store help message to replace
replaceHelp = {}

# print help message
# replace help message if argument present in replaceHelp dict
def printHelp(parser):
    helpStr = parser.format_help()
    helpLines = helpStr.splitlines()
    for r in replaceHelp.keys():
        for i in range(len(helpLines)):
            if r in helpLines[i]:
                # do not replace summary usage
                if helpLines[i].strip()[0] == '[':
                    continue
                # keep np space
                nbSpace = 0
                for c in helpLines[i]:
                    if c in ' \t':
                        nbSpace += 1
                    else:
                        break
                # replace argument explanation
                helpLines[i] = helpLines[i][0:nbSpace] + replaceHelp[r]

    print '\n'.join(helpLines)


def getSelectedHosts(hosts, host_groups):
    selected = []
    if hosts != None:
        selected = hosts
    if host_groups != None:
        for host_group in host_groups:
            selected += getHostsOfGroup(host_group)
    return selected

def getSelectedPackages(packages, package_groups):
    selected = []
    if packages != None:
        selected = packages
    if package_groups != None:
        for package_group in package_groups:
            selected += getPackagesOfGroup(package_group)
    return selected

examples = """
=========
Examples:

# list every packages
opsi-pkg -l -p

# list every hosts and show their up status
opsi-pkg -l -h --ping

-

# list installed packages on every computer
opsi-pkg -l -p -h

# list installed packages on tata computer
opsi-pkg -l -p -h tata

# list package font on every computer
opsi-pkg -l -p font -h

# list packages  font and office  on  tata and titi  computers
opsi-pkg -l -p font office -h tata titi

-

# list host groups
opsi-pkg -l -hg 

# create an host group "pc" without asking it
opsi-pkg -a -hg pc 'all desktop pc' -y

# create an host group "chicago" and wich has for parent 'pc' host group
opsi-pkg -a -hg chicago 'chicago network' pc

# add to host group "pc" the machines "tata, toto, and titi"
opsi-pkg -a -hg pc -h tata toto titi

# add to host group "pc" every machines of OPSI
opsi-pkg -a -hg pc -h

# delete host titi from host group "pc"
opsi-pkg -d -hg pc -h titi

# delete host group "pc"
opsi-pkg -d -hg pc

# list package groups
opsi-pkg -l -pg 

# create a package group "base" 
opsi-pkg -a -pg base 'default program'

-

# call 'hello' popup on each computer that belongs to 'pc' host group
opsi-pkg -c popup 'hello' -hg pc

# delete host tutu from OPSI
opsi-pkg -c delete -h tutu

-

# install firefox on computer tata
opsi-pkg -r setup -p firefox -h tata

# install firefox on computers that belongs to 'pc' host group even if it is 
# already install and up-to-date and push the setup now
opsi-pkg -s -p firefox -hg pc --force --fire

# install firefox on computers that belongs to 'pc' host group
opsi-pkg -s -p firefox -hg pc

# install base packages on computers that belongs to 'pc' host group
opsi-pkg -s -pg base -hg pc

# uninstall firefox on computers that belongs to 'pc' host group
opsi-pkg -u -p firefox -hg pc

# reset request on package firefox of the computer tata
opsi-pkg -r none -p firefox -h tata


======
Notes:
  OPSI cannot have same name in product group and host group

"""

if __name__ == '__main__':

    parser = argparse.ArgumentParser(description="simple opsi package manager interface", 
        epilog=examples, 
        add_help=False, 
        formatter_class=argparse.RawTextHelpFormatter)
    parser.add_argument('--help', action='store_const', const=[], 
                        help='show this help message and exit')

    parser.add_argument('--quiet', '-q', action='store_const', const=[], 
                        help='do not print info log')
    parser.add_argument('--verbose', '-v', action='store_const', const=[], 
                        help='print more log')
    parser.add_argument('--debug', action='store_const', const=[], 
                        help='print even more log')

    parser.add_argument('--default', action='store_const', const=[], 
                        help='answer default answer to every question')
    parser.add_argument('--yes', '-y', action='store_const', const=[], 
                        help='answer yes to every question')
    parser.add_argument('--no', action='store_const', const=[], 
                        help='answer no to every question')

    parser.add_argument('--force', '-f', action='store_const', const=[],
                        help='force doing action that does seems to be needed. '
                        'e.g. install a package on a host '
                        'that has already the last package installed.')

    parser.add_argument('--fire', action='store_const', const=[],
                        help='Fire the request now (i.e. push the request to the hosts).')

    parser.add_argument('--ping', action='store_const', const=[], 
                        help='show hosts status (on/off)')

    parser.add_argument('--list', '-l', action='store_const', const=[], 
        help='List something. Combine it with: -h, -p, -h -p, -hg, -pg.')
    parser.add_argument('--add', '-a', action='store_const', const=[], 
        help='Add something. Combine it with: -hg, -pg, -hg -h, -pg -p')
    parser.add_argument('--delete', '-d', action='store_const', const=[], 
        help='Delete something. Combine it with: -hg, -pg, -hg -h, -pg -p.')
    parser.add_argument('--call', '-c', nargs='*', help='Launch action (push it to hosts). Combine it with: -h, -hg')
    replaceHelp['--call'] = '--call, -c {shutdown, reboot, wakeup, delete, fire, popup "message"}'
    parser.add_argument('--request', '-r', choices=requestsName,  
        help='Set next action (pulled by hosts) on given hosts and packages. Combine it with: -h, -hg, -p, -pg, --fire.')
    replaceHelp['--request'] = '--request, -r {setup, uninstall, once, update, custom, userLogin, always}'
    parser.add_argument('--setup', '-s', action='store_const', const=[],  
        help='Shortcut for "-request setup".')
    parser.add_argument('--uninstall', '-u', action='store_const', const=[],  
        help='Shortcut for "-request uninstall".')

    parser.add_argument('--host', '-h', nargs='*', help='Do sthg with host. No host given means every hosts.)')
    parser.add_argument('--package', '-p', nargs='*', help='Do sthg with package. No package given means every packages.')
    parser.add_argument('--host-group', '-hg', nargs='*', help='Do sthg with host group')
    parser.add_argument('--package-group', '-pg', nargs='*', help='Do sthg with package group')


    args = parser.parse_args()

    # print help if launched without args
    if string.find(str(args), '[') == -1 and string.find(str(args), "='") == -1:
        printHelp(parser)

    # print help if asked
    if args.help != None:
        printHelp(parser)


    if args.debug != None:
        debug = True
        logDebug("args: " + str(args))

    if args.verbose != None:
        verbose = True

    if args.quiet != None:
        info = False


    if args.default != None:
        default = True
    if args.yes != None:
        yes = True
    if args.no != None:
        no = True

    if args.force != None:
        force = True

    if args.ping != None:
        ping = True


    cmd = False

    if args.list != None:
        cmd = True
        what = False
        if args.host != None and args.package != None:
            what = True
            printPackagesFromHosts(addDefaultDomain(args.host), args.package) 
        else:
            if args.host != None:
                what = True
                printHosts()
            if args.package != None:
                what = True
                printPackages()
        if args.host_group != None:
            what = True
            printGroups('HostGroup')
        if args.package_group != None:
            what = True
            printGroups('ProductGroup')
        if not what:
            logWarning("You did not specify what to list!")


    if args.add != None:
        cmd = True
        what = False
        if args.host_group != None and len(args.host_group) > 0:
            what = True
            if args.host != None:
                addGroupObjects(args.host_group[0], args.host)
            else:
                description = args.host_group[1] if len(args.host_group) > 1 else ''
                parent = args.host_group[2] if len(args.host_group) > 2 else None
                addGroup('HostGroup', args.host_group[0], description, parent)
            #print ''
            #printGroups('HostGroup')

        if args.package_group != None and len(args.package_group) > 0:
            what = True
            if args.package != None:
                addGroupObjects(args.package_group[0], args.package)
            else:
                description = args.package_group[1] if len(args.package_group) > 1 else ''
                parent = args.package_group[2] if len(args.package_group) > 2 else None
                addGroup('ProductGroup', args.package_group[0], description, parent)
            #print ''
            #printGroups('ProductGroup')
         
        if not what:
            logWarning("You did not specify what to add!")


    if args.delete != None:
        cmd = True
        what = False

        if args.host_group != None:
            what = True
            hosts = []
            if args.host != None:
                hosts = args.host
            for group in args.host_group:
                deleteGroup(group, hosts)
            # todo: flush cache before printing groups
            #printGroups('HostGroup')

        if args.package_group != None:
            what = True
            packages = []
            if args.package != None:
                packages = args.package
            for group in args.package_group:
                deleteGroup(group, packages)
            # todo: flush cache before printing groups
            #printGroups('ProductGroup')

        if not what:
            logWarning("You did not specify what to delete!")


    if args.call != None:
        cmd = True
        what = False
        if len(args.call) > 0:
            what = True
            hosts = getSelectedHosts(args.host, args.host_group)
            # todo: add host of host group
            actionHosts(args.call, hosts)

        if not what:
            logWarning("You did not specify what to call!")


    if args.request != None or args.setup != None or args.uninstall != None:
        cmd = True
        what = False

        request = None
        if args.request != None and args.request in requestsName:
            request = args.request
        elif args.setup != None:
            request = 'setup'
        elif args.uninstall != None:
            request = 'uninstall'
            
        if request != None:
            hosts = getSelectedHosts(args.host, args.host_group)
            packages = getSelectedPackages(args.package, args.package_group)

            if len(packages) > 0:
                requestPackage(request, packages, hosts, args.fire != None)
            else:
                logWarning("You did not specify packages for request " + request + "!")
        else:
            logWarning("request not/wrongly specified!")



    if not cmd:
        logWarning("You did not specify a command!")