NOTE: Certainly a broken commit.

This continues the work of adding database support (and ancilliary functions) to m3.
* Adds Alert->check_alert_sent() and Alert->register_alert(), providing similar functions as in m2.
* Adds Database->disconnect(), Database->insert_or_update_states(), Database->locking() (still in progress), Database->mark_active(), Database->_find_behind_database(), Database->_mark_database_as_behind() and Database->_test_access().

Signed-off-by: Digimer <digimer@alteeve.ca>
main
Digimer 8 years ago
parent 13ae1f1701
commit 66a984adbc
  1. 6
      AN/Tools.pm
  2. 221
      AN/Tools.sql
  3. 375
      AN/Tools/Alert.pm
  4. 860
      AN/Tools/Database.pm
  5. 34
      AN/an-tools.xml
  6. 4
      scancore.sql
  7. 2
      striker.conf

@ -578,6 +578,9 @@ sub _set_defaults
server => "", server => "",
tag => "an-tools", tag => "an-tools",
}, },
sql => {
test_table => "hosts",
},
template => { template => {
html => "alteeve", html => "alteeve",
}, },
@ -641,6 +644,9 @@ sub _set_paths
sysfs => { sysfs => {
network_interfaces => "/sys/class/net", network_interfaces => "/sys/class/net",
}, },
sql => {
'Tools.sql' => "/usr/share/perl5/AN/Tools.sql",
},
urls => { urls => {
skins => "/skins", skins => "/skins",
}, },

@ -0,0 +1,221 @@
-- This is the core database schema for AN::Tools.
-- It expects PostgreSQL v. 9.1+
SET client_encoding = 'UTF8';
CREATE SCHEMA IF NOT EXISTS history;
-- This stores information about the host machine. This is the master table that everything will be linked
-- to.
CREATE TABLE hosts (
host_uuid uuid not null primary key, -- This is the single most important record in ScanCore. Everything links back to here.
host_name text not null,
host_type text not null, -- Either 'node' or 'dashboard'.
modified_date timestamp with time zone not null,
FOREIGN KEY(host_location_uuid) REFERENCES locations(location_uuid)
);
ALTER TABLE hosts OWNER TO #!variable!user!#;
CREATE TABLE history.hosts (
history_id bigserial,
host_uuid uuid not null,
host_name text not null,
host_type text not null,
modified_date timestamp with time zone not null
);
ALTER TABLE history.hosts OWNER TO #!variable!user!#;
CREATE FUNCTION history_hosts() RETURNS trigger
AS $$
DECLARE
history_hosts RECORD;
BEGIN
SELECT INTO history_hosts * FROM hosts WHERE host_uuid = new.host_uuid;
INSERT INTO history.hosts
(host_uuid,
host_name,
host_type,
modified_date)
VALUES
(history_hosts.host_uuid,
history_hosts.host_name,
history_hosts.host_type,
history_hosts.modified_date);
RETURN NULL;
END;
$$
LANGUAGE plpgsql;
ALTER FUNCTION history_hosts() OWNER TO #!variable!user!#;
CREATE TRIGGER trigger_hosts
AFTER INSERT OR UPDATE ON hosts
FOR EACH ROW EXECUTE PROCEDURE history_hosts();
-- This stores special variables for a given host that programs may want to record.
CREATE TABLE host_variable (
host_variable_uuid uuid not null primary key, -- This is the single most important record in ScanCore. Everything links back to here.
host_variable_host_uuid uuid not null,
host_variable_name text not null,
host_variable_value text,
modified_date timestamp with time zone not null,
FOREIGN KEY(host_location_uuid) REFERENCES locations(location_uuid)
);
ALTER TABLE host_variable OWNER TO #!variable!user!#;
CREATE TABLE history.host_variable (
history_id bigserial,
host_variable_host_uuid uuid,
host_variable_name text,
host_variable_value text,
modified_date timestamp with time zone not null
);
ALTER TABLE history.host_variable OWNER TO #!variable!user!#;
CREATE FUNCTION history_host_variable() RETURNS trigger
AS $$
DECLARE
history_host_variable RECORD;
BEGIN
SELECT INTO history_host_variable * FROM host_variable WHERE host_uuid = new.host_uuid;
INSERT INTO history.host_variable
(host_variable_uuid,
host_variable_host_uuid,
host_variable_name,
host_variable_value,
modified_date)
VALUES
(host_variable_uuid,
host_variable_host_uuid,
host_variable_name,
host_variable_value,
history_host_variable.modified_date);
RETURN NULL;
END;
$$
LANGUAGE plpgsql;
ALTER FUNCTION history_host_variable() OWNER TO #!variable!user!#;
CREATE TRIGGER trigger_host_variable
AFTER INSERT OR UPDATE ON host_variable
FOR EACH ROW EXECUTE PROCEDURE history_host_variable();
-- This stores alerts coming in from various sources
CREATE TABLE alerts (
alert_uuid uuid primary key,
alert_host_uuid uuid not null, -- The name of the node or dashboard that this alert came from.
alert_set_by text not null,
alert_level text not null, -- debug (log only), info (+ admin email), notice (+ curious users), warning (+ client technical staff), critical (+ all)
alert_title_key text not null, -- ScanCore will read in the agents <name>.xml words file and look for this message key
alert_title_variables text, -- List of variables to substitute into the message key. Format is 'var1=val1 #!# var2 #!# val2 #!# ... #!# varN=valN'.
alert_message_key text not null -- ScanCore will read in the agents <name>.xml words file and look for this message key
alert_message_variables text, -- List of variables to substitute into the message key. Format is 'var1=val1 #!# var2 #!# val2 #!# ... #!# varN=valN'.
alert_sort text, -- The alerts will sort on this column. It allows for an optional sorting of the messages in the alert.
alert_header boolean not null default TRUE, -- This can be set to have the alert be printed with only the contents of the string, no headers.
modified_date timestamp with time zone not null,
FOREIGN KEY(alert_host_uuid) REFERENCES hosts(host_uuid)
);
ALTER TABLE alerts OWNER TO #!variable!user!#;
CREATE TABLE history.alerts (
history_id bigserial,
alert_uuid uuid,
alert_host_uuid uuid,
alert_set_by text,
alert_level text,
alert_title_key text,
alert_title_variables text,
alert_message_key text,
alert_message_variables text,
alert_sort text,
alert_header boolean,
modified_date timestamp with time zone not null
);
ALTER TABLE history.alerts OWNER TO #!variable!user!#;
CREATE FUNCTION history_alerts() RETURNS trigger
AS $$
DECLARE
history_alerts RECORD;
BEGIN
SELECT INTO history_alerts * FROM alerts WHERE alert_uuid = new.alert_uuid;
INSERT INTO history.alerts
(alert_uuid,
alert_host_uuid,
alert_set_by,
alert_level,
alert_title_key,
alert_title_variables,
alert_message_key,
alert_message_variables,
alert_sort,
alert_header,
modified_date)
VALUES
(history_alerts.alert_uuid,
history_alerts.alert_host_uuid,
history_alerts.alert_set_by,
history_alerts.alert_level,
history_alerts.alert_title_key,
history_alerts.alert_title_variables,
history_alerts.alert_message_key,
history_alerts.alert_message_variables,
history_alerts.alert_sort,
history_alerts.alert_header,
history_alerts.modified_date);
RETURN NULL;
END;
$$
LANGUAGE plpgsql;
ALTER FUNCTION history_alerts() OWNER TO #!variable!user!#;
CREATE TRIGGER trigger_alerts
AFTER INSERT OR UPDATE ON alerts
FOR EACH ROW EXECUTE PROCEDURE history_alerts();
-- ------------------------------------------------------------------------------------------------------- --
-- These are special tables with no history or tracking UUIDs that simply record transient information. --
-- ------------------------------------------------------------------------------------------------------- --
-- This table records the last time a scan ran.
CREATE TABLE updated (
updated_host_uuid uuid not null,
updated_by text not null, -- The name of the agent (or "ScanCore' itself) that updated.
modified_date timestamp with time zone not null,
FOREIGN KEY(updated_host_uuid) REFERENCES hosts(host_uuid)
);
ALTER TABLE updated OWNER TO #!variable!user!#;
-- To avoid "waffling" when a sensor is close to an alert (or cleared) threshold, a gap between the alarm
-- value and the clear value is used. If the sensor climbs above (or below) the "clear" value, but didn't
-- previously pass the "alert" threshold, we DON'T want to send an "all clear" message. So do solve that,
-- this table is used by agents to record when a warning message was sent.
CREATE TABLE alert_sent (
alert_sent_host_uuid uuid not null, -- The node associated with this alert
alert_set_by text not null, -- name of the program that set this alert
alert_record_locator text, not null -- String used by the agent to identify the source of the alert (ie: UPS serial number)
alert_name text not null, -- A free-form name used by the caller to identify this alert.
modified_date timestamp with time zone not null,
FOREIGN KEY(alert_sent_host_uuid) REFERENCES hosts(host_uuid)
);
ALTER TABLE updated OWNER TO #!variable!user!#;
-- This stores state information, like the whether migrations are happening and so on.
CREATE TABLE states (
state_uuid uuid primary key,
state_name text not null, -- This is the name of the state (ie: 'migration', etc)
state_host_uuid uuid not null, -- The UUID of the machine that the state relates to. In migrations, this is the UUID of the target
state_note text, -- This is a free-form note section that the application setting the state can use for extra information (like the name of the server being migrated)
modified_date timestamp with time zone not null,
FOREIGN KEY(state_host_uuid) REFERENCES hosts(host_uuid)
);
ALTER TABLE states OWNER TO #!variable!user!#;

@ -10,7 +10,9 @@ our $VERSION = "3.0.0";
my $THIS_FILE = "Alert.pm"; my $THIS_FILE = "Alert.pm";
### Methods; ### Methods;
# check_alert_sent
# error # error
# register_alert
=pod =pod
@ -66,6 +68,379 @@ sub parent
############################################################################################################# #############################################################################################################
=head2 check_alert_sent
This is used by scan agents that need to track whether an alert was sent when a sensor dropped below/rose above a set alert threshold. For example, if a sensor alerts at 20°C and clears at 25°C, this will be called when either value is passed. When passing the warning threshold, the alert is registered and sent to the user. Once set, no further warning alerts are sent. When the value passes over the clear threshold, this is checked and if an alert was previously registered, it is removed and an "all clear" message is sent. In this way, multiple alerts will not go out if a sensor floats around the warning threshold and a "cleared" message won't be sent unless a "warning" message was previously sent.
If there is a problem, C<< undef >> is returned.
Parameters;
=head3 modified_date (optional)
By default, this is set to C<< sys::db_timestamp >>. If you want to force a different timestamp, you can do so with this parameter.
=head3 name (required)
This is the name of the alert. So for an alert related to a critically high temperature, this might get set to C<< temperature_high_critical >>. It is meant to compliment the C<< record_locator >> parameter.
=head3 record_locator
This is a record locator, which generally allows a given alert to be tied to a given source. For example, an alert related to a temperature might use C<< an-a01n01.alteeve.com:cpu1_temperature >>.
=head3 set_by (required)
This is a string, usually the name of the program, that set the alert. Usuall this is simple C<< $THIS_FILE >> or C<< $0 >>.
=head3 type (required)
This is set to C<< set >> or C<< clear >>.
If set to C<< set >>, C<< 1 >> will be returned if this is the first time we've tried to set this alert. If the alert was set before, C<< 0 >> is returned.
If set to C<< clear >>, C<< 1 >> will be returned if this is the alert existed and was cleared. If the alert didn't exist (and thus didn't need to be cleared), C<< 0 >> is returned.
=cut
sub check_alert_sent
{
my $self = shift;
my $parameter = shift;
my $an = $self->parent;
my $modified_date = $parameter->{modified_date} ? $parameter->{modified_date} : $an->data->{sys}{db_timestamp};
my $name = $parameter->{name} ? $parameter->{name} : "";
my $record_locator = $parameter->{record_locator} ? $parameter->{record_locator} : "";
my $set_by = $parameter->{set_by} ? $parameter->{set_by} : "";
my $type = $parameter->{type} ? $parameter->{type} : "";
$an->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => {
modified_date => $modified_date,
name => $name,
record_locator => $record_locator,
set_by => $set_by,
type => $type,
}});
# Do we have a timestamp?
if (not $modified_date)
{
# Nope
$an->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "log_0093"});
return(undef);
}
# Do we have an alert name?
if (not $name)
{
# Nope
$an->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "log_0094"});
return(undef);
}
# Do we have an record locator?
if (not $record_locator)
{
# Nope
$an->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "log_0095"});
return(undef);
}
# Do we know who is setting this??
if (not $set_by)
{
# Nope
$an->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "log_0096"});
return(undef);
}
# Are we setting or clearing?
if (not $type)
{
# Neither...
$an->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "log_0097"});
return(undef);
}
# This will get set to '1' if an alert is added or removed.
my $set = 0;
my $query = "
SELECT
COUNT(*)
FROM
alert_sent
WHERE
alert_sent_host_uuid = ".$an->data->{sys}{use_db_fh}->quote($an->data->{sys}{host_uuid})."
AND
alert_set_by = ".$an->data->{sys}{use_db_fh}->quote($alert_set_by)."
AND
alert_record_locator = ".$an->data->{sys}{use_db_fh}->quote($alert_record_locator)."
AND
alert_name = ".$an->data->{sys}{use_db_fh}->quote($alert_name)."
;";
$an->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { query => $query }});
my $count = $an->DB->do_db_query({query => $query, source => $THIS_FILE, line => __LINE__})->[0]->[0];
$an->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => {
type => $type,
query => $query,
}});
# Now, if this is type=set, register the alert if it doesn't exist. If it is type=clear, remove the
# alert if it exists.
if (($type eq "set") && (not $count))
{
### New alert
# Make sure this host is in the database... It might not be on the very first run of ScanCore
# before the peer exists (tried to connect to the peer, fails, tries to send an alert, but
# this host hasn't been added because it is the very first attempt to connect...)
if (not $an->data->{sys}{host_is_in_db})
{
my $query = "
SELECT
COUNT(*)
FROM
hosts
WHERE
host_uuid = ".$an->data->{sys}{use_db_fh}->quote($an->data->{sys}{host_uuid})."
;";
$an->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { query => $query }});
my $count = $an->DB->do_db_query({query => $query, source => $THIS_FILE, line => __LINE__})->[0]->[0];
$an->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { count => $count }});
if (not $count)
{
# Too early, we can't set an alert.
$an->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "alert", key => "log_0098", variables => {
type => $type,
alert_set_by => $alert_set_by,
alert_record_locator => $alert_record_locator,
alert_name => $alert_name,
modified_date => $modified_date,
}});
return(undef);
}
else
{
$an->data->{sys}{host_is_in_db} = 1;
$an->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { 'sys::host_is_in_db' => $an->data->{sys}{host_is_in_db} }});
}
}
$set = 1;
my $query = "
INSERT INTO
alert_sent
(
alert_sent_host_uuid,
alert_set_by,
alert_record_locator,
alert_name,
modified_date
) VALUES (
".$an->data->{sys}{use_db_fh}->quote($an->data->{sys}{host_uuid}).",
".$an->data->{sys}{use_db_fh}->quote($alert_set_by).",
".$an->data->{sys}{use_db_fh}->quote($alert_record_locator).",
".$an->data->{sys}{use_db_fh}->quote($alert_name).",
".$an->data->{sys}{use_db_fh}->quote($an->data->{sys}{db_timestamp})."
);
";
$an->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => {
query => $query,
set => $set,
}});
$an->Database->write({query => $query, source => $THIS_FILE, line => __LINE__});
}
elsif (($type eq "clear") && ($count))
{
# Alert previously existed, clear it.
$set = 1;
my $query = "
DELETE FROM
alert_sent
WHERE
alert_sent_host_uuid = ".$an->data->{sys}{use_db_fh}->quote($an->data->{sys}{host_uuid})."
AND
alert_set_by = ".$an->data->{sys}{use_db_fh}->quote($alert_set_by)."
AND
alert_record_locator = ".$an->data->{sys}{use_db_fh}->quote($alert_record_locator)."
AND
alert_name = ".$an->data->{sys}{use_db_fh}->quote($alert_name)."
;";
$an->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => {
query => $query,
set => $set,
}});
$an->DB->do_db_write({query => $query, source => $THIS_FILE, line => __LINE__});
}
$an->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { set => $set }});
return($set);
}
=head2 register_alert
This registers an alert to be sent later.
If anything goes wrong, C<< undef >> will be returned.
=cut
sub register_alert
{
my $self = shift;
my $parameter = shift;
my $an = $self->parent;
my $header = defined $parameter->{header} ? $parameter->{header} : 1;
my $level = defined $parameter->{level} ? $parameter->{level} : "warning";
my $message_key = defined $parameter->{message_key} ? $parameter->{message_key} : "";
my $message_variables = defined $parameter->{message_variables} ? $parameter->{message_variables} : "";
my $set_by = defined $parameter->{set_by} ? $parameter->{set_by} : "";
my $sort = defined $parameter->{'sort'} ? $parameter->{'sort'} : 9999;
my $title_key = defined $parameter->{title_key} ? $parameter->{title_key} : "title_0003";
my $title_variables = defined $parameter->{title_variables} ? $parameter->{title_variables} : "";
$an->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => {
header => $header,
level => $level,
message_key => $message_key,
message_variables => $message_variables,
set_by => $set_by,
'sort' => $sort,
title_key => $title_key,
title_variables => $title_variables,
}});
if (not $set_by)
{
$an->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "log_0099"});
return(undef);
}
if (not $message_key)
{
$an->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "log_0100"});
return(undef);
}
if (($header) && (not $title_key))
{
$an->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "log_0101"});
return(undef);
}
# zero-pad sort numbers so that they sort properly.
$alert_sort = sprintf("%04d", $alert_sort);
$an->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { alert_sort => $alert_sort }});
# Convert the hash of title variables and message variables into '!!x!y!!,!!a!b!!,...' strings.
my $title_variables = "";
if (ref($alert_title_variables) eq "HASH")
{
foreach my $key (sort {$a cmp $b} keys %{$alert_title_variables})
{
$alert_title_variables->{$key} = "--" if not defined $alert_title_variables->{$key};
$title_variables .= "!!$key!".$alert_title_variables->{$key}."!!,";
}
}
my $message_variables = "";
if (ref($alert_message_variables) eq "HASH")
{
foreach my $key (sort {$a cmp $b} keys %{$alert_message_variables})
{
$alert_message_variables->{$key} = "--" if not defined $alert_message_variables->{$key};
$message_variables .= "!!$key!".$alert_message_variables->{$key}."!!,";
}
}
# In most cases, no one is listening to 'debug' or 'info' level alerts. If that is the case here,
# don't record the alert because it can cause the history.alerts table to grow needlessly. So find
# the lowest level log level actually being listened to and simply skip anything lower than that.
# 5 == debug
# 1 == critical
my $lowest_log_level = 5;
foreach my $integer (sort {$a cmp $b} keys %{$an->data->{alerts}{recipient}})
{
# We want to know the alert level, regardless of whether the recipient is an email of file
# target.
my $this_level;
if ($an->data->{alerts}{recipient}{$integer}{email})
{
# Email recipient
$this_level = ($an->data->{alerts}{recipient}{$integer}{email} =~ /level="(.*?)"/)[0];
$an->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { this_level => $this_level }});
}
elsif ($an->data->{alerts}{recipient}{$integer}{file})
{
# File target
$this_level = ($an->data->{alerts}{recipient}{$integer}{file} =~ /level="(.*?)"/)[0];
$an->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { this_level => $this_level }});
}
$an->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { this_level => $this_level }});
if ($this_level)
{
$this_level = $an->Alert->convert_level_name_to_number({level => $this_level});
$an->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => {
this_level => $this_level,
lowest_log_level => $lowest_log_level,
}});
if ($this_level < $lowest_log_level)
{
$lowest_log_level = $this_level;
$an->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { lowest_log_level => $lowest_log_level }});
}
}
}
# Now get the numeric value of this alert and return if it is higher.
my $this_level = $an->Alert->convert_level_name_to_number({level => $alert_level});
$an->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => {
alert_level => $alert_level,
this_level => $this_level,
lowest_log_level => $lowest_log_level,
}});
if ($this_level > $lowest_log_level)
{
# Return.
$an->Log->entry({source => $THIS_FILE, line => __LINE__, level => 2, key => "log_0102", variables => { message_key => $alert_message_key }});
return(0);
}
# Always INSERT. ScanCore removes them as they're acted on (copy is left in history.alerts).
my $query = "
INSERT INTO
alerts
(
alert_uuid,
alert_host_uuid,
alert_set_by,
alert_level,
alert_title_key,
alert_title_variables,
alert_message_key,
alert_message_variables,
alert_sort,
alert_header,
modified_date
) VALUES (
".$an->data->{sys}{use_db_fh}->quote($an->Get->uuid()).",
".$an->data->{sys}{use_db_fh}->quote($an->data->{sys}{host_uuid}).",
".$an->data->{sys}{use_db_fh}->quote($alert_set_by).",
".$an->data->{sys}{use_db_fh}->quote($alert_level).",
".$an->data->{sys}{use_db_fh}->quote($alert_title_key).",
".$an->data->{sys}{use_db_fh}->quote($title_variables).",
".$an->data->{sys}{use_db_fh}->quote($alert_message_key).",
".$an->data->{sys}{use_db_fh}->quote($message_variables).",
".$an->data->{sys}{use_db_fh}->quote($alert_sort).",
".$an->data->{sys}{use_db_fh}->quote($alert_header).",
".$an->data->{sys}{use_db_fh}->quote($an->data->{sys}{db_timestamp})."
);
";
$an->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { query => $query }});
$an->DB->do_db_write({query => $query, source => $THIS_FILE, line => __LINE__});
return(0);
}
=head2 error =head2 error
=cut =cut

@ -12,11 +12,18 @@ my $THIS_FILE = "Database.pm";
### Methods; ### Methods;
# connect # connect
# disconnect
# get_local_id # get_local_id
# initialize # initialize
# insert_or_update_states
# locking
# mark_active
# query # query
# test_access # test_access
# write # write
# _find_behind_database
# _mark_database_as_behind
# _test_access
=pod =pod
@ -106,17 +113,44 @@ This module will return the number of databases that were successfully connected
Parameters; Parameters;
=head3 file (required) =head3 source (optional)
The C<< source >> parameter is used to check the special C<< updated >> table one all connected databases to see when that source (program name, usually) last updated a given database. If the date stamp is the same on all connected databases, nothing further happens. If one of the databases differ, however, a resync will be requested.
If not defined, the core database will be checked.
The C<< file >> parameter is used to check the special C<< updated >> table one all connected databases to see when that file (program name) last updated a given database. If the date stamp is the same on all connected databases, nothing further happens. If one of the databases differ, however, a resync will be requested. If this is not set, no attempt to resync the database will be made.
=head3 sql_file (optional)
This is the SQL schema file that will be used to initialize the database, if the C<< test_table >> isn't found in a given database that is connected to. By default, this is C<< path::sql::Tools.sql >> (C<< /usr/share/perl/AN/Tools.sql >> by default).
=head3 tables (optional) =head3 tables (optional)
This is an optional array reference of tables to specifically check when connecting to databases. If specified, the table's most recent C<< modified_date >> time stamp will be read (specifically; C<< SELECT modified_date FROM $table ORDER BY modified_date DESC LIMIT 1 >>) and if a table doesn't return, or any of the time stamps are missing, a resync will be requested. This is an optional hash reference of tables and their host UUID columns to specifically check when connecting to databases. If specified, the table's most recent C<< modified_date >> time stamp will be read (specifically; C<< SELECT modified_date FROM history.$table WHERE $host_uuid_column = 'sys::host_uuid' ORDER BY modified_date DESC LIMIT 1 >>) and if a table doesn't return, or any of the time stamps are missing, a resync will be requested.
Example use;
To use this, use; $an->Database->connect({
tables => {
upses => "ups_host_uuid",
ups_batteries => "ups_battery_host_uuid",
},
});
If you want to specify a table that is not linked to a host, set the hash variable's value as an empty string.
$an->Database->connect({
tables => {
servers => "",
},
});
$an->Database->connect({file => $THIS_FILE, tables => ("table1", "table2")}); =head3 test_table (optional)
Once connected to the database, a query is made to see if the database needs to be initialized. Usually this is C<< defaults::sql::test_table >> (C<< hosts>> by default).
If you set this table manually, it will be checked and if the table doesn't exist on a connected database, the database will be initialized with the C<< sql_file >> parameter's file.
=cut =cut
sub connect sub connect
@ -126,8 +160,16 @@ sub connect
my $an = $self->parent; my $an = $self->parent;
$an->Log->entry({log_level => 3, message_key => "tools_log_0001", message_variables => { function => "connect_to_databases" }, file => $THIS_FILE, line => __LINE__}); $an->Log->entry({log_level => 3, message_key => "tools_log_0001", message_variables => { function => "connect_to_databases" }, file => $THIS_FILE, line => __LINE__});
my $file = defined $parameter->{file} ? $parameter->{file} : ""; my $source = defined $parameter->{source} ? $parameter->{source} : "core";
my $sql_file = defined $parameter->{sql_file} ? $parameter->{sql_file} : $an->data->{path}{sql}{'Tools.sql'};
my $tables = defined $parameter->{tables} ? $parameter->{tables} : ""; my $tables = defined $parameter->{tables} ? $parameter->{tables} : "";
my $test_table = defined $parameter->{test_table} ? $parameter->{test_table} : $an->data->{defaults}{sql}{test_table};
$an->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => {
source => $source,
sql_file => $sql_file,
tables => $tables,
test_table => $test_table,
}});
# We need the host_uuid before we connect. # We need the host_uuid before we connect.
if (not $an->data->{sys}{host_uuid}) if (not $an->data->{sys}{host_uuid})
@ -140,7 +182,7 @@ sub connect
$an->data->{sys}{local_db_id} = ""; $an->data->{sys}{local_db_id} = "";
# This will be set to '1' if either DB needs to be initialized or if the last_updated differs on any node. # This will be set to '1' if either DB needs to be initialized or if the last_updated differs on any node.
$an->data->{database_resync_needed} = 0; $an->data->{database}{general}{resync_needed} = 0;
# Now setup or however-many connections # Now setup or however-many connections
my $seen_connections = []; my $seen_connections = [];
@ -295,7 +337,7 @@ sub connect
}}); }});
# Now that I have connected, see if my 'hosts' table exists. # Now that I have connected, see if my 'hosts' table exists.
my $query = "SELECT COUNT(*) FROM pg_catalog.pg_tables WHERE tablename='hosts' AND schemaname='public';"; my $query = "SELECT COUNT(*) FROM pg_catalog.pg_tables WHERE tablename=".$an->data->{sys}{use_db_fh}->quote($test_table)." AND schemaname='public';";
$an->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { query => $query }}); $an->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { query => $query }});
my $count = $an->Database->query({id => $id, query => $query, source => $THIS_FILE, line => __LINE__})->[0]->[0]; my $count = $an->Database->query({id => $id, query => $query, source => $THIS_FILE, line => __LINE__})->[0]->[0];
@ -304,7 +346,7 @@ sub connect
if ($count < 1) if ($count < 1)
{ {
# Need to load the database. # Need to load the database.
$an->Database->initialize({id => $id}); $an->Database->initialize({id => $id, sql_file => $sql_file});
} }
$an->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { $an->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => {
@ -336,9 +378,7 @@ sub connect
"sys::db_timestamp" => $an->data->{sys}{db_timestamp} "sys::db_timestamp" => $an->data->{sys}{db_timestamp}
}); });
# Pick a timestamp for this run, if we haven't yet.
### NOTE: Left off here.
if (not $an->data->{sys}{db_timestamp}) if (not $an->data->{sys}{db_timestamp})
{ {
my $query = "SELECT cast(now() AS timestamp with time zone)"; my $query = "SELECT cast(now() AS timestamp with time zone)";
@ -379,36 +419,32 @@ sub connect
# If I've not sent an alert about this DB loss before, send one now. # If I've not sent an alert about this DB loss before, send one now.
my $set = $an->Alert->check_alert_sent({ my $set = $an->Alert->check_alert_sent({
type => "warning", type => "set",
alert_sent_by => $THIS_FILE, alert_set_by => $THIS_FILE,
alert_record_locator => $id, alert_record_locator => $id,
alert_name => "connect_to_db", alert_name => "connect_to_db",
modified_date => $an->data->{sys}{db_timestamp}, modified_date => $an->data->{sys}{db_timestamp},
}); });
$an->Log->entry({log_level => 3, message_key => "an_variables_0001", message_variables => { $an->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { set => $set });
name1 => "set", value1 => $set
}, file => $THIS_FILE, line => __LINE__});
if ($set) if ($set)
{ {
$an->Log->entry({log_level => 3, message_key => "an_variables_0001", message_variables => { $an->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { error_array => $error_array });
name1 => "error_array", value1 => $error_array
}, file => $THIS_FILE, line => __LINE__});
foreach my $hash (@{$error_array}) foreach my $hash (@{$error_array})
{ {
my $message_key = $hash->{message_key}; my $message_key = $hash->{message_key};
my $message_variables = $hash->{message_variables}; my $message_variables = $hash->{message_variables};
$an->Log->entry({log_level => 3, message_key => "an_variables_0003", message_variables => { $an->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => {
name1 => "hash", value1 => $hash, hash => $hash,
name2 => "message_key", value2 => $message_key, message_key => $message_key,
name3 => "message_variables", value3 => $message_variables, message_variables => $message_variables,
}, file => $THIS_FILE, line => __LINE__}); });
# These are warning level alerts. # These are warning level alerts.
$an->Alert->register_alert({ $an->Alert->register_alert({
alert_level => "warning", alert_level => "warning",
alert_agent_name => "ScanCore", alert_set_by => $THIS_FILE,
alert_title_key => "an_alert_title_0004", alert_title_key => "alert_title_0003",
alert_message_key => $message_key, alert_message_key => $message_key,
alert_message_variables => $message_variables, alert_message_variables => $message_variables,
}); });
@ -422,13 +458,10 @@ sub connect
# Query to see if the newly connected host is in the DB yet. If it isn't, don't send an # Query to see if the newly connected host is in the DB yet. If it isn't, don't send an
# alert as it'd cause a duplicate UUID error. # alert as it'd cause a duplicate UUID error.
my $query = "SELECT COUNT(*) FROM hosts WHERE host_name = ".$an->data->{sys}{use_db_fh}->quote($an->data->{database}{$id}{host}).";"; my $query = "SELECT COUNT(*) FROM hosts WHERE host_name = ".$an->data->{sys}{use_db_fh}->quote($an->data->{database}{$id}{host}).";";
$an->Log->entry({log_level => 3, message_key => "an_variables_0001", message_variables => { $an->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { query => $query });
name1 => "query", value1 => $query
}, file => $THIS_FILE, line => __LINE__});
my $count = $an->Database->query({id => $id, query => $query, source => $THIS_FILE, line => __LINE__})->[0]->[0]; my $count = $an->Database->query({id => $id, query => $query, source => $THIS_FILE, line => __LINE__})->[0]->[0];
$an->Log->entry({log_level => 3, message_key => "an_variables_0001", message_variables => { $an->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { count => $count });
name1 => "count", value1 => $count
}, file => $THIS_FILE, line => __LINE__});
if ($count > 0) if ($count > 0)
{ {
@ -456,32 +489,72 @@ sub connect
} }
} }
$an->Log->entry({log_level => 3, message_key => "an_variables_0001", message_variables => { $an->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { "sys::host_uuid" => $an->data->{sys}{host_uuid} });
name1 => "sys::host_uuid", value1 => $an->data->{sys}{host_uuid},
}, file => $THIS_FILE, line => __LINE__});
if ($an->data->{sys}{host_uuid} !~ /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/) if ($an->data->{sys}{host_uuid} !~ /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/)
{ {
# derp # derp. bad UUID
$an->Log->entry({log_level => 0, message_key => "error_message_0061", file => $THIS_FILE, line => __LINE__}); $an->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "log_0103"});
### TODO: Left off here
# Disconnect and set the connection count to '0'. # Disconnect and set the connection count to '0'.
$an->DB->disconnect_from_databases(); $an->Database->disconnect();
$connections = 0; $connections = 0;
} }
# For now, we just find which DBs are behind and let each agent deal with bringing their tables up to # For now, we just find which DBs are behind and let each agent deal with bringing their tables up to
# date. # date.
$an->DB->find_behind_databases({file => $file}); $an->database->_find_behind_databases({
source => $source,
tables => $$tables,
});
# Hold if a lock has been requested. # Hold if a lock has been requested.
$an->DB->locking(); $an->Database->locking();
# Mark that we're not active. # Mark that we're not active.
$an->DB->mark_active({set => 1}); $an->Database->mark_active({set => 1});
return($connections); return($connections);
} }
=head2
This cleanly closes any open file handles to all connected databases and clears some internal database related variables.
=cut
sub disconnect
{
my $self = shift;
my $parameter = shift;
my $an = $self->parent;
$an->Log->entry({log_level => 3, message_key => "tools_log_0001", message_variables => { function => "disconnect_from_databases" }, file => $THIS_FILE, line => __LINE__});
my $marked_inactive = 0;
foreach my $id (sort {$a cmp $b} keys %{$an->data->{database}})
{
# Don't do anything if there isn't an active file handle for this DB.
next if ((not $an->data->{cache}{db_fh}{$id}) or ($an->data->{cache}{db_fh}{$id} !~ /^DBI::db=HASH/));
# Clear locks and mark that we're done running.
if (not $marked_inactive)
{
$an->Database->mark_active({set => 0});
$an->DB->locking({release => 1});
$marked_inactive = 1;
}
$an->data->{cache}{db_fh}{$id}->disconnect;
delete $an->data->{cache}{db_fh}{$id};
}
# Delete the stored DB-related values.
delete $an->data->{sys}{db_timestamp};
delete $an->data->{sys}{use_db_fh};
delete $an->data->{sys}{read_db_id};
return(0);
}
=head2 get_local_id =head2 get_local_id
This returns the database ID from 'C<< striker.conf >>' based on matching the 'C<< database::<id>::host >>' to the local machine's host name or one of the active IP addresses on the host. This returns the database ID from 'C<< striker.conf >>' based on matching the 'C<< database::<id>::host >>' to the local machine's host name or one of the active IP addresses on the host.
@ -532,7 +605,7 @@ sub get_local_id
=head2 initialize =head2 initialize
This will initialize an empty database. This will initialize a database using a given file.
=cut =cut
sub initialize sub initialize
@ -542,7 +615,7 @@ sub initialize
my $an = $self->parent; my $an = $self->parent;
my $id = $parameter->{id} ? $parameter->{id} : $an->data->{sys}{read_db_id}; my $id = $parameter->{id} ? $parameter->{id} : $an->data->{sys}{read_db_id};
my $sql_file = $parameter->{sql_file} ? $parameter->{sql_file} : $an->data->{database}{$id}{core_sql}; my $sql_file = $parameter->{sql_file} ? $parameter->{sql_file} : $an->data->{path}{sql}{'Tools.sql'};
my $success = 1; my $success = 1;
$an->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { $an->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => {
id => $id, id => $id,
@ -620,11 +693,461 @@ sub initialize
$an->data->{sys}{db_initialized}{$id} = 1; $an->data->{sys}{db_initialized}{$id} = 1;
# Mark that we need to update the DB. # Mark that we need to update the DB.
$an->data->{database_resync_needed} = 1; $an->data->{database}{general}{resync_needed} = 1;
return($success); return($success);
}; };
=head2 insert_or_update_states
This updates (or inserts) a record in the 'states' table. The C<< state_uuid >> referencing the database row will be returned.
If there is an error, an empty string is returned.
Parameters;
=head3 state_uuid (optional)
This is the C<< state_uuid >> to update. If it is not specified but the C<< state_name >> is, a check will be made to see if an entry already exists. If so, that row will be UPDATEd. If not, a random UUID will be generated and a new entry will be INSERTed.
=head3 state_name (required)
This is the C<< state_name >> to INSERT or UPDATE. If a C<< state_uuid >> is passed, then the C<< state_name >> can be changed.
=head3 state_host_uuid (optional)
This is the host's UUID that this state entry belongs to. If not passed, C<< sys::host_uuid >> will be used.
=head3 state_note (optional)
This is an optional note related to this state entry.
=cut
sub insert_or_update_states
{
my $self = shift;
my $parameter = shift;
my $an = $self->parent;
my $state_uuid = $parameter->{state_uuid} ? $parameter->{state_uuid} : "";
my $state_name = $parameter->{state_name} ? $parameter->{state_name} : "";
my $state_host_uuid = $parameter->{state_host_uuid} ? $parameter->{state_host_uuid} : $an->data->{sys}{host_uuid};
my $state_note = $parameter->{state_note} ? $parameter->{state_note} : "NULL";
$an->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => {
state_uuid => $state_uuid,
state_name => $state_name,
state_host_uuid => $state_host_uuid,
state_note => $state_note,
}});
if (not $state_name)
{
# Throw an error and exit.
$an->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "log_0107"});
return("");
}
if (not $state_host_uuid)
{
# Throw an error and exit.
$an->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "log_0108"});
return("");
}
# If we don't have a UUID, see if we can find one for the given state server name.
if (not $state_uuid)
{
my $query = "
SELECT
state_uuid
FROM
states
WHERE
state_name = ".$an->data->{sys}{use_db_fh}->quote($state_name)."
AND
state_host_uuid = ".$an->data->{sys}{use_db_fh}->quote($state_host_uuid)."
;";
$an->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { query => $query }});
my $results = $an->DB->do_db_query({query => $query, source => $THIS_FILE, line => __LINE__});
my $count = @{$results};
$an->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => {
results => $results,
count => $count,
}});
foreach my $row (@{$results})
{
$state_uuid = $row->[0];
$an->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { state_uuid => $state_uuid }});
}
}
# If I still don't have an state_uuid, we're INSERT'ing .
$an->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { state_uuid => $state_uuid }});
if (not $state_uuid)
{
# It's possible that this is called before the host is recorded in the database. So to be
# safe, we'll return without doing anything if there is no host_uuid in the database.
my $hosts = $an->ScanCore->get_hosts();
my $found = 0;
foreach my $hash_ref (@{$hosts})
{
$an->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => {
"hash_ref->{host_uuid}" => $hash_ref->{host_uuid},
"sys::host_uuid" => $an->data->{sys}{host_uuid},
}});
if ($hash_ref->{host_uuid} eq $an->data->{sys}{host_uuid})
{
$found = 1;
$an->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { found => $found }});
}
}
if (not $found)
{
# We're out.
return("");
}
# INSERT
$state_uuid = $an->Get->uuid();
my $query = "
INSERT INTO
states
(
state_uuid,
state_name,
state_host_uuid,
state_note,
modified_date
) VALUES (
".$an->data->{sys}{use_db_fh}->quote($state_uuid).",
".$an->data->{sys}{use_db_fh}->quote($state_name).",
".$an->data->{sys}{use_db_fh}->quote($state_host_uuid).",
".$an->data->{sys}{use_db_fh}->quote($state_note).",
".$an->data->{sys}{use_db_fh}->quote($an->data->{sys}{db_timestamp})."
);
";
$query =~ s/'NULL'/NULL/g;
$an->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { query => $query }});
$an->DB->do_db_write({query => $query, source => $THIS_FILE, line => __LINE__});
}
else
{
# Query the rest of the values and see if anything changed.
my $query = "
SELECT
state_name,
state_host_uuid,
state_note
FROM
states
WHERE
state_uuid = ".$an->data->{sys}{use_db_fh}->quote($state_uuid)."
;";
$an->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { query => $query }});
my $results = $an->DB->do_db_query({query => $query, source => $THIS_FILE, line => __LINE__});
my $count = @{$results};
$an->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => {
results => $results,
count => $count,
}});
foreach my $row (@{$results})
{
my $old_state_name = $row->[0];
my $old_state_host_uuid = $row->[1];
my $old_state_note = defined $row->[2] ? $row->[2] : "NULL";
$an->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => {
old_state_name => $old_state_name,
old_state_host_uuid => $old_state_host_uuid,
old_state_note => $old_state_note,
}});
# Anything change?
if (($old_state_name ne $state_name) or
($old_state_host_uuid ne $state_host_uuid) or
($old_state_note ne $state_note))
{
# Something changed, save.
my $query = "
UPDATE
states
SET
state_name = ".$an->data->{sys}{use_db_fh}->quote($state_name).",
state_host_uuid = ".$an->data->{sys}{use_db_fh}->quote($state_host_uuid).",
state_note = ".$an->data->{sys}{use_db_fh}->quote($state_note).",
modified_date = ".$an->data->{sys}{use_db_fh}->quote($an->data->{sys}{db_timestamp})."
WHERE
state_uuid = ".$an->data->{sys}{use_db_fh}->quote($state_uuid)."
";
$query =~ s/'NULL'/NULL/g;
$an->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { query => $query }});
$an->DB->do_db_write({query => $query, source => $THIS_FILE, line => __LINE__});
}
}
}
$an->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { state_uuid => $state_uuid }});
return($state_uuid);
}
=head2 locking
This handles requesting, releasing and waiting on locks.
Parameters;
=head3
=cut
sub locking
{
my $self = shift;
my $parameter = shift;
my $an = $self->parent;
my $request = defined $parameter->{request} ? $parameter->{request} : 0;
my $release = defined $parameter->{release} ? $parameter->{release} : 0;
my $renew = defined $parameter->{renew} ? $parameter->{renew} : 0;
my $check = defined $parameter->{check} ? $parameter->{check} : 0;
my $source_name = $parameter->{source_name} ? $parameter->{source_name} : $an->hostname;
my $source_uuid = $parameter->{source_uuid} ? $parameter->{source_uuid} : $an->data->{sys}{host_uuid};
$an->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => {
request => $request,
release => $release,
renew => $renew,
check => $check,
source_name => $source_name,
source_uuid => $source_uuid,
}});
### TODO: Left off here
my $set = 0;
my $variable_name = "lock_request";
my $variable_value = $source_name."::".$source_uuid."::".time;
$an->Log->entry({log_level => 3, message_key => "an_variables_0002", message_variables => {
name1 => "variable_name", value1 => $variable_name,
name2 => "variable_value", value2 => $variable_value,
}, file => $THIS_FILE, line => __LINE__});
# Make sure we have a sane lock age
if ((not $an->data->{scancore}{locking}{reap_age}) or ($an->data->{scancore}{locking}{reap_age} =~ /\D/))
{
$an->data->{scancore}{locking}{reap_age} = 300;
$an->Log->entry({log_level => 3, message_key => "an_variables_0001", message_variables => {
name1 => "scancore::locking::reap_age", value1 => $an->data->{scancore}{locking}{reap_age},
}, file => $THIS_FILE, line => __LINE__});
}
# If I have been asked to check, we will return the variable_uuid if a lock is set.
if ($check)
{
my ($lock_value, $variable_uuid, $modified_date) = $an->ScanCore->read_variable({variable_name => $variable_name});
$an->Log->entry({log_level => 3, message_key => "an_variables_0003", message_variables => {
name1 => "lock_value", value1 => $lock_value,
name2 => "variable_uuid", value2 => $variable_uuid,
name3 => "modified_date", value3 => $modified_date,
}, file => $THIS_FILE, line => __LINE__});
return($lock_value);
}
# If I've been asked to clear a lock, do so now.
if ($release)
{
# We check to see if there is a lock before we clear it. This way we don't log that we
# released a lock unless we really released a lock.
my ($lock_value, $variable_uuid, $modified_date) = $an->ScanCore->read_variable({variable_name => $variable_name});
$an->Log->entry({log_level => 3, message_key => "an_variables_0003", message_variables => {
name1 => "lock_value", value1 => $lock_value,
name2 => "variable_uuid", value2 => $variable_uuid,
name3 => "modified_date", value3 => $modified_date,
}, file => $THIS_FILE, line => __LINE__});
if ($lock_value)
{
my $variable_uuid = $an->ScanCore->insert_or_update_variables({
variable_name => $variable_name,
variable_value => "",
update_value_only => 1,
});
$an->data->{sys}{local_lock_active} = 0;
$an->Log->entry({log_level => 3, message_key => "an_variables_0002", message_variables => {
name1 => "variable_uuid", value1 => $variable_uuid,
name2 => "sys::local_lock_active", value2 => $an->data->{sys}{local_lock_active},
}, file => $THIS_FILE, line => __LINE__});
$an->Log->entry({log_level => 1, message_key => "tools_log_0040", message_variables => { host => $an->hostname }, file => $THIS_FILE, line => __LINE__});
}
return($set);
}
# If I've been asked to renew, do so now.
if ($renew)
{
# Yup, do it.
my $variable_uuid = $an->ScanCore->insert_or_update_variables({
variable_name => $variable_name,
variable_value => $variable_value,
update_value_only => 1,
});
$an->Log->entry({log_level => 3, message_key => "an_variables_0001", message_variables => {
name1 => "variable_uuid", value1 => $variable_uuid,
}, file => $THIS_FILE, line => __LINE__});
if ($variable_uuid)
{
$set = 1;
$an->Log->entry({log_level => 3, message_key => "an_variables_0001", message_variables => {
name1 => "set", value1 => $set,
}, file => $THIS_FILE, line => __LINE__});
}
$an->data->{sys}{local_lock_active} = time;
$an->Log->entry({log_level => 3, message_key => "an_variables_0002", message_variables => {
name1 => "variable_uuid", value1 => $variable_uuid,
name2 => "sys::local_lock_active", value2 => $an->data->{sys}{local_lock_active},
}, file => $THIS_FILE, line => __LINE__});
$an->Log->entry({log_level => 1, message_key => "tools_log_0039", message_variables => { host => $an->hostname }, file => $THIS_FILE, line => __LINE__});
return($set);
}
# No matter what, we always check for, and then wait for, locks. Read in the locks, if any. If any
# are set and they are younger than scancore::locking::reap_age, we'll hold.
my $waiting = 1;
while ($waiting)
{
# Set the 'waiting' to '0'. If we find a lock, we'll set it back to '1'.
$waiting = 0;
# See if we had a lock.
my ($lock_value, $variable_uuid, $modified_date) = $an->ScanCore->read_variable({variable_name => $variable_name});
$an->Log->entry({log_level => 3, message_key => "an_variables_0004", message_variables => {
name1 => "waiting", value1 => $waiting,
name2 => "lock_value", value2 => $lock_value,
name3 => "variable_uuid", value3 => $variable_uuid,
name4 => "modified_date", value4 => $modified_date,
}, file => $THIS_FILE, line => __LINE__});
if ($lock_value =~ /^(.*?)::(.*?)::(\d+)/)
{
my $lock_source_name = $1;
my $lock_source_uuid = $2;
my $lock_time = $3;
my $current_time = time;
my $timeout_time = $lock_time + $an->data->{scancore}{locking}{reap_age};
my $lock_age = $current_time - $lock_time;
$an->Log->entry({log_level => 3, message_key => "an_variables_0006", message_variables => {
name1 => "lock_source_name", value1 => $lock_source_name,
name2 => "lock_source_uuid", value2 => $lock_source_uuid,
name3 => "current_time", value3 => $current_time,
name4 => "lock_time", value4 => $lock_time,
name5 => "timeout_time", value5 => $timeout_time,
name6 => "lock_age", value6 => $lock_age,
}, file => $THIS_FILE, line => __LINE__});
# If the lock is stale, delete it.
if ($current_time > $timeout_time)
{
# The lock is stale.
my $variable_uuid = $an->ScanCore->insert_or_update_variables({
variable_name => $variable_name,
variable_value => "",
update_value_only => 1,
});
$an->Log->entry({log_level => 3, message_key => "an_variables_0001", message_variables => {
name1 => "variable_uuid", value1 => $variable_uuid,
}, file => $THIS_FILE, line => __LINE__});
}
# Only wait if this isn't our own lock.
elsif ($lock_source_uuid ne $source_uuid)
{
# Mark 'wait', set inactive and sleep.
$an->DB->mark_active({set => 0});
$waiting = 1;
$an->Log->entry({log_level => 3, message_key => "an_variables_0003", message_variables => {
name1 => "lock_source_uuid", value1 => $lock_source_uuid,
name2 => "source_uuid", value2 => $source_uuid,
name3 => "waiting", value3 => $waiting,
}, file => $THIS_FILE, line => __LINE__});
sleep 5;
}
}
}
# If I am here, there are no pending locks. Have I been asked to set one?
if ($request)
{
# Yup, do it.
my $variable_uuid = $an->ScanCore->insert_or_update_variables({
variable_name => $variable_name,
variable_value => $variable_value,
update_value_only => 1,
});
$an->Log->entry({log_level => 3, message_key => "an_variables_0001", message_variables => {
name1 => "variable_uuid", value1 => $variable_uuid,
}, file => $THIS_FILE, line => __LINE__});
if ($variable_uuid)
{
$set = 1;
$an->data->{sys}{local_lock_active} = time;
$an->Log->entry({log_level => 3, message_key => "an_variables_0003", message_variables => {
name1 => "set", value1 => $set,
name2 => "variable_uuid", value2 => $variable_uuid,
name3 => "sys::local_lock_active", value3 => $an->data->{sys}{local_lock_active},
}, file => $THIS_FILE, line => __LINE__});
$an->Log->entry({log_level => 1, message_key => "tools_log_0038", message_variables => { host => $an->hostname }, file => $THIS_FILE, line => __LINE__});
}
}
# Now return.
return($set);
}
=head2 mark_active
This sets or clears that the caller is about to work on the database
Parameters;
=head3 set (optional, default C<< 1 >>)
If set to c<< 0 >>,
=cut
sub mark_active
{
my $self = shift;
my $parameter = shift;
my $an = $self->parent;
my $set = defined $parameter->{set} ? $parameter->{set} : 1;
$an->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { set => $set }});
# If I haven't connected to a database yet, why am I here?
if (not $an->data->{sys}{read_db_id})
{
return(0);
}
my $value = "false";
if ($set)
{
$value = "true";
}
$an->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { value => $value }});
my $state_uuid = $an->Database->insert_or_update_states({
state_name => "db_in_use",
state_host_uuid => $an->data->{sys}{host_uuid},
state_note => $value,
});
$an->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { state_uuid => $state_uuid }});
return($state_uuid);
}
=head2 query =head2 query
This performs a query and returns an array reference of array references (from C<< DBO->fetchall_arrayref >>). The first array contains all the returned rows and each row is an array reference of columns in that row. This performs a query and returns an array reference of array references (from C<< DBO->fetchall_arrayref >>). The first array contains all the returned rows and each row is an array reference of columns in that row.
@ -966,6 +1489,253 @@ sub write
# Private functions # # Private functions #
############################################################################################################# #############################################################################################################
=head2 _find_behind_databases
This returns the most up to date database ID, the time it was last updated and an array or DB IDs that are behind.
If there is a problem, C<< undef >> is returned.
Parameters;
=head3 source (required)
This is used the same as in C<< Database->connect >>'s C<< source >> parameter. Please read that for usage information.
=head3 tables (optional)
This is used the same as in C<< Database->connect >>'s C<< tables >> parameter. Please read that for usage information.
=cut
sub _find_behind_databases
{
my $self = shift;
my $parameter = shift;
my $an = $self->parent;
$an->Log->entry({log_level => 3, title_key => "tools_log_0001", title_variables => { function => "find_behind_databases" }, message_key => "tools_log_0002", file => $THIS_FILE, line => __LINE__});
my $source = $parameter->{source} ? $parameter->{source} : "";
my $tables = $parameter->{tables} ? $parameter->{tables} : "";
$an->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => {
source => $source,
tables => $tables,
}});
# This should always be set, but just in case...
if (not $source)
{
$an->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "log_0105"});
return(undef);
}
# Look at all the databases and find the most recent time stamp (and the ID of the DB).
$an->data->{database}{general}{source_db_id} = 0;
$an->data->{database}{general}{source_updated_time} = 0;
foreach my $id (sort {$a cmp $b} keys %{$an->data->{database}})
{
my $name = $an->data->{database}{$id}{name};
my $user = $an->data->{database}{$id}{user};
# Read the table's last modified_date
my $query = "
SELECT
round(extract(epoch from modified_date))
FROM
updated
WHERE
updated_host_uuid = ".$an->data->{sys}{use_db_fh}->quote($an->data->{sys}{host_uuid})."
AND
updated_by = ".$an->data->{sys}{use_db_fh}->quote($source).";";
;";";
$an->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => {
id => $id,
query => $query,
});
my $last_updated = $an->DB->do_db_query({id => $id, query => $query, source => $THIS_FILE, line => __LINE__})->[0]->[0];
$last_updated = 0 if not defined $last_updated;
$an->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => {
last_updated => $last_updated,
"database::general::source_updated_time" => $an->data->{database}{general}{source_updated_time},
});
if ($last_updated > $an->data->{database}{general}{source_updated_time})
{
$an->data->{database}{general}{source_updated_time} = $last_updated;
$an->data->{database}{general}{source_db_id} = $id;
$an->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => {
"database::general::source_db_id" => $an->data->{database}{general}{source_db_id},
"database::general::source_updated_time" => $an->data->{database}{general}{source_updated_time},
});
}
# Get the last updated time for this database (and source).
$an->data->{database}{$id}{last_updated} = $last_updated;
$an->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => {
"database::general::source_updated_time" => $an->data->{database}{general}{source_updated_time},
"database::general::source_db_id" => $an->data->{database}{general}{source_db_id},
"database::${id}::last_updated" => $an->data->{database}{$id}{last_updated}
});
# If we have a tables hash, look into them, too.
if (ref($tables) eq "HASH")
{
foreach my $table (sort {$a cmp $b} keys %{$tables})
{
# I'm going to both check the number of entries in the history schema
my $table_name = $an->data->{sys}{use_db_fh}->quote($test_table);
$table_name =~ s/'(.*?)'/$1/;
my $host_column = $an->data->{sys}{use_db_fh}->quote($tables->{$table});
$host_column =~ s/'(.*?)'/$1/;
$an->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => {
table_name => $table_name,
host_column => $host_column,
});
my $query = "
SELECT
round(extract(epoch from modified_date))
FROM
history.$table_name ";
if ($host_column)
{
$query .= "
WHERE
$host_column = ".$an->data->{sys}{use_db_fh}->quote($an->data->{sys}{host_uuid})."
";
}
$query .= "
ORDER BY
modified_date DESC
;";
$an->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => {
id => $id,
query => $query,
});
my $last_updated = $an->DB->do_db_query({id => $id, query => $query, source => $THIS_FILE, line => __LINE__})->[0]->[0];
$last_updated = 0 if not defined $last_updated;
$an->data->{database}{$id}{tables}{$table}{last_updated} = $last_updated;
### TODO: Left off here. Loop through these looking for differences in the tables.
$an->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => {
"database::${id}::tables::${table}::last_updated" => $an->data->{database}{$id}{tables}{$table}{last_updated},
});
}
}
}
# Find which DB is most up to date.
$an->data->{database}{general}{to_update} = {};
foreach my $id (sort {$a cmp $b} keys %{$an->data->{database}})
{
$an->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => {
"database::general::source_updated_time" => $an->data->{database}{general}{source_updated_time},
"database::${id}::last_updated" => $an->data->{database}{$id}{last_updated},
});
if ($an->data->{database}{general}{source_updated_time} > $an->data->{database}{$id}{last_updated})
{
# This database is behind
$an->Log->entry({source => $THIS_FILE, line => __LINE__, level => 1, priority => "alert", key => "log_0104", variables => {
id => $id,
file => $file,
}});
# A database is behind, resync
$an->Database->_mark_database_as_behind({id => $id});
}
else
{
# This database is up to date (so far).
$an->data->{database}{general}{to_update}{$id}{behind} = 0;
$an->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => {
"database::general::to_update::${id}::behind" => $an->data->{database}{general}{to_update}{$id}{behind},
});
}
# If we don't yet need a resync, and if we were passed one or more tables, check those tables
# for differences
if ((not $an->data->{database}{general}{resync_needed}) && (ref($tables) eq "HASH"))
{
foreach my $table (sort {$a cmp $b} keys %{$tables})
{
if (not defined $an->data->{database}{general}{tables}{$table}{last_updated})
{
# First we've seen, set the general updated time to this entry
$an->data->{database}{general}{tables}{$table}{last_updated} = $an->data->{database}{$id}{tables}{$table}{last_updated};
$an->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => {
"database::general::tables::${table}::last_updated" => $an->data->{database}{general}{tables}{$table}{last_updated}
});
}
if ($an->data->{database}{general}{tables}{$table}{last_updated} > $an->data->{database}{$id}{tables}{$table}{last_updated})
{
# This database is behind
$an->Log->entry({source => $THIS_FILE, line => __LINE__, level => 1, priority => "alert", key => "log_0106", variables => {
id => $id,
file => $file,
table => $table,
}});
}
# Mark it as behind.
$an->Database->_mark_database_as_behind({id => $id});
}
}
}
return(0);
}
=head2 _mark_database_as_behind
This method marks that a resync is needed and, if needed, switches the database this machine will read from.
Parameters;
=head3 id
This is the C<< id >> of the database being marked as "behind".
=cut
sub _mark_database_as_behind
{
my $self = shift;
my $parameter = shift;
my $an = $self->parent;
my $id = $parameter->{id} ? $parameter->{id} : "";
$an->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { id => $id }});
$an->data->{database}{general}{to_update}{$id}{behind} = 1;
$an->data->{database}{general}{resync_needed} = 1;
$an->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => {
"database::general::to_update::${id}::behind" => $an->data->{database}{general}{to_update}{$id}{behind},
"database::general::resync_needed" => $an->data->{database}{general}{resync_needed},
});
# We can't trust this database for reads, so switch to another database for reads if
# necessary.
$an->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => {
id => $id,
"sys::read_db_id" => $an->data->{sys}{read_db_id},
});
}, file => $THIS_FILE, line => __LINE__});
if ($id eq $an->data->{sys}{read_db_id})
{
# Switch.
$an->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { ">> sys::read_db_id" => $an->data->{sys}{read_db_id} });
foreach my $this_id (sort {$a cmp $b} keys %{$an->data->{database}})
{
next if $this_id eq $id;
$an->data->{sys}{read_db_id} = $this_id;
$an->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { "<< sys::read_db_id" => $an->data->{sys}{read_db_id} });
last;
}
}
return(0);
}
=head2 _test_access =head2 _test_access
This method takes a database ID and performs a simple C<< SELECT 1 >> query, wrapped in a ten second C<< alarm >>. If the database has died, the query will hang and the C<< alarm >> will fire, killing this program. If the call returns, the C<< alarm >> is cancelled. This method takes a database ID and performs a simple C<< SELECT 1 >> query, wrapped in a ten second C<< alarm >>. If the database has died, the query will hang and the C<< alarm >> will fire, killing this program. If the call returns, the C<< alarm >> is cancelled.

@ -13,6 +13,16 @@ This is the AN::Tools master 'words' file.
<!-- Canadian English --> <!-- Canadian English -->
<language name="en_CA" long_name="Canadian English" description="Created by Madison Kelly (mkelly@alteeve.ca) for the AN::Tools suite of perl modules"> <language name="en_CA" long_name="Canadian English" description="Created by Madison Kelly (mkelly@alteeve.ca) for the AN::Tools suite of perl modules">
<!-- Alert titles -->
<key name="alert_title_0001">Debug</key>
<key name="alert_title_0002">Information</key>
<key name="alert_title_0003">Notice</key>
<key name="alert_title_0004">Warning</key>
<key name="alert_title_0005">Critical</key>
<key name="alert_title_0006">Warning Cleared</key>
<key name="alert_title_0007">Critical Cleared</key>
<key name="alert_title_0008">Important</key> <!-- This is used for 'warning' level alerts that aren't a problem, but important information. -->
<!-- Messages for users (less technical than log entries), though sometimes used for logs, too. --> <!-- Messages for users (less technical than log entries), though sometimes used for logs, too. -->
<key name="message_0001">The host name: [#!variable!target!#] does not resolve to an IP address.</key> <key name="message_0001">The host name: [#!variable!target!#] does not resolve to an IP address.</key>
<key name="message_0002">The connection to: [#!variable!target!#] on port: [#!variable!port!#] as the user: [#!variable!user!#] was refused. If you recently booted the target, it may be just about finished booting. It is normal for the connection to be refused for a brief time during the boot process.</key> <key name="message_0002">The connection to: [#!variable!target!#] on port: [#!variable!port!#] as the user: [#!variable!user!#] was refused. If you recently booted the target, it may be just about finished booting. It is normal for the connection to be refused for a brief time during the boot process.</key>
@ -144,6 +154,30 @@ The database connection error was:
<key name="log_0090"><![CDATA[[ Error ] - Failed to 'do' the database query: [#!variable!query!#] on: [#!variable!server!#]. The error was: [#!variable!db_error!#]. Note that if the query reports '--', the query was listed as containing sensitive data and '$an->Log->secure' is not set.]]></key> <key name="log_0090"><![CDATA[[ Error ] - Failed to 'do' the database query: [#!variable!query!#] on: [#!variable!server!#]. The error was: [#!variable!db_error!#]. Note that if the query reports '--', the query was listed as containing sensitive data and '$an->Log->secure' is not set.]]></key>
<key name="log_0091">Failed to connect to any database.</key> <key name="log_0091">Failed to connect to any database.</key>
<key name="log_0092"><![CDATA[[ Error ] - Unable to connect to the database: [#!variable!server!#] (id: [#!variable!id!#]_.]]></key> <key name="log_0092"><![CDATA[[ Error ] - Unable to connect to the database: [#!variable!server!#] (id: [#!variable!id!#]_.]]></key>
<key name="log_0093"><![CDATA[[ Error ] - The method Alert->check_alert_sent() was called but the 'modified_date' parameter was not passed and/or 'sys::db_timestamp' is not set. Did the program fail to connect to any databases?]]></key>
<key name="log_0094"><![CDATA[[ Error ] - The method Alert->check_alert_sent() was called but the 'name' parameter was not passed or it is empty.]]></key>
<key name="log_0095"><![CDATA[[ Error ] - The method Alert->check_alert_sent() was called but the 'record_locator' parameter was not passed or it is empty.]]></key>
<key name="log_0096"><![CDATA[[ Error ] - The method Alert->check_alert_sent() was called but the 'set_by' parameter was not passed or it is empty.]]></key>
<key name="log_0097"><![CDATA[[ Error ] - The method Alert->check_alert_sent() was called but the 'set' parameter was not passed or it is empty. It should be 'set' or 'clear'.]]></key>
<key name="log_0098">
[ Warning ] - Failed to set an alert because this host is not yet in the database. This can happen if the alert was set before this host was added to the database.
* Details of the alert:
- Type: [#!variable!type!#]
- Sent By: [#!variable!alert_sent_by!#]
- Record Locator: [#!variable!alert_record_locator!#]
- Name: [#!variable!alert_name!#]
- Timestamp: [#!variable!modified_date!#]
</key>
<key name="log_0099"><![CDATA[[ Error ] - The method Alert->register_alert() was called but the 'set_by' parameter was not passed or it is empty.]]></key>
<key name="log_0100"><![CDATA[[ Error ] - The method Alert->register_alert() was called but the 'message_key' parameter was not passed or it is empty.]]></key>
<key name="log_0101"><![CDATA[[ Error ] - The method Alert->register_alert() was called but the 'title_key' parameter was not passed or it is empty and 'header' is enable (default).]]></key>
<key name="log_0102">I am not recording the alert with message_key: [#!variable!message_key!#] to the database because its log level was lower than any recipients.</key>
<key name="log_0103">The local machine's UUID was not read properly. It should be stored in: [#!data!sys::host_uuid!#] and contain hexadecimal characters in the format: '012345-6789-abcd-ef01-23456789abcd' and usually matches the output of 'dmidecode --string system-uuid'. If this file exists and if there is a string in the file, please verify that it is structured correctly.</key>
<key name="log_0104">The database with ID: [#!variable!id!#] for: [#!variable!file!#] is behind.</key>
<key name="log_0105"><![CDATA[[ Error ] - The method Database->_find_behind_databases() was called but the 'source' parameter was not passed or it is empty.]]></key>
<key name="log_0106">The database with ID: [#!variable!id!#] for: [#!variable!file!#] and table: [#!variable!table!#] is behind.</key>
<key name="log_0107"><![CDATA[[ Error ] - The method Database->insert_or_update_states() was called but the 'state_name' parameter was not passed or it is empty.]]></key>
<key name="log_0108"><![CDATA[[ Error ] - The method Database->insert_or_update_states() was called but the 'state_host_uuid' parameter was not passed or it is empty. Normally this is .]]></key>
<!-- Test words. Do NOT change unless you update 't/Words.t' or tests will needlessly fail. --> <!-- Test words. Do NOT change unless you update 't/Words.t' or tests will needlessly fail. -->
<key name="t_0000">Test</key> <key name="t_0000">Test</key>

@ -0,0 +1,4 @@
-- This is the core database schema for ScanCore.
-- It builds on AN::Tools.sql.

@ -4,7 +4,6 @@
database::1::host = 192.168.122.201 database::1::host = 192.168.122.201
database::1::port = 5432 database::1::port = 5432
database::1::name = scancore database::1::name = scancore
database::1::core_sql = /etc/scancore/scancore.sql
database::1::user = admin database::1::user = admin
database::1::password = Initial1 database::1::password = Initial1
database::1::ping_before_connect = 1 database::1::ping_before_connect = 1
@ -12,7 +11,6 @@ database::1::ping_before_connect = 1
database::2::host = an-striker02.alteeve.com database::2::host = an-striker02.alteeve.com
database::2::port = 5432 database::2::port = 5432
database::2::name = scancore database::2::name = scancore
database::1::core_sql = /etc/scancore/scancore.sql
database::2::user = admin database::2::user = admin
database::2::password = Initial1 database::2::password = Initial1
database::2::ping_before_connect = 1 database::2::ping_before_connect = 1

Loading…
Cancel
Save