一键重新挂盘

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import json
import datetime
import logging
import os
import subprocess

logging.basicConfig(level=logging.INFO,
                    format='%(asctime)s %(process)d %(thread)d %(levelname)s %(message)s @%(pathname)s:%(lineno)d')


def custom_subprocess_run(cmd):
    logging.info('execute cmd %s' % cmd)
    return subprocess.check_output(cmd, shell=True)


class DiskTool(object):

    def __init__(self, prefix, skip_disk_with_mount=True):
        self.skip_disk_with_mount = skip_disk_with_mount
        self.prefix = prefix

    @staticmethod
    def ls_blk():
        output = custom_subprocess_run("lsblk --json --output-all")
        blk_info = json.loads(output)
        return blk_info['blockdevices']

    def find_mount_line_by_dev(self, dev):
        mount_points = []
        for disk in self.ls_blk():

            if disk['kname'] == dev and disk.get('mountpoint'):
                mount_points.append(disk['mountpoint'])

            if disk.get('children'):
                for part in disk['children']:
                    if part['kname'].startswith(dev) and part.get('mountpoint'):
                        mount_points.append(part['mountpoint'])

        return mount_points

    def is_dev_need_process(self, dev):
        if dev.get('type') not in ('disk', 'part'):
            # 只处理磁盘和分区, 不处理loop设备等
            logging.info("dev {} type is {}, no need process".format(dev['kname'], dev['type']))
            return False

        if dev.get('mountpoint') == '/':  # 不处理系统盘
            logging.info("dev {} mount /, no need process".format(dev['kname']))
            return False

        if dev['mountpoint'] and self.skip_disk_with_mount:
            logging.info("dev {} mount {}, no need process".format(dev['kname'], dev['mountpoint']))
            return False

        return True

    def find_disk_to_process(self):
        ret = []
        for disk in self.ls_blk():
            if not self.is_dev_need_process(disk):
                continue

            for part in disk.get('children') or []:
                if not self.is_dev_need_process(part):
                    break
            else:
                ret.append(disk)
        return ret

    @staticmethod
    def get_disk_and_part_device_from_disks(disks):
        ret = []
        for disk in disks:
            ret.append(disk.get('kname'))
            for part in disk.get('children') or []:
                ret.append(part.get('kname'))
        return ret

    @staticmethod
    def format_dev(dev, label="ahaha"):
        try_counter = 20
        while try_counter > 0:
            try:
                try_counter -= 1
                cmd = 'mkfs.ext4 -F -L {} /dev/{}'.format(label, dev)
                return custom_subprocess_run(cmd)
            except Exception as e:
                logging.exception('format error: {}'.format(e))
                raise

    def get_uuids(self, disk_dev=None):
        uuids = []
        for dev in self.ls_blk():
            if not disk_dev or dev['kname'] == disk_dev:
                if dev['uuid']:
                    uuids.append(dev['uuid'])
                if dev.get('children'):
                    for part in dev['children']:
                        uuids.append(part['uuid'])
        return uuids

    def is_fstab_line_valid(self, disk_dev, line):
        line = line.strip()
        items = [item.strip() for item in line.split()]
        if not line:
            return False
        if items[1] == "/":  # / mount reserved
            return True

        if line.startswith('#') or line.find(disk_dev) != -1:
            return False

        dev_uuids = self.get_uuids(disk_dev)
        for uuid in dev_uuids:
            if line.find(uuid) != -1:
                return False

        if items[0].startswith('UUID'):  # clear invalid uuid
            mount_uuid = items[0].split('=')[1]
            blk_uuids = self.get_uuids()
            if mount_uuid not in set(blk_uuids):
                return False

        for point in self.find_mount_line_by_dev(disk_dev):
            if items[1].strip() == point:
                return False

        return True

    def cleanup_fstab(self, disk_dev):
        new_lines = []
        with open('/etc/fstab', 'r') as f:
            lines = f.readlines()
        for line in lines:
            if self.is_fstab_line_valid(disk_dev, line):
                new_lines.append(line)
            else:
                logging.info("we remove fstab line {}".format(line))
        with open('/etc/fstab', 'w') as f:
            f.writelines(new_lines)

    def add_disk_to_fstab(self, disk_dev, mountpoint):
        uuids = self.get_uuids(disk_dev)
        if len(uuids) > 1:
            raise Exception("{} has partition, format failed".format(disk_dev))
        with open('/etc/fstab', 'a') as f:
            line = 'UUID={} {} ext4 nofail,noatime 0 2\n'.format(uuids[0], mountpoint)
            logging.info('adding line to /etc/fstab, content=%s' % line.strip())
            f.write(line)

    def find_next_available_dataxx_mountpoint_from_fstab(self):
        with open('/etc/fstab', 'r') as f:
            fstab = f.read()
        for i in range(0, 100):
            mountpoint = self.prefix + str(i).zfill(2)
            if mountpoint not in fstab:
                logging.info('found available mount point from /etc/fstab mount point=%s' % mountpoint)
                return mountpoint
        return None

    def umount_repartition_format_cleanup_fstab_of_disk(self, disk):
        logging.info('processing disk.kname=%s', disk['kname'])
        self.umount_disk([disk])
        self.format_dev(disk['kname'])
        self.cleanup_fstab(disk['kname'])
        return disk['kname']

    def umount_dev_if_mounted(self, dev):
        mount_points = self.find_mount_line_by_dev(dev)
        logging.info(mount_points)
        if not mount_points:
            return True

        for mount in mount_points:
            cmd = 'umount {}'.format(mount)
            custom_subprocess_run(cmd)

    def umount_disk(self, disks):
        disk_and_disk_parts = self.get_disk_and_part_device_from_disks(disks)
        for dev in disk_and_disk_parts:
            self.umount_dev_if_mounted(dev)

    @staticmethod
    def backup_fstab():
        now_str = datetime.datetime.now().strftime("%y.%m.%d_%H.%M.%S")
        custom_subprocess_run("cp /etc/fstab /etc/fstab.{}.bak".format(now_str))

    def remount_all(self):
        self.backup_fstab()
        data_disks = self.find_disk_to_process()
        for disk in data_disks:
            disk_dev = self.umount_repartition_format_cleanup_fstab_of_disk(disk)
            mountpoint = self.find_next_available_dataxx_mountpoint_from_fstab()
            if not os.path.exists(mountpoint):
                os.makedirs(mountpoint)
            self.add_disk_to_fstab(disk_dev, mountpoint)
        custom_subprocess_run("mount -a")


def main():
    # 如果磁盘已经挂载默认不作处理, 设置环境变量skip_disk_with_mount=yes后执行,会卸载掉已经挂载的盘进行重新格式化&&挂载: 高危操作
    skip_disk_with_mount = os.environ.get('skip_disk_with_mount', 'no') in ('true', 'yes', 'y', '1')
    # 默认挂载点以/data 作为前缀, 挂载成/data00、/data01、/data99这样的格式
    prefix = os.environ.get("mount_prefix", "/data")

    try:
        disk_tool = DiskTool(prefix, skip_disk_with_mount)
        disk_tool.remount_all()
    except Exception as e:
        logging.exception("do remount data disk faild: {}".format(str(e)))


if __name__ == '__main__':
    main()

你可能感兴趣的:(一键重新挂盘)