/** Enterprise Control Configuration and Logging Copyright (C) 2012 Free Software Foundation, Inc. Written by: Richard Frith-Macdonald Date: Febrary 2010 Originally developed from 1996 to 2012 by Brainstorm, and donated to the FSF. This file is part of the GNUstep project. This library 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 3 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111 USA. */ #import #import "EcAlarm.h" #import "EcAlarmSinkSNMP.h" #import "EcAlerter.h" #import "EcClientI.h" #import "EcHost.h" #import "EcProcess.h" #import "EcUserDefaults.h" #import "NSFileHandle+Printf.h" #include "config.h" #if defined(HAVE_LIBCRYPT) extern char *crypt(const char *key, const char *salt); #endif /* * Catagory so that NSHost objects can be safely used as dictionary keys. */ @implementation NSHost (ControlExtension) - (id) copyWithZone: (NSZone*)z { return RETAIN(self); } @end static NSString *controlKey = nil; static EcAlarmSinkSNMP *sink = nil; static NSMutableDictionary *lastAlertInfo = nil; static NSTimeInterval pingDelay = 240.0; static int alertAlarmThreshold = EcAlarmSeverityMajor; static int reminderInterval = 0; static uint64_t alarmsAlerted = 0; static uint64_t alarmsIgnored = 0; static int comp_len = 0; static int comp(NSString *s0, NSString *s1) { if ([s0 length] > [s1 length]) { comp_len = -1; return -1; } if ([s1 compare: s0 options: NSCaseInsensitiveSearch|NSLiteralSearch range: NSMakeRange(0, [s0 length])] == NSOrderedSame) { comp_len = [s0 length]; if (comp_len == [s1 length]) { return 0; } else { return 1; } } else { comp_len = -1; return -1; } } static BOOL matchCmd(NSString *word, NSString *reference, NSArray *allow) { if (comp(word, reference) < 0) { return NO; } if (nil == allow || [allow containsObject: reference]) { return YES; } return NO; } static NSString* cmdWord(NSArray* a, unsigned int pos) { if (a != nil && [a count] > pos) { return [a objectAtIndex: pos]; } else { return @""; } } @interface CommandInfo : EcClientI { NSArray *servers; } - (NSString*) serverByAbbreviation: (NSString*)s; - (NSArray*) servers; - (void) setServers: (NSArray*)s; @end @implementation CommandInfo - (void) dealloc { DESTROY(servers); [super dealloc]; } - (NSString*) serverByAbbreviation: (NSString*)s { NSString *svr; int i; int best_pos = -1; int best_len = 0; /* * Special case - a numeric value is used as an index into the array. */ if (isdigit(*[s cString])) { i = [s intValue]; if (i >= 0 && i < [servers count]) { svr = [servers objectAtIndex: i]; return svr; } } for (i = 0; i < [servers count]; i++) { svr = [servers objectAtIndex: i]; if (comp(s, svr) == 0) { return svr; } if (comp_len > best_len) { best_len = comp_len; best_pos = i; } } if (best_pos >= 0) { svr = [servers objectAtIndex: best_pos]; return svr; } return nil; } - (NSArray*) servers { return servers; } - (void) setServers: (NSArray*)s { ASSIGN(servers, s); } @end @interface ConsoleInfo : EcClientI { NSString *cserv; NSString *chost; NSString *pass; BOOL general; BOOL warnings; BOOL errors; BOOL alerts; BOOL audits; BOOL mark; BOOL prompt; } - (id) initFor: (id)o name: (NSString*)n with: (id)s pass: (NSString*)p; - (NSString*) chost; - (NSString*) cserv; - (BOOL) getAlerts; - (BOOL) getAudits; - (BOOL) getErrors; - (BOOL) getGeneral; - (BOOL) getMark; - (BOOL) getPrompt; - (BOOL) getWarnings; - (NSString*) pass; - (NSString*) promptAfter: (NSString*)msg from: (NSString*)sender; - (void) setAlerts: (BOOL)flag; - (void) setAudits: (BOOL)flag; - (void) setConnectedHost: (NSString*)c; - (void) setConnectedServ: (NSString*)c; - (void) setErrors: (BOOL)flag; - (void) setGeneral: (BOOL)flag; - (void) setMark: (BOOL)flag; - (void) setPrompt: (BOOL)flag; - (void) setWarnings: (BOOL)flag; @end @implementation ConsoleInfo - (NSString*) chost { return chost; } - (NSString*) cserv { return cserv; } - (void) dealloc { DESTROY(chost); DESTROY(cserv); DESTROY(pass); [super dealloc]; } - (BOOL) getAlerts { return alerts; } - (BOOL) getAudits { return audits; } - (BOOL) getErrors { return errors; } - (BOOL) getGeneral { return general; } - (BOOL) getMark { return mark; } - (BOOL) getPrompt { return prompt; } - (BOOL) getWarnings { return warnings; } - (id) initFor: (id)o name: (NSString*)n with: (id)s pass: (NSString*)p { self = [super initFor: o name: n with: s]; if (self != nil) { pass = [p copy]; chost = nil; cserv = nil; general = NO; warnings = NO; alerts = YES; audits = NO; errors = YES; mark = NO; prompt = YES; } return self; } - (NSString*) pass { return pass; } - (NSString*) promptAfter: (NSString*)msg from: (NSString*)sender { NSString *sMark = @""; NSString *eMark = @""; if (sender && mark) { sMark = [NSString stringWithFormat: @"[start message from %@]\n", sender]; eMark = [NSString stringWithFormat: @"\n[end message from %@]", sender]; } if (NO == prompt) { return [NSString stringWithFormat: @"%@%@%@", sMark, msg, eMark]; } else if (chost && cserv) { return [NSString stringWithFormat: @"%@%@%@\n%@:%@> ", sMark, msg, eMark, chost, cserv]; } else if (chost) { return [NSString stringWithFormat: @"%@%@%@\n%@:> ", sMark, msg, eMark, chost]; } else if (cserv) { return [NSString stringWithFormat: @"%@%@%@\n:%@> ", sMark, msg, eMark, cserv]; } else { return [NSString stringWithFormat: @"%@%@%@\nControl> ", sMark, msg, eMark]; } } - (void) setAlerts: (BOOL)flag { alerts = flag; } - (void) setAudits: (BOOL)flag { audits = flag; } - (void) setConnectedHost: (NSString*)c { ASSIGNCOPY(chost, c); } - (void) setConnectedServ: (NSString*)c { ASSIGNCOPY(cserv, c); } - (void) setErrors: (BOOL)flag { errors = flag; } - (void) setGeneral: (BOOL)flag { general = flag; } - (void) setMark: (BOOL)flag { mark = (flag ? YES : NO); } - (void) setPrompt: (BOOL)flag { prompt = (flag ? YES : NO); } - (void) setWarnings: (BOOL)flag { warnings = flag; } @end @interface EcControl : EcProcess { NSFileManager *mgr; NSMutableArray *commands; NSMutableArray *consoles; NSString *logname; NSDictionary *config; NSDictionary *controlConfig; NSMutableDictionary *operators; NSMutableDictionary *fileBodies; NSMutableDictionary *fileDates; NSTimer *timer; NSTimer *terminating; unsigned commandPingPosition; unsigned consolePingPosition; NSString *configFailed; NSString *configIncludeFailed; NSRegularExpression *alarmFilter; EcAlerter *alerter; } - (NSFileHandle*) openLog: (NSString*)lname; - (oneway void) cmdGnip: (id )from sequence: (unsigned)num extra: (NSData*)data; - (oneway void) cmdPing: (id )from sequence: (unsigned)num extra: (NSData*)data; - (oneway void) cmdQuit: (NSInteger)status; - (void) command: (NSData*)dat from: (NSString*)f; - (BOOL) connection: (NSConnection*)ancestor shouldMakeNewConnection: (NSConnection*)newConn; - (id) connectionBecameInvalid: (NSNotification*)notification; - (EcClientI*) findIn: (NSArray*)a byAbbreviation: (NSString*)s; - (EcClientI*) findIn: (NSArray*)a byName: (NSString*)s; - (EcClientI*) findIn: (NSArray*)a byObject: (id)s; - (void) information: (NSString*)inf type: (EcLogType)t to: (NSString*)to from: (NSString*)from; - (NSString*) messageForAlarm: (EcAlarm*)alarm; - (NSData*) registerCommand: (id)c name: (NSString*)n; - (NSString*) registerConsole: (id)c name: (NSString*)n pass: (NSString*)p; - (void) reply: (NSString*) msg to: (NSString*)n from: (NSString*)c; - (void) reportAlarm: (EcAlarm*)alarm withMessage: (NSString*)message isCleared: (BOOL)cleared reminder: (int)reminder; - (void) reportAlarms; - (void) servers: (NSData*)d on: (id)s; - (void) timedOut: (NSTimer*)t; - (void) unregister: (id)obj; - (BOOL) update; - (void) updateConfig: (NSData*)dummy; - (id) getInclude: (id)s; - (id) recursiveInclude: (id)o; - (id) tryInclude: (id)s multi: (BOOL*)flag; @end @implementation EcControl - (oneway void) alarm: (in bycopy EcAlarm*)alarm { EcAlarmSeverity severity; EcAlarm *old; NSString *mesg; NSString *desc; NSRange range; if (NO == [NSThread isMainThread]) { [self performSelectorOnMainThread: _cmd withObject: alarm waitUntilDone: NO]; return; } severity = [alarm perceivedSeverity]; old = [sink latest: alarm]; if (old && (EcAlarmSeverityCleared == severity && [old perceivedSeverity] == severity)) { return; // Duplicate clear ignored. } else { NSRegularExpression *re; [self ecDoLock]; re = AUTORELEASE(RETAIN(alarmFilter)); [self ecUnLock]; mesg = [self messageForAlarm: alarm]; if (re != nil) { NSRange r; r = [re rangeOfFirstMatchInString: mesg options: 0 range: NSMakeRange(0, [mesg length])]; if (r.length > 0) { NSLog(@"AlarmFilter config removes alarm: %@", mesg); return; } } } desc = [alarm description]; /* A local copy of an alarm will have an address differing from that of the * alarm in the originating process. Avoid reporting that here. */ if ((range = [desc rangeOfString: @"0x"]).length > 0) { desc = [desc substringFromIndex: NSMaxRange(range)]; range = [desc rangeOfString: @" "]; desc = [desc substringFromIndex: range.location]; desc = [_(@"Alarm with") stringByAppendingString: desc]; } if (EcAlarmSeverityCleared != severity && EcAlarmSeverityIndeterminate != severity) { NSArray *a = [NSArray arrayWithArray: consoles]; NSUInteger i = [a count]; /* * Work with a copy of the consoles array in case one goes away * or is added while we are doing this! */ while (i-- > 0) { ConsoleInfo *c = [a objectAtIndex: i]; if ([consoles indexOfObjectIdenticalTo: c] == NSNotFound) { continue; } if (EcAlarmSeverityWarning == severity && [c getWarnings] == NO) { continue; } if ((EcAlarmSeverityMajor == severity || EcAlarmSeverityMinor == severity) && [c getErrors] == NO) { continue; } if (EcAlarmSeverityCritical == severity && [c getAlerts] == NO) { continue; } NS_DURING { [[c obj] information: [c promptAfter: desc from: nil]]; } NS_HANDLER { NSLog(@"Caught: %@", localException); } NS_ENDHANDLER } } [[self cmdLogFile: logname] puts: desc]; if (EcAlarmSeverityCleared == severity) { NSArray *a = [sink alarms]; NSUInteger index = [a indexOfObject: alarm]; if (NSNotFound != index) { EcAlarm *old = [a objectAtIndex: index]; int notificationID = [old notificationID]; severity = [old perceivedSeverity]; if (severity <= alertAlarmThreshold && notificationID > 0) { NSDictionary *info; NSString *key; int reminder; key = [NSString stringWithFormat: @"%d", notificationID]; mesg = [self messageForAlarm: old]; if (nil == (info = [lastAlertInfo objectForKey: key])) { BOOL cleared; /* Alarm not yet reported ... report it before clearing. */ if ([old perceivedSeverity] == EcAlarmSeverityCleared) { cleared = YES; } else { cleared = NO; } [self reportAlarm: old withMessage: mesg isCleared: cleared reminder: 0]; } /* Report the clearing of the alarm. */ reminder = [[info objectForKey: @"Reminder"] intValue]; [lastAlertInfo removeObjectForKey: key]; [self reportAlarm: old withMessage: mesg isCleared: YES reminder: reminder]; } } } /* Now, send the alarm to the alarm sink. */ [sink alarm: alarm]; } - (NSString*) description { return [NSString stringWithFormat: @"%@ running since %@\n" @" Alarms which generated alerts: %"PRIu64"\n" @" Alarms ignored (low severity): %"PRIu64"\n%@\n%@", [super description], [self ecStarted], alarmsAlerted, alarmsIgnored, alerter, sink]; } - (oneway void) domanage: (NSString*)name { [sink domanage: name]; } - (NSFileHandle*) openLog: (NSString*)lname { NSFileHandle *lf; if ([mgr isWritableFileAtPath: lname] == NO) { if ([mgr createFileAtPath: lname contents: nil attributes: nil] == NO) { NSLog(@"Log file '%@' is not writable and can't be created", lname); return nil; } } lf = [NSFileHandle fileHandleForUpdatingAtPath: lname]; if (lf == nil) { NSLog(@"Unable to log to %@", lname); return nil; } [lf seekToEndOfFile]; return lf; } - (void) cmdDefaultsChanged: (NSNotification*)n { NSRegularExpression *re = nil; NSString *str; [super cmdDefaultsChanged: n]; str = [[self cmdDefaults] stringForKey: @"AlarmFilter"]; if (str) { re = [[NSRegularExpression alloc] initWithPattern: str options: 0 error: NULL]; } [self ecDoLock]; ASSIGN(alarmFilter, re); [self ecUnLock]; } - (oneway void) cmdGnip: (id )from sequence: (unsigned)num extra: (NSData*)data { EcClientI *r; /* * See if we have a fitting client - and update records. */ r = [self findIn: commands byObject: (id)from]; if (r == nil) { r = [self findIn: consoles byObject: (id)from]; } if (r != nil) { [r gnip: num]; } } - (BOOL) cmdIsClient { return NO; // Control is not a client of Command } - (oneway void) cmdPing: (id )from sequence: (unsigned)num extra: (NSData*)data { [from cmdGnip: self sequence: num extra: nil]; } - (oneway void) cmdQuit: (NSInteger)status { [sink shutdown]; exit (status); } - (void) command: (NSData*)dat from: (NSString*)f { NSMutableArray *cmd; ConsoleInfo *console; BOOL silent = NO; cmd = [NSPropertyListSerialization propertyListWithData: dat options: NSPropertyListMutableContainers format: 0 error: 0]; if ([[cmd firstObject] isEqual: @"silent"]) { silent = YES; [cmd removeObjectAtIndex: 0]; } console = (ConsoleInfo*)[self findIn: consoles byName: f]; if (console == nil) { NSString *m; m = [NSString stringWithFormat: cmdLogFormat(LT_ERROR, @"command from unregistered console (from %@)"), f]; [self information: m type: LT_ERROR to: nil from: nil]; } else if (cmd == nil || [cmd count] == 0) { NSString *m; m = [NSString stringWithFormat: cmdLogFormat(LT_AUDIT, @"no command entered (from %@ %@)"), f, [console name]]; [self information: m type: LT_AUDIT to: [console name] from: nil]; } else { NSMutableString *full; NSArray *allow; NSString *hname = nil; NSString *m = @""; NSString *wd = cmdWord(cmd, 0); unsigned count = [cmd count]; unsigned i; BOOL connected = NO; /* * Log this command! */ full = [[NSMutableString alloc] initWithCapacity: 1024]; for (i = 0; i < count; i++) { [full appendFormat: @" %@", [cmd objectAtIndex: i]]; } [[self cmdLogFile: logname] printf: @"Console (%@): %@ Cmd -%@\n", [console name], [NSDate date], full]; RELEASE(full); if ([console chost] != nil || [console cserv] != nil) { connected = YES; } /* If we have a 'control' command - act as if the console was not * connected to a host or server. */ if (comp(wd, @"control") == 0) { [cmd removeObjectAtIndex: 0]; wd = cmdWord(cmd, 0); connected = NO; } #if !defined(HAVE_LIBCRYPT) if (comp(wd, @"password") == 0) { NSRange r = [[console name] rangeOfString: @":"]; NSString *s = [[console name] substringToIndex: r.location]; NSMutableDictionary *d; NSString *p; NSString *path; if ([cmd count] != 3) { [self information: @"parameters are old password and new one.\n" type: LT_CONSOLE to: [console name] from: nil]; return; } if (operators == nil) { operators = [NSMutableDictionary dictionaryWithCapacity: 1]; RETAIN(operators); [operators setObject: @"" forKey: s]; } d = AUTORELEASE([[operators objectForKey: s] mutableCopy]); p = [d objectForKey: @"Password"]; if ([p length] == 0 || [p isEqual: [cmd objectAtIndex: 1]]) { [d setObject: [cmd objectAtIndex: 2] forKey: @"Password"]; [operators setObject: d forKey: s]; p = [operators description]; path = [[self cmdDataDirectory] stringByAppendingPathComponent: @"Operators.plist"]; [p writeToFile: path atomically: YES]; [self information: @"new password set.\n" type: LT_CONSOLE to: [console name] from: nil]; return; } else { [self information: @"old password is incorrect.\n" type: LT_CONSOLE to: [console name] from: nil]; return; } } #endif /* * If we are not connected, but have an 'on ....' at the start of * the command line, then we send to the specified host as if we * were connected to it. */ if (connected) { hname = [console chost]; } else if (comp(wd, @"on") == 0) { [cmd removeObjectAtIndex: 0]; wd = cmdWord(cmd, 0); hname = AUTORELEASE(RETAIN(wd)); if ([hname length] > 0) { CommandInfo *c; c = (CommandInfo*)[self findIn: commands byAbbreviation: hname]; if (c) { hname = [c name]; [cmd removeObjectAtIndex: 0]; } else { [self information: @"Attempt to work on unknown host" type: LT_CONSOLE to: [console name] from: nil]; return; } wd = cmdWord(cmd, 0); } else { hname = nil; } } /* Find the commands allowed for this user. */ allow = [self ecCommands: [console name]]; if (connected == YES || hname != nil) { NSArray *hosts; /* Make an array of hosts to work with - the connected host * or all hosts. If the connected host has gone away - disconnect. */ if (hname == nil) { hosts = [NSArray arrayWithArray: commands]; } else { CommandInfo *c; c = (CommandInfo*)[self findIn: commands byName: hname]; if (c) { hosts = [NSArray arrayWithObject: c]; } else { if (connected) { [console setConnectedHost: nil]; } [self information: @"Host has gone away" type: LT_ERROR to: [console name] from: nil]; return; } } if ([wd length] == 0) { /* Quietly ignore. */ } else { BOOL foundServer = NO; int i; m = nil; /* Let remote host generate messages. */ /* Perform operation on connected host (or all hosts if * not connected to a host). */ for (i = 0; i < [hosts count]; i++) { CommandInfo *c = [hosts objectAtIndex: i]; if ([commands indexOfObjectIdenticalTo: c] != NSNotFound) { if (NO == connected || [console cserv] == nil) { foundServer = YES; NS_DURING { NSData *dat = [NSPropertyListSerialization dataFromPropertyList: cmd format: NSPropertyListBinaryFormat_v1_0 errorDescription: 0]; [[c obj] command: dat to: nil from: [console name]]; } NS_HANDLER { NSLog(@"Caught: %@", localException); } NS_ENDHANDLER } else { NSString *to; to = [c serverByAbbreviation: [console cserv]]; if (to) { foundServer = YES; NS_DURING { NSData *dat; dat = [NSPropertyListSerialization dataFromPropertyList: cmd format: NSPropertyListBinaryFormat_v1_0 errorDescription: 0]; [[c obj] command: dat to: to from: [console name]]; } NS_HANDLER { NSLog(@"Caught: %@", localException); } NS_ENDHANDLER } } } } if (foundServer == NO) { [console setConnectedServ: nil]; [self information: @"Server has gone away" type: LT_CONSOLE to: [console name] from: nil]; } } } else if ([wd length] == 0) { /* Quietly ignore. */ } else if (matchCmd(wd, @"alarms", allow)) { NSArray *a = [sink alarms]; if (0 == [a count]) { m = @"No alarms currently active.\n"; } else { int i; a = [a sortedArrayUsingSelector: @selector(compare:)]; m = @"Current alarms -\n"; for (i = 0; i < [a count]; i++) { EcAlarm *alarm = [a objectAtIndex: i]; m = [m stringByAppendingString: [alarm description]]; m = [m stringByAppendingString: @"\n"]; } } } else if (matchCmd(wd, @"archive", allow)) { m = [NSString stringWithFormat: @"\n%@\n\n", [self ecArchive: nil]]; } else if (matchCmd(wd, @"clear", allow)) { NSArray *a = [sink alarms]; unsigned index = 1; m = @""; while ([(wd = cmdWord(cmd, index++)) length] > 0) { EcAlarm *alarm = nil; int n = [wd intValue]; int i = [a count]; while (i-- > 0) { alarm = [a objectAtIndex: i]; if ([alarm notificationID] == n) { break; } alarm = nil; } if (nil == alarm) { m = [NSString stringWithFormat: @"%@No alarm found with ID %@\n", m, wd]; } else { NSArray *hosts = [[commands copy] autorelease]; NSUInteger i; m = [NSString stringWithFormat: @"%@Clearing %@\n", m, alarm]; alarm = [alarm clear]; for (i = 0; i < [hosts count]; i++) { CommandInfo *c = [hosts objectAtIndex: i]; if ([commands indexOfObjectIdenticalTo: c] != NSNotFound) { NS_DURING { [[c obj] clear: alarm]; } NS_HANDLER { NSLog(@"Caught: %@", localException); } NS_ENDHANDLER } } [self alarm: alarm]; // In case the originator failed } } if (0 == [m length]) { m = @"The 'suppress' command requires one or more IDs\n" @"These are the unique identifiers used for working with\n" @"external SNMP monitoring systems.\n"; } } else if (matchCmd(wd, @"connect", nil)) { wd = cmdWord(cmd, 1); if ([wd length] == 0) { [console setConnectedServ: nil]; } else { [console setConnectedServ: wd]; } } else if (matchCmd(wd, @"config", allow)) { BOOL changed; changed = [self update]; if (configFailed != nil) { m = configFailed; } else { if (configIncludeFailed != nil) { m = [NSString stringWithFormat: @"Configuration file re-read and loaded, but %@", configIncludeFailed]; } else if (YES == changed) { m = @"Configuration file re-read ... updates handled.\n"; } else { m = @"Configuration file re-read ... UNCHANGED.\n"; } } [self information: m type: LT_CONSOLE to: [console name] from: nil]; [self information: [NSString stringWithFormat: cmdLogFormat(LT_AUDIT, @"CONSOLE_CONFIG 1 %@"), m] type: LT_AUDIT to: nil from: nil]; } else if (matchCmd(wd, @"flush", allow)) { [alerter flushSms]; [alerter flushEmail]; m = @"Flushed alert messages\n"; } else if (matchCmd(wd, @"help", allow)) { wd = cmdWord(cmd, 1); if ([wd length] == 0) { m = @"Commands are -\n" @"Help\tAlarms\tArchive\tClear\tConfig\tConnect\t" @"Flush\tHost\tList\tMemory\tOn\t" #if !defined(HAVE_LIBCRYPT) @"Password\t" #endif @"Repeat\tRestart\tQuit\tSet\tStatus\t" @"Suppress\tTell\tUnset\n\n" @"Type 'help' followed by a command word for details.\n" @"Use 'tell xxx help' to get help for a specific client.\n" @"A command line consists of a sequence of words, " @"the first of which is the command to be executed. " @"A word can be a simple sequence of non-space characters, " @"or it can be a 'quoted string'. " @"Simple words are converted to lower case before " @"matching them against commands and their parameters. " @"Text in a 'quoted string' is NOT converted to lower case " @"but a '\\' character is treated in a special manner -\n" @" \\b is replaced by a backspace\n" @" \\f is replaced by a formfeed\n" @" \\n is replaced by a linefeed\n" @" \\r is replaced by a carriage-return\n" @" \\t is replaced by a tab\n" @" \\0 followed by up to 3 octal digits is replaced" @" by the octal value\n" @" \\x followed by up to 2 hex digits is replaced" @" by the hex value\n" @" \\ followed by any other character is replaced by" @" the second character.\n" @" This permits use of quotes and backslashes inside" @" a quoted string.\n"; } else { if (comp(wd, @"Alarms") >= 0) { m = @"Alarms\nLists the currently active alarms ordered " @"by their notificationIDs.\n" @"The notification ID is the unique identifier used " @"for working with\n" @"external SNMP monitoring systems.\n"; } else if (comp(wd, @"Archive") >= 0) { m = @"Archive\nArchives the log file. The archived log " @"file is stored in a subdirectory whose name is of " @"the form YYYY-MM-DD being the date at " @"which the archive was created.\n"; } else if (comp(wd, @"Clear") >= 0) { m = @"Clear\n\n" @"Instructs the Control server to clear " @"one or more alarms (identified by numeric\n" @"notificationIDs).\n\n" @"NB. This command clears the alarm(s) in the " @"central records of the Control server,\n" @"and also in the originating process.\n" @"You cannot clear an alarm centrally once " @"it has been suppressed; in that case you\n" @"must issue a 'clear'command directly to " @"the originating process itsself.\n"; } else if (comp(wd, @"Config") >= 0) { m = @"Config\nInstructs the Control server to re-load " @"it's configuration information and pass it on to " @"all connected Command servers.\n"; } else if (comp(wd, @"Connect") >= 0) { m = @"Connect name\nInstructs the Control server that " @"commands from your console should go to clients " @"whose names match the value you give.\n" @"eg. 'connect foo' is equivalent to prefixing " @"subsequent commands with 'tell foo'\n'" @"To remove this association, you will need to type " @"the command 'control connect'\n'"; } else if (comp(wd, @"Flush") >= 0) { m = @"Flush\nInstructs the Control server that messages " @"about errors and alerts which are currently saved " @"in the batching process should be sent out.\n"; } else if (comp(wd, @"Host") >= 0) { m = @"Host name\nInstructs the Control server that " @"commands from your console should go to the host " @"whose names match the value you give.\n" @"eg. 'host xxx' is equivalent to prefixing " @"subsequent commands with 'on xxx'\n'" @"To remove this association, you will need to type " @"the command 'control host'\n'"; } else if (comp(wd, @"List") >= 0) { m = @"List\nLists all the connected Command processes " @"and any client processes on each.\n" @"List consoles\nLists all the connected consoles.\n"; } else if (comp(wd, @"Memory") >= 0) { m = @"Memory\nDisplays recent memory allocation stats.\n" @"Memory all\nDisplays all memory allocation stats.\n"; } else if (comp(wd, @"On") >= 0) { m = @"On host ...\nSends a command to the named host.\n" @"eg. 'on remotehost tell myserver help'.\n"; } #if !defined(HAVE_LIBCRYPT) else if (comp(wd, @"Password") >= 0) { m = @"Password \nSets your password.\n"; } #endif else if (comp(wd, @"Quit") >= 0) { m = @"Quit 'self'\n" @"Shuts down the Control server process - don't do " @"this lightly - all other servers are coordinated " @"by the Control server\n" @"\nUse 'on quit ' to shut down " @"individual servers on the specified host.\n" @"\nUse 'on quit all' to shut down all the " @"servers on the specified host.\n" @"\nUse 'on quit self' to shut down the " @"Command server on the specified host - be careful " @"using this - the other servers on a host are all " @"coordinated (and log through) the Command server.\n"; } else if (comp(wd, @"Repeat") >= 0) { m = @"Repeat\nRepeat the last command.\n"; } else if (comp(wd, @"Restart") >= 0) { m = @"Restart all\nAsks all hosts to restart all clients.\n" @"Restart self\nRestarts the Control server itself.\n" @"NB. On a system using encryption with EcControlKey,\n" @"restarting the server does not require key re-entry.\n"; } else if (comp(wd, @"Set") >= 0) { m = @"Set\n" @"Changes settings -\n" @" set display warnings\n" @" displays warning messages from the server to " @"which you are connected.\n" @" set display general\n" @" display messages (if selected) " @"from ALL servers, not just connected ones.\n" @" set display errors\n" @" displays error (and major/minor severity alarm) messages.\n" @" set display alerts\n" @" displays alert (and critical severity alarm) messages.\n" @" set display audits\n" @" displays audit messages.\n" @" set display mark\n" @" marks start and end of messages.\n" @" set display prompt\n" @" outputs a prompt at the end of each message.\n" @"\n"; } else if (comp(wd, @"Status") >= 0) { m = @"Status\nInstructs the Control server to report its " @"current status ... mostly the buffered alert and " @"error messages waiting to be sent out.\n"; } else if (comp(wd, @"Suppress") >= 0) { m = @"Suppress\n\n" @"Instructs the Control server to suppress " @"one or more alarms (identified by numeric\n" @"notificationIDs).\n\n" @"NB. This command clears the alarm(s) in the " @"central records of the Control server,\n" @"but NOT in the originating process.\n" @"This feature means that you can suppress " @"an alarm centrally while the underlying\n" @"problem has not been corrected and, " @"because the originating process has\n" @"already forwarded the alarm it will " @"not re-raise it with the central system.\n" @"This can be useful where you wish to " @"suppress some sort of repeated nagging by\n" @"the central system.\n" @"To reset things so the alarm may be " @"raised again you must issue a 'clear'\n" @"command directly to the originating process itsself.\n"; } else if (comp(wd, @"Tell") >= 0) { m = @"Tell 'name' 'command'\n" @"Sends the command to the named client on the host " @"specified by 'host' or 'on', or by default on the " @"same host as the Control server.\n" @"eg. 'tell myserver help'.\n" @"Use 'all' as a special case to send a command to " @"all clients on the host.\n" @"Use an integer value to send a command to the " @"client listed at that position on the host.\n" @"Otherwise, the command is sent to any client " @"whose name matches the text entered.\n " @"A 'tell' to a non-existent client is ignored.\n"; } else if (comp(wd, @"UnSet") >= 0) { m = @"UnSet\n" @"Changes settings -\n" @" unset display general\n" @" unset display warnings\n" @" unset display errors\n" @" unset display alerts\n" @" unset display audits\n" @" unset display mark\n" @" unset display prompt\n" @"\n"; } } } else if (matchCmd(wd, @"host", allow)) { wd = cmdWord(cmd, 1); if ([wd length] == 0) { [console setConnectedHost: nil]; } else { CommandInfo *c; c = (CommandInfo*)[self findIn: commands byAbbreviation: wd]; if (c) { [console setConnectedHost: [c name]]; } } } else if (matchCmd(wd, @"list", allow)) { wd = cmdWord(cmd, 1); if ([wd length] > 0 && comp(wd, @"consoles") >= 0) { if ([consoles count] == 1) { m = @"No other consoles currently connected.\n"; } else { int i; m = @"Current console processes -\n"; for (i = 0; i < [consoles count]; i++) { ConsoleInfo *c; c = (ConsoleInfo*)[consoles objectAtIndex: i]; if (c == console) { m = [m stringByAppendingFormat: @"%2d. your console\n", i]; } else { m = [m stringByAppendingFormat: @"%2d. %s\n", i, [[c name] cString]]; } } } } else { if ([commands count] == 0) { if (nil == alerter) { m = @"No database/alerter available.\n"; } else { m = @"No hosts currently connected.\n"; } } else { int i; m = @"Current server hosts -\n"; for (i = 0; i < [commands count]; i++) { CommandInfo* c; c = (CommandInfo*)[commands objectAtIndex: i]; m = [m stringByAppendingFormat: @"%2d. %-32.32s\n", i, [[c name] cString]]; if ([c servers] == nil || [[c servers] count] == 0) { m = [m stringByAppendingString: @" no servers connected\n"]; } else { NSArray *svrs = [c servers]; int j; for (j = 0; j < [svrs count]; j++) { m = [m stringByAppendingFormat: @" %2d. %@\n", j, [svrs objectAtIndex: j]]; } } } } } } else if (matchCmd(wd, @"memory", allow)) { if (GSDebugAllocationActive(YES) == NO) { m = @"Memory statistics were not being gathered.\n" @"Statistics Will start from NOW.\n"; } else { const char* list; wd = cmdWord(cmd, 1); if ([wd length] > 0 && comp(wd, @"all") >= 0) { list = GSDebugAllocationList(NO); } else { list = GSDebugAllocationList(YES); } m = [NSString stringWithCString: list]; } } else if (matchCmd(wd, @"quit", allow)) { m = @"Try 'help quit' for information about shutting down.\n"; wd = cmdWord(cmd, 1); if ([wd length] > 0 && comp(wd, @"self") == 0) { [sink setMonitor: nil]; if (alerter) { [alerter shutdown]; NSLog(@"Uninstalled alerter: %@", NSStringFromClass([alerter class])); DESTROY(alerter); } exit(0); } } else if (matchCmd(wd, @"restart", allow)) { wd = cmdWord(cmd, 1); if ([wd length] > 0 && comp(wd, @"self") == 0) { if (terminating == nil) { [self information: @"Re-starting Control server\n" type: LT_CONSOLE to: nil from: nil]; exit(-1); // Watcher should restart us } else { m = @"Already terminating!\n"; } } else if ([wd length] > 0 && comp(wd, @"all") == 0) { NSUInteger i; NSArray *hosts = [[commands copy] autorelease]; for (i = 0; i < [hosts count]; i++) { CommandInfo *c = [hosts objectAtIndex: i]; if ([commands indexOfObjectIdenticalTo: c] != NSNotFound) { NS_DURING { NSData *dat = [NSPropertyListSerialization dataFromPropertyList: cmd format: NSPropertyListBinaryFormat_v1_0 errorDescription: 0]; [[c obj] command: dat to: nil from: [console name]]; } NS_HANDLER { NSLog(@"Caught: %@", localException); } NS_ENDHANDLER } } } else { m = @"Try 'restart self', 'restart all'" @" or 'on host restart ...\n"; } } else if (matchCmd(wd, @"set", allow)) { m = @"ok - set confirmed.\n"; wd = cmdWord(cmd, 1); if ([wd length] == 0) { m = @"no parameter to 'set'\n"; } else if (comp(wd, @"display") >= 0) { wd = cmdWord(cmd, 2); if ([wd length] == 0) { m = [NSString stringWithFormat: @"display settings -\n" @"general: %d warnings: %d errors: %d alerts: %d" @" audits: %d mark:%@ prompt:%@\n", [console getGeneral], [console getWarnings], [console getErrors], [console getAlerts], [console getAudits], ([console getMark] ? @"yes" : @"no"), ([console getPrompt] ? @"yes" : @"no")]; } else if (comp(wd, @"alerts") >= 0) { [console setAlerts: YES]; } else if (comp(wd, @"audits") >= 0) { [console setAudits: YES]; } else if (comp(wd, @"errors") >= 0) { [console setErrors: YES]; } else if (comp(wd, @"general") >= 0) { [console setGeneral: YES]; } else if (comp(wd, @"mark") >= 0) { [console setMark: YES]; } else if (comp(wd, @"prompt") >= 0) { [console setPrompt: YES]; } else if (comp(wd, @"warnings") >= 0) { [console setWarnings: YES]; } else { m = @"unknown parameter to 'set display'\n"; } } else { m = @"unknown parameter to 'set'\n"; } } else if (matchCmd(wd, @"status", allow)) { m = [self description]; } else if (matchCmd(wd, @"suppress", allow)) { NSArray *a = [sink alarms]; unsigned index = 1; m = @""; while ([(wd = cmdWord(cmd, index++)) length] > 0) { EcAlarm *alarm = nil; int n = [wd intValue]; int i = [a count]; while (i-- > 0) { alarm = [a objectAtIndex: i]; if ([alarm notificationID] == n) { break; } alarm = nil; } if (nil == alarm) { m = [NSString stringWithFormat: @"%@No alarm found with ID %@\n", m, wd]; } else { m = [NSString stringWithFormat: @"%@Suppressing %@\n", m, alarm]; alarm = [alarm clear]; [self alarm: alarm]; } } if (0 == [m length]) { m = @"The 'suppress' command requires one or more IDs\n" @"These are the unique identifiers used for working with\n" @"external SNMP monitoring systems.\n"; } } else if (matchCmd(wd, @"tell", nil)) { wd = cmdWord(cmd, 1); if ([wd length] > 0) { NSUInteger i; NSArray *a; /* A simple 'tell' command which was not sent to a specific * host using 'on host tell ...' should be forwarded to each * Command server. */ a = [NSArray arrayWithArray: commands]; for (i = 0; i < [a count]; i++) { CommandInfo* c = (CommandInfo*)[a objectAtIndex: i]; if ([commands indexOfObjectIdenticalTo: c] != NSNotFound) { NS_DURING { [[c obj] command: dat to: nil from: [console name]]; } NS_HANDLER { NSLog(@"Caught: %@", localException); } NS_ENDHANDLER } } m = nil; } else { m = @"Tell where?.\n"; } } else if (matchCmd(wd, @"unset", allow)) { m = @"ok - unset confirmed.\n"; wd = cmdWord(cmd, 1); if ([wd length] == 0) { m = @"no parameter to 'set'\n"; } else if (comp(wd, @"display") >= 0) { wd = cmdWord(cmd, 2); if ([wd length] == 0) { m = [NSString stringWithFormat: @"display settings -\n" @"general: %d warnings: %d errors: %d alerts: %d" @" audits: %d mark:%@ prompt:%@\n", [console getGeneral], [console getWarnings], [console getErrors], [console getAlerts], [console getAudits], ([console getMark] ? @"yes" : @"no"), ([console getPrompt] ? @"yes" : @"no")]; } else if (comp(wd, @"alerts") >= 0) { [console setAlerts: NO]; } else if (comp(wd, @"audits") >= 0) { [console setAudits: NO]; } else if (comp(wd, @"errors") >= 0) { [console setErrors: NO]; } else if (comp(wd, @"general") >= 0) { [console setGeneral: NO]; } else if (comp(wd, @"mark") >= 0) { [console setMark: NO]; } else if (comp(wd, @"prompt") >= 0) { [console setPrompt: NO]; } else if (comp(wd, @"warnings") >= 0) { [console setWarnings: NO]; } else { m = @"unknown parameter to 'unset display'\n"; } } else { m = @"unknown parameter to 'unset'\n"; } } else { m = [NSString stringWithFormat: @"Unknown command - '%@'\n", wd]; } if (m != nil && NO == silent) { [self information: m type: LT_CONSOLE to: [console name] from: ecFullName()]; } } } - (BOOL) connection: (NSConnection*)ancestor shouldMakeNewConnection: (NSConnection*)newConn { [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(connectionBecameInvalid:) name: NSConnectionDidDieNotification object: (id)newConn]; [newConn setDelegate: self]; return YES; } - (id) connectionBecameInvalid: (NSNotification*)notification { id conn = [notification object]; [[NSNotificationCenter defaultCenter] removeObserver: self name: NSConnectionDidDieNotification object: conn]; if ([conn isKindOfClass: [NSConnection class]]) { NSMutableArray *a = [NSMutableArray arrayWithCapacity: 2]; NSMutableString *m; unsigned i; /* * Now remove any consoles that have disconnected. */ m = [NSMutableString stringWithCapacity: 100]; i = [consoles count]; while (i-- > 0) { ConsoleInfo *o = (ConsoleInfo*)[consoles objectAtIndex: i]; if ([[o obj] connectionForProxy] == conn) { [a addObject: o]; [m appendFormat: cmdLogFormat(LT_AUDIT, @"removed (lost) console '%@'"), [o name]]; [consoles removeObjectAtIndex: i]; if (i <= consolePingPosition && consolePingPosition > 0) { consolePingPosition--; } } } if ([m length] > 0) { [self information: m type: LT_AUDIT to: nil from: nil]; [m setString: @""]; } /* * Now remove the commands from the active list. */ i = [commands count]; while (i-- > 0) { CommandInfo* o = (CommandInfo*)[commands objectAtIndex: i]; if ([[o obj] connectionForProxy] == conn) { EcAlarm *e; NSString *s; [a addObject: o]; s = EcMakeManagedObject([o name], @"Command", nil); e = [EcAlarm alarmForManagedObject: s at: nil withEventType: EcAlarmEventTypeProcessingError probableCause: EcAlarmSoftwareProgramAbnormallyTerminated specificProblem: @"Host command/control availability" perceivedSeverity: EcAlarmSeverityCritical proposedRepairAction: @"Check network/host/process" additionalText: @"remove (lost) host"]; [self alarm: e]; [commands removeObjectAtIndex: i]; if (i <= commandPingPosition && commandPingPosition > 0) { commandPingPosition--; } } } [a removeAllObjects]; } else { [self error: "non-Connection sent invalidation"]; } if (nil != terminating && 0 == [commands count]) { [self cmdQuit: 0]; } return self; } - (void) dealloc { [sink setMonitor: nil]; if (alerter) { [alerter shutdown]; NSLog(@"Uninstalled alerter: %@", NSStringFromClass([alerter class])); DESTROY(alerter); } [self cmdLogEnd: logname]; if (timer != nil) { [timer invalidate]; } DESTROY(mgr); DESTROY(fileBodies); DESTROY(fileDates); DESTROY(operators); DESTROY(config); DESTROY(commands); DESTROY(consoles); DESTROY(configFailed); DESTROY(configIncludeFailed); DESTROY(alarmFilter); DESTROY(controlConfig); [super dealloc]; } - (EcClientI*) findIn: (NSArray*)a byAbbreviation: (NSString*)s { EcClientI *o; int i; int best_pos = -1; int best_len = 0; /* * Special case - a numeric value is used as an index into the array. */ if (isdigit(*[s cString])) { i = [s intValue]; if (i >= 0 && i < [a count]) { return (EcClientI*)[a objectAtIndex: i]; } } for (i = 0; i < [a count]; i++) { o = (EcClientI*)[a objectAtIndex: i]; if (comp(s, [o name]) == 0) { return o; } if (comp_len > best_len) { best_len = comp_len; best_pos = i; } } if (best_pos >= 0) { return (EcClientI*)[a objectAtIndex: best_pos]; } return nil; } - (EcClientI*) findIn: (NSArray*)a byName: (NSString*)s { EcClientI *o; int i; for (i = 0; i < [a count]; i++) { o = (EcClientI*)[a objectAtIndex: i]; if (comp([o name], s) == 0) { return o; } } return nil; } - (EcClientI*) findIn: (NSArray*)a byObject: (id)s { EcClientI *o; int i; for (i = 0; i < [a count]; i++) { o = (EcClientI*)[a objectAtIndex: i]; if ([o obj] == s) { return o; } } return nil; } - (void) information: (NSString*)inf type: (EcLogType)t to: (NSString*)to from: (NSString*)from { /* * Send anything but debug or accounting info to consoles. */ if (t != LT_DEBUG) { NSArray *a; unsigned i; /* * Work with a copy of the consoles array in case one goes away * or is added while we are doing this! */ a = [NSArray arrayWithArray: consoles]; for (i = 0; i < [a count]; i++) { ConsoleInfo *c = [a objectAtIndex: i]; NSString *name = [c name]; /* * If this console has gone away - skip it. */ if ([consoles indexOfObjectIdenticalTo: c] == NSNotFound) { continue; } /* * If 'to' is nil - send to all consoles */ if (to != nil && [to isEqual: name] == NO) { continue; } /* * If this is a warning message and the console is not interested * in warning then we skip this console. */ if (t == LT_WARNING) { if ([c getWarnings] == NO) { continue; } else if ([c getGeneral] == NO) { if (to != nil && [to isEqual: name] == NO) { continue; } } } else if (t == LT_ERROR && [c getErrors] == NO && (nil == to || [to isEqual: name] == NO)) { continue; } else if (t == LT_ALERT && [c getAlerts] == NO && (nil == to || [to isEqual: name] == NO)) { continue; } else if (t == LT_AUDIT && [c getAudits] == NO && (nil == to || [to isEqual: name] == NO)) { continue; } NS_DURING { /* Finally - we send the message to the console along with * a prompt. */ [[c obj] information: [c promptAfter: inf from: from]]; } NS_HANDLER { NSLog(@"Caught: %@", localException); } NS_ENDHANDLER } } /* * Log, alerts, and accounting get written to the log file too. */ if (t == LT_AUDIT || t == LT_ALERT || t == LT_CONSOLE) { [[self cmdLogFile: logname] puts: inf]; } /* * Errors, audit logs, and alerts (severe errors) get passed to a handler. */ if (t == LT_ERROR || t == LT_AUDIT || t == LT_ALERT || t == LT_CONSOLE) { if (alerter != nil) { [alerter handleInfo: inf]; } } } - (id) initWithDefaults: (NSDictionary*)defs { ASSIGN(controlKey, [defs objectForKey: @"EcControlKey"]); ecSetLogsSubdirectory(@"Logs"); self = [super initWithDefaults: defs]; if (self != nil) { commands = [[NSMutableArray alloc] initWithCapacity: 10]; consoles = [[NSMutableArray alloc] initWithCapacity: 2]; logname = [[self cmdName] stringByAppendingPathExtension: @"log"]; RETAIN(logname); mgr = [NSFileManager defaultManager]; RETAIN(mgr); if ([self cmdLogFile: logname] == nil) { exit(0); } [self update]; if (configFailed != nil) { [self cmdQuit: 1]; return nil; } if (operators == nil) { [self cmdQuit: 1]; return nil; } fileBodies = [[NSMutableDictionary alloc] initWithCapacity: 8]; fileDates = [[NSMutableDictionary alloc] initWithCapacity: 8]; timer = [NSTimer scheduledTimerWithTimeInterval: 15.0 target: self selector: @selector(timedOut:) userInfo: nil repeats: YES]; [self timedOut: nil]; } return self; } - (void) ecAwaken { [super ecAwaken]; [[self ecAlarmDestination] setCoalesce: NO]; } - (int) ecRun { int result; /* Ensure config files have been read. */ [self update]; /* Start the SNMP alarm sink before entering run loop. */ NSDictionary *alertConf = [[self cmdDefaults] dictionaryForKey: @"Alerter"]; NSString *host = [alertConf objectForKey: @"SNMPMasterAgentHost"]; NSString *port = [alertConf objectForKey: @"SNMPMasterAgentPort"]; lastAlertInfo = [NSMutableDictionary new]; sink = [[EcAlarmSinkSNMP alloc] initWithHost: host name: port]; /* An alerter which confrms to the correct protocol is assumed to * want to monitor alarms. */ if ([alerter conformsToProtocol: @protocol(EcAlarmMonitor)]) { [sink setMonitor: (id)alerter]; } result = [super ecRun]; [sink shutdown]; DESTROY(sink); return result; } - (void) quitAll { NSArray *hosts; NSUInteger i; hosts = [[commands copy] autorelease]; i = [hosts count]; while (i-- > 0) { CommandInfo *c = [hosts objectAtIndex: i]; if ([commands indexOfObjectIdenticalTo: c] != NSNotFound) { NS_DURING { [[c obj] terminate: nil]; } NS_HANDLER { NSLog(@"Caught: %@", localException); } NS_ENDHANDLER } } if (0 == [commands count]) { [self cmdQuit: 0]; } } - (NSData*) registerCommand: (id)c name: (NSString*)n { NSMutableDictionary *dict; CommandInfo *obj; CommandInfo *old; NSHost *h; NSString *m; EcAlarm *a; id o; if (nil == alerter) { [self update]; // Regenerate info from Control.plist if (nil == alerter) { m = [NSString stringWithFormat: @"Rejected new host '%@' (not ready yet; no alerter) at %@\n", n, [NSDate date]]; [self information: m type: LT_CONSOLE to: nil from: nil]; return nil; // Not fully configured yet } } [(NSDistantObject*)c setProtocolForProxy: @protocol(Command)]; dict = [NSMutableDictionary dictionaryWithCapacity: 3]; h = [NSHost hostWithWellKnownName: n]; if (nil == h) { h = [NSHost hostWithName: n]; } if (nil == h) { m = [NSString stringWithFormat: @"Rejected new host with bad name '%@' at %@\n", n, [NSDate date]]; [self information: m type: LT_CONSOLE to: nil from: nil]; [dict setObject: @"unable to handle given hostname." forKey: @"rejected"]; return [NSPropertyListSerialization dataFromPropertyList: dict format: NSPropertyListBinaryFormat_v1_0 errorDescription: 0]; } n = [h wellKnownName]; old = (CommandInfo*)[self findIn: commands byObject: c]; if (old != nil && [[old name] isEqual: n]) { m = [NSString stringWithFormat: @"Re-registered existing host with name '%@' at %@\n", n, [NSDate date]]; [self information: m type: LT_CONSOLE to: nil from: nil]; } else { /* * Create a new reference for this client. */ obj = [[CommandInfo alloc] initFor: c name: n with: self]; old = (CommandInfo*)[self findIn: commands byName: n]; [commands addObject: obj]; RELEASE(obj); [commands sortUsingSelector: @selector(compare:)]; if (nil == old) { m = [NSString stringWithFormat: @"Registered new host with name '%@' at %@\n", n, [NSDate date]]; [self domanage: EcMakeManagedObject(n, @"Command", nil)]; } else { [old setObj: nil]; m = [NSString stringWithFormat: @"Re-registered host with name '%@' at %@\n", n, [NSDate date]]; [commands removeObjectIdenticalTo: old]; } [self information: m type: LT_CONSOLE to: nil from: nil]; } /* Inform SNMP monitoring of new Command server. */ a = [EcAlarm alarmForManagedObject: EcMakeManagedObject(n, @"Command", nil) at: nil withEventType: EcAlarmEventTypeProcessingError probableCause: EcAlarmSoftwareProgramAbnormallyTerminated specificProblem: @"Host command/control availability" perceivedSeverity: EcAlarmSeverityCleared proposedRepairAction: nil additionalText: nil]; [self alarm: a]; /* * Return configuration information - general stuff, plus * host specific stuff. */ o = [config objectForKey: @"*"]; if (o != nil) { [dict setObject: o forKey: @"*"]; } // Configuration keys may be NSHost objects o = [config objectForKey: (NSString*)h]; if (o != nil) { [dict setObject: o forKey: n]; } if (operators != nil) { [dict setObject: operators forKey: @"Operators"]; } return [NSPropertyListSerialization dataFromPropertyList: dict format: NSPropertyListBinaryFormat_v1_0 errorDescription: 0]; } - (NSString*) registerConsole: (id)c name: (NSString*)n pass: (NSString*)p { id obj; NSString *m; [(NSDistantObject*)c setProtocolForProxy: @protocol(Console)]; obj = [self findIn: consoles byName: n]; if (obj != nil) { m = [NSString stringWithFormat: cmdLogFormat(LT_AUDIT, @"CONSOLE_LOGIN_FAILED 1 Rejected console" @" with info '%@' (already registered by name)"), n]; [self information: m type: LT_AUDIT to: nil from: nil]; return @"Already registered by that name"; } obj = [self findIn: consoles byObject: c]; if (obj != nil) { m = [NSString stringWithFormat: cmdLogFormat(LT_AUDIT, @"CONSOLE_LOGIN_FAILED 1 Rejected console with" @" info '%@' (already registered)"), n]; [self information: m type: LT_AUDIT to: nil from: nil]; return @"Already registered"; /* Already registered. */ } if (operators != nil) { NSRange r = [n rangeOfString: @":"]; NSString *user = [n substringToIndex: r.location]; NSDictionary *info = [operators objectForKey: user]; NSString *passwd = [info objectForKey: @"Password"]; if (nil == info) { /* If Operators.plist contains a user with an empty string as * a name, this will match any login attempt not already matched. */ info = [operators objectForKey: @""]; passwd = [info objectForKey: @"Password"]; } if (info == nil) { m = [NSString stringWithFormat: cmdLogFormat(LT_AUDIT, @"CONSOLE_LOGIN_FAILED 1 Rejected console with" @" info '%@' (unknown operator)"), n]; [self information: m type: LT_AUDIT to: nil from: nil]; return @"Unknown user name"; } /* We have four cases: * Empty/missing Password ... can log in without a password * Password == User ... can log in with username as password * Password == '-' ... login prohibited * Other ... the entered password must hash to the stored one * (or be equal to the stored one if built without crypt). */ if ([passwd isEqual: @"-"]) { m = [NSString stringWithFormat: cmdLogFormat(LT_AUDIT, @"CONSOLE_LOGIN_FAILED 1 Rejected console with" @" info '%@' (prohibited user)"), n]; [self information: m type: LT_AUDIT to: nil from: nil]; return @"Bad username/password combination"; } if (passwd && [passwd length]) { #if defined(HAVE_LIBCRYPT) char *ptr = (char*)[passwd UTF8String]; int len = strlen(ptr); if (len > 2 && NO == [passwd isEqual: user]) { char salt[len+1]; int slen = 2; /* Old crypt format is 2 chars of salt followed by key * New format salt is $id$chars$ where id is a single digit * and chars are up to 16 random characters. */ if ('$' == ptr[0] && isdigit(ptr[1]) && '$' == ptr[2] && strchr(ptr + 3, '$') != 0) { slen = strchr(ptr + 3, '$') - ptr + 1; } memcpy(salt, ptr, slen); salt[slen] = '\0'; ptr = crypt([p UTF8String], salt); p = [NSString stringWithUTF8String: ptr]; } #endif if ([passwd isEqual: p] == NO) { m = [NSString stringWithFormat: cmdLogFormat(LT_AUDIT, @"CONSOLE_LOGIN_FAILED 1 Rejected console with" @" info '%@' (bad password)"), n]; [self information: m type: LT_AUDIT to: nil from: nil]; return @"Bad username/password combination"; } } } obj = [[ConsoleInfo alloc] initFor: c name: n with: self pass: p]; [consoles addObject: obj]; [consoles sortUsingSelector: @selector(compare:)]; RELEASE(obj); m = [NSString stringWithFormat: cmdLogFormat(LT_AUDIT, @"CONSOLE_LOGIN 1 Registered new console" @" with info '%@'"), n]; [self information: m type: LT_AUDIT to: nil from: nil]; m = [NSString stringWithFormat: @"Logged in with info '%@'\n", n]; [self information: m type: LT_AUDIT to: n from: nil]; return nil; } - (void) reply: (NSString*) msg to: (NSString*)n from: (NSString*)c { [self information: msg type: LT_CONSOLE to: n from: c]; } - (NSString*) messageForAlarm: (EcAlarm*)alarm { NSString *additional; NSString *component; NSString *connector; NSString *instance; NSString *message; NSString *repair; NSString *spacing1; NSString *spacing2; instance = [alarm moInstance]; if ([instance length] == 0) { instance = @""; connector = @""; } else { connector = @"-"; } component = [alarm moComponent]; if (0 == [component length]) { component = @""; } else { component = [NSString stringWithFormat: @"(%@)", component]; } additional = [alarm additionalText]; if ([additional length] == 0) { additional = @""; spacing1 = @""; } else { spacing1 = @": "; } repair = [alarm proposedRepairAction]; if ([repair length] == 0) { repair = @""; spacing2 = @""; } else { spacing2 = @", "; } message = [NSString stringWithFormat: @"%@%@%@%@%@ - '%@%@%@%@' on %@", [alarm specificProblem], spacing1, additional, spacing2, repair, [alarm moProcess], connector, instance, component, [alarm moHost]]; return message; } - (void) reportAlarm: (EcAlarm*)alarm withMessage: (NSString*)message isCleared: (BOOL)cleared reminder: (int)reminder { NSString *identifier; identifier = [NSString stringWithFormat: @"%d", [alarm notificationID]]; if (cleared) { message = [NSString stringWithFormat: @"Clear %@ (%@)\n%@", identifier, [EcAlarm stringFromSeverity: [alarm perceivedSeverity]], message]; } else { message = [NSString stringWithFormat: @"Alarm %@ (%@)\n%@", identifier, [EcAlarm stringFromSeverity: [alarm perceivedSeverity]], message]; } alarm = [[alarm copy] autorelease]; if (cleared) { [alarm setExtra: @"Clear"]; } else { if (reminder > 0) { [alarm setExtra: @"Reminder"]; } else { [alarm setExtra: @"Alarm"]; } } [alerter handleEvent: message withHost: [alarm moHost] andServer: [alarm moInstancedProcess] timestamp: [alarm eventDate] identifier: identifier alarm: alarm reminder: reminder]; } - (void) reportAlarms { NSMutableDictionary *current; NSEnumerator *enumerator; EcAlarm *alarm; NSDate *now; NSString *key; NSArray *a; now = [NSDate date]; a = [sink alarms]; current = [NSMutableDictionary dictionaryWithCapacity: [a count]]; enumerator = [a objectEnumerator]; while (nil != (alarm = [enumerator nextObject])) { int notificationID = [alarm notificationID]; if (notificationID > 0) { NSDictionary *info; NSDate *when; int reminder; NSTimeInterval ti; ti = reminderInterval * 60.0; key = [NSString stringWithFormat: @"%d", notificationID]; [current setObject: alarm forKey: key]; info = [lastAlertInfo objectForKey: key]; if (nil == info) { when = nil; reminder = 0; } else { when = [info objectForKey: @"When"]; reminder = [[info objectForKey: @"Reminder"] intValue]; } if (nil == when || (ti > 0.0 && [now timeIntervalSinceDate: when] > ti)) { EcAlarmSeverity severity = [alarm perceivedSeverity]; if (severity <= alertAlarmThreshold) { if (nil == when) { alarmsAlerted++; } if (EcAlarmSeverityCleared == severity) { [self reportAlarm: alarm withMessage: [self messageForAlarm: alarm] isCleared: YES reminder: 0]; } else { [self reportAlarm: alarm withMessage: [self messageForAlarm: alarm] isCleared: NO reminder: reminder]; } } else { if (nil == when) { alarmsIgnored++; } } info = [NSDictionary dictionaryWithObjectsAndKeys: now, @"When", [NSNumber numberWithInt: reminder + 1], @"Reminder", nil]; [lastAlertInfo setObject: info forKey: key]; } } } /* Remove any alarms which no longer exist. */ enumerator = [[lastAlertInfo allKeys] objectEnumerator]; while (nil != (key = [enumerator nextObject])) { alarm = [current objectForKey: key]; if (nil == alarm) { [lastAlertInfo removeObjectForKey: key]; } } } - (oneway void) requestConfigFor: (id)c { return; } - (void) servers: (NSData*)d on: (id)s { NSArray *a; CommandInfo *o; a = [NSPropertyListSerialization propertyListWithData: d options: NSPropertyListMutableContainers format: 0 error: 0]; o = (CommandInfo*)[self findIn: commands byObject: s]; if (o != nil) { [o setServers: a]; } } - (void) terminate: (NSTimer*)t { if (nil == terminating) { [self information: @"Handling terminate." type: LT_CONSOLE to: nil from: nil]; } if (nil == terminating) { terminating = [NSTimer scheduledTimerWithTimeInterval: 10.0 target: self selector: @selector(terminate:) userInfo: [NSDate new] repeats: YES]; } [self quitAll]; if (t != nil) { NSDate *when = (NSDate*)[t userInfo]; if ([when timeIntervalSinceNow] < -60.0) { [[self cmdLogFile: logname] puts: @"Final shutdown.\n"]; [terminating invalidate]; terminating = nil; [self cmdQuit: 0]; } } } - (void) terminate { [self terminate: nil]; } - (void) timedOut: (NSTimer*)t { static BOOL inTimeout = NO; NSDate *now = [t fireDate]; if (now == nil) { now = [NSDate date]; } [[self cmdLogFile: logname] synchronizeFile]; if (inTimeout == NO) { NSString *alive; NSString *mesg; NSString *ver; unsigned count; inTimeout = YES; count = [commands count]; while (count-- > 0) { EcClientI *r = [commands objectAtIndex: count]; NSDate *d = [r outstanding]; if (d != nil && [d timeIntervalSinceDate: now] < -pingDelay) { EcAlarm *a; NSString *s; NSString *t; s = EcMakeManagedObject([r name], @"Command", nil); t = [NSString stringWithFormat: @"failed to respond for over %d seconds", (int)pingDelay]; a = [EcAlarm alarmForManagedObject: s at: nil withEventType: EcAlarmEventTypeProcessingError probableCause: EcAlarmSoftwareProgramAbnormallyTerminated specificProblem: @"Host command/control availability" perceivedSeverity: EcAlarmSeverityCritical proposedRepairAction: @"Check network/host/process" additionalText: t]; [self alarm: a]; [[[r obj] connectionForProxy] invalidate]; [commands removeObjectIdenticalTo: r]; } } count = [consoles count]; while (count-- > 0) { EcClientI *r = [consoles objectAtIndex: count]; NSDate *d = [r outstanding]; if (d != nil && [d timeIntervalSinceDate: now] < -pingDelay) { NSString *m; m = [NSString stringWithFormat: cmdLogFormat(LT_WARNING, @"Operator '%@' failed to respond for over %d seconds"), [r name], (int)pingDelay]; [[[r obj] connectionForProxy] invalidate]; [consoles removeObjectIdenticalTo: r]; [self information: m type: LT_WARNING to: nil from: nil]; } } /* * We ping each client in turn. If there are fewer than 4 clients, * we skip timeouts so that clients get pinged no more frequently * than one per 4 timeouts. */ count = [commands count]; if (commandPingPosition >= 4 && commandPingPosition >= count) { commandPingPosition = 0; } if (commandPingPosition < count) { [[commands objectAtIndex: commandPingPosition++] ping]; } count = [consoles count]; if (consolePingPosition >= 4 && consolePingPosition >= count) { consolePingPosition = 0; } if (consolePingPosition < count) { [[consoles objectAtIndex: consolePingPosition++] ping]; } /* * Write heartbeat file to let external programs know we are alive. */ ver = [[self cmdDefaults] stringForKey: @"ControlVersion"]; if (nil == ver) { mesg = [NSString stringWithFormat: @"Heartbeat: %@\n", now]; } else { mesg = [NSString stringWithFormat: @"Heartbeat: %@\nVersion: %@\n", now, ver]; } alive = [NSString stringWithFormat: @"/tmp/%@.alive", [self cmdName]]; [mesg writeToFile: alive atomically: YES]; [mgr changeFileAttributes: [NSDictionary dictionaryWithObjectsAndKeys: [NSNumber numberWithInt: 0666], NSFilePosixPermissions, nil] atPath: alive]; } [self reportAlarms]; inTimeout = NO; if (nil != terminating && 0 == [commands count]) { [self cmdQuit: 0]; } } - (id) recursiveInclude: (id)o { id tmp; if ([o isKindOfClass: [NSArray class]] == YES) { NSMutableArray *n; unsigned i; unsigned c = [o count]; n = [[NSMutableArray alloc] initWithCapacity: c]; for (i = 0; i < c; i++) { id tmp = [o objectAtIndex: i]; if ([tmp isKindOfClass: [NSString class]] == YES) { BOOL multi = NO; tmp = [self tryInclude: tmp multi: &multi]; if (multi == YES) { [n addObjectsFromArray: tmp]; } else if (tmp != nil) { [n addObject: tmp]; } } else { tmp = [self recursiveInclude: tmp]; if (tmp != nil) { [n addObject: tmp]; } } } return AUTORELEASE(n); } else if ([o isKindOfClass: [NSDictionary class]] == YES) { NSMutableDictionary *n; NSEnumerator *e = [o keyEnumerator]; NSString *k; unsigned c = [o count]; n = [[NSMutableDictionary alloc] initWithCapacity: c]; while ((k = [e nextObject]) != nil) { id tmp = [self tryInclude: k multi: 0]; if ([tmp isKindOfClass: [NSDictionary class]] == YES) { [n addEntriesFromDictionary: tmp]; } else { tmp = [self recursiveInclude: [(NSDictionary *)o objectForKey: k]]; if (tmp != nil) { [n setObject: tmp forKey: k]; } } } return AUTORELEASE(n); } else { tmp = [self tryInclude: o multi: 0]; return tmp; } } - (id) getInclude: (id)s { NSString *base = [self cmdDataDirectory]; NSString *file; id info; NSRange r; r = [s rangeOfCharacterFromSet: [NSCharacterSet whitespaceCharacterSet]]; if (r.length == 0) { return s; // Not an include. } file = [s substringFromIndex: NSMaxRange(r)]; if ([file isAbsolutePath] == NO) { file = [base stringByAppendingPathComponent: file]; } if ([mgr isReadableFileAtPath: file] == NO) { NSString *e; e = [NSString stringWithFormat: @"Unable to read file for '%@'\n", s]; ASSIGN(configIncludeFailed, e); [[self cmdLogFile: logname] printf: @"%@", configIncludeFailed]; return nil; } info = [NSString stringWithContentsOfFile: file]; if (info == nil) { NSString *e; e = [NSString stringWithFormat: @"Unable to load string for '%@'\n", s]; ASSIGN(configIncludeFailed, e); [[self cmdLogFile: logname] printf: @"%@", configIncludeFailed]; return nil; } NS_DURING { info = [info propertyList]; } NS_HANDLER { NSString *e; e = [NSString stringWithFormat: @"Unable to parse for '%@' - %@\n", s, localException]; ASSIGN(configIncludeFailed, e); [[self cmdLogFile: logname] printf: @"%@", configIncludeFailed]; info = nil; } NS_ENDHANDLER s = info; return s; } - (id) tryInclude: (id)s multi: (BOOL*)flag { if (flag != 0) { *flag = NO; } if ([s isKindOfClass: [NSString class]] == YES) { if ([s hasPrefix: @"#include "] == YES) { s = [self getInclude: s]; } else if ([s hasPrefix: @"#includeKeys "] == YES) { s = [self getInclude: s]; if ([s isKindOfClass: [NSDictionary class]] == YES) { s = [s allKeys]; } if (flag != 0 && [s isKindOfClass: [NSArray class]] == YES) { *flag = YES; } } else if ([s hasPrefix: @"#includeValues "] == YES) { s = [self getInclude: s]; if ([s isKindOfClass: [NSDictionary class]] == YES) { s = [s allValues]; } if (flag != 0 && [s isKindOfClass: [NSArray class]] == YES) { *flag = YES; } } } return s; } - (oneway void) unmanage: (NSString*)name { [sink unmanage: name]; } - (void) unregister: (id)obj { NSString *m; EcClientI *o = [self findIn: commands byObject: obj]; if (o == nil) { o = [self findIn: consoles byObject: obj]; if (o == nil) { m = [NSString stringWithFormat: @"%@ unregister by unknown host/console\n", [NSDate date]]; } else { m = [NSString stringWithFormat: @"%@ removed (unregistered) console - '%@'\n", [NSDate date], [o name]]; [o setUnregistered: YES]; [consoles removeObjectIdenticalTo: o]; } } else { [self unmanage: EcMakeManagedObject([o name], @"Command", nil)]; m = [NSString stringWithFormat: @"%@ removed (unregistered) host - '%@'\n", [NSDate date], [o name]]; [o setUnregistered: YES]; [commands removeObjectIdenticalTo: o]; } [self information: m type: LT_CONSOLE to: nil from: nil]; if (nil != terminating && 0 == [commands count]) { [self cmdQuit: 0]; } } - (Class) _loadClassFromBundle: (NSString*)bundleName { NSString *path = nil; Class c = Nil; NSBundle *bundle = nil; path = [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSLocalDomainMask, YES) lastObject]; path = [path stringByAppendingPathComponent: @"Bundles"]; path = [path stringByAppendingPathComponent: bundleName]; path = [path stringByAppendingPathExtension: @"bundle"]; bundle = [NSBundle bundleWithPath: path]; if (nil == bundle) { [[self cmdLogFile: logname] printf: @"Couldn't load bundle '%@'\n", bundleName]; } else if (Nil == (c = [bundle principalClass])) { [[self cmdLogFile: logname] printf: @"Couldn't load principal class from %@" @" at %@.\n", bundleName, path]; } else if (NO == [c isSubclassOfClass: [EcAlerter class]]) { [[self cmdLogFile: logname] printf: @"%@ is not a subclass of EcAlerter\n", NSStringFromClass(c)]; c = Nil; } return c; } - (BOOL) update { NSMutableDictionary *dict; NSDictionary *conf; NSDictionary *alertConfig = nil; NSDictionary *d; NSArray *a; NSHost *host; NSString *base; NSString *path; NSString *str; unsigned count; unsigned i; BOOL changed = NO; Class alerterClass = Nil; host = [NSHost currentHost]; str = [NSHost controlWellKnownName]; if (nil != str) { if (NO == [[host wellKnownName] isEqual: str] && NO == [[host names] containsObject: str] && NO == [[host addresses] containsObject: str]) { NSLog(@"ControlHost well known name (%@) does not match" @" the current host (%@)", str, host); [self cmdQuit: 1]; return NO; } } base = [self cmdDataDirectory]; /* The contents of AlertConfig.plist will override any configuration * of Alerter in the global ("*"."*") or Control server ("*"."") * sections of Control.plist. */ path = [base stringByAppendingPathComponent: @"AlertConfig.plist"]; if ([mgr isReadableFileAtPath: path] == YES) { if ((d = [NSDictionary dictionaryWithContentsOfFile: path]) == nil || (d = [self recursiveInclude: d]) == nil) { [[self cmdLogFile: logname] printf: @"Failed to load %@\n", path]; return NO; } else { alertConfig = [[self cmdDefaults] dictionaryForKey: @"Alerter"]; if (NO == [alertConfig isEqual: d]) { changed = YES; } alertConfig = d; } } path = [base stringByAppendingPathComponent: @"Operators.plist"]; if ([mgr isReadableFileAtPath: path] == NO || (d = [NSDictionary dictionaryWithContentsOfFile: path]) == nil || (d = [self recursiveInclude: d]) == nil) { [[self cmdLogFile: logname] printf: @"Failed to load %@\n", path]; return NO; } else { if (operators == nil || [operators isEqual: d] == NO) { changed = YES; RELEASE(operators); operators = [d mutableCopy]; } } DESTROY(configIncludeFailed); path = [base stringByAppendingPathComponent: @"Control.plist"]; if ([mgr isReadableFileAtPath: path] == NO || (str = [NSString stringWithContentsOfFile: path]) == nil) { NSString *e; e = [NSString stringWithFormat: @"Failed to load %@\n", path]; ASSIGN(configFailed, e); [[self cmdLogFile: logname] printf: @"%@", configFailed]; return NO; } NS_DURING { conf = [str propertyList]; if ([conf isKindOfClass: [NSDictionary class]] == NO) { [NSException raise: NSGenericException format: @"Contents of file not a dictionary"]; } } NS_HANDLER { NSString *e; e = [NSString stringWithFormat: @"Failed to load %@ - %@\n", path, [localException reason]]; ASSIGN(configFailed, e); [[self cmdLogFile: logname] printf: @"%@", configFailed]; return NO; } NS_ENDHANDLER if (nil != conf) { NSMutableDictionary *root; NSEnumerator *rootEnum; id hostKey; NSString *digest = nil; BOOL foundControlConfig = NO; if ([conf isKindOfClass: [NSDictionary class]] == NO) { NSString *e; e = [NSString stringWithFormat: @"%@ top-level is not a dictionary.\n", path]; ASSIGN(configFailed, e); [[self cmdLogFile: logname] printf: @"%@", configFailed]; return NO; } /* * Build version with mutable dictionaries at the hosts and * applications/classes levels of the configuration. */ conf = [self recursiveInclude: conf]; /* Get the EcControlKey from the generic area of the configuration. * If present, this should be an MD5 digest of the actual key. */ if ([[conf objectForKey: @"*"] isKindOfClass: [NSDictionary class]] && [[[conf objectForKey: @"*"] objectForKey: @"*"] isKindOfClass: [NSDictionary class]]) { digest = [[[conf objectForKey: @"*"] objectForKey: @"*"] objectForKey: @"EcControlKey"]; } if ([controlKey length] > 0 && nil == digest) { ASSIGN(configFailed, @"EcControlKey supplied on startup but not in Control.plist\n"); [[self cmdLogFile: logname] printf: @"%@", configFailed]; return NO; } if ([controlKey length] == 0 && digest != nil) { NSString *key; if ([(key = [[[conf objectForKey: @"*"] objectForKey: @"*"] objectForKey: @"EcControlKeyTest"]) length] == 64) { /* Operating in test mode with the master key stored in * Control.plist as EcControlKeyTest */ ASSIGN(controlKey, key); } else { ASSIGN(configFailed, @"EcControlKey configured but no value supplied on startup\n"); [[self cmdLogFile: logname] printf: @"%@", configFailed]; return NO; } } if (digest != nil) { NSData *key; NSData *md5; NSString *hex; key = [[NSData alloc] initWithHexadecimalRepresentation: controlKey]; md5 = [key md5Digest]; RELEASE(key); hex = [md5 hexadecimalRepresentation]; if (NO == [digest isEqual: hex]) { ASSIGN(configFailed, @"EcControlKey is not the MD5 digest of value from startup\n"); [[self cmdLogFile: logname] printf: @"%@", configFailed]; return NO; } } [conf writeToFile: @"/tmp/Control.cnf" atomically: YES]; [mgr changeFileAttributes: [NSDictionary dictionaryWithObjectsAndKeys: [NSNumber numberWithInt: 0666], NSFilePosixPermissions, nil] atPath: @"/tmp/Control.cnf"]; root = [NSMutableDictionary dictionaryWithCapacity: [conf count]]; rootEnum = [conf keyEnumerator]; while ((hostKey = [rootEnum nextObject]) != nil) { NSDictionary *rootObj; NSMutableDictionary *host; NSEnumerator *hostEnum; NSString *appKey; rootObj = [conf objectForKey: hostKey]; if ([rootObj isKindOfClass: [NSDictionary class]] == NO) { NSString *e; e = [NSString stringWithFormat: @"%@ host-level is not a dictionary for '%@'.\n", path, hostKey]; ASSIGN(configFailed, e); [[self cmdLogFile: logname] printf: @"%@", configFailed]; return NO; } if ([hostKey isEqual: @"*"] == NO) { id o = hostKey; if ([o isEqual: @""] || [o caseInsensitiveCompare: @"localhost"] == NSOrderedSame) { hostKey = [NSHost currentHost]; } else { hostKey = [NSHost hostWithWellKnownName: o]; if (nil == hostKey) { hostKey = [NSHost hostWithName: o]; } } if (hostKey == nil) { NSString *e; e = [NSString stringWithFormat: @"%@ host '%@' unknown.\n", path, o]; ASSIGN(configFailed, e); [[self cmdLogFile: logname] printf: @"%@", configFailed]; return NO; } } host = [NSMutableDictionary dictionaryWithCapacity: [rootObj count]]; hostEnum = [rootObj keyEnumerator]; while ((appKey = [hostEnum nextObject]) != nil) { NSDictionary *hostObj; NSMutableDictionary *app; hostObj = [rootObj objectForKey: appKey]; if ([hostObj isKindOfClass: [NSDictionary class]] == NO) { NSString *e; e = [NSString stringWithFormat: @"%@ app-level is not a dictionary for '%@'\n", path, appKey]; ASSIGN(configFailed, e); [[self cmdLogFile: logname] printf: @"%@", configFailed]; return NO; } app = [self recursiveInclude: hostObj]; if ([appKey isEqual: @""] || [appKey isEqual: @"Control"]) { if (NO == [hostKey isEqual: @"*"]) { NSString *e; NSString *k = appKey; if (0 == [k length]) { k = @"\"\""; } e = [NSString stringWithFormat: @"%@ app-level has special key '%@' in host '%@'\n", path, k, hostKey]; ASSIGN(configFailed, e); [[self cmdLogFile: logname] printf: @"%@", configFailed]; return NO; } if (foundControlConfig) { NSString *e; e = [NSString stringWithFormat: @"%@ app-level has both 'Control' and '\"\"' in '%@'\n", path, hostKey]; ASSIGN(configFailed, e); [[self cmdLogFile: logname] printf: @"%@", configFailed]; return NO; } foundControlConfig = YES; if (NO == [controlConfig isEqual: app]) { ASSIGN(controlConfig, app); changed = YES; } continue; } [host setObject: app forKey: appKey]; /* Set EcControlKey for all apps on all hosts. */ [app setObject: controlKey forKey: @"EcControlKey"]; } [root setObject: host forKey: hostKey]; } if (NO == foundControlConfig) { if (controlConfig) { DESTROY(controlConfig); changed = YES; } } if (config == nil || [config isEqual: root] == NO) { changed = YES; ASSIGN(config, root); } } [self ecOperators: operators]; if (YES == changed) { NSString *alerterDef; NSString *str; /* Merge the global configuration and Control server specific * configuration into this process' user defaults. */ d = [config objectForKey: @"*"]; // Config across all hosts if ([d isKindOfClass: [NSDictionary class]]) { d = [d objectForKey: @"*"]; } if (YES == [d isKindOfClass: [NSDictionary class]]) { dict = [[d mutableCopy] autorelease]; } else { dict = [NSMutableDictionary dictionary]; } /* Control server specific config. */ if ([controlConfig isKindOfClass: [NSDictionary class]]) { [dict addEntriesFromDictionary: controlConfig]; } /* If AlertConfig.plist was found, it overrides any value for Alerter * configured in Control.plist. */ if (nil != alertConfig) { [dict setObject: alertConfig forKey: @"Alerter"]; } [[self cmdDefaults] setConfiguration: dict]; /* Now that our defaults are set, it's safe to update the alerter * configuration. */ alertConfig = [dict objectForKey: @"Alerter"]; alerterDef = [alertConfig objectForKey: @"AlerterBundle"]; str = [alertConfig objectForKey: @"AlertAlarmThreshold"]; if ([str length] == 0) { /* default value as documented in EcAlerter.h */ str = [NSString stringWithFormat: @"%d", EcAlarmSeverityMajor]; } alertAlarmThreshold = [str intValue]; if (alertAlarmThreshold < EcAlarmSeverityCritical) { alertAlarmThreshold = EcAlarmSeverityCritical; } if (alertAlarmThreshold > EcAlarmSeverityWarning) { alertAlarmThreshold = EcAlarmSeverityWarning; } str = [alertConfig objectForKey: @"AlertReminderInterval"]; if ([str length] == 0) { /* default value as documented in EcAlerter.h */ str = @"0"; } reminderInterval = [str intValue]; if (reminderInterval < 0) { reminderInterval = 0; } if (nil == alerterDef) { alerterClass = [EcAlerter class]; } else { /* First, let's try whether this corresponds to * a class we already loaded. */ alerterClass = NSClassFromString(alerterDef); if (Nil == alerterClass) { /* We didn't link the class. Try to load it * from a bundle. */ alerterClass = [self _loadClassFromBundle: alerterDef]; } } if (Nil == alerterClass) { NSLog(@"Could not load alerter class '%@'", alerterDef); } else if ([alerter class] != alerterClass) { [sink setMonitor: nil]; if (alerter) { [alerter shutdown]; NSLog(@"Uninstalled alerter: %@", NSStringFromClass([alerter class])); DESTROY(alerter); } } if (nil == alerter) { alerter = [alerterClass new]; NSLog(@"Installed alerter: %@", NSStringFromClass([alerter class])); } /* An alerter which confrms to the correct protocol is assumed to * want to monitor alarms. */ if ([alerter conformsToProtocol: @protocol(EcAlarmMonitor)]) { [sink setMonitor: (id)alerter]; } dict = [NSMutableDictionary dictionaryWithCapacity: 3]; /* * Now per-host config dictionaries consisting of general and * host-specific dictionaries. */ a = [NSArray arrayWithArray: commands]; count = [a count]; for (i = 0; i < count; i++) { CommandInfo *c = [a objectAtIndex: i]; if ([commands indexOfObjectIdenticalTo: c] != NSNotFound) { id o; NSHost *h; [dict removeAllObjects]; o = [config objectForKey: @"*"]; if (o != nil) { [dict setObject: o forKey: @"*"]; } h = [NSHost hostWithWellKnownName: [c name]]; if (nil == h) { h = [NSHost hostWithName: [c name]]; } // Configuration keys may be NSHost objects o = [config objectForKey: (NSString*)h]; if (o != nil) { [dict setObject: o forKey: [c name]]; } if (operators != nil) { [dict setObject: operators forKey: @"Operators"]; } NS_DURING { NSData *dat; dat = [NSPropertyListSerialization dataFromPropertyList: dict format: NSPropertyListBinaryFormat_v1_0 errorDescription: 0]; [[c obj] updateConfig: dat]; } NS_HANDLER { [[self cmdLogFile: logname] printf: @"Updating config for '%@':%@\n", [c name], localException]; } NS_ENDHANDLER } } } DESTROY(configFailed); return changed; } - (void) updateConfig: (NSData*)dummy { [self update]; } @end