分类 技术 下的文章

在本系列教程中所构建的网络引导服务器有一个很重要的限制,那就是所提供的操作系统镜像是只读的。一些使用场景或许要求终端用户能够修改操作系统镜像。例如,一些教师或许希望学生能够安装和配置一些像 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中国 荣誉推出

许多 GNU/Linux 程序的一个特点是有个易于编辑的配置文件。几乎所有常见的自由软件都将配置设置保存在纯文本文件中,通常采用结构化格式,如 JSON、YAML 或“类似 ini” 的文件中。这些配置文件经常隐藏在用户的主目录中。但是,基本的 ls 不会显示它们。UNIX 标准要求以点开头的任何文件或目录名称都被视为“隐藏”,除非用户特意要求,否则不会列在目录列表中。例如,要使用 ls 列出所有文件,要传递 -a 选项。

随着时间的推移,这些配置文件会有很多定制配置,管理它们变得越来越具有挑战性。不仅如此,在多台计算机之间保持同步是大型组织所面临的共同挑战。最后,许多用户也对其独特的配置感到自豪,并希望以简单的方式与朋友分享。这就是用到 rcm 介入的地方。

rcm 是一个 “rc” 文件管理套件(“rc” 是命名配置文件的另一种约定,它已被某些 GNU/Linux 程序采用,如 screenbash)。 rcm 提供了一套命令来管理和列出它跟踪的文件。使用 dnf 安装 rcm。

开始使用

默认情况下,rcm 使用 ~/.dotfiles 来存储它管理的所有隐藏文件。一个被管理的隐藏文件实际保存在 ~/.dotfiles 目录中,而它的符号链接会放在文件原本的位置。例如,如果 ~/.bashrc 由 rcm 所管理,那么详细列表将如下所示。

[link@localhost ~]$ ls -l ~/.bashrc
lrwxrwxrwx. 1 link link 27 Dec 16 05:19 .bashrc -> /home/link/.dotfiles/bashrc
[link@localhost ~]$

rcm 包含 4 个命令:

  • mkrc – 将文件转换为由 rcm 管理的隐藏文件
  • lsrc – 列出由 rcm 管理的文件
  • rcup – 同步由 rcm 管理的隐藏文件
  • rcdn – 删除 rcm 管理的所有符号链接

在两台计算机上共享 bashrc

如今用户在多台计算机上拥有 shell 帐户并不罕见。在这些计算机之间同步隐藏文件可能是一个挑战。这里将提供一种可能的解决方案,仅使用 rcm 和 git。

首先使用 mkrc 将文件转换成由 rcm 管理的文件。

[link@localhost ~]$ mkrc -v ~/.bashrc
Moving...
'/home/link/.bashrc' -> '/home/link/.dotfiles/bashrc'
Linking...
'/home/link/.dotfiles/bashrc' -> '/home/link/.bashrc'
[link@localhost ~]$

接下来使用 lsrc 验证列表是否正确。

[link@localhost ~]$ lsrc
/home/link/.bashrc:/home/link/.dotfiles/bashrc
[link@localhost ~]$

现在在 ~/.dotfiles 中创建一个 git 仓库,并使用你选择的 git 仓库托管设置一个远程仓库。提交 bashrc 文件并推送一个新分支。

[link@localhost ~]$ cd ~/.dotfiles
[link@localhost .dotfiles]$ git init
Initialized empty Git repository in /home/link/.dotfiles/.git/
[link@localhost .dotfiles]$ git remote add origin [email protected]:linkdupont/dotfiles.git
[link@localhost .dotfiles]$ git add bashrc
[link@localhost .dotfiles]$ git commit -m "initial commit"
[master (root-commit) b54406b] initial commit
1 file changed, 15 insertions(+)
create mode 100644 bashrc
[link@localhost .dotfiles]$ git push -u origin master
...
[link@localhost .dotfiles]$

在第二台机器上,克隆这个仓库到 ~/.dotfiles 中。

[link@remotehost ~]$ git clone [email protected]:linkdupont/dotfiles.git ~/.dotfiles
...
[link@remotehost ~]$

现在使用 rcup 更新受 rcm 管理的符号链接。

[link@remotehost ~]$ rcup -v
replacing identical but unlinked /home/link/.bashrc
removed '/home/link/.bashrc'
'/home/link/.dotfiles/bashrc' -> '/home/link/.bashrc'
[link@remotehost ~]$

覆盖现有的 ~/.bashrc(如果存在)并重启 shell。

就是这些了!指定主机选项 (-o) 是对上面这种情况的有用补充。如往常一样,请阅读手册页。它们包含了很多示例命令。


via: https://fedoramagazine.org/managing-dotfiles-rcm/

作者:Link Dupont 选题:lujun9972 译者:geekpi 校对:wxy

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

Paul Brown 解释了 Linux shell 命令中那个不起眼的“点”的各种意思和用法。

在现实情况中,使用 shell 命令编写的单行命令或脚本可能会令人很困惑。你使用的很多工具的名称与它们的实际功能相差甚远(grepteeawk,还有吗?),而当你将两个或更多个组合起来时,所组成的 “句子” 看起来更像某种外星人的天书。

因此,上面说的这些对于你并无帮助,因为你用来编写一连串的指令所使用的符号根据你使用的场景有着不同的意义。

位置、位置、位置

就拿这个不起眼的点(.)来说吧。当它放在一个需要一个目录名称的命令的参数处时,表示“当前目录”:

find . -name "*.jpg"

意思就是“在当前目录(包括子目录)中寻找以 .jpg 结尾的文件”。

ls .cd . 结果也如你想的那样,它们分别列举和“进入”到当前目录,虽然在这两种情况下这个点都是多余的。

而一个紧接着另一个的两个点呢,在同样的场景下(即当你的命令期望一个文件目录的时候)表示“当前目录的父目录”。如果你当前在 /home/your_directory 下并且运行:

cd ..

你就会进入到 /home。所以,你可能认为这仍然适合“点代表附近目录”的叙述,并且毫不复杂,对吧?

那下面这样会怎样呢?如果你在一个文件或目录的开头加上点,它表示这个文件或目录会被隐藏:

$ touch somedir/file01.txt somedir/file02.txt somedir/.secretfile.txt
$ ls -l somedir/
total 0
-rw-r--r-- 1 paul paul 0 Jan 13 19:57 file01.txt
-rw-r--r-- 1 paul paul 0 Jan 13 19:57 file02.txt
$ # 注意上面列举的文件中没有 .secretfile.txt
$ ls -la somedir/
total 8
drwxr-xr-x 2 paul paul 4096 Jan 13 19:57 .
drwx------ 48 paul paul 4096 Jan 13 19:57 ..
-rw-r--r-- 1 paul paul 0 Jan 13 19:57 file01.txt
-rw-r--r-- 1 paul paul 0 Jan 13 19:57 file02.txt
-rw-r--r-- 1 paul paul 0 Jan 13 19:57 .secretfile.txt
$ # 这个 -a  选项告诉 ls 去展示“all”文件,包括那些隐藏的

然后就是你可以将 . 当作命令。是的,你听我说:. 是个真真正正的命令。它是 source 命令的代名词,所以你可以用它在当前 shell 中执行一个文件,而不是以某种其它的方式去运行一个脚本文件(这通常指的是 Bash 会产生一个新的 shell 去运行它)

很困惑?别担心 —— 试试这个:创建一个名为 myscript 的脚本,内容包含下面一行:

myvar="Hello"

然后通过常规的方法执行它,也就是用 sh myscript(或者通过 chmod a+x myscript 命令让它可执行,然后运行 ./myscript)。现在尝试并且观察 myvar 的内容,通过 echo $myvar(理所当然你什么也得不到)。那是因为,当你的脚本赋值 "Hello"myvar 时,它是在一个隔离的bash shell 实例中进行的。当脚本运行结束时,这个新产生的实例会消失并且将控制权转交给原来的shell,而原来的 shell 里甚至都不存在 myvar 变量。

然而,如果你这样运行 myscript

. myscript

echo $myvar 就会打印 Hello 到命令行上。

当你的 .bashrc 文件发生变化后,你经常会用到 .(或 source)命令,就像当你要扩展 PATH 变量那样。在你的当前 shell 实例中,你可以使用 . 来让变化立即生效。

双重麻烦

就像看似无关紧要的一个点有多个含义一样,两个点也是如此。除了指向当前目录的父级之外,两个点(..)也用于构建序列。

尝试下这个:

echo {1..10}

它会打印出从 1 到 10 的序列。在这种场景下,.. 表示 “从左边的值开始,计数到右边的值”。

现在试下这个:

echo {1..10..2}

你会得到 1 3 5 7 9..2 这部分命令告诉 Bash 输出这个序列,不过不是每个相差 1,而是相差 2。换句话说,就是你会得到从 1 到 10 之间的奇数。

它反着也仍然有效:

echo {10..1..2}

你也可以用多个 0 填充你的数字。这样:

echo {000..121..2}

会这样打印出从 0 到 121 之间的偶数(填充了前置 0):

000 002 004 006 ... 050 052 054 ... 116 118 120

不过这样的序列发生器有啥用呢?当然,假设您的新年决心之一是更加谨慎控制您的帐户花销。作为决心的一部分,您需要创建目录,以便对过去 10 年的数字发票进行分类:

mkdir {2009..2019}_Invoices

工作完成。

或者你可能有数百个带编号的文件,比如从视频剪辑中提取的帧,或许因为某种原因,你只想从第 43 帧到第 61 帧每隔三帧删除一帧:

rm frame_{043..61..3}

很可能,如果你有超过 100 个帧,它们将以填充 0 命名,如下所示:

frame_000 frame_001 frame_002 ...

那就是为什么你在命令中要用 043,而不是43 的原因。

花括号花招

说实话,序列的神奇之处不在于双点,而是花括号({})的巫术。看看它对于字母是如何工作的。这样做:

touch file_{a..z}.txt

它创建了从 file_a.txtfile_z.txt 的文件。

但是,你必须小心。使用像 {Z..a} 这样的序列将产生一大堆大写字母和小写字母之间的非字母、数字的字符(既不是数字或字母的字形)。其中一些字形是不可打印的或具有自己的特殊含义。使用它们来生成文件名称可能会导致一系列意外和可能令人不快的影响。

最后一件值得指出的事:包围在 {...} 的序列,它们也可以包含字符串列表:

touch {blahg,splurg,mmmf}_file.txt

将创建了 blahg_file.txtsplurg_file.txtmmmf_file.txt

当然,在别的场景中,大括号也有不同的含义(惊喜吗!)。不过那是别的文章的内容了。

总结

Bash 以及运行于其中的各种工具已经被寻求解决各种特定问题的系统管理员们把玩了数十年。要说这种有自己之道的系统管理员是一种特殊物种的话,那是有点轻描淡写。总而言之,与其他语言相反,Bash 的设计目标并不是为了用户友好、简单、甚至合乎逻辑。

但这并不意味着它不强大 —— 恰恰相反。Bash 的语法和 shell 工具可能不一致且很庞大,但它们也提供了一系列令人眼花缭乱的方法来完成您可能想象到的一切。就像有一个工具箱,你可以从中找到从电钻到勺子的所有东西,以及橡皮鸭、一卷胶带和一些指甲钳。

除了引人入胜之外,探明你可以直接在 shell 中达成的所有能力也很有趣,所以下次我们将深入探讨如何构建更大更好的 Bash 命令行。

在那之前,玩得开心!


via: https://www.linux.com/blog/learn/2019/1/linux-tools-meaning-dot

作者:Paul Brown 选题:lujun9972 译者:asche910 校对:wxy

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

前几天,我想知道如何将多个文件类型从一个目录移动(不复制)到另一个目录。我已经知道如何查找并将某些类型的文件从一个目录复制到另一个目录。但是,我不知道如何同时移动多种文件类型。如果你曾遇到这样的情况,我知道在类 Unix 系统中从命令行执行该操作的一个简单方法。

同时移动多种文件类型

想象一下这种场景,你在名为 dir1 的目录中有多种类型的文件,例如 .pdf、 .doc、 .mp3、 .mp4、 .txt 等等。我们来看看 dir1 的内容:

$ ls dir1
file.txt image.jpg mydoc.doc personal.pdf song.mp3 video.mp4

你希望将某些文件类型(不是所有文件类型)移动到另一个位置。例如,假设你想将 .doc、 .pdf 和 .txt 文件一次性移动到名为 dir2 的另一个目录中。

要同时将 .doc、 .pdf 和 .txt 文件从 dir1 移动到 dir2,命令是:

$ mv dir1/*.{doc,pdf,txt} dir2/

很容易,不是吗?

现在让我们来查看一下 dir2 的内容:

$ ls dir2/
file.txt mydoc.doc personal.pdf

看到了吗?只有 .doc、 .pdf 和 .txt 从 dir1 移到了 dir2

在上面的命令中,你可以在花括号内添加任意数量的文件类型,以将它们移动到不同的目录中。它在 Bash 上非常适合我。

另一种移动多种文件类型的方法是转到源目录,在我们的例子中即为 dir1

$ cd ~/dir1

将你选择的文件类型移动到目的地(即 dir2),如下所示:

$ mv *.doc *.txt *.pdf /home/sk/dir2/

要移动具有特定扩展名的所有文件,例如 .doc,运行:

$ mv dir1/*.doc dir2/

更多细节,参考 man 页:

$ man mv

移动一些相同或不同的文件类型很容易!你可以在 GUI 模式下单击几下鼠标,或在 CLI 模式下使用一行命令来完成。但是,如果目录中有数千种不同的文件类型,并且希望一次将多种文件类型移动到不同的目录,这将是一项繁琐的任务。对我来说,上面的方法很容易完成工作!如果你知道任何其它一行命令可以一次移动多种文件类型,请在下面的评论部分与我们分享。我会核对并更新指南。

这些就是全部了,希望这很有用。更多好东西将要来了,敬请关注!

共勉!


via: https://www.ostechnix.com/how-to-move-multiple-file-types-simultaneously-from-commandline/

作者:SK 选题:lujun9972 译者:MjSeven 校对:wxy

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

简介

对于最后的项目,你有两个选择:

  • 继续使用你自己的 JOS 内核并做 实验 6,包括实验 6 中的一个挑战问题。(你可以随意地、以任何有趣的方式去扩展实验 6 或者 JOS 的任何部分,当然了,这不是课程规定的。)
  • 在一个、二个或三个人组成的团队中,你选择去做一个涉及了你的 JOS 的项目。这个项目必须是涉及到与实验 6 相同或更大的领域(如果你是团队中的一员)。

目标是为了获得乐趣或探索更高级的 O/S 的话题;你不需要做最新的研究。

如果你做了你自己的项目,我们将根据你的工作量有多少、你的设计有多优雅、你的解释有多高明、以及你的解决方案多么有趣或多有创意来为你打分。我们知道时间有限,因此也不期望你能在本学期结束之前重写 Linux。要确保你的目标是合理的;合理地设定一个绝对可以实现的最小目标(即:控制你的实验 6 的规模),如果进展顺利,可以设定一个更大的目标。

如果你做了实验 6,我们将根据你是否通过了测试和挑战练习来为你打分。

交付期限

11 月 3 日:Piazza 讨论和 1、2、或 3 年级组选择(根据你的最终选择来定)。使用在 Piazza 上的 lab7 标记/目录。在 Piazza 上的文章评论区与其它人计论想法。使用这些文章帮你去找到有类似想法的其它学生一起组建一个小组。课程的教学人员将在 Piazza 上为你的项目想法给出反馈;如果你想得到更详细的反馈,可以与我们单独讨论。

.

11 月 9 日:在 提交网站 上提交一个提议,只需要一到两个段落就可以。提议要包括你的小组成员列表、你的计划、以及明确的设计和实现打算。(如果你做实验 6,就不用做这个了)

.

12 月 7 日:和你的简短报告一起提交源代码。将你的报告放在与名为 “README.pdf” 的文件相同的目录下。由于你只是这个实验任务小组中的一员,你可能需要去使用 git 在小组成员之间共享你的项目代码。因此你需要去决定哪些源代码将作为你的小组项目的共享起始点。一定要为你的最终项目去创建一个分支,并且命名为 lab7。(如果你做了实验 6,就按实验 6 的提交要求做即可。)

.

12 月 11 日这一周:简短的课堂演示。为你的 JOS 项目准备一个简短的课堂演示。为了你的项目演示,我们将提供一个投影仪。根据小组数量和每个小组选择的项目类型,我们可能会限制总的演讲数,并且有些小组可能最终没有机会上台演示。

.

12 月 11 日这一周:助教们验收。向助教演示你的项目,因此我们可能会提问一些问题,去了解你所做的一些细节。

项目想法

如果你不做实验 6,下面是一个启迪你的想法列表。但是,你应该大胆地去实现你自己的想法。其中一些想法只是一个开端,并且本身不在实验 6 的领域内,并且其它的可能是在更大的领域中。

  • 使用 x86 虚拟机支持 去构建一个能够运行多个访客系统(比如,多个 JOS 实例)的虚拟机监视器。
  • 使用 Intel SGX 硬件保护机制做一些有用的事情。这是使用 Intel SGX 的最新的论文
  • 让 JOS 文件系统支持写入、文件创建、为持久性使用日志、等等。或许你可以从 Linux EXT3 上找到一些启示。
  • 软更新WAFL、ZFS、或其它较高级的文件系统上找到一些使用文件系统的想法。
  • 给一个文件系统添加快照功能,以便于用户能够查看过去的多个时间点上的文件系统。为了降低空间使用量,你或许要使用一些写时复制技术。
  • 使用分页去提供实时共享的内存,来构建一个 分布式的共享内存(DSM)系统,以便于你在一个机器集群上运行多线程的共享内存的并行程序。当一个线程尝试去访问位于另外一个机器上的页时,页故障将给 DSM 系统提供一个机会,让它基于网络去从当前存储这个页的任意一台机器上获取这个页。
  • 允许进程在机器之间基于网络进行迁移。你将需要做一些关于一个进程状态的多个片段方面的事情,但是由于在 JOS 中许多状态是在用户空间中,它或许从 Linux 上的进程迁移要容易一些。
  • 在 JOS 中实现 分页 到磁盘,这样那个进程使用的内存就可以大于真实的内存。使用交换空间去扩展你的内存。
  • 为 JOS 实现文件的 mmap()
  • 使用 xfi 将一个进程的代码沙箱化。
  • 支持 x86 的 2MB 或 4MB 的页大小)。
  • 修改 JOS 让内核支持进程内的线程。从查看 课堂上的 uthread 任务 去开始。实现调度器触发将是实现这个项目的一种方式。
  • 在 JOS 的内核中或文件系统中(实现多线程之后),使用细粒度锁或无锁并发。Linux 内核使用 读复制更新 去执行无需上锁的读取操作。通过在 JOS 中实现它来探索 RCU,并使用它去支持无锁读取的名称缓存。
  • 实现 外内核论文 中的想法。例如包过滤器。
  • 使 JOS 拥有软实时行为。用它来辨识一些应用程序时非常有用。
  • 使 JOS 运行在 64 位 CPU 上。这包括重设计虚拟内存让它使用 4 级页表。有关这方面的文档,请查看 参考页
  • 移植 JOS 到一个不同的微处理器。这个 osdev wiki 或许对你有帮助。
  • 为 JOS 系统增加一个“窗口”系统,包括图形驱动和鼠标。有关这方面的文档,请查看 参考页sqrt(x) 就是一个 JOS “窗口” 系统的示例。
  • 在 JOS 中实现 dune,以提供特权硬件指令给用户空间应用程序。
  • 写一个用户级调试器,添加类似跟踪的功能;硬件寄存器概要(即:Oprofile);调用跟踪等等。
  • 为(静态的)Linux 可运行程序做一个二进制仿真。

via: https://pdos.csail.mit.edu/6.828/2018/labs/lab7/

作者:csail.mit 选题:lujun9972 译者:qhwdw 校对:wxy

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

这篇文章将帮助你了解 Linux 中 /etc/services 文件,包括它的内容,格式以及重要性。

Internet 守护程序(ineted)是 Linux 世界中的重要服务。它借助 /etc/services 文件来处理所有网络服务。在本文中,我们将向你介绍这个文件的内容,格式以及它对于 Linux 系统的意义。

/etc/services 文件包含网络服务和它们映射端口的列表。inetdxinetd 会查看这些细节,以便在数据包到达各自的端口或服务有需求时,它会调用特定的程序。

作为普通用户,你可以查看此文件,因为文件一般都是可读的。要编辑此文件,你需要有 root 权限。

$ ll /etc/services
-rw-r--r--. 1 root root 670293 Jun  7  2013 /etc/services

/etc/services 文件格式

service-name    port/protocol   [aliases..]  [#comment]

最后两个字段是可选的,因此用 [ ] 表示。

其中:

  • service-name 是网络服务的名称。例如 telnetftp 等。
  • port/protocol 是网络服务使用的端口(一个数值)和服务通信使用的协议(TCP/UDP)。
  • alias 是服务的别名。
  • comment 是你可以添加到服务的注释或说明。以 # 标记开头。

/etc/services 文件示例

# 每行描述一个服务,形式如下:
#
# service-name  port/protocol  [aliases ...]   [# comment]

tcpmux          1/tcp                           # TCP port service multiplexer
rje             5/tcp                           # Remote Job Entry
echo            7/udp
discard         9/udp           sink null

在这里,你可以看到可选的最后两个字段的用处。discard 服务的别名为 sinknull


via: https://kerneltalks.com/linux/understanding-etc-services-file-in-linux/

作者:kerneltalks 选题:lujun9972 译者:MjSeven 校对:wxy

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