1
0
mirror of https://github.com/systemd/systemd.git synced 2025-03-29 06:50:16 +03:00

Merge pull request #19096 from poettering/repart-features

repart: four new features: CopyBlocks=auto + --image= + ReadOnly=/Flags= + MakeDirectories=
This commit is contained in:
Lennart Poettering 2021-04-20 10:20:22 +02:00 committed by GitHub
commit fd2f6f7248
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 1064 additions and 155 deletions

2
TODO
View File

@ -400,8 +400,6 @@ Features:
* systemd-repart: allow sizing partitions as factor of available RAM, so that
we can reasonably size swap partitions for hibernation.
* systemd-repart: allow managing the gpt read-only partition flag + auto-mount flag
* systemd-repart: allow boolean option that ensures that if existing partition
doesn't exist within the configured size bounds the whole command fails. This
is useful to implement ESP vs. XBOOTLDR schemes in installers: have one set

View File

@ -422,12 +422,25 @@
<varlistentry>
<term><varname>CopyBlocks=</varname></term>
<listitem><para>Takes a path to a regular file, block device node or directory. If specified and the
partition is newly created the data from the specified path is written to the newly created
partition, on the block level. If a directory is specified the backing block device of the file
system the directory is on is determined and the data read directly from that. This option is useful
to efficiently replicate existing file systems on the block level on a new partition, for example to
build a simple OS installer or OS image builder.</para>
<listitem><para>Takes a path to a regular file, block device node or directory, or the special value
<literal>auto</literal>. If specified and the partition is newly created, the data from the specified
path is written to the newly created partition, on the block level. If a directory is specified, the
backing block device of the file system the directory is on is determined, and the data read directly
from that. This option is useful to efficiently replicate existing file systems onto new partitions
on the block level — for example to build a simple OS installer or an OS image builder.</para>
<para>If the special value <literal>auto</literal> is specified, the source to copy from is
automatically picked up from the running system (or the image specified with
<option>--image=</option> — if used). A partition that matches both the configured partition type (as
declared with <varname>Type=</varname> above), and the currently mounted directory appropriate for
that partition type is determined. For example, if the partition type is set to
<literal>root</literal> the partition backing the root directory (<filename>/</filename>) is used as
source to copy from — if its partition type is set to <literal>root</literal> as well. If the
declared type is <literal>usr</literal> the partition backing <filename>/usr/</filename> is used as
source to copy blocks from — if its partition type is set to <literal>usr</literal> too. The logic is
capable of automatically tracking down the the backing partitions for encrypted and Verity-enabled
volumes. <literal>CopyBlocks=auto</literal> is useful for implementing "self-replicating" systems,
i.e. systems that are their own installer.</para>
<para>The file specified here must have a size that is a multiple of the basic block size 512 and not
be empty. If this option is used, the size allocation algorithm is slightly altered: the partition is
@ -486,7 +499,38 @@
<para>The copy operation is executed before the file system is registered in the partition table,
thus ensuring that a file system populated this way only ever exists fully initialized.</para>
<para>This option cannot be combined with <varname>CopyBlocks=</varname>.</para></listitem>
<para>This option cannot be combined with <varname>CopyBlocks=</varname>.</para>
<para>When <command>systemd-repart</command> is invoked with the <option>--image=</option> or
<option>--root=</option> command line switches the source paths specified are taken relative to the
specified root directory or disk image root.</para></listitem>
</varlistentry>
<varlistentry>
<term><varname>MakeDirectories=</varname></term>
<listitem><para>akes one or more absolute paths, separated by whitespace, each declaring a directory
to create within the new file system. Behaviour is similar to <varname>CopyFiles=</varname>, but
instead of copying in a set of files this just creates the specified directories with the default
mode of 0755 owned by the root user and group, plus all their parent directories (with the same
ownership and access mode). To configure directories with different ownership or access mode, use
<varname>CopyFiles=</varname> and specify a source tree to copy containing appropriately
owned/configured directories. This option may be used more than once to create multiple
directories. When <varname>CopyFiles=</varname> and <varname>MakeDirectories=</varname> are used
together the former is applied first. If a directory listed already exists no operation is executed
(in particular, the ownership/access mode of the directories is left as is).</para>
<para>The primary usecase for this option is to create a minimal set of directories that may be
mounted over by other partitions contained in the same disk image. For example, a disk image where
the root file system is formatted at first boot might want to automatically pre-create
<filename>/usr/</filename> in it this way, so that the <literal>usr</literal> partition may
over-mount it.</para>
<para>Consider using
<citerefentry><refentrytitle>systemd-tmpfiles</refentrytitle><manvolnum>8</manvolnum></citerefentry>
with its <option>--image=</option> option to pre-create other, more complex directory hierarchies (as
well as other inodes) with fine-grained control of ownership, access modes and other file
attributes.</para></listitem>
</varlistentry>
<varlistentry>
@ -521,6 +565,29 @@
factory reset operation. This functionality is useful to implement schemes where images can be reset
into their original state by removing partitions and creating them anew. Defaults to off.</para></listitem>
</varlistentry>
<varlistentry>
<term><varname>Flags=</varname></term>
<listitem><para>Configures the 64bit GPT partition flags to set for the partition when creating
it. This option has no effect if the partition already exists. If not specified the flags values is
set to all zeroes, except if the partition type (as configured with <varname>Type=</varname> above)
refers to a Verity partition, in wich case bit 60 is set (i.e. the read-only bit). This bit may also
be configured separately via <varname>ReadOnly=</varname>, see below. Specify the flags value in
hexadecimal (by prefixing it with <literal>0x</literal>), binary (prefix <literal>0b</literal>) or
decimal (no prefix).</para></listitem>
</varlistentry>
<varlistentry>
<term><varname>ReadOnly=</varname></term>
<listitem><para>Configures the Read-Only partition flags (bit 60) of the partition table entry. This
option is a friendly way to set bit 60 of the partition flags value without setting any of the other
bits, and may be set via <varname>Flags=</varname> too, see above.</para>
<para>If both <varname>Flags=</varname> and <varname>ReadOnly=</varname> are set the latter controls
the value of the flag.</para></listitem>
</varlistentry>
</variablelist>
</refsect1>

View File

@ -40,16 +40,17 @@
<citerefentry><refentrytitle>repart.d</refentrytitle><manvolnum>5</manvolnum></citerefentry>.
</para>
<para>If invoked with no arguments, it operates on the block device backing the root file system partition
of the OS, thus growing and adding partitions of the booted OS image itself. When called in the initial
RAM disk it operates on the block device backing <filename>/sysroot/</filename> instead, i.e. on the
block device the system will soon transition into. The <filename>systemd-repart.service</filename>
service is generally run at boot in the initial RAM disk, in order to augment the partition table of the
OS before its partitions are mounted. <command>systemd-repart</command> (mostly) operates in a purely
incremental mode: it only grows existing and adds new partitions; it does not shrink, delete or move
existing partitions. The service is intended to be run on every boot, but when it detects that the
partition table already matches the installed <filename>repart.d/*.conf</filename> configuration
files, it executes no operation.</para>
<para>If invoked with no arguments, it operates on the block device backing the root file system
partition of the running OS, thus growing and adding partitions of the booted OS image itself. If
<varname>--image=</varname> is used it will operate on the specified image file. When called in the
<literal>initrd</literal> it operates on the block device backing <filename>/sysroot/</filename> instead,
i.e. on the block device the system will soon transition into. The
<filename>systemd-repart.service</filename> service is generally run at boot in the initial RAM disk, in
order to augment the partition table of the OS before its partitions are
mounted. <command>systemd-repart</command> (mostly) operates in a purely incremental mode: it only grows
existing and adds new partitions; it does not shrink, delete or move existing partitions. The service is
intended to be run on every boot, but when it detects that the partition table already matches the
installed <filename>repart.d/*.conf</filename> configuration files, it executes no operation.</para>
<para><command>systemd-repart</command> is intended to be used when deploying OS images, to automatically
adjust them to the system they are running on, during first boot. This way the deployed image can be
@ -251,13 +252,21 @@
<term><option>--root=</option></term>
<listitem><para>Takes a path to a directory to use as root file system when searching for
<filename>repart.d/*.conf</filename> files and for the machine ID file to use as seed. By default
when invoked on the regular system this defaults to the host's root file system
<filename>repart.d/*.conf</filename> files, for the machine ID file to use as seed and for the
<varname>CopyFiles=</varname> and <varname>CopyBlocks=</varname> source files and directories. By
default when invoked on the regular system this defaults to the host's root file system
<filename>/</filename>. If invoked from the initial RAM disk this defaults to
<filename>/sysroot/</filename>, so that the tool operates on the configuration and machine ID stored
in the root file system later transitioned into itself.</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--image=</option></term>
<listitem><para>Takes a path to a disk image file or device to mount and use in a similar fashion to
<option>--root=</option>, see above.</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--seed=</option></term>

View File

@ -1853,7 +1853,7 @@ int setup_namespace(
r = loop_device_make_by_path(
root_image,
FLAGS_SET(dissect_image_flags, DISSECT_IMAGE_READ_ONLY) ? O_RDONLY : -1 /* < 0 means writable if possible, read-only as fallback */,
FLAGS_SET(dissect_image_flags, DISSECT_IMAGE_DEVICE_READ_ONLY) ? O_RDONLY : -1 /* < 0 means writable if possible, read-only as fallback */,
FLAGS_SET(dissect_image_flags, DISSECT_IMAGE_NO_PARTITION_TABLE) ? 0 : LO_FLAGS_PARTSCAN,
&loop_device);
if (r < 0)

View File

@ -770,7 +770,7 @@ static int run(int argc, char *argv[]) {
r = loop_device_make_by_path(
arg_image,
FLAGS_SET(arg_flags, DISSECT_IMAGE_READ_ONLY) ? O_RDONLY : O_RDWR,
FLAGS_SET(arg_flags, DISSECT_IMAGE_DEVICE_READ_ONLY) ? O_RDONLY : O_RDWR,
FLAGS_SET(arg_flags, DISSECT_IMAGE_NO_PARTITION_TABLE) ? 0 : LO_FLAGS_PARTSCAN,
&d);
if (r < 0)

File diff suppressed because it is too large Load Diff

View File

@ -61,4 +61,12 @@ int cryptsetup_get_token_as_json(struct crypt_device *cd, int idx, const char *v
int cryptsetup_get_keyslot_from_token(JsonVariant *v);
int cryptsetup_add_token_json(struct crypt_device *cd, JsonVariant *v);
#else
/* If libcryptsetup is not available, let's at least define the basic type and NOP destructors for it, to
* make a little bit less #ifdeferry necessary in main programs. */
struct crypt_device;
static inline void sym_crypt_free(struct crypt_device* cd) {}
static inline void sym_crypt_freep(struct crypt_device** cd) {}
#endif

View File

@ -1408,7 +1408,7 @@ static int mount_partition(
if (streq(fstype, "crypto_LUKS"))
return -EUNATCH;
rw = m->rw && !(flags & DISSECT_IMAGE_READ_ONLY);
rw = m->rw && !(flags & DISSECT_IMAGE_MOUNT_READ_ONLY);
if (FLAGS_SET(flags, DISSECT_IMAGE_FSCK) && rw) {
r = run_fsck(node, fstype);
@ -1463,6 +1463,27 @@ static int mount_partition(
if (!strextend_with_separator(&options, ",", m->mount_options))
return -ENOMEM;
/* So, when you request MS_RDONLY from ext4, then this means nothing. It happily still writes to the
* backing storage. What's worse, the BLKRO[GS]ET flag and (in case of loopback devices)
* LO_FLAGS_READ_ONLY don't mean anything, they affect userspace accesses only, and write accesses
* from the upper file system still get propagated through to the underlying file system,
* unrestricted. To actually get ext4/xfs/btrfs to stop writing to the device we need to specify
* "norecovery" as mount option, in addition to MS_RDONLY. Yes, this sucks, since it means we need to
* carry a per file system table here.
*
* Note that this means that we might not be able to mount corrupted file systems as read-only
* anymore (since in some cases the kernel implementations will refuse mounting when corrupted,
* read-only and "norecovery" is specified). But I think for the case of automatically determined
* mount options for loopback devices this is the right choice, since otherwise using the same
* loopback file twice even in read-only mode, is going to fail badly sooner or later. The usecase of
* making reuse of the immutable images "just work" is more relevant to us than having read-only
* access that actually modifies stuff work on such image files. Or to say this differently: if
* people want their file systems to be fixed up they should just open them in writable mode, where
* all these problems don't exist. */
if (!rw && STRPTR_IN_SET(fstype, "ext3", "ext4", "xfs", "btrfs"))
if (!strextend_with_separator(&options, ",", "norecovery"))
return -ENOMEM;
r = mount_nofollow_verbose(LOG_DEBUG, node, p, fstype, MS_NODEV|(rw ? 0 : MS_RDONLY), options);
if (r < 0)
return r;
@ -1756,7 +1777,7 @@ static int decrypt_partition(
return log_debug_errno(r, "Failed to load LUKS metadata: %m");
r = sym_crypt_activate_by_passphrase(cd, name, CRYPT_ANY_SLOT, passphrase, strlen(passphrase),
((flags & DISSECT_IMAGE_READ_ONLY) ? CRYPT_ACTIVATE_READONLY : 0) |
((flags & DISSECT_IMAGE_DEVICE_READ_ONLY) ? CRYPT_ACTIVATE_READONLY : 0) |
((flags & DISSECT_IMAGE_DISCARD_ON_CRYPTO) ? CRYPT_ACTIVATE_ALLOW_DISCARDS : 0));
if (r < 0) {
log_debug_errno(r, "Failed to activate LUKS device: %m");
@ -2674,7 +2695,7 @@ int mount_image_privately_interactively(
r = loop_device_make_by_path(
image,
FLAGS_SET(flags, DISSECT_IMAGE_READ_ONLY) ? O_RDONLY : O_RDWR,
FLAGS_SET(flags, DISSECT_IMAGE_DEVICE_READ_ONLY) ? O_RDONLY : O_RDWR,
FLAGS_SET(flags, DISSECT_IMAGE_NO_PARTITION_TABLE) ? 0 : LO_FLAGS_PARTSCAN,
&d);
if (r < 0)

View File

@ -87,13 +87,13 @@ static inline PartitionDesignator PARTITION_VERITY_OF(PartitionDesignator p) {
}
typedef enum DissectImageFlags {
DISSECT_IMAGE_READ_ONLY = 1 << 0,
DISSECT_IMAGE_DEVICE_READ_ONLY = 1 << 0, /* Make device read-only */
DISSECT_IMAGE_DISCARD_ON_LOOP = 1 << 1, /* Turn on "discard" if on a loop device and file system supports it */
DISSECT_IMAGE_DISCARD = 1 << 2, /* Turn on "discard" if file system supports it, on all block devices */
DISSECT_IMAGE_DISCARD_ON_CRYPTO = 1 << 3, /* Turn on "discard" also on crypto devices */
DISSECT_IMAGE_DISCARD_ANY = DISSECT_IMAGE_DISCARD_ON_LOOP |
DISSECT_IMAGE_DISCARD |
DISSECT_IMAGE_DISCARD_ON_CRYPTO,
DISSECT_IMAGE_DISCARD_ANY = DISSECT_IMAGE_DISCARD_ON_LOOP |
DISSECT_IMAGE_DISCARD |
DISSECT_IMAGE_DISCARD_ON_CRYPTO,
DISSECT_IMAGE_GPT_ONLY = 1 << 4, /* Only recognize images with GPT partition tables */
DISSECT_IMAGE_GENERIC_ROOT = 1 << 5, /* If no partition table or only single generic partition, assume it's the root fs */
DISSECT_IMAGE_MOUNT_ROOT_ONLY = 1 << 6, /* Mount only the root and /usr partitions */
@ -107,6 +107,9 @@ typedef enum DissectImageFlags {
DISSECT_IMAGE_MKDIR = 1 << 14, /* Make top-level directory to mount right before mounting, if missing */
DISSECT_IMAGE_USR_NO_ROOT = 1 << 15, /* If no root fs is in the image, but /usr is, then allow this (so that we can mount the rootfs as tmpfs or so */
DISSECT_IMAGE_REQUIRE_ROOT = 1 << 16, /* Don't accept disks without root partition (or at least /usr partition if DISSECT_IMAGE_USR_NO_ROOT is set) */
DISSECT_IMAGE_MOUNT_READ_ONLY = 1 << 17, /* Make mounts read-only */
DISSECT_IMAGE_READ_ONLY = DISSECT_IMAGE_DEVICE_READ_ONLY |
DISSECT_IMAGE_MOUNT_READ_ONLY,
} DissectImageFlags;
struct DissectedImage {

View File

@ -106,3 +106,54 @@ int gpt_partition_label_valid(const char *s) {
return char16_strlen(recoded) <= 36;
}
bool gpt_partition_type_is_root(sd_id128_t id) {
return sd_id128_equal(id, GPT_ROOT_X86) ||
sd_id128_equal(id, GPT_ROOT_X86_64) ||
sd_id128_equal(id, GPT_ROOT_ARM) ||
sd_id128_equal(id, GPT_ROOT_ARM_64) ||
sd_id128_equal(id, GPT_ROOT_IA64) ||
sd_id128_equal(id, GPT_ROOT_RISCV32) ||
sd_id128_equal(id, GPT_ROOT_RISCV64);
}
bool gpt_partition_type_is_root_verity(sd_id128_t id) {
return sd_id128_equal(id, GPT_ROOT_X86_VERITY) ||
sd_id128_equal(id, GPT_ROOT_X86_64_VERITY) ||
sd_id128_equal(id, GPT_ROOT_ARM_VERITY) ||
sd_id128_equal(id, GPT_ROOT_ARM_64_VERITY) ||
sd_id128_equal(id, GPT_ROOT_IA64_VERITY) ||
sd_id128_equal(id, GPT_ROOT_RISCV32_VERITY) ||
sd_id128_equal(id, GPT_ROOT_RISCV64_VERITY);
}
bool gpt_partition_type_is_usr(sd_id128_t id) {
return sd_id128_equal(id, GPT_USR_X86) ||
sd_id128_equal(id, GPT_USR_X86_64) ||
sd_id128_equal(id, GPT_USR_ARM) ||
sd_id128_equal(id, GPT_USR_ARM_64) ||
sd_id128_equal(id, GPT_USR_IA64) ||
sd_id128_equal(id, GPT_USR_RISCV32) ||
sd_id128_equal(id, GPT_USR_RISCV64);
}
bool gpt_partition_type_is_usr_verity(sd_id128_t id) {
return sd_id128_equal(id, GPT_USR_X86_VERITY) ||
sd_id128_equal(id, GPT_USR_X86_64_VERITY) ||
sd_id128_equal(id, GPT_USR_ARM_VERITY) ||
sd_id128_equal(id, GPT_USR_ARM_64_VERITY) ||
sd_id128_equal(id, GPT_USR_IA64_VERITY) ||
sd_id128_equal(id, GPT_USR_RISCV32_VERITY) ||
sd_id128_equal(id, GPT_USR_RISCV64_VERITY);
}
bool gpt_partition_type_knows_read_only(sd_id128_t id) {
return gpt_partition_type_is_root(id) ||
gpt_partition_type_is_usr(id) ||
sd_id128_equal(id, GPT_HOME) ||
sd_id128_equal(id, GPT_SRV) ||
sd_id128_equal(id, GPT_VAR) ||
sd_id128_equal(id, GPT_TMP) ||
gpt_partition_type_is_root_verity(id) || /* pretty much implied, but let's set the bit to make things really clear */
gpt_partition_type_is_usr_verity(id); /* ditto */
}

View File

@ -128,3 +128,10 @@ typedef struct GptPartitionType {
extern const GptPartitionType gpt_partition_type_table[];
int gpt_partition_label_valid(const char *s);
bool gpt_partition_type_is_root(sd_id128_t id);
bool gpt_partition_type_is_root_verity(sd_id128_t id);
bool gpt_partition_type_is_usr(sd_id128_t id);
bool gpt_partition_type_is_usr_verity(sd_id128_t id);
bool gpt_partition_type_knows_read_only(sd_id128_t id);

View File

@ -353,6 +353,7 @@ int loop_device_make(
.nr = nr,
.node = TAKE_PTR(loopdev),
.relinquished = true, /* It's not allocated by us, don't destroy it when this object is freed */
.devno = st.st_rdev,
};
*ret = d;
@ -425,6 +426,10 @@ int loop_device_make(
UINT64_C(240) * USEC_PER_MSEC * n_attempts/64));
}
if (fstat(loop_with_fd, &st) < 0)
return -errno;
assert(S_ISBLK(st.st_mode));
d = new(LoopDevice, 1);
if (!d)
return -ENOMEM;
@ -432,6 +437,7 @@ int loop_device_make(
.fd = TAKE_FD(loop_with_fd),
.node = TAKE_PTR(loopdev),
.nr = nr,
.devno = st.st_rdev,
};
*ret = d;

View File

@ -10,6 +10,7 @@ typedef struct LoopDevice LoopDevice;
struct LoopDevice {
int fd;
int nr;
dev_t devno;
char *node;
bool relinquished;
};

View File

@ -0,0 +1 @@
../TEST-01-BASIC/Makefile

6
test/TEST-58-REPART/test.sh Executable file
View File

@ -0,0 +1,6 @@
#!/usr/bin/env bash
set -e
TEST_DESCRIPTION="test systemd-repart"
. $TEST_BASE_DIR/test-functions
do_test "$@" 56

View File

@ -0,0 +1,6 @@
[Unit]
Description=TEST-56-EXIT-TYPE
[Service]
ExecStart=/usr/lib/systemd/tests/testdata/units/%N.sh
Type=oneshot

72
test/units/testsuite-58.sh Executable file
View File

@ -0,0 +1,72 @@
#!/usr/bin/env bash
set -eux
export SYSTEMD_LOG_LEVEL=debug
export PAGER=cat
mkdir -p /tmp/testsuite-58-defs/
# First part: create a disk image and verify its in order
cat > /tmp/testsuite-58-defs/esp.conf <<EOF
[Partition]
Type=esp
SizeMinBytes=10M
Format=vfat
EOF
cat > /tmp/testsuite-58-defs/usr.conf <<EOF
[Partition]
Type=usr
SizeMinBytes=10M
Format=ext4
ReadOnly=yes
EOF
cat > /tmp/testsuite-58-defs/root.conf <<EOF
[Partition]
Type=root
SizeMinBytes=10M
Format=ext4
MakeDirectories=/usr /efi
EOF
systemd-repart --definitions=/tmp/testsuite-58-defs/ --empty=create --size=auto --seed=750b6cd5c4ae4012a15e7be3c29e6a47 /var/tmp/testsuite-58.img
sfdisk --dump /var/tmp/testsuite-58.img > /tmp/testsuite-58.dump
grep -qxF '/var/tmp/testsuite-58.img1 : start= 2048, size= 20480, type=C12A7328-F81F-11D2-BA4B-00A0C93EC93B, uuid=39107B09-615D-48FB-BA37-C663885FCE67, name="esp"' /tmp/testsuite-58.dump
grep -qxF '/var/tmp/testsuite-58.img2 : start= 22528, size= 20480, type=4F68BCE3-E8CD-4DB1-96E7-FBCAF984B709, uuid=60F33797-1D71-4DCB-AA6F-20564F036CD0, name="root-x86-64"' /tmp/testsuite-58.dump
grep -qxF '/var/tmp/testsuite-58.img3 : start= 43008, size= 20480, type=8484680C-9521-48C6-9C11-B0720656F69E, uuid=7E3369DD-D653-4513-ADF5-B993A9F20C16, name="usr-x86-64", attrs="GUID:60"' /tmp/testsuite-58.dump
# Second part, duplicate it with CopyBlocks=auto
cat > /tmp/testsuite-58-defs/esp.conf <<EOF
[Partition]
Type=esp
CopyBlocks=auto
EOF
cat > /tmp/testsuite-58-defs/usr.conf <<EOF
[Partition]
Type=usr
ReadOnly=yes
CopyBlocks=auto
EOF
cat > /tmp/testsuite-58-defs/root.conf <<EOF
[Partition]
Type=root
CopyBlocks=auto
EOF
systemd-repart --definitions=/tmp/testsuite-58-defs/ --empty=create --size=auto --seed=750b6cd5c4ae4012a15e7be3c29e6a47 --image=/var/tmp/testsuite-58.img /var/tmp/testsuite-58.2.img
cmp /var/tmp/testsuite-58.img /var/tmp/testsuite-58.2.img
rm -f /var/tmp/testsuite-58.img /var/tmp/testsuite-58.2.img /tmp/testsuite-58.dump
rm -rf /tmp/testsuite-58-defs/
echo OK >/testok
exit 0