pax_global_header00006660000000000000000000000064132036754040014516gustar00rootroot0000000000000052 comment=1a5609e77845125e6c318c25baabe6df1411f0f5 charliecloud-0.2.3~pre+1a5609e/000077500000000000000000000000001320367540400160515ustar00rootroot00000000000000charliecloud-0.2.3~pre+1a5609e/.gitmodules000066400000000000000000000001551320367540400202270ustar00rootroot00000000000000[submodule "test/bats"] path = test/bats url = https://github.com/sstephenson/bats.git ignore = untracked charliecloud-0.2.3~pre+1a5609e/.travis.yml000077700000000000000000000000001320367540400231672test/travis.ymlustar00rootroot00000000000000charliecloud-0.2.3~pre+1a5609e/COPYRIGHT000066400000000000000000000024011320367540400173410ustar00rootroot00000000000000Charliecloud is copyright © 2014–2017 Los Alamos National Security, LLC. This software has been approved for open source release, LA-CC 14-096. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this software except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 This material was produced under U.S. Government contract DE-AC52-06NA25396 for Los Alamos National Laboratory (LANL), which is operated by Los Alamos National Security, LLC for the U.S. Department of Energy. The U.S. Government has rights to use, reproduce, and distribute this software. NEITHER THE GOVERNMENT NOR LOS ALAMOS NATIONAL SECURITY, LLC MAKES ANY WARRANTY, EXPRESS OR IMPLIED, OR ASSUMES ANY LIABILITY FOR THE USE OF THIS SOFTWARE. If software is modified to produce derivative works, such modified software should be clearly marked, so as not to confuse it with the version available from LANL. Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. charliecloud-0.2.3~pre+1a5609e/LICENSE000066400000000000000000000261361320367540400170660ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. charliecloud-0.2.3~pre+1a5609e/Makefile000066400000000000000000000134021320367540400175110ustar00rootroot00000000000000SHELL=/bin/bash # Add some good stuff to CFLAGS. export CFLAGS += -std=c11 -Wall .PHONY: all all: VERSION.full bin/version.h bin/version.sh cd bin && $(MAKE) SETUID=$(SETUID) all cd test && $(MAKE) all cd examples/syscalls && $(MAKE) all .PHONY: clean clean: cd bin && $(MAKE) clean cd test && $(MAKE) clean cd examples/syscalls && $(MAKE) clean # VERSION.full contains the version string reported by the executables. # # * If VERSION is an unadorned release (e.g. 0.2.3 not 0.2.3~pre), or there's # no Git information available, VERSION.full is simply a copy of VERSION. # # * Otherwise, we add the Git commit and a note if the working directory # contains uncommitted changes, e.g. "0.2.3~pre+ae24a4e.dirty". # ifeq ($(shell test -d .git && fgrep -q \~ VERSION && echo true),true) .PHONY: VERSION.full # depends on git metadata, not a simple file VERSION.full: printf '%s+%s%s\n' \ $$(cat VERSION) \ $$(git rev-parse --short HEAD) \ $$(git diff-index --quiet HEAD || echo '.dirty') \ > VERSION.full else VERSION.full: VERSION cp VERSION VERSION.full endif bin/version.h: VERSION.full echo "#define VERSION \"$$(cat $<)\"" > $@ bin/version.sh: VERSION.full echo "version () { echo 1>&2 '$$(cat $<)'; }" > $@ # Yes, this is bonkers. We keep it around even though normal "git archive" or # the zip files on Github work, because it provides an easy way to create a # self-contained tarball with embedded Bats. .PHONY: export export: VERSION.full test -d .git -a -f test/bats/.git # need recursive Git checkout git diff-index --quiet HEAD # need clean working directory git archive HEAD --prefix=charliecloud-$$(cat VERSION.full)/ \ -o main.tar cd test/bats && \ git archive HEAD \ --prefix=charliecloud-$$(cat ../../VERSION.full)/test/bats/ \ -o ../../bats.tar tar Af main.tar bats.tar tar --xform=s,^,charliecloud-$$(cat VERSION.full)/, \ -rf main.tar \ VERSION.full gzip -9 main.tar mv main.tar.gz charliecloud-$$(cat VERSION.full).tar.gz rm bats.tar ls -lh charliecloud-$$(cat VERSION.full).tar.gz # PREFIX is the prefix expected at runtime (usually /usr or /usr/local for # system-wide installations). # More: https://www.gnu.org/prep/standards/html_node/Directory-Variables.html # # DESTDIR is the installation directory using during make install, which # usually coincides for manual installation, but is chosen to be a temporary # directory in packaging environments. PREFIX needs to be appended. # More: https://www.gnu.org/prep/standards/html_node/DESTDIR.html # # Reasoning here: Users performing manual install *have* to specify PREFIX; # default is to use that also for DESTDIR. If DESTDIR is provided in addition, # we use that for installation. # INSTALL_PREFIX := $(if $(DESTDIR),$(DESTDIR)/$(PREFIX),$(PREFIX)) BIN := $(INSTALL_PREFIX)/bin DOC := $(INSTALL_PREFIX)/share/doc/charliecloud TEST := $(DOC)/test # LIBEXEC_DIR is modeled after FHS 3.0 and # https://www.gnu.org/prep/standards/html_node/Directory-Variables.html. It # contains any executable helpers that are not needed in PATH. Default is # libexec/charliecloud which will be preprended with the PREFIX. LIBEXEC_DIR ?= libexec/charliecloud LIBEXEC_INST := $(INSTALL_PREFIX)/$(LIBEXEC_DIR) LIBEXEC_RUN := $(PREFIX)/$(LIBEXEC_DIR) .PHONY: install install: all @test -n "$(PREFIX)" || \ (echo "No PREFIX specified. Lasciando ogni speranza." && false) @echo Installing in $(INSTALL_PREFIX) # binaries install -d $(BIN) install -pm 755 -t $(BIN) $$(find bin -type f -executable) # Modify scripts to relate to new libexec location. for scriptfile in $$(find bin -type f -executable -printf "%f\n"); do \ sed -i "s#^LIBEXEC=.*#LIBEXEC=$(LIBEXEC_RUN)#" $(BIN)/$${scriptfile}; \ done # Install ch-run setuid if either SETUID=yes is specified or the binary # in the build directory is setuid. if [ -n "$(SETUID)" ]; then \ if [ $$(id -u) -eq 0 ]; then \ chown root $(BIN)/ch-run; \ chmod u+s $(BIN)/ch-run; \ else \ sudo chown root $(BIN)/ch-run; \ sudo chmod u+s $(BIN)/ch-run; \ fi \ elif [ -u bin/ch-run ]; then \ sudo chmod u+s $(BIN)/ch-run; \ fi # executable helpers install -d $(LIBEXEC_INST) install -pm 644 -t $(LIBEXEC_INST) bin/base.sh bin/version.sh sed -i "s#^LIBEXEC=.*#LIBEXEC=$(LIBEXEC_RUN)#" $(LIBEXEC_INST)/base.sh # misc "documentation" install -d $(DOC) install -pm 644 -t $(DOC) COPYRIGHT LICENSE README.rst # examples for i in examples/syscalls examples/{serial,mpi,other}/*; do \ install -d $(DOC)/$$i; \ install -pm 644 -t $(DOC)/$$i $$i/*; \ done chmod 755 $(DOC)/examples/serial/hello/hello.sh \ $(DOC)/examples/syscalls/pivot_root \ $(DOC)/examples/syscalls/userns find $(DOC)/examples -name Build -exec chmod 755 {} \; # tests install -d $(TEST) install -pm 644 -t $(TEST) test/*.bats test/common.bash test/Makefile install -pm 755 -t $(TEST) test/Build.* install -pm 644 -t $(TEST) test/Dockerfile.* test/Docker_Pull.* install -pm 755 -t $(TEST) test/make-perms-test install -d $(TEST)/chtest install -pm 644 -t $(TEST)/chtest test/chtest/* chmod 755 $(TEST)/chtest/Build $(TEST)/chtest/*.py ln -sf ../../../../bin $(TEST)/bin # Bats (if embedded) if [ -d test/bats/bin ]; then \ install -d $(TEST)/bats && \ install -pm 644 -t $(TEST)/bats test/bats/CONDUCT.md \ test/bats/LICENSE \ test/bats/README.md && \ install -d $(TEST)/bats/libexec && \ install -pm 755 -t $(TEST)/bats/libexec test/bats/libexec/* && \ install -d $(TEST)/bats/bin && \ ln -sf ../libexec/bats $(TEST)/bats/bin/bats && \ ln -sf bats/bin/bats $(TEST)/bats; \ fi charliecloud-0.2.3~pre+1a5609e/README.rst000066400000000000000000000061261320367540400175450ustar00rootroot00000000000000What is Charliecloud? --------------------- Charliecloud provides user-defined software stacks (UDSS) for high-performance computing (HPC) centers. This "bring your own software stack" functionality addresses needs such as: * software dependencies that are numerous, complex, unusual, diferently configured, or simply newer/older than what the center provides; * build-time requirements unavailable within the center, such as relatively unfettered internet access; * validated software stacks and configuration to meet the standards of a particular field of inquiry; * portability of environments between resources, including workstations and other test and development system not managed by the center; * consistent environments, even archivally so, that can be easily, reliabily, and verifiably reproduced in the future; and/or * usability and comprehensibility. **WARNING: Cray CLE in recent versions** has a bug that crashes nodes when cleaning up after some jobs, including if Charliecloud has been used. See the installation instructions for a workaround. How does it work? ----------------- Charliecloud uses Linux user namespaces to run containers with no privileged operations or daemons and minimal configuration changes on center resources. This simple approach avoids most security risks while maintaining access to the performance and functionality already on offer. Container images can be built using Docker or anything else that can generate a standard Linux filesystem tree. Because user namespaces are available only in newer kernel versions, an experimental setuid mode is also provided to let sites evaluate Charliecloud even if they do not have user namespace-capable kernels readily available. How do I learn more? -------------------- * Documentation: https://hpc.github.io/charliecloud * GitHub repository: https://github.com/hpc/charliecloud * We wrote an article for USENIX's magazine *;login:* that explains in more detail the motivation for Charliecloud and the technology upon which it is based: https://www.usenix.org/publications/login/fall2017/priedhorsky * A more technical resource is our Supercomputing 2017 paper: http://permalink.lanl.gov/object/tr?what=info:lanl-repo/lareport/LA-UR-16-22370 Who is responsible? ------------------- The core Charliecloud team at Los Alamos is: * Reid Priedhorsky , co-founder and BDFL * Tim Randles , co-founder * Michael Jennings Patches (code, documentation, etc.) contributed by: * Reid Priedhorsky * Oliver Freyermuth * Matthew Vernon * Lowell Wofford How can I participate? ---------------------- Questions, comments, feature requests, bug reports, etc. can be directed to: * our mailing list: *charliecloud@groups.io* or https://groups.io/g/charliecloud * issues on GitHub Patches are much appreciated on the software itself as well as documentation. Optionally, please include in your first patch a credit for yourself in the list above. We are friendly and welcoming of diversity on all dimensions. charliecloud-0.2.3~pre+1a5609e/VERSION000066400000000000000000000000121320367540400171120ustar00rootroot000000000000000.2.3~pre charliecloud-0.2.3~pre+1a5609e/bin/000077500000000000000000000000001320367540400166215ustar00rootroot00000000000000charliecloud-0.2.3~pre+1a5609e/bin/Makefile000066400000000000000000000007711320367540400202660ustar00rootroot00000000000000bin := ch-run ch-ssh src := $(wildcard *.c) obj := $(src:.c=.o) .PHONY: all all: $(bin) $(obj): charliecloud.h Makefile $(bin): charliecloud.o .PHONY: clean clean: rm -Rf *.o $(bin) ifdef SETUID export CFLAGS += -DSETUID all: setuid setuid: ch-run # Use sudo only if not already root. if [ $$(id -u) -eq 0 ]; then \ chown root $<; \ chmod u+s $<; \ elif ( command -v sudo >/dev/null 2>&1 && sudo -v >/dev/null 2>&1 ); then \ sudo chown root $<; \ sudo chmod u+s $<; \ fi endif charliecloud-0.2.3~pre+1a5609e/bin/base.sh000066400000000000000000000006501320367540400200700ustar00rootroot00000000000000CH_BIN="$(cd "$(dirname "$0")" && pwd)" LIBEXEC="$(cd "$(dirname "$0")" && pwd)" . ${LIBEXEC}/version.sh # Do we need sudo to run docker? if ( docker info > /dev/null 2>&1 ); then export DOCKER="docker" else export DOCKER="sudo docker" fi # Use parallel gzip if it's available. ("command -v" is POSIX.1-2008.) if ( command -v pigz >/dev/null 2>&1 ); then export GZIP_CMD=pigz else export GZIP_CMD=gzip fi charliecloud-0.2.3~pre+1a5609e/bin/ch-build000077500000000000000000000027311320367540400202410ustar00rootroot00000000000000#!/bin/sh LIBEXEC="$(cd "$(dirname "$0")" && pwd)" . ${LIBEXEC}/base.sh usage () { cat 1>&2 <&2 <&2 <&2 < $TAR $DOCKER rm $cid > /dev/null # FIXME: This is brittle. We want the filename and size, but not the rest, so # we can't just ask ls. Another option is stat and numfmt, but the latter may # not be very portable. ls -lh $TAR | awk '{ print $5,$9 }' charliecloud-0.2.3~pre+1a5609e/bin/ch-run.c000066400000000000000000000402231320367540400201620ustar00rootroot00000000000000/* Copyright © Los Alamos National Security, LLC, and others. */ /* Notes: 1. This program does not bother to free memory allocations, since they are modest and the program is short-lived. 2. If you change any of the setuid code, consult the FAQ for some important design goals. */ #define _GNU_SOURCE #include #include #include #include #include #include #include #include #include #include #include #include #include #include "charliecloud.h" /** Constants and macros **/ /* Host filesystems to bind. */ #define USER_BINDS_MAX 10 const char * DEFAULT_BINDS[] = { "/dev", "/etc/passwd", "/etc/group", "/etc/hosts", "/etc/resolv.conf", "/proc", "/sys", NULL }; /* Number of supplemental GIDs we can deal with. */ #define SUPP_GIDS_MAX 128 /* Maximum length of paths we're willing to deal with. (Note that system-defined PATH_MAX isn't reliable.) */ #define PATH_CHARS 4096 /* Log the current UIDs. */ #define LOG_IDS log_ids(__func__, __LINE__) /** Command line options **/ const char usage[] = "\ \n\ Run a command in a Charliecloud container.\n\ \v\ Example:\n\ \n\ $ ch-run /data/foo -- echo hello\n\ hello\n\ \n\ You cannot use this program to actually change your UID."; const char args_doc[] = "NEWROOT CMD [ARG...]"; const struct argp_option options[] = { { "bind", 'b', "SRC[:DST]", 0, "mount SRC at guest DST (default /mnt/0, /mnt/1, etc.)"}, { "write", 'w', 0, 0, "mount image read-write"}, { "no-home", -2, 0, 0, "do not bind-mount your home directory"}, { "cd", 'c', "DIR", 0, "initial working directory in container"}, #ifndef SETUID { "gid", 'g', "GID", 0, "run as GID within container" }, #endif { "is-setuid", -1, 0, 0, "exit successfully if compiled for setuid, else fail" }, { "private-tmp", 't', 0, 0, "use container-private /tmp" }, #ifndef SETUID { "uid", 'u', "UID", 0, "run as UID within container" }, #endif { "verbose", 'v', 0, 0, "be more verbose (debug if repeated)" }, { "version", 'V', 0, 0, "print version and exit" }, { 0 } }; struct bind { char * src; char * dst; }; struct args { struct bind binds[USER_BINDS_MAX+1]; gid_t container_gid; uid_t container_uid; char * newroot; char * initial_working_dir; bool private_home; bool private_tmp; bool writable; int user_cmd_start; // index into argv where NEWROOT is int verbose; }; void enter_udss(char * newroot, bool writeable, struct bind * binds, bool private_tmp, bool private_home); void log_ids(const char * func, int line); void run_user_command(int argc, char * argv[], int user_cmd_start); static error_t parse_opt(int key, char * arg, struct argp_state * state); void privs_verify_invoking(); void setup_namespaces(uid_t cuid, gid_t cgid); #ifdef SETUID void privs_drop_permanently(); void privs_drop_temporarily(); void privs_restore(); #endif struct args args; const struct argp argp = { options, parse_opt, args_doc, usage }; /** Main **/ int main(int argc, char * argv[]) { privs_verify_invoking(); #ifdef SETUID privs_drop_temporarily(); #endif memset(args.binds, 0, sizeof(args.binds)); args.container_gid = getgid(); args.container_uid = getuid(); args.initial_working_dir = NULL; args.private_home = false; args.private_tmp = false; args.verbose = 0; Z_ (setenv("ARGP_HELP_FMT", "opt-doc-col=25,no-dup-args-note", 0)); Z_ (argp_parse(&argp, argc, argv, 0, &(args.user_cmd_start), &args)); Te (args.user_cmd_start < argc - 1, "NEWROOT and/or CMD not specified"); assert(args.binds[USER_BINDS_MAX].src == NULL); // overrun in argp_parse? args.newroot = realpath(argv[args.user_cmd_start++], NULL); Tf (args.newroot != NULL, "couldn't resolve image path"); if (args.verbose) { fprintf(stderr, "newroot: %s\n", args.newroot); fprintf(stderr, "container uid: %u\n", args.container_uid); fprintf(stderr, "container gid: %u\n", args.container_gid); fprintf(stderr, "private /tmp: %d\n", args.private_tmp); } setup_namespaces(args.container_uid, args.container_gid); enter_udss(args.newroot, args.writable, args.binds, args.private_tmp, args.private_home); #ifdef SETUID privs_drop_permanently(); #endif run_user_command(argc, argv, args.user_cmd_start); // should never return exit(EXIT_FAILURE); } /** Supporting functions **/ /* Enter the UDSS. After this, we are inside the UDSS. Note that pivot_root(2) requires a complex dance to work, i.e., to avoid multiple undocumented error conditions. This dance is explained in detail in examples/syscalls/pivot_root.c. */ void enter_udss(char * newroot, bool writable, struct bind * binds, bool private_tmp, bool private_home) { char * base; char * dir; char * oldpath; char * path; char bin[PATH_CHARS]; struct stat st; LOG_IDS; #ifdef SETUID privs_restore(); // Make the whole filesystem tree private. Otherwise, there's a big mess, // as the manipulations of the shared mounts propagate into the parent // namespace. Then the mount(MS_MOVE) call below fails with EINVAL, and // nothing is cleaned up so the mounts are a big tangle and ch-tar2dir will // delete your home directory. I think this is redundant with some of the // below, but it doesn't seem to hurt. Z_ (mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL)); #endif // Claim newroot for this namespace Zf (mount(newroot, newroot, NULL, MS_REC | MS_BIND | MS_PRIVATE, NULL), newroot); // Bind-mount default directories at the same host and guest path for (int i = 0; DEFAULT_BINDS[i] != NULL; i++) { T_ (1 <= asprintf(&path, "%s%s", newroot, DEFAULT_BINDS[i])); Z_ (mount(DEFAULT_BINDS[i], path, NULL, MS_REC | MS_BIND | MS_RDONLY, NULL)); } // Container /tmp T_ (1 <= asprintf(&path, "%s%s", newroot, "/tmp")); if (private_tmp) { Z_ (mount(NULL, path, "tmpfs", 0, 0)); } else { Z_ (mount("/tmp", path, NULL, MS_REC | MS_BIND, NULL)); } if (!private_home) { // Mount tmpfs on guest /home because guest root is read-only T_ (1 <= asprintf(&path, "%s/home", newroot)); Z_ (mount(NULL, path, "tmpfs", 0, "size=4m")); // Bind-mount user's home directory at /home/$USER. The main use case is // dotfiles. T_ (1 <= asprintf(&path, "%s/home/%s", newroot, getenv("USER"))); Z_ (mkdir(path, 0755)); Z_ (mount(getenv("HOME"), path, NULL, MS_REC | MS_BIND, NULL)); } // Bind-mount /usr/bin/ch-ssh if it exists. T_ (1 <= asprintf(&path, "%s/usr/bin/ch-ssh", newroot)); if (stat(path, &st)) { T_ (errno == ENOENT); } else { T_ (-1 != readlink("/proc/self/exe", bin, PATH_CHARS)); bin[PATH_CHARS-1] = 0; // guarantee string termination dir = dirname(bin); T_ (1 <= asprintf(&oldpath, "%s/ch-ssh", dir)); Z_ (mount(oldpath, path, NULL, MS_BIND, NULL)); } // Bind-mount user-specified directories at guest DST and|or /mnt/i, // which must exist for (int i = 0; binds[i].src != NULL; i++) { T_ (1 <= asprintf(&path, "%s%s", newroot, binds[i].dst)); Zf (mount(binds[i].src, path, NULL, MS_REC | MS_BIND, NULL), "could not bind %s to %s", binds[i].src, binds[i].dst); } // Overmount / to avoid EINVAL if it's a rootfs T_ (path = strdup(newroot)); dir = dirname(path); T_ (path = strdup(newroot)); base = basename(path); Z_ (mount(dir, dir, NULL, MS_REC | MS_BIND | MS_PRIVATE, NULL)); Z_ (chdir(dir)); Z_ (mount(dir, "/", NULL, MS_MOVE, NULL)); Z_ (chroot(".")); T_ (1 <= asprintf(&newroot, "/%s", base)); if (!writable && !(access(newroot, W_OK) == -1 && errno == EROFS)) { // Re-mount image read-only Zf (mount(NULL, newroot, NULL, MS_REMOUNT | MS_BIND | MS_RDONLY, NULL), "can't re-mount image read-only (is it on NFS?)"); } // Pivot into the new root. Use /dev because it's available even in // extremely minimal images. T_ (1 <= asprintf(&path, "%s/dev", newroot)); Z_ (chdir(newroot)); Z_ (syscall(SYS_pivot_root, newroot, path)); Z_ (chroot(".")); Z_ (umount2("/dev", MNT_DETACH)); #ifdef SETUID privs_drop_temporarily(); #endif if (args.initial_working_dir != NULL) Zf (chdir(args.initial_working_dir), "can't cd to %s", args.initial_working_dir); } /* If verbose, print uids and gids on stderr prefixed with where. */ void log_ids(const char * func, int line) { uid_t ruid, euid, suid; gid_t rgid, egid, sgid; gid_t supp_gids[SUPP_GIDS_MAX]; int supp_gid_ct; if (args.verbose >= 2) { Z_ (getresuid(&ruid, &euid, &suid)); Z_ (getresgid(&rgid, &egid, &sgid)); fprintf(stderr, "%s %d: uids=%d,%d,%d, gids=%d,%d,%d + ", func, line, ruid, euid, suid, rgid, egid, sgid); supp_gid_ct = getgroups(SUPP_GIDS_MAX, supp_gids); if (supp_gid_ct == -1) { T_ (errno == EINVAL); Te (0, "more than %d groups", SUPP_GIDS_MAX); } for (int i = 0; i < supp_gid_ct; i++) { if (i > 0) fprintf(stderr, ","); fprintf(stderr, "%d", supp_gids[i]); } fprintf(stderr, "\n"); } } /* Parse one command line option. Called by argp_parse(). */ static error_t parse_opt(int key, char * arg, struct argp_state * state) { struct args * as = state->input; int i; long l; switch (key) { case -1: #ifdef SETUID exit(EXIT_SUCCESS); #else exit(EXIT_FAILURE); #endif break; case -2: as->private_home = true; break; case 'c': as->initial_working_dir = arg; break; case 'b': for (i = 0; as->binds[i].src != NULL; i++) ; Te (i < USER_BINDS_MAX, "--bind can be used at most %d times", USER_BINDS_MAX); as->binds[i].src = strsep(&arg, ":"); assert(as->binds[i].src != NULL); if (arg) as->binds[i].dst = arg; else // arg is NULL => no destination specified T_ (1 <= asprintf(&(as->binds[i].dst), "/mnt/%d", i)); Te (as->binds[i].src[0] != 0, "--bind: no source provided"); Te (as->binds[i].dst[0] != 0, "--bind: no destination provided"); break; case 'g': errno = 0; l = strtol(arg, NULL, 0); Te (errno == 0 && l >= 0, "GID must be a non-negative integer"); as->container_gid = (gid_t)l; break; case 't': as->private_tmp = true; break; case 'u': errno = 0; l = strtol(arg, NULL, 0); Te (errno == 0 && l >= 0, "UID must be a non-negative integer"); as->container_uid = (uid_t)l; break; case 'V': version(); exit(EXIT_SUCCESS); break; case 'v': as->verbose++; break; case 'w': as->writable = true; break; default: return ARGP_ERR_UNKNOWN; }; return 0; } /* Validate that the UIDs and GIDs are appropriate for program start, and abort if not. Note: If the binary is setuid, then the real UID will be the invoking user and the effective and saved UIDs will be the owner of the binary. Otherwise, all three IDs are that of the invoking user. */ void privs_verify_invoking() { uid_t ruid, euid, suid; gid_t rgid, egid, sgid; Z_ (getresuid(&ruid, &euid, &suid)); Z_ (getresgid(&rgid, &egid, &sgid)); // Calling the program if user is really root is OK. if ( ruid == 0 && euid == 0 && suid == 0 && rgid == 0 && egid == 0 && sgid == 0) return; // Now that we know user isn't root, no GID privilege is allowed. T_ (egid != 0); // no privilege T_ (egid == rgid && egid == sgid); // no setuid or funny business // Setuid must match the compiled mode. #ifdef SETUID T_ (ruid != 0 && euid == 0 && suid == 0); // must be setuid root #else T_ (euid != 0); // no privilege T_ (euid == ruid && euid == suid); // no setuid or funny business #endif } /* Drop UID privileges permanently. */ #ifdef SETUID void privs_drop_permanently() { uid_t uid_wanted, ruid, euid, suid; gid_t rgid, egid, sgid; // Drop privileges. uid_wanted = getuid(); T_ (uid_wanted != 0); // abort if real UID is root Z_ (setresuid(uid_wanted, uid_wanted, uid_wanted)); // Try to regain privileges; it should fail. T_ (-1 == setuid(0)); T_ (-1 == setresuid(-1, 0, -1)); // UIDs should be unprivileged and the same. Z_ (getresuid(&ruid, &euid, &suid)); T_ (ruid == uid_wanted); T_ (uid_wanted == ruid && uid_wanted == euid && uid_wanted == suid); // GIDs should be unprivileged and the same. Z_ (getresgid(&rgid, &egid, &sgid)); T_ (rgid != 0); T_ (rgid == egid && rgid == sgid); } #endif // SETUID /* Drop UID privileges temporarily; can be regained with privs_restore(). */ #ifdef SETUID void privs_drop_temporarily() { uid_t unpriv_uid = getuid(); if (unpriv_uid == 0) { // Invoked as root, so descend to nobody. unpriv_uid = 65534; } Z_ (setresuid(-1, unpriv_uid, -1)); T_ (unpriv_uid == geteuid()); } #endif // SETUID /* Restore privileges that have been dropped with privs_drop_temporarily(). */ #ifdef SETUID void privs_restore() { uid_t ruid, euid, suid; Z_ (setresuid(-1, 0, -1)); Z_ (getresuid(&ruid, &euid, &suid)); } #endif // SETUID /* Replace the current process with user command and arguments. argv will be overwritten in order to avoid the need for copying it, because execvp() requires null-termination instead of an argument count. */ void run_user_command(int argc, char * argv[], int user_cmd_start) { char * old_path, * new_path; LOG_IDS; for (int i = user_cmd_start; i < argc; i++) argv[i - user_cmd_start] = argv[i]; argv[argc - user_cmd_start] = NULL; // Append /bin to $PATH if not already present. See FAQ. old_path = getenv("PATH"); if (old_path == NULL) { if (args.verbose) fprintf(stderr, "warning: $PATH not set\n"); } else if ( strstr(old_path, "/bin") != old_path && !strstr(old_path, ":/bin")) { T_ (1 <= asprintf(&new_path, "%s:/bin", old_path)); Z_ (setenv("PATH", new_path, 1)); if (args.verbose) fprintf(stderr, "new $PATH: %s\n", new_path); } if (args.verbose) { fprintf(stderr, "cmd at %d/%d:", user_cmd_start, argc); for (int i = 0; argv[i] != NULL; i++) fprintf(stderr, " %s", argv[i]); fprintf(stderr, "\n"); } execvp(argv[0], argv); // only returns if error Tf (0, "can't execve(2) user command"); } /* Activate the desired isolation namespaces. */ void setup_namespaces(uid_t cuid, gid_t cgid) { #ifdef SETUID // can't change IDs from invoking T_ (cuid == getuid()); T_ (cgid == getgid()); privs_restore(); Z_ (unshare(CLONE_NEWNS)); privs_drop_temporarily(); #else // not SETUID int fd; uid_t euid = -1; gid_t egid = -1; euid = geteuid(); egid = getegid(); LOG_IDS; Z_ (unshare(CLONE_NEWNS|CLONE_NEWUSER)); LOG_IDS; /* Write UID map. What we are allowed to put here is quite limited. Because we do not have CAP_SETUID in the *parent* user namespace, we can map exactly one UID: an arbitrary container UID to our EUID in the parent namespace. This is sufficient to change our UID within the container; no setuid(2) or similar required. This is because the EUID of the process in the parent namespace is unchanged, so the kernel uses our new 1-to-1 map to convert that EUID into the container UID for most (maybe all) purposes. */ T_ (-1 != (fd = open("/proc/self/uid_map", O_WRONLY))); T_ (1 <= dprintf(fd, "%d %d 1\n", cuid, euid)); Z_ (close(fd)); LOG_IDS; T_ (-1 != (fd = open("/proc/self/setgroups", O_WRONLY))); T_ (1 <= dprintf(fd, "deny\n")); Z_ (close(fd)); T_ (-1 != (fd = open("/proc/self/gid_map", O_WRONLY))); T_ (1 <= dprintf(fd, "%d %d 1\n", cgid, egid)); Z_ (close(fd)); LOG_IDS; #endif // not SETUID } charliecloud-0.2.3~pre+1a5609e/bin/ch-ssh.c000066400000000000000000000036551320367540400201630ustar00rootroot00000000000000/* Copyright © Los Alamos National Security, LLC, and others. */ #define _GNU_SOURCE #include #include #include #include #include #include "charliecloud.h" const char usage[] = "\ Usage: CH_RUN_ARGS=\"NEWROOT [ARG...]\" ch-ssh [OPTION...] HOST CMD [ARG...]\n\ \n\ Run a remote command in a Charliecloud container.\n\ \n\ Example:\n\ \n\ $ export CH_RUN_ARGS=/data/foo\n\ $ ch-ssh example.com -- echo hello\n\ hello\n\ \n\ Arguments to ch-run, including the image to activate, are specified in the\n\ CH_RUN_ARGS environment variable. Important caveat: Words in CH_RUN_ARGS are\n\ delimited by spaces only; it is not shell syntax. In particular, quotes and\n\ and backslashes are not interpreted.\n"; #define ARGS_MAX 262143 // assume 2MB buffer and length of each argument >= 7 int main(int argc, char * argv[]) { int i, j; char * ch_run_args; char * args[ARGS_MAX+1]; if (argc >= 2 && strcmp(argv[1], "--help") == 0) { fprintf(stderr, usage); return 0; } if (argc >= 2 && strcmp(argv[1], "--version") == 0) { version(); return 0; } memset(args, 0, sizeof(args)); args[0] = "ssh"; // ssh option arguments for (i = 1; i < argc && i < ARGS_MAX && argv[i][0] == '-'; i++) args[i] = argv[i]; // destination host if (i < argc && i < ARGS_MAX) { args[i] = argv[i]; i++; } // insert ch-run command ch_run_args = getenv("CH_RUN_ARGS"); Te (ch_run_args != NULL, "CH_RUN_ARGS not set"); args[i] = "ch-run"; for (j = 1; i + j < ARGS_MAX; j++, ch_run_args = NULL) { args[i+j] = strtok(ch_run_args, " "); if (args[i+j] == NULL) break; } // copy remaining arguments for ( ; i < argc && i + j < ARGS_MAX; i++) args[i+j] = argv[i]; //for (i = 0; args[i] != NULL; i++) // printf("%d: %s\n", i, args[i]); execvp("ssh", args); Tf (0, "can't execute ssh"); } charliecloud-0.2.3~pre+1a5609e/bin/ch-tar2dir000077500000000000000000000043441320367540400205130ustar00rootroot00000000000000#!/bin/sh LIBEXEC="$(cd "$(dirname "$0")" && pwd)" . ${LIBEXEC}/base.sh usage () { cat 1>&2 <&2 exit 1 fi if [ ! -d "$NEWROOT" ]; then echo "creating new image $NEWROOT" else if [ -f "$NEWROOT/$SENTINEL" ] \ && [ -d "$NEWROOT/bin" ] \ && [ -d "$NEWROOT/lib" ] \ && [ -d "$NEWROOT/usr" ]; then echo "replacing existing image $NEWROOT" 1>&2 rm -Rf --one-file-system $NEWROOT else echo "$NEWROOT exists but does not appear to be an image" 1>&2 exit 1 fi fi mkdir "$NEWROOT" echo 'This directory is a Charliecloud container image.' > "$NEWROOT/$SENTINEL" tar x$VERBOSE -I $GZIP_CMD -C "$NEWROOT" -f "$TARBALL" --exclude='dev/*' # Make all directories writeable so we can delete image later (hello, Red Hat). find "$NEWROOT" -type d -a ! -perm /200 -exec chmod u+w {} + # Ensure directories that ch-run needs exist. mkdir -p "$NEWROOT/dev" for i in $(seq 0 9); do mkdir -p "$NEWROOT/mnt/$i"; done echo "$NEWROOT unpacked ok" charliecloud-0.2.3~pre+1a5609e/bin/charliecloud.c000066400000000000000000000020541320367540400214240ustar00rootroot00000000000000/* Copyright © Los Alamos National Security, LLC, and others. */ #define _GNU_SOURCE #include #include #include #include #include #include "version.h" /* Print a formatted error message on stderr, followed by the string expansion of errno, source file, line number, errno, and newline; then exit unsuccessfully. If errno is zero, then don't include it, because then it says "success", which is super confusing in an error message. */ void fatal(char * file, int line, int errno_, char * fmt, ...) { va_list ap; fputs(program_invocation_short_name, stderr); fputs(": ", stderr); if (fmt == NULL) fputs("error", stderr); else { va_start(ap, fmt); vfprintf(stderr, fmt, ap); va_end(ap); } if (errno) fprintf(stderr, ": %s (%s:%d %d)\n", strerror(errno), file, line, errno); else fprintf(stderr, " (%s:%d)\n", file, line); exit(EXIT_FAILURE); } /* Report the version number. */ void version(void) { fprintf(stderr, "%s\n", VERSION); } charliecloud-0.2.3~pre+1a5609e/bin/charliecloud.h000066400000000000000000000036501320367540400214340ustar00rootroot00000000000000/* Copyright © Los Alamos National Security, LLC, and others. */ /* Test some value, and if it's not what we expect, exit with an error. These are macros so we have access to the file and line number. verify x is true (non-zero); otherwise print then exit: T_ (x) default error message including file, line, errno Tf (x, fmt, ...) printf-style message followed by file, line, errno Te (x, fmt, ...) same without errno verify x is zero (false); otherwise print as above & exit Z_ (x) Zf (x, fmt, ...) Ze (x, fmt, ...) errno is omitted if it's zero. Examples: Z_ (chdir("/does/not/exist")); -> ch-run: error: No such file or directory (ch-run.c:138 2) Zf (chdir("/does/not/exist"), "foo"); -> ch-run: foo: No such file or directory (ch-run.c:138 2) Ze (chdir("/does/not/exist"), "foo"); -> ch-run: foo (ch-run.c:138) errno = 0; Zf (0, "foo"); -> ch-run: foo (ch-run.c:138) Typically, Z_ and Zf are used to check system and standard library calls, while T_ and Tf are used to assert developer-specified conditions. errno is not altered by these macros unless they exit the program. FIXME: It would be nice if we could collapse these to fewer macros. However, when looking into that I ended up in preprocessor black magic (e.g. https://stackoverflow.com/a/2308651) that I didn't understand. */ #define T_(x) if (!(x)) fatal(__FILE__, __LINE__, errno, NULL) #define Tf(x, ...) if (!(x)) fatal(__FILE__, __LINE__, errno, __VA_ARGS__) #define Te(x, ...) if (!(x)) fatal(__FILE__, __LINE__, 0, __VA_ARGS__) #define Z_(x) if (x) fatal(__FILE__, __LINE__, errno, NULL) #define Zf(x, ...) if (x) fatal(__FILE__, __LINE__, errno, __VA_ARGS__) #define Ze(x, ...) if (x) fatal(__FILE__, __LINE__, 0, __VA_ARGS__) void fatal(char * file, int line, int errno_, char * fmt, ...); void version(void); charliecloud-0.2.3~pre+1a5609e/doc-src/000077500000000000000000000000001320367540400174035ustar00rootroot00000000000000charliecloud-0.2.3~pre+1a5609e/doc-src/Makefile000066400000000000000000000155721320367540400210550ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # Help dumps from scripts SCRIPTHELPS := $(addsuffix .help, \ $(subst ../bin/,,$(shell find ../bin -executable -type f))) .PHONY: help clean html web dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext all: html # All programs and scripts need to accept "--help", even if they normally use # single-character flags only. # # Adding in $(SCRIPTHELPS) is voodoo to make the pattern work. Otherwise, it # tries to build e.g. vm/vcluster.help from vm/../bin/vcluster. This is # documented, but I didn't really understand. See # http://stackoverflow.com/a/13552576/396038 $(SCRIPTHELPS): %.help: ../bin/% $< --help > $@ 2>&1 # Re-building programs in ch-run from here fails, so don't do it. # FIXME: Rather than stopping the build, it continues with warnings. %.o : %.c % : %.o % : %.c help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: rm -rf $(BUILDDIR)/* find . -name '*.help' -delete rm -rf ../doc/* ../doc/.buildinfo ../doc/.nojekyll html: $(SCRIPTHELPS) $(SPHINXBUILD) -W -b html -D release="$$(cat ../VERSION)" -D version="$$(git rev-parse --short HEAD)" $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." @echo rsync -a --delete --exclude-from RSYNC_EXCLUDE $(BUILDDIR)/html/ ../doc touch ../doc/.nojekyll @echo @echo "HTML pages copied to doc/." # see http://stackoverflow.com/a/2659808/396038 web: html @echo @echo Making sure there are no uncommitted changes git diff-index --quiet --cached HEAD git diff-files --quiet @echo Can we talk to GitHub? cd ../doc && git ls-remote > /dev/null @echo Publishing new docs cd ../doc && git add --all cd ../doc && git commit -a -m "docs for commit $$(cd .. && git rev-parse --short HEAD)" cd ../doc && git push origin gh-pages dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/QUAC.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/QUAC.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/QUAC" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/QUAC" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." charliecloud-0.2.3~pre+1a5609e/doc-src/README000066400000000000000000000030621320367540400202640ustar00rootroot00000000000000This documentation is built using Sphinx with the sphinx-rtd-theme. Both Python 2.7 and 3.5+ should work; 3.5+ is preferred. To build the HTML ----------------- Make sure you have a current version of sphinx-rtd-theme installed: $ pip3 install sphinx sphinx-rtd-theme Note: If you're on Debian Stretch or some version of Ubuntu, this will silently install into ~/.local, leaving the sphinx-build binary in ~/.local/bin, which is not on your path. One workaround (untested) is to run pip3 as root, which violates principle of least-privilege. A better workaround, assuming you can write to /usr/local, is to add the undocumented and non-standard --system argument to install in /usr/local instead. (This matches previous pip behavior.) See: https://bugs.debian.org/725848 Then: $ make The HTML files are copied to ../doc with rsync. Anything that should not be copied should be listed in RSYNC_EXCLUDE. There is also a "clean" target that removes all the derived files as well as everything in ../doc. Publishing to the web --------------------- If you have write access to the repository, you can update the web documentation (i.e., http://hpc.github.io/charliecloud). Normally, ../doc is normal directory that is ignored by Git. To publish to the web, that diretory needs to contain a Git checkout of the gh-pages branch (not a submodule). To set that up: $ rm -Rf doc $ git clone git@github.com:hpc/charliecloud.git doc $ cd doc $ git checkout gh-pages To publish: $ make web It sometimes takes a few minutes for the web pages to update. charliecloud-0.2.3~pre+1a5609e/doc-src/RSYNC_EXCLUDE000066400000000000000000000000461320367540400213750ustar00rootroot00000000000000.git .buildinfo _sources objects.inv charliecloud-0.2.3~pre+1a5609e/doc-src/conf.py000066400000000000000000000201061320367540400207010ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # QUAC documentation build configuration file, created by # sphinx-quickstart on Wed Feb 20 12:04:35 2013. # # This file is execfile()d with the current directory set to its containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys, os # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. #sys.path.insert(0, os.path.abspath('.')) # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.mathjax', 'sphinx.ext.viewcode'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'Charliecloud' copyright = u'2014–2017, Los Alamos National Security, LLC' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. #version = '0.1' # The full version, including alpha/beta/rc tags. #release = '0.1' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. today_fmt = '%Y-%m-%d %H:%M %Z' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build'] # The reST default role (used for this markup: `text`) to use for all documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. #pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. import sphinx_rtd_theme html_theme = 'sphinx_rtd_theme' html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] highlight_language = 'console' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {'bodyfont': 'serif', # for agogo # 'pagewidth': '60em', # 'documentwidth': '43em', # 'sidebarwidth': '17em', # 'textalign':'left'} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". # #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". #html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. html_domain_indices = False # If false, no index is generated. html_use_index = False # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. html_show_sourcelink = False # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. html_show_sphinx = False # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'charliedoc' # -- Options for LaTeX output -------------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ('index', 'charlie.tex', u'Charliecloud Documentation', u'Reid Priedhorsky, Tim Randles, and others', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'Charliecloud', u'Charliecloud Documentation', [u'Reid Priedhorsky, Tim Randles, and others'], 1) ] # If true, show URL addresses after external links. #man_show_urls = False # -- Options for Texinfo output ------------------------------------------------ # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ('index', 'Charliecloud', u'Charliecloud Documentation', u'Reid Priedhorsky, Tim Randles, and others', 'Charliecloud', 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. #texinfo_appendices = [] # If false, no module index is generated. #texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. #texinfo_show_urls = 'footnote' charliecloud-0.2.3~pre+1a5609e/doc-src/copyright.rst000066400000000000000000000001071320367540400221430ustar00rootroot00000000000000Copyright and license ********************* .. include:: ../COPYRIGHT charliecloud-0.2.3~pre+1a5609e/doc-src/faq.rst000066400000000000000000000215151320367540400207100ustar00rootroot00000000000000Frequently asked questions (FAQ) ******************************** .. contents:: :depth: 2 :local: Where did the name Charliecloud come from? ========================================== *Charlie* — Charles F. McMillan was director of Los Alamos National Laboratory from June 2011 until December 2017, i.e., at the time Charliecloud was started in early 2014. He is universally referred to as "Charlie" here. *cloud* — Charliecloud provides cloud-like flexibility for HPC systems. How do you spell Charliecloud? ============================== We try to be consistent with *Charliecloud* — one word, no camel case. That is, *Charlie Cloud* and *CharlieCloud* are both incorrect. My app needs to write to :code:`/var/log`, :code:`/run`, etc. ============================================================= Because the image is mounted read-only by default, log files, caches, and other stuff cannot be written anywhere in the image. You have three options: 1. Configure the application to use a different directory. :code:`/tmp` is often a good choice, because it's shared with the host and fast. 2. Use :code:`RUN` commands in your Dockerfile to create symlinks that point somewhere writeable, e.g. :code:`/tmp`, or :code:`/mnt/0` with :code:`ch-run --bind`. 3. Run the image read-write with :code:`ch-run -w`. Be careful that multiple containers do not try to write to the same image files. Tarball build fails with "No command specified" =============================================== The full error from :code:`ch-docker2tar` or :code:`ch-build2dir` is:: docker: Error response from daemon: No command specified. You will also see it with various plain Docker commands. This happens when there is no default command specified in the Dockerfile or any of its ancestors. Some base images specify one (e.g., Debian) and others don't (e.g., Alpine). Docker requires this even for commands that don't seem like they should need it, such as :code:`docker create` (which is what trips up Charliecloud). The solution is to add a default command to your Dockerfile, such as :code:`CMD ["true"]`. :code:`--uid 0` lets me read files I can't otherwise! ===================================================== Some permission bits can give a surprising result with a container UID of 0. For example:: $ whoami reidpr $ echo surprise > ~/cantreadme $ chmod 000 ~/cantreadme $ ls -l ~/cantreadme ---------- 1 reidpr reidpr 9 Oct 3 15:03 /home/reidpr/cantreadme $ cat ~/cantreadme cat: /home/reidpr/cantreadme: Permission denied $ ch-run /var/tmp/hello cat ~/cantreadme cat: /home/reidpr/cantreadme: Permission denied $ ch-run --uid 0 /var/tmp/hello cat ~/cantreadme surprise At first glance, it seems that we've found an escalation -- we were able to read a file inside a container that we could not read on the host! That seems bad. However, what is really going on here is more prosaic but complicated: 1. After :code:`unshare(CLONE_NEWUSER)`, :code:`ch-run` gains all capabilities inside the namespace. (Outside, capabilities are unchanged.) 2. This include :code:`CAP_DAC_OVERRIDE`, which enables a process to read/write/execute a file or directory mostly regardless of its permission bits. (This is why root isn't limited by permissions.) 3. Within the container, :code:`exec(2)` capability rules are followed. Normally, this basically means that all capabilities are dropped when :code:`ch-run` replaces itself with the user command. However, if EUID is 0, which it is inside the namespace given :code:`--uid 0`, then the subprocess keeps all its capabilities. (This makes sense: if root creates a new process, it stays root.) 4. :code:`CAP_DAC_OVERRIDE` within a user namespace is honored for a file or directory only if its UID and GID are both mapped. In this case, :code:`ch-run` maps :code:`reidpr` to container :code:`root` and group :code:`reidpr` to itself. 5. Thus, files and directories owned by the host EUID and EGID (here :code:`reidpr:reidpr`) are available for all access with :code:`ch-run --uid 0`. This isn't a problem. The quirk applies only to files owned by the invoking user, because :code:`ch-run` is unprivileged outside the namespace, and thus he or she could simply :code:`chmod` the file to read it. Access inside and outside the container remains equivalent. References: * http://man7.org/linux/man-pages/man7/capabilities.7.html * http://lxr.free-electrons.com/source/kernel/capability.c?v=4.2#L442 * http://lxr.free-electrons.com/source/fs/namei.c?v=4.2#L328 Why is :code:`/bin` being added to my :code:`$PATH`? ==================================================== Newer Linux distributions replace some root-level directories, such as :code:`/bin`, with symlinks to their counterparts in :code:`/usr`. Some of these distributions (e.g., Fedora 24) have also dropped :code:`/bin` from the default :code:`$PATH`. This is a problem when the guest OS does *not* have a merged :code:`/usr` (e.g., Debian 8 "Jessie"). While Charliecloud's general philosophy is not to manipulate environment variables, in this case, guests can be severely broken if :code:`/bin` is not in :code:`$PATH`. Thus, we add it if it's not there. Further reading: * `The case for the /usr Merge `_ * `Fedora `_ * `Debian `_ How does setuid mode work? ========================== As noted above, :code:`ch-run` has a transition mode that uses setuid-root privileges instead of user namespaces. The goal of this mode is to let sites evaluate Charliecloud even on systems that do not have a Linux kernel that supports user namespaces. We plan to remove this code once user namespaces are more widely available, and we encourage sites to use the unprivileged, non-setuid mode in production. We haven taken care to (1) drop privileges temporarily upon program start and only re-acquire them when needed and (2) drop privileges permanently before executing user code. In order to reliably verify the latter, :code:`ch-run` in setuid mode will refuse to run if invoked directly by root. It may be better to use capabilities and setcap rather than setuid. However, this also relies on newer features, which would hamper the goal of broadly available testing. For example, NFSv3 does not support extended attributes, which are required for setcap files. Dropping privileges safely requires care. We follow the recommendations in "`Setuid demystified `_" as well as the `system call ordering `_ and `privilege drop verification `_ recommendations of the SEI CERT C Coding Standard. We do not worry about the Linux-specific :code:`fsuid` and :code:`fsgid`, which track :code:`euid`/:code:`egid` unless specifically changed, which we don't do. Kernel bugs have existed that violate this invariant, but none are recent. :code:`ch-run` fails with "can't re-mount image read-only" ========================================================== Normally, :code:`ch-run` re-mounts the image directory read-only within the container. This fails if the image resides on certain filesystems, such as NFS (see `issue #9 `_). There are two solutions: 1. Unpack the image into a different filesystem, such as :code:`tmpfs` or local disk. Consult your local admins for a recommendation. Note that :code:`tmpfs` is a lot faster than Lustre. 2. Use the :code:`-w` switch to leave the image mounted read-write. Note that this has may have an impact on reproducibility (because the application can change the image between runs) and/or stability (if there are multiple application processes and one writes a file in the image that another is reading or writing). Which specific :code:`sudo` commands are needed? ================================================ For running images, :code:`sudo` is not needed at all. For building images, it depends on what you would like to support. For example, do you want to let users build images with Docker? Do you want to let them run the build tests? We do not maintain specific lists, but you can search the source code and documentation for uses of :code:`sudo` and :code:`$DOCKER` and evaluate them on a case-by-case basis. (The latter includes :code:`sudo` if needed to invoke :code:`docker` in your environment.) For example:: $ find . \( -type f -executable \ -o -name Makefile \ -o -name '*.bats' \ -o -name '*.rst' \ -o -name '*.sh' \) \ -exec egrep -H '(sudo|\$DOCKER)' {} \; charliecloud-0.2.3~pre+1a5609e/doc-src/index.rst000066400000000000000000000004301320367540400212410ustar00rootroot00000000000000Overview ******** .. include:: ../README.rst .. note:: This documentation is for Charliecloud version |release| (Git commit |version|) and was built |today|. .. toctree:: :numbered: :hidden: install tutorial script-help virtualbox faq copyright charliecloud-0.2.3~pre+1a5609e/doc-src/install.rst000066400000000000000000000265451320367540400216170ustar00rootroot00000000000000Installation ************ .. warning:: **If you are installing on a Cray** and have not applied the patch for Cray case #188073, you must use the `cray branch `_ to avoid crashing nodes during job completion. This is a Cray bug that Charliecloud happens to tickle. Non-Cray build boxes and others at the same site can still use the master branch. .. contents:: :depth: 2 :local: .. note:: These are general installation instructions. If you'd like specific, step-by-step directions for CentOS 7, section :doc:`virtualbox` has these for a VirtualBox virtual machine. Prequisites =========== Charliecloud is a simple system with limited prerequisites. If your system meets these prerequisites but Charliecloud doesn't work, please report that as a bug. Run time -------- Systems used for running images in the standard unprivileged mode need: * Recent Linux kernel with :code:`CONFIG_USER_NS=y`. We recommend version 4.4 or higher. * C compiler and standard library * POSIX shell and utilities Some distributions need configuration changes to enable user namespaces. For example, Debian Stretch needs sysctl :code:`kernel.unprivileged_userns_clone=1`, and RHEL and CentOS 7.4 need both a `kernel command line option and a sysctl `_ (that put you into "technology preview"). .. note:: An experimental setuid mode is also provided that does not need user namespaces. This should run on most currently supported Linux distributions. Build time ---------- Systems used for building images need the run-time prerequisites, plus: * Bash 4.1+ and optionally: * `Docker `_ 17.03+ * internet access or Docker configured for a local Docker hub * root access using :code:`sudo` Older versions of Docker may work but are untested. We know that 1.7.1 does not work. Test suite ---------- In order to run the test suite on a run or build system (you can test each mode independently), you also need: * Bash 4.1+ * Python 2.6+ * `Bats `_ 0.4.0 * wget .. With respect to curl vs. wget, both will work fine for our purposes (download a URL). According to Debian's popularity contest, 99.88% of reporting systems have wget installed, vs. about 44% for curl. On the other hand, curl is in the minimal install of CentOS 7 while wget is not. For now I just picked wget because I liked it better. Note that without Docker on the build system, some of the test suite will be skipped. Bats can be installed at the system level or embedded in the Charliecloud source code. If it's in both places, the latter is used. To embed Bats, either: * Download Charliecloud using :code:`git clone --recursive`, which will check out Bats as a submodule in :code:`test/bats`. * Unpack the Bats zip file or tarball in :code:`test/bats`. To check an embedded Bats:: $ test/bats/bin/bats --version Bats 0.4.0 Docker install tips =================== Tnstalling Docker is beyond the scope of this documentation, but here are a few tips. Understand the security implications of Docker ---------------------------------------------- Because Docker (a) makes installing random crap from the internet really easy and (b) is easy to deploy insecurely, you should take care. Some of the implications are below. This list should not be considered comprehensive nor a substitute for appropriate expertise; adhere to your moral and institutional responsibilities. :code:`docker` equals root ~~~~~~~~~~~~~~~~~~~~~~~~~~ Anyone who can run the :code:`docker` command or interact with the Docker daemon can `trivially escalate to root `_. This is considered a feature. For this reason, don't create the :code:`docker` group, as this will allow passwordless, unlogged escalation for anyone in the group. Images can contain bad stuff ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Standard hygiene for "installing stuff from the internet" applies. Only work with images you trust. The official Docker Hub repositories can help. Containers run as root ~~~~~~~~~~~~~~~~~~~~~~ By default, Docker runs container processes as root. In addition to being poor hygiene, this can be an escalation path, e.g. if you bind-mount host directories. Docker alters your network configuration ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ To see what it did:: $ ifconfig # note docker0 interface $ brctl show # note docker0 bridge $ route -n Docker installs services ~~~~~~~~~~~~~~~~~~~~~~~~ If you don't want the service starting automatically at boot, e.g.:: $ systemctl is-enabled docker enabled $ systemctl disable docker $ systemctl is-enabled docker disabled Configuring for a proxy ----------------------- By default, Docker does not work if you have a proxy, and it fails in two different ways. The first problem is that Docker itself must be told to use a proxy. This manifests as:: $ sudo docker run hello-world Unable to find image 'hello-world:latest' locally Pulling repository hello-world Get https://index.docker.io/v1/repositories/library/hello-world/images: dial tcp 54.152.161.54:443: connection refused If you have a systemd system, the `Docker documentation `_ explains how to configure this. If you don't have a systemd system, then :code:`/etc/default/docker` might be the place to go? The second problem is that Docker containers need to know about the proxy as well. This manifests as images failing to build because they can't download stuff from the internet. The fix is to set the proxy variables in your environment, e.g.:: export HTTP_PROXY=http://proxy.example.com:8088 export http_proxy=$HTTP_PROXY export HTTPS_PROXY=$HTTP_PROXY export https_proxy=$HTTP_PROXY export ALL_PROXY=$HTTP_PROXY export all_proxy=$HTTP_PROXY export NO_PROXY='localhost,127.0.0.1,.example.com' export no_proxy=$NO_PROXY You also need to teach :code:`sudo` to retain them. Add the following to :code:`/etc/sudoers`:: Defaults env_keep+="HTTP_PROXY http_proxy HTTPS_PROXY https_proxy ALL_PROXY all_proxy NO_PROXY no_proxy" Because different programs use different subsets of these variables, and to avoid a situation where some things work and others don't, the Charliecloud test suite (see below) includes a test that fails if some but not all of the above variables are set. Install Charliecloud ==================== Download -------- See our GitHub project: https://github.com/hpc/charliecloud The recommended download method is :code:`git clone --recursive`. Build ----- To build in the standard, unprivileged mode (recommended):: $ make To build in setuid mode (for testing if your kernel doesn't support the user namespace):: $ make SETUID=yes To build the documentation, see :code:`doc-src/README`. .. warning:: Do not build as root. This is unsupported and may introduce security problems. Install (optional) ------------------ You can run Charliecloud from the source directory, and it's recommended you at least run the test suite before installation to establish that your system will work. To install (FHS-compliant):: $ make install PREFIX=/foo/bar Note that :code:`PREFIX` is required; it does not default to :code:`/usr/local` like many packages. .. _install_test-charliecloud: Test Charliecloud ================= Charliecloud comes with a fairly comprehensive Bats test suite, in :code:`test`. Go there:: $ cd test To check location and version of Bats used by the tests:: $ make where-bats which bats /usr/bin/bats bats --version Bats 0.4.0 Just like for normal use, the Charliecloud test suite is split into build and run phases, and there is an additional phase that runs the examples' test suites. These phases can be tested independently on different systems. Testing is coordinated by :code:`make`. The test targets run one or more test suites. If any test suite has a failure, testing stops with an error message. The tests need three work directories with several gigabytes of free space, in order to store image tarballs, unpacked image directories, and permission test fixtures. These are configured with environment variables:: $ export CH_TEST_TARDIR=/var/tmp/tarballs $ export CH_TEST_IMGDIR=/var/tmp/images $ export CH_TEST_PERMDIRS='/var/tmp /tmp' :code:`CH_TEST_PERMDIRS` can be set to :code:`skip` in order to skip the file permissions tests. (Strictly speaking, the build phase needs only the first, and the example test phase does not need the last one. However, for simplicity, the tests will demand all three for all phases.) .. note:: Bats will wait until all descendant processes finish before exiting, so if you get into a failure mode where a test suite doesn't clean up all its processes, Bats will hang. Build ----- In this phase, image building and associated functionality is tested. :: $ make test-build bats build.bats build_auto.bats build_post.bats ✓ create tarball directory if needed ✓ documentations build ✓ executables seem sane [...] ✓ ch-build obspy ✓ ch-docker2tar obspy ✓ docker pull dockerpull ✓ ch-docker2tar dockerpull ✓ nothing unexpected in tarball directory 41 tests, 0 failures Note that with an empty Docker cache, this test can be quite lengthy, half an hour or more, because it builds all the examples as well as several basic Dockerfiles for common Linux distributions and tools (in :code:`test`). With a full cache, expect more like 1–2 minutes. .. note:: The easiest way to update the Docker images used in this test is to simply delete all Docker containers and images, and let them be rebuilt:: $ sudo docker rm $(sudo docker ps -aq) $ sudo docker rmi -f $(sudo docker images -q) Run --- The run tests require the contents of :code:`$CH_TEST_TARDIR` produced by a successful, complete build test. Copy this directory to the run system. Additionally, the user running the tests needs to be a member of at least 2 groups. File permission enforcement is tested against specially constructed fixture directories. These should include every meaningful mounted filesystem, and they cannot be shared between different users. To create them:: $ for d in $CH_TEST_PERMDIRS; do sudo ./make-perms-test $d $USER nobody; done To skip this test (e.g., if you don't have root), set :code:`$CH_TEST_PERMDIRS` to :code:`skip`. To run the tests:: $ make test-run Examples -------- Some of the examples include test suites of their own. This Charliecloud runs those test suites, using a Slurm allocation if one is available or a single node (localhost) if not. These require that the run tests have been completed successfully. Note that this test can take quite a while, and that single tests from the Charliecloud perspective include entire test suites from the example's perspective, so be patient. To run the tests:: $ make test-test Quick and multiple-phase tests ------------------------------ We also provide the following additional test targets: * :code:`test-quick`: key subset of build and run phases (nice for development) * :code:`test`: build and run phases * :code:`test-all`: all three phases We recommend that a build box pass all phases so it can be used to run containers for testing and development. charliecloud-0.2.3~pre+1a5609e/doc-src/script-help.rst000066400000000000000000000013331320367540400223670ustar00rootroot00000000000000Help text for executables ************************* This section contains the :code:`--help` output for each of the Charliecloud exectuables. .. contents:: :depth: 2 :local: ch-build ======== .. literalinclude:: ch-build.help :language: text ch-build2dir ============ .. literalinclude:: ch-build2dir.help :language: text ch-docker2tar ============= .. literalinclude:: ch-docker2tar.help :language: text ch-docker-run ============= .. literalinclude:: ch-docker-run.help :language: text ch-run ====== .. literalinclude:: ch-run.help :language: text ch-ssh ====== .. literalinclude:: ch-ssh.help :language: text ch-tar2dir ========== .. literalinclude:: ch-tar2dir.help :language: text charliecloud-0.2.3~pre+1a5609e/doc-src/tutorial.rst000066400000000000000000001171141320367540400220050ustar00rootroot00000000000000Tutorial ******** This tutorial will teach you how to create and run Charliecloud images, using both examples included with the source code as well as new ones you create from scratch. This tutorial assumes that: (a) Charliecloud is correctly installed as described in the previous section, (b) Docker is installed on the build system, (c) the executables are in your :code:`$PATH`, and (d) you have access to the examples in the source code. .. contents:: :depth: 2 :local: .. note:: Shell sessions throughout this documentation will use the prompt :code:`$` to indicate commands executed natively on the host and :code:`>` for commands executed in a container. 90 seconds to Charliecloud ========================== This section is for the impatient. It shows you how to quickly build and run a "hello world" Charliecloud container. If you like what you see, then proceed with the rest of the tutorial to understand what is happening and how to use Charliecloud for your own applications. :: $ ch-build -t hello ~/charliecloud Sending build context to Docker daemon 15.67 MB [...] Successfully built 1136de7d4c0a $ ch-docker2tar hello /var/tmp 57M /var/tmp/hello.tar.gz $ ch-tar2dir /var/tmp/hello.tar.gz /var/tmp creating new image /var/tmp/hello /var/tmp/hello unpacked ok $ ch-run /var/tmp/hello -- echo "I'm in a container" I'm in a container Getting help ============ All the executables have decent help and can tell you what version of Charliecloud you have (if not, please report a bug). For example:: $ ch-run --help Usage: ch-run [OPTION...] NEWROOT CMD [ARG...] Run a command in a Charliecloud container. [...] $ ch-run --version 0.2.0+4836ac1 The help text is also collected later in this documentation; see :doc:`script-help`. Your first user-defined software stack ====================================== In this section, we will create and run a simple "hello, world" image. This uses the :code:`hello` example in the Charliecloud source code. Start with:: $ cd examples/serial/hello Defining your UDSS ------------------ You must first write a Dockerfile that describes the image you would like; consult the `Dockerfile documentation `_ for details on how to do this. Note that run-time functionality such as :code:`ENTRYPOINT` is not supported. We will use the following very simple Dockerfile: .. literalinclude:: ../examples/serial/hello/Dockerfile :language: docker This creates a minimal Debian Jessie image with :code:`ssh` installed. We will encounter more complex Dockerfiles later in this tutorial. .. note:: Docker does not update the base image unless asked to. Specific images can be updated manually; in this case:: $ sudo docker pull debian:jessie There are various resources and scripts online to help automate this process. Build Docker image ------------------ Charliecloud provides a convenience wrapper around :code:`docker build` that works around some of its more irritating characteristics. In particular, it passes through any HTTP proxy variables, and by default it uses the Dockerfile in the current directory, rather than at the root of the Docker context directory. (We will address the context directory later.) The two arguments here are a tag for the Docker image and the context directory, which in this case is the Charliecloud source code. :: $ ch-build -t hello ~/charliecloud Sending build context to Docker daemon 15.67 MB Step 1/4 : FROM debian:jessie ---> 86baf4e8cde9 [...] Step 4/4 : RUN touch /usr/bin/ch-ssh ---> 1136de7d4c0a Successfully built 1136de7d4c0a Note that Docker prints each step of the Dockerfile as it's executed. :code:`ch-build` and many other Charliecloud commands wrap various privileged :code:`docker` commands. Thus, you will be prompted for a password to escalate as needed. Note however that most configurations of :code:`sudo` don't require a password on every invocation, so just because you aren't prompted doesn't mean privileged commands aren't running. Share image and other standard Docker stuff ------------------------------------------- If needed, the Docker image can be manipulated with standard Docker commands. In particular, image sharing using a public or private Docker Hub repository can be very useful. :: $ sudo docker images REPOSITORY TAG IMAGE ID CREATED SIZE debian jessie 1742affe03b5 10 days ago 125.1 MB hello latest 1742affe03b5 10 days ago 139.7 MB $ sudo docker push # FIXME Running the image with Docker is not generally useful, because Docker's run-time environment is significantly different than Charliecloud's, but it can have value when debugging Charliecloud. :: $ sudo docker run -it hello /bin/bash # ls / bin dev home lib64 mnt proc run srv tmp var boot etc lib media opt root sbin sys usr # exit exit Flatten image ------------- Next, we flatten the Docker image into a tarball, which is then a plain file amenable to standard file manipulation commands. This tarball is placed in an arbitrary directory, here :code:`/var/tmp`. :: $ ch-docker2tar hello /var/tmp 57M /var/tmp/hello.tar.gz Distribute tarball ------------------ Thus far, the workflow has taken place on the build system. The next step is to copy the tarball to the run system. This can use any appropriate method for moving files: :code:`scp`, :code:`rsync`, something integrated with the scheduler, etc. If the build and run systems are the same, then no copy is needed. This is a typical use case for development and testing. Unpack tarball -------------- Charliecloud runs out of a normal directory rather than a filesystem image. In order to create this directory, we unpack the image tarball. This will replace the image directory if it already exists. :: $ ch-tar2dir /var/tmp/hello.tar.gz /var/tmp creating new image /var/tmp/hello /var/tmp/hello unpacked ok Generally, you should avoid unpacking into shared filesystems such as NFS and Lustre, in favor of local storage such as :code:`tmpfs` and local hard disks. This will yield better performance for you and anyone else on the shared filesystem. .. One potential gotcha is the tarball including special files such as devices. Because :code:`tar` is running unprivileged, these will not be unpacked, and they can cause the extraction to fail. The fix is to delete them in the Dockerfile. .. note:: You can run perfectly well out of :code:`/tmp`, but because it is bind-mounted automatically, the image root will then appear in multiple locations in the container's filesystem tree. This can cause confusion for both users and programs. Activate image -------------- We are now ready to run programs inside a Charliecloud container. This is done with the :code:`ch-run` command:: $ ch-run /var/tmp/hello -- echo hello hello Symbolic links in :code:`/proc` tell us the current namespaces, which are identified by long ID numbers:: $ ls -l /proc/self/ns total 0 lrwxrwxrwx 1 reidpr reidpr 0 Sep 28 11:24 ipc -> ipc:[4026531839] lrwxrwxrwx 1 reidpr reidpr 0 Sep 28 11:24 mnt -> mnt:[4026531840] lrwxrwxrwx 1 reidpr reidpr 0 Sep 28 11:24 net -> net:[4026531969] lrwxrwxrwx 1 reidpr reidpr 0 Sep 28 11:24 pid -> pid:[4026531836] lrwxrwxrwx 1 reidpr reidpr 0 Sep 28 11:24 user -> user:[4026531837] lrwxrwxrwx 1 reidpr reidpr 0 Sep 28 11:24 uts -> uts:[4026531838] $ ch-run /var/tmp/hello -- ls -l /proc/self/ns total 0 lrwxrwxrwx 1 reidpr reidpr 0 Sep 28 17:34 ipc -> ipc:[4026531839] lrwxrwxrwx 1 reidpr reidpr 0 Sep 28 17:34 mnt -> mnt:[4026532257] lrwxrwxrwx 1 reidpr reidpr 0 Sep 28 17:34 net -> net:[4026531969] lrwxrwxrwx 1 reidpr reidpr 0 Sep 28 17:34 pid -> pid:[4026531836] lrwxrwxrwx 1 reidpr reidpr 0 Sep 28 17:34 user -> user:[4026532256] lrwxrwxrwx 1 reidpr reidpr 0 Sep 28 17:34 uts -> uts:[4026531838] Notice that the container has different mount (:code:`mnt`) and user (:code:`user`) namespaces, but the rest of the namespaces are shared with the host. This highlights Charliecloud's focus on functionality (make your UDSS run), rather than isolation (protect the host from your UDSS). Each invocation of :code:`ch-run` creates a new container, so if you have multiple simultaneous invocations, they will not share containers. However, container overhead is minimal, and containers communicate without hassle, so this is generally of peripheral interest. .. note:: The :code:`--` in the :code:`ch-run` command line is a standard argument that separates options from non-option arguments. Without it, :code:`ch-run` would try (and fail) to interpret :code:`ls`’s :code:`-l` argument. These IDs are available both in the symlink target as well as its inode number:: $ stat -L --format='%i' /proc/self/ns/user 4026531837 $ ch-run /var/tmp/hello -- stat -L --format='%i' /proc/self/ns/user 4026532256 You can also run interactive commands, such as a shell:: $ ch-run /var/tmp/hello -- /bin/bash > stat -L --format='%i' /proc/self/ns/user 4026532256 > exit Be aware that wildcards in the :code:`ch-run` command are interpreted by the host, not the container, unless protected. One workaround is to use a sub-shell. For example:: $ ls /usr/bin/oldfind ls: cannot access '/usr/bin/oldfind': No such file or directory $ ch-run /var/tmp/hello -- ls /usr/bin/oldfind /usr/bin/oldfind $ ls /usr/bin/oldf* ls: cannot access '/usr/bin/oldf*': No such file or directory $ ch-run /var/tmp/hello -- ls /usr/bin/oldf* ls: cannot access /usr/bin/oldf*: No such file or directory $ ch-run /var/tmp/hello -- sh -c 'ls /usr/bin/oldf*' /usr/bin/oldfind You have now successfully run commands within a single-node Charliecloud container. Next, we explore how Charliecloud accesses host resources. Interacting with the host ========================= Charliecloud is not an isolation layer, so containers have full access to host resources, with a few quirks. This section demonstrates how this works. Filesystems ----------- Charliecloud makes host directories available inside the container using bind mounts, which is somewhat like a hard link in that it causes a file or directory to appear in multiple places in the filesystem tree, but it is a property of the running kernel rather than the filesystem. Several host directories are always bind-mounted into the container. These include system directories such as :code:`/dev`, :code:`/proc`, and :code:`/sys`; :code:`/tmp`; Charliecloud's :code:`ch-ssh` command in :code:`/usr/bin`; and the invoking user's home directory (for dotfiles), unless :code:`--no-home` is specified. Charliecloud uses recursive bind mounts, so for example if the host has a variety of sub-filesystems under :code:`/sys`, as Ubuntu does, these will be available in the container as well. In addition to the default bind mounts, arbitrary user-specified directories can be added using the :code:`--bind` or :code:`-b` switch. By default, :code:`/mnt/0`, :code:`/mnt/1`, etc., are used for the destination in the guest:: $ mkdir /var/tmp/foo0 $ echo hello > /var/tmp/foo0/bar $ mkdir /var/tmp/foo1 $ echo world > /var/tmp/foo1/bar $ ch-run -b /var/tmp/foo0 -b /var/tmp/foo1 /var/tmp/hello -- bash > ls /mnt 0 1 2 3 4 5 6 7 8 9 > cat /mnt/0/bar hello > cat /mnt/1/bar world Explicit destinations are also possible:: $ ch-run -b /var/tmp/foo0:/mnt /var/tmp/hello -- bash > ls /mnt bar > cat /mnt/bar hello Network ------- Charliecloud containers share the host's network namespace, so most network things should be the same. However, SSH is not aware of Charliecloud containers. If you SSH to a node where Charliecloud is installed, you will get a shell on the host, not in a container, even if :code:`ssh` was initiated from a container:: $ stat -L --format='%i' /proc/self/ns/user 4026531837 $ ssh localhost stat -L --format='%i' /proc/self/ns/user 4026531837 $ ch-run /var/tmp/hello -- /bin/bash > stat -L --format='%i' /proc/self/ns/user 4026532256 > ssh localhost stat -L --format='%i' /proc/self/ns/user 4026531837 There are several ways to SSH to a remote note and run commands inside a container. The simplest is to manually invoke :code:`ch-run` in the :code:`ssh` command:: $ ssh localhost ch-run /var/tmp/hello -- stat -L --format='%i' /proc/self/ns/user 4026532256 .. note:: Recall that each :code:`ch-run` invocation creates a new container. That is, the :code:`ssh` command above has not entered an existing user namespace :code:`’2256`; rather, it has re-used the namespace ID :code:`’2256`. Another is to use the :code:`ch-ssh` wrapper program, which adds :code:`ch-run` to the :code:`ssh` command implicitly. It takes the :code:`ch-run` arguments from the environment variable :code:`CH_RUN_ARGS`, making it mostly a drop-in replacement for :code:`ssh`. For example:: $ export CH_RUN_ARGS="/var/tmp/hello --" $ ch-ssh localhost stat -L --format='%i' /proc/self/ns/user 4026532256 $ ch-ssh -t localhost /bin/bash > stat -L --format='%i' /proc/self/ns/user 4026532256 :code:`ch-ssh` is available inside containers as well (in :code:`/usr/bin` via bind-mount):: $ export CH_RUN_ARGS="/var/tmp/hello --" $ ch-run /var/tmp/hello -- /bin/bash > stat -L --format='%i' /proc/self/ns/user 4026532256 > ch-ssh localhost stat -L --format='%i' /proc/self/ns/user 4026532258 This also demonstrates that :code:`ch-run` does not alter your environment variables. .. warning:: 1. :code:`CH_RUN_ARGS` is interpreted very simply; the sole delimiter is spaces. It is not shell syntax. In particular, quotes and backslashes are not interpreted. 2. Argument :code:`-t` is required for SSH to allocate a pseudo-TTY and thus convince your shell to be interactive. In the case of Bash, otherwise you'll get a shell that accepts commands but doesn't print prompts, among other other issues. (`Issue #2 `_.) A third may be to edit one's shell initialization scripts to check the command line and :code:`exec(1)` :code:`ch-run` if appropriate. This is brittle but avoids wrapping :code:`ssh` or altering its command line. User and group IDs ------------------ Unlike Docker and some other container systems, Charliecloud tries to make the container's users and groups look the same as the host's. (This is accomplished by bind-mounting :code:`/etc/passwd` and :code:`/etc/group` into the container.) For example:: $ id -u 901 $ whoami reidpr $ ch-run /var/tmp/hello -- bash > id -u 901 > whoami reidpr More specifically, the user namespace, when created without privileges as Charliecloud does, lets you map any container UID to your host UID. :code:`ch-run` implements this with the :code:`--uid` switch. So, for example, you can tell Charliecloud you want to be root, and it will tell you that you're root:: $ ch-run --uid 0 /var/tmp/hello -- bash > id -u 0 > whoami root But, this doesn't get you anything useful, because the container UID is mapped back to your UID on the host before permission checks are applied:: > dd if=/dev/mem of=/tmp/pwned dd: failed to open '/dev/mem': Permission denied This mapping also affects how users are displayed. For example, if a file is owned by you, your host UID will be mapped to your container UID, which is then looked up in :code:`/etc/passwd` to determine the display name. In typical usage without :code:`--uid`, this mapping is a no-op, so everything looks normal:: $ ls -nd ~ drwxr-xr-x 87 901 901 4096 Sep 28 12:12 /home/reidpr $ ls -ld ~ drwxr-xr-x 87 reidpr reidpr 4096 Sep 28 12:12 /home/reidpr $ ch-run /var/tmp/hello -- bash > ls -nd ~ drwxr-xr-x 87 901 901 4096 Sep 28 18:12 /home/reidpr > ls -ld ~ drwxr-xr-x 87 reidpr reidpr 4096 Sep 28 18:12 /home/reidpr But if :code:`--uid` is provided, things can seem odd. For example:: $ ch-run --uid 0 /var/tmp/hello -- bash > ls -nd /home/reidpr drwxr-xr-x 87 0 901 4096 Sep 28 18:12 /home/reidpr > ls -ld /home/reidpr drwxr-xr-x 87 root reidpr 4096 Sep 28 18:12 /home/reidpr This UID mapping can contain only one pair: an arbitrary container UID to your effective UID on the host. Thus, all other users are unmapped, and they show up as :code:`nobody`:: $ ls -n /tmp/foo -rw-rw---- 1 902 902 0 Sep 28 15:40 /tmp/foo $ ls -l /tmp/foo -rw-rw---- 1 sig sig 0 Sep 28 15:40 /tmp/foo $ ch-run /var/tmp/hello -- bash > ls -n /tmp/foo -rw-rw---- 1 65534 65534 843 Sep 28 21:40 /tmp/foo > ls -l /tmp/foo -rw-rw---- 1 nobody nogroup 843 Sep 28 21:40 /tmp/foo User namespaces have a similar mapping for GIDs, with the same limitation --- exactly one arbitrary container GID maps to your effective *primary* GID. This can lead to some strange-looking results, because only one of your GIDs can be mapped in any given container. All the rest become :code:`nogroup`:: $ id uid=901(reidpr) gid=901(reidpr) groups=901(reidpr),903(nerds),904(losers) $ ch-run /var/tmp/hello -- id uid=901(reidpr) gid=901(reidpr) groups=901(reidpr),65534(nogroup) $ ch-run --gid 903 /var/tmp/hello -- id uid=901(reidpr) gid=903(nerds) groups=903(nerds),65534(nogroup) However, this doesn't affect access. The container process retains the same GIDs from the host perspective, and as always, the host IDs are what control access:: $ ls -l /tmp/primary /tmp/supplemental -rw-rw---- 1 sig reidpr 0 Sep 28 15:47 /tmp/primary -rw-rw---- 1 sig nerds 0 Sep 28 15:48 /tmp/supplemental $ ch-run /var/tmp/hello -- bash > cat /tmp/primary > /dev/null > cat /tmp/supplemental > /dev/null One area where functionality *is* reduced is that :code:`chgrp(1)` becomes useless. Using an unmapped group or :code:`nogroup` fails, and using a mapped group is a no-op because it's mapped back to the host GID:: $ ls -l /tmp/bar rw-rw---- 1 reidpr reidpr 0 Sep 28 16:12 /tmp/bar $ ch-run /var/tmp/hello -- chgrp nerds /tmp/bar chgrp: changing group of '/tmp/bar': Invalid argument $ ch-run /var/tmp/hello -- chgrp nogroup /tmp/bar chgrp: changing group of '/tmp/bar': Invalid argument $ ch-run --gid 903 /var/tmp/hello -- chgrp nerds /tmp/bar $ ls -l /tmp/bar -rw-rw---- 1 reidpr reidpr 0 Sep 28 16:12 /tmp/bar Workarounds include :code:`chgrp(1)` on the host or fastidious use of setgid directories:: $ mkdir /tmp/baz $ chgrp nerds /tmp/baz $ chmod 2770 /tmp/baz $ ls -ld /tmp/baz drwxrws--- 2 reidpr nerds 40 Sep 28 16:19 /tmp/baz $ ch-run /var/tmp/hello -- touch /tmp/baz/foo $ ls -l /tmp/baz/foo -rw-rw---- 1 reidpr nerds 0 Sep 28 16:21 /tmp/baz/foo This concludes our discussion of how a Charliecloud container interacts with its host and principal Charliecloud quirks. We next move on to installing software. Installing your own software ============================ This section covers four situations for making software available inside a Charliecloud container: 1. Third-party software installed into the image using a package manager. 2. Third-party software compiled from source into the image. 3. Your software installed into the image. 4. Your software stored on the host but compiled in the container. Many of Docker's `Best practices for writing Dockerfiles `_ apply to Charliecloud images as well, so you should be familiar with that document. .. note:: Maybe you don't have to install the software at all. Is there already a trustworthy image on Docker Hub you can use as a base? Third-party software via package manager ---------------------------------------- This approach is the simplest and fastest way to install stuff in your image. The :code:`examples/hello` Dockerfile also seen above does this to install the package :code:`openssh-client`: .. literalinclude:: ../examples/serial/hello/Dockerfile :language: docker :lines: 1-5 You can use distribution package managers such as :code:`apt-get`, as demonstrated above, or others, such as :code:`pip` for Python packages. Be aware that the software will be downloaded anew each time you build the image, unless you add an HTTP cache, which is out of scope of this tutorial. Third-party software compiled from source ----------------------------------------- Under this method, one uses :code:`RUN` commands to fetch the desired software using :code:`curl` or :code:`wget`, compile it, and install. Our example does this with two chained Dockerfiles. First, we build a basic Debian image (:code:`test/Dockerfile.debian8`): .. literalinclude:: ../test/Dockerfile.debian8 :language: docker Then, we add OpenMPI with :code:`test/Dockerfile.debian8openmpi`: .. literalinclude:: ../test/Dockerfile.debian8openmpi :language: docker So what is going on here? 1. Use the latest Debian, Jessie, as the base image. 2. Install a basic build system using the OS package manager. 3. Download and untar OpenMPI. Note the use of variables to make adjusting the URL and MPI version easier, as well as the explanation of why we're not using :code:`apt-get`, given that OpenMPI 1.10 is included in Debian. 4. Build and install OpenMPI. Note the :code:`getconf` trick to guess at an appropriate parallel build. 5. Clean up, in order to reduce the size of layers as well as the resulting Charliecloud tarball (:code:`rm -Rf`). .. Finally, because it's a container image, you can be less tidy than you might be on a normal system. For example, the above downloads and builds in :code:`/` rather than :code:`/usr/local/src`, and it installs MPI into :code:`/usr` rather than :code:`/usr/local`. Your software stored in the image --------------------------------- This method covers software provided by you that is included in the image. This is recommended when your software is relatively stable or is not easily available to users of your image, for example a library rather than simulation code under active development. The general approach is the same as installing third-party software from source, but you use the :code:`COPY` instruction to transfer files from the host filesystem (rather than the network via HTTP) to the image. For example, the :code:`mpihello` Dockerfile uses this approach: .. literalinclude:: ../examples/mpi/mpihello/Dockerfile :language: docker :lines: 19- These Dockerfile instructions: 1. Copy the host directory :code:`examples/mpi/mpihello` to the image at path :code:`/hello`. The host path is *relative to the context directory*; Docker builds have no access to the host filesystem outside the context directory. (This is so the Docker daemon can run on a different machine --- the context directory is tarred up and sent to the daemon, even if it's on the same machine.) The convention for the Charliecloud examples is that the build directory is always rooted at the top of the Charliecloud source code, but we could just as easily have provided the :code:`mpihello` directory. In that case, the source in :code:`COPY` would have been :code:`.`. 2. :code:`cd` to :code:`/hello`. 3. Compile our example. We include :code:`make clean` to remove any leftover build files, since they would be inappropriate inside the container. Once the image is built, we can see the results. (Install the image into :code:`/var/tmp` as outlined above, if you haven't already.) :: $ ch-run /var/tmp/mpihello -- ls -lh /hello total 32K -rw-rw---- 1 reidpr reidpr 908 Oct 4 15:52 Dockerfile -rw-rw---- 1 reidpr reidpr 157 Aug 5 22:37 Makefile -rw-rw---- 1 reidpr reidpr 1.2K Aug 5 22:37 README -rwxr-x--- 1 reidpr reidpr 9.5K Oct 4 15:58 hello -rw-rw---- 1 reidpr reidpr 1.4K Aug 5 22:37 hello.c -rwxrwx--- 1 reidpr reidpr 441 Aug 5 22:37 test.sh We will revisit this image later. Your software stored on the host -------------------------------- This method leaves your software on the host but compiles it in the image. This is recommended when your software is volatile or each image user needs a different version, for example a simulation code under active development. The general approach is to bind-mount the appropriate directory and then run the build inside the container. We can re-use the :code:`mpihello` image to demonstrate this. :: $ cd examples/mpi/mpihello $ ls -l total 20 -rw-rw---- 1 reidpr reidpr 908 Oct 4 09:52 Dockerfile -rw-rw---- 1 reidpr reidpr 1431 Aug 5 16:37 hello.c -rw-rw---- 1 reidpr reidpr 157 Aug 5 16:37 Makefile -rw-rw---- 1 reidpr reidpr 1172 Aug 5 16:37 README $ ch-run -b . /var/tmp/mpihello -- sh -c 'cd /mnt/0 && make' mpicc -std=gnu11 -Wall hello.c -o hello $ ls -l total 32 -rw-rw---- 1 reidpr reidpr 908 Oct 4 09:52 Dockerfile -rwxrwx--- 1 reidpr reidpr 9632 Oct 4 10:43 hello -rw-rw---- 1 reidpr reidpr 1431 Aug 5 16:37 hello.c -rw-rw---- 1 reidpr reidpr 157 Aug 5 16:37 Makefile -rw-rw---- 1 reidpr reidpr 1172 Aug 5 16:37 README A common use case is to leave a container shell open in one terminal for building, and then run using a separate container invoked from a different terminal. Your first single-node, multi-process jobs ========================================== This is an important use case even for large-scale codes, when testing and development happens at small scale but need to use an environment comparable to large-scale runs. This tutorial covers three approaches: 1. Processes are coordinated by the host, i.e., one process per container. 2. Processes are coordinated by the container, i.e., one container with multiple processes, using configuration files from the container. 3. Processes are coordinated by the container using configuration files from the host. In order to test approach 1, you must install OpenMPI 1.10.\ *x* on the host. In our experience, we have had success compiling from source with the same options as in the Dockerfile, but there is probably more nuance to the match than we've discovered. Processes coordinated by host ----------------------------- This approach does the forking and process coordination on the host. Each process is spawned in its own container, and because Charliecloud introduces minimal isolation, they can communicate as if they were running directly on the host. For example, using :code:`mpirun` and the :code:`mpihello` example above:: $ mpirun --version mpirun (Open MPI) 1.10.2 $ stat -L --format='%i' /proc/self/ns/user 4026531837 $ ch-run /var/tmp/mpihello -- mpirun --version mpirun (Open MPI) 1.10.4 $ mpirun -np 4 ch-run /var/tmp/mpihello -- /hello/hello 0: init ok cn001, 4 ranks, userns 4026532256 1: init ok cn001, 4 ranks, userns 4026532267 2: init ok cn001, 4 ranks, userns 4026532269 3: init ok cn001, 4 ranks, userns 4026532271 0: send/receive ok 0: finalize ok The advantage is that we can easily take advantage of host-specific things such as configurations; the disadvantage is that it introduces a close coupling between the host and container that can manifest in complex ways. For example, while OpenMPI 1.10.2 worked with 1.10.4 above, both had to be compiled with the same options. The OpenMPI 1.10.2 packages that come with Ubuntu fail with "orte_util_nidmap_init failed" if run with the container 1.10.4. Processes coordinated by container ---------------------------------- This approach starts a single container process, which then forks and coordinates the parallel work. The advantage is that this approach is completely independent of the host for dependency configuration and installation; the disadvantage is that it cannot take advantage of any host-specific things that might e.g. improve performance. For example:: $ ch-run /var/tmp/mpihello -- mpirun -np 4 /hello/hello 0: init ok cn001, 4 ranks, userns 4026532256 1: init ok cn001, 4 ranks, userns 4026532256 2: init ok cn001, 4 ranks, userns 4026532256 3: init ok cn001, 4 ranks, userns 4026532256 0: send/receive ok 0: finalize ok Processes coordinated by container using host configuration ----------------------------------------------------------- This approach is a middle ground. The use case is when there is some host-specific configuration we want to use, but we don't want to install the entire configured dependency on the host. It would be undesirable to copy this configuration into the image, because that would reduce its portability. The host configuration is communicated to the container by bind-mounting the relevant directory and then pointing the application to it. There are a variety of approaches. Some application or frameworks take command-line parameters specifying the configuration path. The approach used in our example is to set the configuration directory to :code:`/mnt/0`. This is done in :code:`mpihello` with the :code:`--sysconfdir` argument: .. literalinclude:: ../examples/mpi/mpihello/Dockerfile :language: docker :lines: 11-15 The effect is that the image contains a default MPI configuration, but if you specify a different configuration directory with :code:`--bind`, that is overmounted and used instead. For example:: $ ch-run -b /usr/local/etc /var/tmp/mpihello -- mpirun -np 4 /hello/hello 0: init ok cn001, 4 ranks, userns 4026532256 1: init ok cn001, 4 ranks, userns 4026532256 2: init ok cn001, 4 ranks, userns 4026532256 3: init ok cn001, 4 ranks, userns 4026532256 0: send/receive ok 0: finalize ok A similar approach creates a dangling symlink with :code:`RUN` that is resolved when the appropriate host directory is bind-mounted into :code:`/mnt`. Your first multi-node jobs ========================== This section assumes that you are using a Slurm cluster with a working OpenMPI 1.10.\ *x* installation and some type of node-local storage. A :code:`tmpfs` is recommended, and we use :code:`/var/tmp` for this tutorial. (Using :code:`/tmp` often works but can cause confusion because it's shared by the container and host, yielding cycles in the directory tree.) We cover three cases: 1. The MPI hello world example above, run interactively, with the host coordinating. 2. Same, non-interactive. 3. An Apache Spark example, run interactively. 4. Same, non-interactive. We think that container-coordinated MPI jobs will also work, but we haven't worked out how to do this yet. (See `issue #5 `_.) .. note:: The image directory is mounted read-only by default so it can be shared by multiple Charliecloud containers in the same or different jobs. It can be mounted read-write with :code:`ch-run -w`. .. warning:: The image can reside on most filesystems, but be aware of metadata impact. A non-trivial Charliecloud job may overwhelm a network filesystem, earning you the ire of your sysadmins and colleagues. (NFS sometimes does not work for read-only images; see `issue #9 `_.) Interactive MPI hello world --------------------------- First, obtain an interactive allocation of nodes. This tutorial assumes an allocation of 4 nodes (but any number should work) and an interactive shell on one of those nodes. For example:: $ salloc -N4 We also need OpenMPI 1.10.\ *x* available and with the correct mapping policy:: $ mpirun --version mpirun (Open MPI) 1.10.5 $ export OMPI_MCA_rmaps_base_mapping_policy= The next step is to distribute the image tarball to the compute nodes. To do so, we run one instance of :code:`ch-tar2dir` on each node:: $ mpirun -pernode ch-tar2dir mpihello.tar.gz /var/tmp App launch reported: 4 (out of 4) daemons - 3 (out of 4) procs creating new image /tmp/mpihello creating new image /tmp/mpihello creating new image /tmp/mpihello creating new image /tmp/mpihello /tmp/mpihello unpacked ok /tmp/mpihello unpacked ok /tmp/mpihello unpacked ok /tmp/mpihello unpacked ok We can now activate the image and run our program:: $ mpirun ch-run /var/tmp/mpihello -- /hello/hello App launch reported: 4 (out of 4) daemons - 48 (out of 64) procs 2: init ok cn001, 64 ranks, userns 4026532567 4: init ok cn001, 64 ranks, userns 4026532571 8: init ok cn001, 64 ranks, userns 4026532579 [...] 45: init ok cn003, 64 ranks, userns 4026532589 17: init ok cn002, 64 ranks, userns 4026532565 55: init ok cn004, 64 ranks, userns 4026532577 0: send/receive ok 0: finalize ok Success! Non-interactive MPI hello world ------------------------------- Production jobs are normally run non-interactively, via submission of a job script that runs when resources are available, placing output into a file. The MPI hello world example includes such a script, :code:`slurm.sh`: .. literalinclude:: ../examples/mpi/mpihello/slurm.sh :language: bash Note that this script both unpacks the image and runs it. Submit it with something like:: $ sbatch -N4 slurm.sh ~/mpihello.tar.gz /var/tmp 207745 When the job is complete, look at the output:: $ cat slurm-207745.out tarball: /home/reidpr/mpihello.tar.gz image: /var/tmp/mpihello host: mpirun (Open MPI) 1.10.5 App launch reported: 4 (out of 4) daemons - 3 (out of 4) procs creating new image /var/tmp/mpihello creating new image /var/tmp/mpihello [...] /var/tmp/mpihello unpacked ok /var/tmp/mpihello unpacked ok container: mpirun (Open MPI) 1.10.5 App launch reported: 4 (out of 4) daemons - 32 (out of 64) procs 2: init ok cn004, 64 ranks, userns 4026532604 3: init ok cn004, 64 ranks, userns 4026532606 4: init ok cn004, 64 ranks, userns 4026532608 [...] 63: init ok cn007, 64 ranks, userns 4026532630 30: init ok cn005, 64 ranks, userns 4026532628 27: init ok cn005, 64 ranks, userns 4026532622 0: send/receive ok 0: finalize ok Success! Interactive Apache Spark ------------------------ This example is in :code:`examples/spark`. Build a tarball and upload it to your cluster. Once you have an interactive job, unpack the tarball. :: $ srun ch-tar2dir spark.tar.gz /var/tmp creating new image /var/tmp/spark creating new image /var/tmp/spark [...] /var/tmp/spark unpacked ok /var/tmp/spark unpacked ok We need to first create a basic configuration for Spark, as the defaults in the Dockerfile are insufficient. (For real jobs, you'll want to also configure performance parameters such as memory use; see `the documentation `_.) First:: $ mkdir -p ~/sparkconf $ chmod 700 ~/sparkconf We'll want to use the cluster's high-speed network. For this example, we'll find the Spark master's IP manually:: $ ip -o -f inet addr show | cut -d/ -f1 1: lo inet 127.0.0.1 2: eth0 inet 192.168.8.3 8: eth1 inet 10.8.8.3 Your site support can tell you which to use. In this case, we'll use 10.8.8.3. Create some configuration files. Replace :code:`[MYSECRET]` with a string only you know. Edit to match your system; in particular, use local disks instead of :code:`/tmp` if you have them:: $ cat > ~/sparkconf/spark-env.sh SPARK_LOCAL_DIRS=/tmp/spark SPARK_LOG_DIR=/tmp/spark/log SPARK_WORKER_DIR=/tmp/spark SPARK_LOCAL_IP=127.0.0.1 SPARK_MASTER_HOST=10.8.8.3 $ cat > ~/sparkconf/spark-defaults.conf spark.authenticate true spark.authenticate.secret [MYSECRET] We can now start the Spark master:: $ ch-run -b ~/sparkconf /var/tmp/spark -- /spark/sbin/start-master.sh Look at the log in :code:`/tmp/spark/log` to see that the master started correctly:: $ tail -7 /tmp/spark/log/*master*.out 17/02/24 22:37:21 INFO Master: Starting Spark master at spark://10.8.8.3:7077 17/02/24 22:37:21 INFO Master: Running Spark version 2.0.2 17/02/24 22:37:22 INFO Utils: Successfully started service 'MasterUI' on port 8080. 17/02/24 22:37:22 INFO MasterWebUI: Bound MasterWebUI to 127.0.0.1, and started at http://127.0.0.1:8080 17/02/24 22:37:22 INFO Utils: Successfully started service on port 6066. 17/02/24 22:37:22 INFO StandaloneRestServer: Started REST server for submitting applications on port 6066 17/02/24 22:37:22 INFO Master: I have been elected leader! New state: ALIVE If you can run a web browser on the node, browse to :code:`http://localhost:8080` for the Spark master web interface. Because this capability varies, the tutorial does not depend on it, but it can be informative. Refresh after each key step below. The Spark workers need to know how to reach the master. This is via a URL; you can derive it from the above, or consult the web interface. For example:: $ MASTER_URL=spark://10.8.8.3:7077 Next, start one worker on each compute node. This is a little ugly; :code:`mpirun` will wait until everything is finished before returning, but we want to start the workers in the background, so we add :code:`&` and introduce a race condition. (:code:`srun` has different, even less helpful behavior: it kills the worker as soon as it goes into the background.) :: $ mpirun -map-by '' -pernode ch-run -b ~/sparkconf /var/tmp/spark -- \ /spark/sbin/start-slave.sh $MASTER_URL & One of the advantages of Spark is that it's resilient: if a worker becomes available, the computation simply proceeds without it. However, this can mask issues as well. For example, this example will run perfectly fine with just one worker on the same node as the master, which isn't what we want. Check the master log to see that the right number of workers registered:: $ fgrep worker /tmp/spark/log/*master*.out 17/02/24 22:52:24 INFO Master: Registering worker 127.0.0.1:39890 with 16 cores, 187.8 GB RAM 17/02/24 22:52:24 INFO Master: Registering worker 127.0.0.1:44735 with 16 cores, 187.8 GB RAM 17/02/24 22:52:24 INFO Master: Registering worker 127.0.0.1:22445 with 16 cores, 187.8 GB RAM 17/02/24 22:52:24 INFO Master: Registering worker 127.0.0.1:29473 with 16 cores, 187.8 GB RAM Despite the workers calling themselves 127.0.0.1, they really are running across the allocation. (The confusion happens because of our :code:`$SPARK_LOCAL_IP` setting above.) This can be verified by examining logs on each compute node. For example:: $ ssh 10.8.8.4 $ tail -3 /tmp/spark/log/*worker*.out 17/02/24 22:52:24 INFO Worker: Connecting to master 10.8.8.3:7077... 17/02/24 22:52:24 INFO TransportClientFactory: Successfully created connection to /10.8.8.3:7077 after 263 ms (216 ms spent in bootstraps) 17/02/24 22:52:24 INFO Worker: Successfully registered with master spark://10.8.8.3:7077 $ exit We can now start an interactive shell to do some Spark computing:: $ ch-run -b ~/sparkconf /var/tmp/spark -- /spark/bin/pyspark --master $MASTER_URL Let's use this shell to estimate 𝜋 (this is adapted from one of the Spark `examples `_): .. code-block:: pycon >>> import operator >>> import random >>> >>> def sample(p): ... (x, y) = (random.random(), random.random()) ... return 1 if x*x + y*y < 1 else 0 ... >>> SAMPLE_CT = int(2e8) >>> ct = sc.parallelize(xrange(0, SAMPLE_CT)) \ ... .map(sample) \ ... .reduce(operator.add) >>> 4.0*ct/SAMPLE_CT 3.14109824 (Type Control-D to exit.) We can also submit jobs to the Spark cluster. This one runs the same example as included with the Spark source code. (The voluminous logging output is omitted.) :: $ ch-run -b ~/sparkconf /var/tmp/spark -- \ /spark/bin/spark-submit --master $MASTER_URL \ /spark/examples/src/main/python/pi.py 1024 [...] Pi is roughly 3.141211 [...] Exit your allocation. Slurm will clean up the Spark daemons. Success! Next, we'll run a similar job non-interactively. Non-interactive Apache Spark ---------------------------- We'll re-use much of the above to run the same computation non-interactively. For brevity, the Slurm script at :code:`examples/other/spark/slurm.h` is not reproduced here. Submit it as follows. It requires three arguments: the tarball, the image directory to unpack into, and the high-speed network interface. Again, consult your site administrators for the latter. :: $ sbatch -N4 slurm.sh spark.tar.gz /var/tmp eth1 Submitted batch job 86754 Output:: $ fgrep 'Pi is' slurm-86754.out Pi is roughly 3.141393 Success! (to four significant digits) charliecloud-0.2.3~pre+1a5609e/doc-src/virtualbox.rst000066400000000000000000000447001320367540400223410ustar00rootroot00000000000000VirtualBox appliance ******************** This page explains how to create and use a single-node `VirtualBox `_ virtual machine appliance with Charliecloud and Docker pre-installed. This lets you: * use Charliecloud on Macs and Windows * quickly try out Charliecloud without following the install procedure The virtual machine uses CentOS 7 with an ElRepo LTS kernel. We use the :code:`kernel.org` mirror for CentOS, but any should work. Various settings are specified, but in most cases we have not done any particular tuning, so use your judgement, and feedback is welcome. We assume Bash shell. This procedure assumes you already have VirtualBox installed and working. .. contents:: :depth: 2 :local: Install and use the appliance ============================= This procedure imports a provided :code:`.ova` file into VirtualBox and walks you through logging in and running a brief Hello World in Charliecloud. You will act as user :code:`charlie`, who has passwordless :code:`sudo`. .. warning:: These instructions provide for an SSH server in the guest that is accessible to anyone logged into the host. It is your responsibility to ensure this is safe and compliant with your organization's policies, or modify the procedure accordingly. Configure VirtualBox -------------------- 1. Set *Preferences* -> *Proxy* if needed at your site. Install the appliance --------------------- 1. Download the :code:`charliecloud_centos7.ova` file (or whatever your site has called it). 2. *File* -> *Import appliance*. Choose :code:`charliecloud_centos7.ova` and click *Continue*. 3. Review the settings. * CPU should match the number of cores in your system. * RAM should be reasonable. Anywhere from 2GiB to half your system RAM will probably work. * Check *Reinitialize the MAC address of all network cards*. 4. Click *Import*. 5. Verify that the appliance's port forwarding is acceptable to you and your site: *Details* -> *Network* -> *Adapter 1* -> *Advanced* -> *Port Forwarding*. Log in and try Charliecloud --------------------------- 1. Start the VM by clicking the green arrow. 2. Wait for it to boot. 3. Click on the console window, where user :code:`charlie` is logged in. (If the VM "captures" your mouse pointer, type the key combination listed in the lower-right corner of the window to release it.) 4. Change your password: :: $ sudo passwd charlie 5. SSH into the VM using the password you just set. (Accessing the VM using SSH rather than the console is generally more pleasant, because you have a nice terminal with native copy-and-paste, etc.) :: $ ssh -p 2022 charlie@localhost 6. Run a container: :: $ ch-docker2tar hello /var/tmp 57M /var/tmp/hello.tar.gz $ ch-tar2dir /var/tmp/hello.tar.gz /var/tmp creating new image /var/tmp/hello /var/tmp/hello unpacked ok $ cat /etc/redhat-release CentOS Linux release 7.3.1611 (Core) $ ch-run /var/tmp/hello -- /bin/bash > cat /etc/debian_version 8.9 > exit Congratulations! You've successfully used Charliecloud. Now all of your wildest dreams will come true. Shut down the VM at your leisure. Possible next steps: * Follow the :doc:`tutorial `. * Run the :ref:`test suite ` in :code:`/usr/share/doc/charliecloud/test`. (Note that the environment variables are already configured for you in this appliance.) Build the appliance =================== Initialize VM ------------- Configure *Preferences* -> *Proxy* if needed. Create a new VM called *Charliecloud (CentOS 7)* in VirtualBox. We used the following specifications: * *Processors(s):* However many you have in the box you are using to build the appliance. This value will be adjusted by users when they install the appliance. * *Memory:* 4 GiB. Less might work too. This can be adjusted as needed. * *Disk:* 24 GiB, VDI dynamically allocated. We've run demos with 8 GiB, but that's not enough to run the Charliecloud test suite. The downside of being generous is more use of the host disk. The image file starts small and grows as needed, so unused space doesn't consume real resources. Note however that the image file does not shrink if you delete files in the guest (modulo heroics — image files can be compacted to remove zero pages, so you need to zero out the free space in the guest filesystem for this to work). Additional non-default settings: * *Network* * *Adapter 1* * *Advanced* * *Attached to:* NAT * *Adapter Type:* Paravirtualized Network * *Port Forwarding:* add the following rule (but see caveat above): * *Name:* ssh from localhost * *Protocol:* TCP * *Host IP:* 127.0.0.1 * *Host Port:* 2022 * *Guest IP:* 10.0.2.15 * *Guest Port:* 22 Install CentOS -------------- Download the `NetInstall ISO `_ from your favorite mirror. Attach it to the virtual optical drive of your VM by double-clicking on *[Optical drive] Empty*. Start the VM. Choose *Install CentOS Linux 7*. Under *Installation summary*, configure (in this order): * *Network & host name* * Enable *eth0*; verify it gets 10.0.2.15 and correct DNS. * *Date & time* * Enable *Network Time* * Select your time zone * *Installation source* * *On the network*: :code:`https://mirrors.kernel.org/centos/7/os/x86_64/` * *Proxy setup*: as appropriate for your network * *Software selection* * *Base environment:* Minimal Install * *Add-Ons*: Development Tools * *Installation destination* * No changes needed but the installer wants you to click in and look. Click *Begin installation*. Configure: * *Root password:* Something random (e.g. :code:`pwgen -cny 24`), which you can then forget because it will never be needed again. Users of the appliance will not have access to this password but will to its hash in :code:`/etc/shadow`. * *User creation:* * *User name:* charlie * *Make this user administrator:* yes * *Password:* Decent password that meets your organization's requirements. Appliance user access is same as the root password. Click *Finish configuration*, then *Reboot* and wait for the login prompt to come up in the console. Note that the install ISO will be automatically ejected. Configure guest OS ------------------ Log in ~~~~~~ SSH into the guest. (This will give you a fully functional native terminal with copy and paste, your preferred configuration, etc.) :: $ ssh -p 2022 charlie@localhost Update sudoers ~~~~~~~~~~~~~~ We want :code:`sudo` to (1) accept :code:`charlie` without a password and (2) have access to the proxy environment variables. :: $ sudo visudo Comment out: .. code-block:: none ## Allows people in group wheel to run all commands %wheel ALL=(ALL) ALL Uncomment: .. code-block:: none ## Same thing without a password # %wheel ALL=(ALL) NOPASSWD: ALL Add: .. code-block:: none Defaults env_keep+="DISPLAY auto_proxy HTTP_PROXY http_proxy HTTPS_PROXY https_proxy ALL_PROXY all_proxy NO_PROXY no_proxy" Configure proxy ~~~~~~~~~~~~~~~ If your site uses a web proxy, you'll need to configure the VM to use it. The setup described here also lets you turn on and off the proxy as needed with the :code:`proxy-on` and :code:`proxy-off` shell functions. Create a file :code:`/etc/profile.d/proxy.sh` containing, for example, the following. Note that the only editor you have so far is :code:`vi`, and you'll need to :code:`sudo`. .. code-block:: sh proxy-on () { export HTTP_PROXY=http://proxy.example.com:8080 export http_proxy=$HTTP_PROXY export HTTPS_PROXY=$HTTP_PROXY export https_proxy=$HTTP_PROXY export ALL_PROXY=$HTTP_PROXY export all_proxy=$HTTP_PROXY export NO_PROXY='localhost,127.0.0.1,.example.com' export no_proxy=$NO_PROXY } proxy-off () { unset -v HTTP_PROXY http_proxy unset -v HTTPS_PROXY https_proxy unset -v ALL_PROXY all_proxy unset -v NO_PROXY no_proxy } proxy-on Test:: $ exec bash $ set | fgrep -i proxy ALL_PROXY=http://proxy.example.com:8080 [...] $ sudo bash # set | fgrep -i proxy ALL_PROXY=http://proxy.example.com:8080 [...] # exit Install a decent user environment ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Use :code:`yum` to install a basic environment suitable for your site. For example:: $ sudo yum upgrade $ sudo yum install emacs vim wget .. note:: CentOS includes Git 1.8 by default, which is quite old. It's sufficient for installing Charliecloud, but if you expect users to do any real development with Git, you probably want to install a newer version, perhaps from source. Configure auto-login on console ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ This sets the first virtual console to log in :code:`charlie` automatically (i.e., without password). This increases user convenience and, combined with passwordless :code:`sudo` above, it lets users set their own password for :code:`charlie` without you needing to distribute the password set above. Even on multi-user systems, this is secure because the VM console window is displayed only in the invoking user's windowing environment. Adapted from this `forum post `_. :: $ cd /etc/systemd/system/getty.target.wants $ sudo cp /lib/systemd/system/getty\@.service getty\@tty1.service Edit :code:`getty@tty1.service` to modify the :code:`ExecStart` line and add a new line at the end, as follows: .. code-block:: ini [Service] ;... ExecStart=-/sbin/agetty --autologin charlie --noclear %I ;... [Install] ;... ;Alias=getty@tty1.service Reboot. The VM text console should be logged into :code:`charlie` with no user interaction. Upgrade kernel ~~~~~~~~~~~~~~ CentOS 7 comes with kernel version 3.10 (plus lots of Red Hat patches). In order to run Charliecloud well, we need something newer. This can be obtained from `ElRepo `_. First, set the new kernel flavor to be the default on boot. Edit :code:`/etc/sysconfig/kernel` and change :code:`DEFAULTKERNEL` from :code:`kernel` to :code:`kernel-lt`. Next, install the kernel:: $ sudo rpm --import https://www.elrepo.org/RPM-GPG-KEY-elrepo.org $ sudo rpm -Uvh https://www.elrepo.org/elrepo-release-7.0-2.el7.elrepo.noarch.rpm $ sudo yum upgrade $ sudo rpm --erase --nodeps kernel-headers $ sudo yum --enablerepo=elrepo-kernel install kernel-lt kernel-lt-headers kernel-lt-devel $ sudo yum check dependencies Reboot. Log back in and verify that you're in the right kernel:: $ uname -r 4.4.85-1.el7.elrepo.x86_64 Install Guest Additions ~~~~~~~~~~~~~~~~~~~~~~~ The VirtualBox `Guest Additions `_ add various tweaks to the guest to make it work better with the host. #. Raise the VM's console window. #. From the menu bar, choose *Devices* -> *Insert Guest Additions CD Image*. Install. It is OK if you get a complaint about skipping X. :: $ sudo mount /dev/cdrom /mnt $ sudo sh /mnt/VBoxLinuxAdditions.run $ sudo eject Reboot. Install OpenMPI ~~~~~~~~~~~~~~~ This will enable you to run MPI-based images using the host MPI, as you would on a cluster. Match the MPI version in :code:`examples/mpi/mpihello/Dockerfile`. (CentOS has an OpenMPI RPM, but it's the wrong version and lacks an :code:`mpirun` command.) :: $ cd /usr/local/src $ sudo chgrp wheel . $ sudo chmod 2775 . $ ls -ld . drwxrwsr-x. 2 root wheel 6 Nov 5 2016 . $ wget https://www.open-mpi.org/software/ompi/v1.10/downloads/openmpi-1.10.5.tar.gz $ tar xf openmpi-1.10.5.tar.gz $ rm openmpi-1.10.5.tar.gz $ cd openmpi-1.10.5/ $ ./configure --prefix=/usr --disable-mpi-cxx --disable-mpi-fortran $ make -j$(getconf _NPROCESSORS_ONLN) $ sudo make install $ make clean $ ldconfig Sanity:: $ which mpirun $ mpirun --version mpirun (Open MPI) 1.10.5 Install Docker -------------- See also Docker's `CentOS install documentation `_. Install ~~~~~~~ This will offer Docker's GPG key. Verify its fingerprint. :: $ sudo yum install yum-utils $ sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo $ sudo yum install docker-ce $ sudo systemctl enable docker $ sudo systemctl is-enabled docker enabled Configure proxy ~~~~~~~~~~~~~~~ If needed at your site, create a file :code:`/etc/systemd/system/docker.service.d/http-proxy.conf` with the following content: .. code-block:: ini [Service] Environment="HTTP_PROXY=http://proxy.example.com:8080" Environment="HTTPS_PROXY=http://proxy.example.com:8080" Restart Docker and verify:: $ sudo systemctl daemon-reload $ sudo systemctl restart docker $ systemctl show --property=Environment docker Environment=HTTP_PROXY=[...] HTTPS_PROXY=[...] Note that there's nothing special to turn off the proxy if you are off-site; you'll need to edit the file again. Test ~~~~ Test that Docker is installed and working by running the Hello World image:: $ sudo docker run hello-world [...] Hello from Docker! This message shows that your installation appears to be working correctly. Install Charliecloud -------------------- Set environment variables ~~~~~~~~~~~~~~~~~~~~~~~~~ Charliecloud's :code:`make test` needs some environment variables. Set these by default for convenience. Create a file :code:`/etc/profile.d/charliecloud.sh` with the following content: .. code-block:: sh export CH_TEST_TARDIR=/var/tmp/tarballs export CH_TEST_IMGDIR=/var/tmp/images export CH_TEST_PERMDIRS=skip Test:: $ exec bash $ set | fgrep CH_TEST CH_TEST_IMGDIR=/var/tmp/images CH_TEST_PERMDIRS=skip CH_TEST_TARDIR=/var/tmp/tarballs Enable a second :code:`getty` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Charliecloud requires a :code:`getty` process for its test suite. CentOS runs only a single :code:`getty` by default, so if you log in on the console, Charliecloud will not pass its tests. Thus, enable a second one:: $ sudo ln -s /usr/lib/systemd/system/getty@.service /etc/systemd/system/getty.target.wants/getty@tty2.service $ sudo systemctl start getty@tty2.service Test:: $ ps ax | egrep [g]etty 751 tty1 Ss+ 0:00 /sbin/agetty --noclear tty1 linux 2885 tty2 Ss+ 0:00 /sbin/agetty --noclear tty2 linux Build and install Charliecloud ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ This fetches the tip of :code:`master` in Charliecloud's GitHub repository. If you want a different version, use Git commands to check it out. :: $ cd /usr/local/src $ git clone --recursive https://www.github.com/hpc/charliecloud.git $ cd charliecloud $ make $ sudo make install PREFIX=/usr Basic sanity:: $ which ch-run /usr/bin/ch-run $ ch-run --version 0.2.2~pre+00ffb9b .. _virtualbox_prime-docker-cache: Prime Docker cache ~~~~~~~~~~~~~~~~~~ Running :code:`make test-build` will build all the necessary Docker layers. This will speed things up if the user later wishes to make use of them. Note that this step can take 20–30 minutes to do all the builds. :: $ cd /usr/share/doc/charliecloud/test $ make test-build ✓ create tarball directory if needed - documentations build (skipped: sphinx is not installed) ✓ executables seem sane ✓ proxy variables [...] 41 tests, 0 failures, 1 skipped But the tarballs will be overwritten by later runs, so remove them to reduce VM image size for export. We'll zero them out first so that the export sees the blocks as unused. (It does not understand filesystems, so it thinks deleted but non-zero blocks are still in use.) :: $ cd /var/tmp/tarballs $ for i in *.tar.gz; do echo $i; shred -n0 --zero $i; done $ rm *.tar.gz Create export snapshot ---------------------- Charliecloud's :code:`make test-run` and :code:`test-test` produce voluminous image files that need not be in the appliance, in contrast with the primed Docker cache as discussed above. However, we also don't want to export an appliance that hasn't been tested. The solution is to make a snapshot of what we do want to export, run the tests, and then return to the pre-test snapshot and export it. #. Shut down the VM. #. Create a snapshot called *exportme*. #. Boot the VM again and log in. Finish testing Charliecloud --------------------------- This runs the Charliecloud test suite in full. If it passes, then the snapshot you created in the previous step is good to go. :: $ cd /usr/share/doc/charliecloud/test $ make test-all Export appliance ---------------- This creates a :code:`.ova` file, which is a standard way to package a virtual machine image with metadata. Someone else can then import it into their own VirtualBox, as described above. In principle other virtual machine emulators should work as well, though we haven't tried. 1. Shut down the VM. 2. Revert to snapshot *exportme*. 3. *File* -> *Export appliance* 4. Select your VM. Click *Continue*. 5. Configure the export: * *File:* Directory and filename you want. (The install procedure above uses :code:`charliecloud_centos7.ova`.) * *Format:* OVF 2.0 * *Write Manifest file:* unchecked 6. Click *Continue*. 7. Check the decriptive information and click *Export*. 8. Distribute the resulting file (which should be about 5GiB). Upgrade the appliance ===================== Shut down the VM and roll back to *exportme*. OS packages via :code:`yum` --------------------------- :: $ sudo yum upgrade $ sudo yum --enablerepo=elrepo-kernel install kernel-lt kernel-lt-headers kernel-lt-devel You may also want to remove old, unneeded kernel packages:: $ rpm -qa 'kernel*' | sort kernel-3.10.0-514.26.2.el7.x86_64 kernel-3.10.0-514.el7.x86_64 kernel-3.10.0-693.2.2.el7.x86_64 kernel-devel-3.10.0-514.26.2.el7.x86_64 kernel-devel-3.10.0-514.el7.x86_64 kernel-devel-3.10.0-693.2.2.el7.x86_64 kernel-lt-4.4.85-1.el7.elrepo.x86_64 kernel-lt-devel-4.4.85-1.el7.elrepo.x86_64 kernel-lt-headers-4.4.85-1.el7.elrepo.x86_64 kernel-tools-3.10.0-693.2.2.el7.x86_64 kernel-tools-libs-3.10.0-693.2.2.el7.x86_64 $ sudo rpm --erase kernel-3.10.0-514.26.2.el7 [... etc ...] Charliecloud ------------ :: $ cd /usr/local/src/charliecloud $ git pull $ make clean $ make $ sudo make install PREFIX=/usr $ git log -n1 commit 4ebff0a0d7352b69e4cf8b9f529b6247c17dbe86 [...] $ which ch-run /usr/bin/ch-run $ ch-run --version 0.2.2~pre+4ebff0a Make sure the Git hashes match. Docker images ------------- Delete existing containers and images:: $ sudo docker rm $(sudo docker ps -aq) $ sudo docker rmi -f $(sudo docker images -q) Now, go to :ref:`virtualbox_prime-docker-cache` above and proceed. charliecloud-0.2.3~pre+1a5609e/examples/000077500000000000000000000000001320367540400176675ustar00rootroot00000000000000charliecloud-0.2.3~pre+1a5609e/examples/README000066400000000000000000000001101320367540400205370ustar00rootroot00000000000000This directory has been updated to support additional compatible tests. charliecloud-0.2.3~pre+1a5609e/examples/mpi/000077500000000000000000000000001320367540400204545ustar00rootroot00000000000000charliecloud-0.2.3~pre+1a5609e/examples/mpi/lammps_mpi/000077500000000000000000000000001320367540400226125ustar00rootroot00000000000000charliecloud-0.2.3~pre+1a5609e/examples/mpi/lammps_mpi/Dockerfile000066400000000000000000000011641320367540400246060ustar00rootroot00000000000000FROM debian8openmpi WORKDIR / # Packages for building. RUN apt-get install -qy --no-install-recommends \ git \ python-dev # Build LAMMPS ENV LAMMPS_VERSION 17Nov16 ENV LAMMPS_DIR lammps-$LAMMPS_VERSION ENV LAMMPS_TAR $LAMMPS_DIR.tar.gz RUN wget -nv http://lammps.sandia.gov/tars/$LAMMPS_TAR RUN tar xf $LAMMPS_TAR RUN cd $LAMMPS_DIR/src \ && python Make.py -j $(getconf _NPROCESSORS_ONLN) -p none \ std no-lib reax meam poems python reaxc orig -a lib-all mpi RUN mv $LAMMPS_DIR/src/lmp_mpi /usr/bin \ && ln -s /$LAMMPS_DIR /lammps RUN cd $LAMMPS_DIR/python \ && python2.7 install.py charliecloud-0.2.3~pre+1a5609e/examples/mpi/lammps_mpi/test.bats000066400000000000000000000054671320367540400244600ustar00rootroot00000000000000load ../../../test/common # LAMMPS does have a test suite, but we do not use it, because it seems too # fiddly to get it running properly. # # 1. Running the command listed in LAMMPS' Jenkins tests [2] fails with a # strange error: # # $ python run_tests.py tests/test_commands.py tests/test_examples.py # Loading tests from tests/test_commands.py... # Traceback (most recent call last): # File "run_tests.py", line 81, in # tests += load_tests(f) # File "run_tests.py", line 22, in load_tests # for testname in list(tc): # TypeError: 'Test' object is not iterable # # Looking in run_tests.py, this sure looks like a bug (it's expecting a # list of Tests, I think, but getting a single Test). But it works in # Jenkins. Who knows. # # 2. The files test/test_*.py say that the tests can be run with # "nosetests", which they can, after setting several environment # variables. But some of the tests fail for me. I didn't diagnose. # # Instead, we simply run some of the example problems in a loop and see if # they exit with return code zero. We don't check output. # # Note that a lot of the other examples crash. I haven't diagnosed or figured # out if we care. # # We are open to patches if anyone knows how to fix this situation reliably. # # [1]: https://github.com/lammps/lammps-testing # [2]: https://ci.lammps.org/job/lammps/job/master/job/testing/lastSuccessfulBuild/console setup () { prerequisites_ok lammps_mpi IMG=$IMGDIR/lammps_mpi } lammps_try () { # These examples cd because some (not all) of the LAMMPS tests expect to # find things based on $CWD. infiles=$(ch-run $IMG -- bash -c "cd /lammps/examples/$1 && ls in.*") for i in $infiles; do printf '\n\n%s\n' $i # serial (but still uses MPI somehow) ch-run $IMG -- sh -c "cd /lammps/examples/$1 && lmp_mpi -log none -in $i" # parallel if [[ $CHTEST_MULTINODE ]]; then mpirun ch-run $IMG -- sh -c "cd /lammps/examples/$1 && lmp_mpi -log none -in $i" fi done } @test "$EXAMPLE_TAG/using all cores" { [[ -z $CHTEST_MULTINODE ]] && skip run mpirun ch-run $IMG -- lmp_mpi -log none -in /lammps/examples/melt/in.melt echo "$output" [[ $status -eq 0 ]] ranks_found=$( echo "$output" \ | fgrep 'MPI tasks' \ | sed -r 's/^.+with ([0-9]+) MPI tasks.+$/\1/') echo "ranks found: $ranks_found" [[ $ranks_found -eq $CHTEST_CORES ]] } @test "$EXAMPLE_TAG/crack" { lammps_try crack; } @test "$EXAMPLE_TAG/dipole" { lammps_try dipole; } @test "$EXAMPLE_TAG/flow" { lammps_try flow; } @test "$EXAMPLE_TAG/friction" { lammps_try friction; } @test "$EXAMPLE_TAG/melt" { lammps_try melt; } @test "$EXAMPLE_TAG/python" { lammps_try python; } charliecloud-0.2.3~pre+1a5609e/examples/mpi/mpibench/000077500000000000000000000000001320367540400222415ustar00rootroot00000000000000charliecloud-0.2.3~pre+1a5609e/examples/mpi/mpibench/Dockerfile000066400000000000000000000007041320367540400242340ustar00rootroot00000000000000FROM debian8openmpi # Compile the Intel MPI benchmark. Note that this URL comes from a button # labeled "Accept", so you may want to go click it before building the image. WORKDIR /usr/src ENV IMB_VERSION 2017 RUN wget -nv https://software.intel.com/sites/default/files/managed/a3/53/IMB_${IMB_VERSION}.tgz RUN tar xf IMB_${IMB_VERSION}.tgz --strip 1 # The benchmark won't compile in parallel. RUN cd imb/src \ && make CC=mpicc -j1 -f make_ict charliecloud-0.2.3~pre+1a5609e/examples/mpi/mpibench/test.sh000077500000000000000000000012301320367540400235530ustar00rootroot00000000000000#!/bin/bash set -e cd $(dirname $0) CHBASE=$(dirname $0)/../.. CHBIN=$CHBASE/bin OUTDIR=/tmp OUTTAG=$(date -u +'%Y%m%dT%H%M%SZ') IMB=/usr/local/src/imb/src/IMB-MPI1 if [[ $1 == build ]]; then shift $CHBIN/ch-build -t $USER/mpibench $CHBASE $CHBIN/ch-docker2tar $USER/mpibench /tmp $CHBIN/ch-tar2dir /tmp/$USER.mpibench.tar.gz /tmp/mpibench fi if [[ -n $1 ]]; then echo "testing on host" time mpirun -n $1 $IMB \ > $OUTDIR/mpibench.host.$OUTTAG.txt echo "testing in container" time mpirun -n $1 $CHBIN/ch-run /tmp/mpibench -- $IMB \ > $OUTDIR/mpibench.guest.$OUTTAG.txt echo "done; output in $OUTDIR" fi charliecloud-0.2.3~pre+1a5609e/examples/mpi/mpihello/000077500000000000000000000000001320367540400222655ustar00rootroot00000000000000charliecloud-0.2.3~pre+1a5609e/examples/mpi/mpihello/Dockerfile000066400000000000000000000012751320367540400242640ustar00rootroot00000000000000FROM debian:jessie # OS packages needed to build OpenMPI. RUN apt-get update && apt-get install -y g++ gcc make wget \ && rm -rf /var/lib/apt/lists/* # Compile OpenMPI. ENV VERSION 1.10.5 RUN wget -nv https://www.open-mpi.org/software/ompi/v1.10/downloads/openmpi-${VERSION}.tar.gz RUN tar xf openmpi-${VERSION}.tar.gz RUN cd openmpi-${VERSION} \ && CFLAGS=-O3 CXXFLAGS=-O3 \ ./configure --prefix=/usr --sysconfdir=/mnt/0 \ --disable-pty-support --disable-mpi-cxx --disable-mpi-fortran \ && make -j$(getconf _NPROCESSORS_ONLN) install RUN rm -Rf openmpi-${VERSION}* # This example COPY /examples/mpi/mpihello /hello WORKDIR /hello RUN make clean && make charliecloud-0.2.3~pre+1a5609e/examples/mpi/mpihello/Makefile000066400000000000000000000002351320367540400237250ustar00rootroot00000000000000BINS := hello CFLAGS := -std=gnu11 -Wall .PHONY: all all: $(BINS) .PHONY: clean clean: rm -f $(BINS) $(BINS): Makefile %: %.c mpicc $(CFLAGS) $< -o $@ charliecloud-0.2.3~pre+1a5609e/examples/mpi/mpihello/README000066400000000000000000000022241320367540400231450ustar00rootroot00000000000000This example demonstrates use of Charliecloud containers to run a simple OpenMPI application. It uses the host's mpirun command to run MPI ranks inside separate containers. In addition to the OpenMPI inside the container, you need OpenMPI installed on the host. This is very sensitive to versions; for example, even with 1.6.5 inside the container, the distribution 1.6.5 on the host did not work for me ("orte_util_nidmap_init failed"). A stock OpenMPI compiled from source, with the same configure arguments as in the Dockerfile, worked. A script test.sh is provided to demonstrate the build and test procedure. It is designed to run on a single node where you have sudo. With the argument "build", test.sh builds a Docker container and unpacks it in /tmp. With an different argument, it runs the hello app with the specified number of ranks. You can do both as well. For example: $ ./test.sh build 2 Example output: $ ./test.sh 4 parent userns 4026531837 0: init ok, 4 ranks, userns 4026532257 1: init ok, 4 ranks, userns 4026532259 2: init ok, 4 ranks, userns 4026532261 3: init ok, 4 ranks, userns 4026532263 0: send/receive ok 0: finalize ok charliecloud-0.2.3~pre+1a5609e/examples/mpi/mpihello/hello.c000066400000000000000000000030201320367540400235270ustar00rootroot00000000000000/* MPI test program. Reports user namespace and rank, then sends and receives some simple messages. Patterned after: http://en.wikipedia.org/wiki/Message_Passing_Interface#Example_program */ #define _GNU_SOURCE #include #include #include #include #include #include #define TAG 0 int main(int argc, char ** argv) { int msg, rank, rank_ct; struct stat st; MPI_Status mstat; char hostname[HOST_NAME_MAX+1]; stat("/proc/self/ns/user", &st); MPI_Init(&argc, &argv); MPI_Comm_size(MPI_COMM_WORLD, &rank_ct); MPI_Comm_rank(MPI_COMM_WORLD, &rank); gethostname(hostname, HOST_NAME_MAX+1); printf("%d: init ok %s, %d ranks, userns %lu\n", rank, hostname, rank_ct, st.st_ino); fflush(stdout); if (rank == 0) { for (int i = 1; i < rank_ct; i++) { msg = i; MPI_Send(&msg, 1, MPI_INT, i, TAG, MPI_COMM_WORLD); //printf("%d: sent msg=%d\n", rank, msg); MPI_Recv(&msg, 1, MPI_INT, i, TAG, MPI_COMM_WORLD, &mstat); //printf("%d: received msg=%d\n", rank, msg); } } else { MPI_Recv(&msg, 1, MPI_INT, 0, TAG, MPI_COMM_WORLD, &mstat); //printf("%d: received msg=%d\n", rank, msg); msg = -rank; MPI_Send(&msg, 1, MPI_INT, 0, TAG, MPI_COMM_WORLD); //printf("%d: sent msg=%d\n", rank, msg); } if (rank == 0) printf("%d: send/receive ok\n", rank); MPI_Finalize(); if (rank == 0) printf("%d: finalize ok\n", rank); return 0; } charliecloud-0.2.3~pre+1a5609e/examples/mpi/mpihello/slurm.sh000077500000000000000000000015621320367540400237720ustar00rootroot00000000000000#!/bin/bash #SBATCH --time=0:10:00 # Arguments: Path to tarball, path to image parent directory. set -e TAR="$1" IMGDIR="$2" IMG="$2/$(basename "${TAR%.tar.gz}")" if [[ -z $TAR ]]; then echo 'no tarball specified' 1>&2 exit 1 fi printf 'tarball: %s\n' "$TAR" if [[ -z $IMGDIR ]]; then echo 'no image directory specified' 1>&2 exit 1 fi printf 'image: %s\n' "$IMG" # Make Charliecloud available (varies by site). module purge module load openmpi module load sandbox module load charliecloud # Makes "mpirun -pernode" work. export OMPI_MCA_rmaps_base_mapping_policy= # MPI version on host. printf 'host: ' mpirun --version | egrep '^mpirun' # Unpack image. mpirun -pernode ch-tar2dir $TAR $IMGDIR # MPI version in container. printf 'container: ' ch-run $IMG -- mpirun --version | egrep '^mpirun' # Run the app. mpirun ch-run $IMG -- /hello/hello charliecloud-0.2.3~pre+1a5609e/examples/other/000077500000000000000000000000001320367540400210105ustar00rootroot00000000000000charliecloud-0.2.3~pre+1a5609e/examples/other/spark/000077500000000000000000000000001320367540400221305ustar00rootroot00000000000000charliecloud-0.2.3~pre+1a5609e/examples/other/spark/Dockerfile000066400000000000000000000027021320367540400241230ustar00rootroot00000000000000FROM debian:jessie # Install needed OS packages. RUN apt-get update \ && apt-get install -y less openjdk-7-jre-headless python wget \ && rm -rf /var/lib/apt/lists/* # We want ch-ssh RUN touch /usr/bin/ch-ssh # Download and install Spark. # # We're staying on Spark 2.0 because 2.1.0 introduces Hive for metadata # handling somehow [1]. Data for this goes in $CWD by default, which is / and # not writeable in Charliecloud containers. So you get thousands of lines of # stack trace from pyspark. Workarounds exist, including cd to /tmp first or # configure hive-site.xml [2], but I'm not willing to put up with that crap # for demo purposes. Maybe it will be fixed in a 2.1 point release. # # [1]: http://spark.apache.org/docs/latest/sql-programming-guide.html#upgrading-from-spark-sql-20-to-21 # [2]: https://community.cloudera.com/t5/Advanced-Analytics-Apache-Spark/Spark-displays-SQLException-when-Hive-not-installed/td-p/37954 ENV URLPATH http://d3kbcqa49mib13.cloudfront.net ENV DIR spark-2.0.2-bin-hadoop2.7 ENV TAR $DIR.tgz RUN wget -nv $URLPATH/$TAR RUN tar xf $TAR && mv $DIR spark && rm $TAR # Very basic default configuration, to make it run and not do anything stupid. RUN printf '\ SPARK_LOCAL_IP=127.0.0.1\n\ SPARK_LOCAL_DIRS=/tmp\n\ SPARK_LOG_DIR=/tmp\n\ SPARK_WORKER_DIR=/tmp\n\ ' > /spark/conf/spark-env.sh # Move config to /mnt/0 so we can provide a different config if we want RUN mv /spark/conf /mnt/0 \ && ln -s /mnt/0 /spark/conf charliecloud-0.2.3~pre+1a5609e/examples/other/spark/slurm.sh000077500000000000000000000041361320367540400236350ustar00rootroot00000000000000#!/bin/bash #SBATCH --time=0:10:00 # Run an example non-interactive Spark computation. Requires three arguments: # # 1. Image tarball # 2. Directory in which to unpack tarball # 3. High-speed network interface name # # Example: # # $ sbatch slurm.sh /scratch/spark.tar.gz /var/tmp ib0 # # Spark configuration will be generated in ~/slurm-$SLURM_JOB_ID.spark; any # configuration already there will be clobbered. set -e if [[ -z $SLURM_JOB_ID ]]; then echo "not running under Slurm" exit 1 fi TAR="$1" IMGDIR="$2" IMG="$IMGDIR/spark" DEV="$3" CONF="$HOME/slurm-$SLURM_JOB_ID.spark" # Make Charliecloud available (varies by site) module purge module load openmpi module load sandbox module load charliecloud # What IP address to use for master? if [[ -z $DEV ]]; then echo "no high-speed network device specified" exit 1 fi MASTER_IP=$( ip -o -f inet addr show dev $DEV \ | sed -r 's/^.+inet ([0-9.]+).+/\1/') MASTER_URL=spark://$MASTER_IP:7077 if [[ -n $MASTER_IP ]]; then echo "Spark master IP: $MASTER_IP" else echo "no IP address for $DEV found" exit 1 fi # Unpack image srun ch-tar2dir $TAR $IMGDIR # Make Spark configuration mkdir $CONF chmod 700 $CONF cat < $CONF/spark-env.sh SPARK_LOCAL_DIRS=/tmp/spark SPARK_LOG_DIR=/tmp/spark/log SPARK_WORKER_DIR=/tmp/spark SPARK_LOCAL_IP=127.0.0.1 SPARK_MASTER_HOST=$MASTER_IP EOF MYSECRET=$(cat /dev/urandom | tr -dc 'a-z' | head -c 48) cat < $CONF/spark-defaults.sh spark.authenticate true spark.authenticate.secret $MYSECRET EOF chmod 600 $CONF/spark-defaults.sh # Start the Spark master ch-run -b $CONF $IMG -- /spark/sbin/start-master.sh sleep 10 tail -7 /tmp/spark/log/*master*.out fgrep -q 'New state: ALIVE' /tmp/spark/log/*master*.out # Start the Spark workers mpirun -map-by '' -pernode ch-run -b $CONF $IMG -- \ /spark/sbin/start-slave.sh $MASTER_URL & sleep 10 fgrep worker /tmp/spark/log/*master*.out tail -3 /tmp/spark/log/*worker*.out # Compute pi ch-run -b $CONF $IMG -- \ /spark/bin/spark-submit --master $MASTER_URL \ /spark/examples/src/main/python/pi.py 1024 # Let Slurm kill the workers and master charliecloud-0.2.3~pre+1a5609e/examples/other/spark/test.bats000066400000000000000000000107151320367540400237660ustar00rootroot00000000000000load ../../../test/common # Note: If you get output like the following (piping through cat turns of BATS # terminal magic): # # $ ./bats ../examples/spark/test.bats | cat # 1..5 # ok 1 spark/configure # ok 2 spark/start # [...]/test/bats.src/libexec/bats-exec-test: line 329: /tmp/bats.92406.src: No such file or directory # [...]/test/bats.src/libexec/bats-exec-test: line 329: /tmp/bats.92406.src: No such file or directory # [...]/test/bats.src/libexec/bats-exec-test: line 329: /tmp/bats.92406.src: No such file or directory # # that means that mpirun is starting too many processes per node (you want 1). # One solution is to export OMPI_MCA_rmaps_base_mapping_policy= (i.e., set but # empty). setup () { prerequisites_ok spark umask 0077 SPARK_IMG=$IMGDIR/spark SPARK_DIR=~/ch-spark-test.tmp # runs before each test, so no mktemp SPARK_CONFIG=$SPARK_DIR SPARK_LOG=/tmp/sparklog if [[ $CHTEST_MULTINODE ]]; then # Use the last non-loopback IP address. This is a barely educated # guess and shouldn't be relied on for real code, but hopefully it # works for testing. MASTER_IP=$( ip -o -f inet addr show \ | fgrep 'scope global' \ | tail -1 \ | sed -r 's/^.+inet ([0-9.]+).+/\1/') PERNODE='mpirun -pernode' PERNODE_PIDFILE=/tmp/spark-pernode.pid else MASTER_IP=127.0.0.1 PERNODE= PERNODE_PIDFILE= fi MASTER_URL="spark://$MASTER_IP:7077" MASTER_LOG=$SPARK_LOG/*master.Master*.out } @test "$EXAMPLE_TAG/configure" { # check for restrictive umask run umask -S echo "$output" [[ $status -eq 0 ]] [[ $output = 'u=rwx,g=,o=' ]] # create config mkdir -p $SPARK_CONFIG tee < $SPARK_CONFIG/spark-env.sh SPARK_LOCAL_DIRS=/tmp/spark SPARK_LOG_DIR=$SPARK_LOG SPARK_WORKER_DIR=/tmp/spark SPARK_LOCAL_IP=127.0.0.1 SPARK_MASTER_HOST=$MASTER_IP EOF MY_SECRET=$(cat /dev/urandom | tr -dc 'a-z' | head -c 48) tee < $SPARK_CONFIG/spark-defaults.conf spark.authenticate.true spark.authenticate.secret $MY_SECRET EOF } @test "$EXAMPLE_TAG/start" { # remove old master logs so new one has predictable name rm -Rf --one-file-system $SPARKLOG # start the master ch-run -b $SPARK_CONFIG $SPARK_IMG -- /spark/sbin/start-master.sh sleep 5 cat $MASTER_LOG fgrep -q 'New state: ALIVE' $MASTER_LOG # start the workers $PERNODE ch-run -b $SPARK_CONFIG $SPARK_IMG -- \ /spark/sbin/start-slave.sh $MASTER_URL & if [[ -n $PERNODE ]]; then echo $! > $PERNODE_PIDFILE fi sleep 7 } @test "$EXAMPLE_TAG/worker count" { # Note that in the log, each worker shows up as 127.0.0.1, which might # lead you to believe that all the workers started on the same (master) # node. However, I believe this string is self-reported by the workers and # is an artifact of SPARK_LOCAL_IP=127.0.0.1 above, which AFAICT just # tells the workers to put their web interfaces on localhost. They still # connect to the master and get work OK. [[ -z $CHTEST_MULTINODE ]] && SLURM_NNODES=1 worker_ct=$(fgrep -c 'Registering worker' $MASTER_LOG || true) echo "node count: $SLURM_NNODES; worker count: $worker_ct" [[ $worker_ct -eq $SLURM_NNODES ]] } @test "$EXAMPLE_TAG/pi" { run ch-run -b $SPARK_CONFIG $SPARK_IMG -- \ /spark/bin/spark-submit --master $MASTER_URL \ /spark/examples/src/main/python/pi.py 64 echo "$output" [[ $status -eq 0 ]] # This computation converges quite slowly, so we only ask for two correct # digits of pi. [[ $output =~ 'Pi is roughly 3.1' ]] } @test "$EXAMPLE_TAG/stop" { # If the workers were started with mpirun, we have to kill that prior # mpirun before the next one will do anything. Further, we have to abuse # it with SIGKILL because it doesn't quit on SIGTERM. Even further, this # kills all the processes started by mpirun too -- except on the node # where we ran mpirun. if [[ -n $CHTEST_MULTINODE ]]; then kill -9 $(cat $PERNODE_PIDFILE) fi ch-run -b $SPARK_CONFIG $SPARK_IMG -- /spark/sbin/stop-slave.sh ch-run -b $SPARK_CONFIG $SPARK_IMG -- /spark/sbin/stop-master.sh sleep 2 # Any Spark processes left? run $PERNODE ps aux echo "$output" [[ ! $output =~ 'org\.apache\.spark\.deploy' ]] } @test "$EXAMPLE_TAG/hang" { # If there are any test processes remaining, this test will hang. true } charliecloud-0.2.3~pre+1a5609e/examples/serial/000077500000000000000000000000001320367540400211465ustar00rootroot00000000000000charliecloud-0.2.3~pre+1a5609e/examples/serial/hello/000077500000000000000000000000001320367540400222515ustar00rootroot00000000000000charliecloud-0.2.3~pre+1a5609e/examples/serial/hello/Dockerfile000066400000000000000000000002671320367540400242500ustar00rootroot00000000000000FROM debian:jessie RUN apt-get update \ && apt-get install -y openssh-client \ && rm -rf /var/lib/apt/lists/* COPY examples/serial/hello hello RUN touch /usr/bin/ch-ssh charliecloud-0.2.3~pre+1a5609e/examples/serial/hello/README000066400000000000000000000004041320367540400231270ustar00rootroot00000000000000This example is a hello world Charliecloud container. It demonstrates running a command on the host from inside a container. A script test.sh is provided to demonstrate the build and run procedure. Detailed instructions are in the Charliecloud documentation. charliecloud-0.2.3~pre+1a5609e/examples/serial/hello/hello.sh000077500000000000000000000000461320367540400237130ustar00rootroot00000000000000#!/bin/sh set -e echo 'hello world' charliecloud-0.2.3~pre+1a5609e/examples/serial/hello/test.bats000066400000000000000000000012421320367540400241020ustar00rootroot00000000000000load ../../../test/common setup () { prerequisites_ok hello } @test "$EXAMPLE_TAG/hello" { run ch-run $EXAMPLE_IMG -- /hello/hello.sh echo "$output" [[ $status -eq 0 ]] [[ $output = 'hello world' ]] } @test "$EXAMPLE_TAG/distribution sanity" { # Try various simple things that should work in a basic Debian # distribution. (This does not test anything Charliecloud manipulates.) ch-run $EXAMPLE_IMG -- /bin/bash -c true ch-run $EXAMPLE_IMG -- /bin/true ch-run $EXAMPLE_IMG -- find /etc -name 'a*' ch-run $EXAMPLE_IMG -- ps aux ch-run $EXAMPLE_IMG -- sh -c 'echo foo | /bin/egrep foo' ch-run $EXAMPLE_IMG -- nice true } charliecloud-0.2.3~pre+1a5609e/examples/serial/obspy/000077500000000000000000000000001320367540400223025ustar00rootroot00000000000000charliecloud-0.2.3~pre+1a5609e/examples/serial/obspy/Dockerfile.skipped000066400000000000000000000017161320367540400257370ustar00rootroot00000000000000FROM debian:jessie RUN apt-get update \ && apt-get install -y \ bzip2 \ wget \ && rm -rf /var/lib/apt/lists/* # Install Miniconda into /usr. Some of the instructions [1] warn against # putting conda in $PATH; others don't. We are going to play fast and loose. # # [1]: http://conda.pydata.org/docs/help/silent.html WORKDIR /usr/src ENV MC_VERSION 4.2.12 ENV MC_FILE Miniconda3-$MC_VERSION-Linux-x86_64.sh RUN wget -nv https://repo.continuum.io/miniconda/$MC_FILE RUN bash $MC_FILE -bf -p /usr RUN rm -Rf $MC_FILE # Disable automatic conda upgrades for predictable versioning. RUN conda config --set auto_update_conda False # Install obspy. (Matplotlib 2.0 -- the default as of 2016-01-24 and what # obspy depends on -- with ObsPy 1.0.2 causes lots of test failures.) RUN conda config --add channels conda-forge RUN conda install --yes obspy=1.0.2 \ matplotlib=1.5.3 \ basemap-data-hires=1.0.8.dev0 charliecloud-0.2.3~pre+1a5609e/examples/serial/obspy/README000066400000000000000000000003451320367540400231640ustar00rootroot00000000000000This started failing Travis on roughly 10/31/2017 with: pkg_resources.DistributionNotFound: The 'pytz' distribution was not found and is required by matplotlib I'm not sure how to fix it, so disabling for now. See issue #64. charliecloud-0.2.3~pre+1a5609e/examples/serial/obspy/test.bats.skipped000066400000000000000000000011321320367540400255670ustar00rootroot00000000000000load ../../../test/common setup () { prerequisites_ok obspy IMG=$IMGDIR/obspy } @test "$EXAMPLE_TAG/runtests" { # Some tests try to use the network even when they're not supposed to. I # reported this as a bug on ObsPy [1]. In the meantime, exclude the # modules with those tests. This is pretty heavy handed, as only three # tests in these two modules have this problem, but I couldn't find a # finer-grained exclusion mechanism. # # [1]: https://github.com/obspy/obspy/issues/1660 ch-run $IMG -- bash -c '. activate && obspy-runtests -d -x core -x signal' } charliecloud-0.2.3~pre+1a5609e/examples/syscalls/000077500000000000000000000000001320367540400215245ustar00rootroot00000000000000charliecloud-0.2.3~pre+1a5609e/examples/syscalls/Makefile000066400000000000000000000002341320367540400231630ustar00rootroot00000000000000BINS := $(patsubst %.c,%,$(wildcard *.c)) .PHONY: all all: $(BINS) .PHONY: clean clean: rm -f $(BINS) $(BINS): Makefile %: %.c gcc $(CFLAGS) $< -o $@ charliecloud-0.2.3~pre+1a5609e/examples/syscalls/pivot_root.c000066400000000000000000000123641320367540400241020ustar00rootroot00000000000000/* This example program walks through the complete namespace / pivot_root(2) dance to enter a Charliecloud container, with each step documented. If you can compile it and run it without error as a normal user, ch-run will work too (if not, that's a bug). If not, this will hopefully help you understand more clearly what went wrong. pivot_root(2) has a large number of error conditions resulting in EINVAL that are not documented in the man page [1]. The ones we ran into are: 1. The new root cannot be shared [2] outside the mount namespace. This makes sense, as we as an unprivileged user inside our namespace should not be able to change privileged things owned by other namespaces. This condition arises on systemd systems, which mount everything shared by default. 2. The new root must not have been mounted before unshare(2), and/or it must be a mount point. The man page says "new_root does not have to be a mount point", but the source code comment says "[i]t must be a mount point" [3]. (I haven't isolated which was our problem.) In either case, this is a very common situation. 3. The old root is a "rootfs" [4]. This is documented in a source code comment [3] but not the man page. This is an unusual situation for most contexts, because the rootfs is typically the initramfs overmounted during boot. However, some cluster provisioning systems, e.g. Perceus, use the original rootfs directly. Regarding overlayfs: It's very attractive to union-mount a tmpfs over the read-only image; then all programs can write to their hearts' desire, and the image does not change. This also simplifies the code. Unfortunately, overlayfs + userns is not allowed as of 4.4.23. See: https://lwn.net/Articles/671774/ [1]: http://man7.org/linux/man-pages/man2/pivot_root.2.html [2]: https://www.kernel.org/doc/Documentation/filesystems/sharedsubtree.txt [3]: http://lxr.free-electrons.com/source/fs/namespace.c?v=4.4#L2952 [4]: https://www.kernel.org/doc/Documentation/filesystems/ramfs-rootfs-initramfs.txt */ #define _GNU_SOURCE #include #include #include #include #include #include #include #include #define TRY(x) if (x) fatal_errno(__LINE__) void fatal_errno(int line) { printf("error at line %d, errno=%d\n", line, errno); exit(1); } int main(void) { /* Ensure that our image directory exists. It doesn't really matter what's in it. */ if (mkdir("/tmp/newroot", 0755) && errno != EEXIST) TRY (errno); /* Enter the mount and user namespaces. Note that in some cases (e.g., RHEL 6.8), this will succeed even though the userns is not created. In that case, the following mount(2) will fail with EPERM. */ TRY (unshare(CLONE_NEWNS|CLONE_NEWUSER)); /* Claim the image for our namespace by recursively bind-mounting it over itself. This standard trick avoids conditions 1 and 2. */ TRY (mount("/tmp/newroot", "/tmp/newroot", NULL, MS_REC | MS_BIND | MS_PRIVATE, NULL)); /* The next few calls deal with condition 3. The solution is to overmount the root filesystem with literally anything else. We use the parent of the image, /tmp. This doesn't hurt if / is not a rootfs, so we always do it for simplicity. */ /* Claim /tmp for our namespace. You would think that because /tmp contains /tmp/newroot and it's a recursive bind mount, we could claim both in the same call. But, this causes pivot_root(2) to fail later with EBUSY. */ TRY (mount("/tmp", "/tmp", NULL, MS_REC | MS_BIND | MS_PRIVATE, NULL)); /* chdir to /tmp. This moves the process' special "." pointer to the soon-to-be root filesystem. Otherwise, it will keep pointing to the overmounted root. See the e-mail at the end of: https://git.busybox.net/busybox/tree/util-linux/switch_root.c?h=1_24_2 */ TRY (chdir("/tmp")); /* Move /tmp to /. (One could use this to directly enter the image, avoiding pivot_root(2) altogether. However, there are ways to remove all active references to the root filesystem. Then, the image could be unmounted, exposing the old root filesystem underneath. While Charliecloud does not claim a strong isolation boundary, we do want to make activating the UDSS irreversible.) */ TRY (mount("/tmp", "/", NULL, MS_MOVE, NULL)); /* Move the "/" special pointer to the new root filesystem, for the reasons above. (Similar reasoning applies for why we don't use chroot(2) to directly activate the UDSS.) */ TRY (chroot(".")); /* Make a place for the old (intermediate) root filesystem to land. */ if (mkdir("/newroot/oldroot", 0755) && errno != EEXIST) TRY (errno); /* Re-mount the image read-only. */ TRY (mount(NULL, "/newroot", NULL, MS_REMOUNT | MS_BIND | MS_RDONLY, NULL)); /* Finally, make our "real" newroot into the root filesystem. */ TRY (chdir("/newroot")); TRY (syscall(SYS_pivot_root, "/newroot", "/newroot/oldroot")); TRY (chroot(".")); /* Unmount the old filesystem and it's gone for good. */ TRY (umount2("/oldroot", MNT_DETACH)); /* Report success. */ printf("ok\n"); } charliecloud-0.2.3~pre+1a5609e/examples/syscalls/userns.c000066400000000000000000000007651320367540400232170ustar00rootroot00000000000000/* This is a simple hello-world implementation of user namespaces. */ #define _GNU_SOURCE #include #include #include #include #include int main(void) { uid_t euid = geteuid(); int fd; printf("outside userns, uid=%d\n", euid); unshare(CLONE_NEWUSER); fd = open("/proc/self/uid_map", O_WRONLY); dprintf(fd, "0 %d 1\n", euid); close(fd); printf("in userns, uid=%d\n", geteuid()); execlp("/bin/bash", "bash", NULL); } charliecloud-0.2.3~pre+1a5609e/test/000077500000000000000000000000001320367540400170305ustar00rootroot00000000000000charliecloud-0.2.3~pre+1a5609e/test/Build.missing000077500000000000000000000001121320367540400214570ustar00rootroot00000000000000#!/bin/bash # This image's prerequisites can never be satisfied. exit 65 charliecloud-0.2.3~pre+1a5609e/test/Docker_Pull.alpine36_dp000066400000000000000000000000131320367540400232530ustar00rootroot00000000000000alpine:3.6 charliecloud-0.2.3~pre+1a5609e/test/Dockerfile.alpine36000066400000000000000000000001641320367540400224430ustar00rootroot00000000000000FROM alpine:3.6 RUN apk add --no-cache bc # Base image has no default command; we need one to build. CMD ["true"] charliecloud-0.2.3~pre+1a5609e/test/Dockerfile.alpineedge000066400000000000000000000001651320367540400231200ustar00rootroot00000000000000FROM alpine:edge RUN apk add --no-cache bc # Base image has no default command; we need one to build. CMD ["true"] charliecloud-0.2.3~pre+1a5609e/test/Dockerfile.centos6000066400000000000000000000000671320367540400224050ustar00rootroot00000000000000FROM centos:6 RUN yum -y install bc RUN yum clean all charliecloud-0.2.3~pre+1a5609e/test/Dockerfile.centos7000066400000000000000000000000671320367540400224060ustar00rootroot00000000000000FROM centos:7 RUN yum -y install bc RUN yum clean all charliecloud-0.2.3~pre+1a5609e/test/Dockerfile.debian8000066400000000000000000000001641320367540400223340ustar00rootroot00000000000000FROM debian:jessie ENV DEBIAN_FRONTEND noninteractive RUN apt-get update \ && apt-get install -y apt-utils charliecloud-0.2.3~pre+1a5609e/test/Dockerfile.debian8openmpi000066400000000000000000000015141320367540400237240ustar00rootroot00000000000000FROM debian8 # OS packages needed to build OpenMPI. RUN apt-get install -y \ file \ flex \ g++ \ gcc \ gfortran \ less \ libdb5.3-dev \ make \ wget # Compile OpenMPI. We can't use the Debian package because # --disable-pty-support is needed to avoid "pipe function call failed when # setting up I/O forwarding subsystem". ENV MPI_URL https://www.open-mpi.org/software/ompi/v1.10/downloads ENV MPI_VERSION 1.10.5 WORKDIR /usr/src RUN wget -nv ${MPI_URL}/openmpi-${MPI_VERSION}.tar.gz RUN tar xf openmpi-${MPI_VERSION}.tar.gz RUN cd openmpi-${MPI_VERSION} \ && CFLAGS=-O3 \ CXXFLAGS=-O3 \ ./configure --prefix=/usr \ --sysconfdir=/mnt/0 \ --disable-pty-support \ && make -j$(getconf _NPROCESSORS_ONLN) install RUN rm -Rf openmpi-${MPI_VERSION}* charliecloud-0.2.3~pre+1a5609e/test/Dockerfile.python3000066400000000000000000000000251320367540400224220ustar00rootroot00000000000000FROM python:3-alpine charliecloud-0.2.3~pre+1a5609e/test/Dockerfile.ubuntu1604000066400000000000000000000001551320367540400226570ustar00rootroot00000000000000FROM ubuntu:16.04 RUN apt-get update \ && apt-get install -y bc \ && rm -rf /var/lib/apt/lists/* charliecloud-0.2.3~pre+1a5609e/test/Makefile000066400000000000000000000043541320367540400204760ustar00rootroot00000000000000export LC_ALL := C IMAGES := chtest/Build \ $(sort $(wildcard ./Build.*)) \ $(sort $(wildcard ./Dockerfile.*)) \ $(sort $(wildcard ./Docker_Pull.*)) \ $(sort $(wildcard ../examples/*/*/Build)) \ $(sort $(wildcard ../examples/*/*/Dockerfile)) \ $(sort $(wildcard ../examples/*/*/Docker_Pull)) IMAGESQUICK := chtest/Build \ Build.missing \ Dockerfile.alpine36 \ Docker_Pull.alpine36_dp EXAMPLETESTS := $(sort $(wildcard ../examples/*/*/test.bats)) ifdef CH_TEST_OMIT EXAMPLETESTS := $(foreach i,$(EXAMPLETESTS),$(if $(findstring $(CH_TEST_OMIT),$(i)),,$(i))) endif # Favor embedded Bats, if installed, over system Bats. export PATH := $(CURDIR)/bats/bin:$(PATH) # Used by "make all" at top level to build these files for "make install". .PHONY: all all: build_auto.bats build_auto-quick.bats run_auto.bats run_auto-quick.bats .PHONY: test test: test-build test-run .PHONY: test-all test-all: test test-test .PHONY: test-build test-build: build_auto.bats bats build.bats build_auto.bats build_post.bats # Note: This will will not find ch-run correctly if $CWD is not the test # directory, which I believe is assumed elsewhere in the test suite as well. .PHONY: test-run test-run: run_auto.bats bats run.bats run_uidgid.bats run_auto.bats set -e; \ if ( ! bin/ch-run --is-setuid ); then \ for GUEST_USER in $$(id -un) root nobody; do \ for GUEST_GROUP in $$(id -gn) root $$(id -gn nobody); do \ export GUEST_USER; \ export GUEST_GROUP; \ echo testing as: $$GUEST_USER $$GUEST_GROUP; \ bats run_uidgid.bats; \ done; \ done; fi .PHONY: test-test test-test: $(EXAMPLETESTS) bats $(EXAMPLETESTS) .PHONY: test-quick test-quick: build_auto-quick.bats run_auto-quick.bats bats build.bats build_auto-quick.bats run.bats run_uidgid.bats run_auto-quick.bats .PHONY: clean clean: rm -f *_auto*.bats .PHONY: where-bats where-bats: which bats bats --version build_auto.bats: $(IMAGES) ./make-auto build $^ > $@ build_auto-quick.bats: $(IMAGESQUICK) ./make-auto build $^ > $@ run_auto.bats: $(IMAGES) ./make-auto run $^ > $@ run_auto-quick.bats: $(IMAGESQUICK) ./make-auto run $^ > $@ charliecloud-0.2.3~pre+1a5609e/test/README000066400000000000000000000041731320367540400177150ustar00rootroot00000000000000How to write a test image ------------------------- The Charliecloud test suite can build images by three methods: 1. From a Dockerfile, using ch-docker-build. 2. By pulling a Docker image, with "docker pull". 3. By running a custom script. To create an image that will be built, unpacked, and basic tests run within, create a file in test/ called {Dockerfile,Docker_Pull,Build}.foo. This will create an image tagged "foo". To create an image with its own tests, documentation, etc., create a directory in examples/* containing a file called {Dockerfile,Docker_Pull,Build}. Optionally, include a file test.bats containing Bats tests for the image. Additional subdirectories can be symlinked into examples and will be integrated into the test suite. This allows you to create a site-specific test suite. You cannot have multiple images with the same tag. Dockerfile: * It's a Dockerfile. Docker_Pull: * One line stating the address to pull from Docker Hub. * Examples (these refer to the same image as of this writing): alpine:3.6 alpine@sha256:f006ecbb824d87947d0b51ab8488634bf69fe4094959d935c0c103f4820a417d Build: * Script or program that builds the image. * Arguments: * $1: Absolute path to directory containing Build. * $2: Absolute path and name of gzipped tarball output. * $3: Absolute path to appropriate temporary directory. * The script must not write anything in $PWD. * Temporary directory can be used for whatever and need not be cleaned up. (It will be deleted by the test harness.) * The first entry in $PATH will be the Charliecloud under test (i.e., bare ch-* commands will be the right ones). * The tarball must not contain leading directory components; top-level filesystem directories such as bin and usr must be at the root of the tarball with no leading path ("./" is acceptable). * Any scripting language is permitted. To be included in the Charliecloud source code, a language already within the prerequisites is required. * Exit codes: * 0: Image tarball successfully created. * 65: One or more prerequisites were not met. * else: An error occurred. charliecloud-0.2.3~pre+1a5609e/test/bats/000077500000000000000000000000001320367540400177615ustar00rootroot00000000000000charliecloud-0.2.3~pre+1a5609e/test/bin000077700000000000000000000000001320367540400204242../binustar00rootroot00000000000000charliecloud-0.2.3~pre+1a5609e/test/build.bats000066400000000000000000000053461320367540400210120ustar00rootroot00000000000000load common @test 'create tarball directory if needed' { mkdir -p $TARDIR } @test 'documentations build' { command -v sphinx-build > /dev/null 2>&1 || skip "sphinx is not installed" test -d ../doc-src || skip "documentation source code absent" cd ../doc-src && make } @test 'executables seem sane' { # Assume that everything in $CH_BIN is ours if it starts with "ch-" and # either (1) is executable or (2) ends in ".c". Demand satisfaction from # each. The latter is to catch cases when we haven't compiled everything; # if we have, the test makes duplicate demands, but that's low cost. for i in $(find $CH_BIN -name 'ch-*' -a \( -executable -o -name '*.c' \)); do i=$(echo $i | sed s/.c$//) echo echo $i # --version: one line, starts with "0.", looks like a version number. run $i --version echo "$output" [[ $status -eq 0 ]] [[ ${#lines[@]} -eq 1 ]] [[ ${lines[0]} =~ 0\.[0-9]+\.[0-9]+ ]] # --help: returns 0, says "Usage:" somewhere. run $i --help echo "$output" [[ $status -eq 0 ]] [[ $output =~ Usage: ]] # not setuid or setgid (ch-run tested elsewhere) if [[ ! $i =~ .*/ch-run ]]; then ls -l $i [[ ! -u $i ]] [[ ! -g $i ]] fi done } @test 'proxy variables' { # Proxy variables are a mess on UNIX. There are a lot them, and different # programs use them inconsistently. This test is based on the assumption # that if one of the proxy variables are set, then they all should be, in # order to prepare for diverse internet access at build time. # # Coordinate this test with bin/ch-build. # # Note: ALL_PROXY and all_proxy aren't currently included, because they # cause image builds to fail until Docker 1.13 # (https://github.com/docker/docker/pull/27412). V=' no_proxy http_proxy https_proxy' V+=$(echo "$V" | tr '[:lower:]' '[:upper:]') empty_ct=0 for i in $V; do if [[ -n ${!i} ]]; then echo "$i is non-empty" for j in $V; do echo " $j=${!j}" if [[ -z ${!j} ]]; then (( ++empty_ct )) fi done break fi done [[ $empty_ct -eq 0 ]] } @test 'ch-build2dir' { # This test unpacks into $TARDIR so we don't put anything in $IMGDIR at # build time. It removes the image on completion. need_docker TAR=$TARDIR/alpine36.tar.gz IMG=$TARDIR/test [[ ! -e $IMG ]] ch-build2dir .. $TARDIR --file=Dockerfile.alpine36 docker_ok test image_ok $IMG # Remove since we don't want it hanging around later. rm -Rf --one-file-system $TAR $IMG } charliecloud-0.2.3~pre+1a5609e/test/build_post.bats000066400000000000000000000004051320367540400220460ustar00rootroot00000000000000load common @test 'nothing unexpected in tarball directory' { # We want nothing that's not a .tar.gz or .pg_missing. run find $TARDIR -mindepth 1 \ -not \( -name '*.tar.gz' -o -name '*.pq_missing' \) echo "$output" [[ $output = '' ]] } charliecloud-0.2.3~pre+1a5609e/test/chtest/000077500000000000000000000000001320367540400203225ustar00rootroot00000000000000charliecloud-0.2.3~pre+1a5609e/test/chtest/Build000077500000000000000000000067361320367540400213230ustar00rootroot00000000000000#!/bin/bash # Build an Alpine Linux image roughly following the chroot(2) instructions: # https://wiki.alpinelinux.org/wiki/Installing_Alpine_Linux_in_a_chroot # # We deliberately do not sudo. It's a little rough around the edges, because # apk expects root, but it better follows the principle of least privilege. We # could tidy by using the fakeroot utility, but AFAICT that's not particularly # common and we'd prefer not to introduce another dependency. For example, # it's a standard tool on Debian but only in EPEL for CentOS. set -ex SRCDIR=$1 TARBALL=$2 WORKDIR=$3 MIRROR=http://dl-cdn.alpinelinux.org/alpine/v3.6 APK_TOOLS=apk-tools-static-2.7.4-r0.apk IMG="$WORKDIR/img" cd $WORKDIR # "apk add" wants to install a bunch of files root:root. Thus, if we don't map # ourselves to root:root, we get thousands of errors about "Failed to set # ownership". Mapping is not available in setuid mode, so adjust the ch-run # arguments accordingly, and make some manual corrections below. # # For most Build scripts, we'd simply error out with missing prerequisites, # but this is a core image that much of the test suite depends on. if ( ch-run --is-setuid ); then CH_RUN="ch-run -w --no-home $IMG" else CH_RUN="ch-run -u0 -g0 -w --no-home $IMG" fi ## Bootstrap base Alpine Linux. # Download statically linked apk. wget $MIRROR/main/x86_64/$APK_TOOLS # Bootstrap directories. mkdir img mkdir img/{dev,etc,proc,sys,tmp} touch img/etc/{group,hosts,passwd,resolv.conf} # Bootstrap static apk. (cd img && tar xf ../$APK_TOOLS) mkdir img/etc/apk echo $MIRROR/main > img/etc/apk/repositories # Install the base system and a dynamically linked apk. # # This will give a few errors about chown failures. However, the install does # seem to work, so we ignore the failed exit code. $CH_RUN -- /sbin/apk.static \ --allow-untrusted --initdb --update-cache \ add alpine-base apk-tools \ || true # apk doesn't install the busybox symlinks in setuid mode. if ( ch-run --is-setuid ); then rm img/bin/sh for i in $($CH_RUN -- /bin/busybox --list); do ln -s /bin/busybox img/bin/$i done for i in env; do ln -s /bin/busybox img/usr/bin/$i done fi # Now that we've bootstrapped, we don't need apk.static any more. It wasn't # installed using apk, so it's not in the database and can just be rm'ed. rm img/sbin/apk.static.* # Install packages we need for our tests. $CH_RUN -- /sbin/apk add gcc make musl-dev python3 || true # Validate the install. $CH_RUN -- /sbin/apk audit --system $CH_RUN -- /sbin/apk stats # Fix permissions. # # Note that this removes setuid/setgid bits from a few files (and # directories). There is not a race condition, i.e., a window where setuid # executables could become the invoking users, which would be a security hole, # because the setuid/setgid binaries are not group- or world-readable until # after this chmod. chmod -R u+rw,ug-s img ## Install our test stuff. # Sentinel file for --no-home --bind test echo "tmpfs and host home are not overmounted" \ > img/home/overmount-me # We want ch-ssh touch img/usr/bin/ch-ssh # Install test programs. cp -r $SRCDIR img/test $CH_RUN -- sh -c 'cd /test && make' ## Tar it up. # Using pigz saves about 8 seconds. Normally we wouldn't care about that, but # this script is park of "make test-quick", which we'd like developers to use # frequently, so every second matters. if ( command -v pigz >/dev/null 2>&1 ); then GZIP_CMD=pigz else GZIP_CMD=gzip fi cd img tar c . | $GZIP_CMD > $TARBALL charliecloud-0.2.3~pre+1a5609e/test/chtest/Makefile000066400000000000000000000002251320367540400217610ustar00rootroot00000000000000BINS := chroot-escape mknods setgroups setuid ALL := $(BINS) CFLAGS := -std=gnu11 -Wall .PHONY: all all: $(ALL) .PHONY: clean clean: rm -f $(ALL) charliecloud-0.2.3~pre+1a5609e/test/chtest/bind_priv.py000077500000000000000000000022461320367540400226570ustar00rootroot00000000000000#!/usr/bin/env python3 # This script tries to bind to a privileged port on each of the IP addresses # specified on the command line. import errno import socket import sys PORT = 7 # echo results = dict() try: for ip in sys.argv[1:]: s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: s.bind((ip, PORT)) except OSError as x: if (x.errno in (errno.EACCES, errno.EADDRNOTAVAIL)): results[ip] = x.errno else: raise else: results[ip] = 0 except Exception as x: print('ERROR\texception: %s' % x) rc = 1 else: if (len(results) < 1): print('ERROR\tnothing to test', end='') rc = 1 elif (len(set(results.values())) != 1): print('ERROR\tmixed results: ', end='') rc = 1 else: result = next(iter(results.values())) if (result != 0): print('SAFE\t%d (%s) ' % (result, errno.errorcode[result]), end='') rc = 0 else: print('RISK\tsuccessful bind ', end='') rc = 1 explanation = ' '.join('%s=%d' % (ip, e) for (ip, e) in sorted(results.items())) print(explanation) sys.exit(rc) charliecloud-0.2.3~pre+1a5609e/test/chtest/chroot-escape.c000066400000000000000000000041631320367540400232260ustar00rootroot00000000000000/* This program tries to escape a chroot using well-established methods, which are not an exploit but rather take advantage of chroot(2)'s well-defined behavior. We use device and inode numbers to test whether the root directory is the same before and after the escape. References: https://filippo.io/escaping-a-chroot-jail-slash-1/ http://www.bpfh.net/simes/computing/chroot-break.html */ #include #include #include #include #include #include #include #include void fatal(char * msg) { printf("ERROR\t%s: %s\n", msg, strerror(errno)); exit(EXIT_FAILURE); } int main() { struct stat before, after; int fd; int status = EXIT_FAILURE; char tmpdir_template[] = "/tmp/chtest.tmp.chroot.XXXXXX"; char * tmpdir_name; if (stat("/", &before)) fatal("stat before"); tmpdir_name = mkdtemp(tmpdir_template); if (tmpdir_name == NULL) fatal("mkdtemp"); if ((fd = open(".", O_RDONLY)) < 0) fatal("open"); if (chroot(tmpdir_name)) { if (errno == EPERM) { printf("SAFE\tchroot(2) failed with EPERM\n"); status = EXIT_SUCCESS; } else { fatal("chroot"); } } else { if (fchdir(fd)) fatal("fchdir"); if (close(fd)) fatal("close"); for (int i = 0; i < 1024; i++) if (chdir("..")) fatal("chdir"); /* If we got this far, we should be able to call chroot(2), so failure is an error. */ if (chroot(".")) fatal("chroot"); /* If root directory is the same before and after the attempted escape, then the escape failed, and we should be happy. */ if (stat("/", &after)) fatal("stat after"); if (before.st_dev == after.st_dev && before.st_ino == after.st_ino) { printf("SAFE\t"); status = EXIT_SUCCESS; } else { printf("RISK\t"); status = EXIT_FAILURE; } printf("dev/inode before %lu/%lu, after %lu/%lu\n", before.st_dev, before.st_ino, after.st_dev, after.st_ino); } if (rmdir(tmpdir_name)) fatal("rmdir"); return status; } charliecloud-0.2.3~pre+1a5609e/test/chtest/dev_proc_sys.py000077500000000000000000000022541320367540400234010ustar00rootroot00000000000000#!/usr/bin/env python3 import os.path import sys # File in /sys seem to vary between Linux systems. Thus, try a few candidates # and use the first one that exists. What we want is any file under /sys with # permissions root:root -rw------- that's in a directory readable and # executable by unprivileged users, so we know we're testing permissions on # the file rather than any of its containing directories. This may help: # # $ find /sys -type f -a -perm 600 -ls # sys_file = None for f in ("/sys/devices/cpu/rdpmc", "/sys/kernel/mm/page_idle/bitmap", "/sys/module/nf_conntrack_ipv4/parameters/hashsize", "/sys/kernel/slab/request_sock_TCP/red_zone"): if (os.path.exists(f)): sys_file = f break if (sys_file is None): print("ERROR\tno test candidates in /sys exist") sys.exit(1) problem_ct = 0 for f in ("/dev/mem", "/proc/kcore", sys_file): try: open(f, "rb").read(1) print("RISK\t%s: read allowed" % f) problem_ct += 1 except PermissionError: print("SAFE\t%s: read not allowed" % f) except OSError as x: print("ERROR\t%s: exception: %s" % (f, x)) problem_ct += 1 sys.exit(problem_ct != 0) charliecloud-0.2.3~pre+1a5609e/test/chtest/fs_perms.py000077500000000000000000000067301320367540400225230ustar00rootroot00000000000000#!/usr/bin/env python3 # This script walks the directories specified in sys.argv[1:] prepared by # make-perms-test.sh and attempts to read, write, and traverse (cd) each of # the entries within. It compares the result to the expectation encoded in the # filename. # # A summary line is printed on stdout. Running chatter describing each # evaluation is printed on stderr. # # Note: This works more or less the same as an older version embodied by # `examples/sandbox.py --filesystem` but is implemented in pure Python without # shell commands. Thus, the whole script must be run as root if you want to # see what root can do. import os.path import random import re import sys EXPECTED_RE = re.compile(r'~(...)$') class Makes_No_Sense(TypeError): pass VERBOSE = False def main(): if (sys.argv[1] == '--verbose'): global VERBOSE VERBOSE = True sys.argv.pop(1) d = sys.argv[1] mismatch_ct = 0 test_ct = 0 for path in sorted(os.listdir(d)): test_ct += 1 mismatch_ct += not test('%s/%s' % (d, path)) if (test_ct <= 0 or test_ct % 2887 != 0): error("unexpected number of tests: %d" % test_ct) if (mismatch_ct == 0): print('SAFE\t', end='') else: print('RISK\t', end='') print('%d mismatches in %d tests' % (mismatch_ct, test_ct)) sys.exit(mismatch_ct != 0) # Table of test function name fragments. testvec = { (False, False, False): ('X', 'bad'), (False, False, True ): ('l', 'broken_symlink'), (False, True, False): ('f', 'file'), (False, True, True ): ('f', 'file'), (True, False, False): ('d', 'dir'), (True, False, True ): ('d', 'dir') } def error(msg): print('ERROR\t%s' % msg) sys.exit(1) def expected(path): m = EXPECTED_RE.search(path) if (m is None): return '*' else: return m.group(1) def test(path): filetype = (os.path.isdir(path), os.path.isfile(path), os.path.islink(path)) report = '%s %-24s ' % (testvec[filetype][0], path) expect = expected(path) result = '' for op in 'r', 'w', 't': # read, write, traverse f = globals()['try_%s_%s' % (op, testvec[filetype][1])] try: f(path) except (PermissionError, Makes_No_Sense): result += '-' except Exception as x: error('exception on %s: %s' % (path, x)) else: result += op report += result if (expect != '*' and result != expect): print('%s mismatch' % report) return False else: if (VERBOSE): print('%s ok' % report) return True def try_r_bad(path): error('bad file type: %s' % path) try_t_bad = try_r_bad try_w_bad = try_r_bad def try_r_broken_symlink(path): raise Makes_No_Sense() try_t_broken_symlink = try_r_broken_symlink try_w_broken_symlink = try_r_broken_symlink def try_r_dir(path): os.listdir(path) def try_t_dir(path): try_r_file(path + '/file') def try_w_dir(path): fpath = '%s/a%d' % (path, random.getrandbits(64)) try_w_file(fpath) os.unlink(fpath) def try_r_file(path): with open(path, 'rb', buffering=0) as fp: fp.read(1) def try_t_file(path): raise Makes_No_Sense() def try_w_file(path): # The file should exist, but this will create it if it doesn't. We don't # check for that error condition because we *only* want to touch the OS for # open(2) and write(2). with open(path, 'wb', buffering=0) as fp: fp.write(b'written by fs_test.py\n') if (__name__ == '__main__'): main() charliecloud-0.2.3~pre+1a5609e/test/chtest/mknods.c000066400000000000000000000040661320367540400217670ustar00rootroot00000000000000/* Try to make some device files, and print a message to stdout describing what happened. See: https://www.kernel.org/doc/Documentation/devices.txt */ #define _GNU_SOURCE #include #include #include #include #include #include const unsigned char_devs[] = { 1, 3, /* /dev/null -- most innocuous */ 1, 1, /* /dev/mem -- most juicy */ 0 }; int main(int argc, char ** argv) { dev_t dev; char * dir; int i, j; unsigned maj, min; char * path; for (i = 1; i < argc; i++) { dir = argv[i]; for (j = 0; char_devs[j] != 0; j += 2) { maj = char_devs[j]; min = char_devs[j + 1]; if (0 > asprintf(&path, "%s/c%d.%d", dir, maj, min)) { printf("ERROR\tasprintf() failed with errno=%d\n", errno); return 1; } fprintf(stderr, "trying to mknod %s\n", path); dev = makedev(maj, min); if (mknod(path, S_IFCHR | 0500, dev)) { // could not create device, make sure it's an error we expected switch (errno) { case EACCES: case EINVAL: // e.g. /sys/firmware/efi/efivars case ENOENT: // e.g. /proc case ENOTDIR: // for bind-mounted files e.g. /etc/passwd case EPERM: case EROFS: break; default: printf("ERROR\tmknod(2) failed on %s with errno=%d\n", path, errno); return 1; } } else { // created device OK, now try to remove it if (unlink(path)) { printf("ERROR\tmknod(2) succeeded on %s and then unlink(2) " "failed with errno=%d", path, errno); return 1; } printf("RISK\tmknod(2) succeeded on %s (now removed)\n", path); return 1; } } } printf("SAFE\t%d devices in %d dirs failed\n", (i - 1) * (j / 2), i - 1); return 0; } charliecloud-0.2.3~pre+1a5609e/test/chtest/setgroups.c000066400000000000000000000016161320367540400225250ustar00rootroot00000000000000/* Try to drop the last supplemental group, and print a message to stdout describing what happened. */ #include #include #include #include #include #define NGROUPS_MAX 128 int main() { int group_ct; gid_t groups[NGROUPS_MAX]; group_ct = getgroups(NGROUPS_MAX, groups); if (group_ct == -1) { printf("ERROR\tgetgroups(2) failed with errno=%d\n", errno); return 1; } fprintf(stderr, "found %d groups; trying to drop last group %d\n", group_ct, groups[group_ct - 1]); if (setgroups(group_ct - 1, groups)) { if (errno == EPERM) { printf("SAFE\tsetgroups(2) failed with EPERM\n"); return 0; } else { printf("ERROR\tsetgroups(2) failed with errno=%d\n", errno); return 1; } } else { printf("RISK\tsetgroups(2) succeeded\n"); return 1; } } charliecloud-0.2.3~pre+1a5609e/test/chtest/setuid.c000066400000000000000000000016111320367540400217620ustar00rootroot00000000000000/* Try to change effective UID. */ #define _GNU_SOURCE #include #include #include #include #define NOBODY 65534 #define NOBODY2 65533 int main(int argc, char ** argv) { // target UID is nobody, unless we're already nobody uid_t start = geteuid(); uid_t target = start != NOBODY ? NOBODY : NOBODY2; int result; fprintf(stderr, "current EUID=%u, attempting EUID=%u\n", start, target); result = seteuid(target); if (result == 0) { printf("RISK\tsetuid(2) succeeded for EUID=%u\n", target); return 1; } else if (errno == EINVAL) { printf("SAFE\tsetuid(2) failed as expected with EINVAL\n"); return 0; } else if (errno == EPERM) { printf("SAFE\tsetuid(2) failed as expected with EPERM\n"); return 0; } printf("ERROR\tsetuid(2) failed unexpectedly with errno=%d\n", errno); return 1; } charliecloud-0.2.3~pre+1a5609e/test/chtest/signal_out.py000077500000000000000000000020441320367540400230430ustar00rootroot00000000000000#!/usr/bin/env python3 # Send a signal to a process outside the container. # # This is a little tricky. We want a process that: # # 1. is certain to exist, to avoid false negatives # 2. we shouldn't be able to signal (specifically, we can't create a process # to serve as the target) # 3. is outside the container # 4. won't crash the host too badly if killed by the signal # # We want a signal that: # # 5. will be harmless if received # 6. is not blocked # # Accordingly, this test sends SIGCONT to the youngest getty process. The # thinking is that the virtual terminals are unlikely to be in use, so losing # one will be straightforward to clean up. import os import signal import subprocess import sys pdata = subprocess.check_output(["pgrep", "-nl", "getty"]) if (len(pdata) == 0): print("ERROR\tno getty process found") sys.exit(1) pid = int(pdata.split()[0]) try: os.kill(pid, signal.SIGCONT) except PermissionError as x: print("SAFE\tfailed as expected: %s" % x) sys.exit(0) print("RISK\tsucceeded") sys.exit(1) charliecloud-0.2.3~pre+1a5609e/test/common.bash000066400000000000000000000063231320367540400211630ustar00rootroot00000000000000docker_ok () { sudo docker images | fgrep -q $1 } env_require () { if [[ -z ${!1} ]]; then printf "\$$1 is empty or not set\n\n" >&2 exit 1 fi } image_ok () { ls -ld $1 $1/WEIRD_AL_YANKOVIC || true test -d $1 ls -ld $1 || true byte_ct=$(du -s -B1 $1 | cut -f1) echo "$byte_ct" [[ $byte_ct -ge 3145728 ]] # image is at least 3MiB } need_docker () { # Skip test if $CH_TEST_SKIP_DOCKER is true. If argument provided, use # that tag as missing prerequisite sentinel file. PQ=$TARDIR/$1.pq_missing if [[ $PQ ]]; then rm -f $PQ fi if [[ $CH_TEST_SKIP_DOCKER ]]; then if [[ $PQ ]]; then touch $PQ fi skip 'Docker not found or user-skipped' fi } prerequisites_ok () { if [[ -f $TARDIR/$1.pq_missing ]]; then skip 'build prerequisites not met' fi } tarball_ok () { ls -ld $1 || true test -f $1 test -s $1 } # Predictable sorting and collation export LC_ALL=C # Set path to the right Charliecloud. This uses a symlink in this directory # called "bin" which points to the corresponding bin directory, either simply # up and over (source code) or set during "make install". # # Note that sudo resets $PATH, so if you want to run any Charliecloud stuff # under sudo, you must use an absolute path. CH_BIN="$(cd "$(dirname ${BASH_SOURCE[0]})/bin" && pwd)" CH_BIN="$(readlink -f "$CH_BIN")" export PATH=$CH_BIN:$PATH CH_RUN_FILE="$(which ch-run)" if [[ -u $CH_RUN_FILE ]]; then CH_RUN_SETUID=yes fi # User-private temporary directory in case multiple users are running the # tests simultaenously. btnew=$BATS_TMPDIR/bats.tmp.$USER mkdir -p $btnew chmod 700 $btnew export BATS_TMPDIR=$btnew [[ $(stat -c '%a' $BATS_TMPDIR) = '700' ]] # Separate directories for tarballs and images TARDIR=$CH_TEST_TARDIR IMGDIR=$CH_TEST_IMGDIR # Some test variables EXAMPLE_TAG=$(basename $BATS_TEST_DIRNAME) EXAMPLE_IMG=$IMGDIR/$EXAMPLE_TAG CHTEST_TARBALL=$TARDIR/chtest.tar.gz CHTEST_IMG=$IMGDIR/chtest CHTEST_MULTINODE=$SLURM_JOB_ID if [[ $CHTEST_MULTINODE ]]; then # $SLURM_NTASKS isn't always set CHTEST_CORES=$(($SLURM_CPUS_ON_NODE * $SLURM_JOB_NUM_NODES)) fi # If the variable CH_TEST_SKIP_DOCKER is true, we skip all the tests that # depend on Docker. It's true if user-set or command "docker" is not in $PATH. if ( ! command -v docker >/dev/null 2>&1 ); then CH_TEST_SKIP_DOCKER=yes fi # Do we have sudo? if ( command -v sudo >/dev/null 2>&1 && sudo -v >/dev/null 2>&1 ); then # This isn't super reliable; it returns true if we have *any* sudo # privileges, not specifically to run the commands we want to run. CHTEST_HAVE_SUDO=yes fi # Do we have what we need? env_require CH_TEST_TARDIR env_require CH_TEST_IMGDIR env_require CH_TEST_PERMDIRS if ( bash -c 'set -e; [[ 1 = 0 ]]; exit 0' ); then # Bash bug: [[ ... ]] expression doesn't exit with set -e # https://github.com/sstephenson/bats/issues/49 printf 'Need at least Bash 4.1 for these tests.\n\n' >&2 exit 1 fi if [[ ! -x $CH_BIN/ch-run ]]; then printf 'Must build with "make" before running tests.\n\n' >&2 exit 1 fi if ( mount | fgrep -q $IMGDIR ); then printf 'Something is mounted under %s.\n\n' $IMGDIR >&2 exit 1 fi charliecloud-0.2.3~pre+1a5609e/test/make-auto000077500000000000000000000046521320367540400206500ustar00rootroot00000000000000#!/usr/bin/env python # If environment variable CH_TEST_OMIT is set, then avoid Dockerfiles that # seem lengthy. This supports job time limits in Travis CI. See source code # below for the heuristic. from __future__ import print_function import os import os.path import sys mode = sys.argv[1] print("""\ # Do not edit this file; it's autogenerated. load common """) for path in sys.argv[2:]: (d, f) = os.path.split(path) if (d == ""): d = "." # Find image tag. if (f in ("Build", "Dockerfile", "Docker_Pull")): tag = os.path.basename(d) # no extension; use parent directory else: tag = os.path.splitext(f)[1][1:] # use extension assert (tag != "") # Should we omit the test? try: if (os.environ["CH_TEST_OMIT"] in tag): continue except KeyError: pass # Build a tarball: different test for each type. if (mode == "build"): if ("Build" in f): template = """\ @test 'custom build %(tag)s' { TARBALL=$TARDIR/%(tag)s.tar.gz PQ=$TARDIR/%(tag)s.pq_missing WORKDIR=$TARDIR/%(tag)s.tmp rm -f $PQ mkdir $WORKDIR cd %(d)s run ./%(f)s $PWD $TARBALL $WORKDIR echo "$output" rm -Rf $WORKDIR if [[ $status -eq 65 ]]; then touch $PQ skip 'prerequisites not met' fi [[ $status -eq 0 ]] }""" elif ("Dockerfile" in f or "Docker_Pull" in f): if ("Dockerfile" in f): template = """\ @test 'ch-build %(tag)s' { need_docker %(tag)s ch-build -t %(tag)s --file=%(path)s .. docker_ok %(tag)s }""" else: assert ("Docker_Pull" in f) with open(path) as fp: addr = fp.readline().rstrip() template = """\ @test 'docker pull %(tag)s' { need_docker %(tag)s sudo docker pull %(addr)s sudo docker tag %(addr)s %(tag)s }""" template += """ @test 'ch-docker2tar %(tag)s' { need_docker TARBALL=$TARDIR/%(tag)s.tar.gz ch-docker2tar %(tag)s $TARDIR tarball_ok $TARBALL }""" else: assert False, "unknown build type" print("\n" + template % locals()) # Unpack tarball and run: same for all types. if (mode == "run"): print() print("""\ @test 'ch-tar2dir %(tag)s' { prerequisites_ok %(tag)s ch-tar2dir $TARDIR/%(tag)s.tar.gz $IMGDIR } @test 'ch-run %(tag)s /bin/true' { prerequisites_ok %(tag)s IMG=$IMGDIR/%(tag)s ch-run $IMG /bin/true }""" % locals()) charliecloud-0.2.3~pre+1a5609e/test/make-perms-test000077500000000000000000000144271320367540400220040ustar00rootroot00000000000000#!/usr/bin/env python # This script sets up a test directory for testing filesystem permissions # enforcement in UDSS such as virtual machines and containers. It must be run # as root. For example: # # $ sudo ./make-perms-test /data $USER nobody # $ ./fs_perms.py /data/perms_test/pass 2>&1 | egrep -v 'ok$' # d /data/perms_test/pass/ld.out-a~--- --- rwt mismatch # d /data/perms_test/pass/ld.out-r~--- --- rwt mismatch # f /data/perms_test/pass/lf.out-a~--- --- rw- mismatch # f /data/perms_test/pass/lf.out-r~--- --- rw- mismatch # RISK 4 mismatches in 1 directories # # In this case, there will be four mismatches because the symlinks are # expected to be invalid after the pass directory is attached to the UDSS. # # Roughly 3,000 permission settings are evaluated in order to check files and # directories against user, primary group, and supplemental group access. # # For files, we test read and write. For directories, read, write, and # traverse. Files are not tested for execute because it's a more complicated # test (new process needed) and if readable, someone could simply make their # own executable copy. # # Compatibility: As of February 2016, this needs to be compatible with Python # 2.6 because that's the highest version that comes with RHEL 6. We're also # aiming to be source-compatible with Python 3.4+, but that's untested. # # Help: http://python-future.org/compatible_idioms.html from __future__ import division, print_function, unicode_literals import grp import os import os.path import pwd import sys if (len(sys.argv) != 4): print('usage error (PEBKAC)', file=sys.stderr) sys.exit(1) FILE_PERMS = set([0, 2, 4, 6]) DIR_PERMS = set([0, 1, 2, 3, 4, 5, 6, 7]) ALL_PERMS = FILE_PERMS | DIR_PERMS FILE_CONTENT = 'gary' * 19 + '\n' testdir = os.path.abspath(sys.argv[1] + '/perms_test') my_user = sys.argv[2] yr_user = sys.argv[3] me = pwd.getpwnam(my_user) you = pwd.getpwnam(yr_user) my_uid = me.pw_uid my_gid = me.pw_gid my_group = grp.getgrgid(my_gid).gr_name yr_uid = you.pw_uid yr_gid = you.pw_gid yr_group = grp.getgrgid(yr_gid).gr_name # find an arbitrary supplemental group for my_user my_group2 = None my_gid2 = None for g in grp.getgrall(): if (my_user in g.gr_mem and g.gr_name != my_group): my_group2 = g.gr_name my_gid2 = g.gr_gid break if (my_group2 is None): print("couldn't find supplementary group for %s" % my_user, file=sys.stderr) sys.exit(1) if (my_gid == yr_gid or my_gid == my_gid2): print('%s and %s share a group' % (my_user, yr_user), file=sys.stderr) sys.exit(1) print('''\ test directory: %(testdir)s me: %(my_user)s %(my_uid)d you: %(yr_user)s %(yr_uid)d my primary group: %(my_group)s %(my_gid)d my supp. group: %(my_group2)s %(my_gid2)d your primary group: %(yr_group)s %(yr_gid)d ''' % locals()) def set_perms(name, uid, gid, mode): os.chown(name, uid, gid) os.chmod(name, mode) def symlink(src, link_name): if (not os.path.exists(src)): print('link target does not exist: %s' % src) sys.exit(1) os.symlink(src, link_name) class Test(object): def __init__(self, uid, gid, up, gp, op, name=None): self.uid = uid self.group = grp.getgrgid(gid).gr_name self.gid = gid self.user = pwd.getpwuid(uid).pw_name self.up = up self.gp = gp self.op = op self.name_override = name self.mode = up << 6 | gp << 3 | op # Which permission bits govern? if (self.uid == my_uid): self.p = self.up elif (self.gid in (my_gid, my_gid2)): self.p = self.gp else: self.p = self.op @property def name(self): if (self.name_override is not None): return self.name_override else: return ('%s.%s-%s.%03o~%s' % (self.type_, self.user, self.group, self.mode, self.expect)) @property def valid(self): return (all(x in self.valid_perms for x in (self.up, self.gp, self.op))) def write(self): if (not self.valid): return 0 self.write_real() set_perms(self.name, self.uid, self.gid, self.mode) return 1 class Test_Directory(Test): type_ = 'd' valid_perms = DIR_PERMS @property def expect(self): return ( ('r' if (self.p & 4) else '-') + ('w' if (self.p & 3 == 3) else '-') + ('t' if (self.p & 1) else '-')) def write_real(self): os.mkdir(self.name) # Create a file R/W by me, for testing traversal. file_ = self.name + '/file' with open(file_, 'w') as fp: fp.write(FILE_CONTENT) set_perms(file_, my_uid, my_uid, 0660) class Test_File(Test): type_ = 'f' valid_perms = FILE_PERMS @property def expect(self): return ( ('r' if (self.p & 4) else '-') + ('w' if (self.p & 2) else '-') + '-') def write_real(self): with open(self.name, 'w') as fp: fp.write(FILE_CONTENT) try: os.mkdir(testdir) except OSError as x: print("can't mkdir %s: %s" % (testdir, str(x))) sys.exit(1) set_perms(testdir, my_uid, my_gid, 0770) os.chdir(testdir) Test_Directory(my_uid, my_gid, 7, 7, 0, 'nopass').write() os.chdir('nopass') Test_Directory(my_uid, my_gid, 7, 7, 0, 'dir').write() Test_File(my_uid, my_gid, 6, 6, 0, 'file').write() os.chdir('..') Test_Directory(my_uid, my_gid, 7, 7, 0, 'pass').write() os.chdir('pass') ct = 0 for uid in (my_uid, yr_uid): for gid in (my_gid, my_gid2, yr_gid): if (uid == my_uid and gid == my_gid): # Files owned by my_uid:my_gid are not a meaningful access control # test; check the documentation for why. continue for up in ALL_PERMS: for gp in ALL_PERMS: for op in ALL_PERMS: f = Test_File(uid, gid, up, gp, op) #print(f.name) ct += f.write() d = Test_Directory(uid, gid, up, gp, op) #print(d.name) ct += d.write() #print(ct) symlink('f.%s-%s.600~rw-' % (my_user, yr_group), 'lf.in~rw-') symlink('d.%s-%s.700~rwt' % (my_user, yr_group), 'ld.in~rwt') symlink('%s/nopass/file' % testdir, 'lf.out-a~---') symlink('%s/nopass/dir' % testdir, 'ld.out-a~---') symlink('../nopass/file', 'lf.out-r~---') symlink('../nopass/dir', 'ld.out-r~---') print("created %d files and directories" % ct) charliecloud-0.2.3~pre+1a5609e/test/run.bats000066400000000000000000000324401320367540400205120ustar00rootroot00000000000000load common @test 'prepare images directory' { shopt -s nullglob # globs that match nothing yield empty string if [[ -e $IMGDIR ]]; then # Images directory exists. If all it contains is Charliecloud images # or supporting directories, or nothing, then we're ok. Remove any # images (this makes test-build and test-run follow the same path when # run on the same or different machines). Otherwise, error. for i in $IMGDIR/*; do if [[ -d $i && -f $i/WEIRD_AL_YANKOVIC ]]; then echo "found image $i; removing" rm -Rf --one-file-system $i else echo "found non-image $i; aborting" false fi done fi mkdir -p $IMGDIR mkdir -p $IMGDIR/bind1 touch $IMGDIR/bind1/WEIRD_AL_YANKOVIC # fool logic above touch $IMGDIR/bind1/file1 mkdir -p $IMGDIR/bind2 touch $IMGDIR/bind2/WEIRD_AL_YANKOVIC touch $IMGDIR/bind2/file2 } @test 'executables --help' { ch-tar2dir --help ch-run --help ch-ssh --help } @test 'setuid bit matches --is-setuid' { test $CH_RUN_FILE -ef $(which ch-run) [[ -e $CH_RUN_FILE ]] ls -l $CH_RUN_FILE if ( ch-run --is-setuid ); then [[ -n $CH_RUN_SETUID ]] [[ -u $CH_RUN_FILE ]] [[ $(stat -c %U $CH_RUN_FILE) = root ]] else [[ -z $CH_RUN_SETUID ]] [[ ! -u $CH_RUN_FILE ]] #[[ $(stat -c %U $CH_RUN_FILE) != root ]] fi } @test 'setgid bit is off' { [[ -e $CH_RUN_FILE ]] [[ ! -g $CH_RUN_FILE ]] #[[ $(stat -c %G $CH_RUN_FILE) != root ]] } @test 'ch-run refuses to run if setgid' { CH_RUN_TMP=$BATS_TMPDIR/ch-run.setgid GID=$(id -g) GID2=$(id -G | cut -d' ' -f2) echo "GIDs: $GID $GID2" [[ $GID != $GID2 ]] cp -a $CH_RUN_FILE $CH_RUN_TMP ls -l $CH_RUN_TMP chgrp $GID2 $CH_RUN_TMP chmod g+s $CH_RUN_TMP ls -l $CH_RUN_TMP [[ -g $CH_RUN_TMP ]] run $CH_RUN_TMP --version echo "$output" [[ $status -eq 1 ]] [[ $output =~ 'ch-run.setgid: error (' ]] rm $CH_RUN_TMP } @test 'ch-run refuses to run if setuid' { [[ -z $CH_RUN_SETUID ]] || skip 'compiled in setuid mode' [[ -n $CHTEST_HAVE_SUDO ]] || skip 'sudo not available' CH_RUN_TMP=$BATS_TMPDIR/ch-run.setuid cp -a $CH_RUN_FILE $CH_RUN_TMP ls -l $CH_RUN_TMP sudo chown root $CH_RUN_TMP sudo chmod u+s $CH_RUN_TMP ls -l $CH_RUN_TMP [[ -u $CH_RUN_TMP ]] run $CH_RUN_TMP --version echo "$output" [[ $status -eq 1 ]] [[ $output =~ 'ch-run.setuid: error (' ]] sudo rm $CH_RUN_TMP } @test 'ch-run as root: --version and --test' { [[ -n $CHTEST_HAVE_SUDO ]] || skip 'sudo not available' sudo $CH_RUN_FILE --version sudo $CH_RUN_FILE --help } @test 'ch-run as root: run image' { # Running an image should work as root, but it doesn't, and I'm not sure # why, so skip this test. This fails in the test suite with: # # ch-run: couldn't resolve image path: No such file or directory (ch-run.c:139:2) # # but when run manually (with same arguments?) it fails differently with: # # $ sudo bin/ch-run $CH_TEST_IMGDIR/chtest -- true # ch-run: [...]/chtest: Permission denied (ch-run.c:195:13) # skip 'issue #76' sudo $CH_RUN_FILE $CHTEST_IMG -- true } @test 'ch-run as root: root with non-zero GID refused' { [[ -z $TRAVIS ]] || skip 'not permitted on Travis' run sudo -u root -g $(id -gn) $CH_RUN_FILE -v --version echo "$output" [[ $status -eq 1 ]] [[ $output =~ 'error (' ]] } @test 'ch-run -u and -g refused in setuid mode' { [[ -n $CH_RUN_SETUID ]] || skip 'not compiled for setuid' run ch-run -u 65534 echo "$output" [[ $status -eq 64 ]] [[ $output =~ "ch-run: invalid option -- 'u'" ]] run ch-run -g 65534 echo "$output" [[ $status -eq 64 ]] [[ $output =~ "ch-run: invalid option -- 'g'" ]] } @test 'syscalls/pivot_root' { [[ -n $CH_RUN_SETUID ]] && skip cd ../examples/syscalls ./pivot_root } @test 'unpack chtest image' { if ( image_ok $CHTEST_IMG ); then # image exists, remove so we can test new unpack rm -Rf --one-file-system $CHTEST_IMG fi ch-tar2dir $CHTEST_TARBALL $IMGDIR # new unpack image_ok $CHTEST_IMG ch-tar2dir $CHTEST_TARBALL $IMGDIR # overwrite image_ok $CHTEST_IMG } @test 'ch-tar2dir errors' { # tarball doesn't exist run ch-tar2dir does_not_exist.tar.gz $IMGDIR echo "$output" [[ $status -eq 1 ]] [[ $output = "can't read does_not_exist.tar.gz" ]] # tarball exists but isn't readable touch $BATS_TMPDIR/unreadable.tar.gz chmod 000 $BATS_TMPDIR/unreadable.tar.gz run ch-tar2dir $BATS_TMPDIR/unreadable.tar.gz $IMGDIR echo "$output" [[ $status -eq 1 ]] [[ $output = "can't read $BATS_TMPDIR/unreadable.tar.gz" ]] } @test 'workaround for /bin not in $PATH' { echo "$PATH" # if /bin is in $PATH, latter passes through unchanged PATH2="$CH_BIN:/bin:/usr/bin" echo $PATH2 PATH=$PATH2 run ch-run $CHTEST_IMG -- /bin/sh -c 'echo $PATH' echo "$output" [[ $status -eq 0 ]] [[ $output = $PATH2 ]] PATH2="/bin:$CH_BIN:/usr/bin" echo $PATH2 PATH=$PATH2 run ch-run $CHTEST_IMG -- /bin/sh -c 'echo $PATH' echo "$output" [[ $status -eq 0 ]] [[ $output = $PATH2 ]] # if /bin isn't in $PATH, former is added to end PATH2="$CH_BIN:/usr/bin" echo $PATH2 PATH=$PATH2 run ch-run $CHTEST_IMG -- /bin/sh -c 'echo $PATH' echo "$output" [[ $status -eq 0 ]] [[ $output = $PATH2:/bin ]] } @test '$PATH unset' { BACKUP_PATH=$PATH unset PATH run $CH_RUN_FILE $CHTEST_IMG -- \ /usr/bin/python3 -c 'import os; print(os.getenv("PATH") is None)' PATH=$BACKUP_PATH echo "$output" [[ $status -eq 0 ]] [[ $output = "True" ]] } @test 'mountns id differs' { host_ns=$(stat -Lc '%i' /proc/self/ns/mnt) echo "host: $host_ns" guest_ns=$(ch-run $CHTEST_IMG -- stat -Lc '%i' /proc/self/ns/mnt) echo "guest: $guest_ns" [[ -n $host_ns && -n $guest_ns && $host_ns -ne $guest_ns ]] } @test 'userns id differs' { [[ -n $CH_RUN_SETUID ]] && skip host_ns=$(stat -Lc '%i' /proc/self/ns/user) echo "host: $host_userns" guest_ns=$(ch-run $CHTEST_IMG -- stat -Lc '%i' /proc/self/ns/user) echo "guest: $guest_ns" [[ -n $host_ns && -n $guest_ns && $host_ns -ne $guest_ns ]] } @test 'distro differs' { # This is a catch-all and a bit of a guess. Even if it fails, however, we # get an empty string, which is fine for the purposes of the test. echo hello world host_distro=$( cat /etc/os-release /etc/*-release /etc/*_version \ | egrep -m1 '[A-Za-z] [0-9]' \ | sed -r 's/^(.*")?(.+)(")$/\2/') echo "host: $host_distro" guest_expected='Alpine Linux v3.6' echo "guest expected: $guest_expected" if [[ $host_distro = $guest_expected ]]; then skip 'host matches expected guest distro' fi guest_distro=$(ch-run $CHTEST_IMG -- \ cat /etc/os-release \ | fgrep PRETTY_NAME \ | sed -r 's/^(.*")?(.+)(")$/\2/') echo "guest: $guest_distro" [[ $guest_distro = $guest_expected ]] [[ $guest_distro != $host_distro ]] } @test 'user and group match host' { host_uid=$(id -u) guest_uid=$(ch-run $CHTEST_IMG -- id -u) [[ $host_uid = $guest_uid ]] host_pgid=$(id -g) guest_pgid=$(ch-run $CHTEST_IMG -- id -g) [[ $host_pgid = $guest_pgid ]] host_username=$(id -un) guest_username=$(ch-run $CHTEST_IMG -- id -un) [[ $host_username = $guest_username ]] host_pgroup=$(id -gn) guest_pgroup=$(ch-run $CHTEST_IMG -- id -gn) [[ $host_pgroup = $guest_pgroup ]] } @test 'mount image read-only' { run ch-run $CHTEST_IMG sh < write' ch-run -w $CHTEST_IMG rm write } @test 'ch-run --bind' { # one bind, default destination (/mnt/0) ch-run -b $IMGDIR/bind1 $CHTEST_IMG -- cat /mnt/0/file1 # one bind, explicit destination ch-run -b $IMGDIR/bind1:/mnt/9 $CHTEST_IMG -- cat /mnt/9/file1 # two binds, default destination ch-run -b $IMGDIR/bind1 -b $IMGDIR/bind2 $CHTEST_IMG \ -- cat /mnt/0/file1 /mnt/1/file2 # two binds, explicit destinations ch-run -b $IMGDIR/bind1:/mnt/8 -b $IMGDIR/bind2:/mnt/9 $CHTEST_IMG \ -- cat /mnt/8/file1 /mnt/9/file2 # two binds, default/explicit ch-run -b $IMGDIR/bind1 -b $IMGDIR/bind2:/mnt/9 $CHTEST_IMG \ -- cat /mnt/0/file1 /mnt/9/file2 # two binds, explicit/default ch-run -b $IMGDIR/bind1:/mnt/8 -b $IMGDIR/bind2 $CHTEST_IMG \ -- cat /mnt/8/file1 /mnt/1/file2 # bind one source at two destinations ch-run -b $IMGDIR/bind1:/mnt/8 -b $IMGDIR/bind1:/mnt/9 $CHTEST_IMG \ -- diff -u /mnt/8/file1 /mnt/9/file1 # bind two sources at one destination ch-run -b $IMGDIR/bind1:/mnt/9 -b $IMGDIR/bind2:/mnt/9 $CHTEST_IMG \ -- sh -c '[ ! -e /mnt/9/file1 ] && cat /mnt/9/file2' # omit tmpfs at /home, which shouldn't be empty ch-run --no-home $CHTEST_IMG -- cat /home/overmount-me # overmount tmpfs at /home ch-run -b $IMGDIR/bind1:/home $CHTEST_IMG -- cat /home/file1 # bind to /home without overmount ch-run --no-home -b $IMGDIR/bind1:/home $CHTEST_IMG -- cat /home/file1 # omit default /home, with unrelated --bind ch-run --no-home -b $IMGDIR/bind1 $CHTEST_IMG -- cat /mnt/0/file1 } @test 'ch-run --bind errors' { # too many binds (11) run ch-run -b0 -b1 -b2 -b3 -b4 -b5 -b6 -b7 -b8 -b9 -b10 $CHTEST_IMG -- true echo "$output" [[ $status -eq 1 ]] [[ $output =~ 'ch-run: --bind can be used at most 10 times' ]] # no argument to --bind run ch-run $CHTEST_IMG -b echo "$output" [[ $status -eq 64 ]] [[ $output =~ 'ch-run: option requires an argument' ]] # empty argument to --bind run ch-run -b '' $CHTEST_IMG -- true echo "$output" [[ $status -eq 1 ]] [[ $output =~ 'ch-run: --bind: no source provided' ]] # source not provided run ch-run -b :/mnt/9 $CHTEST_IMG -- true echo "$output" [[ $status -eq 1 ]] [[ $output =~ 'ch-run: --bind: no source provided' ]] # destination not provided run ch-run -b $IMGDIR/bind1: $CHTEST_IMG -- true echo "$output" [[ $status -eq 1 ]] [[ $output =~ 'ch-run: --bind: no destination provided' ]] # source does not exist run ch-run -b $IMGDIR/hoops $CHTEST_IMG -- true echo "$output" [[ $status -eq 1 ]] r='^ch-run: could not bind .+/hoops to /mnt/0: No such file or directory' [[ $output =~ $r ]] # destination does not exist run ch-run -b $IMGDIR/bind1:/goops $CHTEST_IMG -- true echo "$output" [[ $status -eq 1 ]] r='^ch-run: could not bind .+/bind1 to /goops: No such file or directory' [[ $output =~ $r ]] # neither source nor destination exist run ch-run -b $IMGDIR/hoops:/goops $CHTEST_IMG -- true echo "$output" [[ $status -eq 1 ]] r='^ch-run: could not bind .+/hoops to /goops: No such file or directory' [[ $output =~ $r ]] # correct bind followed by source does not exist run ch-run -b $IMGDIR/bind1:/mnt/9 -b $IMGDIR/hoops $CHTEST_IMG -- true echo "$output" [[ $status -eq 1 ]] r='^ch-run: could not bind .+/hoops to /mnt/1: No such file or directory' [[ $output =~ $r ]] # correct bind followed by destination does not exist run ch-run -b $IMGDIR/bind1 -b $IMGDIR/bind2:/goops $CHTEST_IMG -- true echo "$output" [[ $status -eq 1 ]] r='^ch-run: could not bind .+/bind2 to /goops: No such file or directory' [[ $output =~ $r ]] } @test 'ch-run --cd' { # Default initial working directory is /. run ch-run $CHTEST_IMG -- pwd echo "$output" [[ $status -eq 0 ]] [[ $output = '/' ]] # Specify initial working directory. run ch-run --cd /dev $CHTEST_IMG -- pwd echo "$output" [[ $status -eq 0 ]] [[ $output = '/dev' ]] # Error if directory does not exist. run ch-run --cd /goops $CHTEST_IMG -- true echo "$output" [[ $status -eq 1 ]] [[ $output =~ "ch-run: can't cd to /goops: No such file or directory" ]] } @test '/usr/bin/ch-ssh' { ls -l $CH_BIN/ch-ssh ch-run $CHTEST_IMG -- ls -l /usr/bin/ch-ssh ch-run $CHTEST_IMG -- test -x /usr/bin/ch-ssh host_size=$(stat -c %s $CH_BIN/ch-ssh) guest_size=$(ch-run $CHTEST_IMG -- stat -c %s /usr/bin/ch-ssh) echo "host: $host_size, guest: $guest_size" [[ $host_size -eq $guest_size ]] } @test 'permissions test directories exist' { if [[ $CH_TEST_PERMDIRS = skip ]]; then skip fi for d in $CH_TEST_PERMDIRS; do d=$d/perms_test echo $d test -d $d test -d $d/pass test -f $d/pass/file test -d $d/nopass test -d $d/nopass/dir test -f $d/nopass/file done } @test 'relative path to image' { # bug number 6 DIRNAME=$(dirname $CHTEST_IMG) BASEDIR=$(basename $CHTEST_IMG) cd $DIRNAME && ch-run $BASEDIR -- true } @test 'symlink to image' { # bug number 50 ln -s $CHTEST_IMG $BATS_TMPDIR/symlink-test ch-run $BATS_TMPDIR/symlink-test -- true } charliecloud-0.2.3~pre+1a5609e/test/run_uidgid.bats000066400000000000000000000112561320367540400220410ustar00rootroot00000000000000load common setup () { if [[ -n $GUEST_USER ]]; then # Specific user requested for testing. [[ -n $GUEST_GROUP ]] GUEST_UID=$(id -u $GUEST_USER) GUEST_GID=$(getent group $GUEST_GROUP | cut -d: -f3) UID_ARGS="-u $GUEST_UID" GID_ARGS="-g $GUEST_GID" echo "ID arguments: $GUEST_USER/$GUEST_UID $GUEST_GROUP/$GUEST_GID" echo else # No specific user requested. [[ -z $GUEST_GROUP ]] GUEST_USER=$(id -un) GUEST_UID=$(id -u) [[ $GUEST_USER = $USER ]] [[ $GUEST_UID -ne 0 ]] GUEST_GROUP=$(id -gn) GUEST_GID=$(id -g) [[ $GUEST_GID -ne 0 ]] UID_ARGS= GID_ARGS= echo "no ID arguments" echo fi } @test 'user and group as specified' { g=$(ch-run $UID_ARGS $GID_ARGS $CHTEST_IMG -- id -un) [[ $GUEST_USER = $g ]] g=$(ch-run $UID_ARGS $GID_ARGS $CHTEST_IMG -- id -u) [[ $GUEST_UID = $g ]] g=$(ch-run $UID_ARGS $GID_ARGS $CHTEST_IMG -- id -gn) [[ $GUEST_GROUP = $g ]] g=$(ch-run $UID_ARGS $GID_ARGS $CHTEST_IMG -- id -g) [[ $GUEST_GID = $g ]] } @test 'chroot escape' { # Try to escape a chroot(2) using the standard approach. ch-run $UID_ARGS $GID_ARGS $CHTEST_IMG -- /test/chroot-escape } @test '/dev /proc /sys' { # Read some files in /dev, /proc, and /sys that I shouldn't have access to. ch-run $UID_ARGS $GID_ARGS $CHTEST_IMG -- /test/dev_proc_sys.py } @test 'filesystem permission enforcement' { if [[ $CH_TEST_PERMDIRS = skip ]]; then skip fi for d in $CH_TEST_PERMDIRS; do d="$d/perms_test/pass" echo "verifying: $d" ch-run --no-home --private-tmp \ $UID_ARGS $GID_ARGS -b $d $CHTEST_IMG -- \ /test/fs_perms.py /mnt/0 done } @test 'mknod(2)' { # Make some device files. If this works, we might be able to later read or # write them to do things we shouldn't. Try on all mount points. ch-run $UID_ARGS $GID_ARGS $CHTEST_IMG -- \ sh -c '/test/mknods $(cat /proc/mounts | cut -d" " -f2)' } @test 'privileged IPv4 bind(2)' { # Bind to privileged ports on all host IPv4 addresses. # # Some supported distributions don't have "hostname --all-ip-addresses". # Hence the awk voodoo. ADDRS=$(ip -o addr | awk '/inet / {gsub(/\/.*/, " ",$4); print $4}') ch-run $UID_ARGS $GID_ARGS $CHTEST_IMG -- /test/bind_priv.py $ADDRS } @test 'remount host root' { # Re-mount the root filesystem. Notes: # # - Because we have /dev from the host, we don't need to create a new # device node. This makes the test simpler. In particular, we can # treat network and local root the same. # # - We leave the filesystem mounted even if successful, again to make # the test simpler. The rest of the tests will ignore it or maybe # over-mount something else. ch-run $UID_ARGS $GID_ARGS $CHTEST_IMG -- \ sh -c '[ -f /bin/mount -a -x /bin/mount ]' dev=$(findmnt -n -o SOURCE -T /) type=$(findmnt -n -o FSTYPE -T /) opts=$(findmnt -n -o OPTIONS -T /) run ch-run $UID_ARGS $GID_ARGS $CHTEST_IMG -- \ /bin/mount -n -o $opts -t $type $dev /mnt/0 echo "$output" # return codes from http://man7.org/linux/man-pages/man8/mount.8.html # busybox seems to use the same list case $status in 0) # "success" printf 'RISK\tsuccessful mount\n' return 1 ;; 1) ;& # "incorrect invocation of permissions" (we care which) 255) # undocumented if [[ $output =~ 'ermission denied' ]]; then printf 'SAFE\tmount exit %d, permission denied\n' $status return 0 elif [[ $dev = 'rootfs' && $output =~ 'No such device' ]]; then printf 'SAFE\tmount exit %d, no such device for rootfs' $status return 0 else printf 'RISK\tmount exit %d w/o known explanation\n' $status return 1 fi ;; 32) # "mount failed" printf 'SAFE\tmount exited with code 32\n' return 0 ;; esac printf 'ERROR\tunknown exit code: %s\n' $status return 1 } @test 'setgroups(2)' { # Can we change our supplemental groups? ch-run $UID_ARGS $GID_ARGS $CHTEST_IMG -- /test/setgroups } @test 'seteuid(2)' { # Try to seteuid(2) to another UID we shouldn't have access to ch-run $UID_ARGS $GID_ARGS $CHTEST_IMG -- /test/setuid } @test 'signal process outside container' { # Send a signal to a process we shouldn't be able to signal. ch-run $UID_ARGS $GID_ARGS $CHTEST_IMG -- /test/signal_out.py } charliecloud-0.2.3~pre+1a5609e/test/travis.sh000077500000000000000000000024661320367540400207070ustar00rootroot00000000000000#!/bin/bash # Warning: This script installs software and messes with your "docker" binary. # Don't run it unless you know what you are doing. # We start in the Charliecloud Git working directory. set -e echo "SETUID=$SETUID TARBALL=$TARBALL INSTALL=$INSTALL" # Remove sbin directories from $PATH (see issue #43). Assume none are first. echo $PATH for i in /sbin /usr/sbin /usr/local/sbin; do export PATH=${PATH/:$i/} done echo $PATH set -x case $TARBALL in export) make export tar xf charliecloud-*.tar.gz cd charliecloud-* ;; archive) # The Travis image already has Bats installed. git archive HEAD --prefix=charliecloud/ -o charliecloud.tar tar xf charliecloud.tar cd charliecloud ;; esac make SETUID=$SETUID bin/ch-run --version if [[ $INSTALL ]]; then sudo make install PREFIX=/usr/local cd /usr/local/share/doc/charliecloud fi cd test make where-bats make test-quick make test-all # To test without Docker, move the binary out of the way. DOCKER=$(which docker) sudo mv $DOCKER $DOCKER.tmp make test-all # For Travis, this isn't really necessary, since the VM will go away # immediately after this script exits. However, restore the binary to enable # testing this script in other environments. sudo mv $DOCKER.tmp $DOCKER charliecloud-0.2.3~pre+1a5609e/test/travis.yml000066400000000000000000000036721320367540400210730ustar00rootroot00000000000000dist: trusty sudo: required language: c compiler: gcc # This defines a "matrix" of jobs. Each combination of environment variables # defines a different job. They run in parallel, five at a time. # # FIXME: Each job starts with a cold Docker cache, which wastes work heating # it up in parallel. It would be nice if "make test-build" could be done # serially before splitting into parallel jobs. # # TARBALL= # build in Git checkout & use embedded Bats # TARBALL=archive # build from "git archive" tarball & use system Bats # TARBALL=export # build from "make export" tarball & use embedded Bats # INSTALL= # run from build directory # INSTALL=yes # make install to /usr/local, run that one # SETUID= # standard build (user + mount namespaces) # SETUID=yes # setuid build (mount namespace, setuid binary) # env: - SETUID= TARBALL= INSTALL= - SETUID= TARBALL= INSTALL=yes - SETUID= TARBALL=archive INSTALL= - SETUID= TARBALL=archive INSTALL=yes - SETUID= TARBALL=export INSTALL= - SETUID= TARBALL=export INSTALL=yes # Setuid mode is only sort of supported, so don't test it as carefully. - SETUID=yes TARBALL= INSTALL= - SETUID=yes TARBALL= INSTALL=yes # - SETUID=yes TARBALL=archive INSTALL= # - SETUID=yes TARBALL=archive INSTALL=yes # - SETUID=yes TARBALL=export INSTALL= # - SETUID=yes TARBALL=export INSTALL=yes install: - sudo apt-get install pigz - sudo pip install sphinx sphinx-rtd-theme before_script: - getconf _NPROCESSORS_ONLN - free -m - df -h - df -h /var/tmp - export CH_TEST_TARDIR=/var/tmp/tarballs - export CH_TEST_IMGDIR=/var/tmp/images - export CH_TEST_PERMDIRS='/var/tmp /run' - export CH_TEST_OMIT=mpi - unset JAVA_HOME # otherwise Spark tries to use host's Java - for d in $CH_TEST_PERMDIRS; do sudo test/make-perms-test $d $USER nobody; done script: - test/travis.sh after_script: - free -m - df -h charliecloud-0.2.3~pre+1a5609e/test/unused/000077500000000000000000000000001320367540400203335ustar00rootroot00000000000000charliecloud-0.2.3~pre+1a5609e/test/unused/echo-euid.c000066400000000000000000000004051320367540400223400ustar00rootroot00000000000000/* This program prints the effective user ID on stdout and exits. It is useful for testing whether the setuid bit was effective. */ #include #include #include int main(void) { printf("%u\n", geteuid()); return 0; } charliecloud-0.2.3~pre+1a5609e/test/unused/su_wrap.py000077500000000000000000000036721320367540400224000ustar00rootroot00000000000000#!/usr/bin/env python3 # This script tries to use su to gain root privileges, assuming that # /etc/shadow has been changed such that no password is required. It uses # pexpect to emulate the terminal that su requires. # # WARNING: This does not work. For example: # # $ whoami ; echo $UID EUID # reidpr # 1001 1001 # $ /bin/su -c whoami # root # $ ./su_wrap.py 2>> /dev/null # SAFE escalation failed: empty password rejected # # That is, manual su can escalate without a password (and doesn't without the # /etc/shadow hack), but when this program tries to do apparently the same # thing, su wants a password. # # I have not been able to track down why this happens. I suspect that PAM has # some extra smarts about TTY that causes it to ask for a password under # pexpect. I'm leaving the code in the repository in case some future person # can figure it out. import sys import pexpect # Invoke su. This will do one of three things: # # 1. Print 'root'; the escalation was successful. # 2. Ask for a password; the escalation was unsuccessful. # 3. Something else; this is an error. # p = pexpect.spawn('/bin/su', ['-c', 'whoami'], timeout=5, encoding='UTF-8', logfile=sys.stderr) i = p.expect_exact(['root', 'Password:']) try: if (i == 0): # printed "root" print('RISK\tescalation successful: no password requested') elif (i == 1): # asked for password p.sendline() # try empty password i = p.expect_exact(['root', 'Authentication failure']) if (i == 0): # printed "root" print('RISK\tescalation successful: empty password accepted') elif (i == 1): # explicit failure print('SAFE\tescalation failed: empty password rejected') else: assert False else: assert False except p.EOF: print('ERROR\tsu exited unexpectedly') except p.TIMEOUT: print('ERROR\ttimed out waiting for su') except AssertionError: print('ERROR\tassertion failed') pax_global_header00006660000000000000000000000064126616572620014527gustar00rootroot0000000000000052 comment=03608115df2071fff4eaaff1605768c275e5f81f charliecloud-0.2.3~pre+1a5609e/test/bats/000077500000000000000000000000001266165726200177725ustar00rootroot00000000000000charliecloud-0.2.3~pre+1a5609e/test/bats/.gitattributes000077500000000000000000000000511266165726200226640ustar00rootroot00000000000000* text=auto *.sh eol=lf libexec/* eol=lf charliecloud-0.2.3~pre+1a5609e/test/bats/.travis.yml000066400000000000000000000001261266165726200221020ustar00rootroot00000000000000language: c script: bin/bats --tap test notifications: email: on_success: never charliecloud-0.2.3~pre+1a5609e/test/bats/CONDUCT.md000066400000000000000000000063431266165726200214210ustar00rootroot00000000000000# Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting one of the project maintainers listed below. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Project Maintainers * Sam Stephenson <> ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] [homepage]: http://contributor-covenant.org [version]: http://contributor-covenant.org/version/1/4/ charliecloud-0.2.3~pre+1a5609e/test/bats/LICENSE000066400000000000000000000020421266165726200207750ustar00rootroot00000000000000Copyright (c) 2014 Sam Stephenson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. charliecloud-0.2.3~pre+1a5609e/test/bats/README.md000066400000000000000000000227671266165726200212670ustar00rootroot00000000000000# Bats: Bash Automated Testing System Bats is a [TAP](http://testanything.org)-compliant testing framework for Bash. It provides a simple way to verify that the UNIX programs you write behave as expected. A Bats test file is a Bash script with special syntax for defining test cases. Under the hood, each test case is just a function with a description. ```bash #!/usr/bin/env bats @test "addition using bc" { result="$(echo 2+2 | bc)" [ "$result" -eq 4 ] } @test "addition using dc" { result="$(echo 2 2+p | dc)" [ "$result" -eq 4 ] } ``` Bats is most useful when testing software written in Bash, but you can use it to test any UNIX program. Test cases consist of standard shell commands. Bats makes use of Bash's `errexit` (`set -e`) option when running test cases. If every command in the test case exits with a `0` status code (success), the test passes. In this way, each line is an assertion of truth. ## Running tests To run your tests, invoke the `bats` interpreter with a path to a test file. The file's test cases are run sequentially and in isolation. If all the test cases pass, `bats` exits with a `0` status code. If there are any failures, `bats` exits with a `1` status code. When you run Bats from a terminal, you'll see output as each test is performed, with a check-mark next to the test's name if it passes or an "X" if it fails. $ bats addition.bats ✓ addition using bc ✓ addition using dc 2 tests, 0 failures If Bats is not connected to a terminal—in other words, if you run it from a continuous integration system, or redirect its output to a file—the results are displayed in human-readable, machine-parsable [TAP format](http://testanything.org). You can force TAP output from a terminal by invoking Bats with the `--tap` option. $ bats --tap addition.bats 1..2 ok 1 addition using bc ok 2 addition using dc ### Test suites You can invoke the `bats` interpreter with multiple test file arguments, or with a path to a directory containing multiple `.bats` files. Bats will run each test file individually and aggregate the results. If any test case fails, `bats` exits with a `1` status code. ## Writing tests Each Bats test file is evaluated _n+1_ times, where _n_ is the number of test cases in the file. The first run counts the number of test cases, then iterates over the test cases and executes each one in its own process. For more details about how Bats evaluates test files, see [Bats Evaluation Process](https://github.com/sstephenson/bats/wiki/Bats-Evaluation-Process) on the wiki. ### `run`: Test other commands Many Bats tests need to run a command and then make assertions about its exit status and output. Bats includes a `run` helper that invokes its arguments as a command, saves the exit status and output into special global variables, and then returns with a `0` status code so you can continue to make assertions in your test case. For example, let's say you're testing that the `foo` command, when passed a nonexistent filename, exits with a `1` status code and prints an error message. ```bash @test "invoking foo with a nonexistent file prints an error" { run foo nonexistent_filename [ "$status" -eq 1 ] [ "$output" = "foo: no such file 'nonexistent_filename'" ] } ``` The `$status` variable contains the status code of the command, and the `$output` variable contains the combined contents of the command's standard output and standard error streams. A third special variable, the `$lines` array, is available for easily accessing individual lines of output. For example, if you want to test that invoking `foo` without any arguments prints usage information on the first line: ```bash @test "invoking foo without arguments prints usage" { run foo [ "$status" -eq 1 ] [ "${lines[0]}" = "usage: foo " ] } ``` ### `load`: Share common code You may want to share common code across multiple test files. Bats includes a convenient `load` command for sourcing a Bash source file relative to the location of the current test file. For example, if you have a Bats test in `test/foo.bats`, the command ```bash load test_helper ``` will source the script `test/test_helper.bash` in your test file. This can be useful for sharing functions to set up your environment or load fixtures. ### `skip`: Easily skip tests Tests can be skipped by using the `skip` command at the point in a test you wish to skip. ```bash @test "A test I don't want to execute for now" { skip run foo [ "$status" -eq 0 ] } ``` Optionally, you may include a reason for skipping: ```bash @test "A test I don't want to execute for now" { skip "This command will return zero soon, but not now" run foo [ "$status" -eq 0 ] } ``` Or you can skip conditionally: ```bash @test "A test which should run" { if [ foo != bar ]; then skip "foo isn't bar" fi run foo [ "$status" -eq 0 ] } ``` ### `setup` and `teardown`: Pre- and post-test hooks You can define special `setup` and `teardown` functions, which run before and after each test case, respectively. Use these to load fixtures, set up your environment, and clean up when you're done. ### Code outside of test cases You can include code in your test file outside of `@test` functions. For example, this may be useful if you want to check for dependencies and fail immediately if they're not present. However, any output that you print in code outside of `@test`, `setup` or `teardown` functions must be redirected to `stderr` (`>&2`). Otherwise, the output may cause Bats to fail by polluting the TAP stream on `stdout`. ### Special variables There are several global variables you can use to introspect on Bats tests: * `$BATS_TEST_FILENAME` is the fully expanded path to the Bats test file. * `$BATS_TEST_DIRNAME` is the directory in which the Bats test file is located. * `$BATS_TEST_NAMES` is an array of function names for each test case. * `$BATS_TEST_NAME` is the name of the function containing the current test case. * `$BATS_TEST_DESCRIPTION` is the description of the current test case. * `$BATS_TEST_NUMBER` is the (1-based) index of the current test case in the test file. * `$BATS_TMPDIR` is the location to a directory that may be used to store temporary files. ## Installing Bats from source Check out a copy of the Bats repository. Then, either add the Bats `bin` directory to your `$PATH`, or run the provided `install.sh` command with the location to the prefix in which you want to install Bats. For example, to install Bats into `/usr/local`, $ git clone https://github.com/sstephenson/bats.git $ cd bats $ ./install.sh /usr/local Note that you may need to run `install.sh` with `sudo` if you do not have permission to write to the installation prefix. ## Support The Bats source code repository is [hosted on GitHub](https://github.com/sstephenson/bats). There you can file bugs on the issue tracker or submit tested pull requests for review. For real-world examples from open-source projects using Bats, see [Projects Using Bats](https://github.com/sstephenson/bats/wiki/Projects-Using-Bats) on the wiki. To learn how to set up your editor for Bats syntax highlighting, see [Syntax Highlighting](https://github.com/sstephenson/bats/wiki/Syntax-Highlighting) on the wiki. ## Version history *0.4.0* (August 13, 2014) * Improved the display of failing test cases. Bats now shows the source code of failing test lines, along with full stack traces including function names, filenames, and line numbers. * Improved the display of the pretty-printed test summary line to include the number of skipped tests, if any. * Improved the speed of the preprocessor, dramatically shortening test and suite startup times. * Added support for absolute pathnames to the `load` helper. * Added support for single-line `@test` definitions. * Added bats(1) and bats(7) manual pages. * Modified the `bats` command to default to TAP output when the `$CI` variable is set, to better support environments such as Travis CI. *0.3.1* (October 28, 2013) * Fixed an incompatibility with the pretty formatter in certain environments such as tmux. * Fixed a bug where the pretty formatter would crash if the first line of a test file's output was invalid TAP. *0.3.0* (October 21, 2013) * Improved formatting for tests run from a terminal. Failing tests are now colored in red, and the total number of failing tests is displayed at the end of the test run. When Bats is not connected to a terminal (e.g. in CI runs), or when invoked with the `--tap` flag, output is displayed in standard TAP format. * Added the ability to skip tests using the `skip` command. * Added a message to failing test case output indicating the file and line number of the statement that caused the test to fail. * Added "ad-hoc" test suite support. You can now invoke `bats` with multiple filename or directory arguments to run all the specified tests in aggregate. * Added support for test files with Windows line endings. * Fixed regular expression warnings from certain versions of Bash. * Fixed a bug running tests containing lines that begin with `-e`. *0.2.0* (November 16, 2012) * Added test suite support. The `bats` command accepts a directory name containing multiple test files to be run in aggregate. * Added the ability to count the number of test cases in a file or suite by passing the `-c` flag to `bats`. * Preprocessed sources are cached between test case runs in the same file for better performance. *0.1.0* (December 30, 2011) * Initial public release. --- © 2014 Sam Stephenson. Bats is released under an MIT-style license; see `LICENSE` for details. charliecloud-0.2.3~pre+1a5609e/test/bats/bin/000077500000000000000000000000001266165726200205425ustar00rootroot00000000000000charliecloud-0.2.3~pre+1a5609e/test/bats/bin/bats000077700000000000000000000000001266165726200241132../libexec/batsustar00rootroot00000000000000charliecloud-0.2.3~pre+1a5609e/test/bats/install.sh000077500000000000000000000013341266165726200220000ustar00rootroot00000000000000#!/usr/bin/env bash set -e resolve_link() { $(type -p greadlink readlink | head -1) "$1" } abs_dirname() { local cwd="$(pwd)" local path="$1" while [ -n "$path" ]; do cd "${path%/*}" local name="${path##*/}" path="$(resolve_link "$name" || true)" done pwd cd "$cwd" } PREFIX="$1" if [ -z "$1" ]; then { echo "usage: $0 " echo " e.g. $0 /usr/local" } >&2 exit 1 fi BATS_ROOT="$(abs_dirname "$0")" mkdir -p "$PREFIX"/{bin,libexec,share/man/man{1,7}} cp -R "$BATS_ROOT"/bin/* "$PREFIX"/bin cp -R "$BATS_ROOT"/libexec/* "$PREFIX"/libexec cp "$BATS_ROOT"/man/bats.1 "$PREFIX"/share/man/man1 cp "$BATS_ROOT"/man/bats.7 "$PREFIX"/share/man/man7 echo "Installed Bats to $PREFIX/bin/bats" charliecloud-0.2.3~pre+1a5609e/test/bats/libexec/000077500000000000000000000000001266165726200214055ustar00rootroot00000000000000charliecloud-0.2.3~pre+1a5609e/test/bats/libexec/bats000077500000000000000000000054201266165726200222650ustar00rootroot00000000000000#!/usr/bin/env bash set -e version() { echo "Bats 0.4.0" } usage() { version echo "Usage: bats [-c] [-p | -t] [ ...]" } help() { usage echo echo " is the path to a Bats test file, or the path to a directory" echo " containing Bats test files." echo echo " -c, --count Count the number of test cases without running any tests" echo " -h, --help Display this help message" echo " -p, --pretty Show results in pretty format (default for terminals)" echo " -t, --tap Show results in TAP format" echo " -v, --version Display the version number" echo echo " For more information, see https://github.com/sstephenson/bats" echo } resolve_link() { $(type -p greadlink readlink | head -1) "$1" } abs_dirname() { local cwd="$(pwd)" local path="$1" while [ -n "$path" ]; do cd "${path%/*}" local name="${path##*/}" path="$(resolve_link "$name" || true)" done pwd cd "$cwd" } expand_path() { { cd "$(dirname "$1")" 2>/dev/null local dirname="$PWD" cd "$OLDPWD" echo "$dirname/$(basename "$1")" } || echo "$1" } BATS_LIBEXEC="$(abs_dirname "$0")" export BATS_PREFIX="$(abs_dirname "$BATS_LIBEXEC")" export BATS_CWD="$(abs_dirname .)" export PATH="$BATS_LIBEXEC:$PATH" options=() arguments=() for arg in "$@"; do if [ "${arg:0:1}" = "-" ]; then if [ "${arg:1:1}" = "-" ]; then options[${#options[*]}]="${arg:2}" else index=1 while option="${arg:$index:1}"; do [ -n "$option" ] || break options[${#options[*]}]="$option" let index+=1 done fi else arguments[${#arguments[*]}]="$arg" fi done unset count_flag pretty [ -t 0 ] && [ -t 1 ] && pretty="1" [ -n "$CI" ] && pretty="" for option in "${options[@]}"; do case "$option" in "h" | "help" ) help exit 0 ;; "v" | "version" ) version exit 0 ;; "c" | "count" ) count_flag="-c" ;; "t" | "tap" ) pretty="" ;; "p" | "pretty" ) pretty="1" ;; * ) usage >&2 exit 1 ;; esac done if [ "${#arguments[@]}" -eq 0 ]; then usage >&2 exit 1 fi filenames=() for filename in "${arguments[@]}"; do if [ -d "$filename" ]; then shopt -s nullglob for suite_filename in "$(expand_path "$filename")"/*.bats; do filenames["${#filenames[@]}"]="$suite_filename" done shopt -u nullglob else filenames["${#filenames[@]}"]="$(expand_path "$filename")" fi done if [ "${#filenames[@]}" -eq 1 ]; then command="bats-exec-test" else command="bats-exec-suite" fi if [ -n "$pretty" ]; then extended_syntax_flag="-x" formatter="bats-format-tap-stream" else extended_syntax_flag="" formatter="cat" fi set -o pipefail execfail exec "$command" $count_flag $extended_syntax_flag "${filenames[@]}" | "$formatter" charliecloud-0.2.3~pre+1a5609e/test/bats/libexec/bats-exec-suite000077500000000000000000000017511266165726200243410ustar00rootroot00000000000000#!/usr/bin/env bash set -e count_only_flag="" if [ "$1" = "-c" ]; then count_only_flag=1 shift fi extended_syntax_flag="" if [ "$1" = "-x" ]; then extended_syntax_flag="-x" shift fi trap "kill 0; exit 1" int count=0 for filename in "$@"; do let count+="$(bats-exec-test -c "$filename")" done if [ -n "$count_only_flag" ]; then echo "$count" exit fi echo "1..$count" status=0 offset=0 for filename in "$@"; do index=0 { IFS= read -r # 1..n while IFS= read -r line; do case "$line" in "begin "* ) let index+=1 echo "${line/ $index / $(($offset + $index)) }" ;; "ok "* | "not ok "* ) [ -n "$extended_syntax_flag" ] || let index+=1 echo "${line/ $index / $(($offset + $index)) }" [ "${line:0:6}" != "not ok" ] || status=1 ;; * ) echo "$line" ;; esac done } < <( bats-exec-test $extended_syntax_flag "$filename" ) offset=$(($offset + $index)) done exit "$status" charliecloud-0.2.3~pre+1a5609e/test/bats/libexec/bats-exec-test000077500000000000000000000161341266165726200241700ustar00rootroot00000000000000#!/usr/bin/env bash set -e set -E set -T BATS_COUNT_ONLY="" if [ "$1" = "-c" ]; then BATS_COUNT_ONLY=1 shift fi BATS_EXTENDED_SYNTAX="" if [ "$1" = "-x" ]; then BATS_EXTENDED_SYNTAX="$1" shift fi BATS_TEST_FILENAME="$1" if [ -z "$BATS_TEST_FILENAME" ]; then echo "usage: bats-exec " >&2 exit 1 elif [ ! -f "$BATS_TEST_FILENAME" ]; then echo "bats: $BATS_TEST_FILENAME does not exist" >&2 exit 1 else shift fi BATS_TEST_DIRNAME="$(dirname "$BATS_TEST_FILENAME")" BATS_TEST_NAMES=() load() { local name="$1" local filename if [ "${name:0:1}" = "/" ]; then filename="${name}" else filename="$BATS_TEST_DIRNAME/${name}.bash" fi [ -f "$filename" ] || { echo "bats: $filename does not exist" >&2 exit 1 } source "${filename}" } run() { local e E T oldIFS [[ ! "$-" =~ e ]] || e=1 [[ ! "$-" =~ E ]] || E=1 [[ ! "$-" =~ T ]] || T=1 set +e set +E set +T output="$("$@" 2>&1)" status="$?" oldIFS=$IFS IFS=$'\n' lines=($output) [ -z "$e" ] || set -e [ -z "$E" ] || set -E [ -z "$T" ] || set -T IFS=$oldIFS } setup() { true } teardown() { true } skip() { BATS_TEST_SKIPPED=${1:-1} BATS_TEST_COMPLETED=1 exit 0 } bats_test_begin() { BATS_TEST_DESCRIPTION="$1" if [ -n "$BATS_EXTENDED_SYNTAX" ]; then echo "begin $BATS_TEST_NUMBER $BATS_TEST_DESCRIPTION" >&3 fi setup } bats_test_function() { local test_name="$1" BATS_TEST_NAMES["${#BATS_TEST_NAMES[@]}"]="$test_name" } bats_capture_stack_trace() { BATS_PREVIOUS_STACK_TRACE=( "${BATS_CURRENT_STACK_TRACE[@]}" ) BATS_CURRENT_STACK_TRACE=() local test_pattern=" $BATS_TEST_NAME $BATS_TEST_SOURCE" local setup_pattern=" setup $BATS_TEST_SOURCE" local teardown_pattern=" teardown $BATS_TEST_SOURCE" local frame local index=1 while frame="$(caller "$index")"; do BATS_CURRENT_STACK_TRACE["${#BATS_CURRENT_STACK_TRACE[@]}"]="$frame" if [[ "$frame" = *"$test_pattern" || \ "$frame" = *"$setup_pattern" || \ "$frame" = *"$teardown_pattern" ]]; then break else let index+=1 fi done BATS_SOURCE="$(bats_frame_filename "${BATS_CURRENT_STACK_TRACE[0]}")" BATS_LINENO="$(bats_frame_lineno "${BATS_CURRENT_STACK_TRACE[0]}")" } bats_print_stack_trace() { local frame local index=1 local count="${#@}" for frame in "$@"; do local filename="$(bats_trim_filename "$(bats_frame_filename "$frame")")" local lineno="$(bats_frame_lineno "$frame")" if [ $index -eq 1 ]; then echo -n "# (" else echo -n "# " fi local fn="$(bats_frame_function "$frame")" if [ "$fn" != "$BATS_TEST_NAME" ]; then echo -n "from function \`$fn' " fi if [ $index -eq $count ]; then echo "in test file $filename, line $lineno)" else echo "in file $filename, line $lineno," fi let index+=1 done } bats_print_failed_command() { local frame="$1" local status="$2" local filename="$(bats_frame_filename "$frame")" local lineno="$(bats_frame_lineno "$frame")" local failed_line="$(bats_extract_line "$filename" "$lineno")" local failed_command="$(bats_strip_string "$failed_line")" echo -n "# \`${failed_command}' " if [ $status -eq 1 ]; then echo "failed" else echo "failed with status $status" fi } bats_frame_lineno() { local frame="$1" local lineno="${frame%% *}" echo "$lineno" } bats_frame_function() { local frame="$1" local rest="${frame#* }" local fn="${rest%% *}" echo "$fn" } bats_frame_filename() { local frame="$1" local rest="${frame#* }" local filename="${rest#* }" if [ "$filename" = "$BATS_TEST_SOURCE" ]; then echo "$BATS_TEST_FILENAME" else echo "$filename" fi } bats_extract_line() { local filename="$1" local lineno="$2" sed -n "${lineno}p" "$filename" } bats_strip_string() { local string="$1" printf "%s" "$string" | sed -e "s/^[ "$'\t'"]*//" -e "s/[ "$'\t'"]*$//" } bats_trim_filename() { local filename="$1" local length="${#BATS_CWD}" if [ "${filename:0:length+1}" = "${BATS_CWD}/" ]; then echo "${filename:length+1}" else echo "$filename" fi } bats_debug_trap() { if [ "$BASH_SOURCE" != "$1" ]; then bats_capture_stack_trace fi } bats_error_trap() { BATS_ERROR_STATUS="$?" BATS_ERROR_STACK_TRACE=( "${BATS_PREVIOUS_STACK_TRACE[@]}" ) trap - debug } bats_teardown_trap() { trap "bats_exit_trap" exit local status=0 teardown >>"$BATS_OUT" 2>&1 || status="$?" if [ $status -eq 0 ]; then BATS_TEARDOWN_COMPLETED=1 elif [ -n "$BATS_TEST_COMPLETED" ]; then BATS_ERROR_STATUS="$status" BATS_ERROR_STACK_TRACE=( "${BATS_CURRENT_STACK_TRACE[@]}" ) fi bats_exit_trap } bats_exit_trap() { local status local skipped trap - err exit skipped="" if [ -n "$BATS_TEST_SKIPPED" ]; then skipped=" # skip" if [ "1" != "$BATS_TEST_SKIPPED" ]; then skipped+=" ($BATS_TEST_SKIPPED)" fi fi if [ -z "$BATS_TEST_COMPLETED" ] || [ -z "$BATS_TEARDOWN_COMPLETED" ]; then echo "not ok $BATS_TEST_NUMBER $BATS_TEST_DESCRIPTION" >&3 bats_print_stack_trace "${BATS_ERROR_STACK_TRACE[@]}" >&3 bats_print_failed_command "${BATS_ERROR_STACK_TRACE[${#BATS_ERROR_STACK_TRACE[@]}-1]}" "$BATS_ERROR_STATUS" >&3 sed -e "s/^/# /" < "$BATS_OUT" >&3 status=1 else echo "ok ${BATS_TEST_NUMBER}${skipped} ${BATS_TEST_DESCRIPTION}" >&3 status=0 fi rm -f "$BATS_OUT" exit "$status" } bats_perform_tests() { echo "1..$#" test_number=1 status=0 for test_name in "$@"; do "$0" $BATS_EXTENDED_SYNTAX "$BATS_TEST_FILENAME" "$test_name" "$test_number" || status=1 let test_number+=1 done exit "$status" } bats_perform_test() { BATS_TEST_NAME="$1" if [ "$(type -t "$BATS_TEST_NAME" || true)" = "function" ]; then BATS_TEST_NUMBER="$2" if [ -z "$BATS_TEST_NUMBER" ]; then echo "1..1" BATS_TEST_NUMBER="1" fi BATS_TEST_COMPLETED="" BATS_TEARDOWN_COMPLETED="" trap "bats_debug_trap \"\$BASH_SOURCE\"" debug trap "bats_error_trap" err trap "bats_teardown_trap" exit "$BATS_TEST_NAME" >>"$BATS_OUT" 2>&1 BATS_TEST_COMPLETED=1 else echo "bats: unknown test name \`$BATS_TEST_NAME'" >&2 exit 1 fi } if [ -z "$TMPDIR" ]; then BATS_TMPDIR="/tmp" else BATS_TMPDIR="${TMPDIR%/}" fi BATS_TMPNAME="$BATS_TMPDIR/bats.$$" BATS_PARENT_TMPNAME="$BATS_TMPDIR/bats.$PPID" BATS_OUT="${BATS_TMPNAME}.out" bats_preprocess_source() { BATS_TEST_SOURCE="${BATS_TMPNAME}.src" { tr -d '\r' < "$BATS_TEST_FILENAME"; echo; } | bats-preprocess > "$BATS_TEST_SOURCE" trap "bats_cleanup_preprocessed_source" err exit trap "bats_cleanup_preprocessed_source; exit 1" int } bats_cleanup_preprocessed_source() { rm -f "$BATS_TEST_SOURCE" } bats_evaluate_preprocessed_source() { if [ -z "$BATS_TEST_SOURCE" ]; then BATS_TEST_SOURCE="${BATS_PARENT_TMPNAME}.src" fi source "$BATS_TEST_SOURCE" } exec 3<&1 if [ "$#" -eq 0 ]; then bats_preprocess_source bats_evaluate_preprocessed_source if [ -n "$BATS_COUNT_ONLY" ]; then echo "${#BATS_TEST_NAMES[@]}" else bats_perform_tests "${BATS_TEST_NAMES[@]}" fi else bats_evaluate_preprocessed_source bats_perform_test "$@" fi charliecloud-0.2.3~pre+1a5609e/test/bats/libexec/bats-format-tap-stream000077500000000000000000000052421266165726200256300ustar00rootroot00000000000000#!/usr/bin/env bash set -e # Just stream the TAP output (sans extended syntax) if tput is missing command -v tput >/dev/null || exec grep -v "^begin " header_pattern='[0-9]+\.\.[0-9]+' IFS= read -r header if [[ "$header" =~ $header_pattern ]]; then count="${header:3}" index=0 failures=0 skipped=0 name="" count_column_width=$(( ${#count} * 2 + 2 )) else # If the first line isn't a TAP plan, print it and pass the rest through printf "%s\n" "$header" exec cat fi update_screen_width() { screen_width="$(tput cols)" count_column_left=$(( $screen_width - $count_column_width )) } trap update_screen_width WINCH update_screen_width begin() { go_to_column 0 printf_with_truncation $(( $count_column_left - 1 )) " %s" "$name" clear_to_end_of_line go_to_column $count_column_left printf "%${#count}s/${count}" "$index" go_to_column 1 } pass() { go_to_column 0 printf " ✓ %s" "$name" advance } skip() { local reason="$1" [ -z "$reason" ] || reason=": $reason" go_to_column 0 printf " - %s (skipped%s)" "$name" "$reason" advance } fail() { go_to_column 0 set_color 1 bold printf " ✗ %s" "$name" advance } log() { set_color 1 printf " %s\n" "$1" clear_color } summary() { printf "\n%d test%s" "$count" "$(plural "$count")" printf ", %d failure%s" "$failures" "$(plural "$failures")" if [ "$skipped" -gt 0 ]; then printf ", %d skipped" "$skipped" fi printf "\n" } printf_with_truncation() { local width="$1" shift local string="$(printf "$@")" if [ "${#string}" -gt "$width" ]; then printf "%s..." "${string:0:$(( $width - 4 ))}" else printf "%s" "$string" fi } go_to_column() { local column="$1" printf "\x1B[%dG" $(( $column + 1 )) } clear_to_end_of_line() { printf "\x1B[K" } advance() { clear_to_end_of_line echo clear_color } set_color() { local color="$1" local weight="$2" printf "\x1B[%d;%dm" $(( 30 + $color )) "$( [ "$weight" = "bold" ] && echo 1 || echo 22 )" } clear_color() { printf "\x1B[0m" } plural() { [ "$1" -eq 1 ] || echo "s" } _buffer="" buffer() { _buffer="${_buffer}$("$@")" } flush() { printf "%s" "$_buffer" _buffer="" } finish() { flush printf "\n" } trap finish EXIT while IFS= read -r line; do case "$line" in "begin "* ) let index+=1 name="${line#* $index }" buffer begin flush ;; "ok "* ) skip_expr="ok $index # skip (\(([^)]*)\))?" if [[ "$line" =~ $skip_expr ]]; then let skipped+=1 buffer skip "${BASH_REMATCH[2]}" else buffer pass fi ;; "not ok "* ) let failures+=1 buffer fail ;; "# "* ) buffer log "${line:2}" ;; esac done buffer summary charliecloud-0.2.3~pre+1a5609e/test/bats/libexec/bats-preprocess000077500000000000000000000021211266165726200244430ustar00rootroot00000000000000#!/usr/bin/env bash set -e encode_name() { local name="$1" local result="test_" if [[ ! "$name" =~ [^[:alnum:]\ _-] ]]; then name="${name//_/-5f}" name="${name//-/-2d}" name="${name// /_}" result+="$name" else local length="${#name}" local char i for ((i=0; i [ ...] is the path to a Bats test file, or the path to a directory containing Bats test files. DESCRIPTION ----------- Bats is a TAP-compliant testing framework for Bash. It provides a simple way to verify that the UNIX programs you write behave as expected. A Bats test file is a Bash script with special syntax for defining test cases. Under the hood, each test case is just a function with a description. Test cases consist of standard shell commands. Bats makes use of Bash's `errexit` (`set -e`) option when running test cases. If every command in the test case exits with a `0` status code (success), the test passes. In this way, each line is an assertion of truth. See `bats`(7) for more information on writing Bats tests. RUNNING TESTS ------------- To run your tests, invoke the `bats` interpreter with a path to a test file. The file's test cases are run sequentially and in isolation. If all the test cases pass, `bats` exits with a `0` status code. If there are any failures, `bats` exits with a `1` status code. You can invoke the `bats` interpreter with multiple test file arguments, or with a path to a directory containing multiple `.bats` files. Bats will run each test file individually and aggregate the results. If any test case fails, `bats` exits with a `1` status code. OPTIONS ------- * `-c`, `--count`: Count the number of test cases without running any tests * `-h`, `--help`: Display help message * `-p`, `--pretty`: Show results in pretty format (default for terminals) * `-t`, `--tap`: Show results in TAP format * `-v`, `--version`: Display the version number OUTPUT ------ When you run Bats from a terminal, you'll see output as each test is performed, with a check-mark next to the test's name if it passes or an "X" if it fails. $ bats addition.bats ✓ addition using bc ✓ addition using dc 2 tests, 0 failures If Bats is not connected to a terminal--in other words, if you run it from a continuous integration system or redirect its output to a file--the results are displayed in human-readable, machine-parsable TAP format. You can force TAP output from a terminal by invoking Bats with the `--tap` option. $ bats --tap addition.bats 1..2 ok 1 addition using bc ok 2 addition using dc EXIT STATUS ----------- The `bats` interpreter exits with a value of `0` if all test cases pass, or `1` if one or more test cases fail. SEE ALSO -------- Bats wiki: _https://github.com/sstephenson/bats/wiki/_ `bash`(1), `bats`(7) COPYRIGHT --------- (c) 2014 Sam Stephenson Bats is released under the terms of an MIT-style license. charliecloud-0.2.3~pre+1a5609e/test/bats/man/bats.7000066400000000000000000000114371266165726200215740ustar00rootroot00000000000000.\" generated with Ronn/v0.7.3 .\" http://github.com/rtomayko/ronn/tree/0.7.3 . .TH "BATS" "7" "November 2013" "" "" . .SH "NAME" \fBbats\fR \- Bats test file format . .SH "DESCRIPTION" A Bats test file is a Bash script with special syntax for defining test cases\. Under the hood, each test case is just a function with a description\. . .IP "" 4 . .nf #!/usr/bin/env bats @test "addition using bc" { result="$(echo 2+2 | bc)" [ "$result" \-eq 4 ] } @test "addition using dc" { result="$(echo 2 2+p | dc)" [ "$result" \-eq 4 ] } . .fi . .IP "" 0 . .P Each Bats test file is evaluated n+1 times, where \fIn\fR is the number of test cases in the file\. The first run counts the number of test cases, then iterates over the test cases and executes each one in its own process\. . .SH "THE RUN HELPER" Many Bats tests need to run a command and then make assertions about its exit status and output\. Bats includes a \fBrun\fR helper that invokes its arguments as a command, saves the exit status and output into special global variables, and then returns with a \fB0\fR status code so you can continue to make assertions in your test case\. . .P For example, let\'s say you\'re testing that the \fBfoo\fR command, when passed a nonexistent filename, exits with a \fB1\fR status code and prints an error message\. . .IP "" 4 . .nf @test "invoking foo with a nonexistent file prints an error" { run foo nonexistent_filename [ "$status" \-eq 1 ] [ "$output" = "foo: no such file \'nonexistent_filename\'" ] } . .fi . .IP "" 0 . .P The \fB$status\fR variable contains the status code of the command, and the \fB$output\fR variable contains the combined contents of the command\'s standard output and standard error streams\. . .P A third special variable, the \fB$lines\fR array, is available for easily accessing individual lines of output\. For example, if you want to test that invoking \fBfoo\fR without any arguments prints usage information on the first line: . .IP "" 4 . .nf @test "invoking foo without arguments prints usage" { run foo [ "$status" \-eq 1 ] [ "${lines[0]}" = "usage: foo " ] } . .fi . .IP "" 0 . .SH "THE LOAD COMMAND" You may want to share common code across multiple test files\. Bats includes a convenient \fBload\fR command for sourcing a Bash source file relative to the location of the current test file\. For example, if you have a Bats test in \fBtest/foo\.bats\fR, the command . .IP "" 4 . .nf load test_helper . .fi . .IP "" 0 . .P will source the script \fBtest/test_helper\.bash\fR in your test file\. This can be useful for sharing functions to set up your environment or load fixtures\. . .SH "THE SKIP COMMAND" Tests can be skipped by using the \fBskip\fR command at the point in a test you wish to skip\. . .IP "" 4 . .nf @test "A test I don\'t want to execute for now" { skip run foo [ "$status" \-eq 0 ] } . .fi . .IP "" 0 . .P Optionally, you may include a reason for skipping: . .IP "" 4 . .nf @test "A test I don\'t want to execute for now" { skip "This command will return zero soon, but not now" run foo [ "$status" \-eq 0 ] } . .fi . .IP "" 0 . .P Or you can skip conditionally: . .IP "" 4 . .nf @test "A test which should run" { if [ foo != bar ]; then skip "foo isn\'t bar" fi run foo [ "$status" \-eq 0 ] } . .fi . .IP "" 0 . .SH "SETUP AND TEARDOWN FUNCTIONS" You can define special \fBsetup\fR and \fBteardown\fR functions which run before and after each test case, respectively\. Use these to load fixtures, set up your environment, and clean up when you\'re done\. . .SH "CODE OUTSIDE OF TEST CASES" You can include code in your test file outside of \fB@test\fR functions\. For example, this may be useful if you want to check for dependencies and fail immediately if they\'re not present\. However, any output that you print in code outside of \fB@test\fR, \fBsetup\fR or \fBteardown\fR functions must be redirected to \fBstderr\fR (\fB>&2\fR)\. Otherwise, the output may cause Bats to fail by polluting the TAP stream on \fBstdout\fR\. . .SH "SPECIAL VARIABLES" There are several global variables you can use to introspect on Bats tests: . .IP "\(bu" 4 \fB$BATS_TEST_FILENAME\fR is the fully expanded path to the Bats test file\. . .IP "\(bu" 4 \fB$BATS_TEST_DIRNAME\fR is the directory in which the Bats test file is located\. . .IP "\(bu" 4 \fB$BATS_TEST_NAMES\fR is an array of function names for each test case\. . .IP "\(bu" 4 \fB$BATS_TEST_NAME\fR is the name of the function containing the current test case\. . .IP "\(bu" 4 \fB$BATS_TEST_DESCRIPTION\fR is the description of the current test case\. . .IP "\(bu" 4 \fB$BATS_TEST_NUMBER\fR is the (1\-based) index of the current test case in the test file\. . .IP "\(bu" 4 \fB$BATS_TMPDIR\fR is the location to a directory that may be used to store temporary files\. . .IP "" 0 . .SH "SEE ALSO" \fBbash\fR(1), \fBbats\fR(1) charliecloud-0.2.3~pre+1a5609e/test/bats/man/bats.7.ronn000066400000000000000000000106361266165726200225470ustar00rootroot00000000000000bats(7) -- Bats test file format ================================ DESCRIPTION ----------- A Bats test file is a Bash script with special syntax for defining test cases. Under the hood, each test case is just a function with a description. #!/usr/bin/env bats @test "addition using bc" { result="$(echo 2+2 | bc)" [ "$result" -eq 4 ] } @test "addition using dc" { result="$(echo 2 2+p | dc)" [ "$result" -eq 4 ] } Each Bats test file is evaluated n+1 times, where _n_ is the number of test cases in the file. The first run counts the number of test cases, then iterates over the test cases and executes each one in its own process. THE RUN HELPER -------------- Many Bats tests need to run a command and then make assertions about its exit status and output. Bats includes a `run` helper that invokes its arguments as a command, saves the exit status and output into special global variables, and then returns with a `0` status code so you can continue to make assertions in your test case. For example, let's say you're testing that the `foo` command, when passed a nonexistent filename, exits with a `1` status code and prints an error message. @test "invoking foo with a nonexistent file prints an error" { run foo nonexistent_filename [ "$status" -eq 1 ] [ "$output" = "foo: no such file 'nonexistent_filename'" ] } The `$status` variable contains the status code of the command, and the `$output` variable contains the combined contents of the command's standard output and standard error streams. A third special variable, the `$lines` array, is available for easily accessing individual lines of output. For example, if you want to test that invoking `foo` without any arguments prints usage information on the first line: @test "invoking foo without arguments prints usage" { run foo [ "$status" -eq 1 ] [ "${lines[0]}" = "usage: foo " ] } THE LOAD COMMAND ---------------- You may want to share common code across multiple test files. Bats includes a convenient `load` command for sourcing a Bash source file relative to the location of the current test file. For example, if you have a Bats test in `test/foo.bats`, the command load test_helper will source the script `test/test_helper.bash` in your test file. This can be useful for sharing functions to set up your environment or load fixtures. THE SKIP COMMAND ---------------- Tests can be skipped by using the `skip` command at the point in a test you wish to skip. @test "A test I don't want to execute for now" { skip run foo [ "$status" -eq 0 ] } Optionally, you may include a reason for skipping: @test "A test I don't want to execute for now" { skip "This command will return zero soon, but not now" run foo [ "$status" -eq 0 ] } Or you can skip conditionally: @test "A test which should run" { if [ foo != bar ]; then skip "foo isn't bar" fi run foo [ "$status" -eq 0 ] } SETUP AND TEARDOWN FUNCTIONS ---------------------------- You can define special `setup` and `teardown` functions which run before and after each test case, respectively. Use these to load fixtures, set up your environment, and clean up when you're done. CODE OUTSIDE OF TEST CASES -------------------------- You can include code in your test file outside of `@test` functions. For example, this may be useful if you want to check for dependencies and fail immediately if they're not present. However, any output that you print in code outside of `@test`, `setup` or `teardown` functions must be redirected to `stderr` (`>&2`). Otherwise, the output may cause Bats to fail by polluting the TAP stream on `stdout`. SPECIAL VARIABLES ----------------- There are several global variables you can use to introspect on Bats tests: * `$BATS_TEST_FILENAME` is the fully expanded path to the Bats test file. * `$BATS_TEST_DIRNAME` is the directory in which the Bats test file is located. * `$BATS_TEST_NAMES` is an array of function names for each test case. * `$BATS_TEST_NAME` is the name of the function containing the current test case. * `$BATS_TEST_DESCRIPTION` is the description of the current test case. * `$BATS_TEST_NUMBER` is the (1-based) index of the current test case in the test file. * `$BATS_TMPDIR` is the location to a directory that may be used to store temporary files. SEE ALSO -------- `bash`(1), `bats`(1) charliecloud-0.2.3~pre+1a5609e/test/bats/package.json000066400000000000000000000004611266165726200222610ustar00rootroot00000000000000{ "name": "bats", "version": "v0.4.0", "description": "Bash Automated Testing System", "install": "./install.sh ${PREFIX:-/usr/local}", "scripts": [ "libexec/bats", "libexec/bats-exec-suite", "libexec/bats-exec-test", "libexec/bats-format-tap-stream", "libexec/bats-preprocess", "bin/bats" ] } charliecloud-0.2.3~pre+1a5609e/test/bats/test/000077500000000000000000000000001266165726200207515ustar00rootroot00000000000000charliecloud-0.2.3~pre+1a5609e/test/bats/test/bats.bats000077500000000000000000000174151266165726200225700ustar00rootroot00000000000000#!/usr/bin/env bats load test_helper fixtures bats @test "no arguments prints usage instructions" { run bats [ $status -eq 1 ] [ $(expr "${lines[1]}" : "Usage:") -ne 0 ] } @test "-v and --version print version number" { run bats -v [ $status -eq 0 ] [ $(expr "$output" : "Bats [0-9][0-9.]*") -ne 0 ] } @test "-h and --help print help" { run bats -h [ $status -eq 0 ] [ "${#lines[@]}" -gt 3 ] } @test "invalid filename prints an error" { run bats nonexistent [ $status -eq 1 ] [ $(expr "$output" : ".*does not exist") -ne 0 ] } @test "empty test file runs zero tests" { run bats "$FIXTURE_ROOT/empty.bats" [ $status -eq 0 ] [ "$output" = "1..0" ] } @test "one passing test" { run bats "$FIXTURE_ROOT/passing.bats" [ $status -eq 0 ] [ "${lines[0]}" = "1..1" ] [ "${lines[1]}" = "ok 1 a passing test" ] } @test "summary passing tests" { run filter_control_sequences bats -p $FIXTURE_ROOT/passing.bats [ $status -eq 0 ] [ "${lines[1]}" = "1 test, 0 failures" ] } @test "summary passing and skipping tests" { run filter_control_sequences bats -p $FIXTURE_ROOT/passing_and_skipping.bats [ $status -eq 0 ] [ "${lines[2]}" = "2 tests, 0 failures, 1 skipped" ] } @test "summary passing and failing tests" { run filter_control_sequences bats -p $FIXTURE_ROOT/failing_and_passing.bats [ $status -eq 0 ] [ "${lines[4]}" = "2 tests, 1 failure" ] } @test "summary passing, failing and skipping tests" { run filter_control_sequences bats -p $FIXTURE_ROOT/passing_failing_and_skipping.bats [ $status -eq 0 ] [ "${lines[5]}" = "3 tests, 1 failure, 1 skipped" ] } @test "one failing test" { run bats "$FIXTURE_ROOT/failing.bats" [ $status -eq 1 ] [ "${lines[0]}" = '1..1' ] [ "${lines[1]}" = 'not ok 1 a failing test' ] [ "${lines[2]}" = "# (in test file $RELATIVE_FIXTURE_ROOT/failing.bats, line 4)" ] [ "${lines[3]}" = "# \`eval \"( exit \${STATUS:-1} )\"' failed" ] } @test "one failing and one passing test" { run bats "$FIXTURE_ROOT/failing_and_passing.bats" [ $status -eq 1 ] [ "${lines[0]}" = '1..2' ] [ "${lines[1]}" = 'not ok 1 a failing test' ] [ "${lines[2]}" = "# (in test file $RELATIVE_FIXTURE_ROOT/failing_and_passing.bats, line 2)" ] [ "${lines[3]}" = "# \`false' failed" ] [ "${lines[4]}" = 'ok 2 a passing test' ] } @test "failing test with significant status" { STATUS=2 run bats "$FIXTURE_ROOT/failing.bats" [ $status -eq 1 ] [ "${lines[3]}" = "# \`eval \"( exit \${STATUS:-1} )\"' failed with status 2" ] } @test "failing helper function logs the test case's line number" { run bats "$FIXTURE_ROOT/failing_helper.bats" [ $status -eq 1 ] [ "${lines[1]}" = 'not ok 1 failing helper function' ] [ "${lines[2]}" = "# (from function \`failing_helper' in file $RELATIVE_FIXTURE_ROOT/test_helper.bash, line 6," ] [ "${lines[3]}" = "# in test file $RELATIVE_FIXTURE_ROOT/failing_helper.bats, line 5)" ] [ "${lines[4]}" = "# \`failing_helper' failed" ] } @test "test environments are isolated" { run bats "$FIXTURE_ROOT/environment.bats" [ $status -eq 0 ] } @test "setup is run once before each test" { rm -f "$TMP/setup.log" run bats "$FIXTURE_ROOT/setup.bats" [ $status -eq 0 ] run cat "$TMP/setup.log" [ ${#lines[@]} -eq 3 ] } @test "teardown is run once after each test, even if it fails" { rm -f "$TMP/teardown.log" run bats "$FIXTURE_ROOT/teardown.bats" [ $status -eq 1 ] run cat "$TMP/teardown.log" [ ${#lines[@]} -eq 3 ] } @test "setup failure" { run bats "$FIXTURE_ROOT/failing_setup.bats" [ $status -eq 1 ] [ "${lines[1]}" = 'not ok 1 truth' ] [ "${lines[2]}" = "# (from function \`setup' in test file $RELATIVE_FIXTURE_ROOT/failing_setup.bats, line 2)" ] [ "${lines[3]}" = "# \`false' failed" ] } @test "passing test with teardown failure" { PASS=1 run bats "$FIXTURE_ROOT/failing_teardown.bats" [ $status -eq 1 ] [ "${lines[1]}" = 'not ok 1 truth' ] [ "${lines[2]}" = "# (from function \`teardown' in test file $RELATIVE_FIXTURE_ROOT/failing_teardown.bats, line 2)" ] [ "${lines[3]}" = "# \`eval \"( exit \${STATUS:-1} )\"' failed" ] } @test "failing test with teardown failure" { PASS=0 run bats "$FIXTURE_ROOT/failing_teardown.bats" [ $status -eq 1 ] [ "${lines[1]}" = 'not ok 1 truth' ] [ "${lines[2]}" = "# (in test file $RELATIVE_FIXTURE_ROOT/failing_teardown.bats, line 6)" ] [ "${lines[3]}" = $'# `[ "$PASS" = "1" ]\' failed' ] } @test "teardown failure with significant status" { PASS=1 STATUS=2 run bats "$FIXTURE_ROOT/failing_teardown.bats" [ $status -eq 1 ] [ "${lines[3]}" = "# \`eval \"( exit \${STATUS:-1} )\"' failed with status 2" ] } @test "failing test file outside of BATS_CWD" { cd "$TMP" run bats "$FIXTURE_ROOT/failing.bats" [ $status -eq 1 ] [ "${lines[2]}" = "# (in test file $FIXTURE_ROOT/failing.bats, line 4)" ] } @test "load sources scripts relative to the current test file" { run bats "$FIXTURE_ROOT/load.bats" [ $status -eq 0 ] } @test "load aborts if the specified script does not exist" { HELPER_NAME="nonexistent" run bats "$FIXTURE_ROOT/load.bats" [ $status -eq 1 ] } @test "load sources scripts by absolute path" { HELPER_NAME="${FIXTURE_ROOT}/test_helper.bash" run bats "$FIXTURE_ROOT/load.bats" [ $status -eq 0 ] } @test "load aborts if the script, specified by an absolute path, does not exist" { HELPER_NAME="${FIXTURE_ROOT}/nonexistent" run bats "$FIXTURE_ROOT/load.bats" [ $status -eq 1 ] } @test "output is discarded for passing tests and printed for failing tests" { run bats "$FIXTURE_ROOT/output.bats" [ $status -eq 1 ] [ "${lines[6]}" = '# failure stdout 1' ] [ "${lines[7]}" = '# failure stdout 2' ] [ "${lines[11]}" = '# failure stderr' ] } @test "-c prints the number of tests" { run bats -c "$FIXTURE_ROOT/empty.bats" [ $status -eq 0 ] [ "$output" = "0" ] run bats -c "$FIXTURE_ROOT/output.bats" [ $status -eq 0 ] [ "$output" = "4" ] } @test "dash-e is not mangled on beginning of line" { run bats "$FIXTURE_ROOT/intact.bats" [ $status -eq 0 ] [ "${lines[1]}" = "ok 1 dash-e on beginning of line" ] } @test "dos line endings are stripped before testing" { run bats "$FIXTURE_ROOT/dos_line.bats" [ $status -eq 0 ] } @test "test file without trailing newline" { run bats "$FIXTURE_ROOT/without_trailing_newline.bats" [ $status -eq 0 ] [ "${lines[1]}" = "ok 1 truth" ] } @test "skipped tests" { run bats "$FIXTURE_ROOT/skipped.bats" [ $status -eq 0 ] [ "${lines[1]}" = "ok 1 # skip a skipped test" ] [ "${lines[2]}" = "ok 2 # skip (a reason) a skipped test with a reason" ] } @test "extended syntax" { run bats-exec-test -x "$FIXTURE_ROOT/failing_and_passing.bats" [ $status -eq 1 ] [ "${lines[1]}" = 'begin 1 a failing test' ] [ "${lines[2]}" = 'not ok 1 a failing test' ] [ "${lines[5]}" = 'begin 2 a passing test' ] [ "${lines[6]}" = 'ok 2 a passing test' ] } @test "pretty and tap formats" { run bats --tap "$FIXTURE_ROOT/passing.bats" tap_output="$output" [ $status -eq 0 ] run bats --pretty "$FIXTURE_ROOT/passing.bats" pretty_output="$output" [ $status -eq 0 ] [ "$tap_output" != "$pretty_output" ] } @test "pretty formatter bails on invalid tap" { run bats --tap "$FIXTURE_ROOT/invalid_tap.bats" [ $status -eq 1 ] [ "${lines[0]}" = "This isn't TAP!" ] [ "${lines[1]}" = "Good day to you" ] } @test "single-line tests" { run bats "$FIXTURE_ROOT/single_line.bats" [ $status -eq 1 ] [ "${lines[1]}" = 'ok 1 empty' ] [ "${lines[2]}" = 'ok 2 passing' ] [ "${lines[3]}" = 'ok 3 input redirection' ] [ "${lines[4]}" = 'not ok 4 failing' ] [ "${lines[5]}" = "# (in test file $RELATIVE_FIXTURE_ROOT/single_line.bats, line 9)" ] [ "${lines[6]}" = $'# `@test "failing" { false; }\' failed' ] } @test "testing IFS not modified by run" { run bats "$FIXTURE_ROOT/loop_keep_IFS.bats" [ $status -eq 0 ] [ "${lines[1]}" = "ok 1 loop_func" ] } charliecloud-0.2.3~pre+1a5609e/test/bats/test/fixtures/000077500000000000000000000000001266165726200226225ustar00rootroot00000000000000charliecloud-0.2.3~pre+1a5609e/test/bats/test/fixtures/bats/000077500000000000000000000000001266165726200235535ustar00rootroot00000000000000charliecloud-0.2.3~pre+1a5609e/test/bats/test/fixtures/bats/dos_line.bats000066400000000000000000000000431266165726200262170ustar00rootroot00000000000000@test "foo" { echo "foo" } charliecloud-0.2.3~pre+1a5609e/test/bats/test/fixtures/bats/empty.bats000066400000000000000000000000001266165726200255520ustar00rootroot00000000000000charliecloud-0.2.3~pre+1a5609e/test/bats/test/fixtures/bats/environment.bats000066400000000000000000000002121266165726200267650ustar00rootroot00000000000000@test "setting a variable" { variable=1 [ $variable -eq 1 ] } @test "variables do not persist across tests" { [ -z "$variable" ] } charliecloud-0.2.3~pre+1a5609e/test/bats/test/fixtures/bats/failing.bats000066400000000000000000000001101266165726200260270ustar00rootroot00000000000000@test "a failing test" { true true eval "( exit ${STATUS:-1} )" } charliecloud-0.2.3~pre+1a5609e/test/bats/test/fixtures/bats/failing_and_passing.bats000066400000000000000000000001061266165726200304020ustar00rootroot00000000000000@test "a failing test" { false } @test "a passing test" { true } charliecloud-0.2.3~pre+1a5609e/test/bats/test/fixtures/bats/failing_helper.bats000066400000000000000000000001201266165726200273670ustar00rootroot00000000000000load "test_helper" @test "failing helper function" { true failing_helper } charliecloud-0.2.3~pre+1a5609e/test/bats/test/fixtures/bats/failing_setup.bats000066400000000000000000000000561266165726200272600ustar00rootroot00000000000000setup() { false } @test "truth" { true } charliecloud-0.2.3~pre+1a5609e/test/bats/test/fixtures/bats/failing_teardown.bats000066400000000000000000000001251266165726200277400ustar00rootroot00000000000000teardown() { eval "( exit ${STATUS:-1} )" } @test "truth" { [ "$PASS" = "1" ] } charliecloud-0.2.3~pre+1a5609e/test/bats/test/fixtures/bats/intact.bats000066400000000000000000000001351266165726200257070ustar00rootroot00000000000000@test "dash-e on beginning of line" { run cat - <&2 } @test "failure writing to stdout" { echo "failure stdout 1" echo "failure stdout 2" false } @test "failure writing to stderr" { echo "failure stderr" >&2 false } charliecloud-0.2.3~pre+1a5609e/test/bats/test/fixtures/bats/passing.bats000066400000000000000000000000421266165726200260660ustar00rootroot00000000000000@test "a passing test" { true } charliecloud-0.2.3~pre+1a5609e/test/bats/test/fixtures/bats/passing_and_failing.bats000066400000000000000000000001061266165726200304020ustar00rootroot00000000000000@test "a passing test" { true } @test "a failing test" { false } charliecloud-0.2.3~pre+1a5609e/test/bats/test/fixtures/bats/passing_and_skipping.bats000066400000000000000000000001061266165726200306150ustar00rootroot00000000000000@test "a passing test" { true } @test "a skipping test" { skip } charliecloud-0.2.3~pre+1a5609e/test/bats/test/fixtures/bats/passing_failing_and_skipping.bats000066400000000000000000000001521266165726200323070ustar00rootroot00000000000000@test "a passing test" { true } @test "a skipping test" { skip } @test "a failing test" { false } charliecloud-0.2.3~pre+1a5609e/test/bats/test/fixtures/bats/setup.bats000066400000000000000000000003671266165726200255740ustar00rootroot00000000000000LOG="$TMP/setup.log" setup() { echo "$BATS_TEST_NAME" >> "$LOG" } @test "one" { [ "$(tail -n 1 "$LOG")" = "test_one" ] } @test "two" { [ "$(tail -n 1 "$LOG")" = "test_two" ] } @test "three" { [ "$(tail -n 1 "$LOG")" = "test_three" ] } charliecloud-0.2.3~pre+1a5609e/test/bats/test/fixtures/bats/single_line.bats000066400000000000000000000002201266165726200267100ustar00rootroot00000000000000@test "empty" { } @test "passing" { true; } @test "input redirection" { diff - <( echo hello ); } <> "$LOG" } @test "one" { true } @test "two" { false } @test "three" { true } charliecloud-0.2.3~pre+1a5609e/test/bats/test/fixtures/bats/test_helper.bash000066400000000000000000000000631266165726200267270ustar00rootroot00000000000000help_me() { true } failing_helper() { false } charliecloud-0.2.3~pre+1a5609e/test/bats/test/fixtures/bats/without_trailing_newline.bats000066400000000000000000000000301266165726200315340ustar00rootroot00000000000000@test "truth" { true }charliecloud-0.2.3~pre+1a5609e/test/bats/test/fixtures/suite/000077500000000000000000000000001266165726200237535ustar00rootroot00000000000000charliecloud-0.2.3~pre+1a5609e/test/bats/test/fixtures/suite/empty/000077500000000000000000000000001266165726200251115ustar00rootroot00000000000000charliecloud-0.2.3~pre+1a5609e/test/bats/test/fixtures/suite/empty/.gitkeep000066400000000000000000000000001266165726200265300ustar00rootroot00000000000000charliecloud-0.2.3~pre+1a5609e/test/bats/test/fixtures/suite/multiple/000077500000000000000000000000001266165726200256065ustar00rootroot00000000000000charliecloud-0.2.3~pre+1a5609e/test/bats/test/fixtures/suite/multiple/a.bats000066400000000000000000000000311266165726200266730ustar00rootroot00000000000000@test "truth" { true } charliecloud-0.2.3~pre+1a5609e/test/bats/test/fixtures/suite/multiple/b.bats000066400000000000000000000001111266165726200266730ustar00rootroot00000000000000@test "more truth" { true } @test "quasi-truth" { [ -z "$FLUNK" ] } charliecloud-0.2.3~pre+1a5609e/test/bats/test/fixtures/suite/single/000077500000000000000000000000001266165726200252345ustar00rootroot00000000000000charliecloud-0.2.3~pre+1a5609e/test/bats/test/fixtures/suite/single/test.bats000066400000000000000000000000421266165726200270620ustar00rootroot00000000000000@test "a passing test" { true } charliecloud-0.2.3~pre+1a5609e/test/bats/test/suite.bats000077500000000000000000000033271266165726200227650ustar00rootroot00000000000000#!/usr/bin/env bats load test_helper fixtures suite @test "running a suite with no test files" { run bats "$FIXTURE_ROOT/empty" [ $status -eq 0 ] [ "$output" = "1..0" ] } @test "running a suite with one test file" { run bats "$FIXTURE_ROOT/single" [ $status -eq 0 ] [ "${lines[0]}" = "1..1" ] [ "${lines[1]}" = "ok 1 a passing test" ] } @test "counting tests in a suite" { run bats -c "$FIXTURE_ROOT/single" [ $status -eq 0 ] [ "$output" -eq 1 ] run bats -c "$FIXTURE_ROOT/multiple" [ $status -eq 0 ] [ "$output" -eq 3 ] } @test "aggregated output of multiple tests in a suite" { run bats "$FIXTURE_ROOT/multiple" [ $status -eq 0 ] [ "${lines[0]}" = "1..3" ] echo "$output" | grep "^ok . truth" echo "$output" | grep "^ok . more truth" echo "$output" | grep "^ok . quasi-truth" } @test "a failing test in a suite results in an error exit code" { FLUNK=1 run bats "$FIXTURE_ROOT/multiple" [ $status -eq 1 ] [ "${lines[0]}" = "1..3" ] echo "$output" | grep "^not ok . quasi-truth" } @test "running an ad-hoc suite by specifying multiple test files" { run bats "$FIXTURE_ROOT/multiple/a.bats" "$FIXTURE_ROOT/multiple/b.bats" [ $status -eq 0 ] [ "${lines[0]}" = "1..3" ] echo "$output" | grep "^ok . truth" echo "$output" | grep "^ok . more truth" echo "$output" | grep "^ok . quasi-truth" } @test "extended syntax in suite" { FLUNK=1 run bats-exec-suite -x "$FIXTURE_ROOT/multiple/"*.bats [ $status -eq 1 ] [ "${lines[0]}" = "1..3" ] [ "${lines[1]}" = "begin 1 truth" ] [ "${lines[2]}" = "ok 1 truth" ] [ "${lines[3]}" = "begin 2 more truth" ] [ "${lines[4]}" = "ok 2 more truth" ] [ "${lines[5]}" = "begin 3 quasi-truth" ] [ "${lines[6]}" = "not ok 3 quasi-truth" ] } charliecloud-0.2.3~pre+1a5609e/test/bats/test/test_helper.bash000066400000000000000000000004601266165726200241260ustar00rootroot00000000000000fixtures() { FIXTURE_ROOT="$BATS_TEST_DIRNAME/fixtures/$1" RELATIVE_FIXTURE_ROOT="$(bats_trim_filename "$FIXTURE_ROOT")" } setup() { export TMP="$BATS_TEST_DIRNAME/tmp" } filter_control_sequences() { "$@" | sed $'s,\x1b\\[[0-9;]*[a-zA-Z],,g' } teardown() { [ -d "$TMP" ] && rm -f "$TMP"/* } charliecloud-0.2.3~pre+1a5609e/test/bats/test/tmp/000077500000000000000000000000001266165726200215515ustar00rootroot00000000000000charliecloud-0.2.3~pre+1a5609e/test/bats/test/tmp/.gitignore000066400000000000000000000000031266165726200235320ustar00rootroot00000000000000* charliecloud-0.2.3~pre+1a5609e/VERSION.full0000644000175000017500000000002213204627032020347 0ustar00lucaslucas000000000000000.2.3~pre+1a5609e