1# This file is part of Ansible
2#
3# Ansible is free software: you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation, either version 3 of the License, or
6# (at your option) any later version.
7#
8# Ansible is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
15
16from __future__ import (absolute_import, division, print_function)
17__metaclass__ = type
18
19import collections
20import errno
21import glob
22import json
23import os
24import re
25import sys
26import time
27
28from multiprocessing import cpu_count
29from multiprocessing.pool import ThreadPool
30
31from ansible.module_utils._text import to_text
32from ansible.module_utils.six import iteritems
33from ansible.module_utils.common.process import get_bin_path
34from ansible.module_utils.common.text.formatters import bytes_to_human
35from ansible.module_utils.facts.hardware.base import Hardware, HardwareCollector
36from ansible.module_utils.facts.utils import get_file_content, get_file_lines, get_mount_size
37
38# import this as a module to ensure we get the same module instance
39from ansible.module_utils.facts import timeout
40
41
42def get_partition_uuid(partname):
43    try:
44        uuids = os.listdir("/dev/disk/by-uuid")
45    except OSError:
46        return
47
48    for uuid in uuids:
49        dev = os.path.realpath("/dev/disk/by-uuid/" + uuid)
50        if dev == ("/dev/" + partname):
51            return uuid
52
53    return None
54
55
56class LinuxHardware(Hardware):
57    """
58    Linux-specific subclass of Hardware.  Defines memory and CPU facts:
59    - memfree_mb
60    - memtotal_mb
61    - swapfree_mb
62    - swaptotal_mb
63    - processor (a list)
64    - processor_cores
65    - processor_count
66
67    In addition, it also defines number of DMI facts and device facts.
68    """
69
70    platform = 'Linux'
71
72    # Originally only had these four as toplevelfacts
73    ORIGINAL_MEMORY_FACTS = frozenset(('MemTotal', 'SwapTotal', 'MemFree', 'SwapFree'))
74    # Now we have all of these in a dict structure
75    MEMORY_FACTS = ORIGINAL_MEMORY_FACTS.union(('Buffers', 'Cached', 'SwapCached'))
76
77    # regex used against findmnt output to detect bind mounts
78    BIND_MOUNT_RE = re.compile(r'.*\]')
79
80    # regex used against mtab content to find entries that are bind mounts
81    MTAB_BIND_MOUNT_RE = re.compile(r'.*bind.*"')
82
83    # regex used for replacing octal escape sequences
84    OCTAL_ESCAPE_RE = re.compile(r'\\[0-9]{3}')
85
86    def populate(self, collected_facts=None):
87        hardware_facts = {}
88        self.module.run_command_environ_update = {'LANG': 'C', 'LC_ALL': 'C', 'LC_NUMERIC': 'C'}
89
90        cpu_facts = self.get_cpu_facts(collected_facts=collected_facts)
91        memory_facts = self.get_memory_facts()
92        dmi_facts = self.get_dmi_facts()
93        device_facts = self.get_device_facts()
94        uptime_facts = self.get_uptime_facts()
95        lvm_facts = self.get_lvm_facts()
96
97        mount_facts = {}
98        try:
99            mount_facts = self.get_mount_facts()
100        except timeout.TimeoutError:
101            pass
102
103        hardware_facts.update(cpu_facts)
104        hardware_facts.update(memory_facts)
105        hardware_facts.update(dmi_facts)
106        hardware_facts.update(device_facts)
107        hardware_facts.update(uptime_facts)
108        hardware_facts.update(lvm_facts)
109        hardware_facts.update(mount_facts)
110
111        return hardware_facts
112
113    def get_memory_facts(self):
114        memory_facts = {}
115        if not os.access("/proc/meminfo", os.R_OK):
116            return memory_facts
117
118        memstats = {}
119        for line in get_file_lines("/proc/meminfo"):
120            data = line.split(":", 1)
121            key = data[0]
122            if key in self.ORIGINAL_MEMORY_FACTS:
123                val = data[1].strip().split(' ')[0]
124                memory_facts["%s_mb" % key.lower()] = int(val) // 1024
125
126            if key in self.MEMORY_FACTS:
127                val = data[1].strip().split(' ')[0]
128                memstats[key.lower()] = int(val) // 1024
129
130        if None not in (memstats.get('memtotal'), memstats.get('memfree')):
131            memstats['real:used'] = memstats['memtotal'] - memstats['memfree']
132        if None not in (memstats.get('cached'), memstats.get('memfree'), memstats.get('buffers')):
133            memstats['nocache:free'] = memstats['cached'] + memstats['memfree'] + memstats['buffers']
134        if None not in (memstats.get('memtotal'), memstats.get('nocache:free')):
135            memstats['nocache:used'] = memstats['memtotal'] - memstats['nocache:free']
136        if None not in (memstats.get('swaptotal'), memstats.get('swapfree')):
137            memstats['swap:used'] = memstats['swaptotal'] - memstats['swapfree']
138
139        memory_facts['memory_mb'] = {
140            'real': {
141                'total': memstats.get('memtotal'),
142                'used': memstats.get('real:used'),
143                'free': memstats.get('memfree'),
144            },
145            'nocache': {
146                'free': memstats.get('nocache:free'),
147                'used': memstats.get('nocache:used'),
148            },
149            'swap': {
150                'total': memstats.get('swaptotal'),
151                'free': memstats.get('swapfree'),
152                'used': memstats.get('swap:used'),
153                'cached': memstats.get('swapcached'),
154            },
155        }
156
157        return memory_facts
158
159    def get_cpu_facts(self, collected_facts=None):
160        cpu_facts = {}
161        collected_facts = collected_facts or {}
162
163        i = 0
164        vendor_id_occurrence = 0
165        model_name_occurrence = 0
166        processor_occurence = 0
167        physid = 0
168        coreid = 0
169        sockets = {}
170        cores = {}
171
172        xen = False
173        xen_paravirt = False
174        try:
175            if os.path.exists('/proc/xen'):
176                xen = True
177            else:
178                for line in get_file_lines('/sys/hypervisor/type'):
179                    if line.strip() == 'xen':
180                        xen = True
181                    # Only interested in the first line
182                    break
183        except IOError:
184            pass
185
186        if not os.access("/proc/cpuinfo", os.R_OK):
187            return cpu_facts
188
189        cpu_facts['processor'] = []
190        for line in get_file_lines('/proc/cpuinfo'):
191            data = line.split(":", 1)
192            key = data[0].strip()
193
194            try:
195                val = data[1].strip()
196            except IndexError:
197                val = ""
198
199            if xen:
200                if key == 'flags':
201                    # Check for vme cpu flag, Xen paravirt does not expose this.
202                    #   Need to detect Xen paravirt because it exposes cpuinfo
203                    #   differently than Xen HVM or KVM and causes reporting of
204                    #   only a single cpu core.
205                    if 'vme' not in val:
206                        xen_paravirt = True
207
208            # model name is for Intel arch, Processor (mind the uppercase P)
209            # works for some ARM devices, like the Sheevaplug.
210            # 'ncpus active' is SPARC attribute
211            if key in ['model name', 'Processor', 'vendor_id', 'cpu', 'Vendor', 'processor']:
212                if 'processor' not in cpu_facts:
213                    cpu_facts['processor'] = []
214                cpu_facts['processor'].append(val)
215                if key == 'vendor_id':
216                    vendor_id_occurrence += 1
217                if key == 'model name':
218                    model_name_occurrence += 1
219                if key == 'processor':
220                    processor_occurence += 1
221                i += 1
222            elif key == 'physical id':
223                physid = val
224                if physid not in sockets:
225                    sockets[physid] = 1
226            elif key == 'core id':
227                coreid = val
228                if coreid not in sockets:
229                    cores[coreid] = 1
230            elif key == 'cpu cores':
231                sockets[physid] = int(val)
232            elif key == 'siblings':
233                cores[coreid] = int(val)
234            elif key == '# processors':
235                cpu_facts['processor_cores'] = int(val)
236            elif key == 'ncpus active':
237                i = int(val)
238
239        # Skip for platforms without vendor_id/model_name in cpuinfo (e.g ppc64le)
240        if vendor_id_occurrence > 0:
241            if vendor_id_occurrence == model_name_occurrence:
242                i = vendor_id_occurrence
243
244        # The fields for ARM CPUs do not always include 'vendor_id' or 'model name',
245        # and sometimes includes both 'processor' and 'Processor'.
246        # The fields for Power CPUs include 'processor' and 'cpu'.
247        # Always use 'processor' count for ARM and Power systems
248        if collected_facts.get('ansible_architecture', '').startswith(('armv', 'aarch', 'ppc')):
249            i = processor_occurence
250
251        # FIXME
252        if collected_facts.get('ansible_architecture') != 's390x':
253            if xen_paravirt:
254                cpu_facts['processor_count'] = i
255                cpu_facts['processor_cores'] = i
256                cpu_facts['processor_threads_per_core'] = 1
257                cpu_facts['processor_vcpus'] = i
258            else:
259                if sockets:
260                    cpu_facts['processor_count'] = len(sockets)
261                else:
262                    cpu_facts['processor_count'] = i
263
264                socket_values = list(sockets.values())
265                if socket_values and socket_values[0]:
266                    cpu_facts['processor_cores'] = socket_values[0]
267                else:
268                    cpu_facts['processor_cores'] = 1
269
270                core_values = list(cores.values())
271                if core_values:
272                    cpu_facts['processor_threads_per_core'] = core_values[0] // cpu_facts['processor_cores']
273                else:
274                    cpu_facts['processor_threads_per_core'] = 1 // cpu_facts['processor_cores']
275
276                cpu_facts['processor_vcpus'] = (cpu_facts['processor_threads_per_core'] *
277                                                cpu_facts['processor_count'] * cpu_facts['processor_cores'])
278
279                # if the number of processors available to the module's
280                # thread cannot be determined, the processor count
281                # reported by /proc will be the default:
282                cpu_facts['processor_nproc'] = processor_occurence
283
284                try:
285                    cpu_facts['processor_nproc'] = len(
286                        os.sched_getaffinity(0)
287                    )
288                except AttributeError:
289                    # In Python < 3.3, os.sched_getaffinity() is not available
290                    try:
291                        cmd = get_bin_path('nproc')
292                    except ValueError:
293                        pass
294                    else:
295                        rc, out, _err = self.module.run_command(cmd)
296                        if rc == 0:
297                            cpu_facts['processor_nproc'] = int(out)
298
299        return cpu_facts
300
301    def get_dmi_facts(self):
302        ''' learn dmi facts from system
303
304        Try /sys first for dmi related facts.
305        If that is not available, fall back to dmidecode executable '''
306
307        dmi_facts = {}
308
309        if os.path.exists('/sys/devices/virtual/dmi/id/product_name'):
310            # Use kernel DMI info, if available
311
312            # DMI SPEC -- https://www.dmtf.org/sites/default/files/standards/documents/DSP0134_3.2.0.pdf
313            FORM_FACTOR = ["Unknown", "Other", "Unknown", "Desktop",
314                           "Low Profile Desktop", "Pizza Box", "Mini Tower", "Tower",
315                           "Portable", "Laptop", "Notebook", "Hand Held", "Docking Station",
316                           "All In One", "Sub Notebook", "Space-saving", "Lunch Box",
317                           "Main Server Chassis", "Expansion Chassis", "Sub Chassis",
318                           "Bus Expansion Chassis", "Peripheral Chassis", "RAID Chassis",
319                           "Rack Mount Chassis", "Sealed-case PC", "Multi-system",
320                           "CompactPCI", "AdvancedTCA", "Blade", "Blade Enclosure",
321                           "Tablet", "Convertible", "Detachable", "IoT Gateway",
322                           "Embedded PC", "Mini PC", "Stick PC"]
323
324            DMI_DICT = {
325                'bios_date': '/sys/devices/virtual/dmi/id/bios_date',
326                'bios_vendor': '/sys/devices/virtual/dmi/id/bios_vendor',
327                'bios_version': '/sys/devices/virtual/dmi/id/bios_version',
328                'board_asset_tag': '/sys/devices/virtual/dmi/id/board_asset_tag',
329                'board_name': '/sys/devices/virtual/dmi/id/board_name',
330                'board_serial': '/sys/devices/virtual/dmi/id/board_serial',
331                'board_vendor': '/sys/devices/virtual/dmi/id/board_vendor',
332                'board_version': '/sys/devices/virtual/dmi/id/board_version',
333                'chassis_asset_tag': '/sys/devices/virtual/dmi/id/chassis_asset_tag',
334                'chassis_serial': '/sys/devices/virtual/dmi/id/chassis_serial',
335                'chassis_vendor': '/sys/devices/virtual/dmi/id/chassis_vendor',
336                'chassis_version': '/sys/devices/virtual/dmi/id/chassis_version',
337                'form_factor': '/sys/devices/virtual/dmi/id/chassis_type',
338                'product_name': '/sys/devices/virtual/dmi/id/product_name',
339                'product_serial': '/sys/devices/virtual/dmi/id/product_serial',
340                'product_uuid': '/sys/devices/virtual/dmi/id/product_uuid',
341                'product_version': '/sys/devices/virtual/dmi/id/product_version',
342                'system_vendor': '/sys/devices/virtual/dmi/id/sys_vendor',
343            }
344
345            for (key, path) in DMI_DICT.items():
346                data = get_file_content(path)
347                if data is not None:
348                    if key == 'form_factor':
349                        try:
350                            dmi_facts['form_factor'] = FORM_FACTOR[int(data)]
351                        except IndexError:
352                            dmi_facts['form_factor'] = 'unknown (%s)' % data
353                    else:
354                        dmi_facts[key] = data
355                else:
356                    dmi_facts[key] = 'NA'
357
358        else:
359            # Fall back to using dmidecode, if available
360            dmi_bin = self.module.get_bin_path('dmidecode')
361            DMI_DICT = {
362                'bios_date': 'bios-release-date',
363                'bios_vendor': 'bios-vendor',
364                'bios_version': 'bios-version',
365                'board_asset_tag': 'baseboard-asset-tag',
366                'board_name': 'baseboard-product-name',
367                'board_serial': 'baseboard-serial-number',
368                'board_vendor': 'baseboard-manufacturer',
369                'board_version': 'baseboard-version',
370                'chassis_asset_tag': 'chassis-asset-tag',
371                'chassis_serial': 'chassis-serial-number',
372                'chassis_vendor': 'chassis-manufacturer',
373                'chassis_version': 'chassis-version',
374                'form_factor': 'chassis-type',
375                'product_name': 'system-product-name',
376                'product_serial': 'system-serial-number',
377                'product_uuid': 'system-uuid',
378                'product_version': 'system-version',
379                'system_vendor': 'system-manufacturer',
380            }
381            for (k, v) in DMI_DICT.items():
382                if dmi_bin is not None:
383                    (rc, out, err) = self.module.run_command('%s -s %s' % (dmi_bin, v))
384                    if rc == 0:
385                        # Strip out commented lines (specific dmidecode output)
386                        thisvalue = ''.join([line for line in out.splitlines() if not line.startswith('#')])
387                        try:
388                            json.dumps(thisvalue)
389                        except UnicodeDecodeError:
390                            thisvalue = "NA"
391
392                        dmi_facts[k] = thisvalue
393                    else:
394                        dmi_facts[k] = 'NA'
395                else:
396                    dmi_facts[k] = 'NA'
397
398        return dmi_facts
399
400    def _run_lsblk(self, lsblk_path):
401        # call lsblk and collect all uuids
402        # --exclude 2 makes lsblk ignore floppy disks, which are slower to answer than typical timeouts
403        # this uses the linux major device number
404        # for details see https://www.kernel.org/doc/Documentation/devices.txt
405        args = ['--list', '--noheadings', '--paths', '--output', 'NAME,UUID', '--exclude', '2']
406        cmd = [lsblk_path] + args
407        rc, out, err = self.module.run_command(cmd)
408        return rc, out, err
409
410    def _lsblk_uuid(self):
411        uuids = {}
412        lsblk_path = self.module.get_bin_path("lsblk")
413        if not lsblk_path:
414            return uuids
415
416        rc, out, err = self._run_lsblk(lsblk_path)
417        if rc != 0:
418            return uuids
419
420        # each line will be in format:
421        # <devicename><some whitespace><uuid>
422        # /dev/sda1  32caaec3-ef40-4691-a3b6-438c3f9bc1c0
423        for lsblk_line in out.splitlines():
424            if not lsblk_line:
425                continue
426
427            line = lsblk_line.strip()
428            fields = line.rsplit(None, 1)
429
430            if len(fields) < 2:
431                continue
432
433            device_name, uuid = fields[0].strip(), fields[1].strip()
434            if device_name in uuids:
435                continue
436            uuids[device_name] = uuid
437
438        return uuids
439
440    def _udevadm_uuid(self, device):
441        # fallback for versions of lsblk <= 2.23 that don't have --paths, see _run_lsblk() above
442        uuid = 'N/A'
443
444        udevadm_path = self.module.get_bin_path('udevadm')
445        if not udevadm_path:
446            return uuid
447
448        cmd = [udevadm_path, 'info', '--query', 'property', '--name', device]
449        rc, out, err = self.module.run_command(cmd)
450        if rc != 0:
451            return uuid
452
453        # a snippet of the output of the udevadm command below will be:
454        # ...
455        # ID_FS_TYPE=ext4
456        # ID_FS_USAGE=filesystem
457        # ID_FS_UUID=57b1a3e7-9019-4747-9809-7ec52bba9179
458        # ...
459        m = re.search('ID_FS_UUID=(.*)\n', out)
460        if m:
461            uuid = m.group(1)
462
463        return uuid
464
465    def _run_findmnt(self, findmnt_path):
466        args = ['--list', '--noheadings', '--notruncate']
467        cmd = [findmnt_path] + args
468        rc, out, err = self.module.run_command(cmd, errors='surrogate_then_replace')
469        return rc, out, err
470
471    def _find_bind_mounts(self):
472        bind_mounts = set()
473        findmnt_path = self.module.get_bin_path("findmnt")
474        if not findmnt_path:
475            return bind_mounts
476
477        rc, out, err = self._run_findmnt(findmnt_path)
478        if rc != 0:
479            return bind_mounts
480
481        # find bind mounts, in case /etc/mtab is a symlink to /proc/mounts
482        for line in out.splitlines():
483            fields = line.split()
484            # fields[0] is the TARGET, fields[1] is the SOURCE
485            if len(fields) < 2:
486                continue
487
488            # bind mounts will have a [/directory_name] in the SOURCE column
489            if self.BIND_MOUNT_RE.match(fields[1]):
490                bind_mounts.add(fields[0])
491
492        return bind_mounts
493
494    def _mtab_entries(self):
495        mtab_file = '/etc/mtab'
496        if not os.path.exists(mtab_file):
497            mtab_file = '/proc/mounts'
498
499        mtab = get_file_content(mtab_file, '')
500        mtab_entries = []
501        for line in mtab.splitlines():
502            fields = line.split()
503            if len(fields) < 4:
504                continue
505            mtab_entries.append(fields)
506        return mtab_entries
507
508    @staticmethod
509    def _replace_octal_escapes_helper(match):
510        # Convert to integer using base8 and then convert to character
511        return chr(int(match.group()[1:], 8))
512
513    def _replace_octal_escapes(self, value):
514        return self.OCTAL_ESCAPE_RE.sub(self._replace_octal_escapes_helper, value)
515
516    def get_mount_info(self, mount, device, uuids):
517
518        mount_size = get_mount_size(mount)
519
520        # _udevadm_uuid is a fallback for versions of lsblk <= 2.23 that don't have --paths
521        # see _run_lsblk() above
522        # https://github.com/ansible/ansible/issues/36077
523        uuid = uuids.get(device, self._udevadm_uuid(device))
524
525        return mount_size, uuid
526
527    def get_mount_facts(self):
528
529        mounts = []
530
531        # gather system lists
532        bind_mounts = self._find_bind_mounts()
533        uuids = self._lsblk_uuid()
534        mtab_entries = self._mtab_entries()
535
536        # start threads to query each mount
537        results = {}
538        pool = ThreadPool(processes=min(len(mtab_entries), cpu_count()))
539        maxtime = globals().get('GATHER_TIMEOUT') or timeout.DEFAULT_GATHER_TIMEOUT
540        for fields in mtab_entries:
541            # Transform octal escape sequences
542            fields = [self._replace_octal_escapes(field) for field in fields]
543
544            device, mount, fstype, options = fields[0], fields[1], fields[2], fields[3]
545
546            if not device.startswith('/') and ':/' not in device or fstype == 'none':
547                continue
548
549            mount_info = {'mount': mount,
550                          'device': device,
551                          'fstype': fstype,
552                          'options': options}
553
554            if mount in bind_mounts:
555                # only add if not already there, we might have a plain /etc/mtab
556                if not self.MTAB_BIND_MOUNT_RE.match(options):
557                    mount_info['options'] += ",bind"
558
559            results[mount] = {'info': mount_info,
560                              'extra': pool.apply_async(self.get_mount_info, (mount, device, uuids)),
561                              'timelimit': time.time() + maxtime}
562
563        pool.close()  # done with new workers, start gc
564
565        # wait for workers and get results
566        while results:
567            for mount in results:
568                res = results[mount]['extra']
569                if res.ready():
570                    if res.successful():
571                        mount_size, uuid = res.get()
572                        if mount_size:
573                            results[mount]['info'].update(mount_size)
574                        results[mount]['info']['uuid'] = uuid or 'N/A'
575                    else:
576                        # give incomplete data
577                        errmsg = to_text(res.get())
578                        self.module.warn("Error prevented getting extra info for mount %s: %s." % (mount, errmsg))
579                        results[mount]['info']['note'] = 'Could not get extra information: %s.' % (errmsg)
580
581                    mounts.append(results[mount]['info'])
582                    del results[mount]
583                    break
584                elif time.time() > results[mount]['timelimit']:
585                    results[mount]['info']['note'] = 'Timed out while attempting to get extra information.'
586                    mounts.append(results[mount]['info'])
587                    del results[mount]
588                    break
589            else:
590                # avoid cpu churn
591                time.sleep(0.1)
592
593        return {'mounts': mounts}
594
595    def get_device_links(self, link_dir):
596        if not os.path.exists(link_dir):
597            return {}
598        try:
599            retval = collections.defaultdict(set)
600            for entry in os.listdir(link_dir):
601                try:
602                    target = os.path.basename(os.readlink(os.path.join(link_dir, entry)))
603                    retval[target].add(entry)
604                except OSError:
605                    continue
606            return dict((k, list(sorted(v))) for (k, v) in iteritems(retval))
607        except OSError:
608            return {}
609
610    def get_all_device_owners(self):
611        try:
612            retval = collections.defaultdict(set)
613            for path in glob.glob('/sys/block/*/slaves/*'):
614                elements = path.split('/')
615                device = elements[3]
616                target = elements[5]
617                retval[target].add(device)
618            return dict((k, list(sorted(v))) for (k, v) in iteritems(retval))
619        except OSError:
620            return {}
621
622    def get_all_device_links(self):
623        return {
624            'ids': self.get_device_links('/dev/disk/by-id'),
625            'uuids': self.get_device_links('/dev/disk/by-uuid'),
626            'labels': self.get_device_links('/dev/disk/by-label'),
627            'masters': self.get_all_device_owners(),
628        }
629
630    def get_holders(self, block_dev_dict, sysdir):
631        block_dev_dict['holders'] = []
632        if os.path.isdir(sysdir + "/holders"):
633            for folder in os.listdir(sysdir + "/holders"):
634                if not folder.startswith("dm-"):
635                    continue
636                name = get_file_content(sysdir + "/holders/" + folder + "/dm/name")
637                if name:
638                    block_dev_dict['holders'].append(name)
639                else:
640                    block_dev_dict['holders'].append(folder)
641
642    def get_device_facts(self):
643        device_facts = {}
644
645        device_facts['devices'] = {}
646        lspci = self.module.get_bin_path('lspci')
647        if lspci:
648            rc, pcidata, err = self.module.run_command([lspci, '-D'], errors='surrogate_then_replace')
649        else:
650            pcidata = None
651
652        try:
653            block_devs = os.listdir("/sys/block")
654        except OSError:
655            return device_facts
656
657        devs_wwn = {}
658        try:
659            devs_by_id = os.listdir("/dev/disk/by-id")
660        except OSError:
661            pass
662        else:
663            for link_name in devs_by_id:
664                if link_name.startswith("wwn-"):
665                    try:
666                        wwn_link = os.readlink(os.path.join("/dev/disk/by-id", link_name))
667                    except OSError:
668                        continue
669                    devs_wwn[os.path.basename(wwn_link)] = link_name[4:]
670
671        links = self.get_all_device_links()
672        device_facts['device_links'] = links
673
674        for block in block_devs:
675            virtual = 1
676            sysfs_no_links = 0
677            try:
678                path = os.readlink(os.path.join("/sys/block/", block))
679            except OSError:
680                e = sys.exc_info()[1]
681                if e.errno == errno.EINVAL:
682                    path = block
683                    sysfs_no_links = 1
684                else:
685                    continue
686            sysdir = os.path.join("/sys/block", path)
687            if sysfs_no_links == 1:
688                for folder in os.listdir(sysdir):
689                    if "device" in folder:
690                        virtual = 0
691                        break
692            d = {}
693            d['virtual'] = virtual
694            d['links'] = {}
695            for (link_type, link_values) in iteritems(links):
696                d['links'][link_type] = link_values.get(block, [])
697            diskname = os.path.basename(sysdir)
698            for key in ['vendor', 'model', 'sas_address', 'sas_device_handle']:
699                d[key] = get_file_content(sysdir + "/device/" + key)
700
701            sg_inq = self.module.get_bin_path('sg_inq')
702
703            if sg_inq:
704                device = "/dev/%s" % (block)
705                rc, drivedata, err = self.module.run_command([sg_inq, device])
706                if rc == 0:
707                    serial = re.search(r"Unit serial number:\s+(\w+)", drivedata)
708                    if serial:
709                        d['serial'] = serial.group(1)
710
711            for key, test in [('removable', '/removable'),
712                              ('support_discard', '/queue/discard_granularity'),
713                              ]:
714                d[key] = get_file_content(sysdir + test)
715
716            if diskname in devs_wwn:
717                d['wwn'] = devs_wwn[diskname]
718
719            d['partitions'] = {}
720            for folder in os.listdir(sysdir):
721                m = re.search("(" + diskname + r"[p]?\d+)", folder)
722                if m:
723                    part = {}
724                    partname = m.group(1)
725                    part_sysdir = sysdir + "/" + partname
726
727                    part['links'] = {}
728                    for (link_type, link_values) in iteritems(links):
729                        part['links'][link_type] = link_values.get(partname, [])
730
731                    part['start'] = get_file_content(part_sysdir + "/start", 0)
732                    part['sectors'] = get_file_content(part_sysdir + "/size", 0)
733
734                    part['sectorsize'] = get_file_content(part_sysdir + "/queue/logical_block_size")
735                    if not part['sectorsize']:
736                        part['sectorsize'] = get_file_content(part_sysdir + "/queue/hw_sector_size", 512)
737                    part['size'] = bytes_to_human((float(part['sectors']) * 512.0))
738                    part['uuid'] = get_partition_uuid(partname)
739                    self.get_holders(part, part_sysdir)
740
741                    d['partitions'][partname] = part
742
743            d['rotational'] = get_file_content(sysdir + "/queue/rotational")
744            d['scheduler_mode'] = ""
745            scheduler = get_file_content(sysdir + "/queue/scheduler")
746            if scheduler is not None:
747                m = re.match(r".*?(\[(.*)\])", scheduler)
748                if m:
749                    d['scheduler_mode'] = m.group(2)
750
751            d['sectors'] = get_file_content(sysdir + "/size")
752            if not d['sectors']:
753                d['sectors'] = 0
754            d['sectorsize'] = get_file_content(sysdir + "/queue/logical_block_size")
755            if not d['sectorsize']:
756                d['sectorsize'] = get_file_content(sysdir + "/queue/hw_sector_size", 512)
757            d['size'] = bytes_to_human(float(d['sectors']) * 512.0)
758
759            d['host'] = ""
760
761            # domains are numbered (0 to ffff), bus (0 to ff), slot (0 to 1f), and function (0 to 7).
762            m = re.match(r".+/([a-f0-9]{4}:[a-f0-9]{2}:[0|1][a-f0-9]\.[0-7])/", sysdir)
763            if m and pcidata:
764                pciid = m.group(1)
765                did = re.escape(pciid)
766                m = re.search("^" + did + r"\s(.*)$", pcidata, re.MULTILINE)
767                if m:
768                    d['host'] = m.group(1)
769
770            self.get_holders(d, sysdir)
771
772            device_facts['devices'][diskname] = d
773
774        return device_facts
775
776    def get_uptime_facts(self):
777        uptime_facts = {}
778        uptime_file_content = get_file_content('/proc/uptime')
779        if uptime_file_content:
780            uptime_seconds_string = uptime_file_content.split(' ')[0]
781            uptime_facts['uptime_seconds'] = int(float(uptime_seconds_string))
782
783        return uptime_facts
784
785    def _find_mapper_device_name(self, dm_device):
786        dm_prefix = '/dev/dm-'
787        mapper_device = dm_device
788        if dm_device.startswith(dm_prefix):
789            dmsetup_cmd = self.module.get_bin_path('dmsetup', True)
790            mapper_prefix = '/dev/mapper/'
791            rc, dm_name, err = self.module.run_command("%s info -C --noheadings -o name %s" % (dmsetup_cmd, dm_device))
792            if rc == 0:
793                mapper_device = mapper_prefix + dm_name.rstrip()
794        return mapper_device
795
796    def get_lvm_facts(self):
797        """ Get LVM Facts if running as root and lvm utils are available """
798
799        lvm_facts = {}
800
801        if os.getuid() == 0 and self.module.get_bin_path('vgs'):
802            lvm_util_options = '--noheadings --nosuffix --units g --separator ,'
803
804            vgs_path = self.module.get_bin_path('vgs')
805            # vgs fields: VG #PV #LV #SN Attr VSize VFree
806            vgs = {}
807            if vgs_path:
808                rc, vg_lines, err = self.module.run_command('%s %s' % (vgs_path, lvm_util_options))
809                for vg_line in vg_lines.splitlines():
810                    items = vg_line.strip().split(',')
811                    vgs[items[0]] = {'size_g': items[-2],
812                                     'free_g': items[-1],
813                                     'num_lvs': items[2],
814                                     'num_pvs': items[1]}
815
816            lvs_path = self.module.get_bin_path('lvs')
817            # lvs fields:
818            # LV VG Attr LSize Pool Origin Data% Move Log Copy% Convert
819            lvs = {}
820            if lvs_path:
821                rc, lv_lines, err = self.module.run_command('%s %s' % (lvs_path, lvm_util_options))
822                for lv_line in lv_lines.splitlines():
823                    items = lv_line.strip().split(',')
824                    lvs[items[0]] = {'size_g': items[3], 'vg': items[1]}
825
826            pvs_path = self.module.get_bin_path('pvs')
827            # pvs fields: PV VG #Fmt #Attr PSize PFree
828            pvs = {}
829            if pvs_path:
830                rc, pv_lines, err = self.module.run_command('%s %s' % (pvs_path, lvm_util_options))
831                for pv_line in pv_lines.splitlines():
832                    items = pv_line.strip().split(',')
833                    pvs[self._find_mapper_device_name(items[0])] = {
834                        'size_g': items[4],
835                        'free_g': items[5],
836                        'vg': items[1]}
837
838            lvm_facts['lvm'] = {'lvs': lvs, 'vgs': vgs, 'pvs': pvs}
839
840        return lvm_facts
841
842
843class LinuxHardwareCollector(HardwareCollector):
844    _platform = 'Linux'
845    _fact_class = LinuxHardware
846
847    required_facts = set(['platform'])
848