#!/usr/bin/perl -wT ############################################################ # # $Id: probe_machines.pl 976 2007-03-04 20:47:36Z nicolaw $ # probe_machine.pl - Example script for Parse::DMIDecode # # Copyright 2006 Nicola Worthington # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ############################################################ # vim:ts=4:sw=4:tw=78 # Database credentials use constant DBI_DSN => 'DBI:mysql:database:hostname'; use constant DBI_USER => 'username'; use constant DBI_PASS => 'password'; # Where and how to generate a list of hosts to probe use constant HOSTS_REGEX => qr{([a-zA-|0-9\-\_\.]+)}; use constant HOSTS_SKIP => qw{(localhost|localhost.localdomain)}; use constant HOSTS_SRC => '/etc/hosts'; # Remote command to gather information use constant SSH_CMD => 'ssh'; use constant REMOTE_CMD => 'export PATH=/bin:/usr/bin:/sbin:/usr/sbin; echo ==dmidecode==; dmidecode; echo ==biosdecode==; biosdecode; echo ==vpddecode==; vpddecode; echo ==distribution==; grep . /etc/*debian* /etc/redhat-release /etc/mandrake-release /etc/SuSE-release; echo ==netstat==; netstat -ltnup; echo ==x86info==; x86info; echo ==lspci==; lspci; echo ==cpuinfo==; cat /proc/cpuinfo; echo ==meminfo==; cat /proc/meminfo; echo ==lsmod==; lsmod; echo ==modules==; cat /proc/modules; echo ==hostname==; hostname; echo ==route==; route -n; echo ==ifconfig==; ifconfig -a; echo ==iptables==; iptables -L -n -v; echo ==ethtool==; ethtool eth0; ethtool eth1; echo ==ipdiscover==; ipdiscover eth0; ipdiscover eth1; echo ==resolv==; cat /etc/resolv.conf; echo ==hosts==; cat /etc/hosts; echo ==uname==; uname -a; echo ==rpm==; rpm -qa --queryformat \"%{NAME} %{VERSION} %{SUMMARY}\n\"; echo ==dpkg==; dpkg -l; echo ==uptime==; uptime; echo ==who==; who; echo ==w==; w; echo ==date==; date; echo ==iostat==; iostat; echo ==vmstat==; vmstat; echo ==free==; free; echo ==pstree==; pstree; echo ==ps==; ps -ef; echo ==last==; last; echo ==issue==; cat /etc/issue; echo ==dmesg==; dmesg; echo ==ide==; grep -r . /proc/ide/; echo ==scsi==; grep -r . /proc/scsi/; echo ==fdisk==; fdisk -l /dev/hd* /dev/sd*; echo ==partitions==; cat /proc/partitions; echo ==mounts==; cat /proc/mounts; echo ==mount==; mount; echo ==df==; df -TP'; ######################################################### # # # No user servicable parts inside past this point # # ######################################################### use 5.6.1; use strict; use DBI qw(); use Getopt::Std qw(getopts); use Parse::DMIDecode 0.02 qw(); %ENV = (PATH => '/bin:/usr/bin'); $|++; my $opts = {}; Getopt::Std::getopts('hCH:',$opts); display_help(),exit if defined $opts->{h}; my $dbh = DBI->connect(DBI_DSN,DBI_USER,DBI_PASS,{AutoCommit => 0}); create_tables() if defined $opts->{C}; my $dmi = Parse::DMIDecode->new(nowarnings => 1); my $skip_regex = HOSTS_SKIP; my @hosts = defined $opts->{H} ? ($opts->{H}) : get_hostnames(); for my $machine (sort(@hosts)) { print "Processing $machine ". "." x (49-length($machine)); print_result('skipped'), next if $machine =~ /$skip_regex/; my $data = probe_server($machine); if (defined $data->{NOCONNECT}) { print_result('connect failed'); } elsif (defined $data->{'system-uuid'} && $data->{'system-uuid'} =~ /^[A-F0-9\-]{36}$/) { update_database($data); print_result('done'); } else { print_result('no uuid'); } } $dbh->disconnect(); exit; sub print_result { my $str = shift; my $width = 15; my $dots = $width - length($str) - 1; printf("%s %s\n", '.' x $dots, $str); } sub probe_server { my ($machine) = $_[0] =~ /([a-z0-9\.\-\_]+)/i; (my $cmd = sprintf('%s %s "%s" 2>/dev/null', SSH_CMD, $machine, REMOTE_CMD)) =~ s/\n//g; my %raw = (HOSTNAME => $machine, NOCONNECT => 1); my $group; if (open(PH,'-|',$cmd)) { while (local $_ = ) { if (/^==+(\S+?)==+$/) { $group = $1; delete $raw{NOCONNECT}; } elsif (defined $group && $group =~ /\S+/) { $raw{$group} .= $_; } else { print $_; } } close(PH); } return \%raw if defined $raw{NOCONNECT}; return parse_raw_data($machine,\%raw); } sub parse_raw_data { my ($machine,$raw) = @_; # ==dmidecode== if (defined $raw->{dmidecode}) { $dmi->parse($raw->{dmidecode}); for (qw(system-uuid system-serial-number system-manufacturer system-product-name system-vendor system-product baseboard-product-name baseboard-manufacturer bios-version bios-vendor chassis-type)) { $raw->{$_} = $dmi->keyword($_); $raw->{$_} = '' unless defined $raw->{$_}; } $raw->{'physical-cpu-qty'} = 0; for my $handle ($dmi->get_handles(group => 'processor')) { next unless defined $handle->keyword('processor-type') && $handle->keyword('processor-type') =~ /Central Processor/i; $raw->{'physical-cpu-qty'}++; for (qw(processor-family processor-manufacturer processor-current-speed processor-id processor-type processor-version processor-signature processor-flags)) { my $value = $handle->keyword($_); if (!defined $value || (defined $value && $value =~ /Not Specified/i)) { $raw->{$_} = ''; } else { $raw->{$_} = $value unless defined $raw->{$_} && $raw->{$_} =~ /\S/; } } } # Account for older versions of dmidecode output if ($raw->{'system-product'} =~ /\S/ && $raw->{'system-product-name'} !~ /\S/) { $raw->{'system-product-name'} = $raw->{'system-product'}; } if ($raw->{'system-vendor'} =~ /\S/ && $raw->{'system-manufacturer'} !~ /\S/) { $raw->{'system-manufacturer'} = $raw->{'system-vendor'}; } # Bodge together a pretend uuid if (!defined($raw->{'system-uuid'}) || $raw->{'system-uuid'} !~ /^[A-F0-9\-]{36}$/) { ($raw->{'system-uuid'} = uc(join('-', ($raw->{'processor-id'}||''), ($raw->{'baseboard-product-name'}||''), ($raw->{'baseboard-manufacturer'}||''), ($raw->{'bios-version'}||''), ($raw->{'bios-vendor'}||''), ($raw->{'processor-signature'}||''), ($raw->{'processor-type'}||''), ($machine x 20), ))) =~ s/[^A-F0-9]//g; $raw->{'system-uuid'} = sprintf('%s-%s-%s-%s-%s', substr($raw->{'system-uuid'},0,8), substr($raw->{'system-uuid'},7,4), substr($raw->{'system-uuid'},11,4), substr($raw->{'system-uuid'},15,4), substr($raw->{'system-uuid'},19,12), ); } } # ==ifconfig== if (defined $raw->{ifconfig}) { $raw->{hwaddr} = [()]; for (split(/\n/,$raw->{ifconfig})) { if (my ($if,$hwaddr) = $_ =~ /^(\S+)\s+.+?\s+HWaddr:?\s+(\S+)\s*$/i) { push @{$raw->{hwaddr}}, "$hwaddr $if" if $if !~ /:/; } } } # ==distribution== if (defined $raw->{distribution}) { if ($raw->{distribution} =~ /Red Hat Enterprise Linux/mi) { $raw->{distribution} = 'RHEL'; } elsif ($raw->{distribution} =~ /Slackware/mi) { $raw->{distribution} = 'Slackware'; } elsif ($raw->{distribution} =~ /Mandrake/mi) { $raw->{distribution} = 'Mandrake'; } elsif ($raw->{distribution} =~ /SuSE/mi) { $raw->{distribution} = 'SuSE'; } elsif ($raw->{distribution} =~ /Ubuntu/mi) { $raw->{distribution} = 'Ubuntu'; } elsif ($raw->{distribution} =~ /Red\s*Hat/mi) { $raw->{distribution} = 'RedHat'; } elsif ($raw->{distribution} =~ /Debian/mi) { $raw->{distribution} = 'Debian'; } else { $raw->{distribution} = 'Linux'; } } return $raw; } sub create_tables { my @statements; my $statement; while (local $_ = ) { chomp; next if /^\s*(#|\-\-|;)/ || /^\s*$/; last if /^__END__\s*$/; s/\t/ /; if (/;\s*$/) { $statement .= $_; push @statements, $statement; $statement = ''; } else { $statement .= $_; } } print "Recreating database tables ..."; for my $statement (@statements) { $statement =~ s/;\s*//; my $sth = $dbh->prepare($statement); $sth->execute; } $dbh->commit; print " done\n"; } sub get_model_id { my ($make,$model,$data) = @_; $make ||= 'Unknown'; $model ||= 'Unknown'; my $sth = $dbh->prepare('SELECT model_id FROM model WHERE make = ? AND model = ?'); $sth->execute($make,$model); my ($model_id) = $sth->rows == 1 ? $sth->fetchrow_array : undef; if (!defined $model_id && $sth->rows <= 1) { my $form = (defined $data->{'chassis-type'} ? $data->{'chassis-type'} : undef); if ($make =~ /^Dell (Inc\.|Computer Corporation)$/ && $model =~ /^PowerEdge (?:750|([12])\d\d0)$/) { $form = defined $1 ? "${1}U" : '1U'; } $sth = $dbh->prepare('INSERT INTO model (make,model,form) VALUES (?,?,?)'); $sth->execute($make,$model,$form); $model_id = $dbh->{'mysql_insertid'}; } $sth->finish; return $model_id; } sub update_record { my $ref = {@_}; die "No table defined." if !exists $ref->{table}; die "No column data defined." if !exists $ref->{cols}; die "No where clause defined." if !exists $ref->{where}; # Delete or check for existing row my @where; my @where_bind; while (my ($col,$value) = each %{$ref->{where}}) { push @where, sprintf(' %s = ? ',$col); push @where_bind, $value; } my $sql = sprintf('%s FROM %s WHERE %s', ($ref->{delete_first} ? 'DELETE' : 'SELECT *'), $ref->{table}, join(' AND ',@where), ); my $sth = $dbh->prepare($sql); $sth->execute(@where_bind); # Update an existing row if (!$ref->{delete_first} && $sth->rows >= 1) { my @set; my @set_bind; while (my ($col,$value) = each %{$ref->{cols}}) { if (defined $value && $value eq 'NOW()') { push @set, sprintf(' %s = %s ',$col,$value); } else { push @set, sprintf(' %s = ? ',$col); push @set_bind, $value; } } $sql = sprintf('UPDATE %s SET %s WHERE %s', $ref->{table}, join(', ',@set), join(' AND ',@where), ); $sth = $dbh->prepare($sql); $sth->execute(@set_bind,@where_bind); # Insert a new row } else { my @cols; my @cols_bind; my @placeholders; while (my ($col,$value) = each %{$ref->{cols}}) { if (defined $value && $value eq 'NOW()') { push @cols, $col; push @placeholders, $value; } else { push @cols, $col; push @cols_bind, $value; push @placeholders, '?'; } } $sql = sprintf('INSERT INTO %s (%s) VALUES (%s)', $ref->{table}, join(',',@cols), join(',',@placeholders), ); $sth = $dbh->prepare($sql); $sth->execute(@cols_bind); } $sth->finish; } sub update_database { my $data = shift; my $model_id = get_model_id( $data->{'system-manufacturer'}, $data->{'system-product-name'}, $data ); update_record( table => 'machine', cols => { model_id => $model_id, serial => $data->{'system-serial-number'}, uuid => $data->{'system-uuid'}, last_checked => 'NOW()', }, where => { uuid => $data->{'system-uuid'}, }, ); update_record( table => 'host', cols => { uuid => $data->{'system-uuid'}, hostname => $data->{HOSTNAME}, os => $data->{'distribution'}, last_checked => 'NOW()', }, where => { hostname => $data->{HOSTNAME}, }, ); my @probes = REMOTE_CMD =~ /==([a-z0-9\-\_]+)==/g; for my $probe (@probes) { update_record( table => 'probe', cols => { uuid => $data->{'system-uuid'}, probe => $probe, data => $data->{$probe}, }, where => { uuid => $data->{'system-uuid'}, probe => $probe, }, ); } my $sth = $dbh->prepare('DELETE FROM host WHERE uuid = ? AND hostname != ?'); $sth->execute($data->{'system-uuid'},$data->{HOSTNAME}); my %seen_hwaddr; for (@{$data->{hwaddr}}) { my ($hwaddr,$interface) = split(/\s+/,$_); $hwaddr =~ s/[^0-9a-f]+//gi; next if $interface =~ /:/ || exists $seen_hwaddr{$hwaddr}; $seen_hwaddr{$hwaddr} = 1; update_record( table => 'nic', cols => { uuid => $data->{'system-uuid'}, hwaddr => $hwaddr, interface => $interface, }, where => { uuid => $data->{'system-uuid'}, hwaddr => $hwaddr, }, ); } my ($cpu_speed) = ($data->{'processor-current-speed'}||'') =~ /(\d+)/; my @processor_flags = (); if (ref($data->{'processor-flags'}) eq 'ARRAY') { for (@{$data->{'processor-flags'}}) { if (/^(\S+)/) { push @processor_flags, $1; } } } update_record( table => 'cpu', cols => { manufacturer => $data->{'processor-manufacturer'}, family => $data->{'processor-family'}, version => $data->{'processor-version'}, speed => $cpu_speed, signature => $data->{'processor-signature'}, flags => join(',',@processor_flags), qty => $data->{'physical-cpu-qty'}, uuid => $data->{'system-uuid'}, }, where => { uuid => $data->{'system-uuid'}, }, ); $sth->finish; $dbh->commit; } sub get_hostnames { print "Getting hostnames ..."; my @data; if (-f HOSTS_SRC && -r HOSTS_SRC) { if (open(FH,'<',HOSTS_SRC)) { @data = ; close(FH); } } else { eval { require LWP::Simple; @data = split(/\n/, LWP::Simple::get(HOSTS_SRC)); }; warn $@ if $@; } my $regex = HOSTS_REGEX; my %hosts; for (@data) { if (/$regex/) { $hosts{$1} = 1; } } my $hosts = scalar(keys %hosts) || 0; print " found $hosts host".($hosts == 1 ? '' : 's')."\n"; return sort keys %hosts; } sub display_help { print qq{Syntax: $0 [-h] [-H ] [-C] -h Display this help -H Only probe -C Recreate the database tables }; } __DATA__ DROP TABLE IF EXISTS nic; DROP TABLE IF EXISTS cpu; DROP TABLE IF EXISTS probe; DROP TABLE IF EXISTS service; DROP TABLE IF EXISTS host; DROP TABLE IF EXISTS machine; DROP TABLE IF EXISTS model; #DROP TABLE IF EXISTS contact; #DROP TABLE IF EXISTS history; CREATE TABLE model ( model_id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, make VARCHAR(32), model VARCHAR(32), form VARCHAR(16) ) ENGINE=InnoDB; CREATE TABLE machine ( uuid CHAR(36) NOT NULL PRIMARY KEY, serial VARCHAR(16), model_id INT UNSIGNED, created DATETIME NOT NULL, last_checked TIMESTAMP NOT NULL, FOREIGN KEY (model_id) REFERENCES model(model_id) ) ENGINE=InnoDB; CREATE TABLE host ( hostname VARCHAR(32) NOT NULL PRIMARY KEY, uuid CHAR(36) NOT NULL, os ENUM('Debian','Mandrake','RedHat','RHEL','Ubuntu','Gentoo','Slackware','SuSE','Windows','Linux'), created DATETIME NOT NULL, last_checked TIMESTAMP NOT NULL, FOREIGN KEY (uuid) REFERENCES machine(uuid) ON DELETE CASCADE ) ENGINE=InnoDB; CREATE TABLE service ( hostname VARCHAR(32) NOT NULL, servicename VARCHAR(32) NOT NULL, description VARCHAR(255), type ENUM('Production','Staging','Development','Infrastructure','Research','Other') NOT NULL, PRIMARY KEY (hostname,servicename), FOREIGN KEY (hostname) REFERENCES host(hostname) ON DELETE CASCADE ) ENGINE=InnoDB; CREATE TABLE nic ( hwaddr CHAR(12) NOT NULL, uuid CHAR(36) NOT NULL, interface VARCHAR(8), PRIMARY KEY (hwaddr,uuid), FOREIGN KEY (uuid) REFERENCES machine(uuid) ON DELETE CASCADE ) ENGINE=InnoDB; CREATE TABLE cpu ( uuid CHAR(36) NOT NULL PRIMARY KEY, manufacturer VARCHAR(16), family VARCHAR(16), version VARCHAR(64), speed INT(4) UNSIGNED, signature VARCHAR(64), flags VARCHAR(255), qty INT(2) UNSIGNED, FOREIGN KEY (uuid) REFERENCES machine(uuid) ON DELETE CASCADE ) ENGINE=InnoDB; CREATE TABLE probe ( uuid CHAR(36) NOT NULL, probe VARCHAR(16) NOT NULL, data TEXT, PRIMARY KEY (uuid,probe), FOREIGN KEY (uuid) REFERENCES machine(uuid) ON DELETE CASCADE ) ENGINE=InnoDB; __END__ BatchMode yes CheckHostIP no ConnectTimeout 6 StrictHostKeyChecking no PreferredAuthentications publickey IdentityFile ~/.ssh/id_dsa_root IdentityFile ~/.ssh/id_dsa NoHostAuthenticationForLocalhost yes ConnectionAttempts 1 PasswordAuthentication no ForwardAgent yes Host server1.acmecompany.com Port 1033 Host server2.acmecompany.com User admin Host * User root Port 22