1package FusionInventory::Agent::Inventory; 2 3use strict; 4use warnings; 5 6use Config; 7use Data::Dumper; 8use Digest::MD5 qw(md5_base64); 9use English qw(-no_match_vars); 10use UNIVERSAL::require; 11use XML::TreePP; 12 13use FusionInventory::Agent::Logger; 14use FusionInventory::Agent::Tools; 15use FusionInventory::Agent::Version; 16 17# Always sort keys in Dumper while computing checksum on HASH 18$Data::Dumper::Sortkeys = 1; 19 20my %fields = ( 21 BIOS => [ qw/SMODEL SMANUFACTURER SSN BDATE BVERSION 22 BMANUFACTURER MMANUFACTURER MSN MMODEL ASSETTAG 23 ENCLOSURESERIAL BIOSSERIAL 24 TYPE SKUNUMBER/ ], 25 HARDWARE => [ qw/USERID OSVERSION PROCESSORN OSCOMMENTS CHECKSUM 26 PROCESSORT NAME PROCESSORS SWAP ETIME TYPE OSNAME 27 IPADDR WORKGROUP DESCRIPTION MEMORY UUID DNS 28 LASTLOGGEDUSER USERDOMAIN DATELASTLOGGEDUSER 29 DEFAULTGATEWAY VMSYSTEM WINOWNER WINPRODID 30 WINPRODKEY WINCOMPANY WINLANG CHASSIS_TYPE 31 VMNAME VMHOSTSERIAL/ ], 32 OPERATINGSYSTEM => [ qw/KERNEL_NAME KERNEL_VERSION NAME VERSION FULL_NAME 33 SERVICE_PACK INSTALL_DATE FQDN DNS_DOMAIN HOSTID 34 SSH_KEY ARCH BOOT_TIME TIMEZONE/ ], 35 ACCESSLOG => [ qw/USERID LOGDATE/ ], 36 37 ANTIVIRUS => [ qw/COMPANY ENABLED GUID NAME UPTODATE VERSION 38 EXPIRATION BASE_CREATION BASE_VERSION/ ], 39 BATTERIES => [ qw/CAPACITY CHEMISTRY DATE NAME SERIAL MANUFACTURER 40 VOLTAGE/ ], 41 CONTROLLERS => [ qw/CAPTION DRIVER NAME MANUFACTURER PCICLASS VENDORID 42 PRODUCTID PCISUBSYSTEMID PCISLOT TYPE REV/ ], 43 CPUS => [ qw/CACHE CORE DESCRIPTION MANUFACTURER NAME THREAD 44 SERIAL STEPPING FAMILYNAME FAMILYNUMBER MODEL 45 SPEED ID EXTERNAL_CLOCK ARCH CORECOUNT/ ], 46 DRIVES => [ qw/CREATEDATE DESCRIPTION FREE FILESYSTEM LABEL 47 LETTER SERIAL SYSTEMDRIVE TOTAL TYPE VOLUMN 48 ENCRYPT_NAME ENCRYPT_ALGO ENCRYPT_STATUS ENCRYPT_TYPE/ ], 49 ENVS => [ qw/KEY VAL/ ], 50 INPUTS => [ qw/NAME MANUFACTURER CAPTION DESCRIPTION INTERFACE 51 LAYOUT POINTINGTYPE TYPE/ ], 52 FIREWALL => [ qw/PROFILE STATUS DESCRIPTION IPADDRESS IPADDRESS6/ ], 53 LICENSEINFOS => [ qw/NAME FULLNAME KEY COMPONENTS TRIAL UPDATE OEM 54 ACTIVATION_DATE PRODUCTID/ ], 55 LOCAL_GROUPS => [ qw/ID MEMBER NAME/ ], 56 LOCAL_USERS => [ qw/HOME ID LOGIN NAME SHELL/ ], 57 LOGICAL_VOLUMES => [ qw/LV_NAME VG_NAME ATTR SIZE LV_UUID SEG_COUNT 58 VG_UUID/ ], 59 MEMORIES => [ qw/CAPACITY CAPTION FORMFACTOR REMOVABLE PURPOSE 60 SPEED SERIALNUMBER TYPE DESCRIPTION NUMSLOTS 61 MEMORYCORRECTION MANUFACTURER/ ], 62 MODEMS => [ qw/DESCRIPTION NAME TYPE MODEL/ ], 63 MONITORS => [ qw/BASE64 CAPTION DESCRIPTION MANUFACTURER SERIAL 64 UUENCODE NAME TYPE ALTSERIAL PORT/ ], 65 NETWORKS => [ qw/DESCRIPTION MANUFACTURER MODEL MANAGEMENT TYPE 66 VIRTUALDEV MACADDR WWN DRIVER FIRMWARE PCIID 67 PCISLOT PNPDEVICEID MTU SPEED STATUS SLAVES BASE 68 IPADDRESS IPSUBNET IPMASK IPDHCP IPGATEWAY 69 IPADDRESS6 IPSUBNET6 IPMASK6 WIFI_BSSID WIFI_SSID 70 WIFI_MODE WIFI_VERSION/ ], 71 PHYSICAL_VOLUMES => [ qw/DEVICE PV_PE_COUNT PV_UUID FORMAT ATTR 72 SIZE FREE PE_SIZE VG_UUID/ ], 73 PORTS => [ qw/CAPTION DESCRIPTION NAME TYPE/ ], 74 POWERSUPPLIES => [ qw/PARTNUM SERIALNUMBER MANUFACTURER POWER_MAX NAME 75 HOTREPLACEABLE PLUGGED STATUS LOCATION MODEL/ ], 76 PRINTERS => [ qw/COMMENT DESCRIPTION DRIVER NAME NETWORK PORT 77 RESOLUTION SHARED STATUS ERRSTATUS SERVERNAME 78 SHARENAME PRINTPROCESSOR SERIAL/ ], 79 PROCESSES => [ qw/USER PID CPUUSAGE MEM VIRTUALMEMORY TTY STARTED 80 CMD/ ], 81 REGISTRY => [ qw/NAME REGVALUE HIVE/ ], 82 REMOTE_MGMT => [ qw/ID TYPE/ ], 83 RUDDER => [ qw/AGENT UUID HOSTNAME SERVER_ROLES AGENT_CAPABILITIES/ ], 84 SLOTS => [ qw/DESCRIPTION DESIGNATION NAME STATUS/ ], 85 SOFTWARES => [ qw/COMMENTS FILESIZE FOLDER FROM HELPLINK INSTALLDATE 86 NAME NO_REMOVE RELEASE_TYPE PUBLISHER 87 UNINSTALL_STRING URL_INFO_ABOUT VERSION 88 VERSION_MINOR VERSION_MAJOR GUID ARCH USERNAME 89 USERID SYSTEM_CATEGORY/ ], 90 SOUNDS => [ qw/CAPTION DESCRIPTION MANUFACTURER NAME/ ], 91 STORAGES => [ qw/DESCRIPTION DISKSIZE INTERFACE MANUFACTURER MODEL 92 NAME TYPE SERIAL SERIALNUMBER FIRMWARE SCSI_COID 93 SCSI_CHID SCSI_UNID SCSI_LUN WWN 94 ENCRYPT_NAME ENCRYPT_ALGO ENCRYPT_STATUS ENCRYPT_TYPE/ ], 95 VIDEOS => [ qw/CHIPSET MEMORY NAME RESOLUTION PCISLOT PCIID/ ], 96 USBDEVICES => [ qw/VENDORID PRODUCTID MANUFACTURER CAPTION SERIAL 97 CLASS SUBCLASS NAME/ ], 98 USERS => [ qw/LOGIN DOMAIN/ ], 99 VIRTUALMACHINES => [ qw/MEMORY NAME UUID STATUS SUBSYSTEM VMTYPE VCPU 100 MAC COMMENT OWNER SERIAL IMAGE/ ], 101 VOLUME_GROUPS => [ qw/VG_NAME PV_COUNT LV_COUNT ATTR SIZE FREE VG_UUID 102 VG_EXTENT_SIZE/ ], 103 VERSIONPROVIDER => [ qw/NAME VERSION COMMENTS PERL_EXE PERL_VERSION PERL_ARGS 104 PROGRAM PERL_CONFIG PERL_INC PERL_MODULE/ ] 105); 106 107my %checks = ( 108 STORAGES => { 109 INTERFACE => qr/^(SCSI|HDC|IDE|USB|1394|Serial-ATA|SAS)$/ 110 }, 111 VIRTUALMACHINES => { 112 STATUS => qr/^(running|blocked|idle|paused|shutdown|crashed|dying|off)$/ 113 }, 114 SLOTS => { 115 STATUS => qr/^(free|used)$/ 116 }, 117 NETWORKS => { 118 TYPE => qr/^(ethernet|wifi|infiniband|aggregate|alias|dialup|loopback|bridge|fibrechannel)$/ 119 }, 120 CPUS => { 121 ARCH => qr/^(MIPS|MIPS64|Alpha|SPARC|SPARC64|m68k|i386|x86_64|PowerPC|PowerPC64|ARM|AArch64)$/ 122 } 123); 124 125# convert fields list into fields hashes, for fast lookup 126foreach my $section (keys %fields) { 127 $fields{$section} = { map { $_ => 1 } @{$fields{$section}} }; 128} 129 130sub new { 131 my ($class, %params) = @_; 132 133 my $self = { 134 deviceid => $params{deviceid}, 135 logger => $params{logger} || FusionInventory::Agent::Logger->new(), 136 fields => \%fields, 137 content => { 138 HARDWARE => { 139 VMSYSTEM => "Physical" # Default value 140 }, 141 VERSIONCLIENT => $FusionInventory::Agent::AGENT_STRING || 142 $FusionInventory::Agent::Version::PROVIDER."-Inventory_v".$FusionInventory::Agent::Version::VERSION 143 } 144 }; 145 bless $self, $class; 146 147 $self->setTag($params{tag}); 148 $self->{last_state_file} = $params{statedir} . '/last_state' 149 if $params{statedir}; 150 151 return $self; 152} 153 154sub getRemote { 155 my ($self) = @_; 156 157 return $self->{_remote} || ''; 158} 159 160sub setRemote { 161 my ($self, $task) = @_; 162 163 $self->{_remote} = $task || ''; 164 165 return $self->{_remote}; 166} 167 168sub getDeviceId { 169 my ($self) = @_; 170 171 return $self->{deviceid} if $self->{deviceid}; 172 173 # compute an unique agent identifier based on current time and inventory 174 # hostnale or provider name 175 my $hostname = $self->getHardware('NAME'); 176 if ($hostname) { 177 my $workgroup = $self->getHardware('WORKGROUP'); 178 $hostname .= "." . $workgroup if $workgroup; 179 } else { 180 FusionInventory::Agent::Tools::Hostname->require(); 181 182 eval { 183 $hostname = FusionInventory::Agent::Tools::Hostname::getHostname(); 184 }; 185 } 186 187 # Fake hostname if no default found 188 $hostname = 'device-by-' . lc($FusionInventory::Agent::Version::PROVIDER) . '-agent' 189 unless $hostname; 190 191 my ($year, $month , $day, $hour, $min, $sec) = 192 (localtime (time))[5, 4, 3, 2, 1, 0]; 193 194 return $self->{deviceid} = sprintf "%s-%02d-%02d-%02d-%02d-%02d-%02d", 195 $hostname, $year + 1900, $month + 1, $day, $hour, $min, $sec; 196} 197 198sub getFields { 199 my ($self) = @_; 200 201 return $self->{fields}; 202} 203 204sub getContent { 205 my ($self) = @_; 206 207 return $self->{content}; 208} 209 210sub getSection { 211 my ($self, $section) = @_; 212 ## no critic (ExplicitReturnUndef) 213 my $content = $self->getContent() or return undef; 214 return exists($content->{$section}) ? $content->{$section} : undef ; 215} 216 217sub getField { 218 my ($self, $section, $field) = @_; 219 ## no critic (ExplicitReturnUndef) 220 $section = $self->getSection($section) or return undef; 221 return exists($section->{$field}) ? $section->{$field} : undef ; 222} 223 224sub mergeContent { 225 my ($self, $content) = @_; 226 227 die "no content" unless $content; 228 229 foreach my $section (keys %$content) { 230 if (ref $content->{$section} eq 'ARRAY') { 231 # a list of entry 232 foreach my $entry (@{$content->{$section}}) { 233 $self->addEntry(section => $section, entry => $entry); 234 } 235 } else { 236 # single entry 237 SWITCH: { 238 if ($section eq 'HARDWARE') { 239 $self->setHardware($content->{$section}); 240 last SWITCH; 241 } 242 if ($section eq 'OPERATINGSYSTEM') { 243 $self->setOperatingSystem($content->{$section}); 244 last SWITCH; 245 } 246 if ($section eq 'BIOS') { 247 $self->setBios($content->{$section}); 248 last SWITCH; 249 } 250 if ($section eq 'ACCESSLOG') { 251 $self->setAccessLog($content->{$section}); 252 last SWITCH; 253 } 254 $self->addEntry( 255 section => $section, entry => $content->{$section} 256 ); 257 } 258 } 259 } 260} 261 262sub addEntry { 263 my ($self, %params) = @_; 264 265 my $entry = $params{entry}; 266 die "no entry" unless $entry; 267 268 my $section = $params{section}; 269 my $fields = $fields{$section}; 270 my $checks = $checks{$section}; 271 die "unknown section $section" unless $fields; 272 273 foreach my $field (keys %$entry) { 274 if (!$fields->{$field}) { 275 # unvalid field, log error and remove 276 $self->{logger}->debug("unknown field $field for section $section"); 277 delete $entry->{$field}; 278 next; 279 } 280 if (!defined $entry->{$field}) { 281 # undefined value, remove 282 delete $entry->{$field}; 283 next; 284 } 285 # sanitize value 286 my $value = getSanitizedString($entry->{$field}); 287 # check value if appliable 288 if ($checks->{$field}) { 289 $self->{logger}->debug( 290 "invalid value $value for field $field for section $section" 291 ) unless $value =~ $checks->{$field}; 292 } 293 $entry->{$field} = $value; 294 } 295 296 # avoid duplicate entries 297 if ($params{noDuplicated}) { 298 my $md5 = md5_base64(Dumper($entry)); 299 return if $self->{seen}->{$section}->{$md5}; 300 $self->{seen}->{$section}->{$md5} = 1; 301 } 302 303 if ($section eq 'STORAGES') { 304 $entry->{SERIALNUMBER} = $entry->{SERIAL} if !$entry->{SERIALNUMBER} 305 } 306 307 push @{$self->{content}{$section}}, $entry; 308} 309 310sub computeLegacyValues { 311 my ($self) = @_; 312 313 # CPU-related values 314 my $cpus = $self->{content}->{CPUS}; 315 if ($cpus) { 316 my $cpu = $cpus->[0]; 317 318 $self->setHardware({ 319 PROCESSORN => scalar @$cpus, 320 PROCESSORS => $cpu->{SPEED}, 321 PROCESSORT => $cpu->{NAME}, 322 }); 323 } 324 325 # network related values 326 my $interfaces = $self->{content}->{NETWORKS}; 327 if ($interfaces) { 328 my @ip_addresses = 329 grep { ! /^127/ } 330 grep { $_ } 331 map { $_->{IPADDRESS} } 332 @$interfaces; 333 334 $self->setHardware({ 335 IPADDR => join('/', @ip_addresses), 336 }); 337 } 338 339 # user-related values 340 my $users = $self->{content}->{USERS}; 341 if ($users) { 342 my $user = $users->[-1]; 343 344 my ($domain, $id); 345 if ($user->{LOGIN} =~ /(\S+)\\(\S+)/) { 346 # Windows fully qualified username: domain\user 347 $domain = $1; 348 $id = $2; 349 } else { 350 # simple username: user 351 $id = $user->{LOGIN}; 352 } 353 354 $self->setHardware({ 355 USERID => $id, 356 USERDOMAIN => $domain, 357 }); 358 } 359} 360 361sub getHardware { 362 my ($self, $field) = @_; 363 return $self->getField('HARDWARE', $field); 364} 365 366sub setHardware { 367 my ($self, $args) = @_; 368 369 foreach my $field (keys %$args) { 370 if (!$fields{HARDWARE}->{$field}) { 371 $self->{logger}->debug("unknown field $field for section HARDWARE"); 372 next 373 } 374 375 # Do not overwrite existing value with undef 376 next unless $args->{$field}; 377 378 $self->{content}->{HARDWARE}->{$field} = 379 getSanitizedString($args->{$field}); 380 } 381} 382 383sub setOperatingSystem { 384 my ($self, $args) = @_; 385 386 foreach my $field (keys %$args) { 387 if (!$fields{OPERATINGSYSTEM}->{$field}) { 388 $self->{logger}->debug( 389 "unknown field $field for section OPERATINGSYSTEM" 390 ); 391 next 392 } 393 $self->{content}->{OPERATINGSYSTEM}->{$field} = 394 getSanitizedString($args->{$field}); 395 } 396} 397 398sub getBios { 399 my ($self, $field) = @_; 400 return $self->getField('BIOS', $field); 401} 402 403sub setBios { 404 my ($self, $args) = @_; 405 406 foreach my $field (keys %$args) { 407 if (!$fields{BIOS}->{$field}) { 408 $self->{logger}->debug("unknown field $field for section BIOS"); 409 next 410 } 411 412 $self->{content}->{BIOS}->{$field} = 413 getSanitizedString($args->{$field}); 414 } 415} 416 417sub setAccessLog { 418 my ($self, $args) = @_; 419 420 foreach my $field (keys %$args) { 421 if (!$fields{ACCESSLOG}->{$field}) { 422 $self->{logger}->debug( 423 "unknown field $field for section ACCESSLOG" 424 ); 425 next 426 } 427 428 $self->{content}->{ACCESSLOG}->{$field} = 429 getSanitizedString($args->{$field}); 430 } 431} 432 433sub setTag { 434 my ($self, $tag) = @_; 435 436 return unless $tag; 437 438 $self->{content}{ACCOUNTINFO} = [{ 439 KEYNAME => "TAG", 440 KEYVALUE => $tag 441 }]; 442 443} 444 445sub computeChecksum { 446 my ($self) = @_; 447 448 my $logger = $self->{logger}; 449 450 # to apply to $checksum with an OR 451 my %mask = ( 452 HARDWARE => 1, 453 BIOS => 2, 454 MEMORIES => 4, 455 SLOTS => 8, 456 REGISTRY => 16, 457 CONTROLLERS => 32, 458 MONITORS => 64, 459 PORTS => 128, 460 STORAGES => 256, 461 DRIVES => 512, 462 INPUT => 1024, 463 MODEMS => 2048, 464 NETWORKS => 4096, 465 PRINTERS => 8192, 466 SOUNDS => 16384, 467 VIDEOS => 32768, 468 SOFTWARES => 65536, 469 ); 470 # TODO CPUS is not in the list 471 472 if ($self->{last_state_file}) { 473 if (-f $self->{last_state_file}) { 474 eval { 475 $self->{last_state_content} = XML::TreePP->new()->parsefile( 476 $self->{last_state_file} 477 ); 478 }; 479 if (ref($self->{last_state_content}) ne 'HASH') { 480 $self->{last_state_content} = {}; 481 } 482 } else { 483 $logger->debug( 484 "last state file '$self->{last_state_file}' doesn't exist" 485 ); 486 } 487 } 488 489 my $checksum = 0; 490 foreach my $section (keys %mask) { 491 my $hash = 492 md5_base64(Dumper($self->{content}->{$section})); 493 494 # check if the section did change since the last run 495 next if 496 $self->{last_state_content}->{$section} && 497 $self->{last_state_content}->{$section} eq $hash; 498 499 $logger->debug("Section $section has changed since last inventory"); 500 501 # add the mask of the current section to the checksum 502 $checksum |= $mask{$section}; ## no critic (ProhibitBitwise) 503 504 # store the new value. 505 $self->{last_state_content}->{$section} = $hash; 506 } 507 508 509 $self->setHardware({CHECKSUM => $checksum}); 510} 511 512sub saveLastState { 513 my ($self) = @_; 514 515 my $logger = $self->{logger}; 516 517 if (!defined($self->{last_state_content})) { 518 $self->computeChecksum(); 519 } 520 if ($self->{last_state_file}) { 521 eval { 522 XML::TreePP->new()->writefile( 523 $self->{last_state_file}, $self->{last_state_content} 524 ); 525 } 526 } else { 527 $logger->debug( 528 "last state file is not defined, last state not saved" 529 ); 530 } 531 532 my $tpp = XML::TreePP->new(); 533} 534 5351; 536__END__ 537 538=head1 NAME 539 540FusionInventory::Agent::Inventory - Inventory data structure 541 542=head1 DESCRIPTION 543 544This is a data structure corresponding to an hardware and software inventory. 545 546=head1 METHODS 547 548=head2 new(%params) 549 550The constructor. The following parameters are allowed, as keys of the 551%params hash: 552 553=over 554 555=item I<logger> 556 557a logger object 558 559=item I<statedir> 560 561a path to a writable directory containing the last serialized inventory 562 563=item I<tag> 564 565an arbitrary label, used for server-side filtering 566 567=back 568 569=head2 getContent() 570 571Get content attribute. 572 573=head2 getSection($section) 574 575Get full machine inventory section. 576 577=head2 getField($section,$field) 578 579Get a field from a full machine inventory section. 580 581=head2 mergeContent($content) 582 583Merge content to the inventory. 584 585=head2 addEntry(%params) 586 587Add a new entry to the inventory. The following parameters are allowed, as keys 588of the %params hash: 589 590=over 591 592=item I<section> 593 594the entry section (mandatory) 595 596=item I<entry> 597 598the entry (mandatory) 599 600=item I<noDuplicated> 601 602ignore entry if already present 603 604=back 605 606=head2 setTag($tag) 607 608Set inventory tag, an arbitrary label used for filtering on server side. 609 610=head2 getHardware($field) 611 612Get machine global information from known machine inventory. 613 614=head2 setHardware() 615 616Save global information regarding the machine. 617 618=head2 setOperatingSystem() 619 620Operating System information. 621 622=head2 getBios($field) 623 624Get BIOS information from known inventory. 625 626=head2 setBios() 627 628Set BIOS information. 629 630=head2 setAccessLog() 631 632What is that for? :) 633 634=head2 computeChecksum() 635 636Compute the inventory checksum. This information is used by the server to 637know which parts of the inventory have changed since the last one. 638 639=head2 computeLegacyValues() 640 641Compute the inventory global values, meaning values in hardware section such as 642CPU number, speed and model, computed from other values, but needed for OCS 643compatibility. 644 645=head2 saveLastState() 646 647At the end of the process IF the inventory was saved 648correctly, the last_state is saved. 649 650=head2 getRemote() 651 652Method to get the parent task remote status. 653 654Returns the string set by setRemote() API or an empty string. 655 656=head2 setRemote([$task]) 657 658Method to set or reset the parent task remote status. 659 660Without $task parameter, the API resets the parent remote status to an empty string. 661