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