source: opsi/server/dass-opsi-tools/usr/bin/opsiclient@ 1237

Last change on this file since 1237 was 1234, checked in by joergs, on Apr 3, 2017 at 3:54:11 PM

added queries 'listInstalled', 'isInstalled' and 'clientLastSeen'
Nagios NRPE compatible output for some queries
prepared for python3 (print function)

  • Property svn:executable set to *
File size: 24.9 KB
Line 
1#!/usr/bin/env python
2
3# -*- coding: utf-8 -*-
4
5"""ospi-client: performs operation for opsi clients on opsi server via JSON-RPC."""
6
7from __future__ import print_function
8
9__author__ = "Joerg Steffens"
10__copyright__ = "Copyright 2012-2017, dass IT GmbH"
11__license__ = "GPL"
12__version__ = "1.2"
13__email__ = "joerg.steffens@dass-it.de"
14
15#self.command("opsi-admin -d method host_createOpsiClient "+ \
16 #computername + " null " + "\\'"+description+"\\'" + \
17 #" \\'created by dassadmin\\' " + mac_address + " " + \
18 #ip_address)
19#self.command("opsi-admin -d method configState_create clientconfig.depot.id " + \
20 #computername + " " + depotName)
21
22import argparse
23from datetime import datetime, timedelta
24from dateutil import parser as dateparser
25import jsonrpc
26import logging
27import os
28from pprint import pprint, pformat
29import sys
30import time
31
32UrlJsonRpc="https://<username>:<password>@opsi:4447/rpc"
33
34HelpEpilog="WARNING: python-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."
35
36class Nrpe:
37 OK = 0
38 WARNING = 1
39 CRITICAL = 2
40 UNKNOWN = 3
41
42 def toString(self, status):
43 if status == self.OK:
44 return 'OK'
45 elif status == self.WARNING:
46 return 'WARNING'
47 elif status == self.CRITICAL:
48 return 'CRITICAL'
49 else:
50 return 'UNKNOWN'
51
52class Output(Nrpe):
53 def __init__(self, nagios=False):
54 self.nagios = nagios
55 self.result = Nrpe.UNKNOWN
56 self.message = ''
57
58 def setStatus(self, status, message = None):
59 self.result = status
60 if message is not None:
61 self.setMessage(message)
62
63 def setMessage(self, string):
64 self.message = string
65
66 def finalize(self):
67 if self.nagios:
68 print('{0} - {1}'.format(self.toString(self.result), self.message))
69 sys.exit(self.result)
70 else:
71 print('{0} - {1}'.format(self.toString(self.result), self.message))
72 return (self.result == Nrpe.OK)
73
74class OpsiRpc:
75
76 UrlJsonRpcDefault="https://opsi:4447/rpc"
77
78 ProductAttributesCopy = ['actionRequest','actionResult','installationStatus','packageVersion','productVersion']
79
80 def __init__(self, urlJsonRpc = UrlJsonRpcDefault, debug=False, nagios=False):
81 self.logger=logging.getLogger(__name__)
82 self.debug=debug
83 self.nagios=nagios
84 self.urlJsonRpc=urlJsonRpc
85 self.rpc=jsonrpc.ServiceProxy(self.urlJsonRpc)
86 self.logger.debug( "initialized: " + self.urlJsonRpc )
87
88
89 def list(self):
90 try:
91 print( "\n".join( self.rpc.getClientIds_list() ) )
92 except jsonrpc.json.JSONDecodeException as e:
93 self.logger.debug( pformat(self.rpc.getClientIds_list()) )
94 self.logger.exception( "failed" )
95 return True
96
97
98 def getClientsWithProduct( self, product ):
99 return self.rpc.productOnClient_getObjects( [], { "productId": product, "installationStatus": "installed" } )
100
101 def getHardwareSerialNumber(self, src):
102 serialNumbers = self.rpc.auditHardwareOnHost_getHashes( [], {"hostId":src, "hardwareClass":"BIOS", "serialNumber":"*"} )
103 serialNumbers += self.rpc.auditHardwareOnHost_getHashes( [], {"hostId":src, "hardwareClass":"CHASSIS", "serialNumber":"*"} )
104 result = set()
105 for i in serialNumbers:
106 if i['serialNumber']:
107 result.add(i['serialNumber'])
108 if len(result) == 1:
109 return result.pop()
110 elif len(result) > 1:
111 self.logger.warning("found more then one serial number")
112 return list(result)
113
114 def listClients( self, product ):
115 if product:
116 for client in self.getClientsWithProduct( product ):
117 print(client['clientId'])
118 else:
119 return self.list()
120 return True
121
122 def exists(self, src):
123 return len( self.rpc.host_getObjects( [], {"id":src} ) ) == 1
124
125 def info(self, src):
126 if not self.exists( src ):
127 print("failed: opsi client", src, "does not exist")
128 return False
129 print(src + ":")
130 host = self.rpc.host_getHashes( [], {"id":src} )[0]
131 print(" IP:", host["ipAddress"])
132 print(" MAC:", host["hardwareAddress"])
133 print(" inventory:", host["inventoryNumber"])
134 print(" serial number:", self.getHardwareSerialNumber(src))
135 print(" last seen:", host["lastSeen"])
136 print(" notes:", host["notes"])
137 print(" depot:", self.clientGetDepot(src))
138
139 print(" products:")
140 products = self.getProductOnClient( src, [] )
141 for i in products:
142 print(" " + i['productId'] + ":")
143 print(" " + i['installationStatus'], "(", end='')
144 if i['actionRequest']:
145 print(i['actionRequest'], end='')
146 if i['actionProgress']:
147 print(i['actionProgress'], end='')
148 print(")")
149 print(" ", end='')
150 pprint( i, indent=8 )
151 return True
152
153 def clean(self, src):
154 if not self.exists( src ):
155 return False
156 products = self.rpc.productOnClient_getObjects( [], { 'clientId': src } )
157 self.rpc.productOnClient_deleteObjects( products )
158
159 products = self.rpc.productPropertyState_getObjects( [], { 'objectId': src } )
160 self.rpc.productPropertyState_deleteObjects( products )
161
162 if self.debug:
163 pprint( self.getProductOnClient( src ) )
164 return True
165
166 def getOpsiConfigserverId(self):
167 # there should always be only one OpsiConfigserver
168 opsiConfigservers=self.rpc.host_getHashes( [], { "type": "OpsiConfigserver" } )
169 try:
170 return opsiConfigservers[0]['id']
171 except (KeyError,IndexError) as e:
172 self.logger.error( "failed to retreive OpsiConfigserver" )
173
174 def createClient(self, name, opsiHostKey, description, notes, hardwareAddress, ipAddress):
175 # self.rpc.host_createOpsiClient( name, opsiHostKey, description, notes, hardwareAddress, ipAddress )
176 self.updateClient( name, opsiHostKey, description, notes, None, hardwareAddress, ipAddress )
177
178 def deleteClient(self, name):
179 self.rpc.host_delete( name )
180
181 def updateClient(self, src, opsiHostKey = None, description = None, notes = None, inventoryNumber = None, hardwareAddress = None, ipAddress = None, depot = None ):
182 obj = {
183 "id" : src,
184 "type" : "OpsiClient",
185 }
186 if opsiHostKey:
187 obj['opsiHostKey'] = opsiHostKey
188 if description:
189 obj['description'] = description
190 if notes:
191 obj['notes'] = notes
192 if inventoryNumber:
193 obj['inventoryNumber'] = inventoryNumber
194 if hardwareAddress:
195 obj['hardwareAddress'] = hardwareAddress
196 if ipAddress:
197 obj['ipAddress'] = ipAddress
198
199 if self.exists( src ):
200 self.rpc.host_updateObject(obj)
201 else:
202 self.rpc.host_insertObject(obj)
203
204 if depot:
205 self.clientSetDepot(src,depot)
206 return True
207
208 def clientGetDepot(self, name):
209 depot = self.rpc.configState_getHashes( [], {
210 "configId": "clientconfig.depot.id",
211 "objectId": name } )
212 try:
213 return depot[0]["values"][0]
214 except (IndexError,KeyError):
215 return self.getOpsiConfigserverId()
216
217 def clientSetDepot(self, name, depot):
218 self.rpc.configState_create( "clientconfig.depot.id", name, depot )
219
220 def copyClient( self, src, dst, ipAddress = None, hardwareAddress = None, depot = None, description = "", copyProperties = True ):
221
222 print("create/update", dst, "from template", src + ":", end='')
223 obj = {
224 "id" : dst,
225 "type" : "OpsiClient",
226 "notes" : "copy of " + src,
227 "description" : description,
228 #"inventoryNumber" : "",
229 }
230 if hardwareAddress:
231 obj['hardwareAddress'] = hardwareAddress
232 if ipAddress:
233 obj['ipAddress'] = ipAddress
234
235 if self.exists( dst ):
236 self.rpc.host_updateObject(obj)
237 else:
238 self.rpc.host_insertObject(obj)
239
240 if depot:
241 self.clientSetDepot(dst,depot)
242
243 if self.debug:
244 pprint( self.getProductOnClient( src ) )
245 self.copyProductOnClient( src, dst )
246 if copyProperties:
247 if self.debug:
248 print("copy product properties")
249 if not depot:
250 # get default Properties from Master Depot Server (OpsiConfigserver)
251 depot = self.getOpsiConfigserverId()
252 self.copyProductPropertyState( src, dst, depot )
253 print("done")
254 return True
255
256 def getProductOnClient( self, client, attributes = ProductAttributesCopy ):
257 return self.rpc.productOnClient_getHashes( [], { 'clientId': client } )
258
259 def copyProductOnClient( self, src, dst, attributes = ProductAttributesCopy ):
260 products_src = self.rpc.productOnClient_getHashes( attributes, { 'clientId': src } )
261 products_dst = []
262 for i in products_src:
263 if self.debug:
264 print(i['productId'])
265 pprint( i )
266 i['clientId'] = dst
267 products_dst.append(i)
268 self.rpc.productOnClient_createObjects( products_dst )
269 if self.debug:
270 pprint( self.getProductOnClient( dst ) )
271
272
273 def getProductPropertyState( self, client, attributes = [] ):
274 return self.rpc.productPropertyState_getHashes( [], { 'objectId': client } )
275
276
277 def copyProductPropertyState( self, src, dst, default = None, attributes = [] ):
278 if default:
279 productProperties_default = self.getProductPropertyState( default, attributes )
280 else:
281 productProperties_default = []
282 productProperties_src = self.getProductPropertyState( src, attributes )
283 productProperties_dst = []
284 for i in productProperties_src:
285 use_default=False
286 default_value=None
287 for j in productProperties_default:
288 if i['productId'] == j['productId'] and i["propertyId"] == j["propertyId"]:
289 default_value = j['values']
290 if i['values'] == j['values']:
291 use_default=True
292 if self.debug:
293 print(i['productId'], "-", i["propertyId"] + ": ", pformat(i["values"]), end='')
294 if use_default:
295 print("(use default)")
296 else:
297 print("(set, default:", default_value, ")")
298 if not use_default:
299 i['objectId'] = dst
300 productProperties_dst.append(i)
301 self.rpc.productPropertyState_createObjects( productProperties_dst )
302 if self.debug:
303 pprint( self.getProductPropertyState( dst ) )
304
305
306 def getClientProductProperty( self, client, product ):
307 return self.rpc.getProductProperties_hash( product, [ client ] )
308
309 def setProductPropertiesOnClient( self, dst, product, properties ):
310 self.rpc.setProductProperties(product,properties,dst)
311
312 def setProductPropertyOnClient( self, dst, product, prop, value):
313 self.rpc.setProductProperty(product,prop,value,dst)
314
315
316 def write_client_conf( self, fd, client, properties ):
317 #Client {
318 #Name = ting-fd
319 #Address = ting.dass-it
320 #FDPort = 9102
321 #Catalog = MyCatalog
322 #Password = "D5w2V5w6B8a9H5Z"
323 #File Retention = 6 months
324 #Job Retention = 6 months
325 #AutoPrune = yes
326 #}
327 params = [ "FDPort", "FileRetention", "JobRetention", "AutoPrune" ]
328 fd.write( "Client {\n" )
329 fd.write( ' Name = "' + properties['filedaemon_full_name'] + '"' + "\n" )
330 fd.write( ' Address = "' + client['clientId'] + '"' + "\n" )
331 # ipAddress: method host_getObjects [] '{"id":client['clientId']}'
332 #print(" # Address =", ipAddress)
333 fd.write( ' Password = "' + properties['filedaemon_full_password'] + '"' + "\n" )
334 try:
335 catalog = properties['catalog']
336 except KeyError:
337 catalog = "MyCatalog"
338 fd.write( ' Catalog = "' + catalog + '"' + "\n" )
339 for i in params:
340 try:
341 fd.write( ' ' + i + ' = "' + properties[i.lower()] + '"' + "\n" )
342 except KeyError:
343 fd.write( ' # ' + i + " = \n" )
344 fd.write( "}\n")
345 fd.write( "\n" )
346
347
348 def write_job_conf( self, fd, client, properties ):
349 #Job {
350 #FileSet = "tingfileset"
351 #Name = "ting"
352 #Client = ting-fd
353 #JobDefs = "LaptopJob"
354 ## Write Bootstrap = "/var/lib/bacula/ting.bsr"
355 #}
356 params = [ "Fileset", "JobDefs" ]
357 fd.write( "Job {" + "\n" )
358 fd.write( ' Name = "' + client['clientId'] + '-job"' + "\n" )
359 fd.write( ' Client = "' + properties['filedaemon_full_name'] + '"' + "\n" )
360 for i in params:
361 fd.write( " " )
362 try:
363 if not properties[i.lower()]:
364 fd.write( "# " )
365 fd.write( i + ' = "' + properties[i.lower()] + '"' + "\n" )
366 except KeyError:
367 fd.write( "# " + i + " = " + "\n" )
368 fd.write( "}" + "\n" )
369 fd.write( "\n" )
370
371
372 def write_config_file_header( self, fd ):
373 try:
374 fd.write( "#\n" )
375 fd.write( "# automatically generated at {0}\n".format( time.asctime() ) )
376 fd.write( "#\n\n" )
377 except BaseException as e:
378 self.logger.exception( "failed to create files" )
379 return False
380 return True
381
382
383 def createBaculaConfigFiles( self ):
384 clientsWithBacula=self.getClientsWithProduct( "bacula" )
385 if clientsWithBacula:
386 try:
387 file_opsi_clients = open('opsi-clients-generated.conf', 'w')
388 self.write_config_file_header( file_opsi_clients )
389
390 file_opsi_jobs = open('opsi-jobs-generated.conf', 'w')
391 self.write_config_file_header( file_opsi_jobs )
392 except BaseException as e:
393 self.logger.exception( "failed to create files" )
394 return False
395 for client in clientsWithBacula:
396 clientId = client['clientId']
397 try:
398 clientBaculaProperties=self.getClientProductProperty( clientId, "bacula" )
399 except ValueError as e:
400 self.logger.warn( "%s: no valid information found: %s" %(clientId, e) )
401 else:
402 if clientBaculaProperties:
403 #pprint( clientBaculaProperties )
404 self.write_client_conf( file_opsi_clients, client, clientBaculaProperties )
405 self.write_job_conf( file_opsi_jobs, client, clientBaculaProperties )
406 self.logger.info( "%s: OK" % clientId )
407 else:
408 self.logger.warn( "%s: failed: no product properties defined" %(clientId) )
409 return True
410
411 def __getVersionString(self, product):
412 return '{productVersion}-{packageVersion}'.format(**product)
413
414 def getProductCurrentVersion(self, productId):
415 products = self.rpc.product_getHashes( [], { 'id': productId } )
416 if products:
417 return self.__getVersionString(products[0])
418 else:
419 return None
420
421 def __getClientId(self, d):
422 return d.get("clientId")
423
424 def listInstalled(self, productId):
425 productVersion = self.getProductCurrentVersion(productId)
426 self.logger.debug('version: {0}'.format(productVersion))
427 products = self.rpc.productOnClient_getHashes( [], { 'productId': productId } )
428 for i in sorted(products, key=self.__getClientId):
429 i['version'] = self.__getVersionString(i)
430 if i.get('installationStatus') == 'installed':
431 if productVersion != i['version']:
432 i['proposedAction'] = 'update'
433 else:
434 i['proposedAction'] = 'None'
435 print('{clientId}: {version} (proposed action={proposedAction})'.format(**i))
436 else:
437 i['proposedAction'] = 'install'
438 print('{clientId}: (proposed action={proposedAction})'.format(**i))
439 self.logger.debug('{clientId}: {actionRequest} {installationStatus} {version}'.format(**i))
440 #pprint( i, indent=8 )
441 return True
442
443 def isInstalled(self, clientId, productId):
444 """
445 CRITICAL: not installed
446 WARNING: installed, but not current version
447 OK: current version is installed
448 UNKNOWN: otherwise
449 """
450 output = Output(self.nagios)
451 if not self.exists(clientId):
452 output.setMessage("failed: opsi client {0} does not exist".format(clientId))
453 return output.finalize()
454 productVersion = self.getProductCurrentVersion(productId)
455 if not productVersion:
456 output.setMessage("failed: product {0} does not exist".format(productId))
457 return output.finalize()
458 self.logger.debug('version: {0}'.format(productVersion))
459 products = self.rpc.productOnClient_getHashes( [], { "clientId": clientId,'productId': productId } )
460 if len(products) != 1:
461 print("failed: opsi client ({0}) product ({1}) combination does not exist".format(clientId, productId))
462 return False
463 for i in sorted(products, key=self.__getClientId):
464 i['version'] = self.__getVersionString(i)
465 if i.get('installationStatus') == 'installed':
466 if productVersion != i['version']:
467 i['proposedAction'] = 'update'
468 output.setStatus(Nrpe.WARNING)
469 else:
470 i['proposedAction'] = 'None'
471 output.setStatus(Nrpe.OK)
472 output.setMessage('{version} (proposed action={proposedAction})'.format(**i))
473 else:
474 i['proposedAction'] = 'install'
475 output.setStatus(Nrpe.CRITICAL, 'not installed (proposed action={proposedAction})'.format(**i))
476 self.logger.debug('{clientId}: {actionRequest} {installationStatus} {version}'.format(**i))
477 return output.finalize()
478
479
480 def clientLastSeen(self, clientId):
481 """
482 < 1 day: OK
483 < 2 days: WARNING
484 otherwise: CRITICAL
485 """
486 output = Output(self.nagios)
487 if not self.exists(clientId):
488 output.setMessage("failed: opsi client {0} does not exist".format(clientId))
489 return output.finalize()
490 host = self.rpc.host_getHashes( [], {"id":clientId} )[0]
491 lastSeen = dateparser.parse(host["lastSeen"])
492 output.setMessage(str(lastSeen))
493 diff = datetime.now() - lastSeen
494 output.setMessage('{0} ({1} ago)'.format(str(lastSeen), diff))
495 if diff < timedelta(1):
496 output.setStatus(Nrpe.OK)
497 elif diff < timedelta(2):
498 output.setStatus(Nrpe.WARNING)
499 else:
500 output.setStatus(Nrpe.CRITICAL)
501 return output.finalize()
502
503
504
505if __name__ == '__main__':
506 logging.basicConfig(format='%(message)s')
507 logger = logging.getLogger(__name__)
508 logger.setLevel(logging.INFO)
509
510 parser = argparse.ArgumentParser(description='Command line tool for OPSI configuration.', epilog=HelpEpilog )
511
512 parser.add_argument( '--debug', action='store_true', help="enable debugging output" )
513 parser.add_argument('--nagios', action='store_true', help='output in Nagios NRPE format')
514
515 parser_url = parser.add_mutually_exclusive_group(required=True)
516 parser_url.add_argument( '--url', help="OPSI Server JSON-RPC url, in following format: " + UrlJsonRpc )
517
518 parser_url.add_argument( '--server', help="OPSI Server (instead of URL)" )
519 username_default=os.getlogin()
520
521 parser.add_argument( '--username', help="username (instead of URL), default: " + username_default, default=username_default )
522 parser.add_argument( '--password', help="password (instead of URL)" )
523
524 subparsers = parser.add_subparsers(title='subcommands',
525 description='valid subcommands',
526 help='additional help',
527 dest='subcommand' )
528
529 parser_clean = subparsers.add_parser('clean', help='remove all product states from a opsi client' )
530 parser_clean.add_argument( 'src', help="source opsi client to clean" )
531
532 parser_copy = subparsers.add_parser('copy', help='copy/create a opsi client from a template opsi client')
533 parser_copy.add_argument( 'src', help="source/template opsi client" )
534 parser_copy.add_argument( 'dst', help="opsi client to be created" )
535 parser_copy.add_argument( '--ip', help="IP address of the new opsi client" )
536 parser_copy.add_argument( '--mac', help="MAC address of the new opsi client" )
537 parser_copy.add_argument( '--depot', help="depot server the new opsi client should be located" )
538 #parser_copy.add_argument( '--no-properties', action='store_false', help="don't copy product properties" )
539
540 parser_createBaculaConfigFiles = subparsers.add_parser('createBaculaConfigFiles', help='create Bacula config files for all clients that have bacula installed')
541
542 parser_exists = subparsers.add_parser('exists', help='check, if a opsi clients exists' )
543 parser_exists.add_argument( 'src', help="source opsi client" )
544 #parser_list = subparsers.add_parser('list', help='list all opsi clients' )
545
546 parser_listClients = subparsers.add_parser('listClients', help='list opsi clients')
547 parser_listClients.add_argument( '--product', help="only list clients, that have product installed" )
548
549 parser_info = subparsers.add_parser('info', help='print information about a opsi client' )
550 parser_info.add_argument( 'src', help="opsi client" )
551
552 parser_clientLastSeen = subparsers.add_parser('clientLastSeen', help='print information about a opsi client' )
553 parser_clientLastSeen.add_argument( 'client', help="opsi client" )
554
555 parser_listInstalled = subparsers.add_parser('listInstalled', help='check if product is installed on client')
556 parser_listInstalled.add_argument('product', help='opsi product')
557
558 parser_isInstalled = subparsers.add_parser('isInstalled', help='check if product is installed on client')
559 parser_isInstalled.add_argument('client', help='opsi client')
560 parser_isInstalled.add_argument('product', help='opsi product')
561
562 parser_update = subparsers.add_parser('update', help='update/create a opsi client')
563 parser_update.add_argument( 'src', help="opsi client to be created/updated" )
564 parser_update.add_argument( '--ip', help="IP address of the opsi client" )
565 parser_update.add_argument( '--mac', help="MAC address of the opsi client" )
566 parser_update.add_argument( '--description', help="a description of the client" )
567 parser_update.add_argument( '--notes', help="notes about the client" )
568 parser_update.add_argument( '--inventory', help="inventory number" )
569 parser_update.add_argument( '--depot', help="depot server the opsi client should be located" )
570
571 args = parser.parse_args()
572
573 if args.debug:
574 logger.setLevel(logging.DEBUG)
575
576 url=args.url
577 if (not url):
578 if args.server:
579 account=""
580 if args.username and args.password:
581 account=args.username + ":" + args.password + "@"
582 elif args.username:
583 account=args.username + "@"
584 url="https://" + account + args.server + ":4447/rpc"
585 else:
586 parser.error( "argument --url is required" )
587
588 opsi=OpsiRpc(url, args.debug, args.nagios)
589
590 result = True
591
592 try:
593 if args.subcommand == "clean":
594 result = opsi.clean( args.src )
595 elif args.subcommand == "copy":
596 result = opsi.copyClient( args.src, args.dst, args.ip, args.mac, args.depot )
597 elif args.subcommand == "createBaculaConfigFiles":
598 result = opsi.createBaculaConfigFiles()
599 elif args.subcommand == "exists":
600 result = opsi.exists( args.src )
601 elif args.subcommand == "list":
602 result = opsi.list()
603 elif args.subcommand == "listClients":
604 result = opsi.listClients( args.product )
605 elif args.subcommand == "info":
606 result = opsi.info( args.src )
607 elif args.subcommand == "update":
608 result = opsi.updateClient( args.src, None, args.description, args.notes, args.inventory, args.mac, args.ip, args.depot )
609 elif args.subcommand == 'listInstalled':
610 result = opsi.listInstalled(args.product)
611 elif args.subcommand == 'isInstalled':
612 result = opsi.isInstalled(args.client, args.product)
613 elif args.subcommand == 'clientLastSeen':
614 result = opsi.clientLastSeen(args.client)
615 else:
616 print("not yet implemented")
617 except IOError as e:
618 result = False
619 # connection refused
620 print("failed:", e)
621
622 if args.debug: print(result)
623
624 if result:
625 exit(0)
626 else:
627 exit(1)
Note: See TracBrowser for help on using the repository browser.