Created
June 22, 2012 11:17
-
-
Save mgedmin/2972159 to your computer and use it in GitHub Desktop.
disk-inventory
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/python | |
""" | |
Produce a disk inventory for fridge: | |
- how many hard disks and how large | |
- how are they partitioned | |
- how are the RAID devices defined | |
- where are they mounted | |
- how much space is used and how much is free | |
Written by Marius Gedminas <[email protected]> | |
""" | |
import collections | |
import optparse | |
import os | |
__version__ = '1.1.3' | |
FilesystemInfo = collections.namedtuple('FilesystemInfo', | |
'device mountpoint fstype size_kb used_kb avail_kb') | |
PVInfo = collections.namedtuple('PVInfo', 'device vgname') | |
VGInfo = collections.namedtuple('VGInfo', 'name size_kb used_kb free_kb') | |
LVInfo = collections.namedtuple('LVInfo', 'name vgname size_sectors device') | |
DMInfo = collections.namedtuple('LVInfo', 'name major minor') | |
class LinuxDiskInfo(object): | |
_swap_devices_cache = None | |
_filesystems_cache = None | |
_filesystems_by_device_cache = None | |
_lvm_pvs_cache = None | |
_dm_cache = None | |
@property | |
def _swap_devices(self): | |
if self._swap_devices_cache is None: | |
self._swap_devices_cache = self.list_swap_devices() | |
return self._swap_devices_cache | |
@property | |
def _filesystems(self): | |
if self._filesystems_cache is None: | |
self._filesystems_cache = self.list_filesystems() | |
return self._filesystems_cache | |
@property | |
def _filesystems_by_device(self): | |
if self._filesystems_by_device_cache is None: | |
self._filesystems_by_device_cache = dict( | |
(r.device, r) for r in self._filesystems) | |
return self._filesystems_by_device_cache | |
@property | |
def _lvm_pvs(self): | |
if self._lvm_pvs_cache is None: | |
self._lvm_pvs_cache = dict( | |
(pv.device, pv) for pv in self.list_lvm_physical_volumes()) | |
return self._lvm_pvs_cache | |
@property | |
def _dms(self): | |
if self._dm_cache is None: | |
self._dm_cache = dict( | |
(dm.name, dm) for dm in self.list_device_mapper()) | |
return self._dm_cache | |
def _read_string(self, filename): | |
with open(filename) as f: | |
return f.read().strip() | |
def _read_int(self, filename): | |
return int(self._read_string(filename)) | |
def list_swap_devices(self): | |
"""Return short device names such as ['sda1']""" | |
res = [] | |
with open('/proc/swaps') as f: | |
f.readline() # skip header | |
for line in f: | |
filename = line.split()[0] | |
if filename.startswith('/dev/'): | |
res.append(filename[len('/dev/'):]) | |
return res | |
def list_filesystems(self): | |
"""Return a list of FilesystemInfo tuples.""" | |
res = [] | |
with os.popen('df -P --local --print-type') as f: | |
f.readline() # skip header | |
for line in f: | |
device, fstype, size_kb, used_kb, avail_kb, use_percent, mountpoint = line.split() | |
if device.startswith('/dev/'): | |
res.append(FilesystemInfo(device[len('/dev/'):], mountpoint, fstype, int(size_kb), int(used_kb), int(avail_kb))) | |
return res | |
def list_physical_disks(self): | |
"""Return physical disk names such as ['sda', 'sdb']. | |
Limitations: only handles ATA/SATA/SCSI disks; no RAID or whatnot. | |
""" | |
return sorted(name for name in os.listdir('/sys/block') | |
if name.startswith('sd')) | |
def list_device_mapper(self): | |
"""Return a list of DMInfo tuples.""" | |
res = [] | |
with os.popen('dmsetup -c --noheadings info') as f: | |
for line in f: | |
# name, major, minor, attr, open, segments, events, uuid. | |
name, major, minor, _, _, _, _, _ = line.split(':') | |
res.append(DMInfo(name, int(major), int(minor))) | |
return res | |
def list_lvm_volume_groups(self): | |
"""Return volume groups names.""" | |
res = [] | |
with os.popen('vgdisplay -c 2>/dev/null') as f: | |
for line in f: | |
# columns: | |
# 1 volume group name | |
# 2 volume group access | |
# 3 volume group status | |
# 4 internal volume group number | |
# 5 maximum number of logical volumes | |
# 6 current number of logical volumes | |
# 7 open count of all logical volumes in this volume group | |
# 8 maximum logical volume size | |
# 9 maximum number of physical volumes | |
# 10 current number of physical volumes | |
# 11 actual number of physical volumes | |
# 12 size of volume group in kilobytes | |
# 13 physical extent size | |
# 14 total number of physical extents for this volume group | |
# 15 allocated number of physical extents for this volume group | |
# 16 free number of physical extents for this volume group | |
# 17 uuid of volume group | |
(vgname, _, _, _, _, _, _, _, _, _, _, size_kb, extent_size_kb, | |
n_extents, used_extents, free_extents, | |
uuid) = line.strip().split(':') | |
res.append(VGInfo(vgname, int(size_kb), | |
int(used_extents) * int(extent_size_kb), | |
int(free_extents) * int(extent_size_kb))) | |
return res | |
def list_lvm_physical_volumes(self): | |
"""Return a list of PVInfo tuples.""" | |
res = [] | |
with os.popen('pvdisplay -c 2>/dev/null') as f: | |
for line in f: | |
# the "wtf" column is "physical volume (not) allocatable" | |
# the _ column is "internal physical volume number (obsolete)" | |
(device, vgname, size_kb, _, status, wtf, n_volumes, | |
extent_size_kb, n_extents, free_extents, | |
used_extents, uuid) = line.strip().split(':') | |
if device.startswith('/dev/'): | |
res.append(PVInfo(device[len('/dev/'):], vgname)) | |
return res | |
def list_lvm_logical_volumes(self): | |
res = [] | |
with os.popen('lvdisplay -c 2>/dev/null') as f: | |
for line in f: | |
# columns: | |
# - logical volume name | |
# - volume group name | |
# - logical volume access | |
# - logical volume status | |
# - internal logical volume number | |
# - open count of logical volume | |
# - logical volume size in sectors | |
# - current logical extents associated to logical volume | |
# - allocated logical extents of logical volume | |
# - allocation policy of logical volume | |
# - read ahead sectors of logical volume | |
# - major device number of logical volume | |
# - minor device number of logical volume | |
(name, vgname, _, _, _, _, size_sectors, cur_extents, | |
alloc_extents, _, _, _, _) = line.strip().split(':') | |
lvname = name.rpartition('/')[-1] # /dev/{vgname}/{lvname} | |
device = 'mapper/{vgname}-{lvname}'.format( | |
vgname=vgname.replace('-', '--'), | |
lvname=lvname.replace('-', '--'), | |
) | |
res.append(LVInfo(lvname, vgname, int(size_sectors), device)) | |
return res | |
def get_disk_size_sectors(self, disk_name): | |
return self._read_int('/sys/block/%s/size' % disk_name) | |
def get_disk_size_bytes(self, disk_name): | |
# Experiments show that the kernel always reports 512-byte sectors, | |
# even when the disk uses 4KiB sectors. | |
return self.get_disk_size_sectors(disk_name) * 512 | |
def get_disk_model(self, disk_name): | |
return self._read_string('/sys/block/%s/device/model' % disk_name) | |
def get_disk_firmware_rev(self, disk_name): | |
return self._read_string('/sys/block/%s/device/rev' % disk_name) | |
def list_partitions(self, disk_name): | |
"""Return partition names such as ['sda1', ...]. | |
Includes primary, extended and logical partition names. | |
""" | |
return sorted(name for name in os.listdir('/sys/block/%s' % disk_name) | |
if name.startswith(disk_name)) | |
def get_dm_for(self, name): | |
"""Find device mapper with a given name.""" | |
return 'dm-%d' % self._dms[name].minor | |
def get_sys_dir_for_partition(self, partition_name): | |
"""Find /sys directory name from partition name (e.g. 'sda1').""" | |
if partition_name.startswith('mapper/'): | |
name = partition_name[len('mapper/'):] | |
return '/sys/block/%s' % self.get_dm_for(name) | |
return '/sys/block/%s/%s' % (partition_name[:3], partition_name) | |
def get_partition_size_sectors(self, partition_name): | |
sysdir = self.get_sys_dir_for_partition(partition_name) | |
return self._read_int(sysdir + '/size') | |
def get_partition_size_bytes(self, partition_name): | |
return self.get_partition_size_sectors(partition_name) * 512 | |
def get_partition_offset_sectors(self, partition_name): | |
sysdir = self.get_sys_dir_for_partition(partition_name) | |
return self._read_int(sysdir + '/start') | |
def get_partition_offset_bytes(self, partition_name): | |
return self.get_partition_offset_sectors(partition_name) * 512 | |
def list_partition_holders(self, partition_name): | |
sysdir = self.get_sys_dir_for_partition(partition_name) | |
return sorted(os.listdir(sysdir + '/holders')) | |
def get_partition_fsinfo(self, partition_name): | |
holders = self.list_partition_holders(partition_name) | |
raid_devices = [d for d in holders if d.startswith('md')] | |
for dev in [partition_name] + raid_devices: | |
fsinfo = self._filesystems_by_device.get(dev) | |
if fsinfo is not None: | |
return fsinfo | |
return None | |
def get_partition_usage(self, partition_name): | |
users = [] | |
pv = self._lvm_pvs.get(partition_name) | |
if pv: | |
users.append('LVM: ' + pv.vgname) | |
users.extend(self.list_partition_holders(partition_name)) | |
if partition_name in self._swap_devices: | |
users.append('swap') | |
fsinfo = self.get_partition_fsinfo(partition_name) | |
if fsinfo is not None: | |
users.append(fsinfo.fstype) | |
users.append(fsinfo.mountpoint) | |
return ' '.join(users) | |
def fmt_size_si(bytes): | |
size, units = bytes, 'B' | |
for prefix in 'KiB', 'MiB', 'GiB', 'TiB', 'PiB': | |
if size >= 1024: | |
size, units = size / 1024., prefix | |
return '%.1f %s' % (size, units) | |
def fmt_size_decimal(bytes): | |
size, units = bytes, 'B' | |
for prefix in 'KB', 'MB', 'GB', 'TB', 'PB': | |
if size >= 1000: | |
size, units = size / 1000., prefix | |
return '%.1f %s' % (size, units) | |
def main(): | |
parser = optparse.OptionParser(usage='%prog [options]') | |
parser.add_option('-v', '--verbose', action='count', dest='verbose', | |
default=1) | |
parser.add_option('--decimal', help='use decimal units (1 KB = 1000 B)', | |
action='store_const', dest='fmt_size', | |
const=fmt_size_decimal) | |
parser.add_option('--si', help='use SI units (1 KiB = 1024 B)', | |
action='store_const', dest='fmt_size', | |
const=fmt_size_si) | |
parser.set_defaults(fmt_size=fmt_size_decimal) | |
opts, args = parser.parse_args() | |
fmt_size = opts.fmt_size | |
info = LinuxDiskInfo() | |
for disk in info.list_physical_disks(): | |
disk_size_bytes = info.get_disk_size_bytes(disk) | |
template = "{disk}: {model} ({size})" | |
if opts.verbose >= 2: | |
template += ', firmware revision {fwrev}' | |
print template.format( | |
disk=disk, | |
model=info.get_disk_model(disk), | |
size=fmt_size(disk_size_bytes), | |
fwrev=info.get_disk_firmware_rev(disk), | |
) | |
unallocated = disk_size_bytes | |
partition = None | |
last_partition_end = 0 | |
for partition in info.list_partitions(disk): | |
partition_size_bytes = info.get_partition_size_bytes(partition) | |
if partition_size_bytes <= 1024*1024 and opts.verbose < 2: | |
# all extended partitions I've seen show up as being 2 sectors | |
# big. maybe they would become bigger if I had more logical | |
# partitions? | |
continue | |
usage = info.get_partition_usage(partition) | |
fsinfo = info.get_partition_fsinfo(partition) | |
print " {name:8} {size:>10} {usage:30} {free_space:>15}".format( | |
name=partition + ':', | |
usage=usage, | |
size=fmt_size(partition_size_bytes), | |
free_space=fmt_size(fsinfo.avail_kb * 1024) + ' free' if fsinfo | |
else '', | |
).rstrip() | |
unallocated -= partition_size_bytes | |
partition_start = info.get_partition_offset_bytes(partition) | |
partition_end = partition_start + partition_size_bytes | |
last_partition_end = max(last_partition_end, partition_end) | |
free_space_at_end = disk_size_bytes - last_partition_end | |
if free_space_at_end and (opts.verbose >= 2 | |
or free_space_at_end > 100*1000**2): # megs | |
print " {spacing:8} {size:>10} (unused)".format( | |
spacing='', | |
size=fmt_size(free_space_at_end), | |
) | |
unallocated -= free_space_at_end | |
if unallocated and opts.verbose >= 2: | |
print " {spacing:8} {size:>10} (metadata/internal fragmentation)".format( | |
spacing='', | |
size=fmt_size(unallocated), | |
) | |
for vgroup in info.list_lvm_volume_groups(): | |
template = "{vgroup}: LVM ({size})" | |
print template.format( | |
vgroup=vgroup.name, | |
size=fmt_size(vgroup.size_kb * 1024), | |
) | |
for lv in info.list_lvm_logical_volumes(): | |
if lv.vgname != vgroup.name: | |
continue | |
usage = info.get_partition_usage(lv.device) | |
fsinfo = info.get_partition_fsinfo(lv.device) | |
print " {name:8} {size:>10} {usage:30} {free_space:>15}".format( | |
name=lv.name+':', | |
usage=usage, | |
size=fmt_size(lv.size_sectors * 512), | |
free_space=fmt_size(fsinfo.avail_kb * 1024) + ' free' if fsinfo | |
else '', | |
).rstrip() | |
if vgroup.free_kb >= 1024 or opts.verbose >= 2: | |
print " {name:8} {size:>10}".format( | |
name='free:', | |
size=fmt_size(vgroup.free_kb * 1024), | |
) | |
if __name__ == '__main__': | |
main() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
mg@fridge:~ $ disk-inventory | |
sda: ST1000NM0011 (1.0 TB) | |
sda1: 2.0 GB swap | |
sda2: 1.0 GB md0 ext3 / 311.2 MB free | |
sda5: 15.0 GB md1 ext3 /var 3.3 GB free | |
sda6: 5.0 GB md2 ext3 /usr 1.4 GB free | |
sda7: 230.0 GB md3 ext3 /home 25.4 GB free | |
sda8: 247.1 GB md4 ext3 /stuff 70.7 GB free | |
sda9: 500.1 GB | |
sdb: ST3500320AS (500.1 GB) | |
sdb1: 2.0 GB swap | |
sdb2: 1.0 GB md0 ext3 / 311.2 MB free | |
sdb5: 15.0 GB md1 ext3 /var 3.3 GB free | |
sdb6: 5.0 GB md2 ext3 /usr 1.4 GB free | |
sdb7: 230.0 GB md3 ext3 /home 25.4 GB free | |
sdb8: 247.1 GB md4 ext3 /stuff 70.7 GB free |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Now packaged as part of https://github.com/ProgrammersOfVilnius/pov-admin-tools