#!/bin/bash
#
# Set up the control group hierarchy.
#
# Copyright IBM Corporation. 2008
#
# Authors:     Balbir Singh <balbir@linux.vnet.ibm.com>
# This program is free software; you can redistribute it and/or modify it
# under the terms of version 2.1 of the GNU Lesser General Public License
# as published by the Free Software Foundation.
#
# This program is distributed in the hope that it would be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
#
# cgconfig Control Groups Configuration Startup
# chkconfig: - 5 95
# description: Sets up the control group filesystem.  On the unified (cgroup
#              v2) hierarchy it mounts /sys/fs/cgroup, makes the available
#              controllers usable by child cgroups and delegates a subtree to
#              each user.  If /etc/cgconfig.conf declares v1 mount points it
#              falls back to running cgconfigparser on it instead.

### BEGIN INIT INFO
# Short-Description:    Create and setup control group filesystem(s)
# Description:          Create and setup control group filesystem(s)
# Should-Start:         ypbind
# Should-Stop:          ypbind
PIDFILE=/var/run/${servicename}
RUNLEVEL=S
NEEDS="+local_fs +syslog"
### END INIT INFO

. /etc/init.d/smgl_init

# get correct location of binaries from configure
prefix=/usr/;exec_prefix=${prefix};sbindir=${exec_prefix}/sbin
servicename=cgconfig
# auto_init insists PROGRAM be executable, and the unified hierarchy needs
# nothing from libcgroup itself; cgconfigparser is only used for the v1 path.
PROGRAM=/bin/mount
CGCONFIGPARSER=$sbindir/cgconfigparser
CONFIG_FILE=/etc/cgconfig.conf
lockfile=/var/lock/${servicename}

# read the config
CREATE_DEFAULT=yes
CGROUP_ROOT=/sys/fs/cgroup
CGROUP_MOUNT_OPTIONS=nsdelegate,memory_recursiveprot
# Controllers to hand down to child cgroups; empty means every controller the
# kernel offers.
CGROUP_CONTROLLERS=
# Parent of the per-user delegated subtrees.
USER_SLICE=user.slice
# Delegate a subtree to every user in this UID range, plus DELEGATE_USERS.
DELEGATE=yes
DELEGATE_UID_MIN=1000
DELEGATE_UID_MAX=60000
DELEGATE_USERS=
if [ -e /etc/sysconfig/cgconfig ]; then
	. /etc/sysconfig/cgconfig
fi

#
# cgroup v2 (unified hierarchy)
#
# A controller belongs to exactly one hierarchy, so v1 and v2 cannot both be
# used for it; whichever is mounted first wins.  This is why the v1 path below
# is only taken when the admin has explicitly asked for it.
#

cgroup_fstype() {
	awk -v root="$CGROUP_ROOT" '$2 == root { type = $3 } END { print type }' \
		/proc/self/mounts
}

# True when /etc/cgconfig.conf asks for v1 mount points, i.e. when the admin
# has deliberately configured the legacy hierarchies.
wants_cgroup_v1() {
	[ -s "$CONFIG_FILE" ] &&
	grep -q '^[[:space:]]*mount[[:space:]]*{' "$CONFIG_FILE"
}

mount_cgroup2() {
	case "$(cgroup_fstype)" in
	(cgroup2)
		return 0
		;;
	(cgroup)
		print_status warning "$CGROUP_ROOT holds a cgroup v1 hierarchy"
		echo "$servicename: replace the $CGROUP_ROOT line in /etc/fstab with"
		echo "$servicename:     none $CGROUP_ROOT cgroup2 $CGROUP_MOUNT_OPTIONS 0 0"
		echo "$servicename: the controllers cannot be handed to v2 while v1 holds them"
		return 1
		;;
	esac

	mkdir -p "$CGROUP_ROOT" &&
	mount -t cgroup2 -o "$CGROUP_MOUNT_OPTIONS" cgroup2 "$CGROUP_ROOT"
}

# A cgroup can only offer a controller to its children after listing it in
# cgroup.subtree_control, and it can only offer what its own parent gave it in
# cgroup.controllers.  Enable them one at a time so that one controller the
# kernel refuses does not take the rest down with it.
enable_controllers() {
	local cg=$1 avail want c

	avail=$(cat "$cg/cgroup.controllers" 2>/dev/null) || return 0
	want=${CGROUP_CONTROLLERS:-$avail}

	for c in $want; do
		case " $avail " in
		(*" $c "*)
			echo "+$c" > "$cg/cgroup.subtree_control" 2>/dev/null
			;;
		esac
	done

	return 0
}

# Delegation hands a subtree to an unprivileged user.  The kernel checks the
# owner of the directory itself, of cgroup.procs and cgroup.threads (moving a
# process) and of cgroup.subtree_control (handing controllers further down);
# nothing else must become writable, or the user could escape the subtree.
delegate_cgroup() {
	local cg=$1 owner=$2 f

	mkdir -p "$cg" || return 1
	chown "$owner" "$cg" || return 1

	for f in cgroup.procs cgroup.threads cgroup.subtree_control; do
		if [ -e "$cg/$f" ]; then
			chown "$owner" "$cg/$f" || return 1
		fi
	done

	return 0
}

delegated_users() {
	awk -F: -v min="$DELEGATE_UID_MIN" -v max="$DELEGATE_UID_MAX" \
		'$3 >= min && $3 <= max { print $3 }' /etc/passwd

	local user uid

	for user in $DELEGATE_USERS; do
		uid=$(id -u "$user" 2>/dev/null) && echo "$uid"
	done
}

setup_user_slices() {
	local slice=$CGROUP_ROOT/$USER_SLICE uid

	mkdir -p "$slice" || return 1

	# user.slice itself never holds processes - sessions go into a scope
	# below their own user-<uid>.slice - so it is free to pass controllers
	# down.
	enable_controllers "$slice"

	for uid in $(delegated_users | sort -un); do
		delegate_cgroup "$slice/user-$uid.slice" "$uid" ||
			print_status warning "could not delegate user-$uid.slice"
	done

	return 0
}

start_v2() {
	mount_cgroup2 || return 1

	# The root cgroup is the only one exempt from the "no internal
	# processes" rule, so it can keep the boot-time processes and still
	# pass controllers down.
	enable_controllers "$CGROUP_ROOT"

	if [ "$DELEGATE" = "yes" ]; then
		setup_user_slices || return 1
	fi

	print_status success
	return 0
}

stop_v2() {
	local slice=$CGROUP_ROOT/$USER_SLICE cg

	# rmdir on a cgroup that still holds processes fails, so this only
	# reaps the ones that are genuinely finished.
	for cg in "$slice"/*/*/ "$slice"/*/ "$slice"/; do
		[ -d "$cg" ] && rmdir "$cg" 2>/dev/null
	done

	return 0
}

#
# cgroup v1 (legacy, driven by /etc/cgconfig.conf)
#

create_default_groups() {
	defaultcgroup=

        if [ -f /etc/cgrules.conf ]; then
	    read user ctrl defaultcgroup <<< \
		    $(grep -m1 '^\*[[:space:]]\+' /etc/cgrules.conf)
            if [ -n "$defaultcgroup" -a "$defaultcgroup" = "*" ]; then
                print_status warning "/etc/cgrules.conf incorrect"
                print_status warning "Overriding it"
                defaultcgroup=
            fi
        fi

        if [ -z $defaultcgroup ]
        then
            defaultcgroup=sysdefault/
        fi

        #
        # Find all mounted subsystems and create comma-separated list
        # of controllers.
        #
        controllers=`lssubsys 2>/dev/null | tr '\n' ',' | sed s/.$//`

        #
        # Create the default group, ignore errors when the default group
        # already exists.
        #
        cgcreate -f 664 -d 775 -g $controllers:$defaultcgroup 2>/dev/null

        #
        # special rule for cpusets
        #
        if echo $controllers | grep -q -w cpuset; then
                cpus=`cgget -nv -r cpuset.cpus /`
                cgset -r cpuset.cpus=$cpus $defaultcgroup
                mems=`cgget -nv -r cpuset.mems /`
                cgset -r cpuset.mems=$mems $defaultcgroup
        fi

        #
        # Classify everything to default cgroup. Ignore errors, some processes
        # may exit after ps is run and before cgclassify moves them.
        #
        cgclassify -g $controllers:$defaultcgroup `ps --no-headers -eL o tid` \
                 2>/dev/null || :
}

start_v1() {
	if [ -f "$lockfile" ] && lscgroup > /dev/null 2>&1; then
		print_status warning "cgroups are already mounted"
		return 0
	fi  &&

	$CGCONFIGPARSER -l $CONFIG_FILE
	evaluate_retval || ( print_status failure "Failed to parse $CONFIG_FILE" && return 1 )

	if [ $CREATE_DEFAULT = "yes" ]; then
		create_default_groups
	fi

	touch "$lockfile" || ( print_status failure "Failed to touch $lockfile" && return 1 )
}

stop_v1() {
	cgclear -l $CONFIG_FILE
	rm -f "$lockfile"
	evaluate_retval
}

#
# init script entry points
#

start() {
	echo -n "Starting cgconfig service: "

	if wants_cgroup_v1; then
		start_v1
	else
		start_v2
	fi
}

stop() {
	echo -n "Stopping cgconfig service: "

	if [ "$(cgroup_fstype)" = cgroup2 ]; then
		stop_v2
		print_status success
	else
		stop_v1
	fi
}

status() {
	case "$(cgroup_fstype)" in
	(cgroup2)
		print_status success "${servicename}: unified hierarchy on $CGROUP_ROOT"
		;;
	(cgroup)
		print_status success "${servicename}: legacy v1 hierarchy on $CGROUP_ROOT"
		;;
	(*)
		print_status failure "${servicename} not started"
		;;
	esac

	return 0
}

restart() {
	stop
	sleep 1
	start
}
