]> CyberLeo.Net >> Repos - CDN/j.git/blob - j
j: add cgroups support, to isolate and permit signalling of chroot processes
[CDN/j.git] / j
1 #!/bin/sh
2 # Copyright 2011 CyberLeo, All Rights Reserved
3 # http://wiki.cyberleo.net/wiki/CyberLeo/COPYRIGHT
4
5 : ${ORIG_USER=$(id -un)}
6
7 # Need root beyond here
8 [ "$(id -u)" -eq 0 ] || exec sudo env "J_ARCH=${J_ARCH}" "J_BASE=${J_BASE}" "J_NAME=${J_NAME}" "J_USER=${J_USER:-${USER}}" "ORIG_USER=${ORIG_USER}" "DISPLAY=${DISPLAY}" "${0}" "${@}"
9
10 meh() { printf " \033[1;32m*\033[0m %s%s\n" "${jname:+${jname}: }" "${*}"; }
11 omg() { printf " \033[1;33m*\033[0m %s%s\n" "${jname:+${jname}: }" "${*}"; }
12 wtf() { printf " \033[1;31m*\033[0m %s%s\n" "${jname:+${jname}: }" "${*}"; exit 1; }
13 pebkac() {
14   [ "${*}" ] && printf "%s\n\n" "${*}"
15   cat <<EOF
16 Usage:
17   <command> <name> [arguments]
18   list
19   ls      list available chroots
20
21   status  show jail status
22
23   start   prepare an existing chroot for use
24
25   stop    undo what 'start' did
26
27   kill    Forcefully kill all processes; only applicable with cgroups
28
29   enter
30   shell   spawn a shell or command within the chroot
31
32   eval    evaluate a shell command line within the chroot
33
34 EOF
35   exit 1
36 }
37
38 cmd="$(basename "${0}")"
39 jarch="${J_ARCH:-$(uname -m)}"
40 jbase="${J_BASE:-$(realpath "$(dirname "${0}")/../")}"
41 jname="${J_NAME:-$(basename "${1}")}" #"
42 juser="${J_USER}"
43
44 # cgroup to use; if not found, cgroup support will not be used
45 jcgroup="freezer"
46 # cgroup parent to use (${cgroup}/${parent}/${jname})
47 jcgparent="j"
48
49 # Remove chroot name from argument stack, if passed in
50 [ "${J_NAME}" ] || shift
51
52 # Propagate certain environment variables; sterilize the rest of the environment
53 jenv="
54   LANG=${LANG}
55   TERM=${TERM}
56   USER=${USER}
57 "
58
59 # Debian-specific init: prepare Debian chroot with debootstrap
60 j_init_debian() {
61   jdir="${1}"
62   suite="$(echo "${2:-squeeze}" | tr 'A-Z' 'a-z')"
63
64   # Validation
65   [ "$(which debootstrap 2>&-)" ] || pebkac "j_init_debian: debootstrap not found"
66   [ "${jdir}" ] || pebkac "j_init_debian: jdir must be specified"
67   [ ! -d "${jdir}" ] || pebkac "j_init_debian: jdir must not exist ('${jdir}')"
68   [ -d "$(dirname "${jdir}")" ] || pebkac "j_init_debian: parent of jdir must exist ('${jdir}')"
69
70   # Figure out arch
71   case "${jarch}" in
72   x86|i386) arch=i386 ;;
73   amd64|x86_64|x64) arch=amd64 ;;
74   *) pebkac "Unsupported arch '${jarch}'" ;;
75   esac
76
77   cmd="debootstrap --arch=${arch} --include=curl,file,less,locales,sudo,build-essential,libreadline-dev,zlib1g-dev '${suite}' '${jdir}'"
78   echo "Executing ${cmd}"
79   eval "${cmd}"
80
81   # Make sure locales are generated on first start
82   mkdir -p "${jdir}/etc/rcJ.d"
83   cat > "${jdir}/etc/rcJ.d/S00localegen" <<"EOF"
84 #!/bin/sh
85 /bin/sed -i '/en_US/s/^# //' /etc/locale.gen
86 /usr/sbin/locale-gen
87 /bin/rm -f "${0}"
88 EOF
89   chmod 755 "${jdir}/etc/rcJ.d/S00localegen"
90 }
91
92 # Gentoo-specific init: prepare Gentoo chroot with stage3+portage tarballs
93 j_init_gentoo() {
94   arch="$(uname -m)"
95   base="http://distfiles.gentoo.org/releases/${arch}/autobuilds"
96   pointer="${base}/latest-stage3.txt"
97   # Fetch stage3
98   # Fetch portage
99   # Validate signatures
100   # Unpack stage3
101   # Unpack portage
102 }
103
104 # Create a new chroot, somehow
105 j_init() {
106   # Make sure this does NOT exist
107   jname="${1:-jname}"
108   dist="$(echo "${2:-debian}" | tr 'A-Z' 'a-z')"
109   jdir="${jbase}/${jname}"
110   [ -d "${jdir}" ] && pebkac "j_init: ${jname} already exists"
111   shift 2
112   case "${dist}" in
113   debian) j_init_debian "${jdir}" "${@}" ;;
114   gentoo) j_init_gentoo "${jdir}" "${@}" ;;
115   *) pebkac "Unsupported distro '${dist}'" ;;
116   esac
117 }
118
119 # Figure out and set chroot parameters; needed for all functions that follow
120 j_params() {
121   ( jname="${1:-jname}"
122
123     # Make sure jname is not empty
124     if [ -z "${jname}" ]
125     then
126       printf "jerror='%s'\n" "jname empty"
127       return 1
128     fi
129
130     # Given a chroot name, find and set up the chroot dir
131     jdir="${jbase}/${jname}"
132     jroot="${jdir}/root"
133     if [ ! -d "${jdir}" -o ! -d "${jroot}" ]
134     then
135       printf "jerror='%s'\n" "not a directory"
136       return 1
137     fi
138
139     # Where is the shell?
140     jshell=""
141     for shell in /bin/bash /usr/bin/bash /usr/local/bin/bash /bin/sh
142     do
143       if [ -f "${jroot}/${shell}" ]
144       then
145         jshell=${shell}
146         break
147       fi
148     done
149     if [ -z "${jshell}" ]
150     then
151       printf "jerror='%s'\n" "unable to locate usable shell; is this a jail?"
152       return 1
153     fi
154
155     printf "jerror='' jname='%s' jdir='%s' jroot='%s' jshell='%s'\n" "${jname}" "${jdir}" "${jroot}" "${jshell}"
156   )
157 }
158
159 # Find the 'freezer' cgroup, to determine whether the chroot can be further isolated
160 j_locate_cgroup_mount() {
161   cgroup="${1:-freezer}"
162   cat /proc/mounts | while read src dir type opts fsck dump extra
163   do
164     [ "${type}" = "cgroup" ] || continue
165     oldifs="${IFS}"
166     IFS=','
167     set -- ${opts}
168     IFS=${oldifs}
169     while [ "${1}" ]
170     do
171       [ "${1}" = "${cgroup}" ] && echo "${dir}" && break 2
172       shift
173     done
174   done
175 }
176
177 # Cache and expose cgroup mountpoint for given chroot; make sure it exists
178 j_cgroup() {
179   [ "${jcg}" ] && { echo "${jcg}"; return; }
180   jcg="$(j_locate_cgroup_mount "${cgroup}")/${jcgparent}/${jname}"
181   mkdir -p "${jcg}"
182   echo "${jcg}"
183 }
184
185 j_cg_trap() {
186   [ "$(j_cgroup)" ] || return
187   while [ "${1}" ]
188   do
189     echo "${1}" > "$(j_cgroup)/tasks"
190     shift
191   done
192 }
193
194 # Run a command within a cgroup, if available
195 j_cg_eval() {
196   j_cg="$(j_cgroup)"
197   if [ "$(j_cgroup)" ]
198   then
199     sh -ex <<EOF
200 echo $$ > "$(j_cgroup)/tasks"
201 eval "${*}"
202 EOF
203   else
204     eval "${*}"
205   fi
206 }
207
208 # Copy ipcc into the jail and add helpful symlinks, so it can communicate
209 # simple commands to the outside
210 j_ipc_setup() {
211   ipcc="$(dirname "${0}")/ipcc"
212   cp -f "${ipcc}" "${jroot}/bin"
213   ( cd "${jroot}/bin"
214     [ -e ee ] || ln -s ipcc ee
215     [ -e ff ] || ln -s ipcc ff
216     [ -e gitk ] || ln -s ipcc gitk
217     [ -e gitka ] || ln -s ipcc gitka
218   )
219 }
220
221 # Launch ipcd on jail
222 j_ipc_start() {
223   ipcd_pid="${jdir}/ipcd.pid"
224   su "${ORIG_USER}" "${jbase}/j/ipcd" "${jdir}" &
225   pid="${!}"
226   echo "${pid}" > "${ipcd_pid}"
227   j_cg_trap "${pid}"
228 }
229
230 # Stop ipcd on jail
231 j_ipc_stop() {
232   ipcd_pid="${jdir}/ipcd.pid"
233   [ -f "${ipcd_pid}" ] && pid="$(cat "${ipcd_pid}")"
234   [ "${pid}" ] && kill -TERM ${pid}
235 }
236
237 # Is this a chroot?
238 j_is() {
239   eval $(j_params "${1}")
240   [ "${jerror}" ] && return 1 || return 0
241 }
242
243 # List available chroots
244 j_ls() {
245   ( cd "${jbase}"; ls -1 ) | while read jname
246   do
247     j_is "${jname}" && echo "${jname}"
248   done
249 }
250
251 # Chroot is 'up' if /dev/pts and /proc are mounted
252 j_up() {
253   jname="${1:-${jname}}"
254   eval "$(j_params "${jname}")"
255   [ "${jerror}" ] && wtf "${jerror}"
256   grep -q "^devpts ${jroot}/dev/pts devpts" /proc/mounts || return 1
257   grep -q "^proc ${jroot}/proc proc" /proc/mounts || return 1
258   return 0
259 }
260
261 # Poll chroot status (j_up)
262 j_status() {
263   [ -z "${1}" ] && set - $(j_ls)
264   while [ "${1}" ]
265   do
266     j_up "${1}" && meh "$(printf '\033[1;32mup\033[0m')" || meh "$(printf '\033[1;31mdown\033[0m')"
267     shift
268   done
269 }
270
271 # Mount /dev/pts and /proc in the chroot
272 j_start() {
273   jname="${1:-${jname}}"
274   j_up "${jname}" && return 0
275   eval "$(j_params "${jname}")"
276   meh "starting ${jname} ..."
277   mount -t devpts devpts "${jroot}/dev/pts"
278   mount -t proc proc "${jroot}/proc"
279
280   # Copy in ipcc and launch ipcd
281   j_ipc_setup
282   j_ipc_start
283
284   # Start all services in /etc/rcJ.d
285   j_root_eval "${jname}" '[ -d /etc/rcJ.d ] && ( ls -1 /etc/rcJ.d/* 2>&- | grep /S | sort | sed -e "s/$/ start/" | sh )'
286 }
287
288 # Execute command in chroot as root
289 j_root_eval() {
290   jname="${1:-${jname}}"; shift
291   j_up "${jname}" || wtf "chroot not running"
292   eval "$(j_params "${jname}")"
293   j_cg_trap $$
294   env -i ${jenv} /usr/bin/chroot "${jroot}" /bin/sh -c "${*}"
295 }
296
297 # Execute command in chroot
298 j_eval() {
299   jname="${1:-${jname}}"; shift
300   j_up "${jname}" || wtf "chroot not running"
301   eval "$(j_params "${jname}")"
302   j_cg_trap $$
303   env -i ${jenv} /usr/bin/chroot "${jroot}" /bin/su "${juser:-${USER}}" -c "${*}"
304 }
305
306 j_shell() {
307   jname="${1:-${jname}}"
308   eval "$(j_params "${jname}")"
309   j_eval "${jname}" "cd; exec ${jshell} -l"
310 }
311
312 # Unmount /dev/pts and /proc in the chroot
313 j_stop() {
314   jname="${1:-${jname}}"
315   eval "$(j_params "${jname}")"
316   j_up "${jname}" || return 0
317   meh "stopping ${jname} ..."
318
319   # Stop all services in /etc/rcJ.d
320   j_root_eval "${jname}" '[ -d /etc/rcJ.d ] && ( ls -1 /etc/rcJ.d/* 2>&- | grep /S | sort -r | sed -e "s/$/ stop/" | sh )'
321
322   # Terminate ipcd
323   j_ipc_stop
324
325   umount "${jroot}/proc"
326   umount "${jroot}/dev/pts"
327 }
328
329 # Send a signal to all processes inside a chroot, if possible
330 j_signal() {
331   sig="${1:-HUP}"
332   jname="${2:-${jname}}"
333
334   [ "$(j_cgroup)" ] || { echo "No cgroup support; cannot signal"; return 1; }
335   pids="$(cat "$(j_cgroup)/tasks")"
336   if [ "${pids}" ]
337   then
338     echo Sending ${sig} to ${pids} ...
339     echo "FROZEN" > "$(j_cgroup)/freezer.state"
340     kill -${sig} ${pids}
341     echo "THAWED" > "$(j_cgroup)/freezer.state"
342   fi
343 }
344
345 j_kill() {
346   jname="${1:-${jname}}"
347   eval "$(j_params "${jname}")"
348   meh "killing ${jname} ..."
349
350   j_signal "TERM" "${jname}"
351   sleep 2
352   j_signal "KILL" "${jname}"
353 }
354
355 case "${cmd}" in
356 init|create) j_init "${jname}" "${@}" ;;
357 ls|list) j_ls ;;
358 status) j_status "${jname}" "${@}" ;;
359 start) j_start "${jname}" ;;
360 shell|enter) j_shell "${jname}" ;;
361 eval) j_eval "${jname}" "${*}" ;;
362 stop) j_stop "${jname}" ;;
363 kill) j_kill "${jname}" ;;
364 *) pebkac ;;
365 esac