I have a solution that seems to meet my needs, the secret is to use *both* JSON and REST. The JSON API will let me get a list of devices, classes, groups, etc. from the server; and the REST API will let me add a maintenance window.
While it isn't beautiful and may make some assumptions about our environment that don't translate to others, here is what I ended up with (and comments on this script are appreciated). To use this you will need to at least update ZENOSS_INSTANCE in the code to point to your server URL.
#!/usr/bin/env python # # Use the Zenoss JSON API to schedule device or class maintenance windows # import re, string import getopt, sys import os, pwd import time import datetime import dateutil.parser import urllib import urllib2, base64 import json import getpass from subprocess import call debug=0 ROUTERS = { 'MessagingRouter': 'messaging', 'EventsRouter': 'evconsole', 'ProcessRouter': 'process', 'ServiceRouter': 'service', 'DeviceRouter': 'device', 'NetworkRouter': 'network', 'TemplateRouter': 'template', 'DetailNavRouter': 'detailnav', 'ReportRouter': 'report', 'MibRouter': 'mib', 'ZenPackRouter': 'zenpack' } class ZenossAPI(): def __init__(self, debug=False): """ Initialize the API connection, log in, and store authentication cookie """ # Use the HTTPCookieProcessor as urllib2 does not save cookies by default self.urlOpener = urllib2.build_opener(urllib2.HTTPCookieProcessor()) if debug: self.urlOpener.add_handler(urllib2.HTTPHandler(debuglevel=1)) self.reqCount = 1 # Contruct POST params and submit login. loginParams = urllib.urlencode(dict( __ac_name = ZENOSS_USERNAME, __ac_password = ZENOSS_PASSWORD, submitted = 'true', came_from = ZENOSS_INSTANCE + '/zport/dmd')) self.urlOpener.open(ZENOSS_INSTANCE + '/zport/acl_users/cookieAuthHelper/login', loginParams) def _router_request(self, router, method, data=[]): global debug if debug > 1: print "_router_request(%s, %s, %s)" % (router, method, str(data)) if router not in ROUTERS: raise Exception('Router "' + router + '" not available.') # Contruct a standard URL request for API calls req = urllib2.Request(ZENOSS_INSTANCE + '/zport/dmd/' + ROUTERS[router] + '_router') # NOTE: Content-type MUST be set to 'application/json' for these requests req.add_header('Content-type', 'application/json; charset=utf-8') # Convert the request parameters into JSON reqData = json.dumps([dict( action=router, method=method, data=data, type='rpc', tid=self.reqCount)]) # Increment the request count ('tid'). More important if sending multiple # calls in a single request self.reqCount += 1 # Submit the request and convert the returned JSON to objects return json.loads(self.urlOpener.open(req, reqData).read()) # Use the API cookie to request a REST URL def RESTRequest(self, url): req = urllib2.Request(url) return(self.urlOpener.open(req).read()) def get_devices(self, deviceClass='/zport/dmd/Devices', limit=50, start=0, params=None): global debug if debug > 1: print "get_devices(deviceClass=%s,limit=%d,start=%d,params=%s)" % (deviceClass, limit, start, str(params)) return self._router_request('DeviceRouter', 'getDevices', data=[{'uid': deviceClass, 'params': params, 'limit': limit, 'start': start}])['result'] def usage(message=''): if message != '': print message print """Usage: zenoss_downtime -h|--hostname hostname|deviceClass[,hostname|deviceClass[...]] [-b|--start start_time] [-e|--end end_time] [-c|--comment comment] [-i|--instance zenoss_url] [-u|--username user] [-p|--password password] \thostname may be a comma separated list of hosts, \t\ta device class starting with a '/Devices', \t\ta group starting with /Groups, \t\tor a location starting with /Locations \tstart and end times may be any valid time, for example '12/24/2013 10:00' \tcomment should be the reason for the downtime example: zenoss_downtime -h /Devices -s 12:00 -e 15:00 -c "Short PM window" """ sys.exit(3) # Parse a time value, reading from stdin if it cannot be parsed def getTime(value=0, prompt=''): returnTime = 0; while not returnTime: if not value: value = raw_input(prompt + ': ') if value == "now": # Allow for "now" as a valid time returnTime = datetime.datetime.now() else: try: returnTime = dateutil.parser.parse(value) except: print "ERROR: Cannot parse %s from %s" % (prompt.lower(), a) if returnTime < datetime.datetime.now(): print "ERROR: time cannot be in the past: %s" % returnTime returnTime = 0 value = '' return(returnTime) # Get devices classList = [] # List of device classes deviceList = [] # List of devices deviceUID = {} # Dict of device UIDs nextDev = 0 # Starting device for get_devices() lastDev = 1 # Total devices to stop loop devices = [] startTime = 0 endTime = 0 duration = 0 comment = '' # Parse command line try: options, args = getopt.getopt(sys.argv[1:], "d:h:s:e:c:i:u:p:D:", ["deviceclass=", "hostname=", "start=", "end=", "comment=", "instance=", "username=", "password=", "debug="], ) except getopt.GetoptError, err: usage(str(err)) sys.exit(3) # Zenoss connection info, including username and password ZENOSS_INSTANCE = 'http://zenoss.example.com:8080' ZENOSS_USERNAME = pwd.getpwuid(os.getuid()).pw_name ZENOSS_PASSWORD = '' # if not in the args, it will prompt for o, a in options: if o in ("-h", "--hostname"): devices = a.split(',') elif o in ("-D", "--debug"): debug = a elif o in ("-c", "--comment"): comment = a elif o in ("-s", "--start"): if a == "now": # Allow for "now" as a valid time startTime = datetime.datetime.now() else: try: startTime = dateutil.parser.parse(a) except: usage("Cannot parse start time from %s" % a) elif o in ("-e", "--end"): try: endTime = dateutil.parser.parse(a) except: usage("Cannot parse end time from %s" % a) elif o in ("-u", "--instance"): ZENOSS_INSTANCE = a elif o in ("-u", "--username"): ZENOSS_USERNAME = a elif o in ("-p", "--password"): ZENOSS_PASSWORD = a if not ZENOSS_PASSWORD: ZENOSS_PASSWORD = getpass.getpass('Password: ') z = ZenossAPI() # Create API instance # Get unspecified values if not startTime: startTime = getTime(startTime,'Start time') while not endTime or endTime < startTime: if endTime and (endTime <= startTime): print "ERROR: end time cannot be before start time!" endTime = 0 endTime = getTime(endTime,'End time') duration = endTime - startTime while not comment: comment = raw_input('Reason for maintenance window: ') # Check for valid hostname/deviceclass devUIDs = list() devNames = dict() for device in devices: nextDev = 0 # Starting device for get_devices() lastDev = 1 # Total devices to stop loop if re.search('^/',device): # It's a device class devJSON = z.get_devices(deviceClass='/zport/dmd/Devices' + device, limit=100, start=nextDev) if not 'devices' in devJSON or len(devJSON['devices']) <= 0: print("ERROR: unknown device: %s" % device) devices.remove(device) else: uid = '/zport/dmd/Devices' + device devUIDs.append(uid) devNames[uid] = { 'name': device.replace('/','_') } else: params = {'name': device} devJSON = z.get_devices(limit=100, start=nextDev, params=params) if not 'devices' in devJSON or len(devJSON['devices']) <= 0: print("ERROR: unknown device: %s" % device) devices.remove(device) else: devUIDs.append(devJSON['devices'][0]['uid']) devNames[devJSON['devices'][0]['uid']] = { 'name': device } devices[devices.index(device)] = devJSON['devices'][0]['name'] if debug > 2: print json.dumps(devJSON, sort_keys=True, indent=4, separators=(',', ': ')) if len(devUIDs) <= 0: usage("ERROR: you must specify at least one device or device class!"); if debug: print devUIDs print devNames print devices print startTime print duration print comment # Use a REST call as per http://community.zenoss.org/thread/18835 # to schedule the maintenance window timestamp = time.strftime("%Y%m%d%H%M%S",time.localtime(time.time())) for uid in devUIDs: # Add the window #http://zenoss_host:8080/object_path/manage_addMaintenanceWindow?newId=maintenance_window_name addUrl = ZENOSS_INSTANCE + urllib.quote(uid + '/manage_addMaintenanceWindow') + '?newId=' + ZENOSS_USERNAME + '_' + timestamp + '_' + devNames[uid]['name'] if debug > 1: print "Adding downtime with %s" % addUrl data = z.RESTRequest(addUrl) # Update the window #http://zenoss_host:8080/object_path/maintenanceWindows/maintenance_window_name/manage_editMaintenanceWindow?startDate=start_date,startHours=start_hour,startMinutes=start_min,durationDays=duration_days,durationHours=duration_hours,durationMinutes=duration_min editUrl = ZENOSS_INSTANCE + urllib.quote(uid + '/maintenanceWindows/' + ZENOSS_USERNAME + '_' + timestamp + '_' + devNames[uid]['name'] + '/manage_editMaintenanceWindow') + startTime.strftime('?startDate=%m/%d/%Y&startHours=%H&startMinutes=%M') + '&durationDays=' + str(duration.days) + '&durationHours=' + str(int(duration.seconds / 3600)) + '&durationMinutes=' + str(int(duration.seconds - (duration.seconds / 3600) * 3600) / 60) if debug > 1: print "Updating downtime with %s" % editUrl data = z.RESTRequest(editUrl) print 'Added maintenance window: ' + ZENOSS_USERNAME + '_' + timestamp + '_' + devNames[uid]['name']