#!/usr/bin/env python # -*- coding: utf-8 -*- """ospi-client: performs operation for opsi clients on opsi server via JSON-RPC.""" from __future__ import print_function __author__ = "Joerg Steffens" __copyright__ = "Copyright 2012-2021, dass IT GmbH" __license__ = "GPL" __version__ = "1.3" __email__ = "joerg.steffens@dass-it.de" #self.command("opsi-admin -d method host_createOpsiClient "+ \ #computername + " null " + "\\'"+description+"\\'" + \ #" \\'created by dassadmin\\' " + mac_address + " " + \ #ip_address) #self.command("opsi-admin -d method configState_create clientconfig.depot.id " + \ #computername + " " + depotName) import argparse from datetime import datetime, timedelta from dateutil import parser as dateparser import logging import os from pprint import pprint, pformat import sys import ssl import time from tinyrpc.protocols.jsonrpc import JSONRPCProtocol from tinyrpc.transports.http import HttpPostClientTransport from tinyrpc import RPCClient UrlJsonRpc="https://:@opsi:4447/rpc" HelpEpilog="WARNING: json-rpc is known to have problems with HTTP proxies. In case of problems, make sure, the environment variables http_proxy and/or https_proxy are *not* set. It might also be necessary to set variable PYTHONHTTPSVERIFY=0." class Nrpe: OK = 0 WARNING = 1 CRITICAL = 2 UNKNOWN = 3 def toString(self, status): if status == self.OK: return 'OK' elif status == self.WARNING: return 'WARNING' elif status == self.CRITICAL: return 'CRITICAL' else: return 'UNKNOWN' class Output(Nrpe): def __init__(self, nagios=False): self.nagios = nagios self.result = Nrpe.UNKNOWN self.message = '' def setStatus(self, status, message = None): self.result = status if message is not None: self.setMessage(message) def setMessage(self, string): self.message = string def finalize(self): if self.nagios: print('{0} - {1}'.format(self.toString(self.result), self.message)) sys.exit(self.result) else: print('{0} - {1}'.format(self.toString(self.result), self.message)) return (self.result == Nrpe.OK) class OpsiRpc: UrlJsonRpcDefault="https://opsi:4447/rpc" ProductAttributesCopy = ['actionRequest','actionResult','installationStatus','packageVersion','productVersion'] def __init__(self, urlJsonRpc = UrlJsonRpcDefault, debug=False, nagios=False): self.logger=logging.getLogger(__name__) self.debug=debug self.nagios=nagios self.urlJsonRpc=urlJsonRpc self.rpc = RPCClient(JSONRPCProtocol(), HttpPostClientTransport(self.urlJsonRpc, verify=False)).get_proxy() self.logger.debug( "initialized: " + self.urlJsonRpc ) self.logger.debug(dir(self.rpc)) def list(self): exceptions = [] if 'jsonrpc' in sys.modules: exceptions = [ jsonrpc.json.JSONDecodeException ] try: print( "\n".join( self.rpc.getClientIds_list() ) ) except exceptions as e: self.logger.debug( pformat(self.rpc.getClientIds_list()) ) self.logger.exception( "failed" ) return True def getClientsWithProduct( self, product ): return self.rpc.productOnClient_getObjects( [], { "productId": product, "installationStatus": "installed" } ) def getHardwareSerialNumber(self, src): serialNumbers = self.rpc.auditHardwareOnHost_getHashes( [], {"hostId":src, "hardwareClass":"BIOS", "serialNumber":"*"} ) serialNumbers += self.rpc.auditHardwareOnHost_getHashes( [], {"hostId":src, "hardwareClass":"CHASSIS", "serialNumber":"*"} ) result = set() for i in serialNumbers: if i['serialNumber']: result.add(i['serialNumber']) if len(result) == 1: return result.pop() elif len(result) > 1: self.logger.warning("found more then one serial number") return list(result) def listClients( self, product ): if product: for client in self.getClientsWithProduct( product ): print(client['clientId']) else: return self.list() return True def exists(self, src): return len( self.rpc.host_getObjects( [], {"id":src} ) ) == 1 def info(self, src): if not self.exists( src ): print("failed: opsi client", src, "does not exist") return False print(src + ":") host = self.rpc.host_getHashes( [], {"id":src} )[0] print(" IP:", host["ipAddress"]) print(" MAC:", host["hardwareAddress"]) print(" inventory:", host["inventoryNumber"]) print(" serial number:", self.getHardwareSerialNumber(src)) print(" last seen:", host["lastSeen"]) print(" notes:", host["notes"]) print(" depot:", self.clientGetDepot(src)) print(" products:") products = self.getProductOnClient( src, [] ) for i in products: print(" " + i['productId'] + ":") print(" " + i['installationStatus'], "(", end='') if i['actionRequest']: print(i['actionRequest'], end='') if i['actionProgress']: print(i['actionProgress'], end='') print(")") print(" ", end='') pprint( i, indent=8 ) return True def clean(self, src): if not self.exists( src ): return False products = self.rpc.productOnClient_getObjects( [], { 'clientId': src } ) self.rpc.productOnClient_deleteObjects( products ) products = self.rpc.productPropertyState_getObjects( [], { 'objectId': src } ) self.rpc.productPropertyState_deleteObjects( products ) if self.debug: pprint( self.getProductOnClient( src ) ) return True def getOpsiConfigserverId(self): # there should always be only one OpsiConfigserver opsiConfigservers=self.rpc.host_getHashes( [], { "type": "OpsiConfigserver" } ) try: return opsiConfigservers[0]['id'] except (KeyError,IndexError) as e: self.logger.error( "failed to retreive OpsiConfigserver" ) def createClient(self, name, opsiHostKey, description, notes, hardwareAddress, ipAddress): # self.rpc.host_createOpsiClient( name, opsiHostKey, description, notes, hardwareAddress, ipAddress ) self.updateClient( name, opsiHostKey, description, notes, None, hardwareAddress, ipAddress ) def deleteClient(self, name): self.rpc.host_delete( name ) def updateClient(self, src, opsiHostKey = None, description = None, notes = None, inventoryNumber = None, hardwareAddress = None, ipAddress = None, depot = None ): obj = { "id" : src, "type" : "OpsiClient", } if opsiHostKey: obj['opsiHostKey'] = opsiHostKey if description: obj['description'] = description if notes: obj['notes'] = notes if inventoryNumber: obj['inventoryNumber'] = inventoryNumber if hardwareAddress: obj['hardwareAddress'] = hardwareAddress if ipAddress: obj['ipAddress'] = ipAddress if self.exists( src ): self.rpc.host_updateObject(obj) else: self.rpc.host_insertObject(obj) if depot: self.clientSetDepot(src,depot) return True def clientGetDepot(self, name): depot = self.rpc.configState_getHashes( [], { "configId": "clientconfig.depot.id", "objectId": name } ) try: return depot[0]["values"][0] except (IndexError,KeyError): return self.getOpsiConfigserverId() def clientSetDepot(self, name, depot): self.rpc.configState_create( "clientconfig.depot.id", name, depot ) def copyClient( self, src, dst, ipAddress = None, hardwareAddress = None, depot = None, description = "", copyProperties = True ): print("create/update", dst, "from template", src + ":", end='') obj = { "id" : dst, "type" : "OpsiClient", "notes" : "copy of " + src, "description" : description, #"inventoryNumber" : "", } if hardwareAddress: obj['hardwareAddress'] = hardwareAddress if ipAddress: obj['ipAddress'] = ipAddress if self.exists( dst ): self.rpc.host_updateObject(obj) else: self.rpc.host_insertObject(obj) if depot: self.clientSetDepot(dst,depot) if self.debug: pprint( self.getProductOnClient( src ) ) self.copyProductOnClient( src, dst ) if copyProperties: if self.debug: print("copy product properties") if not depot: # get default Properties from Master Depot Server (OpsiConfigserver) depot = self.getOpsiConfigserverId() self.copyProductPropertyState( src, dst, depot ) print("done") return True def getProductOnClient( self, client, attributes = ProductAttributesCopy ): return self.rpc.productOnClient_getHashes( [], { 'clientId': client } ) def copyProductOnClient( self, src, dst, attributes = ProductAttributesCopy ): products_src = self.rpc.productOnClient_getHashes( attributes, { 'clientId': src } ) products_dst = [] for i in products_src: if self.debug: print(i['productId']) pprint( i ) i['clientId'] = dst products_dst.append(i) self.rpc.productOnClient_createObjects( products_dst ) if self.debug: pprint( self.getProductOnClient( dst ) ) def getProductPropertyState( self, client, attributes = [] ): return self.rpc.productPropertyState_getHashes( [], { 'objectId': client } ) def copyProductPropertyState( self, src, dst, default = None, attributes = [] ): if default: productProperties_default = self.getProductPropertyState( default, attributes ) else: productProperties_default = [] productProperties_src = self.getProductPropertyState( src, attributes ) productProperties_dst = [] for i in productProperties_src: use_default=False default_value=None for j in productProperties_default: if i['productId'] == j['productId'] and i["propertyId"] == j["propertyId"]: default_value = j['values'] if i['values'] == j['values']: use_default=True if self.debug: print(i['productId'], "-", i["propertyId"] + ": ", pformat(i["values"]), end='') if use_default: print("(use default)") else: print("(set, default:", default_value, ")") if not use_default: i['objectId'] = dst productProperties_dst.append(i) self.rpc.productPropertyState_createObjects( productProperties_dst ) if self.debug: pprint( self.getProductPropertyState( dst ) ) def getClientProductProperty( self, client, product ): return self.rpc.getProductProperties_hash( product, [ client ] ) def setProductPropertiesOnClient( self, dst, product, properties ): self.rpc.setProductProperties(product,properties,dst) def setProductPropertyOnClient( self, dst, product, prop, value): self.rpc.setProductProperty(product,prop,value,dst) def write_value_conf(self, fd, key, properties, default=None): value = None comment = '' try: value = properties[key.lower()] except KeyError: pass if not value and default: value = default if not value: # prevent a None to be written value = '' comment = '# ' fd.write(' {}{} = "{}"\n'.format(comment, key, value)) def write_client_conf( self, fd, client, properties ): #Client { #Name = ting-fd #Address = ting.dass-it #FDPort = 9102 #Password = "D5w2V5w6B8a9H5Z" #Catalog = MyCatalog #File Retention = 6 months #Job Retention = 6 months #AutoPrune = yes #} params = [ 'catalog', "FDPort", "FileRetention", "JobRetention", "AutoPrune" ] fd.write( "Client {\n" ) fd.write( ' Name = "' + properties['filedaemon_full_name'] + '"' + "\n" ) fd.write( ' Address = "' + properties['filedaemon_client_address'] + '"' + "\n" ) # ipAddress: method host_getObjects [] '{"id":client['clientId']}' #print(" # Address =", ipAddress) fd.write( ' Password = "' + properties['filedaemon_full_password'] + '"' + "\n" ) for i in params: self.write_value_conf(fd, i, properties) fd.write( "}\n") fd.write( "\n" ) def write_job_conf(self, fd, client, properties, defaultjobdefs, defaultfileset): #Job { #FileSet = "tingfileset" #Name = "ting" #Client = ting-fd #JobDefs = "LaptopJob" ## Write Bootstrap = "/var/lib/bacula/ting.bsr" #} params = [ "JobDefs", "FileSet" ] fd.write( "Job {" + "\n" ) fd.write( ' Name = "' + client['clientId'] + '-job"' + "\n" ) fd.write( ' Client = "' + properties['filedaemon_full_name'] + '"' + "\n" ) self.write_value_conf(fd, 'JobDefs', properties, defaultjobdefs) self.write_value_conf(fd, 'FileSet', properties, defaultfileset) fd.write( "}" + "\n" ) fd.write( "\n" ) def write_config_file_header( self, fd ): try: fd.write( "#\n" ) fd.write( "# automatically generated at {0}\n".format( time.asctime() ) ) fd.write( "#\n\n" ) except BaseException as e: self.logger.exception( "failed to create files" ) return False return True def createBareosConfigFiles(self, defaultjobdefs, defaultfileset): bareosDirConfigPath = '/etc/bareos/bareos-dir.d/' clientsWithBacula=self.getClientsWithProduct('winbareos') if clientsWithBacula: try: configfile = bareosDirConfigPath + 'client/opsi-clients-generated.conf' file_opsi_clients = open(configfile, 'w') self.write_config_file_header( file_opsi_clients ) except (BaseException, IOError) as e: self.logger.exception( "failed to create configuration file {}".format(configfile) ) return False try: configfile = bareosDirConfigPath + 'job/opsi-jobs-generated.conf' file_opsi_jobs = open(configfile, 'w') self.write_config_file_header( file_opsi_jobs ) except (BaseException, IOError) as e: self.logger.exception( "failed to create configuration file {}".format(configfile) ) return False for client in clientsWithBacula: clientId = client['clientId'] try: clientBaculaProperties=self.getClientProductProperty( clientId, 'winbareos' ) except ValueError as e: self.logger.warn( "%s: no valid information found: %s" %(clientId, e) ) else: if clientBaculaProperties: #pprint( clientBaculaProperties ) self.write_client_conf(file_opsi_clients, client, clientBaculaProperties) self.write_job_conf(file_opsi_jobs, client, clientBaculaProperties, defaultjobdefs, defaultfileset) self.logger.info( "%s: OK" % clientId ) else: self.logger.warn( "%s: failed: no product properties defined" %(clientId) ) return True def __getVersionString(self, product): return '{productVersion}-{packageVersion}'.format(**product) def getProductCurrentVersion(self, productId): products = self.rpc.product_getHashes( [], { 'id': productId } ) if products: return self.__getVersionString(products[0]) else: return None def __getClientId(self, d): return d.get("clientId") def listInstalled(self, productId): productVersion = self.getProductCurrentVersion(productId) self.logger.debug('version: {0}'.format(productVersion)) products = self.rpc.productOnClient_getHashes( [], { 'productId': productId } ) for i in sorted(products, key=self.__getClientId): i['version'] = self.__getVersionString(i) if i.get('installationStatus') == 'installed': if productVersion != i['version']: i['proposedAction'] = 'update' else: i['proposedAction'] = 'None' print('{clientId}: {version} (proposed action={proposedAction}) request={actionRequest}, result={actionResult}'.format(**i)) else: i['proposedAction'] = 'install' print('{clientId}: (proposed action={proposedAction})'.format(**i)) self.logger.debug('{clientId}: {actionRequest} {installationStatus} {version}'.format(**i)) #pprint( i, indent=8 ) return True def isInstalled(self, clientId, productId): """ CRITICAL: not installed WARNING: installed, but not current version OK: current version is installed UNKNOWN: otherwise """ output = Output(self.nagios) if not self.exists(clientId): output.setMessage("failed: opsi client {0} does not exist".format(clientId)) return output.finalize() productVersion = self.getProductCurrentVersion(productId) if not productVersion: output.setMessage("failed: product {0} does not exist".format(productId)) return output.finalize() self.logger.debug('version: {0}'.format(productVersion)) products = self.rpc.productOnClient_getHashes( [], { "clientId": clientId,'productId': productId } ) if len(products) != 1: print("failed: opsi client ({0}) product ({1}) combination does not exist".format(clientId, productId)) return False for i in sorted(products, key=self.__getClientId): i['version'] = self.__getVersionString(i) if i.get('installationStatus') == 'installed': if productVersion != i['version']: i['proposedAction'] = 'update' output.setStatus(Nrpe.WARNING) else: i['proposedAction'] = 'None' output.setStatus(Nrpe.OK) output.setMessage('{version} (proposed action={proposedAction})'.format(**i)) else: i['proposedAction'] = 'install' output.setStatus(Nrpe.CRITICAL, 'not installed (proposed action={proposedAction})'.format(**i)) self.logger.debug('{clientId}: {actionRequest} {installationStatus} {version}'.format(**i)) return output.finalize() def clientLastSeen(self, clientId): """ < 1 day: OK < 2 days: WARNING otherwise: CRITICAL """ output = Output(self.nagios) if not self.exists(clientId): output.setMessage("failed: opsi client {0} does not exist".format(clientId)) return output.finalize() host = self.rpc.host_getHashes( [], {"id":clientId} )[0] lastSeen = dateparser.parse(host["lastSeen"]) output.setMessage(str(lastSeen)) diff = datetime.now() - lastSeen output.setMessage('{0} ({1} ago)'.format(str(lastSeen), diff)) if diff < timedelta(1): output.setStatus(Nrpe.OK) elif diff < timedelta(2): output.setStatus(Nrpe.WARNING) else: output.setStatus(Nrpe.CRITICAL) return output.finalize() if __name__ == '__main__': logging.basicConfig(format='%(message)s') logger = logging.getLogger(__name__) logger.setLevel(logging.INFO) parser = argparse.ArgumentParser( description='Command line tool for OPSI configuration.', epilog=HelpEpilog ) parser.add_argument('--debug', action='store_true', help="enable debugging output") parser.add_argument('--nagios', action='store_true', help='output in Nagios NRPE format') parser_url = parser.add_mutually_exclusive_group(required=True) parser_url.add_argument( '--url', help="OPSI Server JSON-RPC url, in following format: " + UrlJsonRpc ) parser_url.add_argument( '--server', help="OPSI Server (instead of URL)" ) username_default=os.getlogin() parser.add_argument( '--username', help="username (instead of URL), default: " + username_default, default=username_default ) parser.add_argument( '--password', help="password (instead of URL)" ) subparsers = parser.add_subparsers(title='subcommands', description='valid subcommands', help='additional help', dest='subcommand' ) parser_clean = subparsers.add_parser('clean', help='remove all product states from a opsi client' ) parser_clean.add_argument( 'src', help="source opsi client to clean" ) parser_copy = subparsers.add_parser('copy', help='copy/create a opsi client from a template opsi client') parser_copy.add_argument( 'src', help="source/template opsi client" ) parser_copy.add_argument( 'dst', help="opsi client to be created" ) parser_copy.add_argument( '--ip', help="IP address of the new opsi client" ) parser_copy.add_argument( '--mac', help="MAC address of the new opsi client" ) parser_copy.add_argument( '--depot', help="depot server the new opsi client should be located" ) #parser_copy.add_argument( '--no-properties', action='store_false', help="don't copy product properties" ) parser_createBareosConfigFiles = subparsers.add_parser( 'createBareosConfigFiles', help='create Bareos config files for all clients that have winbareos installed', formatter_class=argparse.ArgumentDefaultsHelpFormatter ) parser_createBareosConfigFiles.add_argument( '--defaultjobdefs', metavar='JobDefs', default='DefaultJob', help="use this JobDefs if no other is defined for a client" ) parser_createBareosConfigFiles.add_argument( '--defaultfileset', metavar='FileSet', help="use this FileSet if no other is defined for a client" ) parser_exists = subparsers.add_parser('exists', help='check, if a opsi clients exists' ) parser_exists.add_argument( 'src', help="source opsi client" ) #parser_list = subparsers.add_parser('list', help='list all opsi clients' ) parser_listClients = subparsers.add_parser('listClients', help='list opsi clients') parser_listClients.add_argument( '--product', help="only list clients, that have product installed" ) parser_info = subparsers.add_parser('info', help='print information about a opsi client' ) parser_info.add_argument( 'src', help="opsi client" ) parser_clientLastSeen = subparsers.add_parser('clientLastSeen', help='print information about a opsi client' ) parser_clientLastSeen.add_argument( 'client', help="opsi client" ) parser_listInstalled = subparsers.add_parser('listInstalled', help='check if product is installed on client') parser_listInstalled.add_argument('product', help='opsi product') parser_isInstalled = subparsers.add_parser('isInstalled', help='check if product is installed on client') parser_isInstalled.add_argument('client', help='opsi client') parser_isInstalled.add_argument('product', help='opsi product') parser_update = subparsers.add_parser('update', help='update/create a opsi client') parser_update.add_argument( 'src', help="opsi client to be created/updated" ) parser_update.add_argument( '--ip', help="IP address of the opsi client" ) parser_update.add_argument( '--mac', help="MAC address of the opsi client" ) parser_update.add_argument( '--description', help="a description of the client" ) parser_update.add_argument( '--notes', help="notes about the client" ) parser_update.add_argument( '--inventory', help="inventory number" ) parser_update.add_argument( '--depot', help="depot server the opsi client should be located" ) args = parser.parse_args() if args.debug: logger.setLevel(logging.DEBUG) url=args.url if (not url): if args.server: account="" if args.username and args.password: account=args.username + ":" + args.password + "@" elif args.username: account=args.username + "@" url="https://" + account + args.server + ":4447/rpc" else: parser.error( "argument --url is required" ) opsi=OpsiRpc(url, args.debug, args.nagios) result = True try: if args.subcommand == "clean": result = opsi.clean( args.src ) elif args.subcommand == "copy": result = opsi.copyClient( args.src, args.dst, args.ip, args.mac, args.depot ) elif args.subcommand == "createBareosConfigFiles": result = opsi.createBareosConfigFiles(args.defaultjobdefs, args.defaultfileset) elif args.subcommand == "exists": result = opsi.exists( args.src ) elif args.subcommand == "list": result = opsi.list() elif args.subcommand == "listClients": result = opsi.listClients( args.product ) elif args.subcommand == "info": result = opsi.info( args.src ) elif args.subcommand == "update": result = opsi.updateClient( args.src, None, args.description, args.notes, args.inventory, args.mac, args.ip, args.depot ) elif args.subcommand == 'listInstalled': result = opsi.listInstalled(args.product) elif args.subcommand == 'isInstalled': result = opsi.isInstalled(args.client, args.product) elif args.subcommand == 'clientLastSeen': result = opsi.clientLastSeen(args.client) else: print("not yet implemented") except IOError as e: result = False # connection refused print("failed:", e) if args.debug: print(result) if result: exit(0) else: exit(1)