libs-ec/EcControl.m
Richard Frith-Macdonald afb667be58 Scripting improvements
2023-04-07 12:09:24 +01:00

3658 lines
90 KiB
Objective-C

/** Enterprise Control Configuration and Logging
Copyright (C) 2012 Free Software Foundation, Inc.
Written by: Richard Frith-Macdonald <rfm@gnu.org>
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 <Foundation/Foundation.h>
#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<CmdClient>)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<CmdClient>)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 <Control>
{
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 <CmdPing>)from
sequence: (unsigned)num
extra: (NSData*)data;
- (oneway void) cmdPing: (id <CmdPing>)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<Command>)c
name: (NSString*)n;
- (NSString*) registerConsole: (id<Console>)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<Command>)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 <CmdPing>)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 <CmdPing>)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 <oldpass> <newpass>\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 <host> quit <name>' to shut down "
@"individual servers on the specified host.\n"
@"\nUse 'on <host> quit all' to shut down all the "
@"servers on the specified host.\n"
@"\nUse 'on <host> 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<EcAlarmMonitor>)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<Command>)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<Console>)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<CmdConfig>)c
{
return;
}
- (void) servers: (NSData*)d
on: (id<Command>)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<EcAlarmMonitor>)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