<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: metis atelier</title>
    <description>The latest articles on DEV Community by metis atelier (@metis_atelier_studio).</description>
    <link>https://dev.to/metis_atelier_studio</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4078619%2F993238ec-d73d-4ccc-831c-1bb6e65cebe3.png</url>
      <title>DEV Community: metis atelier</title>
      <link>https://dev.to/metis_atelier_studio</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/metis_atelier_studio"/>
    <language>en</language>
    <item>
      <title>Home Server Recovery and a Weekly Clone: Blog Post and Reproduction Runbook</title>
      <dc:creator>metis atelier</dc:creator>
      <pubDate>Tue, 03 Nov 2026 13:03:50 +0000</pubDate>
      <link>https://dev.to/metis_atelier_studio/home-server-recovery-and-a-weekly-clone-blog-post-and-reproduction-runbook-2jh8</link>
      <guid>https://dev.to/metis_atelier_studio/home-server-recovery-and-a-weekly-clone-blog-post-and-reproduction-runbook-2jh8</guid>
      <description>&lt;p&gt;This file contains:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Part 1: the blog post in English&lt;/li&gt;
&lt;li&gt;Part 2: the blog post in Japanese&lt;/li&gt;
&lt;li&gt;Part 3: a reproduction runbook, with placeholders, that you can follow together with an AI assistant&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Replace every &lt;code&gt;&amp;lt;PLACEHOLDER&amp;gt;&lt;/code&gt; with your own values. Never paste secrets (passwords, API keys, tokens) into a chat.&lt;/p&gt;




&lt;h1&gt;
  
  
  Part 1 (English)
&lt;/h1&gt;

&lt;h2&gt;
  
  
  From a hard disk to an SSD: a bootable spare and a weekly clone on a dual-GPU home server
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Background
&lt;/h3&gt;

&lt;p&gt;This home server has two NVIDIA GPUs in one machine. An RTX 3060 runs local LLM inference: a 12B quantized model served by llama.cpp as a systemd service. An RTX 3070 mines while the machine is idle. The system originally ran from a single 500 GB SATA hard disk, which held the OS, the models, and all service data.&lt;/p&gt;

&lt;p&gt;The project had two goals:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Performance:&lt;/strong&gt; shorten boot time, service start-up time, and model load time, which I expected to be limited by random I/O on the hard disk.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Availability:&lt;/strong&gt; stop depending on a single disk, and keep a second disk that can boot the system on its own.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This post covers the migration, the update that exposed the lack of a fallback, and the weekly clone that now keeps the spare current.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Moving the root filesystem from the hard disk to NVMe
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Why the hard disk was expected to be the bottleneck.&lt;/strong&gt; The services on this machine issue many small reads: Python imports, shared libraries, systemd units, and the model file. On a mechanical disk, those reads are bound by seek latency. NVMe removes most of that latency.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Target layout.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Disk&lt;/th&gt;
&lt;th&gt;Role after migration&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;NVMe SSD (primary)&lt;/td&gt;
&lt;td&gt;EFI system partition and root filesystem; boots by default&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SATA hard disk (spare)&lt;/td&gt;
&lt;td&gt;Previous system kept as a cold fallback; mounted read-only with &lt;code&gt;nofail&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;What had to change.&lt;/strong&gt; Copying files is not enough to make a system boot. Four things decide it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Every entry in &lt;code&gt;/etc/fstab&lt;/code&gt; must use a UUID, not a device name such as &lt;code&gt;/dev/sda2&lt;/code&gt;. Device names can change when disks are added or removed.&lt;/li&gt;
&lt;li&gt;The EFI partition must live on the new disk, and its boot entry must point at the new loader.&lt;/li&gt;
&lt;li&gt;GRUB's configuration must reference the new root UUID. A copied configuration still points at the old one.&lt;/li&gt;
&lt;li&gt;The old disk must not be mounted writable at boot, so that no boot writes to it by accident.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Verify before rebooting.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;findmnt &lt;span class="nt"&gt;-no&lt;/span&gt; SOURCE /     &lt;span class="c"&gt;# which device is the running root&lt;/span&gt;
lsblk &lt;span class="nt"&gt;-f&lt;/span&gt;                 &lt;span class="c"&gt;# filesystems, labels, and UUIDs&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;blkid               &lt;span class="c"&gt;# confirm each UUID&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; /etc/fstab           &lt;span class="c"&gt;# every entry should be UUID-based, or intentionally absent&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep the old disk attached but unmounted until the first successful boot from the new disk.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Results.&lt;/strong&gt; I did not record benchmarks before or after the migration, so this post makes no numeric claims about speed. Subjectively, the improvement was noticeable for only a few days before it became the new normal. The lesson: if performance is the goal, record a baseline before the change, not after it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gotcha.&lt;/strong&gt; The old disk still held a complete, valid system. If its old UUID stayed in &lt;code&gt;fstab&lt;/code&gt; as a writable mount, the next boot would try to mount it. Remove the entry, or mark it &lt;code&gt;nofail&lt;/code&gt; and read-only.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why the spare mattered
&lt;/h3&gt;

&lt;p&gt;A routine system update went wrong partway through, and the primary disk could no longer be trusted to keep running as before. The spare was not bootable, so I had no working fallback. Closing that gap is the point of this post.&lt;/p&gt;

&lt;h3&gt;
  
  
  Making the spare disk bootable
&lt;/h3&gt;

&lt;p&gt;I wanted a second system that could boot on its own, so I formatted the spare disk's root partition and copied the running system to it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Back up the important configuration files.&lt;/li&gt;
&lt;li&gt;Format the spare root partition with &lt;code&gt;mkfs.ext4&lt;/code&gt;. Formatting avoids old files mixing into the new copy.&lt;/li&gt;
&lt;li&gt;Copy the root filesystem with &lt;code&gt;rsync -aHAXx --numeric-ids&lt;/code&gt;. The &lt;code&gt;-x&lt;/code&gt; flag keeps the EFI partition out of the root copy.&lt;/li&gt;
&lt;li&gt;Rewrite the copy's &lt;code&gt;/etc/fstab&lt;/code&gt; with the spare disk's own UUIDs, and remove any entry for a mount that no longer exists.&lt;/li&gt;
&lt;li&gt;Copy the EFI contents, then reinstall GRUB inside a chroot with &lt;code&gt;--no-nvram&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Two gotchas came up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Copied EFI loader stubs hard-code the root UUID.&lt;/strong&gt; The copied &lt;code&gt;grub.cfg&lt;/code&gt; still pointed at the original disk, so selecting the spare disk booted the original system. Every stub the firmware can load must point at the spare disk's root.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;grub-install&lt;/code&gt; writes to NVRAM by default.&lt;/strong&gt; When two disks share a boot label, that can repoint the original disk's firmware entry. &lt;code&gt;--no-nvram&lt;/code&gt; leaves firmware entries alone, and boot order is set in the firmware setup instead.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Booting the spare disk from the firmware boot menu brought up the cloned system. Check every loader path, not just the one you expect.&lt;/p&gt;

&lt;h3&gt;
  
  
  A weekly one-way clone with interlocks
&lt;/h3&gt;

&lt;p&gt;A one-time clone protects against nothing that happens after it. So I added a weekly job that mirrors the running primary root to the spare root:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Schedule:&lt;/strong&gt; a systemd timer every Friday at 02:00, with &lt;code&gt;Persistent=true&lt;/code&gt; so a missed run happens after the next boot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Direction:&lt;/strong&gt; one way only, primary to spare.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interlock 1:&lt;/strong&gt; the job runs only when the machine is booted from the primary disk. If it is booted from the spare, it logs a skip and writes nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interlock 2:&lt;/strong&gt; the destination must match the expected UUID, must not be the running root, and must not already be mounted. Otherwise it refuses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sync:&lt;/strong&gt; &lt;code&gt;rsync -aHAXx --numeric-ids --delete&lt;/code&gt;, excluding virtual filesystems and the EFI partition.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fixups:&lt;/strong&gt; after the sync, the spare's fstab is rewritten from a template, and the primary's UUID is replaced in the spare's &lt;code&gt;grub.cfg&lt;/code&gt;. The job fails if any primary UUID remains.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alerts:&lt;/strong&gt; failures send an email through &lt;code&gt;msmtp&lt;/code&gt; with an app password stored in a root-only config file.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The clone is a single generation. A bad change on the primary reaches the spare on the next run. This is deliberate: the clone guarantees a bootable copy of the latest state, not a history.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lessons
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A fallback only counts once you have booted it.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloned boot loaders are not portable.&lt;/strong&gt; Rewrite every UUID the firmware might load, and avoid touching firmware boot entries when installing a bootloader for a clone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Put interlocks in the script.&lt;/strong&gt; A job that writes to a disk should check which disk it is running from, and refuse if the answer is wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep the old kernel as a rollback path&lt;/strong&gt; until the new one has run for a while.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A mirror is not a backup.&lt;/strong&gt; If you need history, plan it separately.&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  Part 2 (日本語)
&lt;/h1&gt;

&lt;h2&gt;
  
  
  ハードディスクからSSDへの換装、起動できる予備ディスク、そして毎週のクローン
&lt;/h2&gt;

&lt;h3&gt;
  
  
  背景
&lt;/h3&gt;

&lt;p&gt;このマシンには NVIDIA GPU を 2 枚載せている。RTX 3060 はローカル LLM の推論に使う。12B の量子化モデルを llama.cpp で動かし、systemd のサービスとして提供している。RTX 3070 は、アイドル時にマイニングへ回す。当初は 500GB の SATA ハードディスク 1 台で動かしていて、OS もモデルもサービスのデータもすべてそこに載っていた。&lt;/p&gt;

&lt;p&gt;目的は二つだった。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;性能&lt;/strong&gt;：起動時間、サービスの立ち上がり時間、モデルの読み込み時間を短くすること。ハードディスクのランダム I/O がボトルネックになると想定していた。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;可用性&lt;/strong&gt;：ディスク 1 台に依存する状態をやめること。単独で起動できる予備ディスクを持つこと。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;この記事では、換装、その後に起きた更新中の障害とフォールバックの欠如、そして予備を最新に保つための毎週のクローンについて書く。&lt;/p&gt;

&lt;h3&gt;
  
  
  手順 1：ハードディスクから NVMe SSD への root の移行
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;ボトルネックと想定した理由。&lt;/strong&gt; このマシンのサービスは、Python の import、共有ライブラリ、systemd のユニット、モデルファイルなど、小さな読み込みを大量に行う。機械式ディスクでは、これらがシーク時間に左右される。NVMe にすれば、そのレイテンシは大きく減るはずだった。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;換装後の構成。&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;ディスク&lt;/th&gt;
&lt;th&gt;換装後の役割&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;NVMe SSD（主）&lt;/td&gt;
&lt;td&gt;EFI パーティションと root。既定で起動する&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SATA ハードディスク（予備）&lt;/td&gt;
&lt;td&gt;旧システムを冷たい予備として保持。読み取り専用・&lt;code&gt;nofail&lt;/code&gt; でマウント&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;変更が必要だったこと。&lt;/strong&gt; ファイルをコピーしただけでは、起動できるかどうかは決まらない。起動を左右するのは次の四点だ。&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;/etc/fstab&lt;/code&gt; のすべてのエントリを、&lt;code&gt;/dev/sda2&lt;/code&gt; のようなデバイス名ではなく UUID で指定する。デバイス名は、ディスクを追加・取り外しすると変わることがある。&lt;/li&gt;
&lt;li&gt;EFI パーティションを新しいディスクに置き、起動エントリが新しいローダーを指すようにする。&lt;/li&gt;
&lt;li&gt;GRUB の設定が新しい root の UUID を参照していること。コピーした設定は、古い UUID を指したままになる。&lt;/li&gt;
&lt;li&gt;古いディスクが起動時に書き込み可能な状態でマウントされないようにする。起動のたびに、誤って書き込まれるのを防ぐためだ。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;再起動前の確認。&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;findmnt &lt;span class="nt"&gt;-no&lt;/span&gt; SOURCE /     &lt;span class="c"&gt;# 実行中の root のデバイス&lt;/span&gt;
lsblk &lt;span class="nt"&gt;-f&lt;/span&gt;                 &lt;span class="c"&gt;# ファイルシステム、ラベル、UUID&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;blkid               &lt;span class="c"&gt;# 各 UUID の確認&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; /etc/fstab           &lt;span class="c"&gt;# すべて UUID 指定、または意図的に無いこと&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;新しいディスクから最初に起動が成功するまで、古いディスクは接続したままにして、マウントはしない。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;結果。&lt;/strong&gt; 換装前後のベンチマークは記録していない。したがって、この記事では速度について数値による主張はしない。体感の改善は数日で薄れ、以降は当たり前の状態になった。性能が目的なら、変更の前にベースラインを記録しておくこと。後から測っても、比べられない。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;注意点。&lt;/strong&gt; 古いディスクには、動く完全なシステムが残っている。古い UUID のまま書き込み可能なマウントとして fstab に残すと、次の起動でそれをマウントしようとする。エントリを削除するか、&lt;code&gt;nofail&lt;/code&gt; と読み取り専用にすること。&lt;/p&gt;

&lt;h3&gt;
  
  
  予備ディスクが必要になった理由
&lt;/h3&gt;

&lt;p&gt;更新の途中で、主ディスクをこれまでどおりには信用できなくなった。予備ディスクは起動できない状態だったから、フォールバックがなかった。このギャップを埋めるのが、この記事の目的だ。&lt;/p&gt;

&lt;h3&gt;
  
  
  予備ディスクを起動できるようにする
&lt;/h3&gt;

&lt;p&gt;単独で起動できる第二のシステムが必要だったので、予備ディスクの root パーティションをフォーマットし、動いているシステムを丸ごとコピーした。&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;重要な設定ファイルを退避する。&lt;/li&gt;
&lt;li&gt;予備の root パーティションを &lt;code&gt;mkfs.ext4&lt;/code&gt; でフォーマットする。古いファイルが混ざらないように、新規に作る。&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;rsync -aHAXx --numeric-ids&lt;/code&gt; で root をコピーする。&lt;code&gt;-x&lt;/code&gt; によって、EFI パーティションはコピーから外れる。&lt;/li&gt;
&lt;li&gt;コピー先の &lt;code&gt;/etc/fstab&lt;/code&gt; を、予備ディスク自身の UUID で書き直す。存在しないマウントの行は削除する。&lt;/li&gt;
&lt;li&gt;EFI の中身をコピーし、chroot 内で &lt;code&gt;--no-nvram&lt;/code&gt; を付けて GRUB を入れ直す。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;ここで二つの落とし穴があった。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;コピーした EFI の stub は、root の UUID を埋め込んでいる。&lt;/strong&gt; コピーした &lt;code&gt;grub.cfg&lt;/code&gt; が元のディスクを指したままだったため、予備ディスクを選んでも元のシステムが起動した。ファームウェアが読み込みうる stub は、すべて予備ディスクの root を指す必要がある。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;grub-install&lt;/code&gt; は、デフォルトで NVRAM に書き込む。&lt;/strong&gt; 二つのディスクが同じラベルを持つと、元のディスクのファームウェアの起動エントリを書き換えてしまうことがある。&lt;code&gt;--no-nvram&lt;/code&gt; を付ければ、ファームウェアの起動エントリには触らずに済む。起動順はファームウェアの設定画面で決める。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;ファームウェアの起動メニューから予備ディスクを選ぶと、クローンしたシステムが立ち上がった。期待している経路だけでなく、すべての読み込み経路を確認すること。ここを省くと、あとで必ず痛い目を見る。&lt;/p&gt;

&lt;h3&gt;
  
  
  インターロック付きの毎週の一方向クローン
&lt;/h3&gt;

&lt;p&gt;一度きりのクローンは、そのあとの変化を守ってくれない。そこで、動いている主ディスクの root を予備に毎週ミラーするジョブを追加した。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;実行タイミング：&lt;/strong&gt; systemd タイマーで毎週金曜 02:00。&lt;code&gt;Persistent=true&lt;/code&gt; にしてあるので、実行を逃したときは次の起動後に実行される。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;方向：&lt;/strong&gt; 主ディスクから予備への一方通行。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;インターロック 1：&lt;/strong&gt; 主ディスクで起動しているときだけ実行する。予備ディスクで起動しているときは、skip と記録するだけで、何も書き込まない。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;インターロック 2：&lt;/strong&gt; 同期先は、期待する UUID と一致し、実行中の root ではなく、マウントされていないことを確認する。一つでも違えば拒否する。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;同期：&lt;/strong&gt; &lt;code&gt;rsync -aHAXx --numeric-ids --delete&lt;/code&gt;。仮想ファイルシステムと EFI パーティションは除外する。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;後処理：&lt;/strong&gt; 予備側の fstab をテンプレートから書き直し、予備側の &lt;code&gt;grub.cfg&lt;/code&gt; の主ディスクの UUID を置き換える。主ディスクの UUID が残っていたら、失敗として止まる。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;通知：&lt;/strong&gt; 失敗したら &lt;code&gt;msmtp&lt;/code&gt; でメールを送る。アプリパスワードは root だけが読める設定ファイルに置く。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;クローンは世代を持たないので、主ディスクで壊れた変更は次の同期で予備にも反映される。これは意図的なトレードオフだ。クローンが保証するのは「最新の状態を起動できるコピー」であって、履歴ではない。&lt;/p&gt;

&lt;h3&gt;
  
  
  学んだこと
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;実際に起動したことがあるものだけが、フォールバックになる。&lt;/li&gt;
&lt;li&gt;クローンした起動ローダーは、そのままでは移せない。ファームウェアが読み込みうる UUID をすべて書き換えること。&lt;/li&gt;
&lt;li&gt;インターロックはスクリプトの中に入れる。ディスクに書き込むジョブは、いま自分がどのディスクから動いているかを確認し、おかしければ拒否するべきだ。&lt;/li&gt;
&lt;li&gt;性能は、変更の前に測る。&lt;/li&gt;
&lt;li&gt;古いカーネルは、新しいものが十分に動くまで残す。&lt;/li&gt;
&lt;li&gt;ミラーはバックアップではない。履歴が必要なら、別に計画する。&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  Part 3: Reproduction runbook
&lt;/h1&gt;

&lt;p&gt;This runbook is written so that you can follow it with an AI assistant. Give the assistant the whole file, then tell it what your disks are.&lt;/p&gt;

&lt;h2&gt;
  
  
  0. How to work with the AI assistant
&lt;/h2&gt;

&lt;p&gt;Paste this at the start of the session, filling in the placeholders:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;We are setting up a weekly one-way clone from my primary root disk (&amp;lt;PRIMARY_ROOT_DEVICE&amp;gt;, UUID &amp;lt;PRIMARY_ROOT_UUID&amp;gt;) to a spare disk (&amp;lt;SPARE_DEVICE&amp;gt;, root partition UUID &amp;lt;SPARE_ROOT_UUID&amp;gt;, EFI UUID &amp;lt;SPARE_EFI_UUID&amp;gt;).

Rules:
- Use read-only commands first and show me the output.
- Before any command that writes to a disk (mkfs, mount with write, rsync --delete, grub-install), show me the exact command and wait for my "go".
- Back up every file before editing it.
- Never print secrets. I will type passwords myself.
- Verify the target device by UUID and size before every destructive step.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  1. Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A primary disk that holds the running root (NVMe or SATA).&lt;/li&gt;
&lt;li&gt;A spare disk with at least one partition large enough for the root filesystem, and a separate EFI partition.&lt;/li&gt;
&lt;li&gt;Packages: &lt;code&gt;rsync&lt;/code&gt;, &lt;code&gt;msmtp&lt;/code&gt;, &lt;code&gt;grub-efi-amd64&lt;/code&gt; (or the distro equivalent).&lt;/li&gt;
&lt;li&gt;A working email account with an app password (never put the password in a shell history or a chat).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  2. Identify the disks (read-only)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;findmnt &lt;span class="nt"&gt;-no&lt;/span&gt; SOURCE /
lsblk &lt;span class="nt"&gt;-f&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;blkid
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Write down:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the primary root UUID (&lt;code&gt;&amp;lt;PRIMARY_ROOT_UUID&amp;gt;&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;the spare root partition and its UUID after formatting (&lt;code&gt;&amp;lt;SPARE_ROOT_UUID&amp;gt;&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;the spare EFI UUID (&lt;code&gt;&amp;lt;SPARE_EFI_UUID&amp;gt;&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. Back up before changing anything
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/backup-&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%Y%m%d&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;sudo cp&lt;/span&gt; /etc/fstab ~/backup-&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%Y%m%d&lt;span class="si"&gt;)&lt;/span&gt;/fstab.bak
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  4. Format the spare root partition (destructive)
&lt;/h2&gt;

&lt;p&gt;Confirm the device first. The UUID must be the spare's old root, not the primary's.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;umount /path/to/spare/root 2&amp;gt;/dev/null
&lt;span class="nb"&gt;sudo &lt;/span&gt;mkfs.ext4 &lt;span class="nt"&gt;-L&lt;/span&gt; root /dev/&amp;lt;SPARE_ROOT_PARTITION&amp;gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;blkid /dev/&amp;lt;SPARE_ROOT_PARTITION&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  5. Copy the running root
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /mnt/clone
&lt;span class="nb"&gt;sudo &lt;/span&gt;mount /dev/&amp;lt;SPARE_ROOT_PARTITION&amp;gt; /mnt/clone
&lt;span class="nb"&gt;sudo &lt;/span&gt;rsync &lt;span class="nt"&gt;-aHAXx&lt;/span&gt; &lt;span class="nt"&gt;--numeric-ids&lt;/span&gt; &lt;span class="nt"&gt;--stats&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--exclude&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/proc/&lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="nt"&gt;--exclude&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/sys/&lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="nt"&gt;--exclude&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/dev/&lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="nt"&gt;--exclude&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/run/&lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--exclude&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/tmp/&lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="nt"&gt;--exclude&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/var/tmp/&lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="nt"&gt;--exclude&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/mnt/&lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="nt"&gt;--exclude&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/media/&lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--exclude&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/lost+found &lt;span class="nt"&gt;--exclude&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/boot/efi/&lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  / /mnt/clone/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;rsync&lt;/code&gt; exit code 24 (files vanished during transfer) is normal on a live system. Any other non-zero code is a failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Rewrite the copy's fstab and EFI loader
&lt;/h2&gt;

&lt;p&gt;Write a fstab for the spare disk, using its own UUIDs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo tee&lt;/span&gt; /mnt/clone/etc/fstab &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
UUID=&amp;lt;SPARE_ROOT_UUID&amp;gt;    /          ext4  defaults  0  1
UUID=&amp;lt;SPARE_EFI_UUID&amp;gt;     /boot/efi  vfat  defaults  0  1
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mount the spare EFI, then reinstall GRUB inside a chroot. &lt;code&gt;--no-nvram&lt;/code&gt; is important.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;mount /dev/&amp;lt;SPARE_EFI_PARTITION&amp;gt; /mnt/clone/boot/efi
&lt;span class="k"&gt;for &lt;/span&gt;d &lt;span class="k"&gt;in &lt;/span&gt;dev proc sys run&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;mount &lt;span class="nt"&gt;--bind&lt;/span&gt; /&lt;span class="nv"&gt;$d&lt;/span&gt; /mnt/clone/&lt;span class="nv"&gt;$d&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done
&lt;/span&gt;&lt;span class="nb"&gt;sudo chroot&lt;/span&gt; /mnt/clone grub-install &lt;span class="nt"&gt;--target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;x86_64-efi &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--efi-directory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/boot/efi &lt;span class="nt"&gt;--bootloader-id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;spare &lt;span class="nt"&gt;--no-nvram&lt;/span&gt; &lt;span class="nt"&gt;--recheck&lt;/span&gt;
&lt;span class="nb"&gt;sudo chroot&lt;/span&gt; /mnt/clone update-grub
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify that every loader stub points at the spare root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo grep&lt;/span&gt; &lt;span class="nt"&gt;-rH&lt;/span&gt; &lt;span class="s2"&gt;"search.fs_uuid&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;UUID="&lt;/span&gt; /mnt/clone/boot/efi/EFI/ /mnt/clone/boot/grub/grub.cfg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If any stub still shows the primary UUID, fix it with &lt;code&gt;sed&lt;/code&gt; (after backing it up) and check again.&lt;/p&gt;

&lt;p&gt;Clean up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;d &lt;span class="k"&gt;in &lt;/span&gt;run sys proc dev&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;umount /mnt/clone/&lt;span class="nv"&gt;$d&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done
&lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;umount /mnt/clone/boot/efi /mnt/clone
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  7. Boot test
&lt;/h2&gt;

&lt;p&gt;Shut down. Use the firmware boot menu to choose the spare disk's entry. Do not change the firmware boot order yet. After boot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;findmnt &lt;span class="nt"&gt;-no&lt;/span&gt; SOURCE /     &lt;span class="c"&gt;# should show the spare root partition&lt;/span&gt;
&lt;span class="nb"&gt;uname&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  8. Install the weekly job
&lt;/h2&gt;

&lt;p&gt;Install &lt;code&gt;msmtp&lt;/code&gt; and write &lt;code&gt;/etc/msmtprc&lt;/code&gt; with the app password (edit it yourself):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/msmtprc &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
defaults
auth on
tls on
tls_starttls on
tls_trust_file /etc/ssl/certs/ca-certificates.crt
logfile /var/log/msmtp.log

account gmail
host smtp.gmail.com
port 587
from &amp;lt;YOUR_ADDRESS&amp;gt;
user &amp;lt;YOUR_ADDRESS&amp;gt;
password "PASTE_APP_PASSWORD_HERE"

account default : gmail
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;span class="nb"&gt;sudo chown &lt;/span&gt;root:root /etc/msmtprc &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo chmod &lt;/span&gt;600 /etc/msmtprc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Write the fstab template for the spare disk:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo install&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; 755 /etc/clone-job
&lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/clone-job/fstab.spare &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
UUID=&amp;lt;SPARE_ROOT_UUID&amp;gt;    /          ext4  defaults  0  1
UUID=&amp;lt;SPARE_EFI_UUID&amp;gt;     /boot/efi  vfat  defaults  0  1
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Write the sync script. It runs only from the primary disk and refuses to write anywhere else.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo tee&lt;/span&gt; /usr/local/sbin/clone-job.sh &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
#!/bin/bash
# One-way weekly clone: primary root -&amp;gt; spare root. Does nothing when booted from the spare.
set -uo pipefail

SRC_UUID=&amp;lt;PRIMARY_ROOT_UUID&amp;gt;
DST_UUID=&amp;lt;SPARE_ROOT_UUID&amp;gt;
FSTAB_TEMPLATE=/etc/clone-job/fstab.spare
MAIL_TO=&amp;lt;YOUR_ADDRESS&amp;gt;
MNT=/mnt/clone-job-dst
LOCK=/run/clone-job.lock
DRYRUN=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DRYRUN&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;0&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;

log() { logger -t clone-job -- "&lt;/span&gt;&lt;span class="nv"&gt;$*&lt;/span&gt;&lt;span class="sh"&gt;"; echo "&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="s1"&gt;'+%F %T'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="sh"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$*&lt;/span&gt;&lt;span class="sh"&gt;"; }
cleanup() { mountpoint -q "&lt;/span&gt;&lt;span class="nv"&gt;$MNT&lt;/span&gt;&lt;span class="sh"&gt;" &amp;amp;&amp;amp; umount "&lt;/span&gt;&lt;span class="nv"&gt;$MNT&lt;/span&gt;&lt;span class="sh"&gt;"; }
trap cleanup EXIT

fail() {
  log "FAILED: &lt;/span&gt;&lt;span class="nv"&gt;$*&lt;/span&gt;&lt;span class="sh"&gt;"
  printf 'To: %s&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;Subject: [clone-job] FAILED on %s&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&gt;%s&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;%s&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;' &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="sh"&gt;
    "&lt;/span&gt;&lt;span class="nv"&gt;$MAIL_TO&lt;/span&gt;&lt;span class="sh"&gt;" "&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;hostname&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;" "&lt;/span&gt;&lt;span class="nv"&gt;$*&lt;/span&gt;&lt;span class="sh"&gt;" "&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;" | msmtp "&lt;/span&gt;&lt;span class="nv"&gt;$MAIL_TO&lt;/span&gt;&lt;span class="sh"&gt;" &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="sh"&gt;
    || log "could not send failure mail"
  exit 1
}

exec 9&amp;gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOCK&lt;/span&gt;&lt;span class="sh"&gt;"
flock -n 9 || { log "skip: another run is in progress"; exit 0; }

# Interlock 1: only run when booted from the primary disk
ROOT_SRC=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;findmnt &lt;span class="nt"&gt;-no&lt;/span&gt; SOURCE /&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;
ROOT_UUID=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;blkid &lt;span class="nt"&gt;-s&lt;/span&gt; UUID &lt;span class="nt"&gt;-o&lt;/span&gt; value &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ROOT_SRC&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;
if [ "&lt;/span&gt;&lt;span class="nv"&gt;$ROOT_UUID&lt;/span&gt;&lt;span class="sh"&gt;" != "&lt;/span&gt;&lt;span class="nv"&gt;$SRC_UUID&lt;/span&gt;&lt;span class="sh"&gt;" ]; then
  log "skip: booted from &lt;/span&gt;&lt;span class="nv"&gt;$ROOT_SRC&lt;/span&gt;&lt;span class="sh"&gt;, not the primary disk. Nothing done."
  exit 0
fi

# Interlock 2: destination must be the spare root, not the running root, and not mounted
DST_PART=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;readlink&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"/dev/disk/by-uuid/&lt;/span&gt;&lt;span class="nv"&gt;$DST_UUID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;
[ -n "&lt;/span&gt;&lt;span class="nv"&gt;$DST_PART&lt;/span&gt;&lt;span class="sh"&gt;" ] &amp;amp;&amp;amp; [ -b "&lt;/span&gt;&lt;span class="nv"&gt;$DST_PART&lt;/span&gt;&lt;span class="sh"&gt;" ] || fail "spare root (&lt;/span&gt;&lt;span class="nv"&gt;$DST_UUID&lt;/span&gt;&lt;span class="sh"&gt;) not found"
[ "&lt;/span&gt;&lt;span class="nv"&gt;$DST_PART&lt;/span&gt;&lt;span class="sh"&gt;" != "&lt;/span&gt;&lt;span class="nv"&gt;$ROOT_SRC&lt;/span&gt;&lt;span class="sh"&gt;" ] || fail "destination equals the running root; refusing"
[ "&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;blkid &lt;span class="nt"&gt;-s&lt;/span&gt; UUID &lt;span class="nt"&gt;-o&lt;/span&gt; value &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DST_PART&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;" = "&lt;/span&gt;&lt;span class="nv"&gt;$DST_UUID&lt;/span&gt;&lt;span class="sh"&gt;" ] || fail "destination UUID mismatch"
findmnt -S "&lt;/span&gt;&lt;span class="nv"&gt;$DST_PART&lt;/span&gt;&lt;span class="sh"&gt;" &amp;gt;/dev/null &amp;amp;&amp;amp; fail "destination is already mounted; refusing"
[ -f "&lt;/span&gt;&lt;span class="nv"&gt;$FSTAB_TEMPLATE&lt;/span&gt;&lt;span class="sh"&gt;" ] || fail "fstab template missing"

mkdir -p "&lt;/span&gt;&lt;span class="nv"&gt;$MNT&lt;/span&gt;&lt;span class="sh"&gt;"
mount -o noatime "&lt;/span&gt;&lt;span class="nv"&gt;$DST_PART&lt;/span&gt;&lt;span class="sh"&gt;" "&lt;/span&gt;&lt;span class="nv"&gt;$MNT&lt;/span&gt;&lt;span class="sh"&gt;" || fail "mount failed"

RSYNC_OPTS=(-aHAXx --numeric-ids --delete --stats)
[ "&lt;/span&gt;&lt;span class="nv"&gt;$DRYRUN&lt;/span&gt;&lt;span class="sh"&gt;" = 1 ] &amp;amp;&amp;amp; RSYNC_OPTS+=(-n)
EXCLUDES=(
  --exclude=/proc/* --exclude=/sys/* --exclude=/dev/* --exclude=/run/*
  --exclude=/tmp/* --exclude=/var/tmp/* --exclude=/mnt/* --exclude=/media/*
  --exclude=/lost+found --exclude=/srv/backup-hdd/* --exclude=/boot/efi/*
)
rsync "&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;RSYNC_OPTS&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;" "&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;EXCLUDES&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;" / "&lt;/span&gt;&lt;span class="nv"&gt;$MNT&lt;/span&gt;&lt;span class="sh"&gt;"/
rc=&lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;&lt;span class="sh"&gt;
if [ "&lt;/span&gt;&lt;span class="nv"&gt;$rc&lt;/span&gt;&lt;span class="sh"&gt;" -ne 0 ] &amp;amp;&amp;amp; [ "&lt;/span&gt;&lt;span class="nv"&gt;$rc&lt;/span&gt;&lt;span class="sh"&gt;" -ne 24 ]; then fail "rsync exited with code &lt;/span&gt;&lt;span class="nv"&gt;$rc&lt;/span&gt;&lt;span class="sh"&gt;"; fi

if [ "&lt;/span&gt;&lt;span class="nv"&gt;$DRYRUN&lt;/span&gt;&lt;span class="sh"&gt;" = 1 ]; then log "dry run finished (nothing written)"; exit 0; fi

# Spare-specific fixups
install -m 644 "&lt;/span&gt;&lt;span class="nv"&gt;$FSTAB_TEMPLATE&lt;/span&gt;&lt;span class="sh"&gt;" "&lt;/span&gt;&lt;span class="nv"&gt;$MNT&lt;/span&gt;&lt;span class="sh"&gt;/etc/fstab" || fail "could not write fstab"
sed -i "s/&lt;/span&gt;&lt;span class="nv"&gt;$SRC_UUID&lt;/span&gt;&lt;span class="sh"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;$DST_UUID&lt;/span&gt;&lt;span class="sh"&gt;/g" "&lt;/span&gt;&lt;span class="nv"&gt;$MNT&lt;/span&gt;&lt;span class="sh"&gt;/boot/grub/grub.cfg" || fail "could not edit grub.cfg"
grep -q "&lt;/span&gt;&lt;span class="nv"&gt;$SRC_UUID&lt;/span&gt;&lt;span class="sh"&gt;" "&lt;/span&gt;&lt;span class="nv"&gt;$MNT&lt;/span&gt;&lt;span class="sh"&gt;/boot/grub/grub.cfg" "&lt;/span&gt;&lt;span class="nv"&gt;$MNT&lt;/span&gt;&lt;span class="sh"&gt;/etc/fstab" &amp;amp;&amp;amp; fail "primary UUID still present on spare"
grep -q "UUID=&lt;/span&gt;&lt;span class="nv"&gt;$DST_UUID&lt;/span&gt;&lt;span class="sh"&gt;" "&lt;/span&gt;&lt;span class="nv"&gt;$MNT&lt;/span&gt;&lt;span class="sh"&gt;/etc/fstab" || fail "fstab root check failed"

sync
log "OK: primary -&amp;gt; spare clone finished (rsync rc=&lt;/span&gt;&lt;span class="nv"&gt;$rc&lt;/span&gt;&lt;span class="sh"&gt;)"
exit 0
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;span class="nb"&gt;sudo chown &lt;/span&gt;root:root /usr/local/sbin/clone-job.sh &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo chmod &lt;/span&gt;755 /usr/local/sbin/clone-job.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install the systemd service and timer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/systemd/system/clone-job.service &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
[Unit]
Description=Weekly one-way clone primary root -&amp;gt; spare root
ConditionPathExists=/usr/local/sbin/clone-job.sh

[Service]
Type=oneshot
ExecStart=/usr/local/sbin/clone-job.sh
Nice=10
TimeoutStartSec=2h
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/systemd/system/clone-job.timer &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
[Unit]
Description=Run clone-job every Friday 02:00

[Timer]
OnCalendar=Fri *-*-* 02:00:00
Persistent=true

[Install]
WantedBy=timers.target
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl daemon-reload &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; clone-job.timer
systemctl list-timers clone-job.timer &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  9. Test in order
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# (a) mail&lt;/span&gt;
&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'To: &amp;lt;YOUR_ADDRESS&amp;gt;\nSubject: [test] mail\n\nhello\n'&lt;/span&gt; | &lt;span class="nb"&gt;sudo &lt;/span&gt;msmtp &amp;lt;YOUR_ADDRESS&amp;gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo &lt;/span&gt;sent

&lt;span class="c"&gt;# (b) dry run, writes nothing&lt;/span&gt;
&lt;span class="nb"&gt;sudo env &lt;/span&gt;&lt;span class="nv"&gt;DRYRUN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 /usr/local/sbin/clone-job.sh 2&amp;gt;&amp;amp;1 | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-15&lt;/span&gt;

&lt;span class="c"&gt;# (c) real run, from the primary disk&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl start clone-job.service
journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; clone-job.service &lt;span class="nt"&gt;-n&lt;/span&gt; 40 &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then boot from the spare disk and run the job once more. The expected output is a &lt;code&gt;skip&lt;/code&gt; line, and nothing should be written:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl start clone-job.service
journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; clone-job.service &lt;span class="nt"&gt;-n&lt;/span&gt; 10 &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
findmnt | &lt;span class="nb"&gt;grep &lt;/span&gt;clone-job &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"no clone mount (good)"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  10. Rollback
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;To stop the weekly job: &lt;code&gt;sudo systemctl disable --now clone-job.timer&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;To remove everything: delete the files listed above, then &lt;code&gt;sudo systemctl daemon-reload&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;To return to the original disk: select it in the firmware boot menu. Do not run &lt;code&gt;grub-install&lt;/code&gt; on the primary unless you have a backup of its EFI partition.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Prepared as a reproduction guide. Test every step on a non-production machine first if you can.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>automation</category>
      <category>devops</category>
      <category>hardware</category>
      <category>linux</category>
    </item>
  </channel>
</rss>
