...

Text file src/github.com/opencontainers/runc/tests/integration/helpers.bash

Documentation: github.com/opencontainers/runc/tests/integration

     1#!/bin/bash
     2
     3bats_require_minimum_version 1.5.0
     4
     5# Root directory of integration tests.
     6INTEGRATION_ROOT=$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")
     7
     8# Download images, get *_IMAGE variables.
     9IMAGES=$("${INTEGRATION_ROOT}"/get-images.sh)
    10eval "$IMAGES"
    11unset IMAGES
    12
    13: "${RUNC:="${INTEGRATION_ROOT}/../../runc"}"
    14RECVTTY="${INTEGRATION_ROOT}/../../contrib/cmd/recvtty/recvtty"
    15SD_HELPER="${INTEGRATION_ROOT}/../../contrib/cmd/sd-helper/sd-helper"
    16SECCOMP_AGENT="${INTEGRATION_ROOT}/../../contrib/cmd/seccompagent/seccompagent"
    17
    18# Test data path.
    19# shellcheck disable=SC2034
    20TESTDATA="${INTEGRATION_ROOT}/testdata"
    21
    22# Kernel version
    23KERNEL_VERSION="$(uname -r)"
    24KERNEL_MAJOR="${KERNEL_VERSION%%.*}"
    25KERNEL_MINOR="${KERNEL_VERSION#"$KERNEL_MAJOR".}"
    26KERNEL_MINOR="${KERNEL_MINOR%%.*}"
    27
    28ARCH=$(uname -m)
    29
    30# Seccomp agent socket.
    31SECCCOMP_AGENT_SOCKET="$BATS_TMPDIR/seccomp-agent.sock"
    32
    33# Check if we're in rootless mode.
    34ROOTLESS=$(id -u)
    35
    36# Wrapper for runc.
    37function runc() {
    38	run __runc "$@"
    39
    40	# Some debug information to make life easier. bats will only print it if the
    41	# test failed, in which case the output is useful.
    42	# shellcheck disable=SC2154
    43	echo "$(basename "$RUNC") $* (status=$status):" >&2
    44	# shellcheck disable=SC2154
    45	echo "$output" >&2
    46}
    47
    48# Raw wrapper for runc.
    49function __runc() {
    50	"$RUNC" ${RUNC_USE_SYSTEMD+--systemd-cgroup} --root "$ROOT/state" "$@"
    51}
    52
    53# Wrapper for runc spec.
    54function runc_spec() {
    55	local args=()
    56	if [ "$ROOTLESS" -ne 0 ]; then
    57		args+=("--rootless")
    58	fi
    59
    60	runc spec "${args[@]}"
    61
    62	# Always add additional mappings if we have idmaps.
    63	if [[ "$ROOTLESS" -ne 0 ]] && [[ "$ROOTLESS_FEATURES" == *"idmap"* ]]; then
    64		runc_rootless_idmap
    65	fi
    66}
    67
    68# Helper function to reformat config.json file. Input uses jq syntax.
    69function update_config() {
    70	jq "$@" "./config.json" | awk 'BEGIN{RS="";getline<"-";print>ARGV[1]}' "./config.json"
    71}
    72
    73# Shortcut to add additional uids and gids, based on the values set as part of
    74# a rootless configuration.
    75function runc_rootless_idmap() {
    76	update_config ' .mounts |= map((select(.type == "devpts") | .options += ["gid=5"]) // .)
    77			| .linux.uidMappings += [{"hostID": '"$ROOTLESS_UIDMAP_START"', "containerID": 1000, "size": '"$ROOTLESS_UIDMAP_LENGTH"'}]
    78			| .linux.gidMappings += [{"hostID": '"$ROOTLESS_GIDMAP_START"', "containerID": 100, "size": 1}]
    79			| .linux.gidMappings += [{"hostID": '"$((ROOTLESS_GIDMAP_START + 10))"', "containerID": 1, "size": 20}]
    80			| .linux.gidMappings += [{"hostID": '"$((ROOTLESS_GIDMAP_START + 100))"', "containerID": 1000, "size": '"$((ROOTLESS_GIDMAP_LENGTH - 1000))"'}]'
    81}
    82
    83# Returns systemd version as a number (-1 if systemd is not enabled/supported).
    84function systemd_version() {
    85	if [ -n "${RUNC_USE_SYSTEMD}" ]; then
    86		systemctl --version | awk '/^systemd / {print $2; exit}'
    87		return
    88	fi
    89
    90	echo "-1"
    91}
    92
    93function init_cgroup_paths() {
    94	# init once
    95	test -n "$CGROUP_UNIFIED" && return
    96
    97	if stat -f -c %t /sys/fs/cgroup | grep -qFw 63677270; then
    98		CGROUP_UNIFIED=yes
    99		local controllers="/sys/fs/cgroup/cgroup.controllers"
   100		# For rootless + systemd case, controllers delegation is required,
   101		# so check the controllers that the current user has, not the top one.
   102		# NOTE: delegation of cpuset requires systemd >= 244 (Fedora >= 32, Ubuntu >= 20.04).
   103		if [[ "$ROOTLESS" -ne 0 && -n "$RUNC_USE_SYSTEMD" ]]; then
   104			controllers="/sys/fs/cgroup/user.slice/user-$(id -u).slice/user@$(id -u).service/cgroup.controllers"
   105		fi
   106
   107		# "pseudo" controllers do not appear in /sys/fs/cgroup/cgroup.controllers.
   108		# - devices (since kernel 4.15) we must assume to be supported because
   109		#   it's quite hard to test.
   110		# - freezer (since kernel 5.2) we can auto-detect by looking for the
   111		#   "cgroup.freeze" file a *non-root* cgroup.
   112		CGROUP_SUBSYSTEMS=$(
   113			cat "$controllers"
   114			echo devices
   115		)
   116		CGROUP_BASE_PATH=/sys/fs/cgroup
   117
   118		# Find any cgroup.freeze files...
   119		if [ -n "$(find "$CGROUP_BASE_PATH" -type f -name "cgroup.freeze" -print -quit)" ]; then
   120			CGROUP_SUBSYSTEMS+=" freezer"
   121		fi
   122	else
   123		if stat -f -c %t /sys/fs/cgroup/unified | grep -qFw 63677270; then
   124			CGROUP_HYBRID=yes
   125		fi
   126		CGROUP_UNIFIED=no
   127		CGROUP_SUBSYSTEMS=$(awk '!/^#/ {print $1}' /proc/cgroups)
   128		local g base_path
   129		for g in ${CGROUP_SUBSYSTEMS}; do
   130			base_path=$(gawk '$(NF-2) == "cgroup" && $NF ~ /\<'"${g}"'\>/ { print $5; exit }' /proc/self/mountinfo)
   131			test -z "$base_path" && continue
   132			eval CGROUP_"${g^^}"_BASE_PATH="${base_path}"
   133		done
   134	fi
   135}
   136
   137function create_parent() {
   138	if [ -n "$RUNC_USE_SYSTEMD" ]; then
   139		[ -z "$SD_PARENT_NAME" ] && return
   140		"$SD_HELPER" --parent machine.slice start "$SD_PARENT_NAME"
   141	else
   142		[ -z "$REL_PARENT_PATH" ] && return
   143		if [ "$CGROUP_UNIFIED" == "yes" ]; then
   144			mkdir "/sys/fs/cgroup$REL_PARENT_PATH"
   145		else
   146			local subsys
   147			for subsys in ${CGROUP_SUBSYSTEMS}; do
   148				# Have to ignore EEXIST (-p) as some subsystems
   149				# are mounted together (e.g. cpu,cpuacct), so
   150				# the path is created more than once.
   151				mkdir -p "/sys/fs/cgroup/$subsys$REL_PARENT_PATH"
   152			done
   153		fi
   154	fi
   155}
   156
   157function remove_parent() {
   158	if [ -n "$RUNC_USE_SYSTEMD" ]; then
   159		[ -z "$SD_PARENT_NAME" ] && return
   160		"$SD_HELPER" --parent machine.slice stop "$SD_PARENT_NAME"
   161	else
   162		[ -z "$REL_PARENT_PATH" ] && return
   163		if [ "$CGROUP_UNIFIED" == "yes" ]; then
   164			rmdir "/sys/fs/cgroup/$REL_PARENT_PATH"
   165		else
   166			local subsys
   167			for subsys in ${CGROUP_SUBSYSTEMS} systemd; do
   168				rmdir "/sys/fs/cgroup/$subsys/$REL_PARENT_PATH"
   169			done
   170		fi
   171	fi
   172	unset SD_PARENT_NAME
   173	unset REL_PARENT_PATH
   174}
   175
   176function set_parent_systemd_properties() {
   177	[ -z "$SD_PARENT_NAME" ] && return
   178	local user
   179	[ "$(id -u)" != "0" ] && user="--user"
   180	systemctl set-property $user "$SD_PARENT_NAME" "$@"
   181}
   182
   183# Randomize cgroup path(s), and update cgroupsPath in config.json.
   184# This function sets a few cgroup-related variables.
   185#
   186# Optional parameter $1 is a pod/parent name. If set, a parent/pod cgroup is
   187# created, and variables $REL_PARENT_PATH and $SD_PARENT_NAME can be used to
   188# refer to it.
   189function set_cgroups_path() {
   190	init_cgroup_paths
   191	local pod dash_pod slash_pod pod_slice
   192	if [ "$#" -ne 0 ] && [ "$1" != "" ]; then
   193		# Set up a parent/pod cgroup.
   194		pod="$1"
   195		dash_pod="-$pod"
   196		slash_pod="/$pod"
   197		SD_PARENT_NAME="machine-${pod}.slice"
   198		pod_slice="/$SD_PARENT_NAME"
   199	fi
   200
   201	local rnd="$RANDOM"
   202	if [ -n "${RUNC_USE_SYSTEMD}" ]; then
   203		SD_UNIT_NAME="runc-cgroups-integration-test-${rnd}.scope"
   204		if [ "$(id -u)" = "0" ]; then
   205			REL_PARENT_PATH="/machine.slice${pod_slice}"
   206			OCI_CGROUPS_PATH="machine${dash_pod}.slice:runc-cgroups:integration-test-${rnd}"
   207		else
   208			REL_PARENT_PATH="/user.slice/user-$(id -u).slice/user@$(id -u).service/machine.slice${pod_slice}"
   209			# OCI path doesn't contain "/user.slice/user-$(id -u).slice/user@$(id -u).service/" prefix
   210			OCI_CGROUPS_PATH="machine${dash_pod}.slice:runc-cgroups:integration-test-${rnd}"
   211		fi
   212		REL_CGROUPS_PATH="$REL_PARENT_PATH/$SD_UNIT_NAME"
   213	else
   214		REL_PARENT_PATH="/runc-cgroups-integration-test${slash_pod}"
   215		REL_CGROUPS_PATH="$REL_PARENT_PATH/test-cgroup-${rnd}"
   216		OCI_CGROUPS_PATH=$REL_CGROUPS_PATH
   217	fi
   218
   219	# Absolute path to container's cgroup v2.
   220	if [ "$CGROUP_UNIFIED" == "yes" ]; then
   221		CGROUP_PATH=${CGROUP_BASE_PATH}${REL_CGROUPS_PATH}
   222	fi
   223
   224	[ -n "$pod" ] && create_parent
   225
   226	update_config '.linux.cgroupsPath |= "'"${OCI_CGROUPS_PATH}"'"'
   227}
   228
   229# Get a path to cgroup directory, based on controller name.
   230# Parameters:
   231#  $1: controller name (like "pids") or a file name (like "pids.max").
   232function get_cgroup_path() {
   233	if [ "$CGROUP_UNIFIED" = "yes" ]; then
   234		echo "$CGROUP_PATH"
   235		return
   236	fi
   237
   238	local var cgroup
   239	var=${1%%.*}                  # controller name (e.g. memory)
   240	var=CGROUP_${var^^}_BASE_PATH # variable name (e.g. CGROUP_MEMORY_BASE_PATH)
   241	eval cgroup=\$"${var}${REL_CGROUPS_PATH}"
   242	echo "$cgroup"
   243}
   244
   245# Get a value from a cgroup file.
   246function get_cgroup_value() {
   247	local cgroup
   248	cgroup="$(get_cgroup_path "$1")"
   249	cat "$cgroup/$1"
   250}
   251
   252# Helper to check a if value in a cgroup file matches the expected one.
   253function check_cgroup_value() {
   254	local current
   255	current="$(get_cgroup_value "$1")"
   256	local expected=$2
   257
   258	echo "current $current !? $expected"
   259	[ "$current" = "$expected" ]
   260}
   261
   262# Helper to check a value in systemd.
   263function check_systemd_value() {
   264	[ -z "${RUNC_USE_SYSTEMD}" ] && return
   265	local source="$1"
   266	[ "$source" = "unsupported" ] && return
   267	local expected="$2"
   268	local expected2="$3"
   269	local user=""
   270	[ "$(id -u)" != "0" ] && user="--user"
   271
   272	current=$(systemctl show $user --property "$source" "$SD_UNIT_NAME" | awk -F= '{print $2}')
   273	echo "systemd $source: current $current !? $expected $expected2"
   274	[ "$current" = "$expected" ] || [[ -n "$expected2" && "$current" = "$expected2" ]]
   275}
   276
   277function check_cpu_quota() {
   278	local quota=$1
   279	local period=$2
   280	local sd_quota=$3
   281
   282	if [ "$CGROUP_UNIFIED" = "yes" ]; then
   283		if [ "$quota" = "-1" ]; then
   284			quota="max"
   285		fi
   286		check_cgroup_value "cpu.max" "$quota $period"
   287	else
   288		check_cgroup_value "cpu.cfs_quota_us" $quota
   289		check_cgroup_value "cpu.cfs_period_us" "$period"
   290	fi
   291	# systemd values are the same for v1 and v2
   292	check_systemd_value "CPUQuotaPerSecUSec" "$sd_quota"
   293
   294	# CPUQuotaPeriodUSec requires systemd >= v242
   295	[ "$(systemd_version)" -lt 242 ] && return
   296
   297	local sd_period=$((period / 1000))ms
   298	[ "$sd_period" = "1000ms" ] && sd_period="1s"
   299	local sd_infinity=""
   300	# 100ms is the default value, and if not set, shown as infinity
   301	[ "$sd_period" = "100ms" ] && sd_infinity="infinity"
   302	check_systemd_value "CPUQuotaPeriodUSec" $sd_period $sd_infinity
   303}
   304
   305# Works for cgroup v1 and v2, accepts v1 shares as an argument.
   306function check_cpu_shares() {
   307	local shares=$1
   308
   309	if [ "$CGROUP_UNIFIED" = "yes" ]; then
   310		local weight=$((1 + ((shares - 2) * 9999) / 262142))
   311		check_cpu_weight "$weight"
   312	else
   313		check_cgroup_value "cpu.shares" "$shares"
   314		check_systemd_value "CPUShares" "$shares"
   315	fi
   316}
   317
   318# Works only for cgroup v2, accept v2 weight.
   319function check_cpu_weight() {
   320	local weight=$1
   321
   322	check_cgroup_value "cpu.weight" "$weight"
   323	check_systemd_value "CPUWeight" "$weight"
   324}
   325
   326# Helper function to set a resources limit
   327function set_resources_limit() {
   328	update_config '.linux.resources.pids.limit |= 100'
   329}
   330
   331# Helper function to make /sys/fs/cgroup writable
   332function set_cgroup_mount_writable() {
   333	update_config '.mounts |= map((select(.type == "cgroup") | .options -= ["ro"]) // .)'
   334}
   335
   336# Fails the current test, providing the error given.
   337function fail() {
   338	echo "$@" >&2
   339	exit 1
   340}
   341
   342# Check whether rootless runc can use cgroups.
   343function rootless_cgroup() {
   344	[[ "$ROOTLESS_FEATURES" == *"cgroup"* || -n "$RUNC_USE_SYSTEMD" ]]
   345}
   346
   347# Check if criu is available and working.
   348function have_criu() {
   349	command -v criu &>/dev/null || return 1
   350
   351	# Workaround for https://github.com/opencontainers/runc/issues/3532.
   352	local ver
   353	ver=$(rpm -q criu 2>/dev/null || true)
   354	run ! grep -q '^criu-3\.17-[123]\.el9' <<<"$ver"
   355}
   356
   357# Allows a test to specify what things it requires. If the environment can't
   358# support it, the test is skipped with a message.
   359function requires() {
   360	for var in "$@"; do
   361		local skip_me
   362		case $var in
   363		criu)
   364			if ! have_criu; then
   365				skip_me=1
   366			fi
   367			;;
   368		root)
   369			if [ "$ROOTLESS" -ne 0 ]; then
   370				skip_me=1
   371			fi
   372			;;
   373		rootless)
   374			if [ "$ROOTLESS" -eq 0 ]; then
   375				skip_me=1
   376			fi
   377			;;
   378		rootless_idmap)
   379			if [[ "$ROOTLESS_FEATURES" != *"idmap"* ]]; then
   380				skip_me=1
   381			fi
   382			;;
   383		rootless_cgroup)
   384			if ! rootless_cgroup; then
   385				skip_me=1
   386			fi
   387			;;
   388		rootless_no_cgroup)
   389			if rootless_cgroup; then
   390				skip_me=1
   391			fi
   392			;;
   393		rootless_no_features)
   394			if [ "$ROOTLESS_FEATURES" != "" ]; then
   395				skip_me=1
   396			fi
   397			;;
   398		cgroups_rt)
   399			init_cgroup_paths
   400			if [ ! -e "${CGROUP_CPU_BASE_PATH}/cpu.rt_period_us" ]; then
   401				skip_me=1
   402			fi
   403			;;
   404		cgroups_swap)
   405			init_cgroup_paths
   406			if [ $CGROUP_UNIFIED = "no" ] && [ ! -e "${CGROUP_MEMORY_BASE_PATH}/memory.memsw.limit_in_bytes" ]; then
   407				skip_me=1
   408			fi
   409			;;
   410		cgroupns)
   411			if [ ! -e "/proc/self/ns/cgroup" ]; then
   412				skip_me=1
   413			fi
   414			;;
   415		cgroups_v1)
   416			init_cgroup_paths
   417			if [ "$CGROUP_UNIFIED" != "no" ]; then
   418				skip_me=1
   419			fi
   420			;;
   421		cgroups_v2)
   422			init_cgroup_paths
   423			if [ "$CGROUP_UNIFIED" != "yes" ]; then
   424				skip_me=1
   425			fi
   426			;;
   427		cgroups_hybrid)
   428			init_cgroup_paths
   429			if [ "$CGROUP_HYBRID" != "yes" ]; then
   430				skip_me=1
   431			fi
   432			;;
   433		cgroups_*)
   434			init_cgroup_paths
   435			var=${var#cgroups_}
   436			if [[ "$CGROUP_SUBSYSTEMS" != *"$var"* ]]; then
   437				skip_me=1
   438			fi
   439			;;
   440		smp)
   441			local cpus
   442			cpus=$(grep -c '^processor' /proc/cpuinfo)
   443			if [ "$cpus" -lt 2 ]; then
   444				skip_me=1
   445			fi
   446			;;
   447		systemd)
   448			if [ -z "${RUNC_USE_SYSTEMD}" ]; then
   449				skip_me=1
   450			fi
   451			;;
   452		systemd_v*)
   453			var=${var#systemd_v}
   454			if [ "$(systemd_version)" -lt "$var" ]; then
   455				skip "requires systemd >= v${var}"
   456			fi
   457			;;
   458		no_systemd)
   459			if [ -n "${RUNC_USE_SYSTEMD}" ]; then
   460				skip_me=1
   461			fi
   462			;;
   463		arch_x86_64)
   464			if [ "$ARCH" != "x86_64" ]; then
   465				skip_me=1
   466			fi
   467			;;
   468		more_than_8_core)
   469			local cpus
   470			cpus=$(grep -c '^processor' /proc/cpuinfo)
   471			if [ "$cpus" -le 8 ]; then
   472				skip_me=1
   473			fi
   474			;;
   475		*)
   476			fail "BUG: Invalid requires $var."
   477			;;
   478		esac
   479		if [ -n "$skip_me" ]; then
   480			skip "test requires $var"
   481		fi
   482	done
   483}
   484
   485# Retry a command $1 times until it succeeds. Wait $2 seconds between retries.
   486function retry() {
   487	local attempts=$1
   488	shift
   489	local delay=$1
   490	shift
   491	local i
   492
   493	for ((i = 0; i < attempts; i++)); do
   494		run "$@"
   495		if [[ "$status" -eq 0 ]]; then
   496			return 0
   497		fi
   498		sleep "$delay"
   499	done
   500
   501	echo "Command \"$*\" failed $attempts times. Output: $output"
   502	false
   503}
   504
   505# retry until the given container has state
   506function wait_for_container() {
   507	if [ $# -eq 3 ]; then
   508		retry "$1" "$2" __runc state "$3"
   509	elif [ $# -eq 4 ]; then
   510		retry "$1" "$2" eval "__runc state $3 | grep -qw $4"
   511	else
   512		echo "Usage: wait_for_container ATTEMPTS DELAY ID [STATUS]" 1>&2
   513		return 1
   514	fi
   515}
   516
   517function testcontainer() {
   518	# test state of container
   519	runc state "$1"
   520	if [ "$2" == "checkpointed" ]; then
   521		[ "$status" -eq 1 ]
   522		return
   523	fi
   524	[ "$status" -eq 0 ]
   525	[[ "${output}" == *"$2"* ]]
   526}
   527
   528function setup_recvtty() {
   529	[ -z "$ROOT" ] && return 1 # must not be called without ROOT set
   530	local dir="$ROOT/tty"
   531
   532	mkdir "$dir"
   533	export CONSOLE_SOCKET="$dir/sock"
   534
   535	# We need to start recvtty in the background, so we double fork in the shell.
   536	("$RECVTTY" --pid-file "$dir/pid" --mode null "$CONSOLE_SOCKET" &) &
   537}
   538
   539function teardown_recvtty() {
   540	[ -z "$ROOT" ] && return 0 # nothing to teardown
   541	local dir="$ROOT/tty"
   542
   543	# When we kill recvtty, the container will also be killed.
   544	if [ -f "$dir/pid" ]; then
   545		kill -9 "$(cat "$dir/pid")"
   546	fi
   547
   548	# Clean up the files that might be left over.
   549	rm -rf "$dir"
   550}
   551
   552function setup_seccompagent() {
   553	("${SECCOMP_AGENT}" -socketfile="$SECCCOMP_AGENT_SOCKET" -pid-file "$BATS_TMPDIR/seccompagent.pid" &) &
   554}
   555
   556function teardown_seccompagent() {
   557	if [ -f "$BATS_TMPDIR/seccompagent.pid" ]; then
   558		kill -9 "$(cat "$BATS_TMPDIR/seccompagent.pid")"
   559	fi
   560	rm -f "$BATS_TMPDIR/seccompagent.pid"
   561	rm -f "$SECCCOMP_AGENT_SOCKET"
   562}
   563
   564function setup_bundle() {
   565	local image="$1"
   566
   567	# Root for various container directories (state, tty, bundle).
   568	ROOT=$(mktemp -d "$BATS_RUN_TMPDIR/runc.XXXXXX")
   569	mkdir -p "$ROOT/state" "$ROOT/bundle/rootfs"
   570
   571	# Directories created by mktemp -d have 0700 permission bits. Tests
   572	# running inside userns (see userns.bats) need to access the directory
   573	# as a different user to mount the rootfs. Since kernel v5.12, parent
   574	# directories are also checked. Give a+x for these tests to work.
   575	chmod a+x "$ROOT" "$BATS_RUN_TMPDIR"
   576
   577	setup_recvtty
   578	cd "$ROOT/bundle" || return
   579
   580	tar --exclude './dev/*' -C rootfs -xf "$image"
   581
   582	runc_spec
   583}
   584
   585function setup_busybox() {
   586	setup_bundle "$BUSYBOX_IMAGE"
   587}
   588
   589function setup_debian() {
   590	setup_bundle "$DEBIAN_IMAGE"
   591}
   592
   593function teardown_bundle() {
   594	[ -z "$ROOT" ] && return 0 # nothing to teardown
   595
   596	cd "$INTEGRATION_ROOT" || return
   597	teardown_recvtty
   598	local ct
   599	for ct in $(__runc list -q); do
   600		__runc delete -f "$ct"
   601	done
   602	rm -rf "$ROOT"
   603	remove_parent
   604}
   605
   606function requires_kernel() {
   607	local major_required minor_required
   608	major_required=$(echo "$1" | cut -d. -f1)
   609	minor_required=$(echo "$1" | cut -d. -f2)
   610	if [[ "$KERNEL_MAJOR" -lt $major_required || ("$KERNEL_MAJOR" -eq $major_required && "$KERNEL_MINOR" -lt $minor_required) ]]; then
   611		skip "requires kernel $1"
   612	fi
   613}

View as plain text