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