Клонируем сами, своими руками

3_zusmsjrpxidzb-kjhw6cb0-dm.jpeg


Задача: Вася летел на самолёте. Из-за неисправности двигателя самолёт упал на необитаемом острове. Вася был единственным выжившим. После осмотра уцелевшего багажа Вася понял, что в его распоряжении есть несколько лаптопов и Wi-Fi роутер. Теперь для того, чтобы выжить, Вася решил поднять ЦОД. Для нормальной работы ЦОДа Васе нужно уметь клонировать Debian Linux. Но под рукой нет никаких средств клонирования. Даже старый диск Clone Zilla куда-то запропастился. Что делать Васе?

Итак, для начала посмотрим на кое-какие условия ТЗ, не описанные в первом абзаце.
У нас есть Debian Linux, установленный на виртуальную машину QEMU, в режиме EFI, с диском, подключённым через LVM. То, что нам хочется сделать — это получить клон этой машины, и «растянуть» уже существующую файловую систему, чтобы она занимала всё дисковое пространство на новом носителе.

Работать будем сразу двумя путями. Мы будем писать утилиту для клонирования на Golang, но по факту, эта утилита будет заниматься только вызовом достаточно стандартных команд из Linux. Идея заключается в том, что мы постараемся автоматизировать этот процесс по максимуму. Поэтому читатель может либо выполнять команды вручную, либо писать скрипт, который эти команды выполнит.

▍ Итак, поехали


6xcqqwehaeqywls0uaq1nct6tl4.png
Картина: Начинающие программисты поднимают первый под в кубере

Для начала давайте установим Debian Linux на QEMU, в котором мы подключим LVM диск. Настройте сам Debian как вашей душе будет угодно.

Первый этап процесса — получить изначальную копию диска. Сделать это можно двумя способами. Первый: открыть файл в golang и просто скопировать сам диск в этот файл с помощью io.Copy.

    dst, err := os.OpenFile(path, os.O_WRONLY, os.ModeDevice)
    if err != nil {
        return err
    }
    defer dst.Close()

    src, err := os.Open(filepath.Join(path, name+".img"))
    if err != nil {
        return err
    }
    defer src.Close()

    bw, err := io.Copy(src, dst)
    if err != nil {
        return err
    }
    dst.Close()
    src.Close()


Второй способ, намного проще, мы можем воспользоваться самой продвинутой утилитой клонирования в мире: dd.

dd if=/dev/sda of=~/image.img bs=32M


В любом случае у вас на руках то, что Васе доктор прописал — побитовый клон жёсткого диска.

▍ Восстанавливаем


5hzr3iiuw6v9pfbmfgrrsle5a64.png
Картина маслом: Опытный сисадмин ищет четырёхлетний бэкап на файл-сервере

Для восстановления информации выполните все те же инструкции, только поменяйте местами исходный диск и файл.

Когда клон будет готов, мы сможем приступить к восстановлению системы. На самом деле, на этом этапе мы могли бы просто запустить всё, что у нас есть на руках, и оно бы заработало. Но, вот в чём проблема, новый жёсткий диск был больше, чем исходный. Поэтому мы хотим немного поколдовать. И первое заклинание будет:

partprobe


Удивительно, но если вы клонируете 5 гигабайт жёсткого диска на NVME, то выполнение partprobe на системе займёт больше времени, чем сам процесс клонирования. Как бы то ни было, вы только что сказали вашей системе прошуршать существующие жёсткие диски и обновить информацию в конфигурации всех системных утилит.

На этом этапе у вас в системе есть два жёстких диска, которые полностью идентичны. Систему можно запускать, но она не заработает.

▍ Чиним EFI ударом светового меча от плеча до…


Первой проблемой будет EFI. Тут мне нужна будет помощь Хабра, ибо, возможно, существует лучшее решение этой проблемы. Для загрузки системы Debian создаёт два раздела на нашем жёстком диске. Первый — маленький для EFI Bootloader, второй будет поболе, для самой системы.

Момент заключается в том, что Debian сохраняет свой bootloader в /EFI/debian/shim64.efi, а QEMU ищет bootloader в /EFI/BOOT/BOOTx64.efi.

Тут варианта два. Первый — это переконфигурировать EFI на поиск файла в нужном месте, второе — просто скопировать файл в нужное место. Я всё ещё в поисках решения по первому пути, поэтому предлагаю вам второй путь.

В таком случае давайте сначала определим, как называются наши разделы на жёстком диске. И тут у нас есть прикол. Мы не можем предугадать имя раздела на жёстком диске. Всё зависит от названия самого жёсткого диска. Если название диска заканчивается на букву, то разделы будут называться 1 и 2. А если название диска заканчивается на цифру, то разделы называются p1 и p2 и так далее. Этот подход очень ненадёжен. Мне надо знать, как на самом деле называются разделы. Для этого воспользуемся замечательной утилитой lsblk.

lsblk /dev/mapper/diskname -blJ


В таком состоянии вы получите данные о том, сколько разделов есть на жёстком диске, куда и как они примонтированы, и что в них происходит. Более того, J позволит нам увидеть весь вывод этой утилиты в замечательно фаршированном JSON.

Давайте вызовем эту утилиту из golang, и для начала создадим пару констант и типов.

// LsblkPart data returned by LSBLK
type LsblkPart struct {
    Name        string
    MajMin      string `json:"maj:min"`
    Removable   bool   `json:"rm"`
    Size        uint64
    RO          bool `json:"ro"`
    Type        LsblkType
    Mountpoints []string
}

// list of all types that lsblk can return. Taken from lsblk source code
type LsblkType string

const (
    LsblkTypePart      = LsblkType("part")
    LsblkTypeLvm       = LsblkType("lvm")
    LsblkTypeCrypt     = LsblkType("crypt")
    LsblkTypeDmRaid    = LsblkType("dmraid")
    LsblkTypeMPath     = LsblkType("mpath")
    LsblkTypePath      = LsblkType("path")
    LsblkTypeDm        = LsblkType("dm")
    LsblkTypeLoop      = LsblkType("loop")
    LsblkTypeMd        = LsblkType("md")
    LsblkTypeLinear    = LsblkType("linear")
    LsblkTypeRaid0     = LsblkType("raid0")
    LsblkTypeRaid1     = LsblkType("raid1")
    LsblkTypeRaid4     = LsblkType("raid4")
    LsblkTypeRaid5     = LsblkType("raid5")
    LsblkTypeRaid10    = LsblkType("raid10")
    LsblkTypeMultipath = LsblkType("multipath")
    LsblkTypeDisk      = LsblkType("disk")
    LsblkTypeTape      = LsblkType("tape")
    LsblkTypePrinter   = LsblkType("printer")
    LsblkTypeProcessor = LsblkType("processor")
    LsblkTypeWorm      = LsblkType("worm")
    LsblkTypeRom       = LsblkType("rom")
    LsblkTypeScanner   = LsblkType("scanner")
    LsblkTypeMoDisk    = LsblkType("mo-disk")
    LsblkTypeChanger   = LsblkType("changer")
    LsblkTypeComm      = LsblkType("comm")
    LsblkTypeRaid      = LsblkType("raid")
    LsblkTypeEnclosure = LsblkType("enclosure")
    LsblkTypeRbc       = LsblkType("rbc")
    LsblkTypeOsd       = LsblkType("osd")
    LsblkTypeNoLun     = LsblkType("no-lun")
)


А после, напишем функцию, которая возвращает данные о наших разделах:

// LsblkPartitions returns a list of partitons on a specified VD
// This would return only information about partitions.
func LsblkPartitions(virtualDiskID string) ([]LsblkPart, error) {
    out, err := ExecCommString("lsblk", Params("/dev/mapper/%v %v", virtualDiskID, "-blJ")...)
    if err != nil {
        return nil, fmt.Errorf("can't extract virtual drive partition info. %w", err)
    }

    var p lsbklOutput

    err = json.Unmarshal([]byte(out), &p)
    if err != nil {
        return nil, fmt.Errorf("can't unmarshall lsblk output %w", err)
    }

    var ret []LsblkPart

    for _, p := range p.Blockdevices {
        if p.Type == LsblkTypePart {
            ret = append(ret, p)
        }
    }

    return ret, nil
}


Копипастерам на заметку — ExecCommString — это функция, которая выполняет команду в системе и возвращает ответ в виде строки. Params — это ещё одна функция, которая занимается возвращением параметров для ExecCommString, так что, если хотите, перепишите их сами.

Итак, теперь мы готовы в golang получить данные о разделах на нашем жёстком диске. Теперь у нас есть список разделов и правильных путей к этим разделам. Ещё раз напоминаю, что диски у нас на LVM, поэтому подключаемся к ним через /dev/mapper/storage/.

Теперь мы можем подключить раздел с EFI путём запуска Mount.

err = system.ExecCommNoreturn("mount", system.Params("%v %v", "/dev/mapper/"+vdPartData[0].Name, "/mnt/efi-"+diskId)...)


После чего, путём гигантского количества кода на golang мы можем скопировать все файлы из ./EFI/deiban в ./EFI/BOOT и заменить shimx64.efi на BOOTx64.efi.

Ну что же, теперь наша система грузится на ванильном QEMU.

▍ Доктор дископрав


e4ptftgjkgvimd8zj89dwik1il8.png
Картина: Новый разработчик пытается убедить тимлида, что править диски через консоль на продакшене это — комильфо

Пора поправить сам диск. На самом деле, на этом этапе вам можно сделать дополнительные операции (например, поменять hostname, и записать нужные ключи и так далее в файловую систему на машине.

После этого вы можете править диск.

В чём проблема?

Изначальный образ нашей системы был установлен на диск с 5 гигабайтами пространства. В этом случае такой образ можно записать почти на любой жёсткий диск, а потом растянуть его руками, чтобы главный раздел занимал всё пространство нового диска.

Давайте править новую файловую систему.

Первым делом, размонтируйте всё, что намонтировали в предыдущем разделе.

Для начала запустим sgdisk. Эта замечательная утилита меняет GPT раздел и растягивает весь раздел на целый диск. Но это не файловая система, это просто разметка диска.

err = system.ExecCommNoreturn("sgdisk", system.Params("-e %v", system.VirtualDiskPath(diskId))...)


Следующая команда удаляет второй раздел с диска:

err = system.ExecCommNoreturn("sgdisk", system.Params("-d 2 %v", system.VirtualDiskPath(diskId))...)


А после этого, мы пересоздаём этот второй раздел, но на этот раз говорим, что он будет занимать весь существующий диск.

err = system.ExecCommNoreturn("sgdisk", system.Params("-N 2 %v", system.VirtualDiskPath(diskId))...)


Хорошо. Для продолжения нам снова нужно сделать partprobe, чтобы убедиться, что Linux понял, что диск стал толще и жирнее.

▍ Доктор Айболит, который лечит файловую систему


bago6n5glre6mer7xwbhewnmxyy.png
Картина: Системный администратор получил по заслугам после изменения разметки диска с базой данных

Ну вот, сам диск теперь размечен правильно, осталось подтянуть файловую систему и «растянуть» её на всё оставшееся место на диске.

Теперь давайте запустим e2fsck.

err = system.ExecCommNoreturn("e2fsck", system.Params("-f -y /dev/mapper/%v", vdPartData[1].Name)...)


На всякий пожарный мы проверим, если у нас были проблемы с файловой системой. На моём опыте могу сказать, что ни разу не видел никаких проблем на этом этапе, и его можно запросто пропустить. Хотя, естественно, пропускать не рекомендуется.

Ну и, наконец-то, можно растянуть саму файловую систему, чтобы она соответствовала размеру нового раздела на диске.

err = system.ExecCommNoreturn("resize2fs", system.Params("/dev/mapper/%v", vdPartData[1].Name)...)


▍ Ну вот и всё!


fhqbpwzwhwvgl8ibvmfodmu2t_c.png
Картина: Системный администратор, которому удалось восстановить повреждённый диск с файлами пользователей

Слава богу, всё в порядке! Теперь Вася может собрать эти сниппеты в нормальную программу и будет в состоянии автоматически клонировать диски на своих ноутбуках. ЦОД можно поднимать, и он будет работать быстро и надёжно. В следующей статье мы поговорим о способах, которыми можно запитать ЦОД на необитаемом острове.

А всем моим читателям рекомендую распечатать эту инструкцию на листочке бумаги, и в случае, если вы окажетесь на необитаемом острове, вы сможете спокойно клонировать диски своими руками.

Конкурс статей от RUVDS.COM. Три денежные номинации. Главный приз — 100 000 рублей.

sz7jpfj8i1pa6ocj-eia09dev4q.png

© Habrahabr.ru