# Lint as: python2, python3 """This module gives the mkfs creation options for an existing filesystem. tune2fs or xfs_growfs is called according to the filesystem. The results, filesystem tunables, are parsed and mapped to corresponding mkfs options. """ from __future__ import absolute_import from __future__ import division from __future__ import print_function import os, re, tempfile import six import common from autotest_lib.client.common_lib import error, utils def opt_string2dict(opt_string): """Breaks the mkfs.ext* option string into dictionary.""" # Example string: '-j -q -i 8192 -b 4096'. There may be extra whitespaces. opt_dict = {} for item in opt_string.split('-'): item = item.strip() if ' ' in item: (opt, value) = item.split(' ', 1) opt_dict['-%s' % opt] = value elif item != '': opt_dict['-%s' % item] = None # Convert all the digit strings to int. for key, value in six.iteritems(opt_dict): if value and value.isdigit(): opt_dict[key] = int(value) return opt_dict def parse_mke2fs_conf(fs_type, conf_file='/etc/mke2fs.conf'): """Parses mke2fs config file for default settings.""" # Please see /ect/mke2fs.conf for an example. default_opt = {} fs_opt = {} current_fs_type = '' current_section = '' f = open(conf_file, 'r') for line in f: if '[defaults]' == line.strip(): current_section = '[defaults]' elif '[fs_types]' == line.strip(): current_section = '[fs_types]' elif current_section == '[defaults]': components = line.split('=', 1) if len(components) == 2: default_opt[components[0].strip()] = components[1].strip() elif current_section == '[fs_types]': m = re.search('(\w+) = {', line) if m: current_fs_type = m.group(1) else: components = line.split('=', 1) if len(components) == 2 and current_fs_type == fs_type: default_opt[components[0].strip()] = components[1].strip() f.close() # fs_types options override the defaults options for key, value in six.iteritems(fs_opt): default_opt[key] = value # Convert all the digit strings to int. for key, value in six.iteritems(default_opt): if value and value.isdigit(): default_opt[key] = int(value) return default_opt def convert_conf_opt(default_opt): conf_opt_mapping = {'blocksize': '-b', 'inode_ratio': '-i', 'inode_size': '-I'} mkfs_opt = {} # Here we simply concatenate the feature string while we really need # to do the better and/or operations. if 'base_features' in default_opt: mkfs_opt['-O'] = default_opt['base_features'] if 'default_features' in default_opt: mkfs_opt['-O'] += ',%s' % default_opt['default_features'] if 'features' in default_opt: mkfs_opt['-O'] += ',%s' % default_opt['features'] for key, value in six.iteritems(conf_opt_mapping): if key in default_opt: mkfs_opt[value] = default_opt[key] if '-O' in mkfs_opt: mkfs_opt['-O'] = mkfs_opt['-O'].split(',') return mkfs_opt def merge_ext_features(conf_feature, user_feature): user_feature_list = user_feature.split(',') merged_feature = [] # Removes duplicate entries in conf_list. for item in conf_feature: if item not in merged_feature: merged_feature.append(item) # User options override config options. for item in user_feature_list: if item[0] == '^': if item[1:] in merged_feature: merged_feature.remove(item[1:]) else: merged_feature.append(item) elif item not in merged_feature: merged_feature.append(item) return merged_feature def ext_tunables(dev): """Call tune2fs -l and parse the result.""" cmd = 'tune2fs -l %s' % dev try: out = utils.system_output(cmd) except error.CmdError: tools_dir = os.path.join(os.environ['AUTODIR'], 'tools') cmd = '%s/tune2fs.ext4dev -l %s' % (tools_dir, dev) out = utils.system_output(cmd) # Load option mappings tune2fs_dict = {} for line in out.splitlines(): components = line.split(':', 1) if len(components) == 2: value = components[1].strip() option = components[0] if value.isdigit(): tune2fs_dict[option] = int(value) else: tune2fs_dict[option] = value return tune2fs_dict def ext_mkfs_options(tune2fs_dict, mkfs_option): """Map the tune2fs options to mkfs options.""" def __inode_count(tune_dict, k): return (tune_dict['Block count']/tune_dict[k] + 1) * ( tune_dict['Block size']) def __block_count(tune_dict, k): return int(100*tune_dict[k]/tune_dict['Block count'] + 1) def __volume_name(tune_dict, k): if tune_dict[k] != '': return tune_dict[k] else: return '' # mappings between fs features and mkfs options ext_mapping = {'Blocks per group': '-g', 'Block size': '-b', 'Filesystem features': '-O', 'Filesystem OS type': '-o', 'Filesystem revision #': '-r', 'Filesystem volume name': '-L', 'Flex block group size': '-G', 'Fragment size': '-f', 'Inode count': '-i', 'Inode size': '-I', 'Journal inode': '-j', 'Reserved block count': '-m'} conversions = { 'Journal inode': lambda d, k: None, 'Filesystem volume name': __volume_name, 'Reserved block count': __block_count, 'Inode count': __inode_count, 'Filesystem features': lambda d, k: re.sub(' ', ',', d[k]), 'Filesystem revision #': lambda d, k: d[k][0]} for key, value in six.iteritems(ext_mapping): if key not in tune2fs_dict: continue if key in conversions: mkfs_option[value] = conversions[key](tune2fs_dict, key) else: mkfs_option[value] = tune2fs_dict[key] def xfs_tunables(dev): """Call xfs_grow -n to get filesystem tunables.""" # Have to mount the filesystem to call xfs_grow. tmp_mount_dir = tempfile.mkdtemp() cmd = 'mount %s %s' % (dev, tmp_mount_dir) utils.system_output(cmd) xfs_growfs = os.path.join(os.environ['AUTODIR'], 'tools', 'xfs_growfs') cmd = '%s -n %s' % (xfs_growfs, dev) try: out = utils.system_output(cmd) finally: # Clean. cmd = 'umount %s' % dev utils.system_output(cmd, ignore_status=True) os.rmdir(tmp_mount_dir) ## The output format is given in report_info (xfs_growfs.c) ## "meta-data=%-22s isize=%-6u agcount=%u, agsize=%u blks\n" ## " =%-22s sectsz=%-5u attr=%u\n" ## "data =%-22s bsize=%-6u blocks=%llu, imaxpct=%u\n" ## " =%-22s sunit=%-6u swidth=%u blks\n" ## "naming =version %-14u bsize=%-6u\n" ## "log =%-22s bsize=%-6u blocks=%u, version=%u\n" ## " =%-22s sectsz=%-5u sunit=%u blks, lazy-count=%u\n" ## "realtime =%-22s extsz=%-6u blocks=%llu, rtextents=%llu\n" tune2fs_dict = {} # Flag for extracting naming version number keep_version = False for line in out.splitlines(): m = re.search('^([-\w]+)', line) if m: main_tag = m.group(1) pairs = line.split() for pair in pairs: # naming: version needs special treatment if pair == '=version': # 1 means the next pair is the version number we want keep_version = True continue if keep_version: tune2fs_dict['naming: version'] = pair # Resets the flag since we have logged the version keep_version = False continue # Ignores the strings without '=', such as 'blks' if '=' not in pair: continue key, value = pair.split('=') tagged_key = '%s: %s' % (main_tag, key) if re.match('[0-9]+', value): tune2fs_dict[tagged_key] = int(value.rstrip(',')) else: tune2fs_dict[tagged_key] = value.rstrip(',') return tune2fs_dict def xfs_mkfs_options(tune2fs_dict, mkfs_option): """Maps filesystem tunables to their corresponding mkfs options.""" # Mappings xfs_mapping = {'meta-data: isize': '-i size', 'meta-data: agcount': '-d agcount', 'meta-data: sectsz': '-s size', 'meta-data: attr': '-i attr', 'data: bsize': '-b size', 'data: imaxpct': '-i maxpct', 'data: sunit': '-d sunit', 'data: swidth': '-d swidth', 'data: unwritten': '-d unwritten', 'naming: version': '-n version', 'naming: bsize': '-n size', 'log: version': '-l version', 'log: sectsz': '-l sectsize', 'log: sunit': '-l sunit', 'log: lazy-count': '-l lazy-count', 'realtime: extsz': '-r extsize', 'realtime: blocks': '-r size', 'realtime: rtextents': '-r rtdev'} mkfs_option['-l size'] = tune2fs_dict['log: bsize'] * ( tune2fs_dict['log: blocks']) for key, value in six.iteritems(xfs_mapping): mkfs_option[value] = tune2fs_dict[key] def compare_features(needed_feature, current_feature): """Compare two ext* feature lists.""" if len(needed_feature) != len(current_feature): return False for feature in current_feature: if feature not in needed_feature: return False return True def match_ext_options(fs_type, dev, needed_options): """Compare the current ext* filesystem tunables with needed ones.""" # mkfs.ext* will load default options from /etc/mke2fs.conf conf_opt = parse_mke2fs_conf(fs_type) # We need to convert the conf options to mkfs options. conf_mkfs_opt = convert_conf_opt(conf_opt) # Breaks user mkfs option string to dictionary. needed_opt_dict = opt_string2dict(needed_options) # Removes ignored options. ignored_option = ['-c', '-q', '-E', '-F'] for opt in ignored_option: if opt in needed_opt_dict: del needed_opt_dict[opt] # User options override config options. needed_opt = conf_mkfs_opt for key, value in six.iteritems(needed_opt_dict): if key == '-N' or key == '-T': raise Exception('-N/T is not allowed.') elif key == '-O': needed_opt[key] = merge_ext_features(needed_opt[key], value) else: needed_opt[key] = value # '-j' option will add 'has_journal' feature. if '-j' in needed_opt and 'has_journal' not in needed_opt['-O']: needed_opt['-O'].append('has_journal') # 'extents' will be shown as 'extent' in the outcome of tune2fs if 'extents' in needed_opt['-O']: needed_opt['-O'].append('extent') needed_opt['-O'].remove('extents') # large_file is a byproduct of resize_inode. if 'large_file' not in needed_opt['-O'] and ( 'resize_inode' in needed_opt['-O']): needed_opt['-O'].append('large_file') current_opt = {} tune2fs_dict = ext_tunables(dev) ext_mkfs_options(tune2fs_dict, current_opt) # Does the match for key, value in six.iteritems(needed_opt): if key == '-O': if not compare_features(value, current_opt[key].split(',')): return False elif key not in current_opt or value != current_opt[key]: return False return True def match_xfs_options(dev, needed_options): """Compare the current ext* filesystem tunables with needed ones.""" tmp_mount_dir = tempfile.mkdtemp() cmd = 'mount %s %s' % (dev, tmp_mount_dir) utils.system_output(cmd) xfs_growfs = os.path.join(os.environ['AUTODIR'], 'tools', 'xfs_growfs') cmd = '%s -n %s' % (xfs_growfs, dev) try: current_option = utils.system_output(cmd) finally: # Clean. cmd = 'umount %s' % dev utils.system_output(cmd, ignore_status=True) os.rmdir(tmp_mount_dir) # '-N' has the same effect as '-n' in mkfs.ext*. Man mkfs.xfs for details. cmd = 'mkfs.xfs %s -N -f %s' % (needed_options, dev) needed_out = utils.system_output(cmd) # 'mkfs.xfs -N' produces slightly different result than 'xfs_growfs -n' needed_out = re.sub('internal log', 'internal ', needed_out) if current_option == needed_out: return True else: return False def match_mkfs_option(fs_type, dev, needed_options): """Compare the current filesystem tunables with needed ones.""" if fs_type.startswith('ext'): ret = match_ext_options(fs_type, dev, needed_options) elif fs_type == 'xfs': ret = match_xfs_options(dev, needed_options) else: ret = False return ret