1package FusionInventory::Agent::Task::Inventory::MacOS::Storages;
2
3use strict;
4use warnings;
5
6use parent 'FusionInventory::Agent::Task::Inventory::Module';
7
8use Scalar::Util qw/looks_like_number/;
9
10use FusionInventory::Agent::Tools;
11use FusionInventory::Agent::Tools::MacOS;
12
13sub isEnabled {
14    my (%params) = @_;
15    return 0 if $params{no_category}->{storage};
16    return 1;
17}
18
19sub doInventory {
20    my (%params) = @_;
21
22    my $inventory = $params{inventory};
23    my $logger    = $params{logger};
24
25    my $storages = [
26        _getSerialATAStorages(logger => $logger),
27        _getDiscBurningStorages(logger => $logger),
28        _getCardReaderStorages(logger => $logger),
29        _getUSBStorages(logger => $logger),
30        _getFireWireStorages(logger => $logger)
31    ];
32    foreach my $storage (@$storages) {
33        $inventory->addEntry(
34            section => 'STORAGES',
35            entry   => $storage
36        );
37    }
38}
39
40sub _getStorages {
41    my (%params) = @_;
42
43    my $infos = getSystemProfilerInfos(
44        type   => 'SPStorageDataType',
45        logger => $params{logger},
46        file   => $params{file}
47    );
48
49    # system profiler data structure:
50    # bus
51    # └── controller
52    #     ├── device
53    #     │   ├── subdevice
54    #     │   │   └── key:value
55    #     │   └── key:value
56    #     └── key:value
57
58    my @storages;
59    my @busNames = ('ATA', 'SERIAL-ATA', 'USB', 'FireWire', 'Fibre Channel');
60    foreach my $busName (@busNames) {
61        my $bus = $infos->{$busName};
62        next unless $bus;
63        foreach my $controllerName (keys %{$bus}) {
64            my $controller = $bus->{$controllerName};
65            foreach my $deviceName (keys %{$controller}) {
66                my $device = $controller->{$deviceName};
67                next unless ref $device eq 'HASH';
68                if (_isStorage($device)) {
69                    push @storages,
70                        _getStorage($device, $deviceName, $busName);
71                } else {
72                    foreach my $subdeviceName (keys %{$device}) {
73                        my $subdevice = $device->{$subdeviceName};
74                        next unless ref $subdevice eq 'HASH';
75                        push @storages,
76                            _getStorage($subdevice, $subdeviceName, $busName)
77                            if _isStorage($subdevice);
78                    }
79                }
80
81            }
82        }
83    }
84
85    return @storages;
86}
87
88sub _getSerialATAStorages {
89    my (%params) = @_;
90
91    my $infos = getSystemProfilerInfos(
92        type   => 'SPSerialATADataType',
93        format => 'xml',
94        logger => $params{logger},
95        file   => $params{file}
96    );
97    return unless $infos->{storages};
98    my @storages = ();
99    for my $hash (values %{$infos->{storages}}) {
100        next if $hash->{_name} =~ /controller/i;
101        my $storage = _extractStorage($hash);
102        $storage->{TYPE} = 'Disk drive';
103        $storage->{INTERFACE} = 'SERIAL-ATA';
104        push @storages, _sanitizedHash($storage);
105    }
106
107    return @storages;
108}
109
110sub _extractStorage {
111    my ($hash) = @_;
112
113    my $storage = {
114        NAME         => $hash->{bsd_name} || $hash->{_name},
115        MANUFACTURER => getCanonicalManufacturer($hash->{_name}),
116#        TYPE         => $bus_name eq 'FireWire' ? '1394' : $bus_name,
117        SERIAL       => $hash->{device_serial},
118        MODEL        => $hash->{device_model} || $hash->{_name},
119        FIRMWARE     => $hash->{device_revision},
120        DISKSIZE     => _extractDiskSize($hash),
121        DESCRIPTION  => $hash->{_name}
122    };
123
124    if ($storage->{MODEL}) {
125        $storage->{MODEL} =~ s/\s*$storage->{MANUFACTURER}\s*//i;
126    }
127
128    return $storage;
129}
130
131sub _getDiscBurningStorages {
132    my (%params) = @_;
133
134    my @storages = ();
135    my $infos = getSystemProfilerInfos(
136        type   => 'SPDiscBurningDataType',
137        format => 'xml',
138        logger => $params{logger},
139        file   => $params{file}
140    );
141    return @storages unless $infos->{storages};
142
143    for my $hash (values %{$infos->{storages}}) {
144        my $storage = _extractDiscBurning($hash);
145        $storage->{TYPE} = 'Disk burning';
146        push @storages, _sanitizedHash($storage);
147    }
148
149    return @storages;
150}
151
152sub _extractDiscBurning {
153    my ($hash) = @_;
154
155    my $storage = {
156        NAME         => $hash->{bsd_name} || $hash->{_name},
157        MANUFACTURER => $hash->{manufacturer} ? getCanonicalManufacturer($hash->{manufacturer}) : getCanonicalManufacturer($hash->{_name}),
158        INTERFACE    => $hash->{interconnect},
159        MODEL        => $hash->{_name},
160        FIRMWARE     => $hash->{firmware}
161    };
162
163    if ($storage->{MODEL}) {
164        $storage->{MODEL} =~ s/\s*$storage->{MANUFACTURER}\s*//i;
165    }
166
167    return $storage;
168}
169
170sub _getCardReaderStorages {
171    my (%params) = @_;
172
173    my $infos = getSystemProfilerInfos(
174        type   => 'SPCardReaderDataType',
175        format => 'xml',
176        logger => $params{logger},
177        file   => $params{file}
178    );
179    return unless $infos->{storages};
180
181    my @storages = ();
182    for my $hash (values %{$infos->{storages}}) {
183        my $storage;
184        if ($hash->{_name} eq 'spcardreader') {
185            $storage = _extractCardReader($hash);
186            $storage->{TYPE} = 'Card reader';
187        } else {
188            $storage = _extractSdCard($hash);
189            $storage->{TYPE} = 'SD Card';
190        }
191        push @storages, _sanitizedHash($storage);
192    }
193
194    return @storages;
195}
196
197sub _extractCardReader {
198    my ($hash) = @_;
199
200    my $storage = {
201        NAME         => $hash->{bsd_name} || $hash->{_name},
202        DESCRIPTION  => $hash->{_name},
203        SERIAL       => $hash->{spcardreader_serialnumber},
204        MODEL        => $hash->{_name},
205        FIRMWARE     => $hash->{'spcardreader_revision-id'},
206        MANUFACTURER => $hash->{'spcardreader_vendor-id'}
207    };
208
209    return $storage;
210}
211
212sub _extractSdCard {
213    my ($hash) = @_;
214
215    my $storage = {
216        NAME         => $hash->{bsd_name} || $hash->{_name},
217        DESCRIPTION  => $hash->{_name},
218        DISKSIZE     => _extractDiskSize($hash)
219    };
220
221    return $storage;
222}
223
224sub _isStorage {
225    my ($device) = @_;
226
227    return
228        ($device->{'BSD Name'} && $device->{'BSD Name'} =~ /^disk\d+$/) ||
229        ($device->{'Protocol'} && $device->{'Socket Type'});
230}
231
232sub _getStorage {
233    my ($device, $device_name, $bus_name) = @_;
234
235    my $storage = {
236        NAME         => $device_name,
237        MANUFACTURER => getCanonicalManufacturer($device_name),
238        TYPE         => $bus_name eq 'FireWire' ? '1394' : $bus_name,
239        SERIAL       => $device->{'Serial Number'},
240        FIRMWARE     => $device->{'Revision'},
241        MODEL        => $device->{'Model'},
242        DISKSIZE     => $device->{'Capacity'}
243    };
244
245    if (!$device->{'Protocol'}) {
246        $storage->{DESCRIPTION} = 'Disk drive';
247    } elsif ($device->{'Protocol'} eq 'ATAPI' || $device->{'Drive Type'}) {
248        $storage->{DESCRIPTION} = 'CD-ROM Drive';
249    }
250
251    if ($storage->{DISKSIZE}) {
252        #e.g: Capacity: 320,07 GB (320 072 933 376 bytes)
253        $storage->{DISKSIZE} =~ s/\s*\(.*//;
254        $storage->{DISKSIZE} =~ s/,/./;
255
256        if ($storage->{DISKSIZE} =~ s/\s*TB//) {
257            $storage->{DISKSIZE} = int($storage->{DISKSIZE} * 1000 * 1000);
258        } elsif ($storage->{DISKSIZE} =~ s/\s+GB$//) {
259            $storage->{DISKSIZE} = int($storage->{DISKSIZE} * 1000 * 1000);
260        }
261    }
262
263    if ($storage->{MODEL}) {
264        $storage->{MODEL} =~ s/\s*$storage->{MANUFACTURER}\s*//i;
265    }
266
267    return $storage;
268}
269
270sub _getUSBStorages {
271    my (%params) = @_;
272
273    my $infos = getSystemProfilerInfos(
274        type   => 'SPUSBDataType',
275        format => 'xml',
276        logger => $params{logger},
277        file   => $params{file}
278    );
279    return unless $infos->{storages};
280
281    my @storages = ();
282    for my $hash (values %{$infos->{storages}}) {
283        unless ($hash->{bsn_name} && $hash->{bsd_name} =~ /^disk/) {
284            next if $hash->{_name} eq 'Mass Storage Device';
285            next if $hash->{_name} =~ /keyboard|controller|IR Receiver|built-in|hub|mouse|usb(?:\d+)?bus/i;
286            next if ($hash->{'Built-in_Device'} && $hash->{'Built-in_Device'} eq 'Yes');
287        }
288        my $storage = _extractUSBStorage($hash);
289        $storage->{TYPE} = 'Disk drive';
290        $storage->{INTERFACE} = 'USB';
291        push @storages, _sanitizedHash($storage);
292    }
293
294    return @storages;
295}
296
297sub _extractUSBStorage {
298    my ($hash) = @_;
299
300    my $storage = {
301        NAME         => $hash->{bsd_name} || $hash->{_name},
302        DESCRIPTION  => $hash->{_name},
303        SERIAL       => _extractValueInHashWithKeyPattern(qr/^(?:\w_)?serial_num$/, $hash),
304        MODEL        => _extractValueInHashWithKeyPattern(qr/^(?:\w_)?device_model/, $hash) || $hash->{_name},
305        FIRMWARE     => _extractValueInHashWithKeyPattern(qr/^(?:\w_)?bcd_device$/, $hash),
306        MANUFACTURER => getCanonicalManufacturer(_extractValueInHashWithKeyPattern(qr/(?:\w+_)?manufacturer/, $hash)) || '',
307        DISKSIZE     => _extractDiskSize($hash)
308    };
309
310    return $storage;
311}
312
313sub _extractDiskSize {
314    my ($hash) = @_;
315
316    return $hash->{size_in_bytes} ?
317        getCanonicalSize($hash->{size_in_bytes} . ' bytes', 1024) :
318            getCanonicalSize($hash->{size}, 1024);
319}
320
321sub _extractValueInHashWithKeyPattern {
322    my ($pattern, $hash) = @_;
323
324    my $value = '';
325    my @keyMatches = grep { $_ =~ $pattern } keys %$hash;
326    if (@keyMatches && (scalar @keyMatches) == 1) {
327        $value = $hash->{$keyMatches[0]};
328    }
329    return $value;
330}
331
332sub _getFireWireStorages {
333    my (%params) = @_;
334
335    my $infos = getSystemProfilerInfos(
336        type   => 'SPFireWireDataType',
337        format => 'xml',
338        logger => $params{logger},
339        file   => $params{file}
340    );
341    return unless $infos->{storages};
342
343    my @storages = ();
344    for my $hash (values %{$infos->{storages}}) {
345        my $storage = _extractFireWireStorage($hash);
346        $storage->{TYPE} = 'Disk drive';
347        $storage->{INTERFACE} = 'FireWire';
348        push @storages, _sanitizedHash($storage);
349    }
350
351    return @storages;
352}
353
354sub _extractFireWireStorage {
355    my ($hash) = @_;
356
357    my $storage = {
358        NAME         => $hash->{bsd_name} || $hash->{_name},
359        DESCRIPTION  => $hash->{_name},
360        SERIAL       => _extractValueInHashWithKeyPattern(qr/^(?:\w_)?serial_num$/, $hash) || '',
361        MODEL        => _extractValueInHashWithKeyPattern(qr/^(?:\w_)?product_id$/, $hash) || '',
362        FIRMWARE     => _extractValueInHashWithKeyPattern(qr/^(?:\w_)?bcd_device$/, $hash) || '',
363        MANUFACTURER => getCanonicalManufacturer(_extractValueInHashWithKeyPattern(qr/(?:\w+_)?manufacturer/, $hash)) || '',
364        DISKSIZE     => _extractDiskSize($hash) || ''
365    };
366
367    return $storage;
368}
369
370sub _sanitizedHash {
371    my ($hash) = @_;
372    foreach my $key (keys(%{$hash})) {
373        if (defined($hash->{$key})) {
374            $hash->{$key} = trimWhitespace($hash->{$key});
375        } else {
376            delete $hash->{$key};
377        }
378    }
379    return $hash;
380}
381
3821;
383