标签 网络引导 下的文章

在本系列教程中所构建的网络引导服务器有一个很重要的限制,那就是所提供的操作系统镜像是只读的。一些使用场景或许要求终端用户能够修改操作系统镜像。例如,一些教师或许希望学生能够安装和配置一些像 MariaDB 和 Node.js 这样的包来做为他们课程练习的一部分。

可写镜像的另外的好处是,终端用户“私人定制”的操作系统,在下次不同的工作站上使用时能够“跟着”他们。

修改 Bootmenu 应用程序以使用 HTTPS

为 bootmenu 应用程序创建一个自签名的证书:

$ sudo -i
# MY_NAME=$(</etc/hostname)
# MY_TLSD=/opt/bootmenu/tls
# mkdir $MY_TLSD
# openssl req -newkey rsa:2048 -nodes -keyout $MY_TLSD/$MY_NAME.key -x509 -days 3650 -out $MY_TLSD/$MY_NAME.pem

验证你的证书的值。确保 Subject 行中 CN 的值与你的 iPXE 客户端连接你的网络引导服务器所使用的 DNS 名字是相匹配的:

# openssl x509 -text -noout -in $MY_TLSD/$MY_NAME.pem

接下来,更新 bootmenu 应用程序去监听 HTTPS 端口和新创建的证书及密钥:

# sed -i "s#listen => .*#listen => ['https://$MY_NAME:443?cert=$MY_TLSD/$MY_NAME.pem\&key=$MY_TLSD/$MY_NAME.key\&ciphers=AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA'],#" /opt/bootmenu/bootmenu.conf

注意 iPXE 当前支持的 加密算法是有限制的。

GnuTLS 要求 “CAPDACREAD\_SEARCH” 能力,因此将它添加到 bootmenu 应用程序的 systemd 服务:

# sed -i '/^AmbientCapabilities=/ s/$/ CAP_DAC_READ_SEARCH/' /etc/systemd/system/bootmenu.service
# sed -i 's/Serves iPXE Menus over HTTP/Serves iPXE Menus over HTTPS/' /etc/systemd/system/bootmenu.service
# systemctl daemon-reload

现在,在防火墙中为 bootmenu 服务添加一个例外规则并重启动该服务:

# MY_SUBNET=192.0.2.0
# MY_PREFIX=24
# firewall-cmd --add-rich-rule="rule family='ipv4' source address='$MY_SUBNET/$MY_PREFIX' service name='https' accept"
# firewall-cmd --runtime-to-permanent
# systemctl restart bootmenu.service

使用 wget 去验证是否工作正常:

$ MY_NAME=server-01.example.edu
$ MY_TLSD=/opt/bootmenu/tls
$ wget -q --ca-certificate=$MY_TLSD/$MY_NAME.pem -O - https://$MY_NAME/menu

添加 HTTPS 到 iPXE

更新 init.ipxe 去使用 HTTPS。接着使用选项重新编译 ipxe 引导加载器,以便它包含和信任你为 bootmenu 应用程序创建的自签名证书:

$ echo '#define DOWNLOAD_PROTO_HTTPS' >> $HOME/ipxe/src/config/local/general.h
$ sed -i 's/^chain http:/chain https:/' $HOME/ipxe/init.ipxe
$ cp $MY_TLSD/$MY_NAME.pem $HOME/ipxe
$ cd $HOME/ipxe/src
$ make clean
$ make bin-x86_64-efi/ipxe.efi EMBED=../init.ipxe CERT="../$MY_NAME.pem" TRUST="../$MY_NAME.pem"

你现在可以将启用了 HTTPS 的 iPXE 引导加载器复制到你的客户端上,并测试它能否正常工作:

$ cp $HOME/ipxe/src/bin-x86_64-efi/ipxe.efi $HOME/esp/efi/boot/bootx64.efi

添加用户验证到 Mojolicious 中

为 bootmenu 应用程序创建一个 PAM 服务定义:

# dnf install -y pam_krb5
# echo 'auth required pam_krb5.so' > /etc/pam.d/bootmenu

添加一个库到 bootmenu 应用程序中,它使用 Authen-PAM 的 Perl 模块去执行用户验证:

# dnf install -y perl-Authen-PAM;
# MY_MOJO=/opt/bootmenu
# mkdir $MY_MOJO/lib
# cat << 'END' > $MY_MOJO/lib/PAM.pm
package PAM;

use Authen::PAM;

sub auth {
   my $success = 0;

   my $username = shift;
   my $password = shift;

   my $callback = sub {
      my @res;
      while (@_) {
         my $code = shift;
         my $msg = shift;
         my $ans = "";
   
         $ans = $username if ($code == PAM_PROMPT_ECHO_ON());
         $ans = $password if ($code == PAM_PROMPT_ECHO_OFF());
   
         push @res, (PAM_SUCCESS(), $ans);
      }
      push @res, PAM_SUCCESS();

      return @res;
   };

   my $pamh = new Authen::PAM('bootmenu', $username, $callback);

   {
      last unless ref $pamh;
      last unless $pamh->pam_authenticate() == PAM_SUCCESS;
      $success = 1;
   }

   return $success;
}

return 1;
END

以上的代码是一字不差是从 Authen::PAM::FAQ 的 man 页面中复制来的。

重定义 bootmenu 应用程序,以使它仅当提供了有效的用户名和密码之后返回一个网络引导模板:

# cat << 'END' > $MY_MOJO/bootmenu.pl
#!/usr/bin/env perl

use lib 'lib';

use PAM;
use Mojolicious::Lite;
use Mojolicious::Plugins;
use Mojo::Util ('url_unescape');

plugin 'Config';

get '/menu';
get '/boot' => sub {
   my $c = shift;

   my $instance = $c->param('instance');
   my $username = $c->param('username');
   my $password = $c->param('password');

   my $template = 'menu';

   {
      last unless $instance =~ /^fc[[:digit:]]{2}$/;
      last unless $username =~ /^[[:alnum:]]+$/;
      last unless PAM::auth($username, url_unescape($password));
      $template = $instance;
   }

   return $c->render(template => $template);
};

app->start;
END

bootmenu 应用程序现在查找 lib 命令去找到相应的 WorkingDirectory。但是,默认情况下,对于 systemd 单元它的工作目录设置为服务器的 root 目录。因此,你必须更新 systemd 单元去设置 WorkingDirectory 为 bootmenu 应用程序的根目录:

# sed -i "/^RuntimeDirectory=/ a WorkingDirectory=$MY_MOJO" /etc/systemd/system/bootmenu.service
# systemctl daemon-reload

更新模块去使用重定义后的 bootmenu 应用程序:

# cd $MY_MOJO/templates
# MY_BOOTMENU_SERVER=$(</etc/hostname)
# MY_FEDORA_RELEASES="28 29"
# for i in $MY_FEDORA_RELEASES; do echo '#!ipxe' > fc$i.html.ep; grep "^kernel\|initrd" menu.html.ep | grep "fc$i" >> fc$i.html.ep; echo "boot || chain https://$MY_BOOTMENU_SERVER/menu" >> fc$i.html.ep; sed -i "/^:f$i$/,/^boot /c :f$i\nlogin\nchain https://$MY_BOOTMENU_SERVER/boot?instance=fc$i\&username=\${username}\&password=\${password:uristring} || goto failed" menu.html.ep; done

上面的最后的命令将生成类似下面的三个文件:

menu.html.ep

#!ipxe

set timeout 5000

:menu
menu iPXE Boot Menu
item --key 1 lcl 1. Microsoft Windows 10
item --key 2 f29 2. RedHat Fedora 29
item --key 3 f28 3. RedHat Fedora 28
choose --timeout ${timeout} --default lcl selected || goto shell
set timeout 0
goto ${selected}

:failed
echo boot failed, dropping to shell...
goto shell

:shell
echo type 'exit' to get the back to the menu
set timeout 0
shell
goto menu

:lcl
exit

:f29
login
chain https://server-01.example.edu/boot?instance=fc29&username=${username}&password=${password:uristring} || goto failed

:f28
login
chain https://server-01.example.edu/boot?instance=fc28&username=${username}&password=${password:uristring} || goto failed

fc29.html.ep

#!ipxe
kernel --name kernel.efi ${prefix}/vmlinuz-4.19.5-300.fc29.x86_64 initrd=initrd.img ro ip=dhcp rd.peerdns=0 nameserver=192.0.2.91 nameserver=192.0.2.92 root=/dev/disk/by-path/ip-192.0.2.158:3260-iscsi-iqn.edu.example.server-01:fc29-lun-1 netroot=iscsi:192.0.2.158::::iqn.edu.example.server-01:fc29 console=tty0 console=ttyS0,115200n8 audit=0 selinux=0 quiet
initrd --name initrd.img ${prefix}/initramfs-4.19.5-300.fc29.x86_64.img
boot || chain https://server-01.example.edu/menu

fc28.html.ep

#!ipxe
kernel --name kernel.efi ${prefix}/vmlinuz-4.19.3-200.fc28.x86_64 initrd=initrd.img ro ip=dhcp rd.peerdns=0 nameserver=192.0.2.91 nameserver=192.0.2.92 root=/dev/disk/by-path/ip-192.0.2.158:3260-iscsi-iqn.edu.example.server-01:fc28-lun-1 netroot=iscsi:192.0.2.158::::iqn.edu.example.server-01:fc28 console=tty0 console=ttyS0,115200n8 audit=0 selinux=0 quiet
initrd --name initrd.img ${prefix}/initramfs-4.19.3-200.fc28.x86_64.img
boot || chain https://server-01.example.edu/menu

现在,重启动 bootmenu 应用程序,并验证用户认证是否正常工作:

# systemctl restart bootmenu.service

使得 iSCSI Target 可写

现在,用户验证通过 iPXE 可以正常工作,在用户连接时,你可以按需在只读镜像的上面创建每用户可写的 overlay 叠加层 。使用一个 写时复制 的叠加层与简单地为每个用户复制原始镜像相比有三个好处:

  1. 副本创建非常快。这样就可以按需创建。
  2. 副本并不增加服务器上的磁盘使用。除了原始镜像之外,仅存储用户写入个人镜像的内容。
  3. 由于每个副本的扇区大多都是服务器的存储器上的相同扇区,在随后的用户访问这些操作系统的副本时,它们可能已经加载到内存中,这样就提升了服务器的性能,因为对内存的访问速度要比磁盘 I/O 快得多。

使用写时复制的一个潜在隐患是,一旦叠加层创建后,叠加层之下的镜像就不能再改变。如果它们改变,所有它们之上的叠加层将出错。因此,叠加层必须被删除并用新的、空白的进行替换。即便只是简单地以读写模式加载的镜像,也可能因为某些文件系统更新导致叠加层出错。

由于这个隐患,如果原始镜像被修改将导致叠加层出错,因此运行下列的命令,将原始镜像标记为不可改变:

# chattr +i </path/to/file>

你可以使用 lsattr </path/to/file> 去查看不可改变标志,并可以使用 chattr -i </path/to/file> 取消设置不可改变标志。在设置了不可改变标志之后,即便是 root 用户或以 root 运行的系统进程也不修改或删除这个文件。

停止 tgtd.service 之后,你就可以改变镜像文件:

# systemctl stop tgtd.service

当仍有连接打开的时候,运行这个命令一般需要一分钟或更长的时间。

现在,移除只读的 iSCSI 出口。然后更新模板中的 readonly-root 配置文件,以使镜像不再是只读的:

# MY_FC=fc29
# rm -f /etc/tgt/conf.d/$MY_FC.conf
# TEMP_MNT=$(mktemp -d)
# mount /$MY_FC.img $TEMP_MNT
# sed -i 's/^READONLY=yes$/READONLY=no/' $TEMP_MNT/etc/sysconfig/readonly-root
# sed -i 's/^Storage=volatile$/#Storage=auto/' $TEMP_MNT/etc/systemd/journald.conf
# umount $TEMP_MNT

将 journald 日志从发送到内存修改回缺省值(如果 /var/log/journal 存在的话记录到磁盘),因为一个用户报告说,他的客户端由于应用程序生成了大量的系统日志而产生内存溢出错误,导致它的客户端被卡住。而将日志记录到磁盘的负面影响是客户端产生了额外的写入流量,这将在你的网络引导服务器上可能增加一些没有必要的 I/O。你应该去决定到底使用哪个选择 —— 记录到内存还是记录到硬盘 —— 哪个更合适取决于你的环境。

因为你的模板镜像在以后不能做任何的更改,因此在它上面设置不可更改标志,然后重启动 tgtd.service:

# chattr +i /$MY_FC.img
# systemctl start tgtd.service

现在,更新 bootmenu 应用程序:

# cat << 'END' > $MY_MOJO/bootmenu.pl
#!/usr/bin/env perl

use lib 'lib';

use PAM;
use Mojolicious::Lite;
use Mojolicious::Plugins;
use Mojo::Util ('url_unescape');

plugin 'Config';

get '/menu';
get '/boot' => sub {
   my $c = shift;

   my $instance = $c->param('instance');
   my $username = $c->param('username');
   my $password = $c->param('password');

   my $chapscrt;
   my $template = 'menu';

   {
      last unless $instance =~ /^fc[[:digit:]]{2}$/;
      last unless $username =~ /^[[:alnum:]]+$/;
      last unless PAM::auth($username, url_unescape($password));
      last unless $chapscrt = `sudo scripts/mktgt $instance $username`;
      $template = $instance;
   }

   return $c->render(template => $template, username => $username, chapscrt => $chapscrt);
};

app->start;
END

新版本的 bootmenu 应用程序调用一个定制的 mktgt 脚本,如果成功,它将为每个它自己创建的新的 iSCSI 目标返回一个随机的 CHAP 密码。这个 CHAP 密码可以防止其它用户的 iSCSI 目标以间接方式挂载这个用户的目标。这个应用程序只有在用户密码认证成功之后才返回一个正确的 iSCSI 目标密码。

mktgt 脚本要加 sudo 前缀来运行,因为它需要 root 权限去创建目标。

$username$chapscrt 变量也传递给 render 命令,因此在需要的时候,它们也能够被纳入到模板中返回给用户。

接下来,更新我们的引导模板,以便于它们能够读取用户名和 chapscrt 变量,并传递它们到所属的终端用户。也要更新模板以 rw(读写)模式加载根文件系统:

# cd $MY_MOJO/templates
# sed -i "s/:$MY_FC/:$MY_FC-<%= \$username %>/g" $MY_FC.html.ep
# sed -i "s/ netroot=iscsi:/ netroot=iscsi:<%= \$username %>:<%= \$chapscrt %>@/" $MY_FC.html.ep
# sed -i "s/ ro / rw /" $MY_FC.html.ep

运行上面的命令后,你应该会看到如下的引导模板:

#!ipxe
kernel --name kernel.efi ${prefix}/vmlinuz-4.19.5-300.fc29.x86_64 initrd=initrd.img rw ip=dhcp rd.peerdns=0 nameserver=192.0.2.91 nameserver=192.0.2.92 root=/dev/disk/by-path/ip-192.0.2.158:3260-iscsi-iqn.edu.example.server-01:fc29-<%= $username %>-lun-1 netroot=iscsi:<%= $username %>:<%= $chapscrt %>@192.0.2.158::::iqn.edu.example.server-01:fc29-<%= $username %> console=tty0 console=ttyS0,115200n8 audit=0 selinux=0 quiet
initrd --name initrd.img ${prefix}/initramfs-4.19.5-300.fc29.x86_64.img
boot || chain https://server-01.example.edu/menu

注意:如果在 插入 变量后需要查看引导模板,你可以在 boot 命令之前,在它自己的行中插入 shell 命令。然后在你网络引导你的客户端时,iPXE 将在那里给你提供一个用于交互的 shell,你可以在 shell 中输入 imgstat 去查看传递到内核的参数。如果一切正确,你可以输入 exit 去退出 shell 并继续引导过程。

现在,通过 sudo 允许 bootmenu 用户以 root 权限去运行 mktgt 脚本(仅这个脚本):

# echo "bootmenu ALL = NOPASSWD: $MY_MOJO/scripts/mktgt *" > /etc/sudoers.d/bootmenu

bootmenu 用户不应该写访问 mktgt 脚本或在它的家目录下的任何其它文件。在 /opt/bootmenu 目录下的所有文件的属主应该是 root,并且不应该被其它任何 root 以外的用户可写。

sudo 在使用 systemd 的 DynamicUser 选项下不能正常工作,因此创建一个普通用户帐户,并设置 systemd 服务以那个用户运行:

# useradd -r -c 'iPXE Boot Menu Service' -d /opt/bootmenu -s /sbin/nologin bootmenu
# sed -i 's/^DynamicUser=true$/User=bootmenu/' /etc/systemd/system/bootmenu.service
# systemctl daemon-reload

最后,为写时复制覆盖创建一个目录,并创建管理 iSCSI 目标的 mktgt 脚本和它们的覆盖支持存储:

# mkdir /$MY_FC.cow
# mkdir $MY_MOJO/scripts
# cat << 'END' > $MY_MOJO/scripts/mktgt
#!/usr/bin/env perl

# if another instance of this script is running, wait for it to finish
"$ENV{FLOCKER}" eq 'MKTGT' or exec "env FLOCKER=MKTGT flock /tmp $0 @ARGV";

# use "RETURN" to print to STDOUT; everything else goes to STDERR by default
open(RETURN, '>&', STDOUT);
open(STDOUT, '>&', STDERR);

my $instance = shift or die "instance not provided";
my $username = shift or die "username not provided";

my $img = "/$instance.img";
my $dir = "/$instance.cow";
my $top = "$dir/$username";

-f "$img" or die "'$img' is not a file"; 
-d "$dir" or die "'$dir' is not a directory";

my $base;
die unless $base = `losetup --show --read-only --nooverlap --find $img`;
chomp $base;

my $size;
die unless $size = `blockdev --getsz $base`;
chomp $size;

# create the per-user sparse file if it does not exist
if (! -e "$top") {
   die unless system("dd if=/dev/zero of=$top status=none bs=512 count=0 seek=$size") == 0;
}

# create the copy-on-write overlay if it does not exist
my $cow="$instance-$username";
my $dev="/dev/mapper/$cow";
if (! -e "$dev") {
   my $over;
   die unless $over = `losetup --show --nooverlap --find $top`;
   chomp $over;
   die unless system("echo 0 $size snapshot $base $over p 8 | dmsetup create $cow") == 0;
}

my $tgtadm = '/usr/sbin/tgtadm --lld iscsi';

# get textual representations of the iscsi targets
my $text = `$tgtadm --op show --mode target`;
my @targets = $text =~ /(?:^T.*\n)(?:^ .*\n)*/mg;

# convert the textual representations into a hash table
my $targets = {};
foreach (@targets) {
   my $tgt;
   my $sid;

   foreach (split /\n/) {
      /^Target (\d+)(?{ $tgt = $targets->{$^N} = [] })/;
      /I_T nexus: (\d+)(?{ $sid = $^N })/;
      /Connection: (\d+)(?{ push @{$tgt}, [ $sid, $^N ] })/;
   }
}

my $hostname;
die unless $hostname = `hostname`;
chomp $hostname;

my $target = 'iqn.' . join('.', reverse split('\.', $hostname)) . ":$cow";

# find the target id corresponding to the provided target name and
# close any existing connections to it
my $tid = 0;
foreach (@targets) {
   next unless /^Target (\d+)(?{ $tid = $^N }): $target$/m;
   foreach (@{$targets->{$tid}}) {
      die unless system("$tgtadm --op delete --mode conn --tid $tid --sid $_->[0] --cid $_->[1]") == 0;
   }
}

# create a new target if an existing one was not found
if ($tid == 0) {
   # find an available target id
   my @ids = (0, sort keys %{$targets});
   $tid = 1; while ($ids[$tid]==$tid) { $tid++ }

   # create the target
   die unless -e "$dev";
   die unless system("$tgtadm --op new --mode target --tid $tid --targetname $target") == 0;
   die unless system("$tgtadm --op new --mode logicalunit --tid $tid --lun 1 --backing-store $dev") == 0;
   die unless system("$tgtadm --op bind --mode target --tid $tid --initiator-address ALL") == 0;
}

# (re)set the provided target's chap password
my $password = join('', map(chr(int(rand(26))+65), 1..8));
my $accounts = `$tgtadm --op show --mode account`;
if ($accounts =~ / $username$/m) {
   die unless system("$tgtadm --op delete --mode account --user $username") == 0;
}
die unless system("$tgtadm --op new --mode account --user $username --password $password") == 0;
die unless system("$tgtadm --op bind --mode account --tid $tid --user $username") == 0;

# return the new password to the iscsi target on stdout
print RETURN $password;
END
# chmod +x $MY_MOJO/scripts/mktgt

上面的脚本将做以下五件事情:

  1. 创建 /<instance>.cow/<username> 稀疏文件(如果不存在的话)。
  2. 创建 /dev/mapper/<instance>-<username> 设备节点作为 iSCSI 目标的写时复制支持存储(如果不存在的话)。
  3. 创建 iqn.<reverse-hostname>:<instance>-<username> iSCSI 目标(如果不存在的话)。或者,如果已存在了,它将关闭任何已存在的连接,因为在任何时刻,镜像只能以只读模式从一个地方打开。
  4. 它在 iqn.<reverse-hostname>:<instance>-<username> iSCSI 目标上(重新)设置 chap 密码为一个新的随机值。
  5. (如果前面的所有任务都成功的话)它在 标准输出 上显示新的 chap 密码。

你应该可以在命令行上通过使用有效的测试参数来运行它,以测试 mktgt 脚本能否正常工作。例如:

# echo `$MY_MOJO/scripts/mktgt fc29 jsmith`

当你从命令行上运行时,mktgt 脚本应该会输出 iSCSI 目标的一个随意的八字符随机密码(如果成功的话)或者是出错位置的行号(如果失败的话)。

有时候,你可能需要在不停止整个服务的情况下删除一个 iSCSI 目标。例如,一个用户可能无意中损坏了他的个人镜像,在那种情况下,你可能需要按步骤撤销上面的 mktgt 脚本所做的事情,以便于他下次登入时他将得到一个原始镜像。

下面是用于撤销的 rmtgt 脚本,它以相反的顺序做了上面 mktgt 脚本所做的事情:

# mkdir $HOME/bin
# cat << 'END' > $HOME/bin/rmtgt
#!/usr/bin/env perl

@ARGV >= 2 or die "usage: $0 <instance> <username> [+d|+f]\n";

my $instance = shift;
my $username = shift;

my $rmd = ($ARGV[0] eq '+d'); #remove device node if +d flag is set
my $rmf = ($ARGV[0] eq '+f'); #remove sparse file if +f flag is set
my $cow = "$instance-$username";

my $hostname;
die unless $hostname = `hostname`;
chomp $hostname;

my $tgtadm = '/usr/sbin/tgtadm';
my $target = 'iqn.' . join('.', reverse split('\.', $hostname)) . ":$cow";

my $text = `$tgtadm --op show --mode target`;
my @targets = $text =~ /(?:^T.*\n)(?:^ .*\n)*/mg;

my $targets = {};
foreach (@targets) {
   my $tgt;
   my $sid;

   foreach (split /\n/) {
      /^Target (\d+)(?{ $tgt = $targets->{$^N} = [] })/;
      /I_T nexus: (\d+)(?{ $sid = $^N })/;
      /Connection: (\d+)(?{ push @{$tgt}, [ $sid, $^N ] })/;
   }
}

my $tid = 0;
foreach (@targets) {
   next unless /^Target (\d+)(?{ $tid = $^N }): $target$/m;
   foreach (@{$targets->{$tid}}) {
      die unless system("$tgtadm --op delete --mode conn --tid $tid --sid $_->[0] --cid $_->[1]") == 0;
   }
   die unless system("$tgtadm --op delete --mode target --tid $tid") == 0;
   print "target $tid deleted\n";
   sleep 1;
}

my $dev = "/dev/mapper/$cow";
if ($rmd or ($rmf and -e $dev)) {
   die unless system("dmsetup remove $cow") == 0;
   print "device node $dev deleted\n";
}

if ($rmf) {
   my $sf = "/$instance.cow/$username";
   die "sparse file $sf not found" unless -e "$sf";
   die unless system("rm -f $sf") == 0;
   die unless not -e "$sf";
   print "sparse file $sf deleted\n";
}
END
# chmod +x $HOME/bin/rmtgt

例如,使用上面的脚本去完全删除 fc29-jsmith 目标,包含它的支持存储设备节点和稀疏文件,可以按下列方式运行命令:

# rmtgt fc29 jsmith +f

一旦你验证 mktgt 脚本工作正常,你可以重启动 bootmenu 服务。下次有人从网络引导时,他们应该能够接收到一个他们可以写入的、可”私人定制“的网络引导镜像的副本:

# systemctl restart bootmenu.service

现在,就像下面的截屏示范的那样,用户应该可以修改根文件系统了:


via: https://fedoramagazine.org/how-to-build-a-netboot-server-part-4/

作者:Gregory Bartholomew 选题:lujun9972 译者:qhwdw 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

如何构建一台网络引导服务器(一) 中,我们提供了一个极简的 iPXE 引导脚本来引导你的网络引导镜像。许多用户除了使用网络引导镜像外,可能在机器本地也有一个操作系统。但是使用常见的工作站的 BIOS 去切换引导加载器是很笨拙的。在本系列文件的第三部分,我们将向你展示如何设置一个更复杂的 iPXE 配置。它将允许终端用户以更容易的方式去选择引导哪个操作系统。它也可以配置为让系统管理员从一台中央服务器来统一管理引导菜单。

一个交互式 iPXE 引导菜单

下面这些命令重定义了网络引导镜像的 boot.cfg 来作为一个交互式的 iPXE 引导菜单,并使用了一个 5 秒倒计时的定时器:

$ MY_FVER=29
$ MY_KRNL=$(ls -c /fc$MY_FVER/lib/modules | head -n 1)
$ MY_DNS1=192.0.2.91
$ MY_DNS2=192.0.2.92
$ MY_NAME=server-01.example.edu
$ MY_EMAN=$(echo $MY_NAME | tr '.' "\n" | tac | tr "\n" '.' | cut -b -${#MY_NAME})
$ MY_ADDR=$(host -t A $MY_NAME | awk '{print $4}')
$ cat << END > $HOME/esp/linux/boot.cfg
#!ipxe

set timeout 5000

:menu
menu iPXE Boot Menu
item --key 1 lcl 1. Microsoft Windows 10
item --key 2 f$MY_FVER 2. RedHat Fedora $MY_FVER
choose --timeout \${timeout} --default lcl selected || goto shell
set timeout 0
goto \${selected}

:failed
echo boot failed, dropping to shell...
goto shell

:shell
echo type 'exit' to get the back to the menu
set timeout 0
shell
goto menu

:lcl
exit

:f$MY_FVER
kernel --name kernel.efi \${prefix}/vmlinuz-$MY_KRNL initrd=initrd.img ro ip=dhcp rd.peerdns=0 nameserver=$MY_DNS1 nameserver=$MY_DNS2 root=/dev/disk/by-path/ip-$MY_ADDR:3260-iscsi-iqn.$MY_EMAN:fc$MY_FVER-lun-1 netroot=iscsi:$MY_ADDR::::iqn.$MY_EMAN:fc$MY_FVER console=tty0 console=ttyS0,115200n8 audit=0 selinux=0 quiet
initrd --name initrd.img \${prefix}/initramfs-$MY_KRNL.img
boot || goto failed
END

上述菜单有五个节:

  • menu 定义了显示在屏幕上的实际菜单内容。
  • failed 提示用户发生了错误,并将用户带到 shell 以错误错误。
  • shell 提供了交互式命令提示符。你可以在引导菜单出现时按下 Esc 键进入,或者是 boot 命令失败时也会进入到命令提示符。
  • lcl 包含一个提供给 iPXE 退出的简单命令,以及返还控制权给 BIOS。在 iPXE 之后,无论你希望缺省引导的设备(即:工作站的本地硬件)是什么,都必须在你的工作站的 BIOS 中正确地作为下一个引导设备列出来。
  • f29 包含前面文章提到同一个网络引导代码,但使用最终的退出代码来替换掉 goto failed

从你的 $HOME/esp/linux 目录中复制更新后的 boot.cfg 到所有客户端系统的 ESP 中。如果一切顺利,你应该会看到类似下面图片的结果:

一个服务器托管的引导菜单

你可以添加到网络引导服务器的另一个特性是,能够从一台中央位置去管理所有客户端的引导菜单。这个特性尤其适用于批量安装(升级)一个新版本的操作系统。在你将新内核和新的 initramfs 复制到所有客户端的 ESP 之后,这个特性可以让你执行一种 原子事务) 去切换所有客户端到新操作系统。

安装 Mojolicious:

$ sudo -i
# dnf install -y perl-Mojolicious

定义 “bootmenu” 应用程序:

# mkdir /opt/bootmenu
# cat << END > /opt/bootmenu/bootmenu.pl
#!/usr/bin/env perl
use Mojolicious::Lite;
use Mojolicious::Plugins;

plugin 'Config';

get '/menu';

app->start;
END
# chmod 755 /opt/bootmenu/bootmenu.pl

为 “bootmenu” 应用程序定义配置文件:

# cat << END > /opt/bootmenu/bootmenu.conf
{
 hypnotoad => {
 listen => ['http://*:80'],
 pid_file => '/run/bootmenu/bootmenu.pid',
 }
}
END

这是一个非常简单的 Mojolicious 应用程序,它监听 80 端口,并且只回应对 /menu 的请求。如果你想快速了解 Mojolicious 能做什么,运行 man Mojolicious::Guides::Growing 去查看手册。按 Q 键退出手册。

boot.cfg 移到我们的网络引导应用程序中作为一个名为 menu.html.ep 的模板:

# mkdir /opt/bootmenu/templates
# mv $HOME/esp/linux/boot.cfg /opt/bootmenu/templates/menu.html.ep

定义一个 systemd 服务去管理引导菜单应用程序:

# cat << END > /etc/systemd/system/bootmenu.service
[Unit]
Description=Serves iPXE Menus over HTTP
After=network-online.target

[Service]
Type=forking
DynamicUser=true
RuntimeDirectory=bootmenu
PIDFile=/run/bootmenu/bootmenu.pid
ExecStart=/usr/bin/hypnotoad /opt/bootmenu/bootmenu.pl
ExecReload=/usr/bin/hypnotoad /opt/bootmenu/bootmenu.pl
AmbientCapabilities=CAP_NET_BIND_SERVICE
KillMode=process

[Install]
WantedBy=multi-user.target
END

在本地防火墙中为 HTTP 服务添加一个例外规则,并启动 bootmenu 服务:

# firewall-cmd --add-service http
# firewall-cmd --runtime-to-permanent
# systemctl enable bootmenu.service
# systemctl start bootmenu.service

wget 测试它:

$ sudo dnf install -y wget
$ MY_BOOTMENU_SERVER=server-01.example.edu
$ wget -q -O - http://$MY_BOOTMENU_SERVER/menu

以上的命令应该会输出类似下面的内容:

#!ipxe

set timeout 5000

:menu
menu iPXE Boot Menu
item --key 1 lcl 1. Microsoft Windows 10
item --key 2 f29 2. RedHat Fedora 29
choose --timeout ${timeout} --default lcl selected || goto shell
set timeout 0
goto ${selected}

:failed
echo boot failed, dropping to shell...
goto shell

:shell
echo type 'exit' to get the back to the menu
set timeout 0
shell
goto menu

:lcl
exit

:f29
kernel --name kernel.efi ${prefix}/vmlinuz-4.19.4-300.fc29.x86_64 initrd=initrd.img ro ip=dhcp rd.peerdns=0 nameserver=192.0.2.91 nameserver=192.0.2.92 root=/dev/disk/by-path/ip-192.0.2.158:3260-iscsi-iqn.edu.example.server-01:fc29-lun-1 netroot=iscsi:192.0.2.158::::iqn.edu.example.server-01:fc29 console=tty0 console=ttyS0,115200n8 audit=0 selinux=0 quiet
initrd --name initrd.img ${prefix}/initramfs-4.19.4-300.fc29.x86_64.img
boot || goto failed

现在,引导菜单服务器已经正常工作了,重新构建 ipxe.efi 引导加载器,使用一个初始化脚本指向它。

第一步,先更新我们在本系列文章的第一部分中创建的 init.ipxe 脚本:

$ MY_BOOTMENU_SERVER=server-01.example.edu
$ cat << END > $HOME/ipxe/init.ipxe
#!ipxe

dhcp || exit
set prefix file:///linux
chain http://$MY_BOOTMENU_SERVER/menu || exit
END

现在,重新构建引导加载器:

$ cd $HOME/ipxe/src
$ make clean
$ make bin-x86_64-efi/ipxe.efi EMBED=../init.ipxe

将更新后的引导加载器复制到你的 ESP 中:

$ cp $HOME/ipxe/src/bin-x86_64-efi/ipxe.efi $HOME/esp/efi/boot/bootx64.efi

将更新后的引导加载器复制到所有的客户端中之后,以后更新引导菜单只需要简单地编辑 /opt/bootmenu/templates/menu.html.ep 文件,然后再运行如下命令:

$ sudo systemctl restart bootmenu.service

做一步的改变

如果引导菜单服务器工作正常,在你的客户端系统上的 boot.cfg 文件将更长。

比如,重新添加 Fedora 28 镜像到引导菜单中:

$ sudo -i
# MY_FVER=28
# MY_KRNL=$(ls -c /fc$MY_FVER/lib/modules | head -n 1)
# MY_DNS1=192.0.2.91
# MY_DNS2=192.0.2.92
# MY_NAME=$(</etc/hostname)
# MY_EMAN=$(echo $MY_NAME | tr '.' "\n" | tac | tr "\n" '.' | cut -b -${#MY_NAME})
# MY_ADDR=$(host -t A $MY_NAME | awk '{print $4}')
# cat << END >> /opt/bootmenu/templates/menu.html.ep

:f$MY_FVER
kernel --name kernel.efi \${prefix}/vmlinuz-$MY_KRNL initrd=initrd.img ro ip=dhcp rd.peerdns=0 nameserver=$MY_DNS1 nameserver=$MY_DNS2 root=/dev/disk/by-path/ip-$MY_ADDR:3260-iscsi-iqn.$MY_EMAN:fc$MY_FVER-lun-1 netroot=iscsi:$MY_ADDR::::iqn.$MY_EMAN:fc$MY_FVER console=tty0 console=ttyS0,115200n8 audit=0 selinux=0 quiet
initrd --name initrd.img \${prefix}/initramfs-$MY_KRNL.img
boot || goto failed
END
# sed -i "/item --key 2/a item --key 3 f$MY_FVER 3. RedHat Fedora $MY_FVER" /opt/bootmenu/templates/menu.html.ep
# systemctl restart bootmenu.service

如果一切顺利,你的客户端下次引导时,应该看到如下图所示的结果:


via: https://fedoramagazine.org/how-to-build-a-netboot-server-part-3/

作者:Gregory Bartholomew 选题:lujun9972 译者:qhwdw 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

如何构建一台网络引导服务器(一) 的文章中,我们展示了如何创建一个网络引导镜像,在那个镜像中使用了一个名为 liveuser 帐户,它的家目录位于内存中,重启后家目录中的内容将全部消失。然而很多用户都希望机器重启后保存他们的文件和设置。因此,在本系列的第二部分,我们将向你展示如何在第一部分的基础上,重新配置网络引导镜像,以便 活动目录 中的用户帐户可以进行登录,然后从一个 NFS 服务器上自动挂载他们的家目录。

本系列的第三部分,我们将向你展示网络引导客户端如何与中心化配置的 iPXE 引导菜单进行交互。

设置使用 KRB5 认证的 NFS4 Home 目录

按以前的文章 “使用 Kerberos 强化共享的 NFS Home 目录安全性” 的指导来做这个设置。

删除 Liveuser 帐户

删除本系列文章第一部分中创建的 liveuser 帐户:

$ sudo -i
# sed -i '/automaticlogin/Id' /fc28/etc/gdm/custom.conf
# rm -f /fc28/etc/sudoers.d/liveuser
# for i in passwd shadow group gshadow; do sed -i '/^liveuser:/d' /fc28/etc/$i; done

配置 NTP、KRB5 和 SSSD

接下来,我们需要将 NTP、KRB5 和 SSSD 的配置文件复制进客户端使用的镜像中,以便于它们能够使用同一个帐户:

# MY_HOSTNAME=$(</etc/hostname)
# MY_DOMAIN=${MY_HOSTNAME#*.}
# dnf -y --installroot=/fc28 install ntp krb5-workstation sssd
# cp /etc/ntp.conf /fc28/etc
# chroot /fc28 systemctl enable ntpd.service
# cp /etc/krb5.conf.d/${MY_DOMAIN%%.*} /fc28/etc/krb5.conf.d
# cp /etc/sssd/sssd.conf /fc28/etc/sssd

在已配置的识别服务的基础上,重新配置 sssd 提供认证服务:

# sed -i '/services =/s/$/, pam/' /fc28/etc/sssd/sssd.conf

另外,配置成确保客户端不能更改这个帐户密码:

# sed -i '/id_provider/a \ \ ad_maximum_machine_account_password_age = 0' /fc28/etc/sssd/sssd.conf

另外,复制 nfsnobody 的定义:

# for i in passwd shadow group gshadow; do grep "^nfsnobody:" /etc/$i >> /fc28/etc/$i; done

加入活动目录

接下来,你将执行一个 chroot 将客户端镜像加入到活动目录。从删除预置在网络引导镜像中同名的计算机帐户开始:

# MY_USERNAME=jsmith
# MY_CLIENT_HOSTNAME=$(</fc28/etc/hostname)
# adcli delete-computer "${MY_CLIENT_HOSTNAME%%.*}" -U "$MY_USERNAME"

在网络引导镜像中如果有 krb5.keytab 文件,也删除它:

# rm -f /fc28/etc/krb5.keytab

chroot 到网络引导镜像中:

# for i in dev dev/pts dev/shm proc sys run; do mount -o bind /$i /fc28/$i; done
# chroot /fc28 /usr/bin/bash --login

执行一个加入操作:

# MY_USERNAME=jsmith
# MY_HOSTNAME=$(</etc/hostname)
# MY_DOMAIN=${MY_HOSTNAME#*.}
# MY_REALM=${MY_DOMAIN^^}
# MY_OU="cn=computers,dc=${MY_DOMAIN//./,dc=}"
# adcli join $MY_DOMAIN --login-user="$MY_USERNAME" --computer-name="${MY_HOSTNAME%%.*}" --host-fqdn="$MY_HOSTNAME" --user-principal="host/$MY_HOSTNAME@$MY_REALM" --domain-ou="$MY_OU"

现在登出 chroot,并清除 root 用户的命令历史:

# logout
# for i in run sys proc dev/shm dev/pts dev; do umount /fc28/$i; done
# > /fc28/root/.bash_history

安装和配置 PAM 挂载

我们希望客户端登入后自动挂载用户家目录。为实现这个目的,我们将要使用 pam_mount 模块。安装和配置 pam_mount

# dnf install -y --installroot=/fc28 pam_mount
# cat << END > /fc28/etc/security/pam_mount.conf.xml
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE pam_mount SYSTEM "pam_mount.conf.xml.dtd">
<pam_mount>
<debug enable="0" />
<volume uid="1400000000-1499999999" fstype="nfs4" server="$MY_HOSTNAME" path="/home/%(USER)" mountpoint="/home/%(USER)" options="sec=krb5" />
<mkmountpoint enable="1" remove="0" />
<msg-authpw>Password:</msg-authpw>
</pam_mount>
END

重新配置 PAM 去使用 pam_mount

# dnf install -y patch
# cp -r /fc28/usr/share/authselect/default/sssd /fc28/etc/authselect/custom
# echo 'initgroups: files' >> /fc28/etc/authselect/custom/sssd/nsswitch.conf
# patch /fc28/etc/authselect/custom/sssd/system-auth << END
@@ -12 +12,2 @@
-auth        sufficient                                   pam_sss.so forward_pass
+auth        requisite                                    pam_mount.so {include if "with-pammount"}
+auth        sufficient                                   pam_sss.so {if "with-pammount":use_first_pass|forward_pass}
@@ -35,2 +36,3 @@
 session     required                                     pam_unix.so
+session     optional                                     pam_mount.so {include if "with-pammount"}
 session     optional                                     pam_sss.so
END
# patch /fc28/etc/authselect/custom/sssd/password-auth << END
@@ -9 +9,2 @@
-auth        sufficient                                   pam_sss.so forward_pass
+auth        requisite                                    pam_mount.so {include if "with-pammount"}
+auth        sufficient                                   pam_sss.so {if "with-pammount":use_first_pass|forward_pass}
@@ -32,2 +33,3 @@
 session     required                                     pam_unix.so
+session     optional                                     pam_mount.so {include if "with-pammount"}
 session     optional                                     pam_sss.so
END
# chroot /fc28 authselect select custom/sssd with-pammount --force

另外,要确保从客户端上总是可解析 NFS 服务器的主机名:

# MY_IP=$(host -t A $MY_HOSTNAME | awk '{print $4}')
# echo "$MY_IP $MY_HOSTNAME ${MY_HOSTNAME%%.*}" >> /fc28/etc/hosts

可选,允许所有用户可以使用 sudo

# echo '%users ALL=(ALL) NOPASSWD: ALL' > /fc28/etc/sudoers.d/users

转换 NFS 根目录到一个 iSCSI 后备存储器

在一个 nfsroot 连接建立之后,目前版本的 nfs-utils 可能很难为家目录建立一个从客户端到 NFS 服务器的第二个连接。当尝试去访问家目录时,客户端将被挂起。因此,为了共享网络引导镜像,我们将使用一个不同的协议(iSCSI)来规避这个问题。

首先 chroot 到镜像中,重新配置它的 initramfs,让它从一个 iSCSI 根目录中去引导:

# for i in dev dev/pts dev/shm proc sys run; do mount -o bind /$i /fc28/$i; done
# chroot /fc28 /usr/bin/bash --login
# dnf install -y iscsi-initiator-utils
# sed -i 's/nfs/iscsi/' /etc/dracut.conf.d/netboot.conf
# echo 'omit_drivers+=" qedi "' > /etc/dracut.conf.d/omit-qedi.conf
# echo 'blacklist qedi' > /etc/modprobe.d/blacklist-qedi.conf
# KERNEL=$(ls -c /lib/modules | head -n 1)
# INITRD=$(find /boot -name 'init*' | grep -m 1 $KERNEL)
# dracut -f $INITRD $KERNEL
# logout
# for i in run sys proc dev/shm dev/pts dev; do umount /fc28/$i; done
# > /fc28/root/.bash_history

在测试时,qedi 驱动会破坏 iSCSI,因此我们将它禁用。

接着,创建一个 fc28.img 稀疏文件。这个稀疏文件代表 iSCSI 目标的后备存储器:

# FC28_SIZE=$(du -ms /fc28 | cut -f 1)
# dd if=/dev/zero of=/fc28.img bs=1MiB count=0 seek=$(($FC28_SIZE*2))

(如果你有一个可使用的独立分区或磁盘驱动器,也可以用它,而不用再去创建这个稀疏文件了。)

接着,使用一个文件系统去格式化镜像、挂载它、然后将网络引导镜像复制进去:

# mkfs -t xfs -L NETROOT /fc28.img
# TEMP_MNT=$(mktemp -d)
# mount /fc28.img $TEMP_MNT
# cp -a /fc28/* $TEMP_MNT
# umount $TEMP_MNT

在使用 SquashFS 测试时,客户端偶尔会出现小状况。似乎是因为 SquashFS 在多处理器客户端上没法执行随机 I/O。(更多内容见 squashfs 读取卡顿的奇怪案例)。如果你希望使用文件系统压缩来提升吞吐性能,ZFS 或许是个很好的选择。

如果你对 iSCSI 服务器的吞吐性能要求非常高(比如,成百上千的客户端要连接它),可能需要使用带 负载均衡) 的 Ceph 集群了。更多相关内容,请查看 使用 HAProxy 和 Keepalived 负载均衡的 Ceph 对象网关

安装和配置 iSCSI

为了给我们的客户端提供网络引导镜像,安装 scsi-target-utils 包:

# dnf install -y scsi-target-utils

配置 iSCSI 守护程序去提供 fc28.img 文件:

# MY_REVERSE_HOSTNAME=$(echo $MY_HOSTNAME | tr '.' "\n" | tac | tr "\n" '.' | cut -b -${#MY_HOSTNAME})
# cat << END > /etc/tgt/conf.d/fc28.conf
<target iqn.$MY_REVERSE_HOSTNAME:fc28>
  backing-store /fc28.img
  readonly 1
</target>
END

开头的 iqn./usr/lib/dracut/modules.d/40network/net-lib.sh 所需要的。

添加一个防火墙例外,并启用和启动这个服务:

# firewall-cmd --add-service=iscsi-target
# firewall-cmd --runtime-to-permanent
# systemctl enable tgtd.service
# systemctl start tgtd.service

你现在应该能够使用 tatadm 命令看到这个镜像共享了:

# tgtadm --mode target --op show

上述命令的输出应该类似如下的内容:

Target 1: iqn.edu.example.server-01:fc28
    System information:
        Driver: iscsi
        State: ready
    I_T nexus information:
    LUN information:
        LUN: 0
            Type: controller
            SCSI ID: IET     00010000
            SCSI SN: beaf10
            Size: 0 MB, Block size: 1
            Online: Yes
            Removable media: No
            Prevent removal: No
            Readonly: No
            SWP: No
            Thin-provisioning: No
            Backing store type: null
            Backing store path: None
            Backing store flags: 
        LUN: 1
            Type: disk
            SCSI ID: IET     00010001
            SCSI SN: beaf11
            Size: 10488 MB, Block size: 512
            Online: Yes
            Removable media: No
            Prevent removal: No
            Readonly: Yes
            SWP: No 
            Thin-provisioning: No
            Backing store type: rdwr
            Backing store path: /fc28.img
            Backing store flags:
    Account information:
    ACL information:
        ALL

现在,我们可以去删除本系列文章的第一部分中创建的 NFS 共享了:

# rm -f /etc/exports.d/fc28.exports
# exportfs -rv
# umount /export/fc28
# rmdir /export/fc28
# sed -i '/^\/fc28 /d' /etc/fstab

你也可以删除 /fc28 文件系统,但为了以后进一步更新,你可能需要保留它。

更新 ESP 去使用 iSCSI 内核

更新 ESP 去包含启用了 iSCSI 的 initramfs

$ rm -vf $HOME/esp/linux/*.fc28.*
$ MY_KRNL=$(ls -c /fc28/lib/modules | head -n 1)
$ cp $(find /fc28/lib/modules -maxdepth 2 -name 'vmlinuz' | grep -m 1 $MY_KRNL) $HOME/esp/linux/vmlinuz-$MY_KRNL
$ cp $(find /fc28/boot -name 'init*' | grep -m 1 $MY_KRNL) $HOME/esp/linux/initramfs-$MY_KRNL.img

更新 boot.cfg 文件去传递新的 rootnetroot 参数:

$ MY_NAME=server-01.example.edu
$ MY_EMAN=$(echo $MY_NAME | tr '.' "\n" | tac | tr "\n" '.' | cut -b -${#MY_NAME})
$ MY_ADDR=$(host -t A $MY_NAME | awk '{print $4}')
$ sed -i "s! root=[^ ]*! root=/dev/disk/by-path/ip-$MY_ADDR:3260-iscsi-iqn.$MY_EMAN:fc28-lun-1 netroot=iscsi:$MY_ADDR::::iqn.$MY_EMAN:fc28!" $HOME/esp/linux/boot.cfg

现在,你只需要从 $HOME/esp/linux 目录中复制更新后的文件到所有客户端系统的 ESP 中。你应该会看到类似下面屏幕截图的结果:

更新镜像

首先,复制出一个当前镜像的副本:

# cp -a /fc28 /fc29

chroot 进入到镜像的新副本:

# for i in dev dev/pts dev/shm proc sys run; do mount -o bind /$i /fc29/$i; done
# chroot /fc29 /usr/bin/bash --login

允许更新内核:

# sed -i 's/^exclude=kernel-\*$/#exclude=kernel-*/' /etc/dnf/dnf.conf

执行升级:

# dnf distro-sync -y --releasever=29

阻止更新过的内核被再次更新:

# sed -i 's/^#exclude=kernel-\*$/exclude=kernel-*/' /etc/dnf/dnf.conf

上述命令是可选的,但是在以后,如果在镜像中添加和更新了几个包,在你的客户端之外保存有一个最新内核的副本,会在关键时刻对你非常有帮助。

清理 dnf 的包缓存:

# dnf clean all

退出 chroot 并清理 root 的命令历史:

# logout
# for i in run sys proc dev/shm dev/pts dev; do umount /fc29/$i; done
# > /fc29/root/.bash_history

创建 iSCSI 镜像:

# FC29_SIZE=$(du -ms /fc29 | cut -f 1)
# dd if=/dev/zero of=/fc29.img bs=1MiB count=0 seek=$(($FC29_SIZE*2))
# mkfs -t xfs -L NETROOT /fc29.img
# TEMP_MNT=$(mktemp -d)
# mount /fc29.img $TEMP_MNT
# cp -a /fc29/* $TEMP_MNT
# umount $TEMP_MNT

定义一个新的 iSCSI 目标,指向到新的镜像并导出它:

# MY_HOSTNAME=$(</etc/hostname)
# MY_REVERSE_HOSTNAME=$(echo $MY_HOSTNAME | tr '.' "\n" | tac | tr "\n" '.' | cut -b -${#MY_HOSTNAME})
# cat << END > /etc/tgt/conf.d/fc29.conf
<target iqn.$MY_REVERSE_HOSTNAME:fc29>
 backing-store /fc29.img
 readonly 1
</target>
END
# tgt-admin --update ALL

添加新内核和 initramfs 到 ESP:

$ MY_KRNL=$(ls -c /fc29/lib/modules | head -n 1)
$ cp $(find /fc29/lib/modules -maxdepth 2 -name 'vmlinuz' | grep -m 1 $MY_KRNL) $HOME/esp/linux/vmlinuz-$MY_KRNL
$ cp $(find /fc29/boot -name 'init*' | grep -m 1 $MY_KRNL) $HOME/esp/linux/initramfs-$MY_KRNL.img

更新 ESP 的 boot.cfg

$ MY_DNS1=192.0.2.91
$ MY_DNS2=192.0.2.92
$ MY_NAME=server-01.example.edu
$ MY_EMAN=$(echo $MY_NAME | tr '.' "\n" | tac | tr "\n" '.' | cut -b -${#MY_NAME})
$ MY_ADDR=$(host -t A $MY_NAME | awk '{print $4}')
$ cat << END > $HOME/esp/linux/boot.cfg
#!ipxe

kernel --name kernel.efi \${prefix}/vmlinuz-$MY_KRNL initrd=initrd.img ro ip=dhcp rd.peerdns=0 nameserver=$MY_DNS1 nameserver=$MY_DNS2 root=/dev/disk/by-path/ip-$MY_ADDR:3260-iscsi-iqn.$MY_EMAN:fc29-lun-1 netroot=iscsi:$MY_ADDR::::iqn.$MY_EMAN:fc29 console=tty0 console=ttyS0,115200n8 audit=0 selinux=0 quiet
initrd --name initrd.img \${prefix}/initramfs-$MY_KRNL.img
boot || exit
END

最后,从我的 $HOME/esp/linux 目录中复制文件到所有客户端系统的 ESP 中去使用它吧!


via: https://fedoramagazine.org/how-to-build-a-netboot-server-part-2/

作者:Gregory Bartholomew 选题:lujun9972 译者:qhwdw 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

有些计算机网络需要在各个物理机器上维护相同的软件和配置。学校的计算机实验室就是这样的一个环境。 网络引导 服务器能够被配置为基于网络去提供一个完整的操作系统,以便于客户端计算机从一个中央位置获取配置。本教程将向你展示构建一台网络引导服务器的一种方法。

本教程的第一部分将包括创建一台网络引导服务器和镜像。第二部分将展示如何去添加 Kerberos 验证的家目录到网络引导配置中。

初始化配置

首先去下载 Fedora 服务器的 netinst 镜像,将它刻录到一张光盘上,然后用它引导服务器来重新格式化。我们只需要一个典型的 Fedora Server 的“最小化安装”来作为我们的开端,安装完成后,我们可以使用命令行去添加我们需要的任何额外的包。

注意:本教程中我们将使用 Fedora 28。其它版本在“最小化安装”中包含的包可能略有不同。如果你使用的是不同的 Fedora 版本,如果一个预期的文件或命令不可用,你可能需要做一些调试。

最小化安装的 Fedora Server 运行起来之后,以 root 用户登入:

$ sudo -i

并设置主机名字:

$ MY_HOSTNAME=server-01.example.edu
$ hostnamectl set-hostname $MY_HOSTNAME

注意:Red Hat 建议静态和临时名字应都要与这个机器在 DNS 中的完全合格域名相匹配,比如 host.example.com(了解主机名字)。

注意:本指南为了方便“复制粘贴”。需要自定义的任何值都声明为一个 MY_* 变量,在你运行剩余命令之前,你可能需要调整它。如果你注销之后,变量的赋值将被清除。

注意:Fedora 28 Server 在默认情况下往往会转储大量的日志到控制台上。你可以通过运行命令:sysctl -w kernel.printk=0 去禁用控制台日志输出。

接下来,我们需要在我们的服务器上配置一个静态网络地址。运行下面的一系列命令将找到并重新配置你的默认网络连接:

$ MY_DNS1=192.0.2.91
$ MY_DNS2=192.0.2.92
$ MY_IP=192.0.2.158
$ MY_PREFIX=24
$ MY_GATEWAY=192.0.2.254
$ DEFAULT_DEV=$(ip route show default | awk '{print $5}')
$ DEFAULT_CON=$(nmcli d show $DEFAULT_DEV | sed -n '/^GENERAL.CONNECTION:/s!.*:\s*!! p')
$ nohup bash << END
nmcli con mod "$DEFAULT_CON" connection.id "$DEFAULT_DEV"
nmcli con mod "$DEFAULT_DEV" connection.interface-name "$DEFAULT_DEV"
nmcli con mod "$DEFAULT_DEV" ipv4.method disabled
nmcli con up "$DEFAULT_DEV"
nmcli con add con-name br0 ifname br0 type bridge
nmcli con mod br0 bridge.stp no
nmcli con mod br0 ipv4.dns $MY_DNS1,$MY_DNS2
nmcli con mod br0 ipv4.addresses $MY_IP/$MY_PREFIX
nmcli con mod br0 ipv4.gateway $MY_GATEWAY
nmcli con mod br0 ipv4.method manual
nmcli con up br0
nmcli con add con-name br0-slave0 ifname "$DEFAULT_DEV" type bridge-slave master br0
nmcli con up br0-slave0
END

注意:上面最后的一组命令被封装到一个 nohup 脚本中,因为它将临时禁用网络。这个 nohup 命令可以让 nmcli 命令运行完成,即使你的 SSH 连接断开。注意,连接恢复可能需要 10 秒左右的时间,如果你改变了服务器 IP 地址,你将需要重新启动一个新的 SSH 连接。

注意:上面的网络配置在默认的连接之上创建了一个 网桥),这样我们在后面的测试中就可以直接运行一个虚拟机实例。如果你不想在这台服务器上去直接测试网络引导镜像,你可以跳过创建网桥的命令,并直接在你的默认网络连接上配置静态 IP 地址。

安装和配置 NFS4

从安装 nfs-utils 包开始:

$ dnf install -y nfs-utils

为发布 NFS 去创建一个顶级的 伪文件系统,然后在你的网络上共享它:

$ MY_SUBNET=192.0.2.0
$ mkdir /export
$ echo "/export -fsid=0,ro,sec=sys,root_squash $MY_SUBNET/$MY_PREFIX" > /etc/exports

SELinux 会干扰网络引导服务器的运行。为它配置例外规则超出了本教程中,因此我们这里直接禁用它:

$ sed -i '/GRUB_CMDLINE_LINUX/s/"$/ audit=0 selinux=0"/' /etc/default/grub
$ grub2-mkconfig -o /boot/grub2/grub.cfg
$ sed -i 's/SELINUX=enforcing/SELINUX=disabled/' /etc/sysconfig/selinux
$ setenforce 0
注意:应该不需要编辑 grub 命令行,但我们在测试过程中发现,直接编辑 /etc/sysconfig/selinux 被证明重启后是无效的,因此这样做再次确保设置了 selinux=0 标志。

现在,在本地防火墙中为 NFS 服务添加一个例外规则,然后启动 NFS 服务:

$ firewall-cmd --add-service nfs
$ firewall-cmd --runtime-to-permanent
$ systemctl enable nfs-server.service
$ systemctl start nfs-server.service

创建网络引导镜像

现在我们的 NFS 服务器已经启动运行了,我们需要为它提供一个操作系统镜像,以便于它提供给客户端计算机。我们将从一个非常小的镜像开始,等一切顺利之后再添加。

首先,创建一个存放我们镜像的新目录:

$ mkdir /fc28

使用 dnf 命令在新目录下用几个基础包去构建镜像:

$ dnf -y --releasever=28 --installroot=/fc28 install fedora-release systemd passwd rootfiles sudo dracut dracut-network nfs-utils vim-minimal dnf

在上面的命令中省略了很重要的 kernel 包。在它们被安装完成之前,我们需要去调整一下 initramfs 镜像中包含的驱动程序集,kernel 首次安装时将自动构建这个镜像。尤其是,我们需要禁用 hostonly 模式,以便于 initramfs 镜像能够在各种硬件平台上正常工作,并且我们还需要添加对网络和 NFS 的支持:

$ echo 'hostonly=no' > /fc28/etc/dracut.conf.d/hostonly.conf
$ echo 'add_dracutmodules+=" network nfs "' > /fc28/etc/dracut.conf.d/netboot.conf

现在,安装 kernel 包:

$ dnf -y --installroot=/fc28 install kernel

设置一个阻止 kernel 包被更新的规则:

$ echo 'exclude=kernel-*' >> /fc28/etc/dnf/dnf.conf

设置 locale:

$ echo 'LANG="en_US.UTF-8"' > /fc28/etc/locale.conf
注意:如果 locale 没有正确配置,一些程序(如 GNOME Terminal)将无法正常工作。

设置客户端的主机名字:

$ MY_CLIENT_HOSTNAME=client-01.example.edu
$ echo $MY_CLIENT_HOSTNAME > /fc28/etc/hostname

禁用控制台日志输出:

$ echo 'kernel.printk = 0 4 1 7' > /fc28/etc/sysctl.d/00-printk.conf

定义网络引导镜像中的本地 liveuser 用户:

$ echo 'liveuser:x:1000:1000::/home/liveuser:/bin/bash' >> /fc28/etc/passwd
$ echo 'liveuser::::::::' >> /fc28/etc/shadow
$ echo 'liveuser:x:1000:' >> /fc28/etc/group
$ echo 'liveuser:!::' >> /fc28/etc/gshadow

允许 liveuser 使用 sudo

$ echo 'liveuser ALL=(ALL) NOPASSWD: ALL' > /fc28/etc/sudoers.d/liveuser

启用自动创建家目录:

$ dnf install -y --installroot=/fc28 authselect oddjob-mkhomedir
$ echo 'dirs /home' > /fc28/etc/rwtab.d/home
$ chroot /fc28 authselect select sssd with-mkhomedir --force
$ chroot /fc28 systemctl enable oddjobd.service

由于多个客户端将会同时挂载我们的镜像,我们需要去配置镜像工作在只读模式中:

$ sed -i 's/^READONLY=no$/READONLY=yes/' /fc28/etc/sysconfig/readonly-root

配置日志输出到内存而不是持久存储中:

$ sed -i 's/^#Storage=auto$/Storage=volatile/' /fc28/etc/systemd/journald.conf

配置 DNS:

$ MY_DNS1=192.0.2.91
$ MY_DNS2=192.0.2.92
$ cat << END > /fc28/etc/resolv.conf
nameserver $MY_DNS1
nameserver $MY_DNS2
END

绕开编写本教程时存在的根目录只读挂载的 bug(BZ1542567):

$ echo 'dirs /var/lib/gssproxy' > /fc28/etc/rwtab.d/gssproxy
$ cat << END > /fc28/etc/rwtab.d/systemd
dirs /var/lib/systemd/catalog
dirs /var/lib/systemd/coredump
END

最后,为我们镜像创建 NFS 文件系统,并将它共享到我们的子网中:

$ mkdir /export/fc28
$ echo '/fc28 /export/fc28 none bind 0 0' >> /etc/fstab
$ mount /export/fc28
$ echo "/export/fc28 -ro,sec=sys,no_root_squash $MY_SUBNET/$MY_PREFIX" > /etc/exports.d/fc28.exports
$ exportfs -vr

创建引导加载器

现在,我们已经有了可以进行网络引导的操作系统,我们需要一个引导加载器去从客户端系统上启动它。在本教程中我们使用的是 iPXE

注意:本节和接下来的节使用 QEMU 测试,也能在另外一台单独的计算机上来完成;它们并不需要在网络引导服务器上来运行。

安装 git 并使用它去下载 iPXE:

$ dnf install -y git
$ git clone http://git.ipxe.org/ipxe.git $HOME/ipxe

现在我们需要去为我们的引导加载器创建一个指定的启动脚本:

$ cat << 'END' > $HOME/ipxe/init.ipxe
#!ipxe

prompt --key 0x02 --timeout 2000 Press Ctrl-B for the iPXE command line... && shell ||

dhcp || exit
set prefix file:///linux
chain ${prefix}/boot.cfg || exit
END

启动 “file” 下载协议:

$ echo '#define DOWNLOAD_PROTO_FILE' > $HOME/ipxe/src/config/local/general.h

安装 C 编译器以及相关的工具和库:

$ dnf groupinstall -y "C Development Tools and Libraries"

构建引导加载器:

$ cd $HOME/ipxe/src
$ make clean
$ make bin-x86_64-efi/ipxe.efi EMBED=../init.ipxe

记下新编译的引导加载器的存储位置。我们将在接下来的节中用到它:

$ IPXE_FILE="$HOME/ipxe/src/bin-x86_64-efi/ipxe.efi"

用 QEMU 测试

这一节是可选的,但是你需要去复制下面显示在物理机器上的 EFI 系统分区 的布局,在网络引导时需要去配置它们。

注意:如果你想实现一个完全的无盘系统,你也可以复制那个文件到一个 TFTP 服务器,然后从 DHCP 上指向那台服务器。

为了使用 QEMU 去测试我们的引导加载器,我们继续去创建一个仅包含一个 EFI 系统分区和我们的启动文件的、很小的磁盘镜像。

从创建 EFI 系统分区所需要的目录布局开始,然后把我们在前面节中创建的引导加载器复制进去:

$ mkdir -p $HOME/esp/efi/boot
$ mkdir $HOME/esp/linux
$ cp $IPXE_FILE $HOME/esp/efi/boot/bootx64.efi

下面的命令将识别我们的引导加载器镜像正在使用的内核版本,并将它保存到一个变量中,以备后续的配置命令去使用它:

$ DEFAULT_VER=$(ls -c /fc28/lib/modules | head -n 1)

定义我们的客户端计算机将使用的引导配置:

$ MY_DNS1=192.0.2.91
$ MY_DNS2=192.0.2.92
$ MY_NFS4=server-01.example.edu
$ cat << END > $HOME/esp/linux/boot.cfg
#!ipxe

kernel --name kernel.efi \${prefix}/vmlinuz-$DEFAULT_VER initrd=initrd.img ro ip=dhcp rd.peerdns=0 nameserver=$MY_DNS1 nameserver=$MY_DNS2 root=nfs4:$MY_NFS4:/fc28 console=tty0 console=ttyS0,115200n8 audit=0 selinux=0 quiet
initrd --name initrd.img \${prefix}/initramfs-$DEFAULT_VER.img
boot || exit
END
注意:上面的引导脚本展示了如何使用 iPXE 去网络引导 Linux 的最小示例。还可以做更多更复杂的配置。值得注意的是,iPXE 支持交互式引导菜单,它可以让你配置默认选项和超时时间。比如,一个更高级一点 iPXE 脚本可以默认从本地磁盘引导一个操作系统,如果在倒计时结束之前用户按下了一个键,才会去网络引导一个操作系统。

复制 Linux 内核并分配 initramfs 给 EFI 系统分区:

$ cp $(find /fc28/lib/modules -maxdepth 2 -name 'vmlinuz' | grep -m 1 $DEFAULT_VER) $HOME/esp/linux/vmlinuz-$DEFAULT_VER
$ cp $(find /fc28/boot -name 'init*' | grep -m 1 $DEFAULT_VER) $HOME/esp/linux/initramfs-$DEFAULT_VER.img

我们最终的目录布局应该看起来像下面的样子:

esp
├── efi
│   └── boot
│   └── bootx64.efi
└── linux
 ├── boot.cfg
 ├── initramfs-4.18.18-200.fc28.x86_64.img
 └── vmlinuz-4.18.18-200.fc28.x86_64

要让 QEMU 去使用我们的 EFI 系统分区,我们需要去创建一个小的 uefi.img 磁盘镜像来包含它,然后将它连接到 QEMU 作为主引导驱动器。

开始安装必需的工具:

$ dnf install -y parted dosfstools

现在创建 uefi.img 文件,并将 esp 目录中的文件复制进去:

$ ESP_SIZE=$(du -ks $HOME/esp | cut -f 1)
$ dd if=/dev/zero of=$HOME/uefi.img count=$((${ESP_SIZE}+5000)) bs=1KiB
$ UEFI_DEV=$(losetup --show -f $HOME/uefi.img)
$ parted ${UEFI_DEV} -s mklabel gpt mkpart EFI FAT16 1MiB 100% toggle 1 boot
$ mkfs -t msdos ${UEFI_DEV}p1
$ mkdir -p $HOME/mnt
$ mount ${UEFI_DEV}p1 $HOME/mnt
$ cp -r $HOME/esp/* $HOME/mnt
$ umount $HOME/mnt
$ losetup -d ${UEFI_DEV}

注意:在物理计算机上,你只需要从 esp 目录中复制文件到计算机上已存在的 EFI 系统分区中。你不需要使用 uefi.img 文件去引导物理计算机。

注意:在一个物理计算机上,如果文件名已存在,你可以重命名 bootx64.efi 文件,如果你重命名了它,就需要去编辑计算机的 BIOS 设置,并添加重命令后的 efi 文件到引导列表中。

接下来我们需要去安装 qemu 包:

$ dnf install -y qemu-system-x86

允许 QEMU 访问我们在本教程“初始化配置”一节中创建的网桥:

$ echo 'allow br0' > /etc/qemu/bridge.conf

创建一个 OVMF_VARS.fd 镜像的副本去保存我们虚拟机的持久 BIOS 配置:

$ cp /usr/share/edk2/ovmf/OVMF_VARS.fd $HOME

现在,启动虚拟机:

$ qemu-system-x86_64 -machine accel=kvm -nographic -m 1024 -drive if=pflash,format=raw,unit=0,file=/usr/share/edk2/ovmf/OVMF_CODE.fd,readonly=on -drive if=pflash,format=raw,unit=1,file=$HOME/OVMF_VARS.fd -drive if=ide,format=raw,file=$HOME/uefi.img -net bridge,br=br0 -net nic,model=virtio

如果一切顺利,你将看到类似下图所示的结果:

你可以使用 shutdown 命令关闭虚拟机回到我们的服务器上:

$ sudo shutdown -h now
注意:如果出现了错误或虚拟机挂住了,你可能需要启动一个新的 SSH 会话去连接服务器,使用 kill 命令去终止 qemu-system-x86_64 进程。

镜像中添加包

镜像中添加包应该是一个很简单的问题,在服务器上 chroot 进镜像,然后运行 dnf install <package_name>

在网络引导镜像中并不限制你能安装什么包。一个完整的图形化安装应该能够完美地工作。

下面是一个如何将最小化安装的网络引导镜像变成完整的图形化安装的示例:

$ for i in dev dev/pts dev/shm proc sys run; do mount -o bind /$i /fc28/$i; done
$ chroot /fc28 /usr/bin/bash --login
$ dnf -y groupinstall "Fedora Workstation"
$ dnf -y remove gnome-initial-setup
$ systemctl disable sshd.service
$ systemctl enable gdm.service
$ systemctl set-default graphical.target
$ sed -i 's/SELINUX=enforcing/SELINUX=disabled/' /etc/sysconfig/selinux
$ logout
$ for i in run sys proc dev/shm dev/pts dev; do umount /fc28/$i; done

可选地,你可能希望去启用 liveuser 用户的自动登录:

$ sed -i '/daemon/a AutomaticLoginEnable=true' /fc28/etc/gdm/custom.conf
$ sed -i '/daemon/a AutomaticLogin=liveuser' /fc28/etc/gdm/custom.conf

via: https://fedoramagazine.org/how-to-build-a-netboot-server-part-1/

作者:Gregory Bartholomew 选题:lujun9972 译者:qhwdw 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出