From bef7379541e246836ea93e503d6791f4d5540cf4 Mon Sep 17 00:00:00 2001 From: Julio Montes Date: Wed, 14 Mar 2018 11:25:41 -0600 Subject: [PATCH] cli: Add initial cli implementation. - Add kata-runtime - Add unit test - Add Makefile to build cli Fixes: #33 Signed-off-by: Julio Montes Signed-off-by: James O. D. Hunt Signed-off-by: Jose Carlos Venegas Munoz --- .ci/go-no-os-exit.sh | 23 + .ci/go-static-checks.sh | 99 +++ .ci/go-test.sh | 96 +++ cli/.gitignore | 8 + cli/Makefile | 586 ++++++++++++++ cli/VERSION | 1 + cli/arch/amd64-options.mk | 27 + cli/arch/arm64-options.mk | 21 + cli/config.go | 511 ++++++++++++ cli/config/configuration.toml.in | 142 ++++ cli/config_test.go | 1054 +++++++++++++++++++++++++ cli/console.go | 147 ++++ cli/console_test.go | 131 ++++ cli/create.go | 382 +++++++++ cli/create_test.go | 1197 +++++++++++++++++++++++++++++ cli/delete.go | 155 ++++ cli/delete_test.go | 597 ++++++++++++++ cli/exec.go | 273 +++++++ cli/exec_test.go | 696 +++++++++++++++++ cli/exit.go | 37 + cli/exit_test.go | 51 ++ cli/fatal.go | 80 ++ cli/kata-check.go | 304 ++++++++ cli/kata-check_amd64.go | 132 ++++ cli/kata-check_amd64_test.go | 466 +++++++++++ cli/kata-check_data_amd64_test.go | 58 ++ cli/kata-check_test.go | 698 +++++++++++++++++ cli/kata-env.go | 365 +++++++++ cli/kata-env_test.go | 969 +++++++++++++++++++++++ cli/kill.go | 159 ++++ cli/kill_test.go | 280 +++++++ cli/list.go | 392 ++++++++++ cli/list_test.go | 754 ++++++++++++++++++ cli/logger.go | 79 ++ cli/logger_test.go | 163 ++++ cli/main.go | 386 ++++++++++ cli/main_test.go | 1115 +++++++++++++++++++++++++++ cli/oci.go | 351 +++++++++ cli/oci_test.go | 623 +++++++++++++++ cli/pause.go | 66 ++ cli/pause_test.go | 154 ++++ cli/ps.go | 89 +++ cli/ps_test.go | 134 ++++ cli/run.go | 124 +++ cli/run_test.go | 655 ++++++++++++++++ cli/start.go | 75 ++ cli/start_test.go | 254 ++++++ cli/state.go | 64 ++ cli/state_test.go | 90 +++ cli/utils.go | 207 +++++ cli/utils_test.go | 422 ++++++++++ cli/version.go | 28 + cli/version_test.go | 65 ++ 53 files changed, 16035 insertions(+) create mode 100755 .ci/go-no-os-exit.sh create mode 100755 .ci/go-static-checks.sh create mode 100755 .ci/go-test.sh create mode 100644 cli/.gitignore create mode 100644 cli/Makefile create mode 100644 cli/VERSION create mode 100644 cli/arch/amd64-options.mk create mode 100644 cli/arch/arm64-options.mk create mode 100644 cli/config.go create mode 100644 cli/config/configuration.toml.in create mode 100644 cli/config_test.go create mode 100644 cli/console.go create mode 100644 cli/console_test.go create mode 100644 cli/create.go create mode 100644 cli/create_test.go create mode 100644 cli/delete.go create mode 100644 cli/delete_test.go create mode 100644 cli/exec.go create mode 100644 cli/exec_test.go create mode 100644 cli/exit.go create mode 100644 cli/exit_test.go create mode 100644 cli/fatal.go create mode 100644 cli/kata-check.go create mode 100644 cli/kata-check_amd64.go create mode 100644 cli/kata-check_amd64_test.go create mode 100644 cli/kata-check_data_amd64_test.go create mode 100644 cli/kata-check_test.go create mode 100644 cli/kata-env.go create mode 100644 cli/kata-env_test.go create mode 100644 cli/kill.go create mode 100644 cli/kill_test.go create mode 100644 cli/list.go create mode 100644 cli/list_test.go create mode 100644 cli/logger.go create mode 100644 cli/logger_test.go create mode 100644 cli/main.go create mode 100644 cli/main_test.go create mode 100644 cli/oci.go create mode 100644 cli/oci_test.go create mode 100644 cli/pause.go create mode 100644 cli/pause_test.go create mode 100644 cli/ps.go create mode 100644 cli/ps_test.go create mode 100644 cli/run.go create mode 100644 cli/run_test.go create mode 100644 cli/start.go create mode 100644 cli/start_test.go create mode 100644 cli/state.go create mode 100644 cli/state_test.go create mode 100644 cli/utils.go create mode 100644 cli/utils_test.go create mode 100644 cli/version.go create mode 100644 cli/version_test.go diff --git a/.ci/go-no-os-exit.sh b/.ci/go-no-os-exit.sh new file mode 100755 index 0000000000..51eac257ab --- /dev/null +++ b/.ci/go-no-os-exit.sh @@ -0,0 +1,23 @@ +#!/bin/bash +# Copyright (c) 2018 Intel Corporation +# +# SPDX-License-Identifier: Apache-2.0 + +go_packages=. + +candidates=`go list -f '{{.Dir}}/*.go' $go_packages` +for f in $candidates; do + filename=`basename $f` + # skip exit.go where, the only file we should call os.Exit() from. + [[ $filename == "exit.go" ]] && continue + # skip exit_test.go + [[ $filename == "exit_test.go" ]] && continue + # skip main_test.go + [[ $filename == "main_test.go" ]] && continue + files="$f $files" +done + +if egrep -n '\' $files; then + echo "Direct calls to os.Exit() are forbidden, please use exit() so atexit() works" + exit 1 +fi diff --git a/.ci/go-static-checks.sh b/.ci/go-static-checks.sh new file mode 100755 index 0000000000..8015d259fd --- /dev/null +++ b/.ci/go-static-checks.sh @@ -0,0 +1,99 @@ +#!/bin/bash +# Copyright (c) 2018 Intel Corporation +# +# SPDX-License-Identifier: Apache-2.0 + +set -e + +# Perform static go tests. + +function usage { + echo "Usage $0 [OPTIONS] [PACKAGES]" + echo "Perform static go checks on PACKAGES (./... by default)." + echo + echo "List of options:" + echo " -h, --help print this help" + echo " -n, --no-network do not access the network" +} + +for i in "$@"; do + case $i in + -h|--help) + usage + exit 0 + ;; + -n|--no-network) + NONETWORK=1 + shift + ;; + *) + args="$args $i" + ;; + esac +done + +go_packages=$args + +[ -z "$go_packages" ] && { + go_packages=$(go list ./... | grep -v vendor) +} + +function install_package { + url="$1" + name=${url##*/} + + if [ -n "$NONETWORK" ]; then + echo "Skipping updating package $name, no network access allowed" + return + fi + + echo Updating $name... + go get -u $url +} + +install_package github.com/fzipp/gocyclo +install_package github.com/client9/misspell/cmd/misspell +install_package github.com/golang/lint/golint +install_package github.com/gordonklaus/ineffassign +install_package github.com/opennota/check/cmd/structcheck +install_package honnef.co/go/tools/cmd/unused +install_package honnef.co/go/tools/cmd/staticcheck + +echo Doing go static checks on packages: $go_packages + +echo "Running misspell..." +go list -f '{{.Dir}}/*.go' $go_packages |\ + xargs -I % bash -c "misspell -error %" + +echo "Running go vet..." +go vet $go_packages + +cmd="gofmt -s -d -l" +echo "Running gofmt..." + +# Note: ignore git directory in case any refs end in ".go" too. +diff=$(find . -not -wholename '*/vendor/*' -not -wholename '*/.git/*' -name '*.go' | \ + xargs $cmd) +if [ -n "$diff" -a $(echo "$diff" | wc -l) -ne 0 ] +then + echo 2>&1 "ERROR: '$cmd' found problems:" + echo 2>&1 "$diff" + exit 1 +fi + +echo "Running cyclo..." +gocyclo -over 15 `go list -f '{{.Dir}}/*.go' $go_packages` + +echo "Running golint..." +for p in $go_packages; do golint -set_exit_status $p; done + +echo "Running ineffassign..." +go list -f '{{.Dir}}' $go_packages | xargs -L 1 ineffassign + +for tool in structcheck unused staticcheck +do + echo "Running ${tool}..." + eval "$tool" "$go_packages" +done + +echo "All Good!" diff --git a/.ci/go-test.sh b/.ci/go-test.sh new file mode 100755 index 0000000000..6fae379d86 --- /dev/null +++ b/.ci/go-test.sh @@ -0,0 +1,96 @@ +#!/bin/bash +# Copyright (c) 2018 Intel Corporation +# +# SPDX-License-Identifier: Apache-2.0 + +set -e + +script_dir=$(cd `dirname $0`; pwd) +root_dir=`dirname $script_dir` + +test_packages="." + +# Set default test run timeout value. +# +# CC_GO_TEST_TIMEOUT can be set to any value accepted by +# "go test -timeout X" +timeout_value=${CC_GO_TEST_TIMEOUT:-10s} + +go_test_flags="-v -race -timeout $timeout_value" +cov_file="profile.cov" +tmp_cov_file="profile_tmp.cov" + +# Run a command as either root or the current user (which might still be root). +# +# If the first argument is "root", run using sudo, else run as normal. +# All arguments after the first will be treated as the command to run. +function run_as_user +{ + user="$1" + shift + cmd=$* + + if [ "$user" = root ] + then + # use a shell to ensure PATH is correct. + sudo -E PATH="$PATH" sh -c "$cmd" + else + $cmd + fi +} + +function test_html_coverage +{ + html_report="coverage.html" + + test_coverage + + go tool cover -html="${cov_file}" -o "${html_report}" + rm -f "${cov_file}" + + run_as_user "current" chmod 644 "${html_report}" +} + +function test_coverage +{ + echo "mode: atomic" > "$cov_file" + + if [ $(id -u) -eq 0 ] + then + echo >&2 "WARNING: Already running as root so will not re-run tests as non-root user." + echo >&2 "WARNING: As a result, only a subset of tests will be run" + echo >&2 "WARNING: (run this script as a non-privileged to ensure all tests are run)." + users="current" + else + # Run the unit-tests *twice* (since some must run as root and + # others must run as non-root), combining the resulting test + # coverage files. + users="current root" + fi + + for pkg in $test_packages; do + for user in $users; do + printf "INFO: Running 'go test' as %s user on packages '%s' with flags '%s'\n" "$user" "$test_packages" "$go_test_flags" + + run_as_user "$user" go test $go_test_flags -covermode=atomic -coverprofile="$tmp_cov_file" $pkg + if [ -f "${tmp_cov_file}" ]; then + run_as_user "$user" chmod 644 "$tmp_cov_file" + tail -n +2 "$tmp_cov_file" >> "$cov_file" + run_as_user "$user" rm -f "$tmp_cov_file" + fi + done + done +} + +function test_local +{ + go test $go_test_flags $test_packages +} + +if [ "$1" = "html-coverage" ]; then + test_html_coverage +elif [ "$CI" = "true" ]; then + test_coverage +else + test_local +fi diff --git a/cli/.gitignore b/cli/.gitignore new file mode 100644 index 0000000000..5923111012 --- /dev/null +++ b/cli/.gitignore @@ -0,0 +1,8 @@ +/kata-runtime +/coverage.html +/config-generated.go +/data/kata-collect-data.sh +/data/kata-collect-data.sh.in +/data/completions/bash/kata-runtime +/data/completions/bash/kata-runtime.in +/config/configuration.toml diff --git a/cli/Makefile b/cli/Makefile new file mode 100644 index 0000000000..abddd60115 --- /dev/null +++ b/cli/Makefile @@ -0,0 +1,586 @@ +# Copyright (c) 2017-2018 Intel Corporation +# +# 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. + +MAKEFILE_DIR := $(shell dirname $(abspath $(lastword $(MAKEFILE_LIST)))) +# Determine the lower-case name of the distro +distro := $(shell \ +for file in /etc/os-release /usr/lib/os-release; do \ + if [ -e $$file ]; then \ + grep ^ID= $$file|cut -d= -f2-|tr -d '"'; \ + break; \ + fi \ +done) + +GOARCH=$(shell go env GOARCH) +HOST_ARCH=$(shell arch) + +ifeq ($(ARCH),) + ARCH = $(GOARCH) +endif + +ARCH_DIR = arch +ARCH_FILE_SUFFIX = -options.mk +ARCH_FILE = $(ARCH_DIR)/$(ARCH)$(ARCH_FILE_SUFFIX) +ARCH_FILES = $(wildcard arch/*$(ARCH_FILE_SUFFIX)) +ALL_ARCHES = $(patsubst $(ARCH_DIR)/%$(ARCH_FILE_SUFFIX),%,$(ARCH_FILES)) + +# Load architecture-dependent settings +include $(ARCH_FILE) + +#------------------------------ +# project-specifics + +# build type for Kata Containers +KATA_TYPE = kata + +KATA_PROJECT_NAME = Kata Containers +KATA_PROJECT_TAG = kata-containers +KATA_PROJECT_URL = https://github.com/kata-containers +KATA_BUG_URL = $(KATA_PROJECT_URL)/kata-containers/issues/new + +#------------------------------ + +# all supported project types +PROJECT_TYPES = $(KATA_TYPE) + +# If this environment variable is set to any value, +# enable the Kata Containers system build. +ifneq (,$(KATA_SYSTEM_BUILD)) + system_build_type = $(KATA_TYPE) +endif + +ifneq ($(SYSTEM_BUILD_TYPE),) + ifeq ($(SYSTEM_BUILD_TYPE),$(KATA_TYPE)) + system_build_type = $(KATA_TYPE) + endif +endif + +# A particular project system build is either triggered by +# specifying a special target or via an environment variable. +kata_system_build_requested := $(foreach f,\ + build-$(KATA_TYPE)-system install-$(KATA_TYPE)-system,\ + $(findstring $(f),$(MAKECMDGOALS))) +ifneq (,$(strip $(kata_system_build_requested))) + kata_system_build = yes +else + kata_system_build = no +endif + +ifeq ($(kata_system_build),yes) + system_build_type = $(KATA_TYPE) +endif + +# If this environment variable is set to a valid build type, +# use the value as the build type. + +ifeq ($(system_build_type),) + # Default built is for Kata + PROJECT_TYPE = $(KATA_TYPE) + + PROJECT_NAME = $(KATA_PROJECT_NAME) + PROJECT_TAG = $(KATA_PROJECT_TAG) + PROJECT_URL = $(KATA_PROJECT_URL) + PROJECT_BUG_URL = $(KATA_BUG_URL) +else + ifeq ($(system_build_type),$(KATA_TYPE)) + PROJECT_TYPE = $(KATA_TYPE) + + PROJECT_NAME = $(KATA_PROJECT_NAME) + PROJECT_TAG = $(KATA_PROJECT_TAG) + PROJECT_URL = $(KATA_PROJECT_URL) + PROJECT_BUG_URL = $(KATA_BUG_URL) + endif +endif + +BIN_PREFIX = $(PROJECT_TYPE) +PROJECT_DIR = $(PROJECT_TAG) +IMAGENAME = $(PROJECT_TAG).img + +TARGET = $(BIN_PREFIX)-runtime +DESTDIR := + +installing = $(findstring install,$(MAKECMDGOALS)) + +ifneq ($(system_build_type),) + # Configure the build for a standard system that is + # using OBS-generated packages. + PREFIX := /usr + BINDIR := $(PREFIX)/bin + DESTBINDIR := /usr/local/bin + QEMUBINDIR := $(BINDIR) + SYSCONFDIR := /etc + LOCALSTATEDIR := /var + + ifeq (,$(installing)) + # Force a rebuild to ensure version details are correct + # (but only for a non-install build phase). + EXTRA_DEPS = clean + endif +else + # standard build + PREFIX := /usr/local + BINDIR := $(PREFIX)/bin + DESTBINDIR := $(DESTDIR)/$(BINDIR) + QEMUBINDIR := $(BINDIR) + SYSCONFDIR := $(PREFIX)/etc + LOCALSTATEDIR := $(PREFIX)/var +endif + +LIBEXECDIR := $(PREFIX)/libexec +SHAREDIR := $(PREFIX)/share +DEFAULTSDIR := $(SHAREDIR)/defaults + +PKGDATADIR := $(SHAREDIR)/$(PROJECT_DIR) +PKGLIBDIR := $(LOCALSTATEDIR)/lib/$(PROJECT_DIR) +PKGRUNDIR := $(LOCALSTATEDIR)/run/$(PROJECT_DIR) +PKGLIBEXECDIR := $(LIBEXECDIR)/$(PROJECT_DIR) + +KERNELPATH := $(PKGDATADIR)/vmlinuz.container +IMAGEPATH := $(PKGDATADIR)/$(IMAGENAME) +FIRMWAREPATH := + +QEMUPATH := $(QEMUBINDIR)/$(QEMUCMD) + +SHIMCMD := $(BIN_PREFIX)-shim +SHIMPATH := $(PKGLIBEXECDIR)/$(SHIMCMD) + +PROXYCMD := $(BIN_PREFIX)-proxy +PROXYPATH := $(PKGLIBEXECDIR)/$(PROXYCMD) + +# Default number of vCPUs +DEFVCPUS := 1 +# Default memory size in MiB +DEFMEMSZ := 2048 +#Default number of bridges +DEFBRIDGES := 1 +#Default network model +DEFNETWORKMODEL := macvtap + +DEFDISABLEBLOCK := false +DEFBLOCKSTORAGEDRIVER := virtio-scsi +DEFENABLEMEMPREALLOC := false +DEFENABLEHUGEPAGES := false +DEFENABLESWAP := false +DEFENABLEDEBUG := false +DEFDISABLENESTINGCHECKS := false + +SED = sed + +SOURCES := $(shell find . 2>&1 | grep -E '.*\.(c|h|go)$$') +VERSION := ${shell cat ./VERSION} +COMMIT_NO := $(shell git rev-parse HEAD 2> /dev/null || true) +COMMIT := $(if $(shell git status --porcelain --untracked-files=no),${COMMIT_NO}-dirty,${COMMIT_NO}) + +CONFIG_FILE = configuration.toml +CONFIG = config/$(CONFIG_FILE) +CONFIG_IN = $(CONFIG).in + +DESTTARGET := $(abspath $(DESTBINDIR)/$(TARGET)) + +DESTCONFDIR := $(DESTDIR)/$(DEFAULTSDIR)/$(PROJECT_DIR) +DESTSYSCONFDIR := $(DESTDIR)/$(SYSCONFDIR)/$(PROJECT_DIR) + +# Main configuration file location for stateless systems +DESTCONFIG := $(abspath $(DESTCONFDIR)/$(CONFIG_FILE)) + +# Secondary configuration file location. Note that this takes precedence +# over DESTCONFIG. +DESTSYSCONFIG := $(abspath $(DESTSYSCONFDIR)/$(CONFIG_FILE)) + +DESTSHAREDIR := $(DESTDIR)/$(SHAREDIR) + +SCRIPTS_DIR := $(BINDIR) + +# list of variables the user may wish to override +USER_VARS += ARCH +USER_VARS += BASH_COMPLETIONSDIR +USER_VARS += BINDIR +USER_VARS += DESTCONFIG +USER_VARS += DESTDIR +USER_VARS += DESTSYSCONFIG +USER_VARS += DESTTARGET +USER_VARS += IMAGENAME +USER_VARS += IMAGEPATH +USER_VARS += MACHINETYPE +USER_VARS += KATA_SYSTEM_BUILD +USER_VARS += KERNELPATH +USER_VARS += FIRMWAREPATH +USER_VARS += MACHINEACCELERATORS +USER_VARS += KERNELPARAMS +USER_VARS += LIBEXECDIR +USER_VARS += LOCALSTATEDIR +USER_VARS += PKGDATADIR +USER_VARS += PKGLIBDIR +USER_VARS += PKGLIBEXECDIR +USER_VARS += PKGRUNDIR +USER_VARS += PREFIX +USER_VARS += PROJECT_NAME +USER_VARS += PROJECT_PREFIX +USER_VARS += PROJECT_TYPE +USER_VARS += PROXYPATH +USER_VARS += QEMUBINDIR +USER_VARS += QEMUCMD +USER_VARS += QEMUPATH +USER_VARS += SHAREDIR +USER_VARS += SHIMPATH +USER_VARS += SYSTEM_BUILD_TYPE +USER_VARS += SYSCONFDIR +USER_VARS += DEFVCPUS +USER_VARS += DEFMEMSZ +USER_VARS += DEFBRIDGES +USER_VARS += DEFNETWORKMODEL +USER_VARS += DEFDISABLEBLOCK +USER_VARS += DEFBLOCKSTORAGEDRIVER +USER_VARS += DEFENABLEMEMPREALLOC +USER_VARS += DEFENABLEHUGEPAGES +USER_VARS += DEFENABLESWAP +USER_VARS += DEFENABLEDEBUG +USER_VARS += DEFDISABLENESTINGCHECKS + +V = @ +Q = $(V:1=) +QUIET_BUILD = $(Q:@=@echo ' BUILD '$@;) +QUIET_CHECK = $(Q:@=@echo ' CHECK '$@;) +QUIET_CLEAN = $(Q:@=@echo ' CLEAN '$@;) +QUIET_CONFIG = $(Q:@=@echo ' CONFIG '$@;) +QUIET_GENERATE = $(Q:@=@echo ' GENERATE '$@;) +QUIET_INST = $(Q:@=@echo ' INSTALL '$@;) +QUIET_TEST = $(Q:@=@echo ' TEST '$@;) + +# Return non-empty string if specified directory exists +define DIR_EXISTS +$(shell test -d $(1) && echo "$(1)") +endef + +# $1: name of architecture to display +define SHOW_ARCH + $(shell printf "\\t%s%s\\\n" "$(1)" $(if $(filter $(ARCH),$(1))," (default)","")) +endef + +# Only install git hooks if working in a git clone +ifneq (,$(call DIR_EXISTS,.git)) + HANDLE_GIT_HOOKS = install-git-hooks +endif + +# Don't install hooks when running under the CI as they will stop the +# tests from running. +# +# See: https://github.com/clearcontainers/runtime/issues/984 +ifneq (,$(CI)) + HANDLE_GIT_HOOKS = +endif + +default: $(TARGET) $(CONFIG) $(HANDLE_GIT_HOOKS) +.DEFAULT: default + +build: default + +build-kata-system: default +install-kata-system: install + +define GENERATED_CODE +// WARNING: This file is auto-generated - DO NOT EDIT! +// +// Note that some variables are "var" to allow them to be modified +// by the tests. +package main + +import ( + "fmt" +) + +// name is the name of the runtime +const name = "$(TARGET)" + +// name of the project +const project = "$(PROJECT_NAME)" + +// prefix used to denote non-standard CLI commands and options. +const projectPrefix = "$(PROJECT_TYPE)" + +// systemdUnitName is the systemd(1) target used to launch the agent. +const systemdUnitName = "$(PROJECT_TAG).target" + +// original URL for this project +const projectURL = "$(PROJECT_URL)" + +// commit is the git commit the runtime is compiled from. +var commit = "$(COMMIT)" + +// version is the runtime version. +var version = "$(VERSION)" + +// project-specific command names +var envCmd = fmt.Sprintf("%s-env", projectPrefix) +var checkCmd = fmt.Sprintf("%s-check", projectPrefix) + +// project-specific option names +var configFilePathOption = fmt.Sprintf("%s-config", projectPrefix) +var showConfigPathsOption = fmt.Sprintf("%s-show-default-config-paths", projectPrefix) + +var defaultHypervisorPath = "$(QEMUPATH)" +var defaultImagePath = "$(IMAGEPATH)" +var defaultKernelPath = "$(KERNELPATH)" +var defaultFirmwarePath = "$(FIRMWAREPATH)" +var defaultMachineAccelerators = "$(MACHINEACCELERATORS)" +var defaultShimPath = "$(SHIMPATH)" + +const defaultKernelParams = "$(KERNELPARAMS)" +const defaultMachineType = "$(MACHINETYPE)" +const defaultRootDirectory = "$(PKGRUNDIR)" +const defaultRuntimeLib = "$(PKGLIBDIR)" +const defaultRuntimeRun = "$(PKGRUNDIR)" + +const defaultVCPUCount uint32 = $(DEFVCPUS) +const defaultMemSize uint32 = $(DEFMEMSZ) // MiB +const defaultBridgesCount uint32 = $(DEFBRIDGES) +const defaultInterNetworkingModel = "$(DEFNETWORKMODEL)" +const defaultDisableBlockDeviceUse bool = $(DEFDISABLEBLOCK) +const defaultBlockDeviceDriver = "$(DEFBLOCKSTORAGEDRIVER)" +const defaultEnableMemPrealloc bool = $(DEFENABLEMEMPREALLOC) +const defaultEnableHugePages bool = $(DEFENABLEHUGEPAGES) +const defaultEnableSwap bool = $(DEFENABLESWAP) +const defaultEnableDebug bool = $(DEFENABLEDEBUG) +const defaultDisableNestingChecks bool = $(DEFDISABLENESTINGCHECKS) + +// Default config file used by stateless systems. +var defaultRuntimeConfiguration = "$(DESTCONFIG)" + +// Alternate config file that takes precedence over +// defaultRuntimeConfiguration. +var defaultSysConfRuntimeConfiguration = "$(DESTSYSCONFIG)" + +var defaultProxyPath = "$(PROXYPATH)" +endef + +export GENERATED_CODE + +#Install an executable file +# params: +# $1 : file to install +# $2 : directory path where file will be installed +define INSTALL_EXEC + $(QUIET_INST)install -D $1 $(DESTDIR)$2/$(notdir $1); +endef + +GENERATED_GO_FILES += config-generated.go + +config-generated.go: Makefile VERSION + $(QUIET_GENERATE)echo "$$GENERATED_CODE" >$@ + +$(TARGET): $(EXTRA_DEPS) $(SOURCES) $(GENERATED_GO_FILES) $(GENERATED_FILES) Makefile | show-summary + $(QUIET_BUILD)go build -i -o $@ . + +.PHONY: \ + check \ + check-go-static \ + check-go-test \ + coverage \ + default \ + install \ + install-git-hooks \ + show-header \ + show-summary \ + show-variables + +$(TARGET).coverage: $(SOURCES) $(GENERATED_FILES) Makefile + $(QUIET_TEST)go test -o $@ -covermode count + +GENERATED_FILES += $(CONFIG) + +$(COLLECT_SCRIPT_PROJ): $(COLLECT_SCRIPT_SRC) + $(QUIET_GENERATE)cp $< $(@) + +$(BASH_COMPLETIONS_PROJ): $(BASH_COMPLETIONS_SRC) + $(QUIET_GENERATE)cp $< $(@) + +$(GENERATED_FILES): %: %.in Makefile VERSION + $(QUIET_CONFIG)$(SED) \ + -e "s|@COMMIT@|$(COMMIT)|g" \ + -e "s|@VERSION@|$(VERSION)|g" \ + -e "s|@CONFIG_IN@|$(CONFIG_IN)|g" \ + -e "s|@DESTCONFIG@|$(DESTCONFIG)|g" \ + -e "s|@DESTSYSCONFIG@|$(DESTSYSCONFIG)|g" \ + -e "s|@IMAGEPATH@|$(IMAGEPATH)|g" \ + -e "s|@KERNELPATH@|$(KERNELPATH)|g" \ + -e "s|@FIRMWAREPATH@|$(FIRMWAREPATH)|g" \ + -e "s|@MACHINEACCELERATORS@|$(MACHINEACCELERATORS)|g" \ + -e "s|@KERNELPARAMS@|$(KERNELPARAMS)|g" \ + -e "s|@LOCALSTATEDIR@|$(LOCALSTATEDIR)|g" \ + -e "s|@PKGLIBEXECDIR@|$(PKGLIBEXECDIR)|g" \ + -e "s|@PROXYPATH@|$(PROXYPATH)|g" \ + -e "s|@PROJECT_BUG_URL@|$(PROJECT_BUG_URL)|g" \ + -e "s|@PROJECT_URL@|$(PROJECT_URL)|g" \ + -e "s|@PROJECT_NAME@|$(PROJECT_NAME)|g" \ + -e "s|@PROJECT_TAG@|$(PROJECT_TAG)|g" \ + -e "s|@PROJECT_TYPE@|$(PROJECT_TYPE)|g" \ + -e "s|@PROJECT_TYPES@|$(PROJECT_TYPES)|g" \ + -e "s|@QEMUPATH@|$(QEMUPATH)|g" \ + -e "s|@RUNTIME_NAME@|$(TARGET)|g" \ + -e "s|@MACHINETYPE@|$(MACHINETYPE)|g" \ + -e "s|@SHIMPATH@|$(SHIMPATH)|g" \ + -e "s|@DEFVCPUS@|$(DEFVCPUS)|g" \ + -e "s|@DEFMEMSZ@|$(DEFMEMSZ)|g" \ + -e "s|@DEFBRIDGES@|$(DEFBRIDGES)|g" \ + -e "s|@DEFNETWORKMODEL@|$(DEFNETWORKMODEL)|g" \ + -e "s|@DEFDISABLEBLOCK@|$(DEFDISABLEBLOCK)|g" \ + -e "s|@DEFBLOCKSTORAGEDRIVER@|$(DEFBLOCKSTORAGEDRIVER)|g" \ + -e "s|@DEFENABLEMEMPREALLOC@|$(DEFENABLEMEMPREALLOC)|g" \ + -e "s|@DEFENABLEHUGEPAGES@|$(DEFENABLEHUGEPAGES)|g" \ + -e "s|@DEFENABLEMSWAP@|$(DEFENABLESWAP)|g" \ + -e "s|@DEFENABLEDEBUG@|$(DEFENABLEDEBUG)|g" \ + -e "s|@DEFDISABLENESTINGCHECKS@|$(DEFDISABLENESTINGCHECKS)|g" \ + $< > $@ + +generate-config: $(CONFIG) + +check: check-go-static check-go-test + +check-go-test: $(GENERATED_FILES) + $(QUIET_TEST)$(MAKEFILE_DIR)/../.ci/go-test.sh + +check-go-static: + $(QUIET_CHECK)$(MAKEFILE_DIR)/../.ci/go-static-checks.sh $(GO_STATIC_CHECKS_ARGS) + $(QUIET_CHECK)$(MAKEFILE_DIR)/../.ci/go-no-os-exit.sh + +coverage: + $(QUIET_TEST)$(MAKEFILE_DIR)/../.ci/go-test.sh html-coverage + +install: default install-scripts + $(QUIET_INST)install -D $(TARGET) $(DESTTARGET) + $(QUIET_INST)install -D $(CONFIG) $(DESTCONFIG) + +install-scripts: + $(foreach f,$(SCRIPTS),$(call INSTALL_EXEC,$f,$(SCRIPTS_DIR))) + +clean: + $(QUIET_CLEAN)rm -f $(TARGET) $(CONFIG) $(GENERATED_GO_FILES) $(GENERATED_FILES) $(COLLECT_SCRIPT_PROJ) $(BASH_COMPLETIONS_PROJ) + +show-usage: show-header + @printf "• Overview:\n" + @printf "\n" + @printf "\tTo build $(TARGET), just run, \"make\".\n" + @printf "\n" + @printf "\tFor a verbose build, run \"make V=1\".\n" + @printf "\n" + @printf "• Additional targets:\n" + @printf "\n" + @printf "\tbuild : standard build [1].\n" + @printf "\tbuild-$(KATA_TYPE)-system : build using standard $(KATA_PROJECT_NAME) system paths.\n" + @printf "\tcheck : run tests.\n" + @printf "\tclean : remove built files.\n" + @printf "\tcoverage : run coverage tests.\n" + @printf "\tdefault : same as 'make build' (or just 'make').\n" + @printf "\tgenerate-config : create configuration file.\n" + @printf "\tinstall : install files [2].\n" + @printf "\tinstall-$(KATA_TYPE)-system : install using standard $(KATA_PROJECT_NAME) system paths.\n" + @printf "\tshow-arches : show supported architectures (ARCH variable values).\n" + @printf "\tshow-summary : show install locations.\n" + @printf "\n" + @printf " Notes:\n" + @printf "\n" + @printf " [1] - Equivalent to:\n" + @printf " - 'build-kata-system' if KATA_SYSTEM_BUILD is set.\n" + @printf " - 'build' for the project specified by SYSTEM_BUILD_TYPE.\n" + @printf "\n" + @printf " [2] - Equivalent to:\n" + @printf " - 'install-kata-system' if KATA_SYSTEM_BUILD is set.\n" + @printf " - 'install' for the project specified by SYSTEM_BUILD_TYPE.\n" + @printf "\n" + +handle_help: show-usage show-summary show-variables show-footer + +usage: handle_help +help: handle_help + +show-variables: + @printf "• Variables affecting the build:\n\n" + @printf \ + "$(foreach v,$(sort $(USER_VARS)),$(shell printf "\\t$(v)='$($(v))'\\\n"))" + @printf "\n" + +show-header: + @printf "%s - version %s (commit %s)\n\n" $(TARGET) $(VERSION) $(COMMIT) + +show-arches: show-header + @printf "Supported architectures (possible values for ARCH variable):\n\n" + @printf \ + "$(foreach v,$(ALL_ARCHES),$(call SHOW_ARCH,$(v)))\n" + +show-footer: + @printf "• Project:\n" + @printf "\tHome: $(PROJECT_URL)\n" + @printf "\tBugs: $(PROJECT_BUG_URL)\n\n" + +show-summary: show-header + @printf "• architecture:\n" + @printf "\tHost: $(HOST_ARCH)\n" + @printf "\tgolang: $(GOARCH)\n" + @printf "\tBuild: $(ARCH)\n" + @printf "\n" + @printf "• golang:\n" + @printf "\t" + @go version + @printf "\n" + @printf "• Summary:\n" + @printf "\n" + @printf "\tProject system build type : $(system_build_type)\n" + @printf "\n" + @printf "\tbinary install path (DESTTARGET) : %s\n" $(DESTTARGET) + @printf "\tconfig install path (DESTCONFIG) : %s\n" $(DESTCONFIG) + @printf "\talternate config path (DESTSYSCONFIG) : %s\n" $(DESTSYSCONFIG) + @printf "\thypervisor path (QEMUPATH) : %s\n" $(QEMUPATH) + @printf "\tassets path (PKGDATADIR) : %s\n" $(PKGDATADIR) + @printf "\tproxy+shim path (PKGLIBEXECDIR) : %s\n" $(PKGLIBEXECDIR) + @printf "\n" + + +# The following git hooks handle HEAD changes: +# post-checkout +# post-commit # no parameters +# post-merge +# post-rewrite +# +define GIT_HOOK_POST_CHECKOUT +#!/usr/bin/env bash +prev_head=$$1 +new_head=$$2 +[[ "$$prev_head" == "$$new_head" ]] && exit +printf "\nexecuting post-checkout git hook\n\n" +rm -f config-generated.go +endef +export GIT_HOOK_POST_CHECKOUT + +define GIT_HOOK_POST_GENERIC +#!/usr/bin/env bash +printf "\n executing $$0 git hook\n\n" +rm -f config-generated.go +endef +export GIT_HOOK_POST_GENERIC + +# This git-hook is executed after every checkout git operation +.git/hooks/post-checkout: Makefile + @ mkdir -p .git/hooks/ + $(QUIET_INST)echo "$$GIT_HOOK_POST_CHECKOUT" >$@ + @ chmod +x $@ + +# This git-hook is executed after every commit, merge, amend or rebase git +# operation +.git/hooks/post-commit .git/hooks/post-merge .git/hooks/post-rewrite: Makefile + @ mkdir -p .git/hooks/ + $(QUIET_INST)echo "$$GIT_HOOK_POST_GENERIC" >$@ + @ chmod +x $@ + +install-git-hooks: .git/hooks/post-checkout .git/hooks/post-commit \ + .git/hooks/post-merge .git/hooks/post-rewrite diff --git a/cli/VERSION b/cli/VERSION new file mode 100644 index 0000000000..3eefcb9dd5 --- /dev/null +++ b/cli/VERSION @@ -0,0 +1 @@ +1.0.0 diff --git a/cli/arch/amd64-options.mk b/cli/arch/amd64-options.mk new file mode 100644 index 0000000000..718f83473e --- /dev/null +++ b/cli/arch/amd64-options.mk @@ -0,0 +1,27 @@ +# Copyright (c) 2018 Intel Corporation +# +# 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. + +# Intel x86-64 settings + +MACHINETYPE := pc +KERNELPARAMS := +MACHINEACCELERATORS := + +# The CentOS/RHEL hypervisor binary is not called qemu-lite +ifeq (,$(filter-out centos rhel,$(distro))) + QEMUCMD := qemu-system-x86_64 +else + QEMUCMD := qemu-lite-system-x86_64 +endif + diff --git a/cli/arch/arm64-options.mk b/cli/arch/arm64-options.mk new file mode 100644 index 0000000000..6df4504f88 --- /dev/null +++ b/cli/arch/arm64-options.mk @@ -0,0 +1,21 @@ +# Copyright (c) 2018 Intel Corporation +# +# 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. + +# ARM 64 settings + +MACHINETYPE := virt +KERNELPARAMS := +MACHINEACCELERATORS := + +QEMUCMD := qemu-system-aarch64 diff --git a/cli/config.go b/cli/config.go new file mode 100644 index 0000000000..3efa509056 --- /dev/null +++ b/cli/config.go @@ -0,0 +1,511 @@ +// Copyright (c) 2017 Intel Corporation +// +// 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. + +package main + +import ( + "errors" + "fmt" + "io/ioutil" + goruntime "runtime" + "strings" + + "github.com/BurntSushi/toml" + vc "github.com/kata-containers/runtime/virtcontainers" + "github.com/kata-containers/runtime/virtcontainers/pkg/oci" + "github.com/sirupsen/logrus" +) + +const ( + defaultHypervisor = vc.QemuHypervisor + defaultProxy = vc.KataProxyType + defaultShim = vc.KataShimType + defaultAgent = vc.KataContainersAgent +) + +// The TOML configuration file contains a number of sections (or +// tables). The names of these tables are in dotted ("nested table") +// form: +// +// [.] +// +// The components are hypervisor, proxy, shim and agent. For example, +// +// [proxy.kata] +// +// The currently supported types are listed below: +const ( + // supported hypervisor component types + qemuHypervisorTableType = "qemu" + + // supported proxy component types + ccProxyTableType = "cc" + kataProxyTableType = "kata" + + // supported shim component types + ccShimTableType = "cc" + kataShimTableType = "kata" + + // supported agent component types + hyperstartAgentTableType = "hyperstart" + kataAgentTableType = "kata" + + // the maximum amount of PCI bridges that can be cold plugged in a VM + maxPCIBridges uint32 = 5 +) + +type tomlConfig struct { + Hypervisor map[string]hypervisor + Proxy map[string]proxy + Shim map[string]shim + Agent map[string]agent + Runtime runtime +} + +type hypervisor struct { + Path string `toml:"path"` + Kernel string `toml:"kernel"` + Image string `toml:"image"` + Firmware string `toml:"firmware"` + MachineAccelerators string `toml:"machine_accelerators"` + KernelParams string `toml:"kernel_params"` + MachineType string `toml:"machine_type"` + DefaultVCPUs int32 `toml:"default_vcpus"` + DefaultMemSz uint32 `toml:"default_memory"` + DefaultBridges uint32 `toml:"default_bridges"` + DisableBlockDeviceUse bool `toml:"disable_block_device_use"` + BlockDeviceDriver string `toml:"block_device_driver"` + MemPrealloc bool `toml:"enable_mem_prealloc"` + HugePages bool `toml:"enable_hugepages"` + Swap bool `toml:"enable_swap"` + Debug bool `toml:"enable_debug"` + DisableNestingChecks bool `toml:"disable_nesting_checks"` +} + +type proxy struct { + Path string `toml:"path"` + Debug bool `toml:"enable_debug"` +} + +type runtime struct { + Debug bool `toml:"enable_debug"` + InterNetworkModel string `toml:"internetworking_model"` +} + +type shim struct { + Path string `toml:"path"` + Debug bool `toml:"enable_debug"` +} + +type agent struct { +} + +func (h hypervisor) path() (string, error) { + p := h.Path + + if h.Path == "" { + p = defaultHypervisorPath + } + + return resolvePath(p) +} + +func (h hypervisor) kernel() (string, error) { + p := h.Kernel + + if p == "" { + p = defaultKernelPath + } + + return resolvePath(p) +} + +func (h hypervisor) image() (string, error) { + p := h.Image + + if p == "" { + p = defaultImagePath + } + + return resolvePath(p) +} + +func (h hypervisor) firmware() (string, error) { + p := h.Firmware + + if p == "" { + if defaultFirmwarePath == "" { + return "", nil + } + p = defaultFirmwarePath + } + + return resolvePath(p) +} + +func (h hypervisor) machineAccelerators() string { + var machineAccelerators string + accelerators := strings.Split(h.MachineAccelerators, ",") + acceleratorsLen := len(accelerators) + for i := 0; i < acceleratorsLen; i++ { + if accelerators[i] != "" { + machineAccelerators += strings.Trim(accelerators[i], "\r\t\n ") + "," + } + } + + machineAccelerators = strings.Trim(machineAccelerators, ",") + + return machineAccelerators +} + +func (h hypervisor) kernelParams() string { + if h.KernelParams == "" { + return defaultKernelParams + } + + return h.KernelParams +} + +func (h hypervisor) machineType() string { + if h.MachineType == "" { + return defaultMachineType + } + + return h.MachineType +} + +func (h hypervisor) defaultVCPUs() uint32 { + numCPUs := goruntime.NumCPU() + + if h.DefaultVCPUs < 0 || h.DefaultVCPUs > int32(numCPUs) { + return uint32(numCPUs) + } + if h.DefaultVCPUs == 0 { // or unspecified + return defaultVCPUCount + } + + return uint32(h.DefaultVCPUs) +} + +func (h hypervisor) defaultMemSz() uint32 { + if h.DefaultMemSz < 8 { + return defaultMemSize // MiB + } + + return h.DefaultMemSz +} + +func (h hypervisor) defaultBridges() uint32 { + if h.DefaultBridges == 0 { + return defaultBridgesCount + } + + if h.DefaultBridges > maxPCIBridges { + return maxPCIBridges + } + + return h.DefaultBridges +} + +func (h hypervisor) blockDeviceDriver() (string, error) { + if h.BlockDeviceDriver == "" { + return defaultBlockDeviceDriver, nil + } + + if h.BlockDeviceDriver != vc.VirtioSCSI && h.BlockDeviceDriver != vc.VirtioBlock { + return "", fmt.Errorf("Invalid value %s provided for hypervisor block storage driver, can be either %s or %s", h.BlockDeviceDriver, vc.VirtioSCSI, vc.VirtioBlock) + } + + return h.BlockDeviceDriver, nil +} + +func (p proxy) path() string { + if p.Path == "" { + return defaultProxyPath + } + + return p.Path +} + +func (p proxy) debug() bool { + return p.Debug +} + +func (s shim) path() (string, error) { + p := s.Path + + if p == "" { + p = defaultShimPath + } + + return resolvePath(p) +} + +func (s shim) debug() bool { + return s.Debug +} + +func newQemuHypervisorConfig(h hypervisor) (vc.HypervisorConfig, error) { + hypervisor, err := h.path() + if err != nil { + return vc.HypervisorConfig{}, err + } + + kernel, err := h.kernel() + if err != nil { + return vc.HypervisorConfig{}, err + } + + image, err := h.image() + if err != nil { + return vc.HypervisorConfig{}, err + } + + firmware, err := h.firmware() + if err != nil { + return vc.HypervisorConfig{}, err + } + + machineAccelerators := h.machineAccelerators() + kernelParams := h.kernelParams() + machineType := h.machineType() + + blockDriver, err := h.blockDeviceDriver() + if err != nil { + return vc.HypervisorConfig{}, err + } + + return vc.HypervisorConfig{ + HypervisorPath: hypervisor, + KernelPath: kernel, + ImagePath: image, + FirmwarePath: firmware, + MachineAccelerators: machineAccelerators, + KernelParams: vc.DeserializeParams(strings.Fields(kernelParams)), + HypervisorMachineType: machineType, + DefaultVCPUs: h.defaultVCPUs(), + DefaultMemSz: h.defaultMemSz(), + DefaultBridges: h.defaultBridges(), + DisableBlockDeviceUse: h.DisableBlockDeviceUse, + MemPrealloc: h.MemPrealloc, + HugePages: h.HugePages, + Mlock: !h.Swap, + Debug: h.Debug, + DisableNestingChecks: h.DisableNestingChecks, + BlockDeviceDriver: blockDriver, + }, nil +} + +func newShimConfig(s shim) (vc.ShimConfig, error) { + path, err := s.path() + if err != nil { + return vc.ShimConfig{}, err + } + + return vc.ShimConfig{ + Path: path, + Debug: s.debug(), + }, nil +} + +func updateRuntimeConfig(configPath string, tomlConf tomlConfig, config *oci.RuntimeConfig) error { + for k, hypervisor := range tomlConf.Hypervisor { + switch k { + case qemuHypervisorTableType: + hConfig, err := newQemuHypervisorConfig(hypervisor) + if err != nil { + return fmt.Errorf("%v: %v", configPath, err) + } + + config.VMConfig.Memory = uint(hConfig.DefaultMemSz) + + config.HypervisorConfig = hConfig + } + } + + for k, proxy := range tomlConf.Proxy { + switch k { + case ccProxyTableType: + config.ProxyType = vc.CCProxyType + case kataProxyTableType: + config.ProxyType = vc.KataProxyType + } + + config.ProxyConfig = vc.ProxyConfig{ + Path: proxy.path(), + Debug: proxy.debug(), + } + } + + for k := range tomlConf.Agent { + switch k { + case hyperstartAgentTableType: + config.AgentType = hyperstartAgentTableType + config.AgentConfig = vc.HyperConfig{} + + case kataAgentTableType: + config.AgentType = kataAgentTableType + config.AgentConfig = vc.KataAgentConfig{} + + } + } + + for k, shim := range tomlConf.Shim { + switch k { + case ccShimTableType: + config.ShimType = vc.CCShimType + case kataShimTableType: + config.ShimType = vc.KataShimType + } + + shConfig, err := newShimConfig(shim) + if err != nil { + return fmt.Errorf("%v: %v", configPath, err) + } + + config.ShimConfig = shConfig + } + + return nil +} + +// loadConfiguration loads the configuration file and converts it into a +// runtime configuration. +// +// If ignoreLogging is true, the system logger will not be initialised nor +// will this function make any log calls. +// +// All paths are resolved fully meaning if this function does not return an +// error, all paths are valid at the time of the call. +func loadConfiguration(configPath string, ignoreLogging bool) (resolvedConfigPath string, config oci.RuntimeConfig, err error) { + defaultHypervisorConfig := vc.HypervisorConfig{ + HypervisorPath: defaultHypervisorPath, + KernelPath: defaultKernelPath, + ImagePath: defaultImagePath, + FirmwarePath: defaultFirmwarePath, + MachineAccelerators: defaultMachineAccelerators, + HypervisorMachineType: defaultMachineType, + DefaultVCPUs: defaultVCPUCount, + DefaultMemSz: defaultMemSize, + DefaultBridges: defaultBridgesCount, + MemPrealloc: defaultEnableMemPrealloc, + HugePages: defaultEnableHugePages, + Mlock: !defaultEnableSwap, + Debug: defaultEnableDebug, + DisableNestingChecks: defaultDisableNestingChecks, + BlockDeviceDriver: defaultBlockDeviceDriver, + } + + err = config.InterNetworkModel.SetModel(defaultInterNetworkingModel) + if err != nil { + return "", config, err + } + + defaultAgentConfig := vc.HyperConfig{} + + config = oci.RuntimeConfig{ + HypervisorType: defaultHypervisor, + HypervisorConfig: defaultHypervisorConfig, + AgentType: defaultAgent, + AgentConfig: defaultAgentConfig, + ProxyType: defaultProxy, + ShimType: defaultShim, + } + + var resolved string + + if configPath == "" { + resolved, err = getDefaultConfigFile() + } else { + resolved, err = resolvePath(configPath) + } + + if err != nil { + return "", config, fmt.Errorf("Cannot find usable config file (%v)", err) + } + + configData, err := ioutil.ReadFile(resolved) + if err != nil { + return "", config, err + } + + var tomlConf tomlConfig + _, err = toml.Decode(string(configData), &tomlConf) + if err != nil { + return "", config, err + } + + if tomlConf.Runtime.Debug { + crashOnError = true + } else { + // If debug is not required, switch back to the original + // default log priority, otherwise continue in debug mode. + kataLog.Logger.Level = originalLoggerLevel + } + + if tomlConf.Runtime.InterNetworkModel != "" { + err = config.InterNetworkModel.SetModel(tomlConf.Runtime.InterNetworkModel) + if err != nil { + return "", config, err + } + } + + if !ignoreLogging { + err = handleSystemLog("", "") + if err != nil { + return "", config, err + } + + kataLog.WithFields( + logrus.Fields{ + "format": "TOML", + }).Debugf("loaded configuration") + } + + if err := updateRuntimeConfig(resolved, tomlConf, &config); err != nil { + return "", config, err + } + + return resolved, config, nil +} + +// getDefaultConfigFilePaths returns a list of paths that will be +// considered as configuration files in priority order. +func getDefaultConfigFilePaths() []string { + return []string{ + // normally below "/etc" + defaultSysConfRuntimeConfiguration, + + // normally below "/usr/share" + defaultRuntimeConfiguration, + } +} + +// getDefaultConfigFile looks in multiple default locations for a +// configuration file and returns the resolved path for the first file +// found, or an error if no config files can be found. +func getDefaultConfigFile() (string, error) { + var errs []string + + for _, file := range getDefaultConfigFilePaths() { + resolved, err := resolvePath(file) + if err == nil { + return resolved, nil + } + s := fmt.Sprintf("config file %q unresolvable: %v", file, err) + errs = append(errs, s) + } + + return "", errors.New(strings.Join(errs, ", ")) +} diff --git a/cli/config/configuration.toml.in b/cli/config/configuration.toml.in new file mode 100644 index 0000000000..fc6532c8c8 --- /dev/null +++ b/cli/config/configuration.toml.in @@ -0,0 +1,142 @@ +# XXX: WARNING: this file is auto-generated. +# XXX: +# XXX: Source file: "@CONFIG_IN@" +# XXX: Project: +# XXX: Name: @PROJECT_NAME@ +# XXX: Type: @PROJECT_TYPE@ + +[hypervisor.qemu] +path = "@QEMUPATH@" +kernel = "@KERNELPATH@" +image = "@IMAGEPATH@" +machine_type = "@MACHINETYPE@" + +# Optional space-separated list of options to pass to the guest kernel. +# For example, use `kernel_params = "vsyscall=emulate"` if you are having +# trouble running pre-2.15 glibc. +# +# WARNING: - any parameter specified here will take priority over the default +# parameter value of the same name used to start the virtual machine. +# Do not set values here unless you understand the impact of doing so as you +# may stop the virtual machine from booting. +# To see the list of default parameters, enable hypervisor debug, create a +# container and look for 'default-kernel-parameters' log entries. +kernel_params = "@KERNELPARAMS@" + +# Path to the firmware. +# If you want that qemu uses the default firmware leave this option empty +firmware = "@FIRMWAREPATH@" + +# Machine accelerators +# comma-separated list of machine accelerators to pass to the hypervisor. +# For example, `machine_accelerators = "nosmm,nosmbus,nosata,nopit,static-prt,nofw"` +machine_accelerators="@MACHINEACCELERATORS@" + +# Default number of vCPUs per POD/VM: +# unspecified or 0 --> will be set to @DEFVCPUS@ +# < 0 --> will be set to the actual number of physical cores +# > 0 <= number of physical cores --> will be set to the specified number +# > number of physical cores --> will be set to the actual number of physical cores +default_vcpus = 1 + + +# Bridges can be used to hot plug devices. +# Limitations: +# * Currently only pci bridges are supported +# * Until 30 devices per bridge can be hot plugged. +# * Until 5 PCI bridges can be cold plugged per VM. +# This limitation could be a bug in qemu or in the kernel +# Default number of bridges per POD/VM: +# unspecified or 0 --> will be set to @DEFBRIDGES@ +# > 1 <= 5 --> will be set to the specified number +# > 5 --> will be set to 5 +default_bridges = @DEFBRIDGES@ + +# Default memory size in MiB for POD/VM. +# If unspecified then it will be set @DEFMEMSZ@ MiB. +#default_memory = @DEFMEMSZ@ + +# Disable block device from being used for a container's rootfs. +# In case of a storage driver like devicemapper where a container's +# root file system is backed by a block device, the block device is passed +# directly to the hypervisor for performance reasons. +# This flag prevents the block device from being passed to the hypervisor, +# 9pfs is used instead to pass the rootfs. +disable_block_device_use = @DEFDISABLEBLOCK@ + +# Block storage driver to be used for the hypervisor in case the container +# rootfs is backed by a block device. This is either virtio-scsi or +# virtio-blk. +block_device_driver = "@DEFBLOCKSTORAGEDRIVER@" + +# Enable pre allocation of VM RAM, default false +# Enabling this will result in lower container density +# as all of the memory will be allocated and locked +# This is useful when you want to reserve all the memory +# upfront or in the cases where you want memory latencies +# to be very predictable +# Default false +#enable_mem_prealloc = true + +# Enable huge pages for VM RAM, default false +# Enabling this will result in the VM memory +# being allocated using huge pages. +# This is useful when you want to use vhost-user network +# stacks within the container. This will automatically +# result in memory pre allocation +#enable_hugepages = true + +# Enable swap of vm memory. Default false. +# The behaviour is undefined if mem_prealloc is also set to true +#enable_swap = true + +# This option changes the default hypervisor and kernel parameters +# to enable debug output where available. This extra output is added +# to the proxy logs, but only when proxy debug is also enabled. +# +# Default false +#enable_debug = true + +# Disable the customizations done in the runtime when it detects +# that it is running on top a VMM. This will result in the runtime +# behaving as it would when running on bare metal. +# +#disable_nesting_checks = true + +[proxy.@PROJECT_TYPE@] +path = "@PROXYPATH@" + +# If enabled, proxy messages will be sent to the system log +# (default: disabled) +#enable_debug = true + +[shim.@PROJECT_TYPE@] +path = "@SHIMPATH@" + +# If enabled, shim messages will be sent to the system log +# (default: disabled) +#enable_debug = true + +[agent.@PROJECT_TYPE@] +# There is no field for this section. The goal is only to be able to +# specify which type of agent the user wants to use. + +[runtime] +# If enabled, the runtime will log additional debug messages to the +# system log +# (default: disabled) +#enable_debug = true +# +# Internetworking model +# Determines how the VM should be connected to the +# the container network interface +# Options: +# +# - bridged +# Uses a linux bridge to interconnect the container interface to +# the VM. Works for most cases except macvlan and ipvlan. +# +# - macvtap +# Used when the Container network interface can be bridged using +# macvtap. +internetworking_model="@DEFNETWORKMODEL@" diff --git a/cli/config_test.go b/cli/config_test.go new file mode 100644 index 0000000000..3ba31d17d5 --- /dev/null +++ b/cli/config_test.go @@ -0,0 +1,1054 @@ +// Copyright (c) 2017 Intel Corporation +// +// 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. + +package main + +import ( + "fmt" + "io/ioutil" + "os" + "path" + "path/filepath" + "reflect" + goruntime "runtime" + "strconv" + "strings" + "syscall" + "testing" + + vc "github.com/kata-containers/runtime/virtcontainers" + "github.com/kata-containers/runtime/virtcontainers/pkg/oci" + "github.com/stretchr/testify/assert" +) + +type testRuntimeConfig struct { + RuntimeConfig oci.RuntimeConfig + RuntimeConfigFile string + ConfigPath string + ConfigPathLink string + LogDir string + LogPath string +} + +func makeRuntimeConfigFileData(hypervisor, hypervisorPath, kernelPath, imagePath, kernelParams, machineType, shimPath, proxyPath, logPath string, disableBlock bool, blockDeviceDriver string) string { + return ` + # Runtime configuration file + + [hypervisor.` + hypervisor + `] + path = "` + hypervisorPath + `" + kernel = "` + kernelPath + `" + block_device_driver = "` + blockDeviceDriver + `" + kernel_params = "` + kernelParams + `" + image = "` + imagePath + `" + machine_type = "` + machineType + `" + default_vcpus = ` + strconv.FormatUint(uint64(defaultVCPUCount), 10) + ` + default_memory = ` + strconv.FormatUint(uint64(defaultMemSize), 10) + ` + disable_block_device_use = ` + strconv.FormatBool(disableBlock) + ` + + [proxy.kata] + path = "` + proxyPath + `" + + [shim.kata] + path = "` + shimPath + `" + + [agent.kata] + + [runtime] + ` +} + +func createConfig(configPath string, fileData string) error { + + err := ioutil.WriteFile(configPath, []byte(fileData), testFileMode) + if err != nil { + fmt.Fprintf(os.Stderr, "Unable to create config file %s %v\n", configPath, err) + return err + } + + return nil +} + +// createAllRuntimeConfigFiles creates all files necessary to call +// loadConfiguration(). +func createAllRuntimeConfigFiles(dir, hypervisor string) (config testRuntimeConfig, err error) { + if dir == "" { + return config, fmt.Errorf("BUG: need directory") + } + + if hypervisor == "" { + return config, fmt.Errorf("BUG: need hypervisor") + } + + hypervisorPath := path.Join(dir, "hypervisor") + kernelPath := path.Join(dir, "kernel") + kernelParams := "foo=bar xyz" + imagePath := path.Join(dir, "image") + shimPath := path.Join(dir, "shim") + proxyPath := path.Join(dir, "proxy") + logDir := path.Join(dir, "logs") + logPath := path.Join(logDir, "runtime.log") + machineType := "machineType" + disableBlockDevice := true + blockDeviceDriver := "virtio-scsi" + + runtimeConfigFileData := makeRuntimeConfigFileData(hypervisor, hypervisorPath, kernelPath, imagePath, kernelParams, machineType, shimPath, proxyPath, logPath, disableBlockDevice, blockDeviceDriver) + + configPath := path.Join(dir, "runtime.toml") + err = createConfig(configPath, runtimeConfigFileData) + if err != nil { + return config, err + } + + configPathLink := path.Join(filepath.Dir(configPath), "link-to-configuration.toml") + + // create a link to the config file + err = syscall.Symlink(configPath, configPathLink) + if err != nil { + return config, err + } + + files := []string{hypervisorPath, kernelPath, imagePath, shimPath, proxyPath} + + for _, file := range files { + // create the resource + err = createEmptyFile(file) + if err != nil { + return config, err + } + } + + hypervisorConfig := vc.HypervisorConfig{ + HypervisorPath: hypervisorPath, + KernelPath: kernelPath, + ImagePath: imagePath, + KernelParams: vc.DeserializeParams(strings.Fields(kernelParams)), + HypervisorMachineType: machineType, + DefaultVCPUs: defaultVCPUCount, + DefaultMemSz: defaultMemSize, + DisableBlockDeviceUse: disableBlockDevice, + BlockDeviceDriver: defaultBlockDeviceDriver, + DefaultBridges: defaultBridgesCount, + Mlock: !defaultEnableSwap, + } + + agentConfig := vc.KataAgentConfig{} + + proxyConfig := vc.ProxyConfig{ + Path: proxyPath, + } + + shimConfig := vc.ShimConfig{ + Path: shimPath, + } + + runtimeConfig := oci.RuntimeConfig{ + HypervisorType: defaultHypervisor, + HypervisorConfig: hypervisorConfig, + + AgentType: defaultAgent, + AgentConfig: agentConfig, + + ProxyType: defaultProxy, + ProxyConfig: proxyConfig, + + ShimType: defaultShim, + ShimConfig: shimConfig, + + VMConfig: vc.Resources{ + Memory: uint(defaultMemSize), + }, + } + + config = testRuntimeConfig{ + RuntimeConfig: runtimeConfig, + RuntimeConfigFile: configPath, + ConfigPath: configPath, + ConfigPathLink: configPathLink, + LogDir: logDir, + LogPath: logPath, + } + + return config, nil +} + +// testLoadConfiguration accepts an optional function that can be used +// to modify the test: if a function is specified, it indicates if the +// subsequent call to loadConfiguration() is expected to fail by +// returning a bool. If the function itself fails, that is considered an +// error. +func testLoadConfiguration(t *testing.T, dir string, + fn func(config testRuntimeConfig, configFile string, ignoreLogging bool) (bool, error)) { + subDir := path.Join(dir, "test") + + for _, hypervisor := range []string{"qemu"} { + Loop: + for _, ignoreLogging := range []bool{true, false} { + err := os.RemoveAll(subDir) + assert.NoError(t, err) + + err = os.MkdirAll(subDir, testDirMode) + assert.NoError(t, err) + + testConfig, err := createAllRuntimeConfigFiles(subDir, hypervisor) + assert.NoError(t, err) + + configFiles := []string{testConfig.ConfigPath, testConfig.ConfigPathLink, ""} + + // override + defaultRuntimeConfiguration = testConfig.ConfigPath + defaultSysConfRuntimeConfiguration = "" + + for _, file := range configFiles { + var err error + expectFail := false + + if fn != nil { + expectFail, err = fn(testConfig, file, ignoreLogging) + assert.NoError(t, err) + } + + resolvedConfigPath, config, err := loadConfiguration(file, ignoreLogging) + if expectFail { + assert.Error(t, err) + + // no point proceeding in the error scenario. + break Loop + } else { + assert.NoError(t, err) + } + + if file == "" { + assert.Equal(t, defaultRuntimeConfiguration, resolvedConfigPath) + } else { + assert.Equal(t, testConfig.ConfigPath, resolvedConfigPath) + } + + assert.Equal(t, defaultRuntimeConfiguration, resolvedConfigPath) + result := reflect.DeepEqual(config, testConfig.RuntimeConfig) + if !result { + t.Fatalf("Expected\n%+v\nGot\n%+v", config, testConfig.RuntimeConfig) + } + assert.True(t, result) + + err = os.RemoveAll(testConfig.LogDir) + assert.NoError(t, err) + } + } + } +} + +func TestConfigLoadConfiguration(t *testing.T) { + tmpdir, err := ioutil.TempDir(testDir, "load-config-") + assert.NoError(t, err) + defer os.RemoveAll(tmpdir) + + testLoadConfiguration(t, tmpdir, nil) +} + +func TestConfigLoadConfigurationFailBrokenSymLink(t *testing.T) { + tmpdir, err := ioutil.TempDir(testDir, "runtime-config-") + assert.NoError(t, err) + defer os.RemoveAll(tmpdir) + + testLoadConfiguration(t, tmpdir, + func(config testRuntimeConfig, configFile string, ignoreLogging bool) (bool, error) { + expectFail := false + + if configFile == config.ConfigPathLink { + // break the symbolic link + err = os.Remove(config.ConfigPathLink) + if err != nil { + return expectFail, err + } + + expectFail = true + } + + return expectFail, nil + }) +} + +func TestConfigLoadConfigurationFailSymLinkLoop(t *testing.T) { + tmpdir, err := ioutil.TempDir(testDir, "runtime-config-") + assert.NoError(t, err) + defer os.RemoveAll(tmpdir) + + testLoadConfiguration(t, tmpdir, + func(config testRuntimeConfig, configFile string, ignoreLogging bool) (bool, error) { + expectFail := false + + if configFile == config.ConfigPathLink { + // remove the config file + err = os.Remove(config.ConfigPath) + if err != nil { + return expectFail, err + } + + // now, create a sym-link loop + err := os.Symlink(config.ConfigPathLink, config.ConfigPath) + if err != nil { + return expectFail, err + } + + expectFail = true + } + + return expectFail, nil + }) +} + +func TestConfigLoadConfigurationFailMissingHypervisor(t *testing.T) { + tmpdir, err := ioutil.TempDir(testDir, "runtime-config-") + assert.NoError(t, err) + defer os.RemoveAll(tmpdir) + + testLoadConfiguration(t, tmpdir, + func(config testRuntimeConfig, configFile string, ignoreLogging bool) (bool, error) { + expectFail := true + + err = os.Remove(config.RuntimeConfig.HypervisorConfig.HypervisorPath) + if err != nil { + return expectFail, err + } + + return expectFail, nil + }) +} + +func TestConfigLoadConfigurationFailMissingImage(t *testing.T) { + tmpdir, err := ioutil.TempDir(testDir, "runtime-config-") + assert.NoError(t, err) + defer os.RemoveAll(tmpdir) + + testLoadConfiguration(t, tmpdir, + func(config testRuntimeConfig, configFile string, ignoreLogging bool) (bool, error) { + expectFail := true + + err = os.Remove(config.RuntimeConfig.HypervisorConfig.ImagePath) + if err != nil { + return expectFail, err + } + + return expectFail, nil + }) +} + +func TestConfigLoadConfigurationFailMissingKernel(t *testing.T) { + tmpdir, err := ioutil.TempDir(testDir, "runtime-config-") + assert.NoError(t, err) + defer os.RemoveAll(tmpdir) + + testLoadConfiguration(t, tmpdir, + func(config testRuntimeConfig, configFile string, ignoreLogging bool) (bool, error) { + expectFail := true + + err = os.Remove(config.RuntimeConfig.HypervisorConfig.KernelPath) + if err != nil { + return expectFail, err + } + + return expectFail, nil + }) +} + +func TestConfigLoadConfigurationFailMissingShim(t *testing.T) { + tmpdir, err := ioutil.TempDir(testDir, "runtime-config-") + assert.NoError(t, err) + defer os.RemoveAll(tmpdir) + + testLoadConfiguration(t, tmpdir, + func(config testRuntimeConfig, configFile string, ignoreLogging bool) (bool, error) { + expectFail := true + + shimConfig, ok := config.RuntimeConfig.ShimConfig.(vc.ShimConfig) + if !ok { + return expectFail, fmt.Errorf("cannot determine shim config") + } + err = os.Remove(shimConfig.Path) + if err != nil { + return expectFail, err + } + + return expectFail, nil + }) +} + +func TestConfigLoadConfigurationFailUnreadableConfig(t *testing.T) { + if os.Geteuid() == 0 { + t.Skip(testDisabledNeedNonRoot) + } + + tmpdir, err := ioutil.TempDir(testDir, "runtime-config-") + assert.NoError(t, err) + defer os.RemoveAll(tmpdir) + + testLoadConfiguration(t, tmpdir, + func(config testRuntimeConfig, configFile string, ignoreLogging bool) (bool, error) { + expectFail := true + + // make file unreadable by non-root user + err = os.Chmod(config.ConfigPath, 0000) + if err != nil { + return expectFail, err + } + + return expectFail, nil + }) +} + +func TestConfigLoadConfigurationFailTOMLConfigFileInvalidContents(t *testing.T) { + if os.Geteuid() == 0 { + t.Skip(testDisabledNeedNonRoot) + } + + tmpdir, err := ioutil.TempDir(testDir, "runtime-config-") + assert.NoError(t, err) + defer os.RemoveAll(tmpdir) + + testLoadConfiguration(t, tmpdir, + func(config testRuntimeConfig, configFile string, ignoreLogging bool) (bool, error) { + expectFail := true + + err := createFile(config.ConfigPath, + ` + I am not TOML! ;-) + I am invalid XML!`) + + if err != nil { + return expectFail, err + } + + return expectFail, nil + }) +} + +func TestConfigLoadConfigurationFailTOMLConfigFileDuplicatedData(t *testing.T) { + if os.Geteuid() == 0 { + t.Skip(testDisabledNeedNonRoot) + } + + tmpdir, err := ioutil.TempDir(testDir, "runtime-config-") + assert.NoError(t, err) + defer os.RemoveAll(tmpdir) + + testLoadConfiguration(t, tmpdir, + func(config testRuntimeConfig, configFile string, ignoreLogging bool) (bool, error) { + expectFail := true + + text, err := getFileContents(config.ConfigPath) + if err != nil { + return expectFail, err + } + + // create a config file containing two sets of + // data. + err = createFile(config.ConfigPath, fmt.Sprintf("%s\n%s\n", text, text)) + if err != nil { + return expectFail, err + } + + return expectFail, nil + }) +} + +func TestMinimalRuntimeConfig(t *testing.T) { + dir, err := ioutil.TempDir(testDir, "minimal-runtime-config-") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + + shimPath := path.Join(dir, "shim") + proxyPath := path.Join(dir, "proxy") + + runtimeMinimalConfig := ` + # Runtime configuration file + + [proxy.kata] + path = "` + proxyPath + `" + + [shim.kata] + path = "` + shimPath + `" + + [agent.kata] +` + + configPath := path.Join(dir, "runtime.toml") + err = createConfig(configPath, runtimeMinimalConfig) + if err != nil { + t.Fatal(err) + } + + _, config, err := loadConfiguration(configPath, false) + if err == nil { + t.Fatalf("Expected loadConfiguration to fail as shim path does not exist: %+v", config) + } + + err = createEmptyFile(shimPath) + if err != nil { + t.Error(err) + } + + err = createEmptyFile(proxyPath) + if err != nil { + t.Error(err) + } + + _, config, err = loadConfiguration(configPath, false) + if err != nil { + t.Fatal(err) + } + + expectedHypervisorConfig := vc.HypervisorConfig{ + HypervisorPath: defaultHypervisorPath, + KernelPath: defaultKernelPath, + ImagePath: defaultImagePath, + HypervisorMachineType: defaultMachineType, + DefaultVCPUs: defaultVCPUCount, + DefaultMemSz: defaultMemSize, + DisableBlockDeviceUse: defaultDisableBlockDeviceUse, + DefaultBridges: defaultBridgesCount, + Mlock: !defaultEnableSwap, + BlockDeviceDriver: defaultBlockDeviceDriver, + } + + expectedAgentConfig := vc.KataAgentConfig{} + + expectedProxyConfig := vc.ProxyConfig{ + Path: proxyPath, + } + + expectedShimConfig := vc.ShimConfig{ + Path: shimPath, + } + + expectedConfig := oci.RuntimeConfig{ + HypervisorType: defaultHypervisor, + HypervisorConfig: expectedHypervisorConfig, + + AgentType: defaultAgent, + AgentConfig: expectedAgentConfig, + + ProxyType: defaultProxy, + ProxyConfig: expectedProxyConfig, + + ShimType: defaultShim, + ShimConfig: expectedShimConfig, + } + + if reflect.DeepEqual(config, expectedConfig) == false { + t.Fatalf("Got %+v\n expecting %+v", config, expectedConfig) + } + + if err := os.Remove(configPath); err != nil { + t.Fatal(err) + } +} + +func TestNewQemuHypervisorConfig(t *testing.T) { + dir, err := ioutil.TempDir(testDir, "hypervisor-config-") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + + hypervisorPath := path.Join(dir, "hypervisor") + kernelPath := path.Join(dir, "kernel") + imagePath := path.Join(dir, "image") + machineType := "machineType" + disableBlock := true + + hypervisor := hypervisor{ + Path: hypervisorPath, + Kernel: kernelPath, + Image: imagePath, + MachineType: machineType, + DisableBlockDeviceUse: disableBlock, + } + + files := []string{hypervisorPath, kernelPath, imagePath} + filesLen := len(files) + + for i, file := range files { + _, err := newQemuHypervisorConfig(hypervisor) + if err == nil { + t.Fatalf("Expected newQemuHypervisorConfig to fail as not all paths exist (not created %v)", + strings.Join(files[i:filesLen], ",")) + } + + // create the resource + err = createEmptyFile(file) + if err != nil { + t.Error(err) + } + } + + // all paths exist now + config, err := newQemuHypervisorConfig(hypervisor) + if err != nil { + t.Fatal(err) + } + + if config.HypervisorPath != hypervisor.Path { + t.Errorf("Expected hypervisor path %v, got %v", hypervisor.Path, config.HypervisorPath) + } + + if config.KernelPath != hypervisor.Kernel { + t.Errorf("Expected kernel path %v, got %v", hypervisor.Kernel, config.KernelPath) + } + + if config.ImagePath != hypervisor.Image { + t.Errorf("Expected image path %v, got %v", hypervisor.Image, config.ImagePath) + } + + if config.DisableBlockDeviceUse != disableBlock { + t.Errorf("Expected value for disable block usage %v, got %v", disableBlock, config.DisableBlockDeviceUse) + } + +} + +func TestNewShimConfig(t *testing.T) { + dir, err := ioutil.TempDir(testDir, "shim-config-") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + + shimPath := path.Join(dir, "shim") + + shim := shim{ + Path: shimPath, + } + + _, err = newShimConfig(shim) + if err == nil { + t.Fatalf("Expected newShimConfig to fail as no paths exist") + } + + err = createEmptyFile(shimPath) + if err != nil { + t.Error(err) + } + + shConfig, err := newShimConfig(shim) + if err != nil { + t.Fatalf("newShimConfig failed unexpectedly: %v", err) + } + + if shConfig.Path != shimPath { + t.Errorf("Expected shim path %v, got %v", shimPath, shConfig.Path) + } +} + +func TestHypervisorDefaults(t *testing.T) { + assert := assert.New(t) + + h := hypervisor{} + + assert.Equal(h.machineType(), defaultMachineType, "default hypervisor machine type wrong") + assert.Equal(h.defaultVCPUs(), defaultVCPUCount, "default vCPU number is wrong") + assert.Equal(h.defaultMemSz(), defaultMemSize, "default memory size is wrong") + + machineType := "foo" + h.MachineType = machineType + assert.Equal(h.machineType(), machineType, "custom hypervisor machine type wrong") + + // auto inferring + h.DefaultVCPUs = -1 + assert.Equal(h.defaultVCPUs(), uint32(goruntime.NumCPU()), "default vCPU number is wrong") + + h.DefaultVCPUs = 2 + assert.Equal(h.defaultVCPUs(), uint32(2), "default vCPU number is wrong") + + numCPUs := goruntime.NumCPU() + h.DefaultVCPUs = int32(numCPUs) + 1 + assert.Equal(h.defaultVCPUs(), uint32(numCPUs), "default vCPU number is wrong") + + h.DefaultMemSz = 1024 + assert.Equal(h.defaultMemSz(), uint32(1024), "default memory size is wrong") +} + +func TestHypervisorDefaultsHypervisor(t *testing.T) { + assert := assert.New(t) + + tmpdir, err := ioutil.TempDir(testDir, "") + assert.NoError(err) + defer os.RemoveAll(tmpdir) + + testHypervisorPath := filepath.Join(tmpdir, "hypervisor") + testHypervisorLinkPath := filepath.Join(tmpdir, "hypervisor-link") + + err = createEmptyFile(testHypervisorPath) + assert.NoError(err) + + err = syscall.Symlink(testHypervisorPath, testHypervisorLinkPath) + assert.NoError(err) + + savedHypervisorPath := defaultHypervisorPath + + defer func() { + defaultHypervisorPath = savedHypervisorPath + }() + + defaultHypervisorPath = testHypervisorPath + h := hypervisor{} + p, err := h.path() + assert.NoError(err) + assert.Equal(p, defaultHypervisorPath, "default hypervisor path wrong") + + // test path resolution + defaultHypervisorPath = testHypervisorLinkPath + h = hypervisor{} + p, err = h.path() + assert.NoError(err) + assert.Equal(p, testHypervisorPath) +} + +func TestHypervisorDefaultsKernel(t *testing.T) { + assert := assert.New(t) + + tmpdir, err := ioutil.TempDir(testDir, "") + assert.NoError(err) + defer os.RemoveAll(tmpdir) + + testKernelPath := filepath.Join(tmpdir, "kernel") + testKernelLinkPath := filepath.Join(tmpdir, "kernel-link") + + err = createEmptyFile(testKernelPath) + assert.NoError(err) + + err = syscall.Symlink(testKernelPath, testKernelLinkPath) + assert.NoError(err) + + savedKernelPath := defaultKernelPath + + defer func() { + defaultKernelPath = savedKernelPath + }() + + defaultKernelPath = testKernelPath + + h := hypervisor{} + p, err := h.kernel() + assert.NoError(err) + assert.Equal(p, defaultKernelPath, "default Kernel path wrong") + + // test path resolution + defaultKernelPath = testKernelLinkPath + h = hypervisor{} + p, err = h.kernel() + assert.NoError(err) + assert.Equal(p, testKernelPath) + + assert.Equal(h.kernelParams(), defaultKernelParams, "default hypervisor image wrong") + kernelParams := "foo=bar xyz" + h.KernelParams = kernelParams + assert.Equal(h.kernelParams(), kernelParams, "custom hypervisor kernel parameterms wrong") +} + +func TestHypervisorDefaultsImage(t *testing.T) { + assert := assert.New(t) + + tmpdir, err := ioutil.TempDir(testDir, "") + assert.NoError(err) + defer os.RemoveAll(tmpdir) + + testImagePath := filepath.Join(tmpdir, "image") + testImageLinkPath := filepath.Join(tmpdir, "image-link") + + err = createEmptyFile(testImagePath) + assert.NoError(err) + + err = syscall.Symlink(testImagePath, testImageLinkPath) + assert.NoError(err) + + savedImagePath := defaultImagePath + + defer func() { + defaultImagePath = savedImagePath + }() + + defaultImagePath = testImagePath + h := hypervisor{} + p, err := h.image() + assert.NoError(err) + assert.Equal(p, defaultImagePath, "default Image path wrong") + + // test path resolution + defaultImagePath = testImageLinkPath + h = hypervisor{} + p, err = h.image() + assert.NoError(err) + assert.Equal(p, testImagePath) +} + +func TestProxyDefaults(t *testing.T) { + p := proxy{} + + assert.Equal(t, p.path(), defaultProxyPath, "default proxy path wrong") + + path := "/foo/bar/baz/proxy" + p.Path = path + assert.Equal(t, p.path(), path, "custom proxy path wrong") +} + +func TestShimDefaults(t *testing.T) { + assert := assert.New(t) + + tmpdir, err := ioutil.TempDir(testDir, "") + assert.NoError(err) + defer os.RemoveAll(tmpdir) + + testShimPath := filepath.Join(tmpdir, "shim") + testShimLinkPath := filepath.Join(tmpdir, "shim-link") + + err = createEmptyFile(testShimPath) + assert.NoError(err) + + err = syscall.Symlink(testShimPath, testShimLinkPath) + assert.NoError(err) + + savedShimPath := defaultShimPath + + defer func() { + defaultShimPath = savedShimPath + }() + + defaultShimPath = testShimPath + s := shim{} + p, err := s.path() + assert.NoError(err) + assert.Equal(p, defaultShimPath, "default shim path wrong") + + // test path resolution + defaultShimPath = testShimLinkPath + s = shim{} + p, err = s.path() + assert.NoError(err) + assert.Equal(p, testShimPath) + + assert.False(s.debug()) + s.Debug = true + assert.True(s.debug()) +} + +func TestGetDefaultConfigFilePaths(t *testing.T) { + assert := assert.New(t) + + results := getDefaultConfigFilePaths() + // There should be atleast two config file locations + assert.True(len(results) >= 2) + + for _, f := range results { + // Paths cannot be empty + assert.NotNil(f) + } +} + +func TestGetDefaultConfigFile(t *testing.T) { + assert := assert.New(t) + + tmpdir, err := ioutil.TempDir(testDir, "") + assert.NoError(err) + defer os.RemoveAll(tmpdir) + + hypervisor := "qemu" + confDir := filepath.Join(tmpdir, "conf") + sysConfDir := filepath.Join(tmpdir, "sysconf") + + for _, dir := range []string{confDir, sysConfDir} { + err = os.MkdirAll(dir, testDirMode) + assert.NoError(err) + } + + confDirConfig, err := createAllRuntimeConfigFiles(confDir, hypervisor) + assert.NoError(err) + + sysConfDirConfig, err := createAllRuntimeConfigFiles(sysConfDir, hypervisor) + assert.NoError(err) + + savedConf := defaultRuntimeConfiguration + savedSysConf := defaultSysConfRuntimeConfiguration + + defaultRuntimeConfiguration = confDirConfig.ConfigPath + defaultSysConfRuntimeConfiguration = sysConfDirConfig.ConfigPath + + defer func() { + defaultRuntimeConfiguration = savedConf + defaultSysConfRuntimeConfiguration = savedSysConf + + }() + + got, err := getDefaultConfigFile() + assert.NoError(err) + // defaultSysConfRuntimeConfiguration has priority over defaultRuntimeConfiguration + assert.Equal(got, defaultSysConfRuntimeConfiguration) + + // force defaultRuntimeConfiguration to be returned + os.Remove(defaultSysConfRuntimeConfiguration) + + got, err = getDefaultConfigFile() + assert.NoError(err) + assert.Equal(got, defaultRuntimeConfiguration) + + // force error + os.Remove(defaultRuntimeConfiguration) + + _, err = getDefaultConfigFile() + assert.Error(err) +} + +func TestDefaultBridges(t *testing.T) { + assert := assert.New(t) + + h := hypervisor{DefaultBridges: 0} + + bridges := h.defaultBridges() + assert.Equal(defaultBridgesCount, bridges) + + h.DefaultBridges = maxPCIBridges + 1 + bridges = h.defaultBridges() + assert.Equal(maxPCIBridges, bridges) + + h.DefaultBridges = maxPCIBridges + bridges = h.defaultBridges() + assert.Equal(maxPCIBridges, bridges) +} + +func TestDefaultFirmware(t *testing.T) { + assert := assert.New(t) + + // save default firmware path + oldDefaultFirmwarePath := defaultFirmwarePath + + f, err := ioutil.TempFile(os.TempDir(), "qboot.bin") + assert.NoError(err) + assert.NoError(f.Close()) + defer os.RemoveAll(f.Name()) + + h := hypervisor{} + defaultFirmwarePath = "" + p, err := h.firmware() + assert.NoError(err) + assert.Empty(p) + + defaultFirmwarePath = f.Name() + p, err = h.firmware() + assert.NoError(err) + assert.NotEmpty(p) + + // restore default firmware path + defaultFirmwarePath = oldDefaultFirmwarePath +} + +func TestDefaultMachineAccelerators(t *testing.T) { + assert := assert.New(t) + machineAccelerators := "abc,123,rgb" + h := hypervisor{MachineAccelerators: machineAccelerators} + assert.Equal(machineAccelerators, h.machineAccelerators()) + + machineAccelerators = "" + h.MachineAccelerators = machineAccelerators + assert.Equal(machineAccelerators, h.machineAccelerators()) + + machineAccelerators = "abc" + h.MachineAccelerators = machineAccelerators + assert.Equal(machineAccelerators, h.machineAccelerators()) + + machineAccelerators = "abc,123" + h.MachineAccelerators = "abc,,123" + assert.Equal(machineAccelerators, h.machineAccelerators()) + + machineAccelerators = "abc,123" + h.MachineAccelerators = ",,abc,,123,,," + assert.Equal(machineAccelerators, h.machineAccelerators()) + + machineAccelerators = "abc,123" + h.MachineAccelerators = "abc,,123,,," + assert.Equal(machineAccelerators, h.machineAccelerators()) + + machineAccelerators = "abc" + h.MachineAccelerators = ",,abc," + assert.Equal(machineAccelerators, h.machineAccelerators()) + + machineAccelerators = "abc" + h.MachineAccelerators = ", , abc , ," + assert.Equal(machineAccelerators, h.machineAccelerators()) + + machineAccelerators = "abc" + h.MachineAccelerators = " abc " + assert.Equal(machineAccelerators, h.machineAccelerators()) + + machineAccelerators = "abc,123" + h.MachineAccelerators = ", abc , 123 ," + assert.Equal(machineAccelerators, h.machineAccelerators()) + + machineAccelerators = "abc,123" + h.MachineAccelerators = ",, abc ,,, 123 ,," + assert.Equal(machineAccelerators, h.machineAccelerators()) +} + +func TestUpdateRuntimeConfiguration(t *testing.T) { + assert := assert.New(t) + + assert.NotEqual(defaultAgent, vc.HyperstartAgent) + + config := oci.RuntimeConfig{} + + tomlConf := tomlConfig{ + Agent: map[string]agent{ + // force a non-default value + kataAgentTableType: {}, + }, + } + + assert.NotEqual(config.AgentType, vc.AgentType(kataAgentTableType)) + assert.NotEqual(config.AgentConfig, vc.KataAgentConfig{}) + + err := updateRuntimeConfig("", tomlConf, &config) + assert.NoError(err) + + assert.Equal(config.AgentType, vc.AgentType(kataAgentTableType)) + assert.Equal(config.AgentConfig, vc.KataAgentConfig{}) +} + +func TestUpdateRuntimeConfigurationVMConfig(t *testing.T) { + assert := assert.New(t) + + vcpus := uint(2) + mem := uint(2048) + + config := oci.RuntimeConfig{} + expectedVMConfig := vc.Resources{ + Memory: mem, + } + + tomlConf := tomlConfig{ + Hypervisor: map[string]hypervisor{ + qemuHypervisorTableType: { + DefaultVCPUs: int32(vcpus), + DefaultMemSz: uint32(mem), + Path: "/", + Kernel: "/", + Image: "/", + Firmware: "/", + }, + }, + } + + err := updateRuntimeConfig("", tomlConf, &config) + assert.NoError(err) + + assert.Equal(expectedVMConfig, config.VMConfig) +} diff --git a/cli/console.go b/cli/console.go new file mode 100644 index 0000000000..d9a186331f --- /dev/null +++ b/cli/console.go @@ -0,0 +1,147 @@ +// Copyright (c) 2014,2015,2016 Docker, Inc. +// Copyright (c) 2017 Intel Corporation +// +// 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. + +package main + +import ( + "fmt" + "io" + "os" + "syscall" + "unsafe" + + "golang.org/x/sys/unix" +) + +var ptmxPath = "/dev/ptmx" + +// Console represents a pseudo TTY. +type Console struct { + io.ReadWriteCloser + + master *os.File + slavePath string +} + +// isTerminal returns true if fd is a terminal, else false +func isTerminal(fd uintptr) bool { + var termios syscall.Termios + _, _, err := syscall.Syscall(syscall.SYS_IOCTL, fd, syscall.TCGETS, uintptr(unsafe.Pointer(&termios))) + return err == 0 +} + +// ConsoleFromFile creates a console from a file +func ConsoleFromFile(f *os.File) *Console { + return &Console{ + master: f, + } +} + +// NewConsole returns an initialized console that can be used within a container by copying bytes +// from the master side to the slave that is attached as the tty for the container's init process. +func newConsole() (*Console, error) { + master, err := os.OpenFile(ptmxPath, unix.O_RDWR|unix.O_NOCTTY|unix.O_CLOEXEC, 0) + if err != nil { + return nil, err + } + if err := saneTerminal(master); err != nil { + return nil, err + } + console, err := ptsname(master) + if err != nil { + return nil, err + } + if err := unlockpt(master); err != nil { + return nil, err + } + return &Console{ + slavePath: console, + master: master, + }, nil +} + +// File returns master +func (c *Console) File() *os.File { + return c.master +} + +// Path to slave +func (c *Console) Path() string { + return c.slavePath +} + +// Read from master +func (c *Console) Read(b []byte) (int, error) { + return c.master.Read(b) +} + +// Write to master +func (c *Console) Write(b []byte) (int, error) { + return c.master.Write(b) +} + +// Close master +func (c *Console) Close() error { + if m := c.master; m != nil { + return m.Close() + } + return nil +} + +func ioctl(fd uintptr, flag, data uintptr) error { + if _, _, err := unix.Syscall(unix.SYS_IOCTL, fd, flag, data); err != 0 { + return err + } + return nil +} + +// unlockpt unlocks the slave pseudoterminal device corresponding to the master pseudoterminal referred to by f. +// unlockpt should be called before opening the slave side of a pty. +func unlockpt(f *os.File) error { + var u int32 + return ioctl(f.Fd(), unix.TIOCSPTLCK, uintptr(unsafe.Pointer(&u))) +} + +// ptsname retrieves the name of the first available pts for the given master. +func ptsname(f *os.File) (string, error) { + var n int32 + if err := ioctl(f.Fd(), unix.TIOCGPTN, uintptr(unsafe.Pointer(&n))); err != nil { + return "", err + } + return fmt.Sprintf("/dev/pts/%d", n), nil +} + +// saneTerminal sets the necessary tty_ioctl(4)s to ensure that a pty pair +// created by us acts normally. In particular, a not-very-well-known default of +// Linux unix98 ptys is that they have +onlcr by default. While this isn't a +// problem for terminal emulators, because we relay data from the terminal we +// also relay that funky line discipline. +func saneTerminal(terminal *os.File) error { + // Go doesn't have a wrapper for any of the termios ioctls. + var termios unix.Termios + + if err := ioctl(terminal.Fd(), unix.TCGETS, uintptr(unsafe.Pointer(&termios))); err != nil { + return fmt.Errorf("ioctl(tty, tcgets): %s", err.Error()) + } + + // Set -onlcr so we don't have to deal with \r. + termios.Oflag &^= unix.ONLCR + + if err := ioctl(terminal.Fd(), unix.TCSETS, uintptr(unsafe.Pointer(&termios))); err != nil { + return fmt.Errorf("ioctl(tty, tcsets): %s", err.Error()) + } + + return nil +} diff --git a/cli/console_test.go b/cli/console_test.go new file mode 100644 index 0000000000..cedea3a982 --- /dev/null +++ b/cli/console_test.go @@ -0,0 +1,131 @@ +// Copyright (c) 2017 Intel Corporation +// +// 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. + +package main + +import ( + "io/ioutil" + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestConsoleFromFile(t *testing.T) { + assert := assert.New(t) + + console := ConsoleFromFile(os.Stdout) + + assert.NotNil(console.File(), "console file is nil") +} + +func TestNewConsole(t *testing.T) { + assert := assert.New(t) + + console, err := newConsole() + assert.NoError(err, "failed to create a new console: %s", err) + defer console.Close() + + assert.NotEmpty(console.Path(), "console path is empty") + + assert.NotNil(console.File(), "console file is nil") +} + +func TestIsTerminal(t *testing.T) { + assert := assert.New(t) + + var fd uintptr = 4 + assert.False(isTerminal(fd), "Fd %d is not a terminal", fd) + + console, err := newConsole() + assert.NoError(err, "failed to create a new console: %s", err) + defer console.Close() + + fd = console.File().Fd() + assert.True(isTerminal(fd), "Fd %d is a terminal", fd) +} + +func TestReadWrite(t *testing.T) { + assert := assert.New(t) + + // write operation + f, err := ioutil.TempFile(os.TempDir(), ".tty") + assert.NoError(err, "failed to create a temporal file") + defer os.Remove(f.Name()) + + console := ConsoleFromFile(f) + assert.NotNil(console) + defer console.Close() + + msgWrite := "hello" + l, err := console.Write([]byte(msgWrite)) + assert.NoError(err, "failed to write message: %s", msgWrite) + assert.Equal(len(msgWrite), l) + + console.master.Sync() + console.master.Seek(0, 0) + + // Read operation + msgRead := make([]byte, len(msgWrite)) + l, err = console.Read(msgRead) + assert.NoError(err, "failed to read message: %s", msgWrite) + assert.Equal(len(msgWrite), l) + assert.Equal(msgWrite, string(msgRead)) +} + +func TestNewConsoleFail(t *testing.T) { + assert := assert.New(t) + + orgPtmxPath := ptmxPath + defer func() { ptmxPath = orgPtmxPath }() + + // OpenFile failure + ptmxPath = "/this/file/does/not/exist" + c, err := newConsole() + assert.Error(err) + assert.Nil(c) + + // saneTerminal failure + f, err := ioutil.TempFile("", "") + assert.NoError(err) + assert.NoError(f.Close()) + defer os.Remove(f.Name()) + ptmxPath = f.Name() + c, err = newConsole() + assert.Error(err) + assert.Nil(c) +} + +func TestConsoleClose(t *testing.T) { + assert := assert.New(t) + + // nil master + c := &Console{} + assert.NoError(c.Close()) + + f, err := ioutil.TempFile("", "") + assert.NoError(err) + defer os.Remove(f.Name()) + + c.master = f + assert.NoError(c.Close()) +} + +func TestConsolePtsnameFail(t *testing.T) { + assert := assert.New(t) + + pts, err := ptsname(nil) + assert.Error(err) + assert.Empty(pts) +} diff --git a/cli/create.go b/cli/create.go new file mode 100644 index 0000000000..af75ae62a6 --- /dev/null +++ b/cli/create.go @@ -0,0 +1,382 @@ +// Copyright (c) 2014,2015,2016 Docker, Inc. +// Copyright (c) 2017 Intel Corporation +// +// 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. + +package main + +import ( + "errors" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" + + vc "github.com/kata-containers/runtime/virtcontainers" + "github.com/kata-containers/runtime/virtcontainers/pkg/oci" + "github.com/sirupsen/logrus" + "github.com/urfave/cli" +) + +var createCLICommand = cli.Command{ + Name: "create", + Usage: "Create a container", + ArgsUsage: ` + + is your name for the instance of the container that you + are starting. The name you provide for the container instance must be unique + on your host.`, + Description: `The create command creates an instance of a container for a bundle. The + bundle is a directory with a specification file named "` + specConfig + `" and a + root filesystem. + The specification file includes an args parameter. The args parameter is + used to specify command(s) that get run when the container is started. + To change the command(s) that get executed on start, edit the args + parameter of the spec.`, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "bundle, b", + Value: "", + Usage: `path to the root of the bundle directory, defaults to the current directory`, + }, + cli.StringFlag{ + Name: "console", + Value: "", + Usage: "path to a pseudo terminal", + }, + cli.StringFlag{ + Name: "console-socket", + Value: "", + Usage: "path to an AF_UNIX socket which will receive a file descriptor referencing the master end of the console's pseudoterminal", + }, + cli.StringFlag{ + Name: "pid-file", + Value: "", + Usage: "specify the file to write the process id to", + }, + }, + Action: func(context *cli.Context) error { + runtimeConfig, ok := context.App.Metadata["runtimeConfig"].(oci.RuntimeConfig) + if !ok { + return errors.New("invalid runtime config") + } + + console, err := setupConsole(context.String("console"), context.String("console-socket")) + if err != nil { + return err + } + + return create(context.Args().First(), + context.String("bundle"), + console, + context.String("pid-file"), + true, + runtimeConfig, + ) + }, +} + +// Use a variable to allow tests to modify its value +var getKernelParamsFunc = getKernelParams + +func create(containerID, bundlePath, console, pidFilePath string, detach bool, + runtimeConfig oci.RuntimeConfig) error { + var err error + + // Checks the MUST and MUST NOT from OCI runtime specification + if bundlePath, err = validCreateParams(containerID, bundlePath); err != nil { + return err + } + + ociSpec, err := oci.ParseConfigJSON(bundlePath) + if err != nil { + return err + } + + containerType, err := ociSpec.ContainerType() + if err != nil { + return err + } + + disableOutput := noNeedForOutput(detach, ociSpec.Process.Terminal) + + var process vc.Process + + switch containerType { + case vc.PodSandbox: + process, err = createPod(ociSpec, runtimeConfig, containerID, bundlePath, console, disableOutput) + if err != nil { + return err + } + case vc.PodContainer: + process, err = createContainer(ociSpec, containerID, bundlePath, console, disableOutput) + if err != nil { + return err + } + } + + // config.json provides a cgroups path that has to be used to create "tasks" + // and "cgroups.procs" files. Those files have to be filled with a PID, which + // is shim's in our case. This is mandatory to make sure there is no one + // else (like Docker) trying to create those files on our behalf. We want to + // know those files location so that we can remove them when delete is called. + cgroupsPathList, err := processCgroupsPath(ociSpec, containerType.IsPod()) + if err != nil { + return err + } + + // cgroupsDirPath is CgroupsPath fetch from OCI spec + var cgroupsDirPath string + if ociSpec.Linux != nil { + cgroupsDirPath = ociSpec.Linux.CgroupsPath + } + + if err := createCgroupsFiles(containerID, cgroupsDirPath, cgroupsPathList, process.Pid); err != nil { + return err + } + + // Creation of PID file has to be the last thing done in the create + // because containerd considers the create complete after this file + // is created. + return createPIDFile(pidFilePath, process.Pid) +} + +func getKernelParams(containerID string) []vc.Param { + return []vc.Param{ + { + Key: "init", + Value: "/usr/lib/systemd/systemd", + }, + { + Key: "systemd.unit", + Value: systemdUnitName, + }, + { + Key: "systemd.mask", + Value: "systemd-networkd.service", + }, + { + Key: "systemd.mask", + Value: "systemd-networkd.socket", + }, + { + Key: "ip", + Value: fmt.Sprintf("::::::%s::off::", containerID), + }, + } +} + +// setKernelParams adds the user-specified kernel parameters (from the +// configuration file) to the defaults so that the former take priority. +func setKernelParams(containerID string, runtimeConfig *oci.RuntimeConfig) error { + defaultKernelParams := getKernelParamsFunc(containerID) + + if runtimeConfig.HypervisorConfig.Debug { + strParams := vc.SerializeParams(defaultKernelParams, "=") + formatted := strings.Join(strParams, " ") + + kataLog.WithField("default-kernel-parameters", formatted).Debug() + } + + // retrieve the parameters specified in the config file + userKernelParams := runtimeConfig.HypervisorConfig.KernelParams + + // reset + runtimeConfig.HypervisorConfig.KernelParams = []vc.Param{} + + // first, add default values + for _, p := range defaultKernelParams { + if err := (runtimeConfig).AddKernelParam(p); err != nil { + return err + } + } + + // now re-add the user-specified values so that they take priority. + for _, p := range userKernelParams { + if err := (runtimeConfig).AddKernelParam(p); err != nil { + return err + } + } + + return nil +} + +func createPod(ociSpec oci.CompatOCISpec, runtimeConfig oci.RuntimeConfig, + containerID, bundlePath, console string, disableOutput bool) (vc.Process, error) { + + err := setKernelParams(containerID, &runtimeConfig) + if err != nil { + return vc.Process{}, err + } + + podConfig, err := oci.PodConfig(ociSpec, runtimeConfig, bundlePath, containerID, console, disableOutput) + if err != nil { + return vc.Process{}, err + } + + pod, err := vci.CreatePod(podConfig) + if err != nil { + return vc.Process{}, err + } + + containers := pod.GetAllContainers() + if len(containers) != 1 { + return vc.Process{}, fmt.Errorf("BUG: Container list from pod is wrong, expecting only one container, found %d containers", len(containers)) + } + + return containers[0].Process(), nil +} + +func createContainer(ociSpec oci.CompatOCISpec, containerID, bundlePath, + console string, disableOutput bool) (vc.Process, error) { + + contConfig, err := oci.ContainerConfig(ociSpec, bundlePath, containerID, console, disableOutput) + if err != nil { + return vc.Process{}, err + } + + podID, err := ociSpec.PodID() + if err != nil { + return vc.Process{}, err + } + + _, c, err := vci.CreateContainer(podID, contConfig) + if err != nil { + return vc.Process{}, err + } + + return c.Process(), nil +} + +func createCgroupsFiles(containerID string, cgroupsDirPath string, cgroupsPathList []string, pid int) error { + if len(cgroupsPathList) == 0 { + fields := logrus.Fields{ + "container": containerID, + "pid": pid, + } + kataLog.WithFields(fields).Info("Cgroups files not created because cgroupsPath was empty") + return nil + } + + for _, cgroupsPath := range cgroupsPathList { + if err := os.MkdirAll(cgroupsPath, cgroupsDirMode); err != nil { + return err + } + + if strings.Contains(cgroupsPath, "cpu") && cgroupsDirPath != "" { + parent := strings.TrimSuffix(cgroupsPath, cgroupsDirPath) + copyParentCPUSet(cgroupsPath, parent) + } + + tasksFilePath := filepath.Join(cgroupsPath, cgroupsTasksFile) + procsFilePath := filepath.Join(cgroupsPath, cgroupsProcsFile) + + pidStr := fmt.Sprintf("%d", pid) + + for _, path := range []string{tasksFilePath, procsFilePath} { + f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, cgroupsFileMode) + if err != nil { + return err + } + defer f.Close() + + n, err := f.WriteString(pidStr) + if err != nil { + return err + } + + if n < len(pidStr) { + return fmt.Errorf("Could not write pid to %q: only %d bytes written out of %d", + path, n, len(pidStr)) + } + } + } + + return nil +} + +func createPIDFile(pidFilePath string, pid int) error { + if pidFilePath == "" { + // runtime should not fail since pid file is optional + return nil + } + + if err := os.RemoveAll(pidFilePath); err != nil { + return err + } + + f, err := os.Create(pidFilePath) + if err != nil { + return err + } + defer f.Close() + + pidStr := fmt.Sprintf("%d", pid) + + n, err := f.WriteString(pidStr) + if err != nil { + return err + } + + if n < len(pidStr) { + return fmt.Errorf("Could not write pid to '%s': only %d bytes written out of %d", pidFilePath, n, len(pidStr)) + } + + return nil +} + +// copyParentCPUSet copies the cpuset.cpus and cpuset.mems from the parent +// directory to the current directory if the file's contents are 0 +func copyParentCPUSet(current, parent string) error { + currentCpus, currentMems, err := getCPUSet(current) + if err != nil { + return err + } + + parentCpus, parentMems, err := getCPUSet(parent) + if err != nil { + return err + } + + if len(parentCpus) < 1 || len(parentMems) < 1 { + return nil + } + + var cgroupsFileMode = os.FileMode(0600) + if isEmptyString(currentCpus) { + if err := writeFile(filepath.Join(current, "cpuset.cpus"), string(parentCpus), cgroupsFileMode); err != nil { + return err + } + } + + if isEmptyString(currentMems) { + if err := writeFile(filepath.Join(current, "cpuset.mems"), string(parentMems), cgroupsFileMode); err != nil { + return err + } + } + + return nil +} + +func getCPUSet(parent string) (cpus []byte, mems []byte, err error) { + if cpus, err = ioutil.ReadFile(filepath.Join(parent, "cpuset.cpus")); err != nil { + return + } + + if mems, err = ioutil.ReadFile(filepath.Join(parent, "cpuset.mems")); err != nil { + return + } + + return cpus, mems, nil +} diff --git a/cli/create_test.go b/cli/create_test.go new file mode 100644 index 0000000000..41f7e94f3c --- /dev/null +++ b/cli/create_test.go @@ -0,0 +1,1197 @@ +// Copyright (c) 2017 Intel Corporation +// +// 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. + +package main + +import ( + "errors" + "flag" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" + "testing" + + vc "github.com/kata-containers/runtime/virtcontainers" + "github.com/kata-containers/runtime/virtcontainers/pkg/oci" + "github.com/kata-containers/runtime/virtcontainers/pkg/vcMock" + specs "github.com/opencontainers/runtime-spec/specs-go" + "github.com/stretchr/testify/assert" + "github.com/urfave/cli" +) + +const ( + testPID = 100 + testConsole = "/dev/pts/999" + testContainerTypeAnnotation = "io.kubernetes.cri-o.ContainerType" + testSandboxIDAnnotation = "io.kubernetes.cri-o.SandboxID" + testContainerTypePod = "sandbox" + testContainerTypeContainer = "container" +) + +var testStrPID = fmt.Sprintf("%d", testPID) + +func mockCPUSetContent(contents map[string]string) error { + for filePath, data := range contents { + if err := writeFile(filePath, data, testFileMode); err != nil { + return err + } + } + + return nil +} + +func testCreateCgroupsFilesSuccessful(t *testing.T, cgroupsDirPath string, cgroupsPathList []string, pid int) { + if err := createCgroupsFiles("foo", cgroupsDirPath, cgroupsPathList, pid); err != nil { + t.Fatalf("This test should succeed (cgroupsPath %q, pid %d): %s", cgroupsPathList, pid, err) + } +} + +// return the value of the *last* param with the specified key +func findLastParam(key string, params []vc.Param) (string, error) { + if key == "" { + return "", errors.New("ERROR: need non-nil key") + } + + l := len(params) + if l == 0 { + return "", errors.New("ERROR: no params") + } + + for i := l - 1; i >= 0; i-- { + p := params[i] + + if key == p.Key { + return p.Value, nil + } + } + + return "", fmt.Errorf("no param called %q found", name) +} + +func TestCgroupsFilesEmptyCgroupsPathSuccessful(t *testing.T) { + testCreateCgroupsFilesSuccessful(t, "", []string{}, testPID) +} + +func TestCreateCgroupsFilesFailToWriteFile(t *testing.T) { + if os.Geteuid() == 0 { + // The os.FileMode(0000) trick doesn't work for root. + t.Skip(testDisabledNeedNonRoot) + } + + assert := assert.New(t) + + tmpdir, err := ioutil.TempDir("", "") + assert.NoError(err) + defer os.RemoveAll(tmpdir) + + // create the file as a directory to force an error + file := filepath.Join(tmpdir, "cgroups-file") + err = os.MkdirAll(file, os.FileMode(0000)) + assert.NoError(err) + + files := []string{file} + + err = createCgroupsFiles("foo", "cgroups-file", files, testPID) + assert.Error(err) +} + +func TestCgroupsFilesNonEmptyCgroupsPathSuccessful(t *testing.T) { + cgroupsPath, err := ioutil.TempDir(testDir, "cgroups-path-") + if err != nil { + t.Fatalf("Could not create temporary cgroups directory: %s", err) + } + + testCreateCgroupsFilesSuccessful(t, "cgroups-path-", []string{cgroupsPath}, testPID) + + defer os.RemoveAll(cgroupsPath) + + tasksPath := filepath.Join(cgroupsPath, cgroupsTasksFile) + procsPath := filepath.Join(cgroupsPath, cgroupsProcsFile) + + for _, path := range []string{tasksPath, procsPath} { + if _, err := os.Stat(path); err != nil { + t.Fatalf("Path %q should have been created: %s", path, err) + } + + fileBytes, err := ioutil.ReadFile(path) + if err != nil { + t.Fatalf("Could not read %q previously created: %s", path, err) + } + + if string(fileBytes) != testStrPID { + t.Fatalf("PID %s read from %q different from expected PID %s", string(fileBytes), path, testStrPID) + } + } +} + +func TestCreatePIDFileSuccessful(t *testing.T) { + pidDirPath, err := ioutil.TempDir(testDir, "pid-path-") + if err != nil { + t.Fatalf("Could not create temporary PID directory: %s", err) + } + + pidFilePath := filepath.Join(pidDirPath, "pid-file-path") + if err := createPIDFile(pidFilePath, testPID); err != nil { + t.Fatal(err) + } + + fileBytes, err := ioutil.ReadFile(pidFilePath) + if err != nil { + os.RemoveAll(pidFilePath) + t.Fatalf("Could not read %q: %s", pidFilePath, err) + } + + if string(fileBytes) != testStrPID { + os.RemoveAll(pidFilePath) + t.Fatalf("PID %s read from %q different from expected PID %s", string(fileBytes), pidFilePath, testStrPID) + } + + os.RemoveAll(pidFilePath) +} + +func TestCreatePIDFileEmptyPathSuccessful(t *testing.T) { + file := "" + if err := createPIDFile(file, testPID); err != nil { + t.Fatalf("This test should not fail (pidFilePath %q, pid %d)", file, testPID) + } +} + +func TestCreatePIDFileUnableToRemove(t *testing.T) { + if os.Geteuid() == 0 { + // The os.FileMode(0000) trick doesn't work for root. + t.Skip(testDisabledNeedNonRoot) + } + + assert := assert.New(t) + + tmpdir, err := ioutil.TempDir("", "") + assert.NoError(err) + defer os.RemoveAll(tmpdir) + + subdir := filepath.Join(tmpdir, "dir") + file := filepath.Join(subdir, "pidfile") + + // stop non-root user from removing the directory later + err = os.MkdirAll(subdir, os.FileMode(0000)) + assert.NoError(err) + + err = createPIDFile(file, testPID) + assert.Error(err) + + // let it be deleted + os.Chmod(subdir, testDirMode) +} + +func TestCreatePIDFileUnableToCreate(t *testing.T) { + assert := assert.New(t) + + tmpdir, err := ioutil.TempDir("", "") + assert.NoError(err) + defer os.RemoveAll(tmpdir) + + subdir := filepath.Join(tmpdir, "dir") + file := filepath.Join(subdir, "pidfile") + + err = createPIDFile(file, testPID) + + // subdir doesn't exist + assert.Error(err) + os.Chmod(subdir, testDirMode) +} + +func TestCreateCLIFunctionNoRuntimeConfig(t *testing.T) { + assert := assert.New(t) + + app := cli.NewApp() + ctx := cli.NewContext(app, nil, nil) + app.Name = "foo" + ctx.App.Metadata = map[string]interface{}{ + "foo": "bar", + } + + fn, ok := createCLICommand.Action.(func(context *cli.Context) error) + assert.True(ok) + + err := fn(ctx) + + // no runtime config in the Metadata + assert.Error(err) +} + +func TestCreateCLIFunctionSetupConsoleFail(t *testing.T) { + assert := assert.New(t) + + tmpdir, err := ioutil.TempDir("", "") + assert.NoError(err) + defer os.RemoveAll(tmpdir) + + runtimeConfig, err := newTestRuntimeConfig(tmpdir, testConsole, true) + assert.NoError(err) + + subdir := filepath.Join(tmpdir, "dir") + + // does not exist + consoleSocketPath := filepath.Join(subdir, "console") + + set := flag.NewFlagSet("", 0) + + set.String("console-socket", consoleSocketPath, "") + + app := cli.NewApp() + ctx := cli.NewContext(app, set, nil) + app.Name = "foo" + + ctx.App.Metadata = map[string]interface{}{ + "runtimeConfig": runtimeConfig, + } + + fn, ok := createCLICommand.Action.(func(context *cli.Context) error) + assert.True(ok) + + err = fn(ctx) + + // failed to setup console + assert.Error(err) +} + +func TestCreateCLIFunctionCreateFail(t *testing.T) { + assert := assert.New(t) + + tmpdir, err := ioutil.TempDir("", "") + assert.NoError(err) + defer os.RemoveAll(tmpdir) + + runtimeConfig, err := newTestRuntimeConfig(tmpdir, testConsole, true) + assert.NoError(err) + + set := flag.NewFlagSet("", 0) + + set.String("console-socket", "", "") + + app := cli.NewApp() + ctx := cli.NewContext(app, set, nil) + app.Name = "foo" + + ctx.App.Metadata = map[string]interface{}{ + "runtimeConfig": runtimeConfig, + } + + fn, ok := createCLICommand.Action.(func(context *cli.Context) error) + assert.True(ok) + + err = fn(ctx) + + // create() failed + assert.Error(err) +} + +func TestCreateInvalidArgs(t *testing.T) { + assert := assert.New(t) + + pod := &vcMock.Pod{ + MockID: testPodID, + MockContainers: []*vcMock.Container{ + {MockID: testContainerID}, + {MockID: testContainerID}, + {MockID: testContainerID}, + }, + } + + testingImpl.CreatePodFunc = func(podConfig vc.PodConfig) (vc.VCPod, error) { + return pod, nil + } + + testingImpl.ListPodFunc = func() ([]vc.PodStatus, error) { + // No pre-existing pods + return []vc.PodStatus{}, nil + } + + defer func() { + testingImpl.CreatePodFunc = nil + testingImpl.ListPodFunc = nil + }() + + tmpdir, err := ioutil.TempDir("", "") + assert.NoError(err) + defer os.RemoveAll(tmpdir) + + runtimeConfig, err := newTestRuntimeConfig(tmpdir, testConsole, true) + assert.NoError(err) + + bundlePath := filepath.Join(tmpdir, "bundle") + + err = makeOCIBundle(bundlePath) + assert.NoError(err) + + pidFilePath := filepath.Join(tmpdir, "pidfile.txt") + + type testData struct { + containerID string + bundlePath string + console string + pidFilePath string + detach bool + runtimeConfig oci.RuntimeConfig + } + + data := []testData{ + {"", "", "", "", false, oci.RuntimeConfig{}}, + {"", "", "", "", true, oci.RuntimeConfig{}}, + {"foo", "", "", "", true, oci.RuntimeConfig{}}, + {testContainerID, bundlePath, testConsole, pidFilePath, false, runtimeConfig}, + {testContainerID, bundlePath, testConsole, pidFilePath, true, runtimeConfig}, + } + + for i, d := range data { + err := create(d.containerID, d.bundlePath, d.console, d.pidFilePath, d.detach, d.runtimeConfig) + assert.Errorf(err, "test %d (%+v)", i, d) + } +} + +func TestCreateInvalidConfigJSON(t *testing.T) { + assert := assert.New(t) + + testingImpl.ListPodFunc = func() ([]vc.PodStatus, error) { + // No pre-existing pods + return []vc.PodStatus{}, nil + } + + defer func() { + testingImpl.ListPodFunc = nil + }() + + tmpdir, err := ioutil.TempDir("", "") + assert.NoError(err) + defer os.RemoveAll(tmpdir) + + runtimeConfig, err := newTestRuntimeConfig(tmpdir, testConsole, true) + assert.NoError(err) + + bundlePath := filepath.Join(tmpdir, "bundle") + + err = makeOCIBundle(bundlePath) + assert.NoError(err) + + pidFilePath := filepath.Join(tmpdir, "pidfile.txt") + + ociConfigFile := filepath.Join(bundlePath, "config.json") + assert.True(fileExists(ociConfigFile)) + + f, err := os.OpenFile(ociConfigFile, os.O_APPEND|os.O_WRONLY, testFileMode) + assert.NoError(err) + + // invalidate the JSON + _, err = f.WriteString("{") + assert.NoError(err) + f.Close() + + for detach := range []bool{true, false} { + err := create(testContainerID, bundlePath, testConsole, pidFilePath, true, runtimeConfig) + assert.Errorf(err, "%+v", detach) + assert.False(vcMock.IsMockError(err)) + } +} + +func TestCreateInvalidContainerType(t *testing.T) { + assert := assert.New(t) + + testingImpl.ListPodFunc = func() ([]vc.PodStatus, error) { + // No pre-existing pods + return []vc.PodStatus{}, nil + } + + defer func() { + testingImpl.ListPodFunc = nil + }() + + tmpdir, err := ioutil.TempDir("", "") + assert.NoError(err) + defer os.RemoveAll(tmpdir) + + runtimeConfig, err := newTestRuntimeConfig(tmpdir, testConsole, true) + assert.NoError(err) + + bundlePath := filepath.Join(tmpdir, "bundle") + + err = makeOCIBundle(bundlePath) + assert.NoError(err) + + pidFilePath := filepath.Join(tmpdir, "pidfile.txt") + + ociConfigFile := filepath.Join(bundlePath, "config.json") + assert.True(fileExists(ociConfigFile)) + + spec, err := readOCIConfigFile(ociConfigFile) + assert.NoError(err) + + // Force an invalid container type + spec.Annotations = make(map[string]string) + spec.Annotations[testContainerTypeAnnotation] = "I-am-not-a-valid-container-type" + + // rewrite the file + err = writeOCIConfigFile(spec, ociConfigFile) + assert.NoError(err) + + for detach := range []bool{true, false} { + err := create(testContainerID, bundlePath, testConsole, pidFilePath, true, runtimeConfig) + assert.Errorf(err, "%+v", detach) + assert.False(vcMock.IsMockError(err)) + } +} + +func TestCreateContainerInvalid(t *testing.T) { + assert := assert.New(t) + + testingImpl.ListPodFunc = func() ([]vc.PodStatus, error) { + // No pre-existing pods + return []vc.PodStatus{}, nil + } + + defer func() { + testingImpl.ListPodFunc = nil + }() + + tmpdir, err := ioutil.TempDir("", "") + assert.NoError(err) + defer os.RemoveAll(tmpdir) + + runtimeConfig, err := newTestRuntimeConfig(tmpdir, testConsole, true) + assert.NoError(err) + + bundlePath := filepath.Join(tmpdir, "bundle") + + err = makeOCIBundle(bundlePath) + assert.NoError(err) + + pidFilePath := filepath.Join(tmpdir, "pidfile.txt") + + ociConfigFile := filepath.Join(bundlePath, "config.json") + assert.True(fileExists(ociConfigFile)) + + spec, err := readOCIConfigFile(ociConfigFile) + + assert.NoError(err) + + // Force createContainer() to be called. + spec.Annotations = make(map[string]string) + spec.Annotations[testContainerTypeAnnotation] = testContainerTypeContainer + + // rewrite the file + err = writeOCIConfigFile(spec, ociConfigFile) + assert.NoError(err) + + for detach := range []bool{true, false} { + err := create(testContainerID, bundlePath, testConsole, pidFilePath, true, runtimeConfig) + assert.Errorf(err, "%+v", detach) + assert.False(vcMock.IsMockError(err)) + } +} + +func TestCreateProcessCgroupsPathSuccessful(t *testing.T) { + assert := assert.New(t) + + pod := &vcMock.Pod{ + MockID: testPodID, + MockContainers: []*vcMock.Container{ + {MockID: testContainerID}, + }, + } + + testingImpl.ListPodFunc = func() ([]vc.PodStatus, error) { + // No pre-existing pods + return []vc.PodStatus{}, nil + } + + testingImpl.CreatePodFunc = func(podConfig vc.PodConfig) (vc.VCPod, error) { + return pod, nil + } + + defer func() { + testingImpl.ListPodFunc = nil + testingImpl.CreatePodFunc = nil + }() + + tmpdir, err := ioutil.TempDir("", "") + assert.NoError(err) + defer os.RemoveAll(tmpdir) + + runtimeConfig, err := newTestRuntimeConfig(tmpdir, testConsole, true) + assert.NoError(err) + + bundlePath := filepath.Join(tmpdir, "bundle") + + err = makeOCIBundle(bundlePath) + assert.NoError(err) + + pidFilePath := filepath.Join(tmpdir, "pidfile.txt") + + ociConfigFile := filepath.Join(bundlePath, "config.json") + assert.True(fileExists(ociConfigFile)) + + spec, err := readOCIConfigFile(ociConfigFile) + assert.NoError(err) + + // Force pod-type container + spec.Annotations = make(map[string]string) + spec.Annotations[testContainerTypeAnnotation] = testContainerTypePod + + // Set a limit to ensure processCgroupsPath() considers the + // cgroup part of the spec + limit := int64(1024 * 1024) + spec.Linux.Resources.Memory = &specs.LinuxMemory{ + Limit: &limit, + } + + // Set an absolute path + spec.Linux.CgroupsPath = "/this/is/a/cgroup/path" + + var mounts []specs.Mount + foundMount := false + + // Replace the standard cgroup destination with a temporary one. + for _, mount := range spec.Mounts { + if mount.Type == "cgroup" { + foundMount = true + cgroupDir, err := ioutil.TempDir("", "cgroup") + assert.NoError(err) + + defer os.RemoveAll(cgroupDir) + mount.Destination = cgroupDir + } + + mounts = append(mounts, mount) + } + + assert.True(foundMount) + + // Replace mounts with the newly created one. + spec.Mounts = mounts + + // Rewrite the file + err = writeOCIConfigFile(spec, ociConfigFile) + assert.NoError(err) + + for _, detach := range []bool{true, false} { + err := create(testContainerID, bundlePath, testConsole, pidFilePath, detach, runtimeConfig) + assert.NoError(err, "detached: %+v", detach) + } +} + +func TestCreateCreateCgroupsFilesFail(t *testing.T) { + if os.Geteuid() == 0 { + // The os.FileMode(0000) trick doesn't work for root. + t.Skip(testDisabledNeedNonRoot) + } + + assert := assert.New(t) + + pod := &vcMock.Pod{ + MockID: testPodID, + MockContainers: []*vcMock.Container{ + {MockID: testContainerID}, + }, + } + + testingImpl.ListPodFunc = func() ([]vc.PodStatus, error) { + // No pre-existing pods + return []vc.PodStatus{}, nil + } + + testingImpl.CreatePodFunc = func(podConfig vc.PodConfig) (vc.VCPod, error) { + return pod, nil + } + + defer func() { + testingImpl.ListPodFunc = nil + testingImpl.CreatePodFunc = nil + }() + + tmpdir, err := ioutil.TempDir("", "") + assert.NoError(err) + defer os.RemoveAll(tmpdir) + + runtimeConfig, err := newTestRuntimeConfig(tmpdir, testConsole, true) + assert.NoError(err) + + bundlePath := filepath.Join(tmpdir, "bundle") + + err = makeOCIBundle(bundlePath) + assert.NoError(err) + + pidFilePath := filepath.Join(tmpdir, "pidfile.txt") + + ociConfigFile := filepath.Join(bundlePath, "config.json") + assert.True(fileExists(ociConfigFile)) + + spec, err := readOCIConfigFile(ociConfigFile) + assert.NoError(err) + + // Force pod-type container + spec.Annotations = make(map[string]string) + spec.Annotations[testContainerTypeAnnotation] = testContainerTypePod + + // Set a limit to ensure processCgroupsPath() considers the + // cgroup part of the spec + limit := int64(1024 * 1024) + spec.Linux.Resources.Memory = &specs.LinuxMemory{ + Limit: &limit, + } + + // Override + cgroupsDirPath = filepath.Join(tmpdir, "cgroups") + err = os.MkdirAll(cgroupsDirPath, testDirMode) + assert.NoError(err) + + // Set a relative path + spec.Linux.CgroupsPath = "./a/relative/path" + + dir := filepath.Join(cgroupsDirPath, "memory") + + // Stop directory from being created + err = os.MkdirAll(dir, os.FileMode(0000)) + assert.NoError(err) + + // Rewrite the file + err = writeOCIConfigFile(spec, ociConfigFile) + assert.NoError(err) + + for detach := range []bool{true, false} { + err := create(testContainerID, bundlePath, testConsole, pidFilePath, true, runtimeConfig) + assert.Errorf(err, "%+v", detach) + assert.False(vcMock.IsMockError(err)) + } +} + +func TestCreateCreateCreatePidFileFail(t *testing.T) { + if os.Geteuid() == 0 { + // The os.FileMode(0000) trick doesn't work for root. + t.Skip(testDisabledNeedNonRoot) + } + + assert := assert.New(t) + + pod := &vcMock.Pod{ + MockID: testPodID, + MockContainers: []*vcMock.Container{ + {MockID: testContainerID}, + }, + } + + testingImpl.ListPodFunc = func() ([]vc.PodStatus, error) { + // No pre-existing pods + return []vc.PodStatus{}, nil + } + + testingImpl.CreatePodFunc = func(podConfig vc.PodConfig) (vc.VCPod, error) { + return pod, nil + } + + defer func() { + testingImpl.ListPodFunc = nil + testingImpl.CreatePodFunc = nil + }() + + tmpdir, err := ioutil.TempDir("", "") + assert.NoError(err) + defer os.RemoveAll(tmpdir) + + runtimeConfig, err := newTestRuntimeConfig(tmpdir, testConsole, true) + assert.NoError(err) + + bundlePath := filepath.Join(tmpdir, "bundle") + + err = makeOCIBundle(bundlePath) + assert.NoError(err) + + pidDir := filepath.Join(tmpdir, "pid") + pidFilePath := filepath.Join(pidDir, "pidfile.txt") + + ociConfigFile := filepath.Join(bundlePath, "config.json") + assert.True(fileExists(ociConfigFile)) + + spec, err := readOCIConfigFile(ociConfigFile) + assert.NoError(err) + + // Force pod-type container + spec.Annotations = make(map[string]string) + spec.Annotations[testContainerTypeAnnotation] = testContainerTypePod + + // Set a limit to ensure processCgroupsPath() considers the + // cgroup part of the spec + limit := int64(1024 * 1024) + spec.Linux.Resources.Memory = &specs.LinuxMemory{ + Limit: &limit, + } + + // Rewrite the file + err = writeOCIConfigFile(spec, ociConfigFile) + assert.NoError(err) + + // stop the pidfile from being created + err = os.MkdirAll(pidDir, os.FileMode(0000)) + assert.NoError(err) + + for detach := range []bool{true, false} { + err := create(testContainerID, bundlePath, testConsole, pidFilePath, true, runtimeConfig) + assert.Errorf(err, "%+v", detach) + assert.False(vcMock.IsMockError(err)) + } +} + +func TestCreate(t *testing.T) { + assert := assert.New(t) + + pod := &vcMock.Pod{ + MockID: testPodID, + MockContainers: []*vcMock.Container{ + {MockID: testContainerID}, + }, + } + + testingImpl.ListPodFunc = func() ([]vc.PodStatus, error) { + // No pre-existing pods + return []vc.PodStatus{}, nil + } + + testingImpl.CreatePodFunc = func(podConfig vc.PodConfig) (vc.VCPod, error) { + return pod, nil + } + + defer func() { + testingImpl.ListPodFunc = nil + testingImpl.CreatePodFunc = nil + }() + + tmpdir, err := ioutil.TempDir("", "") + assert.NoError(err) + defer os.RemoveAll(tmpdir) + + runtimeConfig, err := newTestRuntimeConfig(tmpdir, testConsole, true) + assert.NoError(err) + + bundlePath := filepath.Join(tmpdir, "bundle") + + err = makeOCIBundle(bundlePath) + assert.NoError(err) + + pidFilePath := filepath.Join(tmpdir, "pidfile.txt") + + ociConfigFile := filepath.Join(bundlePath, "config.json") + assert.True(fileExists(ociConfigFile)) + + spec, err := readOCIConfigFile(ociConfigFile) + assert.NoError(err) + + // Force pod-type container + spec.Annotations = make(map[string]string) + spec.Annotations[testContainerTypeAnnotation] = testContainerTypePod + + // Set a limit to ensure processCgroupsPath() considers the + // cgroup part of the spec + limit := int64(1024 * 1024) + spec.Linux.Resources.Memory = &specs.LinuxMemory{ + Limit: &limit, + } + + // Rewrite the file + err = writeOCIConfigFile(spec, ociConfigFile) + assert.NoError(err) + + for detach := range []bool{true, false} { + err := create(testContainerID, bundlePath, testConsole, pidFilePath, true, runtimeConfig) + assert.NoError(err, "%+v", detach) + } +} + +func TestCreateInvalidKernelParams(t *testing.T) { + assert := assert.New(t) + + testingImpl.ListPodFunc = func() ([]vc.PodStatus, error) { + // No pre-existing pods + return []vc.PodStatus{}, nil + } + + defer func() { + testingImpl.ListPodFunc = nil + }() + + tmpdir, err := ioutil.TempDir("", "") + assert.NoError(err) + defer os.RemoveAll(tmpdir) + + runtimeConfig, err := newTestRuntimeConfig(tmpdir, testConsole, true) + assert.NoError(err) + + bundlePath := filepath.Join(tmpdir, "bundle") + + err = makeOCIBundle(bundlePath) + assert.NoError(err) + + pidFilePath := filepath.Join(tmpdir, "pidfile.txt") + + ociConfigFile := filepath.Join(bundlePath, "config.json") + assert.True(fileExists(ociConfigFile)) + + spec, err := readOCIConfigFile(ociConfigFile) + assert.NoError(err) + + // Force createPod() to be called. + spec.Annotations = make(map[string]string) + spec.Annotations[testContainerTypeAnnotation] = testContainerTypePod + + // rewrite the file + err = writeOCIConfigFile(spec, ociConfigFile) + assert.NoError(err) + + savedFunc := getKernelParamsFunc + defer func() { + getKernelParamsFunc = savedFunc + }() + + getKernelParamsFunc = func(containerID string) []vc.Param { + return []vc.Param{ + { + Key: "", + Value: "", + }, + } + } + + for detach := range []bool{true, false} { + err := create(testContainerID, bundlePath, testConsole, pidFilePath, true, runtimeConfig) + assert.Errorf(err, "%+v", detach) + assert.False(vcMock.IsMockError(err)) + } +} + +func TestCreateCreatePodPodConfigFail(t *testing.T) { + assert := assert.New(t) + + testingImpl.ListPodFunc = func() ([]vc.PodStatus, error) { + // No pre-existing pods + return []vc.PodStatus{}, nil + } + + defer func() { + testingImpl.ListPodFunc = nil + }() + + tmpdir, err := ioutil.TempDir("", "") + assert.NoError(err) + defer os.RemoveAll(tmpdir) + + runtimeConfig, err := newTestRuntimeConfig(tmpdir, testConsole, true) + assert.NoError(err) + + bundlePath := filepath.Join(tmpdir, "bundle") + + err = makeOCIBundle(bundlePath) + assert.NoError(err) + + ociConfigFile := filepath.Join(bundlePath, "config.json") + assert.True(fileExists(ociConfigFile)) + + spec, err := readOCIConfigFile(ociConfigFile) + assert.NoError(err) + + quota := int64(0) + limit := int64(0) + + spec.Linux.Resources.Memory = &specs.LinuxMemory{ + Limit: &limit, + } + + spec.Linux.Resources.CPU = &specs.LinuxCPU{ + // specify an invalid value + Quota: "a, + } + + _, err = createPod(spec, runtimeConfig, testContainerID, bundlePath, testConsole, true) + assert.Error(err) + assert.False(vcMock.IsMockError(err)) +} + +func TestCreateCreatePodFail(t *testing.T) { + assert := assert.New(t) + + testingImpl.ListPodFunc = func() ([]vc.PodStatus, error) { + // No pre-existing pods + return []vc.PodStatus{}, nil + } + + defer func() { + testingImpl.ListPodFunc = nil + }() + + tmpdir, err := ioutil.TempDir("", "") + assert.NoError(err) + defer os.RemoveAll(tmpdir) + + runtimeConfig, err := newTestRuntimeConfig(tmpdir, testConsole, true) + assert.NoError(err) + + bundlePath := filepath.Join(tmpdir, "bundle") + + err = makeOCIBundle(bundlePath) + assert.NoError(err) + + ociConfigFile := filepath.Join(bundlePath, "config.json") + assert.True(fileExists(ociConfigFile)) + + spec, err := readOCIConfigFile(ociConfigFile) + assert.NoError(err) + + _, err = createPod(spec, runtimeConfig, testContainerID, bundlePath, testConsole, true) + assert.Error(err) + assert.True(vcMock.IsMockError(err)) +} + +func TestCreateCreateContainerContainerConfigFail(t *testing.T) { + assert := assert.New(t) + + testingImpl.ListPodFunc = func() ([]vc.PodStatus, error) { + // No pre-existing pods + return []vc.PodStatus{}, nil + } + + defer func() { + testingImpl.ListPodFunc = nil + }() + + tmpdir, err := ioutil.TempDir("", "") + assert.NoError(err) + defer os.RemoveAll(tmpdir) + + bundlePath := filepath.Join(tmpdir, "bundle") + + err = makeOCIBundle(bundlePath) + assert.NoError(err) + + ociConfigFile := filepath.Join(bundlePath, "config.json") + assert.True(fileExists(ociConfigFile)) + + spec, err := readOCIConfigFile(ociConfigFile) + assert.NoError(err) + + // Set invalid container type + containerType := "你好,世界" + spec.Annotations = make(map[string]string) + spec.Annotations[testContainerTypeAnnotation] = containerType + + // rewrite file + err = writeOCIConfigFile(spec, ociConfigFile) + assert.NoError(err) + + for _, disableOutput := range []bool{true, false} { + _, err = createContainer(spec, testContainerID, bundlePath, testConsole, disableOutput) + assert.Error(err) + assert.False(vcMock.IsMockError(err)) + assert.True(strings.Contains(err.Error(), containerType)) + } +} + +func TestCreateCreateContainerFail(t *testing.T) { + assert := assert.New(t) + + testingImpl.ListPodFunc = func() ([]vc.PodStatus, error) { + // No pre-existing pods + return []vc.PodStatus{}, nil + } + + defer func() { + testingImpl.ListPodFunc = nil + }() + + tmpdir, err := ioutil.TempDir("", "") + assert.NoError(err) + defer os.RemoveAll(tmpdir) + + bundlePath := filepath.Join(tmpdir, "bundle") + + err = makeOCIBundle(bundlePath) + assert.NoError(err) + + ociConfigFile := filepath.Join(bundlePath, "config.json") + assert.True(fileExists(ociConfigFile)) + + spec, err := readOCIConfigFile(ociConfigFile) + assert.NoError(err) + + // set expected container type and podID + spec.Annotations = make(map[string]string) + spec.Annotations[testContainerTypeAnnotation] = testContainerTypeContainer + spec.Annotations[testSandboxIDAnnotation] = testPodID + + // rewrite file + err = writeOCIConfigFile(spec, ociConfigFile) + assert.NoError(err) + + for _, disableOutput := range []bool{true, false} { + _, err = createContainer(spec, testContainerID, bundlePath, testConsole, disableOutput) + assert.Error(err) + assert.True(vcMock.IsMockError(err)) + } +} + +func TestCreateCreateContainer(t *testing.T) { + assert := assert.New(t) + + testingImpl.ListPodFunc = func() ([]vc.PodStatus, error) { + // No pre-existing pods + return []vc.PodStatus{}, nil + } + + testingImpl.CreateContainerFunc = func(podID string, containerConfig vc.ContainerConfig) (vc.VCPod, vc.VCContainer, error) { + return &vcMock.Pod{}, &vcMock.Container{}, nil + } + + defer func() { + testingImpl.ListPodFunc = nil + testingImpl.CreateContainerFunc = nil + }() + + tmpdir, err := ioutil.TempDir("", "") + assert.NoError(err) + defer os.RemoveAll(tmpdir) + + bundlePath := filepath.Join(tmpdir, "bundle") + + err = makeOCIBundle(bundlePath) + assert.NoError(err) + + ociConfigFile := filepath.Join(bundlePath, "config.json") + assert.True(fileExists(ociConfigFile)) + + spec, err := readOCIConfigFile(ociConfigFile) + assert.NoError(err) + + // set expected container type and podID + spec.Annotations = make(map[string]string) + spec.Annotations[testContainerTypeAnnotation] = testContainerTypeContainer + spec.Annotations[testSandboxIDAnnotation] = testPodID + + // rewrite file + err = writeOCIConfigFile(spec, ociConfigFile) + assert.NoError(err) + + for _, disableOutput := range []bool{true, false} { + _, err = createContainer(spec, testContainerID, bundlePath, testConsole, disableOutput) + assert.NoError(err) + } +} + +func TestCopyParentCPUSetFail(t *testing.T) { + assert := assert.New(t) + + cgroupsPath, err := ioutil.TempDir(testDir, "cgroups-path-") + assert.NoError(err) + defer os.RemoveAll(cgroupsPath) + + err = copyParentCPUSet(cgroupsPath, testDir) + assert.Error(err) +} + +func TestCopyParentCPUSetSuccessful(t *testing.T) { + assert := assert.New(t) + + cgroupsPath, err := ioutil.TempDir(testDir, "cgroups-path-") + assert.NoError(err) + defer os.RemoveAll(cgroupsPath) + + cgroupsSrcPath := filepath.Join(cgroupsPath, "src") + err = os.Mkdir(cgroupsSrcPath, testDirMode) + assert.NoError(err) + + err = mockCPUSetContent(map[string]string{ + filepath.Join(cgroupsSrcPath, "cpuset.cpus"): "0-1", + filepath.Join(cgroupsSrcPath, "cpuset.mems"): "0-1", + }) + assert.NoError(err) + + cgroupsDstPath := filepath.Join(cgroupsPath, "dst") + err = os.Mkdir(cgroupsDstPath, testDirMode) + assert.NoError(err) + + fd, err := os.Create(filepath.Join(cgroupsDstPath, "cpuset.cpus")) + assert.NoError(err) + fd.Close() + + fd, err = os.Create(filepath.Join(cgroupsDstPath, "cpuset.mems")) + assert.NoError(err) + fd.Close() + + err = copyParentCPUSet(cgroupsDstPath, cgroupsSrcPath) + assert.NoError(err) + + currentCpus, currentMems, err := getCPUSet(cgroupsDstPath) + assert.NoError(err) + + assert.False(isEmptyString(currentCpus)) + assert.False(isEmptyString(currentMems)) +} + +func TestSetKernelParams(t *testing.T) { + assert := assert.New(t) + + config := oci.RuntimeConfig{} + + assert.Empty(config.HypervisorConfig.KernelParams) + + err := setKernelParams(testContainerID, &config) + assert.NoError(err) + + assert.NotEmpty(config.HypervisorConfig.KernelParams) +} + +func TestSetKernelParamsUserOptionTakesPriority(t *testing.T) { + assert := assert.New(t) + + initName := "init" + initValue := "/sbin/myinit" + + ipName := "ip" + ipValue := "127.0.0.1" + + params := []vc.Param{ + {Key: initName, Value: initValue}, + {Key: ipName, Value: ipValue}, + } + + hypervisorConfig := vc.HypervisorConfig{ + KernelParams: params, + } + + // Config containing user-specified kernel parameters + config := oci.RuntimeConfig{ + HypervisorConfig: hypervisorConfig, + } + + assert.NotEmpty(config.HypervisorConfig.KernelParams) + + err := setKernelParams(testContainerID, &config) + assert.NoError(err) + + kernelParams := config.HypervisorConfig.KernelParams + + init, err := findLastParam(initName, kernelParams) + assert.NoError(err) + assert.Equal(initValue, init) + + ip, err := findLastParam(ipName, kernelParams) + assert.NoError(err) + assert.Equal(ipValue, ip) + +} diff --git a/cli/delete.go b/cli/delete.go new file mode 100644 index 0000000000..53fb97dda4 --- /dev/null +++ b/cli/delete.go @@ -0,0 +1,155 @@ +// Copyright (c) 2014,2015,2016 Docker, Inc. +// Copyright (c) 2017 Intel Corporation +// +// 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. + +package main + +import ( + "fmt" + "os" + + vc "github.com/kata-containers/runtime/virtcontainers" + "github.com/kata-containers/runtime/virtcontainers/pkg/oci" + "github.com/urfave/cli" +) + +var deleteCLICommand = cli.Command{ + Name: "delete", + Usage: "Delete any resources held by one or more containers", + ArgsUsage: ` [container-id...] + + is the name for the instance of the container. + +EXAMPLE: + If the container id is "ubuntu01" and ` + name + ` list currently shows the + status of "ubuntu01" as "stopped" the following will delete resources held + for "ubuntu01" removing "ubuntu01" from the ` + name + ` list of containers: + + # ` + name + ` delete ubuntu01`, + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "force, f", + Usage: "Forcibly deletes the container if it is still running (uses SIGKILL)", + }, + }, + Action: func(context *cli.Context) error { + args := context.Args() + if args.Present() == false { + return fmt.Errorf("Missing container ID, should at least provide one") + } + + force := context.Bool("force") + for _, cID := range []string(args) { + if err := delete(cID, force); err != nil { + return err + } + } + + return nil + }, +} + +func delete(containerID string, force bool) error { + // Checks the MUST and MUST NOT from OCI runtime specification + status, podID, err := getExistingContainerInfo(containerID) + if err != nil { + return err + } + + containerID = status.ID + + containerType, err := oci.GetContainerType(status.Annotations) + if err != nil { + return err + } + + // Retrieve OCI spec configuration. + ociSpec, err := oci.GetOCIConfig(status) + if err != nil { + return err + } + + forceStop := false + if oci.StateToOCIState(status.State) == oci.StateRunning { + if !force { + return fmt.Errorf("Container still running, should be stopped") + } + + forceStop = true + } + + switch containerType { + case vc.PodSandbox: + if err := deletePod(podID); err != nil { + return err + } + case vc.PodContainer: + if err := deleteContainer(podID, containerID, forceStop); err != nil { + return err + } + default: + return fmt.Errorf("Invalid container type found") + } + + // In order to prevent any file descriptor leak related to cgroups files + // that have been previously created, we have to remove them before this + // function returns. + cgroupsPathList, err := processCgroupsPath(ociSpec, containerType.IsPod()) + if err != nil { + return err + } + + return removeCgroupsPath(containerID, cgroupsPathList) +} + +func deletePod(podID string) error { + if _, err := vci.StopPod(podID); err != nil { + return err + } + + if _, err := vci.DeletePod(podID); err != nil { + return err + } + + return nil +} + +func deleteContainer(podID, containerID string, forceStop bool) error { + if forceStop { + if _, err := vci.StopContainer(podID, containerID); err != nil { + return err + } + } + + if _, err := vci.DeleteContainer(podID, containerID); err != nil { + return err + } + + return nil +} + +func removeCgroupsPath(containerID string, cgroupsPathList []string) error { + if len(cgroupsPathList) == 0 { + kataLog.WithField("container", containerID).Info("Cgroups files not removed because cgroupsPath was empty") + return nil + } + + for _, cgroupsPath := range cgroupsPathList { + if err := os.RemoveAll(cgroupsPath); err != nil { + return err + } + } + + return nil +} diff --git a/cli/delete_test.go b/cli/delete_test.go new file mode 100644 index 0000000000..25ac8a7592 --- /dev/null +++ b/cli/delete_test.go @@ -0,0 +1,597 @@ +// Copyright (c) 2017 Intel Corporation +// +// 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. + +package main + +import ( + "flag" + "io/ioutil" + "os" + "path/filepath" + "testing" + + vc "github.com/kata-containers/runtime/virtcontainers" + vcAnnotations "github.com/kata-containers/runtime/virtcontainers/pkg/annotations" + "github.com/kata-containers/runtime/virtcontainers/pkg/vcMock" + "github.com/stretchr/testify/assert" + "github.com/urfave/cli" +) + +func testRemoveCgroupsPathSuccessful(t *testing.T, cgroupsPathList []string) { + if err := removeCgroupsPath("foo", cgroupsPathList); err != nil { + t.Fatalf("This test should succeed (cgroupsPathList = %v): %s", cgroupsPathList, err) + } +} + +func TestRemoveCgroupsPathEmptyPathSuccessful(t *testing.T) { + testRemoveCgroupsPathSuccessful(t, []string{}) +} + +func TestRemoveCgroupsPathNonEmptyPathSuccessful(t *testing.T) { + cgroupsPath, err := ioutil.TempDir(testDir, "cgroups-path-") + if err != nil { + t.Fatalf("Could not create temporary cgroups directory: %s", err) + } + + if err := os.MkdirAll(cgroupsPath, testDirMode); err != nil { + t.Fatalf("CgroupsPath directory %q could not be created: %s", cgroupsPath, err) + } + + testRemoveCgroupsPathSuccessful(t, []string{cgroupsPath}) + + if _, err := os.Stat(cgroupsPath); err == nil { + t.Fatalf("CgroupsPath directory %q should have been removed: %s", cgroupsPath, err) + } +} + +func TestDeleteInvalidContainer(t *testing.T) { + assert := assert.New(t) + + // Missing container id + err := delete("", false) + assert.Error(err) + assert.False(vcMock.IsMockError(err)) + + // Mock Listpod error + err = delete(testContainerID, false) + assert.Error(err) + assert.True(vcMock.IsMockError(err)) + + testingImpl.ListPodFunc = func() ([]vc.PodStatus, error) { + return []vc.PodStatus{}, nil + } + + defer func() { + testingImpl.ListPodFunc = nil + }() + + // Container missing in ListPod + err = delete(testContainerID, false) + assert.Error(err) + assert.False(vcMock.IsMockError(err)) +} + +func TestDeleteMissingContainerTypeAnnotation(t *testing.T) { + assert := assert.New(t) + + pod := &vcMock.Pod{ + MockID: testPodID, + } + + testingImpl.ListPodFunc = func() ([]vc.PodStatus, error) { + return []vc.PodStatus{ + { + ID: pod.ID(), + ContainersStatus: []vc.ContainerStatus{ + { + ID: pod.ID(), + Annotations: map[string]string{}, + }, + }, + }, + }, nil + } + + defer func() { + testingImpl.ListPodFunc = nil + }() + + err := delete(pod.ID(), false) + assert.Error(err) + assert.False(vcMock.IsMockError(err)) +} + +func TestDeleteInvalidConfig(t *testing.T) { + assert := assert.New(t) + + pod := &vcMock.Pod{ + MockID: testPodID, + } + + testingImpl.ListPodFunc = func() ([]vc.PodStatus, error) { + return []vc.PodStatus{ + { + ID: pod.ID(), + ContainersStatus: []vc.ContainerStatus{ + { + ID: pod.ID(), + Annotations: map[string]string{ + vcAnnotations.ContainerTypeKey: string(vc.PodSandbox), + }, + }, + }, + }, + }, nil + } + + defer func() { + testingImpl.ListPodFunc = nil + }() + + err := delete(pod.ID(), false) + assert.Error(err) + assert.False(vcMock.IsMockError(err)) +} + +func testConfigSetup(t *testing.T) string { + assert := assert.New(t) + + tmpdir, err := ioutil.TempDir("", "") + assert.NoError(err) + + bundlePath := filepath.Join(tmpdir, "bundle") + err = os.MkdirAll(bundlePath, testDirMode) + assert.NoError(err) + + err = createOCIConfig(bundlePath) + assert.NoError(err) + + // config json path + configPath := filepath.Join(bundlePath, "config.json") + return configPath +} + +func TestDeletePod(t *testing.T) { + assert := assert.New(t) + + pod := &vcMock.Pod{ + MockID: testPodID, + } + + configPath := testConfigSetup(t) + configJSON, err := readOCIConfigJSON(configPath) + assert.NoError(err) + + testingImpl.ListPodFunc = func() ([]vc.PodStatus, error) { + return []vc.PodStatus{ + { + ID: pod.ID(), + ContainersStatus: []vc.ContainerStatus{ + { + ID: pod.ID(), + Annotations: map[string]string{ + vcAnnotations.ContainerTypeKey: string(vc.PodSandbox), + vcAnnotations.ConfigJSONKey: configJSON, + }, + State: vc.State{ + State: "ready", + }, + }, + }, + }, + }, nil + } + + defer func() { + testingImpl.ListPodFunc = nil + }() + + err = delete(pod.ID(), false) + assert.Error(err) + assert.True(vcMock.IsMockError(err)) + + testingImpl.StopPodFunc = func(podID string) (vc.VCPod, error) { + return pod, nil + } + + defer func() { + testingImpl.StopPodFunc = nil + }() + + err = delete(pod.ID(), false) + assert.Error(err) + assert.True(vcMock.IsMockError(err)) + + testingImpl.DeletePodFunc = func(podID string) (vc.VCPod, error) { + return pod, nil + } + + defer func() { + testingImpl.DeletePodFunc = nil + }() + + err = delete(pod.ID(), false) + assert.Nil(err) +} + +func TestDeleteInvalidContainerType(t *testing.T) { + assert := assert.New(t) + + pod := &vcMock.Pod{ + MockID: testPodID, + } + + configPath := testConfigSetup(t) + configJSON, err := readOCIConfigJSON(configPath) + assert.NoError(err) + + testingImpl.ListPodFunc = func() ([]vc.PodStatus, error) { + return []vc.PodStatus{ + { + ID: pod.ID(), + ContainersStatus: []vc.ContainerStatus{ + { + ID: pod.ID(), + Annotations: map[string]string{ + vcAnnotations.ContainerTypeKey: "InvalidType", + vcAnnotations.ConfigJSONKey: configJSON, + }, + State: vc.State{ + State: "created", + }, + }, + }, + }, + }, nil + } + + defer func() { + testingImpl.ListPodFunc = nil + }() + + // Delete an invalid container type + err = delete(pod.ID(), false) + assert.Error(err) + assert.False(vcMock.IsMockError(err)) +} + +func TestDeletePodRunning(t *testing.T) { + assert := assert.New(t) + + pod := &vcMock.Pod{ + MockID: testPodID, + } + + configPath := testConfigSetup(t) + configJSON, err := readOCIConfigJSON(configPath) + assert.NoError(err) + + testingImpl.ListPodFunc = func() ([]vc.PodStatus, error) { + return []vc.PodStatus{ + { + ID: pod.ID(), + ContainersStatus: []vc.ContainerStatus{ + { + ID: pod.ID(), + Annotations: map[string]string{ + vcAnnotations.ContainerTypeKey: string(vc.PodSandbox), + vcAnnotations.ConfigJSONKey: configJSON, + }, + State: vc.State{ + State: "running", + }, + }, + }, + }, + }, nil + } + + defer func() { + testingImpl.ListPodFunc = nil + }() + + // Delete on a running pod should fail + err = delete(pod.ID(), false) + assert.Error(err) + assert.False(vcMock.IsMockError(err)) + + testingImpl.StopPodFunc = func(podID string) (vc.VCPod, error) { + return pod, nil + } + + defer func() { + testingImpl.StopPodFunc = nil + }() + + // Force delete a running pod + err = delete(pod.ID(), true) + assert.Error(err) + assert.True(vcMock.IsMockError(err)) + + testingImpl.DeletePodFunc = func(podID string) (vc.VCPod, error) { + return pod, nil + } + + defer func() { + testingImpl.DeletePodFunc = nil + }() + + err = delete(pod.ID(), true) + assert.Nil(err) +} + +func TestDeleteRunningContainer(t *testing.T) { + assert := assert.New(t) + + pod := &vcMock.Pod{ + MockID: testPodID, + } + + pod.MockContainers = []*vcMock.Container{ + { + MockID: testContainerID, + MockPod: pod, + }, + } + + configPath := testConfigSetup(t) + configJSON, err := readOCIConfigJSON(configPath) + assert.NoError(err) + + testingImpl.ListPodFunc = func() ([]vc.PodStatus, error) { + return []vc.PodStatus{ + { + ID: pod.ID(), + ContainersStatus: []vc.ContainerStatus{ + { + ID: pod.MockContainers[0].ID(), + Annotations: map[string]string{ + vcAnnotations.ContainerTypeKey: string(vc.PodContainer), + vcAnnotations.ConfigJSONKey: configJSON, + }, + State: vc.State{ + State: "running", + }, + }, + }, + }, + }, nil + } + + defer func() { + testingImpl.ListPodFunc = nil + }() + + // Delete on a running container should fail. + err = delete(pod.MockContainers[0].ID(), false) + assert.Error(err) + assert.False(vcMock.IsMockError(err)) + + // force delete + err = delete(pod.MockContainers[0].ID(), true) + assert.Error(err) + assert.True(vcMock.IsMockError(err)) + + testingImpl.StopContainerFunc = testStopContainerFuncReturnNil + defer func() { + testingImpl.StopContainerFunc = nil + }() + + err = delete(pod.MockContainers[0].ID(), true) + assert.Error(err) + assert.True(vcMock.IsMockError(err)) + + testingImpl.DeleteContainerFunc = func(podID, containerID string) (vc.VCContainer, error) { + return &vcMock.Container{}, nil + } + + defer func() { + testingImpl.DeleteContainerFunc = nil + }() + + err = delete(pod.MockContainers[0].ID(), true) + assert.Nil(err) +} + +func TestDeleteContainer(t *testing.T) { + assert := assert.New(t) + + pod := &vcMock.Pod{ + MockID: testPodID, + } + + pod.MockContainers = []*vcMock.Container{ + { + MockID: testContainerID, + MockPod: pod, + }, + } + + configPath := testConfigSetup(t) + configJSON, err := readOCIConfigJSON(configPath) + assert.NoError(err) + + testingImpl.ListPodFunc = func() ([]vc.PodStatus, error) { + return []vc.PodStatus{ + { + ID: pod.ID(), + ContainersStatus: []vc.ContainerStatus{ + { + ID: pod.MockContainers[0].ID(), + Annotations: map[string]string{ + vcAnnotations.ContainerTypeKey: string(vc.PodContainer), + vcAnnotations.ConfigJSONKey: configJSON, + }, + State: vc.State{ + State: "ready", + }, + }, + }, + }, + }, nil + } + + defer func() { + testingImpl.ListPodFunc = nil + }() + + err = delete(pod.MockContainers[0].ID(), false) + assert.Error(err) + assert.True(vcMock.IsMockError(err)) + + testingImpl.StopContainerFunc = testStopContainerFuncReturnNil + defer func() { + testingImpl.StopContainerFunc = nil + }() + + err = delete(pod.MockContainers[0].ID(), false) + assert.Error(err) + assert.True(vcMock.IsMockError(err)) + + testingImpl.DeleteContainerFunc = func(podID, containerID string) (vc.VCContainer, error) { + return &vcMock.Container{}, nil + } + + defer func() { + testingImpl.DeleteContainerFunc = nil + }() + + err = delete(pod.MockContainers[0].ID(), false) + assert.Nil(err) +} + +func TestDeleteCLIFunction(t *testing.T) { + assert := assert.New(t) + + flagSet := &flag.FlagSet{} + app := cli.NewApp() + + ctx := cli.NewContext(app, flagSet, nil) + + fn, ok := deleteCLICommand.Action.(func(context *cli.Context) error) + assert.True(ok) + + // no container id in the Metadata + err := fn(ctx) + assert.Error(err) + assert.False(vcMock.IsMockError(err)) + + flagSet = flag.NewFlagSet("container-id", flag.ContinueOnError) + flagSet.Parse([]string{"xyz"}) + ctx = cli.NewContext(app, flagSet, nil) + + err = fn(ctx) + assert.Error(err) + assert.True(vcMock.IsMockError(err)) +} + +func TestDeleteCLIFunctionSuccess(t *testing.T) { + assert := assert.New(t) + + pod := &vcMock.Pod{ + MockID: testPodID, + } + + pod.MockContainers = []*vcMock.Container{ + { + MockID: testContainerID, + MockPod: pod, + }, + } + + configPath := testConfigSetup(t) + configJSON, err := readOCIConfigJSON(configPath) + assert.NoError(err) + + testingImpl.ListPodFunc = func() ([]vc.PodStatus, error) { + return []vc.PodStatus{ + { + ID: pod.ID(), + ContainersStatus: []vc.ContainerStatus{ + { + ID: pod.ID(), + Annotations: map[string]string{ + vcAnnotations.ContainerTypeKey: string(vc.PodSandbox), + vcAnnotations.ConfigJSONKey: configJSON, + }, + State: vc.State{ + State: "ready", + }, + }, + }, + }, + }, nil + } + + testingImpl.StopPodFunc = func(podID string) (vc.VCPod, error) { + return pod, nil + } + + testingImpl.DeletePodFunc = func(podID string) (vc.VCPod, error) { + return pod, nil + } + + defer func() { + testingImpl.ListPodFunc = nil + testingImpl.StopPodFunc = nil + testingImpl.DeletePodFunc = nil + }() + + flagSet := &flag.FlagSet{} + app := cli.NewApp() + + ctx := cli.NewContext(app, flagSet, nil) + + fn, ok := deleteCLICommand.Action.(func(context *cli.Context) error) + assert.True(ok) + + err = fn(ctx) + assert.Error(err) + assert.False(vcMock.IsMockError(err)) + + flagSet = flag.NewFlagSet("container-id", flag.ContinueOnError) + flagSet.Parse([]string{pod.ID()}) + ctx = cli.NewContext(app, flagSet, nil) + assert.NotNil(ctx) + + err = fn(ctx) + assert.NoError(err) +} + +func TestRemoveCGroupsPath(t *testing.T) { + if os.Geteuid() == 0 { + t.Skip(testDisabledNeedNonRoot) + } + + assert := assert.New(t) + + tmpdir, err := ioutil.TempDir("", "") + assert.NoError(err) + defer os.RemoveAll(tmpdir) + + dir := filepath.Join(tmpdir, "dir") + + err = os.Mkdir(dir, testDirMode) + assert.NoError(err) + + // make directory unreadable by non-root user + err = os.Chmod(tmpdir, 0000) + assert.NoError(err) + defer func() { + _ = os.Chmod(tmpdir, 0755) + }() + + err = removeCgroupsPath("foo", []string{dir}) + assert.Error(err) +} diff --git a/cli/exec.go b/cli/exec.go new file mode 100644 index 0000000000..2817ce724c --- /dev/null +++ b/cli/exec.go @@ -0,0 +1,273 @@ +// Copyright (c) 2014,2015,2016 Docker, Inc. +// Copyright (c) 2017 Intel Corporation +// +// 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. + +package main + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + "syscall" + + vc "github.com/kata-containers/runtime/virtcontainers" + "github.com/kata-containers/runtime/virtcontainers/pkg/oci" + specs "github.com/opencontainers/runtime-spec/specs-go" + "github.com/urfave/cli" +) + +type execParams struct { + ociProcess oci.CompatOCIProcess + cID string + pidFile string + console string + consoleSock string + processLabel string + detach bool + noSubreaper bool +} + +var execCLICommand = cli.Command{ + Name: "exec", + Usage: "Execute new process inside the container", + ArgsUsage: ` [command options] || -p process.json + + is the name for the instance of the container and + is the command to be executed in the container. can't be empty + unless a "-p" flag provided. + +EXAMPLE: + If the container is configured to run the linux ps command the following + will output a list of processes running in the container: + + # ` + name + ` ps`, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "console", + Usage: "path to a pseudo terminal", + }, + cli.StringFlag{ + Name: "console-socket", + Value: "", + Usage: "path to an AF_UNIX socket which will receive a file descriptor referencing the master end of the console's pseudoterminal", + }, + cli.StringFlag{ + Name: "cwd", + Usage: "current working directory in the container", + }, + cli.StringSliceFlag{ + Name: "env, e", + Usage: "set environment variables", + }, + cli.BoolFlag{ + Name: "tty, t", + Usage: "allocate a pseudo-TTY", + }, + cli.StringFlag{ + Name: "user, u", + Usage: "UID (format: [:])", + }, + cli.StringFlag{ + Name: "process, p", + Usage: "path to the process.json", + }, + cli.BoolFlag{ + Name: "detach,d", + Usage: "detach from the container's process", + }, + cli.StringFlag{ + Name: "pid-file", + Value: "", + Usage: "specify the file to write the process id to", + }, + cli.StringFlag{ + Name: "process-label", + Usage: "set the asm process label for the process commonly used with selinux", + }, + cli.StringFlag{ + Name: "apparmor", + Usage: "set the apparmor profile for the process", + }, + cli.BoolFlag{ + Name: "no-new-privs", + Usage: "set the no new privileges value for the process", + }, + cli.StringSliceFlag{ + Name: "cap, c", + Value: &cli.StringSlice{}, + Usage: "add a capability to the bounding set for the process", + }, + cli.BoolFlag{ + Name: "no-subreaper", + Usage: "disable the use of the subreaper used to reap reparented processes", + Hidden: true, + }, + }, + Action: func(context *cli.Context) error { + return execute(context) + }, +} + +func generateExecParams(context *cli.Context, specProcess *oci.CompatOCIProcess) (execParams, error) { + ctxArgs := context.Args() + + params := execParams{ + cID: ctxArgs.First(), + pidFile: context.String("pid-file"), + console: context.String("console"), + consoleSock: context.String("console-socket"), + detach: context.Bool("detach"), + processLabel: context.String("process-label"), + noSubreaper: context.Bool("no-subreaper"), + } + + if context.String("process") != "" { + var ociProcess oci.CompatOCIProcess + + fileContent, err := ioutil.ReadFile(context.String("process")) + if err != nil { + return execParams{}, err + } + + if err := json.Unmarshal(fileContent, &ociProcess); err != nil { + return execParams{}, err + } + + params.ociProcess = ociProcess + } else { + params.ociProcess = *specProcess + + // Override terminal + if context.IsSet("tty") { + params.ociProcess.Terminal = context.Bool("tty") + } + + // Override user + if context.String("user") != "" { + params.ociProcess.User = specs.User{ + // This field is a Windows-only field + // according to the specification. However, it + // is abused here to allow the username + // specified in the OCI runtime configuration + // file to be overridden by a CLI request. + Username: context.String("user"), + } + } + + // Override env + params.ociProcess.Env = append(params.ociProcess.Env, context.StringSlice("env")...) + + // Override cwd + if context.String("cwd") != "" { + params.ociProcess.Cwd = context.String("cwd") + } + + // Override no-new-privs + if context.IsSet("no-new-privs") { + params.ociProcess.NoNewPrivileges = context.Bool("no-new-privs") + } + + // Override apparmor + if context.String("apparmor") != "" { + params.ociProcess.ApparmorProfile = context.String("apparmor") + } + + params.ociProcess.Args = ctxArgs.Tail() + } + + return params, nil +} + +func execute(context *cli.Context) error { + containerID := context.Args().First() + status, podID, err := getExistingContainerInfo(containerID) + if err != nil { + return err + } + + // Retrieve OCI spec configuration. + ociSpec, err := oci.GetOCIConfig(status) + if err != nil { + return err + } + + params, err := generateExecParams(context, ociSpec.Process) + if err != nil { + return err + } + + params.cID = status.ID + + // container MUST be running + if status.State.State != vc.StateRunning { + return fmt.Errorf("Container %s is not running", params.cID) + } + + envVars, err := oci.EnvVars(params.ociProcess.Env) + if err != nil { + return err + } + + consolePath, err := setupConsole(params.console, params.consoleSock) + if err != nil { + return err + } + + user := fmt.Sprintf("%d:%d", params.ociProcess.User.UID, params.ociProcess.User.GID) + + if params.ociProcess.User.Username != "" { + user = params.ociProcess.User.Username + } + + cmd := vc.Cmd{ + Args: params.ociProcess.Args, + Envs: envVars, + WorkDir: params.ociProcess.Cwd, + User: user, + Interactive: params.ociProcess.Terminal, + Console: consolePath, + Detach: noNeedForOutput(params.detach, params.ociProcess.Terminal), + } + + _, _, process, err := vci.EnterContainer(podID, params.cID, cmd) + if err != nil { + return err + } + + // Creation of PID file has to be the last thing done in the exec + // because containerd considers the exec to have finished starting + // after this file is created. + if err := createPIDFile(params.pidFile, process.Pid); err != nil { + return err + } + + if params.detach { + return nil + } + + p, err := os.FindProcess(process.Pid) + if err != nil { + return err + } + + ps, err := p.Wait() + if err != nil { + return fmt.Errorf("Process state %s, container info %+v: %v", + ps.String(), status, err) + } + + // Exit code has to be forwarded in this case. + return cli.NewExitError("", ps.Sys().(syscall.WaitStatus).ExitStatus()) +} diff --git a/cli/exec_test.go b/cli/exec_test.go new file mode 100644 index 0000000000..1d45cc9f1c --- /dev/null +++ b/cli/exec_test.go @@ -0,0 +1,696 @@ +// Copyright (c) 2017 Intel Corporation +// +// 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. + +package main + +import ( + "flag" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "testing" + + vc "github.com/kata-containers/runtime/virtcontainers" + vcAnnotations "github.com/kata-containers/runtime/virtcontainers/pkg/annotations" + "github.com/kata-containers/runtime/virtcontainers/pkg/oci" + "github.com/kata-containers/runtime/virtcontainers/pkg/vcMock" + "github.com/stretchr/testify/assert" + "github.com/urfave/cli" +) + +func TestExecCLIFunction(t *testing.T) { + assert := assert.New(t) + + flagSet := &flag.FlagSet{} + app := cli.NewApp() + ctx := cli.NewContext(app, flagSet, nil) + + fn, ok := startCLICommand.Action.(func(context *cli.Context) error) + assert.True(ok) + + // no container-id in the Metadata + err := fn(ctx) + assert.Error(err) + assert.False(vcMock.IsMockError(err)) + + // pass container-id + flagSet = flag.NewFlagSet("container-id", flag.ContinueOnError) + flagSet.Parse([]string{"xyz"}) + ctx = cli.NewContext(app, flagSet, nil) + + err = fn(ctx) + assert.Error(err) + assert.True(vcMock.IsMockError(err)) +} + +func TestExecuteErrors(t *testing.T) { + assert := assert.New(t) + + flagSet := flag.NewFlagSet("", 0) + ctx := cli.NewContext(cli.NewApp(), flagSet, nil) + + // missing container id + err := execute(ctx) + assert.Error(err) + assert.False(vcMock.IsMockError(err)) + + // ListPod error + flagSet.Parse([]string{testContainerID}) + err = execute(ctx) + assert.Error(err) + assert.True(vcMock.IsMockError(err)) + + // Config path missing in annotations + annotations := map[string]string{ + vcAnnotations.ContainerTypeKey: string(vc.PodSandbox), + } + + testingImpl.ListPodFunc = func() ([]vc.PodStatus, error) { + return newSingleContainerPodStatusList(testPodID, testContainerID, vc.State{}, vc.State{}, annotations), nil + } + + defer func() { + testingImpl.ListPodFunc = nil + }() + + err = execute(ctx) + assert.Error(err) + assert.False(vcMock.IsMockError(err)) + + // Container not running + configPath := testConfigSetup(t) + configJSON, err := readOCIConfigJSON(configPath) + assert.NoError(err) + + annotations = map[string]string{ + vcAnnotations.ContainerTypeKey: string(vc.PodSandbox), + vcAnnotations.ConfigJSONKey: configJSON, + } + + testingImpl.ListPodFunc = func() ([]vc.PodStatus, error) { + return newSingleContainerPodStatusList(testPodID, testContainerID, vc.State{}, vc.State{}, annotations), nil + } + + defer func() { + testingImpl.ListPodFunc = nil + }() + + err = execute(ctx) + assert.Error(err) + assert.False(vcMock.IsMockError(err)) +} + +func TestExecuteErrorReadingProcessJson(t *testing.T) { + assert := assert.New(t) + + tmpdir, err := ioutil.TempDir("", "") + assert.NoError(err) + defer os.RemoveAll(tmpdir) + + // non-existent path + processPath := filepath.Join(tmpdir, "process.json") + + flagSet := flag.NewFlagSet("", 0) + flagSet.String("process", processPath, "") + flagSet.Parse([]string{testContainerID}) + ctx := cli.NewContext(cli.NewApp(), flagSet, nil) + + configPath := testConfigSetup(t) + configJSON, err := readOCIConfigJSON(configPath) + assert.NoError(err) + + annotations := map[string]string{ + vcAnnotations.ContainerTypeKey: string(vc.PodSandbox), + vcAnnotations.ConfigJSONKey: configJSON, + } + + state := vc.State{ + State: vc.StateRunning, + } + + testingImpl.ListPodFunc = func() ([]vc.PodStatus, error) { + return newSingleContainerPodStatusList(testPodID, testContainerID, state, state, annotations), nil + } + + defer func() { + testingImpl.ListPodFunc = nil + }() + + // Note: flags can only be tested with the CLI command function + fn, ok := execCLICommand.Action.(func(context *cli.Context) error) + assert.True(ok) + + err = fn(ctx) + assert.Error(err) + assert.False(vcMock.IsMockError(err)) +} + +func TestExecuteErrorOpeningConsole(t *testing.T) { + assert := assert.New(t) + + tmpdir, err := ioutil.TempDir("", "") + assert.NoError(err) + defer os.RemoveAll(tmpdir) + + consoleSock := filepath.Join(tmpdir, "console-sock") + + flagSet := flag.NewFlagSet("", 0) + flagSet.String("console-socket", consoleSock, "") + flagSet.Parse([]string{testContainerID}) + ctx := cli.NewContext(cli.NewApp(), flagSet, nil) + + configPath := testConfigSetup(t) + configJSON, err := readOCIConfigJSON(configPath) + assert.NoError(err) + + annotations := map[string]string{ + vcAnnotations.ContainerTypeKey: string(vc.PodSandbox), + vcAnnotations.ConfigJSONKey: configJSON, + } + + state := vc.State{ + State: vc.StateRunning, + } + + testingImpl.ListPodFunc = func() ([]vc.PodStatus, error) { + return newSingleContainerPodStatusList(testPodID, testContainerID, state, state, annotations), nil + } + + defer func() { + testingImpl.ListPodFunc = nil + }() + + // Note: flags can only be tested with the CLI command function + fn, ok := execCLICommand.Action.(func(context *cli.Context) error) + assert.True(ok) + + err = fn(ctx) + assert.Error(err) + assert.False(vcMock.IsMockError(err)) +} + +func testExecParamsSetup(t *testing.T, pidFilePath, consolePath string, detach bool) *flag.FlagSet { + flagSet := flag.NewFlagSet("", 0) + + flagSet.String("pid-file", pidFilePath, "") + flagSet.String("console", consolePath, "") + flagSet.String("console-socket", "", "") + flagSet.Bool("detach", detach, "") + flagSet.String("process-label", "testlabel", "") + flagSet.Bool("no-subreaper", false, "") + + return flagSet +} + +func TestExecuteWithFlags(t *testing.T) { + assert := assert.New(t) + + tmpdir, err := ioutil.TempDir("", "") + assert.NoError(err) + defer os.RemoveAll(tmpdir) + + pidFilePath := filepath.Join(tmpdir, "pid") + consolePath := "/dev/ptmx" + + flagSet := testExecParamsSetup(t, pidFilePath, consolePath, false) + flagSet.String("user", "root", "") + flagSet.String("cwd", "/home/root", "") + flagSet.String("apparmor", "/tmp/profile", "") + flagSet.Bool("no-new-privs", false, "") + + flagSet.Parse([]string{testContainerID, "/tmp/foo"}) + ctx := cli.NewContext(cli.NewApp(), flagSet, nil) + + configPath := testConfigSetup(t) + configJSON, err := readOCIConfigJSON(configPath) + assert.NoError(err) + + annotations := map[string]string{ + vcAnnotations.ContainerTypeKey: string(vc.PodSandbox), + vcAnnotations.ConfigJSONKey: configJSON, + } + + state := vc.State{ + State: vc.StateRunning, + } + + testingImpl.ListPodFunc = func() ([]vc.PodStatus, error) { + return newSingleContainerPodStatusList(testPodID, testContainerID, state, state, annotations), nil + } + + defer func() { + testingImpl.ListPodFunc = nil + }() + + fn, ok := execCLICommand.Action.(func(context *cli.Context) error) + assert.True(ok) + + // EnterContainer error + err = fn(ctx) + assert.Error(err) + assert.True(vcMock.IsMockError(err)) + + testingImpl.EnterContainerFunc = func(podID, containerID string, cmd vc.Cmd) (vc.VCPod, vc.VCContainer, *vc.Process, error) { + return &vcMock.Pod{}, &vcMock.Container{}, &vc.Process{}, nil + } + + defer func() { + testingImpl.EnterContainerFunc = nil + os.Remove(pidFilePath) + }() + + // Process not running error + err = fn(ctx) + assert.Error(err) + assert.False(vcMock.IsMockError(err)) + + os.Remove(pidFilePath) + + // Process ran and exited successfully + testingImpl.EnterContainerFunc = func(podID, containerID string, cmd vc.Cmd) (vc.VCPod, vc.VCContainer, *vc.Process, error) { + // create a fake container process + workload := []string{"cat", "/dev/null"} + command := exec.Command(workload[0], workload[1:]...) + err := command.Start() + assert.NoError(err, "Unable to start process %v: %s", workload, err) + + vcProcess := vc.Process{} + vcProcess.Pid = command.Process.Pid + return &vcMock.Pod{}, &vcMock.Container{}, &vcProcess, nil + } + + defer func() { + testingImpl.EnterContainerFunc = nil + os.Remove(pidFilePath) + }() + + // Should get an exit code when run in non-detached mode. + err = fn(ctx) + _, ok = err.(*cli.ExitError) + assert.True(ok, true, "Exit code not received for fake workload process") +} + +func TestExecuteWithFlagsDetached(t *testing.T) { + assert := assert.New(t) + + tmpdir, err := ioutil.TempDir("", "") + assert.NoError(err) + defer os.RemoveAll(tmpdir) + + pidFilePath := filepath.Join(tmpdir, "pid") + consolePath := "/dev/ptmx" + detach := true + + flagSet := testExecParamsSetup(t, pidFilePath, consolePath, detach) + flagSet.Parse([]string{testContainerID, "/tmp/foo"}) + ctx := cli.NewContext(cli.NewApp(), flagSet, nil) + + configPath := testConfigSetup(t) + configJSON, err := readOCIConfigJSON(configPath) + assert.NoError(err) + + annotations := map[string]string{ + vcAnnotations.ContainerTypeKey: string(vc.PodSandbox), + vcAnnotations.ConfigJSONKey: configJSON, + } + + state := vc.State{ + State: vc.StateRunning, + } + + testingImpl.ListPodFunc = func() ([]vc.PodStatus, error) { + return newSingleContainerPodStatusList(testPodID, testContainerID, state, state, annotations), nil + } + + defer func() { + testingImpl.ListPodFunc = nil + }() + + testingImpl.EnterContainerFunc = func(podID, containerID string, cmd vc.Cmd) (vc.VCPod, vc.VCContainer, *vc.Process, error) { + // create a fake container process + workload := []string{"cat", "/dev/null"} + command := exec.Command(workload[0], workload[1:]...) + err := command.Start() + assert.NoError(err, "Unable to start process %v: %s", workload, err) + + vcProcess := vc.Process{} + vcProcess.Pid = command.Process.Pid + return &vcMock.Pod{}, &vcMock.Container{}, &vcProcess, nil + } + + defer func() { + testingImpl.EnterContainerFunc = nil + os.Remove(pidFilePath) + }() + + fn, ok := execCLICommand.Action.(func(context *cli.Context) error) + assert.True(ok) + + err = fn(ctx) + assert.NoError(err) +} + +func TestExecuteWithInvalidProcessJson(t *testing.T) { + assert := assert.New(t) + + tmpdir, err := ioutil.TempDir("", "") + assert.NoError(err) + defer os.RemoveAll(tmpdir) + + pidFilePath := filepath.Join(tmpdir, "pid") + consolePath := "/dev/ptmx" + detach := false + + flagSet := testExecParamsSetup(t, pidFilePath, consolePath, detach) + + processPath := filepath.Join(tmpdir, "process.json") + flagSet.String("process", processPath, "") + + f, err := os.OpenFile(processPath, os.O_RDWR|os.O_CREATE, testFileMode) + assert.NoError(err) + + // invalidate the JSON + _, err = f.WriteString("{") + assert.NoError(err) + f.Close() + + defer os.Remove(processPath) + + flagSet.Parse([]string{testContainerID}) + ctx := cli.NewContext(cli.NewApp(), flagSet, nil) + + configPath := testConfigSetup(t) + configJSON, err := readOCIConfigJSON(configPath) + assert.NoError(err) + + annotations := map[string]string{ + vcAnnotations.ContainerTypeKey: string(vc.PodSandbox), + vcAnnotations.ConfigJSONKey: configJSON, + } + + state := vc.State{ + State: vc.StateRunning, + } + + testingImpl.ListPodFunc = func() ([]vc.PodStatus, error) { + return newSingleContainerPodStatusList(testPodID, testContainerID, state, state, annotations), nil + } + + defer func() { + testingImpl.ListPodFunc = nil + }() + + fn, ok := execCLICommand.Action.(func(context *cli.Context) error) + assert.True(ok) + + err = fn(ctx) + assert.Error(err) + assert.False(vcMock.IsMockError(err)) +} + +func TestExecuteWithValidProcessJson(t *testing.T) { + assert := assert.New(t) + + tmpdir, err := ioutil.TempDir("", "") + assert.NoError(err) + defer os.RemoveAll(tmpdir) + + pidFilePath := filepath.Join(tmpdir, "pid") + consolePath := "/dev/ptmx" + + flagSet := testExecParamsSetup(t, pidFilePath, consolePath, false) + + processPath := filepath.Join(tmpdir, "process.json") + flagSet.String("process", processPath, "") + + flagSet.Parse([]string{testContainerID, "/tmp/foo"}) + ctx := cli.NewContext(cli.NewApp(), flagSet, nil) + + configPath := testConfigSetup(t) + configJSON, err := readOCIConfigJSON(configPath) + assert.NoError(err) + + annotations := map[string]string{ + vcAnnotations.ContainerTypeKey: string(vc.PodContainer), + vcAnnotations.ConfigJSONKey: configJSON, + } + + state := vc.State{ + State: vc.StateRunning, + } + + testingImpl.ListPodFunc = func() ([]vc.PodStatus, error) { + return newSingleContainerPodStatusList(testPodID, testContainerID, state, state, annotations), nil + } + + defer func() { + testingImpl.ListPodFunc = nil + }() + + processJSON := `{ + "consoleSize": { + "height": 15, + "width": 15 + }, + "terminal": true, + "user": { + "uid": 0, + "gid": 0 + }, + "args": [ + "sh" + ], + "env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "TERM=xterm" + ], + "cwd": "/" + }` + + f, err := os.OpenFile(processPath, os.O_RDWR|os.O_CREATE, testFileMode) + assert.NoError(err) + + _, err = f.WriteString(processJSON) + assert.NoError(err) + f.Close() + + defer os.Remove(processPath) + + workload := []string{"cat", "/dev/null"} + + testingImpl.EnterContainerFunc = func(podID, containerID string, cmd vc.Cmd) (vc.VCPod, vc.VCContainer, *vc.Process, error) { + // create a fake container process + command := exec.Command(workload[0], workload[1:]...) + err := command.Start() + assert.NoError(err, "Unable to start process %v: %s", workload, err) + + vcProcess := vc.Process{} + vcProcess.Pid = command.Process.Pid + + return &vcMock.Pod{}, &vcMock.Container{}, &vcProcess, nil + } + + defer func() { + testingImpl.EnterContainerFunc = nil + os.Remove(pidFilePath) + }() + + fn, ok := execCLICommand.Action.(func(context *cli.Context) error) + assert.True(ok) + + err = fn(ctx) + exitErr, ok := err.(*cli.ExitError) + assert.True(ok, true, "Exit code not received for fake workload process") + assert.Equal(exitErr.ExitCode(), 0, "Exit code should have been 0 for fake workload %s", workload) +} + +func TestExecuteWithInvalidEnvironment(t *testing.T) { + assert := assert.New(t) + + tmpdir, err := ioutil.TempDir("", "") + assert.NoError(err) + defer os.RemoveAll(tmpdir) + + processPath := filepath.Join(tmpdir, "process.json") + + flagSet := flag.NewFlagSet("", 0) + flagSet.String("process", processPath, "") + flagSet.Parse([]string{testContainerID}) + ctx := cli.NewContext(cli.NewApp(), flagSet, nil) + + configPath := testConfigSetup(t) + configJSON, err := readOCIConfigJSON(configPath) + assert.NoError(err) + + annotations := map[string]string{ + vcAnnotations.ContainerTypeKey: string(vc.PodContainer), + vcAnnotations.ConfigJSONKey: configJSON, + } + + state := vc.State{ + State: vc.StateRunning, + } + + testingImpl.ListPodFunc = func() ([]vc.PodStatus, error) { + return newSingleContainerPodStatusList(testPodID, testContainerID, state, state, annotations), nil + } + + defer func() { + testingImpl.ListPodFunc = nil + }() + + processJSON := `{ + "env": [ + "TERM=" + ] + }` + + f, err := os.OpenFile(processPath, os.O_RDWR|os.O_CREATE, testFileMode) + assert.NoError(err) + + _, err = f.WriteString(processJSON) + assert.NoError(err) + f.Close() + + defer os.Remove(processPath) + + fn, ok := execCLICommand.Action.(func(context *cli.Context) error) + assert.True(ok) + + // vcAnnotations.EnvVars error due to incorrect environment + err = fn(ctx) + assert.Error(err) + assert.False(vcMock.IsMockError(err)) +} + +func TestGenerateExecParams(t *testing.T) { + assert := assert.New(t) + + tmpdir, err := ioutil.TempDir("", "") + assert.NoError(err) + defer os.RemoveAll(tmpdir) + + pidFilePath := filepath.Join(tmpdir, "pid") + consolePath := "/dev/ptmx" + consoleSocket := "/tmp/console-sock" + processLabel := "testlabel" + user := "root" + cwd := "cwd" + apparmor := "apparmorProf" + + flagSet := flag.NewFlagSet("", 0) + flagSet.String("pid-file", pidFilePath, "") + flagSet.String("console", consolePath, "") + flagSet.String("console-socket", consoleSocket, "") + flagSet.String("process-label", processLabel, "") + + flagSet.String("user", user, "") + flagSet.String("cwd", cwd, "") + flagSet.String("apparmor", apparmor, "") + + ctx := cli.NewContext(cli.NewApp(), flagSet, nil) + process := &oci.CompatOCIProcess{} + params, err := generateExecParams(ctx, process) + assert.NoError(err) + + assert.Equal(params.pidFile, pidFilePath) + assert.Equal(params.console, consolePath) + assert.Equal(params.consoleSock, consoleSocket) + assert.Equal(params.processLabel, processLabel) + assert.Equal(params.noSubreaper, false) + assert.Equal(params.detach, false) + + assert.Equal(params.ociProcess.Terminal, false) + assert.Equal(params.ociProcess.User.UID, uint32(0)) + assert.Equal(params.ociProcess.User.GID, uint32(0)) + assert.Equal(params.ociProcess.Cwd, cwd) + assert.Equal(params.ociProcess.ApparmorProfile, apparmor) +} + +func TestGenerateExecParamsWithProcessJsonFile(t *testing.T) { + assert := assert.New(t) + + tmpdir, err := ioutil.TempDir("", "") + assert.NoError(err) + defer os.RemoveAll(tmpdir) + + pidFilePath := filepath.Join(tmpdir, "pid") + consolePath := "/dev/ptmx" + consoleSocket := "/tmp/console-sock" + detach := true + processLabel := "testlabel" + + flagSet := flag.NewFlagSet("", 0) + flagSet.String("pid-file", pidFilePath, "") + flagSet.String("console", consolePath, "") + flagSet.String("console-socket", consoleSocket, "") + flagSet.Bool("detach", detach, "") + flagSet.String("process-label", processLabel, "") + + processPath := filepath.Join(tmpdir, "process.json") + flagSet.String("process", processPath, "") + + flagSet.Parse([]string{testContainerID}) + ctx := cli.NewContext(cli.NewApp(), flagSet, nil) + + processJSON := `{ + "consoleSize": { + "height": 15, + "width": 15 + }, + "terminal": true, + "user": { + "uid": 0, + "gid": 0 + }, + "args": [ + "sh" + ], + "env": [ + "TERM=xterm", + "foo=bar" + ], + "cwd": "/" + }` + + f, err := os.OpenFile(processPath, os.O_RDWR|os.O_CREATE, testFileMode) + assert.NoError(err) + + _, err = f.WriteString(processJSON) + assert.NoError(err) + f.Close() + + defer os.Remove(processPath) + + process := &oci.CompatOCIProcess{} + params, err := generateExecParams(ctx, process) + assert.NoError(err) + + assert.Equal(params.pidFile, pidFilePath) + assert.Equal(params.console, consolePath) + assert.Equal(params.consoleSock, consoleSocket) + assert.Equal(params.processLabel, processLabel) + assert.Equal(params.noSubreaper, false) + assert.Equal(params.detach, detach) + + assert.Equal(params.ociProcess.Terminal, true) + assert.Equal(params.ociProcess.ConsoleSize.Height, uint(15)) + assert.Equal(params.ociProcess.ConsoleSize.Width, uint(15)) + assert.Equal(params.ociProcess.User.UID, uint32(0)) + assert.Equal(params.ociProcess.User.GID, uint32(0)) + assert.Equal(params.ociProcess.Cwd, "/") + assert.Equal(params.ociProcess.Env[0], "TERM=xterm") + assert.Equal(params.ociProcess.Env[1], "foo=bar") +} diff --git a/cli/exit.go b/cli/exit.go new file mode 100644 index 0000000000..76c0067512 --- /dev/null +++ b/cli/exit.go @@ -0,0 +1,37 @@ +// Copyright (c) 2017 Intel Corporation +// +// 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. + +package main + +import "os" + +var atexitFuncs []func() + +var exitFunc = os.Exit + +// atexit registers a function f that will be run when exit is called. The +// handlers so registered will be called the in reverse order of their +// registration. +func atexit(f func()) { + atexitFuncs = append(atexitFuncs, f) +} + +// exit calls all atexit handlers before exiting the process with status. +func exit(status int) { + for i := len(atexitFuncs) - 1; i >= 0; i-- { + f := atexitFuncs[i] + f() + } + exitFunc(status) +} diff --git a/cli/exit_test.go b/cli/exit_test.go new file mode 100644 index 0000000000..46e7385b4d --- /dev/null +++ b/cli/exit_test.go @@ -0,0 +1,51 @@ +// Copyright (c) 2017 Intel Corporation +// +// 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. + +package main + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +var testFoo string + +func testFunc() { + testFoo = "bar" +} + +func TestExit(t *testing.T) { + assert := assert.New(t) + + var testExitStatus int + exitFunc = func(status int) { + testExitStatus = status + } + + defer func() { + exitFunc = os.Exit + }() + + // test with no atexit functions added. + exit(1) + assert.Equal(testExitStatus, 1) + + // test with a function added to the atexit list. + atexit(testFunc) + exit(0) + assert.Equal(testFoo, "bar") + assert.Equal(testExitStatus, 0) +} diff --git a/cli/fatal.go b/cli/fatal.go new file mode 100644 index 0000000000..18cae01731 --- /dev/null +++ b/cli/fatal.go @@ -0,0 +1,80 @@ +// Copyright 2018 Intel Corporation. +// +// SPDX-License-Identifier: Apache-2.0 +// + +package main + +import ( + "bytes" + "fmt" + "os/signal" + "runtime/pprof" + "strings" + "syscall" +) + +// List of fatal signals +var sigFatal = map[syscall.Signal]bool{ + syscall.SIGABRT: true, + syscall.SIGBUS: true, + syscall.SIGILL: true, + syscall.SIGQUIT: true, + syscall.SIGSEGV: true, + syscall.SIGSTKFLT: true, + syscall.SIGSYS: true, + syscall.SIGTRAP: true, +} + +func handlePanic() { + r := recover() + + if r != nil { + msg := fmt.Sprintf("%s", r) + kataLog.WithField("panic", msg).Error("fatal error") + + die() + } +} + +func backtrace() { + profiles := pprof.Profiles() + + buf := &bytes.Buffer{} + + for _, p := range profiles { + // The magic number requests a full stacktrace. See + // https://golang.org/pkg/runtime/pprof/#Profile.WriteTo. + pprof.Lookup(p.Name()).WriteTo(buf, 2) + } + + for _, line := range strings.Split(buf.String(), "\n") { + kataLog.Error(line) + } +} + +func fatalSignal(sig syscall.Signal) bool { + return sigFatal[sig] +} + +func fatalSignals() []syscall.Signal { + var signals []syscall.Signal + + for sig := range sigFatal { + signals = append(signals, sig) + + } + + return signals +} + +func die() { + backtrace() + + if crashOnError { + signal.Reset(syscall.SIGABRT) + syscall.Kill(0, syscall.SIGABRT) + } + + exit(1) +} diff --git a/cli/kata-check.go b/cli/kata-check.go new file mode 100644 index 0000000000..0ffd38e7f4 --- /dev/null +++ b/cli/kata-check.go @@ -0,0 +1,304 @@ +// Copyright (c) 2017-2018 Intel Corporation +// +// 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. + +// Note: To add a new architecture, implement all identifiers beginning "arch". + +package main + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "regexp" + "strings" + + vc "github.com/kata-containers/runtime/virtcontainers" + "github.com/sirupsen/logrus" + "github.com/urfave/cli" +) + +type kernelModule struct { + // description + desc string + + // maps parameter names to values + parameters map[string]string +} + +type vmContainerCapableDetails struct { + cpuInfoFile string + requiredCPUFlags map[string]string + requiredCPUAttribs map[string]string + requiredKernelModules map[string]kernelModule +} + +const ( + moduleParamDir = "parameters" + cpuFlagsTag = "flags" + successMessageCapable = "System is capable of running " + project + successMessageCreate = "System can currently create " + project + failMessage = "System is not capable of running " + project + kernelPropertyCorrect = "Kernel property value correct" +) + +// variables rather than consts to allow tests to modify them +var ( + procCPUInfo = "/proc/cpuinfo" + sysModuleDir = "/sys/module" + modInfoCmd = "modinfo" +) + +// getCPUInfo returns details of the first CPU read from the specified cpuinfo file +func getCPUInfo(cpuInfoFile string) (string, error) { + text, err := getFileContents(cpuInfoFile) + if err != nil { + return "", err + } + + cpus := strings.SplitAfter(text, "\n\n") + + trimmed := strings.TrimSpace(cpus[0]) + if trimmed == "" { + return "", fmt.Errorf("Cannot determine CPU details") + } + + return trimmed, nil +} + +// findAnchoredString searches haystack for needle and returns true if found +func findAnchoredString(haystack, needle string) bool { + if haystack == "" || needle == "" { + return false + } + + // Ensure the search string is anchored + pattern := regexp.MustCompile(`\b` + needle + `\b`) + + return pattern.MatchString(haystack) +} + +// getCPUFlags returns the CPU flags from the cpuinfo file specified +func getCPUFlags(cpuinfo string) string { + for _, line := range strings.Split(cpuinfo, "\n") { + if strings.HasPrefix(line, cpuFlagsTag) { + fields := strings.Split(line, ":") + if len(fields) == 2 { + return strings.TrimSpace(fields[1]) + } + } + } + + return "" +} + +// haveKernelModule returns true if the specified module exists +// (either loaded or available to be loaded) +func haveKernelModule(module string) bool { + // First, check to see if the module is already loaded + path := filepath.Join(sysModuleDir, module) + if fileExists(path) { + return true + } + + // Now, check if the module is unloaded, but available + cmd := exec.Command(modInfoCmd, module) + err := cmd.Run() + return err == nil +} + +// checkCPU checks all required CPU attributes modules and returns a count of +// the number of CPU attribute errors (all of which are logged by this +// function). The specified tag is simply used for logging purposes. +func checkCPU(tag, cpuinfo string, attribs map[string]string) (count uint32) { + if cpuinfo == "" { + return 0 + } + + for attrib, desc := range attribs { + fields := logrus.Fields{ + "type": tag, + "name": attrib, + "description": desc, + } + + found := findAnchoredString(cpuinfo, attrib) + if !found { + kataLog.WithFields(fields).Errorf("CPU property not found") + count++ + continue + + } + + kataLog.WithFields(fields).Infof("CPU property found") + } + + return count +} + +func checkCPUFlags(cpuflags string, required map[string]string) uint32 { + return checkCPU("flag", cpuflags, required) +} + +func checkCPUAttribs(cpuinfo string, attribs map[string]string) uint32 { + return checkCPU("attribute", cpuinfo, attribs) +} + +// kernelParamHandler represents a function that allows kernel module +// parameter errors to be ignored for special scenarios. +// +// The function is passed the following parameters: +// +// onVMM - `true` if the host is running under a VMM environment +// fields - A set of fields showing the expected and actual module parameter values. +// msg - The message that would be logged showing the incorrect kernel module +// parameter. +// +// The function must return `true` if the kernel module parameter error should +// be ignored, or `false` if it is a real error. +// +// Note: it is up to the function to add an appropriate log call if the error +// should be ignored. +type kernelParamHandler func(onVMM bool, fields logrus.Fields, msg string) bool + +// checkKernelModules checks all required kernel modules modules and returns a count of +// the number of module errors (all of which are logged by this +// function). Only fatal errors result in an error return. +func checkKernelModules(modules map[string]kernelModule, handler kernelParamHandler) (count uint32, err error) { + onVMM, err := vc.RunningOnVMM(procCPUInfo) + if err != nil { + return 0, err + } + + for module, details := range modules { + fields := logrus.Fields{ + "type": "module", + "name": module, + "description": details.desc, + } + + if !haveKernelModule(module) { + kataLog.WithFields(fields).Error("kernel property not found") + count++ + continue + } + + kataLog.WithFields(fields).Infof("kernel property found") + + for param, expected := range details.parameters { + path := filepath.Join(sysModuleDir, module, moduleParamDir, param) + value, err := getFileContents(path) + if err != nil { + return 0, err + } + + value = strings.TrimRight(value, "\n\r") + + fields["parameter"] = param + fields["value"] = value + + if value != expected { + fields["expected"] = expected + + msg := "kernel module parameter has unexpected value" + + if handler != nil { + ignoreError := handler(onVMM, fields, msg) + if ignoreError { + continue + } + } + + kataLog.WithFields(fields).Error(msg) + count++ + } + + kataLog.WithFields(fields).Info(kernelPropertyCorrect) + } + } + + return count, nil +} + +// hostIsVMContainerCapable checks to see if the host is theoretically capable +// of creating a VM container. +func hostIsVMContainerCapable(details vmContainerCapableDetails) error { + cpuinfo, err := getCPUInfo(details.cpuInfoFile) + if err != nil { + return err + } + + cpuFlags := getCPUFlags(cpuinfo) + if cpuFlags == "" { + return fmt.Errorf("Cannot find CPU flags") + } + + // Keep a track of the error count, but don't error until all tests + // have been performed! + errorCount := uint32(0) + + count := checkCPUAttribs(cpuinfo, details.requiredCPUAttribs) + + errorCount += count + + count = checkCPUFlags(cpuFlags, details.requiredCPUFlags) + + errorCount += count + + count, err = checkKernelModules(details.requiredKernelModules, archKernelParamHandler) + if err != nil { + return err + } + + errorCount += count + + if errorCount == 0 { + return nil + } + + return fmt.Errorf("ERROR: %s", failMessage) +} + +var kataCheckCLICommand = cli.Command{ + Name: checkCmd, + Usage: "tests if system can run " + project, + Action: func(context *cli.Context) error { + + details := vmContainerCapableDetails{ + cpuInfoFile: procCPUInfo, + requiredCPUFlags: archRequiredCPUFlags, + requiredCPUAttribs: archRequiredCPUAttribs, + requiredKernelModules: archRequiredKernelModules, + } + + err := hostIsVMContainerCapable(details) + + if err != nil { + return err + } + + kataLog.Info(successMessageCapable) + + if os.Geteuid() == 0 { + err = archHostCanCreateVMContainer() + if err != nil { + return err + } + + kataLog.Info(successMessageCreate) + } + + return nil + }, +} diff --git a/cli/kata-check_amd64.go b/cli/kata-check_amd64.go new file mode 100644 index 0000000000..5d5a117082 --- /dev/null +++ b/cli/kata-check_amd64.go @@ -0,0 +1,132 @@ +// Copyright (c) 2018 Intel Corporation +// +// 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. + +package main + +/* +#include + +const int ioctl_KVM_CREATE_VM = KVM_CREATE_VM; +*/ +import "C" + +import ( + "syscall" + + "github.com/sirupsen/logrus" +) + +// variables rather than consts to allow tests to modify them +var ( + kvmDevice = "/dev/kvm" +) + +// archRequiredCPUFlags maps a CPU flag value to search for and a +// human-readable description of that value. +var archRequiredCPUFlags = map[string]string{ + "vmx": "Virtualization support", + "lm": "64Bit CPU", + "sse4_1": "SSE4.1", +} + +// archRequiredCPUAttribs maps a CPU (non-CPU flag) attribute value to search for +// and a human-readable description of that value. +var archRequiredCPUAttribs = map[string]string{ + "GenuineIntel": "Intel Architecture CPU", +} + +// archRequiredKernelModules maps a required module name to a human-readable +// description of the modules functionality and an optional list of +// required module parameters. +var archRequiredKernelModules = map[string]kernelModule{ + "kvm": { + desc: "Kernel-based Virtual Machine", + }, + "kvm_intel": { + desc: "Intel KVM", + parameters: map[string]string{ + "nested": "Y", + // "VMX Unrestricted mode support". This is used + // as a heuristic to determine if the system is + // "new enough" to run a Kata Container + // (atleast a Westmere). + "unrestricted_guest": "Y", + }, + }, + "vhost": { + desc: "Host kernel accelerator for virtio", + }, + "vhost_net": { + desc: "Host kernel accelerator for virtio network", + }, +} + +// kvmIsUsable determines if it will be possible to create a full virtual machine +// by creating a minimal VM and then deleting it. +func kvmIsUsable() error { + flags := syscall.O_RDWR | syscall.O_CLOEXEC + + f, err := syscall.Open(kvmDevice, flags, 0) + if err != nil { + return err + } + defer syscall.Close(f) + + fieldLogger := kataLog.WithField("check-type", "full") + + fieldLogger.WithField("device", kvmDevice).Info("device available") + + vm, _, errno := syscall.Syscall(syscall.SYS_IOCTL, + uintptr(f), + uintptr(C.ioctl_KVM_CREATE_VM), + 0) + if errno != 0 { + if errno == syscall.EBUSY { + fieldLogger.WithField("reason", "another hypervisor running").Error("cannot create VM") + } + + return errno + } + defer syscall.Close(int(vm)) + + fieldLogger.WithField("feature", "create-vm").Info("feature available") + + return nil +} + +func archHostCanCreateVMContainer() error { + return kvmIsUsable() +} + +func archKernelParamHandler(onVMM bool, fields logrus.Fields, msg string) bool { + param, ok := fields["parameter"].(string) + if !ok { + return false + } + + // This option is not required when + // already running under a hypervisor. + if param == "unrestricted_guest" && onVMM { + kataLog.WithFields(fields).Warn(kernelPropertyCorrect) + return true + } + + if param == "nested" { + kataLog.WithFields(fields).Warn(msg) + return true + } + + // don't ignore the error + return false +} diff --git a/cli/kata-check_amd64_test.go b/cli/kata-check_amd64_test.go new file mode 100644 index 0000000000..5aa073152e --- /dev/null +++ b/cli/kata-check_amd64_test.go @@ -0,0 +1,466 @@ +// Copyright (c) 2018 Intel Corporation +// +// 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. + +package main + +import ( + "bytes" + "io/ioutil" + "os" + "path" + "path/filepath" + "regexp" + "strings" + "testing" + + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + "github.com/urfave/cli" +) + +func setupCheckHostIsVMContainerCapable(assert *assert.Assertions, cpuInfoFile string, cpuData []testCPUData, moduleData []testModuleData) { + createModules(assert, cpuInfoFile, moduleData) + + // all the modules files have now been created, so deal with the + // cpuinfo data. + for _, d := range cpuData { + err := makeCPUInfoFile(cpuInfoFile, d.vendorID, d.flags) + assert.NoError(err) + + details := vmContainerCapableDetails{ + cpuInfoFile: cpuInfoFile, + requiredCPUFlags: archRequiredCPUFlags, + requiredCPUAttribs: archRequiredCPUAttribs, + requiredKernelModules: archRequiredKernelModules, + } + + err = hostIsVMContainerCapable(details) + if d.expectError { + assert.Error(err) + } else { + assert.NoError(err) + } + } +} + +func TestCCCheckCLIFunction(t *testing.T) { + assert := assert.New(t) + + dir, err := ioutil.TempDir("", "") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + + savedSysModuleDir := sysModuleDir + savedProcCPUInfo := procCPUInfo + + cpuInfoFile := filepath.Join(dir, "cpuinfo") + + // XXX: override + sysModuleDir = filepath.Join(dir, "sys/module") + procCPUInfo = cpuInfoFile + + defer func() { + sysModuleDir = savedSysModuleDir + procCPUInfo = savedProcCPUInfo + }() + + err = os.MkdirAll(sysModuleDir, testDirMode) + if err != nil { + t.Fatal(err) + } + + cpuData := []testCPUData{ + {"GenuineIntel", "lm vmx sse4_1", false}, + } + + moduleData := []testModuleData{ + {filepath.Join(sysModuleDir, "kvm_intel/parameters/unrestricted_guest"), false, "Y"}, + {filepath.Join(sysModuleDir, "kvm_intel/parameters/nested"), false, "Y"}, + } + + devNull, err := os.OpenFile(os.DevNull, os.O_WRONLY, 0666) + assert.NoError(err) + defer devNull.Close() + + savedLogOutput := kataLog.Logger.Out + + // discard normal output + kataLog.Logger.Out = devNull + + defer func() { + kataLog.Logger.Out = savedLogOutput + }() + + setupCheckHostIsVMContainerCapable(assert, cpuInfoFile, cpuData, moduleData) + + app := cli.NewApp() + ctx := cli.NewContext(app, nil, nil) + app.Name = "foo" + + // create buffer to save logger output + buf := &bytes.Buffer{} + + // capture output this time + kataLog.Logger.Out = buf + + fn, ok := kataCheckCLICommand.Action.(func(context *cli.Context) error) + assert.True(ok) + + err = fn(ctx) + assert.NoError(err) + + output := buf.String() + + for _, c := range cpuData { + assert.True(findAnchoredString(output, c.vendorID)) + for _, flag := range strings.Fields(c.flags) { + assert.True(findAnchoredString(output, flag)) + } + } + + for _, m := range moduleData { + name := path.Base(m.path) + assert.True(findAnchoredString(output, name)) + } +} + +func TestCheckCheckKernelModulesNoNesting(t *testing.T) { + assert := assert.New(t) + + dir, err := ioutil.TempDir("", "") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + + savedSysModuleDir := sysModuleDir + savedProcCPUInfo := procCPUInfo + + cpuInfoFile := filepath.Join(dir, "cpuinfo") + + // XXX: override + sysModuleDir = filepath.Join(dir, "sys/module") + procCPUInfo = cpuInfoFile + + defer func() { + sysModuleDir = savedSysModuleDir + procCPUInfo = savedProcCPUInfo + }() + + err = os.MkdirAll(sysModuleDir, testDirMode) + if err != nil { + t.Fatal(err) + } + + requiredModules := map[string]kernelModule{ + "kvm_intel": { + desc: "Intel KVM", + parameters: map[string]string{ + "nested": "Y", + "unrestricted_guest": "Y", + }, + }, + } + + actualModuleData := []testModuleData{ + {filepath.Join(sysModuleDir, "kvm"), true, ""}, + {filepath.Join(sysModuleDir, "kvm_intel"), true, ""}, + {filepath.Join(sysModuleDir, "kvm_intel/parameters/unrestricted_guest"), false, "Y"}, + + // XXX: force a warning + {filepath.Join(sysModuleDir, "kvm_intel/parameters/nested"), false, "N"}, + } + + vendor := "GenuineIntel" + flags := "vmx lm sse4_1 hypervisor" + + _, err = checkKernelModules(requiredModules, archKernelParamHandler) + // no cpuInfoFile yet + assert.Error(err) + + createModules(assert, cpuInfoFile, actualModuleData) + + err = makeCPUInfoFile(cpuInfoFile, vendor, flags) + assert.NoError(err) + + count, err := checkKernelModules(requiredModules, archKernelParamHandler) + assert.NoError(err) + assert.Equal(count, uint32(0)) + + // create buffer to save logger output + buf := &bytes.Buffer{} + + savedLogOutput := kataLog.Logger.Out + + defer func() { + kataLog.Logger.Out = savedLogOutput + }() + + kataLog.Logger.Out = buf + + count, err = checkKernelModules(requiredModules, archKernelParamHandler) + + assert.NoError(err) + assert.Equal(count, uint32(0)) + + re := regexp.MustCompile(`\bwarning\b.*\bnested\b`) + matches := re.FindAllStringSubmatch(buf.String(), -1) + assert.NotEmpty(matches) +} + +func TestCheckCheckKernelModulesNoUnrestrictedGuest(t *testing.T) { + assert := assert.New(t) + + dir, err := ioutil.TempDir("", "") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + + savedSysModuleDir := sysModuleDir + savedProcCPUInfo := procCPUInfo + + cpuInfoFile := filepath.Join(dir, "cpuinfo") + + // XXX: override + sysModuleDir = filepath.Join(dir, "sys/module") + procCPUInfo = cpuInfoFile + + defer func() { + sysModuleDir = savedSysModuleDir + procCPUInfo = savedProcCPUInfo + }() + + err = os.MkdirAll(sysModuleDir, testDirMode) + if err != nil { + t.Fatal(err) + } + + requiredModules := map[string]kernelModule{ + "kvm_intel": { + desc: "Intel KVM", + parameters: map[string]string{ + "nested": "Y", + "unrestricted_guest": "Y", + }, + }, + } + + actualModuleData := []testModuleData{ + {filepath.Join(sysModuleDir, "kvm"), true, ""}, + {filepath.Join(sysModuleDir, "kvm_intel"), true, ""}, + {filepath.Join(sysModuleDir, "kvm_intel/parameters/nested"), false, "Y"}, + + // XXX: force a failure on non-VMM systems + {filepath.Join(sysModuleDir, "kvm_intel/parameters/unrestricted_guest"), false, "N"}, + } + + vendor := "GenuineIntel" + flags := "vmx lm sse4_1" + + _, err = checkKernelModules(requiredModules, archKernelParamHandler) + // no cpuInfoFile yet + assert.Error(err) + + err = makeCPUInfoFile(cpuInfoFile, vendor, flags) + assert.NoError(err) + + createModules(assert, cpuInfoFile, actualModuleData) + + count, err := checkKernelModules(requiredModules, archKernelParamHandler) + + assert.NoError(err) + // fails due to unrestricted_guest not being available + assert.Equal(count, uint32(1)) + + // pretend test is running under a hypervisor + flags += " hypervisor" + + // recreate + err = makeCPUInfoFile(cpuInfoFile, vendor, flags) + assert.NoError(err) + + // create buffer to save logger output + buf := &bytes.Buffer{} + + savedLogOutput := kataLog.Logger.Out + + defer func() { + kataLog.Logger.Out = savedLogOutput + }() + + kataLog.Logger.Out = buf + + count, err = checkKernelModules(requiredModules, archKernelParamHandler) + + // no error now because running under a hypervisor + assert.NoError(err) + assert.Equal(count, uint32(0)) + + re := regexp.MustCompile(`\bwarning\b.*\bunrestricted_guest\b`) + matches := re.FindAllStringSubmatch(buf.String(), -1) + assert.NotEmpty(matches) +} + +func TestCheckHostIsVMContainerCapable(t *testing.T) { + assert := assert.New(t) + + dir, err := ioutil.TempDir("", "") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + + savedSysModuleDir := sysModuleDir + savedProcCPUInfo := procCPUInfo + + cpuInfoFile := filepath.Join(dir, "cpuinfo") + + // XXX: override + sysModuleDir = filepath.Join(dir, "sys/module") + procCPUInfo = cpuInfoFile + + defer func() { + sysModuleDir = savedSysModuleDir + procCPUInfo = savedProcCPUInfo + }() + + err = os.MkdirAll(sysModuleDir, testDirMode) + if err != nil { + t.Fatal(err) + } + + cpuData := []testCPUData{ + {"", "", true}, + {"Intel", "", true}, + {"GenuineIntel", "", true}, + {"GenuineIntel", "lm", true}, + {"GenuineIntel", "lm vmx", true}, + {"GenuineIntel", "lm vmx sse4_1", false}, + } + + moduleData := []testModuleData{ + {filepath.Join(sysModuleDir, "kvm"), true, ""}, + {filepath.Join(sysModuleDir, "kvm_intel"), true, ""}, + {filepath.Join(sysModuleDir, "kvm_intel/parameters/nested"), false, "Y"}, + {filepath.Join(sysModuleDir, "kvm_intel/parameters/unrestricted_guest"), false, "Y"}, + } + + setupCheckHostIsVMContainerCapable(assert, cpuInfoFile, cpuData, moduleData) + + // remove the modules to force a failure + err = os.RemoveAll(sysModuleDir) + assert.NoError(err) + + details := vmContainerCapableDetails{ + cpuInfoFile: cpuInfoFile, + requiredCPUFlags: archRequiredCPUFlags, + requiredCPUAttribs: archRequiredCPUAttribs, + requiredKernelModules: archRequiredKernelModules, + } + + err = hostIsVMContainerCapable(details) + assert.Error(err) +} + +func TestArchKernelParamHandler(t *testing.T) { + assert := assert.New(t) + + type testData struct { + onVMM bool + fields logrus.Fields + msg string + expectIgnore bool + } + + data := []testData{ + {true, logrus.Fields{}, "", false}, + {false, logrus.Fields{}, "", false}, + + { + false, + logrus.Fields{ + // wrong type + "parameter": 123, + }, + "foo", + false, + }, + + { + false, + logrus.Fields{ + "parameter": "unrestricted_guest", + }, + "", + false, + }, + + { + true, + logrus.Fields{ + "parameter": "unrestricted_guest", + }, + "", + true, + }, + + { + false, + logrus.Fields{ + "parameter": "nested", + }, + "", + true, + }, + } + + for i, d := range data { + result := archKernelParamHandler(d.onVMM, d.fields, d.msg) + if d.expectIgnore { + assert.True(result, "test %d (%+v)", i, d) + } else { + assert.False(result, "test %d (%+v)", i, d) + } + } +} + +func TestKvmIsUsable(t *testing.T) { + assert := assert.New(t) + + dir, err := ioutil.TempDir("", "") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + + savedKvmDevice := kvmDevice + fakeKVMDevice := filepath.Join(dir, "kvm") + kvmDevice = fakeKVMDevice + + defer func() { + kvmDevice = savedKvmDevice + }() + + err = kvmIsUsable() + assert.Error(err) + + err = createEmptyFile(fakeKVMDevice) + assert.NoError(err) + + err = kvmIsUsable() + assert.Error(err) +} diff --git a/cli/kata-check_data_amd64_test.go b/cli/kata-check_data_amd64_test.go new file mode 100644 index 0000000000..9e85635581 --- /dev/null +++ b/cli/kata-check_data_amd64_test.go @@ -0,0 +1,58 @@ +package main + +const testCPUInfoTemplate = ` +processor : 0 +vendor_id : {{.VendorID}} +cpu family : 6 +model : 61 +model name : Intel(R) Core(TM) i7-5600U CPU @ 2.60GHz +stepping : 4 +microcode : 0x25 +cpu MHz : 1999.987 +cache size : 4096 KB +physical id : 0 +siblings : 4 +core id : 0 +cpu cores : 2 +apicid : 0 +initial apicid : 0 +fpu : yes +fpu_exception : yes +cpuid level : 20 +wp : yes +flags : {{.Flags}} +bugs : +bogomips : 5188.36 +clflush size : 64 +cache_alignment : 64 +address sizes : 39 bits physical, 48 bits virtual +power management: + +processor : 1 +vendor_id : {{.VendorID}} +cpu family : 6 +model : 61 +model name : Intel(R) Core(TM) i7-5600U CPU @ 2.60GHz +stepping : 4 +microcode : 0x25 +cpu MHz : 1999.987 +cache size : 4096 KB +physical id : 0 +siblings : 4 +core id : 0 +cpu cores : 2 +apicid : 1 +initial apicid : 1 +fpu : yes +fpu_exception : yes +cpuid level : 20 +wp : yes +flags : {{.Flags}} +bugs : +bogomips : 5194.90 +clflush size : 64 +cache_alignment : 64 +address sizes : 39 bits physical, 48 bits virtual +power management: + +` diff --git a/cli/kata-check_test.go b/cli/kata-check_test.go new file mode 100644 index 0000000000..f8994d02b6 --- /dev/null +++ b/cli/kata-check_test.go @@ -0,0 +1,698 @@ +// Copyright (c) 2017 Intel Corporation +// +// 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. + +package main + +import ( + "bytes" + "fmt" + "html/template" + "io/ioutil" + "os" + "path" + "path/filepath" + "testing" + + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + "github.com/urfave/cli" +) + +type testModuleData struct { + path string + isDir bool + contents string +} + +type testCPUData struct { + vendorID string + flags string + expectError bool +} + +func createFile(file, contents string) error { + return ioutil.WriteFile(file, []byte(contents), testFileMode) +} + +func createModules(assert *assert.Assertions, cpuInfoFile string, moduleData []testModuleData) { + for _, d := range moduleData { + var dir string + + if d.isDir { + dir = d.path + } else { + dir = path.Dir(d.path) + } + + err := os.MkdirAll(dir, testDirMode) + assert.NoError(err) + + if !d.isDir { + err = createFile(d.path, d.contents) + assert.NoError(err) + } + + details := vmContainerCapableDetails{ + cpuInfoFile: cpuInfoFile, + } + + err = hostIsVMContainerCapable(details) + if fileExists(cpuInfoFile) { + assert.NoError(err) + } else { + assert.Error(err) + } + } +} + +func checkKernelParamHandler(assert *assert.Assertions, kernelModulesToCreate, expectedKernelModules map[string]kernelModule, handler kernelParamHandler, expectHandlerError bool, expectedErrorCount uint32) { + err := os.RemoveAll(sysModuleDir) + assert.NoError(err) + + count, err := checkKernelModules(map[string]kernelModule{}, handler) + + // No required modules means no error + assert.NoError(err) + assert.Equal(count, uint32(0)) + + count, err = checkKernelModules(expectedKernelModules, handler) + assert.NoError(err) + + // No modules exist + expectedCount := len(expectedKernelModules) + assert.Equal(count, uint32(expectedCount)) + + err = os.MkdirAll(sysModuleDir, testDirMode) + assert.NoError(err) + + for module, details := range kernelModulesToCreate { + path := filepath.Join(sysModuleDir, module) + err = os.MkdirAll(path, testDirMode) + assert.NoError(err) + + paramDir := filepath.Join(path, "parameters") + err = os.MkdirAll(paramDir, testDirMode) + assert.NoError(err) + + for param, value := range details.parameters { + paramPath := filepath.Join(paramDir, param) + err = createFile(paramPath, value) + assert.NoError(err) + } + } + + count, err = checkKernelModules(expectedKernelModules, handler) + + if expectHandlerError { + assert.Error(err) + return + } + + assert.NoError(err) + assert.Equal(count, expectedErrorCount) +} + +func makeCPUInfoFile(path, vendorID, flags string) error { + t := template.New("cpuinfo") + + t, err := t.Parse(testCPUInfoTemplate) + if err != nil { + return err + } + + args := map[string]string{ + "Flags": flags, + "VendorID": vendorID, + } + + contents := &bytes.Buffer{} + + err = t.Execute(contents, args) + if err != nil { + return err + } + + return ioutil.WriteFile(path, contents.Bytes(), testFileMode) +} + +func TestCheckGetCPUInfo(t *testing.T) { + assert := assert.New(t) + + type testData struct { + contents string + expectedResult string + expectError bool + } + + data := []testData{ + {"", "", true}, + {" ", "", true}, + {"\n", "", true}, + {"\n\n", "", true}, + {"hello\n", "hello", false}, + {"foo\n\n", "foo", false}, + {"foo\n\nbar\n\n", "foo", false}, + {"foo\n\nbar\nbaz\n\n", "foo", false}, + } + + dir, err := ioutil.TempDir("", "") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + + file := filepath.Join(dir, "cpuinfo") + // file doesn't exist + _, err = getCPUInfo(file) + assert.Error(err) + + for _, d := range data { + err = ioutil.WriteFile(file, []byte(d.contents), testFileMode) + if err != nil { + t.Fatal(err) + } + defer os.Remove(file) + + contents, err := getCPUInfo(file) + if d.expectError { + assert.Error(err, fmt.Sprintf("got %q, test data: %+v", contents, d)) + } else { + assert.NoError(err, fmt.Sprintf("got %q, test data: %+v", contents, d)) + } + + assert.Equal(d.expectedResult, contents) + } +} + +func TestCheckFindAnchoredString(t *testing.T) { + assert := assert.New(t) + + type testData struct { + haystack string + needle string + expectSuccess bool + } + + data := []testData{ + {"", "", false}, + {"", "foo", false}, + {"foo", "", false}, + {"food", "foo", false}, + {"foo", "foo", true}, + {"foo bar", "foo", true}, + {"foo bar baz", "bar", true}, + } + + for _, d := range data { + result := findAnchoredString(d.haystack, d.needle) + + if d.expectSuccess { + assert.True(result) + } else { + assert.False(result) + } + } +} + +func TestCheckGetCPUFlags(t *testing.T) { + assert := assert.New(t) + + type testData struct { + cpuinfo string + expectedFlags string + } + + data := []testData{ + {"", ""}, + {"foo", ""}, + {"foo bar", ""}, + {":", ""}, + {"flags", ""}, + {"flags:", ""}, + {"flags: a b c", "a b c"}, + {"flags: a b c foo bar d", "a b c foo bar d"}, + } + + for _, d := range data { + result := getCPUFlags(d.cpuinfo) + assert.Equal(d.expectedFlags, result) + } +} + +func TestCheckCheckCPUFlags(t *testing.T) { + assert := assert.New(t) + + type testData struct { + cpuflags string + required map[string]string + expectCount uint32 + } + + data := []testData{ + { + "", + map[string]string{}, + 0, + }, + { + "", + map[string]string{ + "a": "A flag", + }, + 0, + }, + { + "", + map[string]string{ + "a": "A flag", + "b": "B flag", + }, + 0, + }, + { + "a b c", + map[string]string{ + "b": "B flag", + }, + 0, + }, + { + "a b c", + map[string]string{ + "x": "X flag", + "y": "Y flag", + "z": "Z flag", + }, + 3, + }, + } + + for _, d := range data { + count := checkCPUFlags(d.cpuflags, d.required) + assert.Equal(d.expectCount, count, "%+v", d) + } +} + +func TestCheckCheckCPUAttribs(t *testing.T) { + assert := assert.New(t) + + type testData struct { + cpuinfo string + required map[string]string + expectCount uint32 + } + + data := []testData{ + { + "", + map[string]string{}, + 0, + }, + { + "", + map[string]string{ + "a": "", + }, + 0, + }, + { + "a: b", + map[string]string{ + "b": "B attribute", + }, + 0, + }, + { + "a: b\nc: d\ne: f", + map[string]string{ + "b": "B attribute", + }, + 0, + }, + { + "a: b\n", + map[string]string{ + "b": "B attribute", + "c": "C attribute", + "d": "D attribute", + }, + 2, + }, + { + "a: b\nc: d\ne: f", + map[string]string{ + "b": "B attribute", + "d": "D attribute", + "f": "F attribute", + }, + 0, + }, + } + + for _, d := range data { + count := checkCPUAttribs(d.cpuinfo, d.required) + assert.Equal(d.expectCount, count, "%+v", d) + } +} + +func TestCheckHaveKernelModule(t *testing.T) { + assert := assert.New(t) + + dir, err := ioutil.TempDir("", "") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + + savedModInfoCmd := modInfoCmd + savedSysModuleDir := sysModuleDir + + // XXX: override (fake the modprobe command failing) + modInfoCmd = "false" + sysModuleDir = filepath.Join(dir, "sys/module") + + defer func() { + modInfoCmd = savedModInfoCmd + sysModuleDir = savedSysModuleDir + }() + + err = os.MkdirAll(sysModuleDir, testDirMode) + if err != nil { + t.Fatal(err) + } + + module := "foo" + + result := haveKernelModule(module) + assert.False(result) + + // XXX: override - make our fake "modprobe" succeed + modInfoCmd = "true" + + result = haveKernelModule(module) + assert.True(result) + + // disable "modprobe" again + modInfoCmd = "false" + + fooDir := filepath.Join(sysModuleDir, module) + err = os.MkdirAll(fooDir, testDirMode) + if err != nil { + t.Fatal(err) + } + + result = haveKernelModule(module) + assert.True(result) +} + +func TestCheckCheckKernelModules(t *testing.T) { + assert := assert.New(t) + + dir, err := ioutil.TempDir("", "") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + + savedModInfoCmd := modInfoCmd + savedSysModuleDir := sysModuleDir + + // XXX: override (fake the modprobe command failing) + modInfoCmd = "false" + sysModuleDir = filepath.Join(dir, "sys/module") + + defer func() { + modInfoCmd = savedModInfoCmd + sysModuleDir = savedSysModuleDir + }() + + err = os.MkdirAll(sysModuleDir, testDirMode) + if err != nil { + t.Fatal(err) + } + + testData := map[string]kernelModule{ + "foo": { + desc: "desc", + parameters: map[string]string{}, + }, + "bar": { + desc: "desc", + parameters: map[string]string{ + "param1": "hello", + "param2": "world", + "param3": "a", + "param4": ".", + }, + }, + } + + count, err := checkKernelModules(map[string]kernelModule{}, nil) + // No required modules means no error + assert.NoError(err) + assert.Equal(count, uint32(0)) + + count, err = checkKernelModules(testData, nil) + assert.NoError(err) + // No modules exist + assert.Equal(count, uint32(2)) + + for module, details := range testData { + path := filepath.Join(sysModuleDir, module) + err = os.MkdirAll(path, testDirMode) + if err != nil { + t.Fatal(err) + } + + paramDir := filepath.Join(path, "parameters") + err = os.MkdirAll(paramDir, testDirMode) + if err != nil { + t.Fatal(err) + } + + for param, value := range details.parameters { + paramPath := filepath.Join(paramDir, param) + err = createFile(paramPath, value) + if err != nil { + t.Fatal(err) + } + } + } + + count, err = checkKernelModules(testData, nil) + assert.NoError(err) + assert.Equal(count, uint32(0)) +} + +func TestCheckCheckKernelModulesUnreadableFile(t *testing.T) { + assert := assert.New(t) + + if os.Geteuid() == 0 { + t.Skip(testDisabledNeedNonRoot) + } + + dir, err := ioutil.TempDir("", "") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + + testData := map[string]kernelModule{ + "foo": { + desc: "desc", + parameters: map[string]string{ + "param1": "wibble", + }, + }, + } + + savedModInfoCmd := modInfoCmd + savedSysModuleDir := sysModuleDir + + // XXX: override (fake the modprobe command failing) + modInfoCmd = "false" + sysModuleDir = filepath.Join(dir, "sys/module") + + defer func() { + modInfoCmd = savedModInfoCmd + sysModuleDir = savedSysModuleDir + }() + + modPath := filepath.Join(sysModuleDir, "foo/parameters") + err = os.MkdirAll(modPath, testDirMode) + assert.NoError(err) + + modParamFile := filepath.Join(modPath, "param1") + + err = createEmptyFile(modParamFile) + assert.NoError(err) + + // make file unreadable by non-root user + err = os.Chmod(modParamFile, 0000) + assert.NoError(err) + + _, err = checkKernelModules(testData, nil) + assert.Error(err) +} + +func TestCheckCheckKernelModulesInvalidFileContents(t *testing.T) { + assert := assert.New(t) + + dir, err := ioutil.TempDir("", "") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + + testData := map[string]kernelModule{ + "foo": { + desc: "desc", + parameters: map[string]string{ + "param1": "wibble", + }, + }, + } + + savedModInfoCmd := modInfoCmd + savedSysModuleDir := sysModuleDir + + // XXX: override (fake the modprobe command failing) + modInfoCmd = "false" + sysModuleDir = filepath.Join(dir, "sys/module") + + defer func() { + modInfoCmd = savedModInfoCmd + sysModuleDir = savedSysModuleDir + }() + + modPath := filepath.Join(sysModuleDir, "foo/parameters") + err = os.MkdirAll(modPath, testDirMode) + assert.NoError(err) + + modParamFile := filepath.Join(modPath, "param1") + + err = createFile(modParamFile, "burp") + assert.NoError(err) + + count, err := checkKernelModules(testData, nil) + assert.NoError(err) + assert.Equal(count, uint32(1)) +} + +func TestCheckCLIFunctionFail(t *testing.T) { + assert := assert.New(t) + + dir, err := ioutil.TempDir("", "") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + + oldProcCPUInfo := procCPUInfo + + // doesn't exist + procCPUInfo = filepath.Join(dir, "cpuinfo") + + defer func() { + procCPUInfo = oldProcCPUInfo + }() + + app := cli.NewApp() + ctx := cli.NewContext(app, nil, nil) + app.Name = "foo" + + fn, ok := kataCheckCLICommand.Action.(func(context *cli.Context) error) + assert.True(ok) + + err = fn(ctx) + assert.Error(err) +} + +func TestCheckKernelParamHandler(t *testing.T) { + assert := assert.New(t) + + dir, err := ioutil.TempDir("", "") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + + savedModInfoCmd := modInfoCmd + savedSysModuleDir := sysModuleDir + + // XXX: override (fake the modprobe command failing) + modInfoCmd = "false" + sysModuleDir = filepath.Join(dir, "sys/module") + + defer func() { + modInfoCmd = savedModInfoCmd + sysModuleDir = savedSysModuleDir + }() + + handler := func(onVMM bool, fields logrus.Fields, msg string) bool { + param, ok := fields["parameter"].(string) + if !ok { + return false + } + + if param == "param1" { + return true + } + + // don't ignore the error + return false + } + + testData1 := map[string]kernelModule{ + "foo": { + desc: "desc", + parameters: map[string]string{}, + }, + "bar": { + desc: "desc", + parameters: map[string]string{ + "param1": "hello", + "param2": "world", + }, + }, + } + + checkKernelParamHandler(assert, testData1, testData1, handler, false, uint32(0)) + + testDataToCreate := map[string]kernelModule{ + "foo": { + desc: "desc", + parameters: map[string]string{ + "param1": "moo", + }, + }, + } + + testDataToExpect := map[string]kernelModule{ + "foo": { + desc: "desc", + parameters: map[string]string{ + "param1": "bar", + }, + }, + } + + // Expected and actual are different, but the handler should deal with + // the problem. + checkKernelParamHandler(assert, testDataToCreate, testDataToExpect, handler, false, uint32(0)) + + // Expected and actual are different, so with no handler we expect a + // single error (due to "param1"'s value being different) + checkKernelParamHandler(assert, testDataToCreate, testDataToExpect, nil, false, uint32(1)) +} diff --git a/cli/kata-env.go b/cli/kata-env.go new file mode 100644 index 0000000000..a02d991475 --- /dev/null +++ b/cli/kata-env.go @@ -0,0 +1,365 @@ +// Copyright (c) 2017-2018 Intel Corporation +// +// 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. + +package main + +import ( + "errors" + "os" + "strings" + + "github.com/BurntSushi/toml" + vc "github.com/kata-containers/runtime/virtcontainers" + "github.com/kata-containers/runtime/virtcontainers/pkg/oci" + specs "github.com/opencontainers/runtime-spec/specs-go" + "github.com/urfave/cli" +) + +// Semantic version for the output of the command. +// +// XXX: Increment for every change to the output format +// (meaning any change to the EnvInfo type). +const formatVersion = "1.0.9" + +// MetaInfo stores information on the format of the output itself +type MetaInfo struct { + // output format version + Version string +} + +// KernelInfo stores kernel details +type KernelInfo struct { + Path string + Parameters string +} + +// ImageInfo stores root filesystem image details +type ImageInfo struct { + Path string +} + +// CPUInfo stores host CPU details +type CPUInfo struct { + Vendor string + Model string +} + +// RuntimeConfigInfo stores runtime config details. +type RuntimeConfigInfo struct { + Path string +} + +// RuntimeInfo stores runtime details. +type RuntimeInfo struct { + Version RuntimeVersionInfo + Config RuntimeConfigInfo + Debug bool +} + +// RuntimeVersionInfo stores details of the runtime version +type RuntimeVersionInfo struct { + Semver string + Commit string + OCI string +} + +// HypervisorInfo stores hypervisor details +type HypervisorInfo struct { + MachineType string + Version string + Path string + Debug bool + BlockDeviceDriver string +} + +// ProxyInfo stores proxy details +type ProxyInfo struct { + Type string + Version string + Path string + Debug bool +} + +// ShimInfo stores shim details +type ShimInfo struct { + Type string + Version string + Path string + Debug bool +} + +// AgentInfo stores agent details +type AgentInfo struct { + Type string + Version string +} + +// DistroInfo stores host operating system distribution details. +type DistroInfo struct { + Name string + Version string +} + +// HostInfo stores host details +type HostInfo struct { + Kernel string + Architecture string + Distro DistroInfo + CPU CPUInfo + VMContainerCapable bool +} + +// EnvInfo collects all information that will be displayed by the +// env command. +// +// XXX: Any changes must be coupled with a change to formatVersion. +type EnvInfo struct { + Meta MetaInfo + Runtime RuntimeInfo + Hypervisor HypervisorInfo + Image ImageInfo + Kernel KernelInfo + Proxy ProxyInfo + Shim ShimInfo + Agent AgentInfo + Host HostInfo +} + +func getMetaInfo() MetaInfo { + return MetaInfo{ + Version: formatVersion, + } +} + +func getRuntimeInfo(configFile string, config oci.RuntimeConfig) RuntimeInfo { + runtimeVersion := RuntimeVersionInfo{ + Semver: version, + Commit: commit, + OCI: specs.Version, + } + + runtimeConfig := RuntimeConfigInfo{ + Path: configFile, + } + + return RuntimeInfo{ + Version: runtimeVersion, + Config: runtimeConfig, + } +} + +func getHostInfo() (HostInfo, error) { + hostKernelVersion, err := getKernelVersion() + if err != nil { + return HostInfo{}, err + } + + hostDistroName, hostDistroVersion, err := getDistroDetails() + if err != nil { + return HostInfo{}, err + } + + cpuVendor, cpuModel, err := getCPUDetails() + if err != nil { + return HostInfo{}, err + } + + hostVMContainerCapable := true + + details := vmContainerCapableDetails{ + cpuInfoFile: procCPUInfo, + requiredCPUFlags: archRequiredCPUFlags, + requiredCPUAttribs: archRequiredCPUAttribs, + requiredKernelModules: archRequiredKernelModules, + } + + if err = hostIsVMContainerCapable(details); err != nil { + hostVMContainerCapable = false + } + + hostDistro := DistroInfo{ + Name: hostDistroName, + Version: hostDistroVersion, + } + + hostCPU := CPUInfo{ + Vendor: cpuVendor, + Model: cpuModel, + } + + host := HostInfo{ + Kernel: hostKernelVersion, + Architecture: arch, + Distro: hostDistro, + CPU: hostCPU, + VMContainerCapable: hostVMContainerCapable, + } + + return host, nil +} + +func getProxyInfo(config oci.RuntimeConfig) (ProxyInfo, error) { + version, err := getCommandVersion(defaultProxyPath) + if err != nil { + version = unknown + } + + proxy := ProxyInfo{ + Type: string(config.ProxyType), + Version: version, + Path: config.ProxyConfig.Path, + Debug: config.ProxyConfig.Debug, + } + + return proxy, nil +} + +func getCommandVersion(cmd string) (string, error) { + return runCommand([]string{cmd, "--version"}) +} + +func getShimInfo(config oci.RuntimeConfig) (ShimInfo, error) { + shimConfig, ok := config.ShimConfig.(vc.ShimConfig) + if !ok { + return ShimInfo{}, errors.New("cannot determine shim config") + } + + shimPath := shimConfig.Path + + version, err := getCommandVersion(shimPath) + if err != nil { + version = unknown + } + + shim := ShimInfo{ + Type: string(config.ShimType), + Version: version, + Path: shimPath, + Debug: shimConfig.Debug, + } + + return shim, nil +} + +func getAgentInfo(config oci.RuntimeConfig) AgentInfo { + agent := AgentInfo{ + Type: string(config.AgentType), + Version: unknown, + } + + return agent +} + +func getHypervisorInfo(config oci.RuntimeConfig) HypervisorInfo { + hypervisorPath := config.HypervisorConfig.HypervisorPath + + version, err := getCommandVersion(hypervisorPath) + if err != nil { + version = unknown + } + + return HypervisorInfo{ + MachineType: config.HypervisorConfig.HypervisorMachineType, + Version: version, + Path: hypervisorPath, + BlockDeviceDriver: config.HypervisorConfig.BlockDeviceDriver, + } +} + +func getEnvInfo(configFile string, config oci.RuntimeConfig) (env EnvInfo, err error) { + meta := getMetaInfo() + + runtime := getRuntimeInfo(configFile, config) + + host, err := getHostInfo() + if err != nil { + return EnvInfo{}, err + } + + proxy, _ := getProxyInfo(config) + + shim, err := getShimInfo(config) + if err != nil { + return EnvInfo{}, err + } + + agent := getAgentInfo(config) + + hypervisor := getHypervisorInfo(config) + + image := ImageInfo{ + Path: config.HypervisorConfig.ImagePath, + } + + kernel := KernelInfo{ + Path: config.HypervisorConfig.KernelPath, + Parameters: strings.Join(vc.SerializeParams(config.HypervisorConfig.KernelParams, "="), " "), + } + + env = EnvInfo{ + Meta: meta, + Runtime: runtime, + Hypervisor: hypervisor, + Image: image, + Kernel: kernel, + Proxy: proxy, + Shim: shim, + Agent: agent, + Host: host, + } + + return env, nil +} + +func showSettings(env EnvInfo, file *os.File) error { + encoder := toml.NewEncoder(file) + + err := encoder.Encode(env) + if err != nil { + return err + } + + return nil +} + +func handleSettings(file *os.File, metadata map[string]interface{}) error { + if file == nil { + return errors.New("Invalid output file specified") + } + + configFile, ok := metadata["configFile"].(string) + if !ok { + return errors.New("cannot determine config file") + } + + runtimeConfig, ok := metadata["runtimeConfig"].(oci.RuntimeConfig) + if !ok { + return errors.New("cannot determine runtime config") + } + + env, err := getEnvInfo(configFile, runtimeConfig) + if err != nil { + return err + } + + return showSettings(env, file) +} + +var kataEnvCLICommand = cli.Command{ + Name: envCmd, + Usage: "display settings", + Action: func(context *cli.Context) error { + return handleSettings(defaultOutputFile, context.App.Metadata) + }, +} diff --git a/cli/kata-env_test.go b/cli/kata-env_test.go new file mode 100644 index 0000000000..9393cb863c --- /dev/null +++ b/cli/kata-env_test.go @@ -0,0 +1,969 @@ +// Copyright (c) 2017 Intel Corporation +// +// 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. + +package main + +import ( + "bytes" + "fmt" + "io/ioutil" + "os" + "path" + "path/filepath" + goruntime "runtime" + "strings" + "testing" + + "github.com/BurntSushi/toml" + vc "github.com/kata-containers/runtime/virtcontainers" + specs "github.com/opencontainers/runtime-spec/specs-go" + "github.com/urfave/cli" + + "github.com/kata-containers/runtime/virtcontainers/pkg/oci" + "github.com/stretchr/testify/assert" +) + +const testProxyURL = "file:///proxyURL" +const testProxyVersion = "proxy version 0.1" +const testShimVersion = "shim version 0.1" +const testHypervisorVersion = "QEMU emulator version 2.7.0+git.741f430a96-6.1, Copyright (c) 2003-2016 Fabrice Bellard and the QEMU Project developers" + +// makeVersionBinary creates a shell script with the specified file +// name. When run as "file --version", it will display the specified +// version to stdout and exit successfully. +func makeVersionBinary(file, version string) error { + err := createFile(file, + fmt.Sprintf(`#!/bin/sh + [ "$1" = "--version" ] && echo "%s"`, version)) + if err != nil { + return err + } + + err = os.Chmod(file, testExeFileMode) + if err != nil { + return err + } + + return nil +} + +func makeRuntimeConfig(prefixDir string) (configFile string, config oci.RuntimeConfig, err error) { + const logPath = "/log/path" + hypervisorPath := filepath.Join(prefixDir, "hypervisor") + kernelPath := filepath.Join(prefixDir, "kernel") + imagePath := filepath.Join(prefixDir, "image") + kernelParams := "foo=bar xyz" + machineType := "machineType" + shimPath := filepath.Join(prefixDir, "shim") + proxyPath := filepath.Join(prefixDir, "proxy") + disableBlock := true + blockStorageDriver := "virtio-scsi" + + // override + defaultProxyPath = proxyPath + + filesToCreate := []string{ + hypervisorPath, + kernelPath, + imagePath, + } + + for _, file := range filesToCreate { + err := createEmptyFile(file) + if err != nil { + return "", oci.RuntimeConfig{}, err + } + } + + err = makeVersionBinary(shimPath, testShimVersion) + if err != nil { + return "", oci.RuntimeConfig{}, err + } + + err = makeVersionBinary(proxyPath, testProxyVersion) + if err != nil { + return "", oci.RuntimeConfig{}, err + } + + err = makeVersionBinary(hypervisorPath, testHypervisorVersion) + if err != nil { + return "", oci.RuntimeConfig{}, err + } + + runtimeConfig := makeRuntimeConfigFileData( + "qemu", + hypervisorPath, + kernelPath, + imagePath, + kernelParams, + machineType, + shimPath, + testProxyURL, + logPath, + disableBlock, + blockStorageDriver) + + configFile = path.Join(prefixDir, "runtime.toml") + err = createConfig(configFile, runtimeConfig) + if err != nil { + return "", oci.RuntimeConfig{}, err + } + + _, config, err = loadConfiguration(configFile, true) + if err != nil { + return "", oci.RuntimeConfig{}, err + } + + return configFile, config, nil +} + +func getExpectedProxyDetails(config oci.RuntimeConfig) (ProxyInfo, error) { + return ProxyInfo{ + Type: string(config.ProxyType), + Version: testProxyVersion, + Path: config.ProxyConfig.Path, + Debug: config.ProxyConfig.Debug, + }, nil +} + +func getExpectedShimDetails(config oci.RuntimeConfig) (ShimInfo, error) { + shimConfig, ok := config.ShimConfig.(vc.ShimConfig) + if !ok { + return ShimInfo{}, fmt.Errorf("failed to get shim config") + } + + shimPath := shimConfig.Path + + return ShimInfo{ + Type: string(config.ShimType), + Version: testShimVersion, + Path: shimPath, + }, nil +} + +func getExpectedAgentDetails(config oci.RuntimeConfig) (AgentInfo, error) { + return AgentInfo{ + Type: string(config.AgentType), + Version: unknown, + }, nil +} + +func getExpectedHostDetails(tmpdir string) (HostInfo, error) { + type filesToCreate struct { + file string + contents string + } + + const expectedKernelVersion = "99.1" + const expectedArch = goruntime.GOARCH + + expectedDistro := DistroInfo{ + Name: "Foo", + Version: "42", + } + + expectedCPU := CPUInfo{ + Vendor: "moi", + Model: "awesome XI", + } + + expectedHostDetails := HostInfo{ + Kernel: expectedKernelVersion, + Architecture: expectedArch, + Distro: expectedDistro, + CPU: expectedCPU, + VMContainerCapable: false, + } + + testProcCPUInfo := filepath.Join(tmpdir, "cpuinfo") + testOSRelease := filepath.Join(tmpdir, "os-release") + + // XXX: This file is *NOT* created by this function on purpose + // (to ensure the only file checked by the tests is + // testOSRelease). osReleaseClr handling is tested in + // utils_test.go. + testOSReleaseClr := filepath.Join(tmpdir, "os-release-clr") + + testProcVersion := filepath.Join(tmpdir, "proc-version") + + // override + procVersion = testProcVersion + osRelease = testOSRelease + osReleaseClr = testOSReleaseClr + procCPUInfo = testProcCPUInfo + + procVersionContents := fmt.Sprintf("Linux version %s a b c", + expectedKernelVersion) + + osReleaseContents := fmt.Sprintf(` +NAME="%s" +VERSION_ID="%s" +`, expectedDistro.Name, expectedDistro.Version) + + procCPUInfoContents := fmt.Sprintf(` +vendor_id : %s +model name : %s +`, expectedCPU.Vendor, expectedCPU.Model) + + data := []filesToCreate{ + {procVersion, procVersionContents}, + {osRelease, osReleaseContents}, + {procCPUInfo, procCPUInfoContents}, + } + + for _, d := range data { + err := createFile(d.file, d.contents) + if err != nil { + return HostInfo{}, err + } + } + + return expectedHostDetails, nil +} + +func getExpectedHypervisor(config oci.RuntimeConfig) HypervisorInfo { + return HypervisorInfo{ + Version: testHypervisorVersion, + Path: config.HypervisorConfig.HypervisorPath, + MachineType: config.HypervisorConfig.HypervisorMachineType, + BlockDeviceDriver: config.HypervisorConfig.BlockDeviceDriver, + } +} + +func getExpectedImage(config oci.RuntimeConfig) ImageInfo { + return ImageInfo{ + Path: config.HypervisorConfig.ImagePath, + } +} + +func getExpectedKernel(config oci.RuntimeConfig) KernelInfo { + return KernelInfo{ + Path: config.HypervisorConfig.KernelPath, + Parameters: strings.Join(vc.SerializeParams(config.HypervisorConfig.KernelParams, "="), " "), + } +} + +func getExpectedRuntimeDetails(configFile string) RuntimeInfo { + return RuntimeInfo{ + Version: RuntimeVersionInfo{ + Semver: version, + Commit: commit, + OCI: specs.Version, + }, + Config: RuntimeConfigInfo{ + Path: configFile, + }, + } +} + +func getExpectedSettings(config oci.RuntimeConfig, tmpdir, configFile string) (EnvInfo, error) { + meta := getExpectedMetaInfo() + + runtime := getExpectedRuntimeDetails(configFile) + + proxy, err := getExpectedProxyDetails(config) + if err != nil { + return EnvInfo{}, err + } + + shim, err := getExpectedShimDetails(config) + if err != nil { + return EnvInfo{}, err + } + + agent, err := getExpectedAgentDetails(config) + if err != nil { + return EnvInfo{}, err + } + + host, err := getExpectedHostDetails(tmpdir) + if err != nil { + return EnvInfo{}, err + } + + hypervisor := getExpectedHypervisor(config) + kernel := getExpectedKernel(config) + image := getExpectedImage(config) + + env := EnvInfo{ + Meta: meta, + Runtime: runtime, + Hypervisor: hypervisor, + Image: image, + Kernel: kernel, + Proxy: proxy, + Shim: shim, + Agent: agent, + Host: host, + } + + return env, nil +} + +func getExpectedMetaInfo() MetaInfo { + return MetaInfo{ + Version: formatVersion, + } +} + +func TestEnvGetMetaInfo(t *testing.T) { + expectedMeta := getExpectedMetaInfo() + + meta := getMetaInfo() + + assert.Equal(t, expectedMeta, meta) +} + +func TestEnvGetHostInfo(t *testing.T) { + tmpdir, err := ioutil.TempDir("", "") + if err != nil { + panic(err) + } + defer os.RemoveAll(tmpdir) + + expectedHostDetails, err := getExpectedHostDetails(tmpdir) + assert.NoError(t, err) + + host, err := getHostInfo() + assert.NoError(t, err) + + assert.Equal(t, expectedHostDetails, host) +} + +func TestEnvGetHostInfoNoProcCPUInfo(t *testing.T) { + tmpdir, err := ioutil.TempDir("", "") + if err != nil { + panic(err) + } + defer os.RemoveAll(tmpdir) + + _, err = getExpectedHostDetails(tmpdir) + assert.NoError(t, err) + + err = os.Remove(procCPUInfo) + assert.NoError(t, err) + + _, err = getHostInfo() + assert.Error(t, err) +} + +func TestEnvGetHostInfoNoOSRelease(t *testing.T) { + tmpdir, err := ioutil.TempDir("", "") + if err != nil { + panic(err) + } + defer os.RemoveAll(tmpdir) + + _, err = getExpectedHostDetails(tmpdir) + assert.NoError(t, err) + + err = os.Remove(osRelease) + assert.NoError(t, err) + + _, err = getHostInfo() + assert.Error(t, err) +} + +func TestEnvGetHostInfoNoProcVersion(t *testing.T) { + tmpdir, err := ioutil.TempDir("", "") + if err != nil { + panic(err) + } + defer os.RemoveAll(tmpdir) + + _, err = getExpectedHostDetails(tmpdir) + assert.NoError(t, err) + + err = os.Remove(procVersion) + assert.NoError(t, err) + + _, err = getHostInfo() + assert.Error(t, err) +} + +func TestEnvGetEnvInfo(t *testing.T) { + tmpdir, err := ioutil.TempDir("", "") + if err != nil { + panic(err) + } + defer os.RemoveAll(tmpdir) + + configFile, config, err := makeRuntimeConfig(tmpdir) + assert.NoError(t, err) + + expectedEnv, err := getExpectedSettings(config, tmpdir, configFile) + assert.NoError(t, err) + + env, err := getEnvInfo(configFile, config) + assert.NoError(t, err) + + assert.Equal(t, expectedEnv, env) +} + +func TestEnvGetEnvInfoNoHypervisorVersion(t *testing.T) { + assert := assert.New(t) + + tmpdir, err := ioutil.TempDir("", "") + assert.NoError(err) + defer os.RemoveAll(tmpdir) + + configFile, config, err := makeRuntimeConfig(tmpdir) + assert.NoError(err) + + expectedEnv, err := getExpectedSettings(config, tmpdir, configFile) + assert.NoError(err) + + err = os.Remove(config.HypervisorConfig.HypervisorPath) + assert.NoError(err) + + expectedEnv.Hypervisor.Version = unknown + + env, err := getEnvInfo(configFile, config) + assert.NoError(err) + + assert.Equal(expectedEnv, env) +} + +func TestEnvGetEnvInfoShimError(t *testing.T) { + assert := assert.New(t) + + tmpdir, err := ioutil.TempDir("", "") + assert.NoError(err) + defer os.RemoveAll(tmpdir) + + configFile, config, err := makeRuntimeConfig(tmpdir) + assert.NoError(err) + + config.ShimConfig = "invalid shim config" + + _, err = getEnvInfo(configFile, config) + assert.Error(err) +} + +func TestEnvGetEnvInfoAgentError(t *testing.T) { + assert := assert.New(t) + + tmpdir, err := ioutil.TempDir("", "") + assert.NoError(err) + defer os.RemoveAll(tmpdir) + + configFile, config, err := makeRuntimeConfig(tmpdir) + assert.NoError(err) + + config.AgentConfig = "invalid agent config" + + _, err = getEnvInfo(configFile, config) + assert.Error(err) +} + +func TestEnvGetEnvInfoNoOSRelease(t *testing.T) { + tmpdir, err := ioutil.TempDir("", "") + if err != nil { + panic(err) + } + defer os.RemoveAll(tmpdir) + + configFile, config, err := makeRuntimeConfig(tmpdir) + assert.NoError(t, err) + + _, err = getExpectedSettings(config, tmpdir, configFile) + assert.NoError(t, err) + + err = os.Remove(osRelease) + assert.NoError(t, err) + + _, err = getEnvInfo(configFile, config) + assert.Error(t, err) +} + +func TestEnvGetEnvInfoNoProcCPUInfo(t *testing.T) { + tmpdir, err := ioutil.TempDir("", "") + if err != nil { + panic(err) + } + defer os.RemoveAll(tmpdir) + + configFile, config, err := makeRuntimeConfig(tmpdir) + assert.NoError(t, err) + + _, err = getExpectedSettings(config, tmpdir, configFile) + assert.NoError(t, err) + + err = os.Remove(procCPUInfo) + assert.NoError(t, err) + + _, err = getEnvInfo(configFile, config) + assert.Error(t, err) +} + +func TestEnvGetEnvInfoNoProcVersion(t *testing.T) { + tmpdir, err := ioutil.TempDir("", "") + if err != nil { + panic(err) + } + defer os.RemoveAll(tmpdir) + + configFile, config, err := makeRuntimeConfig(tmpdir) + assert.NoError(t, err) + + _, err = getExpectedSettings(config, tmpdir, configFile) + assert.NoError(t, err) + + err = os.Remove(procVersion) + assert.NoError(t, err) + + _, err = getEnvInfo(configFile, config) + assert.Error(t, err) +} + +func TestEnvGetRuntimeInfo(t *testing.T) { + tmpdir, err := ioutil.TempDir("", "") + if err != nil { + panic(err) + } + defer os.RemoveAll(tmpdir) + + configFile, config, err := makeRuntimeConfig(tmpdir) + assert.NoError(t, err) + + expectedRuntime := getExpectedRuntimeDetails(configFile) + + runtime := getRuntimeInfo(configFile, config) + + assert.Equal(t, expectedRuntime, runtime) +} + +func TestEnvGetProxyInfo(t *testing.T) { + tmpdir, err := ioutil.TempDir("", "") + if err != nil { + panic(err) + } + defer os.RemoveAll(tmpdir) + + _, config, err := makeRuntimeConfig(tmpdir) + assert.NoError(t, err) + + expectedProxy, err := getExpectedProxyDetails(config) + assert.NoError(t, err) + + proxy, err := getProxyInfo(config) + assert.NoError(t, err) + + assert.Equal(t, expectedProxy, proxy) +} + +func TestEnvGetProxyInfoNoVersion(t *testing.T) { + tmpdir, err := ioutil.TempDir("", "") + if err != nil { + panic(err) + } + defer os.RemoveAll(tmpdir) + + _, config, err := makeRuntimeConfig(tmpdir) + assert.NoError(t, err) + + expectedProxy, err := getExpectedProxyDetails(config) + assert.NoError(t, err) + + // remove the proxy ensuring its version cannot be queried + err = os.Remove(defaultProxyPath) + assert.NoError(t, err) + + expectedProxy.Version = unknown + + proxy, err := getProxyInfo(config) + assert.NoError(t, err) + + assert.Equal(t, expectedProxy, proxy) +} + +func TestEnvGetShimInfo(t *testing.T) { + tmpdir, err := ioutil.TempDir("", "") + if err != nil { + panic(err) + } + defer os.RemoveAll(tmpdir) + + _, config, err := makeRuntimeConfig(tmpdir) + assert.NoError(t, err) + + expectedShim, err := getExpectedShimDetails(config) + assert.NoError(t, err) + + shim, err := getShimInfo(config) + assert.NoError(t, err) + + assert.Equal(t, expectedShim, shim) +} + +func TestEnvGetShimInfoNoVersion(t *testing.T) { + tmpdir, err := ioutil.TempDir("", "") + if err != nil { + panic(err) + } + defer os.RemoveAll(tmpdir) + + _, config, err := makeRuntimeConfig(tmpdir) + assert.NoError(t, err) + + expectedShim, err := getExpectedShimDetails(config) + assert.NoError(t, err) + + shimPath := expectedShim.Path + + // ensure querying the shim version fails + err = createFile(shimPath, `#!/bin/sh + exit 1`) + assert.NoError(t, err) + + expectedShim.Version = unknown + + shim, err := getShimInfo(config) + assert.NoError(t, err) + + assert.Equal(t, expectedShim, shim) +} + +func TestEnvGetShimInfoInvalidType(t *testing.T) { + tmpdir, err := ioutil.TempDir("", "") + if err != nil { + panic(err) + } + defer os.RemoveAll(tmpdir) + + _, config, err := makeRuntimeConfig(tmpdir) + assert.NoError(t, err) + + _, err = getExpectedShimDetails(config) + assert.NoError(t, err) + + config.ShimConfig = "foo" + _, err = getShimInfo(config) + assert.Error(t, err) +} + +func TestEnvGetAgentInfo(t *testing.T) { + tmpdir, err := ioutil.TempDir("", "") + if err != nil { + panic(err) + } + defer os.RemoveAll(tmpdir) + + _, config, err := makeRuntimeConfig(tmpdir) + assert.NoError(t, err) + + expectedAgent, err := getExpectedAgentDetails(config) + assert.NoError(t, err) + + agent := getAgentInfo(config) + assert.Equal(t, expectedAgent, agent) +} + +func testEnvShowSettings(t *testing.T, tmpdir string, tmpfile *os.File) error { + + runtime := RuntimeInfo{} + + hypervisor := HypervisorInfo{ + Path: "/resolved/hypervisor/path", + MachineType: "hypervisor-machine-type", + } + + image := ImageInfo{ + Path: "/resolved/image/path", + } + + kernel := KernelInfo{ + Path: "/kernel/path", + Parameters: "foo=bar xyz", + } + + proxy := ProxyInfo{ + Type: "proxy-type", + Version: "proxy-version", + Path: "file:///proxy-url", + Debug: false, + } + + shim := ShimInfo{ + Type: "shim-type", + Version: "shim-version", + Path: "/resolved/shim/path", + } + + agent := AgentInfo{ + Type: "agent-type", + Version: "agent-version", + } + + expectedHostDetails, err := getExpectedHostDetails(tmpdir) + assert.NoError(t, err) + + env := EnvInfo{ + Runtime: runtime, + Hypervisor: hypervisor, + Image: image, + Kernel: kernel, + Proxy: proxy, + Shim: shim, + Agent: agent, + Host: expectedHostDetails, + } + + err = showSettings(env, tmpfile) + if err != nil { + return err + } + + contents, err := getFileContents(tmpfile.Name()) + assert.NoError(t, err) + + buf := new(bytes.Buffer) + encoder := toml.NewEncoder(buf) + err = encoder.Encode(env) + assert.NoError(t, err) + + expectedContents := buf.String() + + assert.Equal(t, expectedContents, contents) + + return nil +} + +func TestEnvShowSettings(t *testing.T) { + tmpdir, err := ioutil.TempDir("", "") + if err != nil { + panic(err) + } + defer os.RemoveAll(tmpdir) + + tmpfile, err := ioutil.TempFile("", "envShowSettings-") + assert.NoError(t, err) + defer os.Remove(tmpfile.Name()) + + err = testEnvShowSettings(t, tmpdir, tmpfile) + assert.NoError(t, err) +} + +func TestEnvShowSettingsInvalidFile(t *testing.T) { + tmpdir, err := ioutil.TempDir("", "") + if err != nil { + panic(err) + } + defer os.RemoveAll(tmpdir) + + tmpfile, err := ioutil.TempFile("", "envShowSettings-") + assert.NoError(t, err) + + // close the file + tmpfile.Close() + + err = testEnvShowSettings(t, tmpdir, tmpfile) + assert.Error(t, err) +} + +func TestEnvHandleSettings(t *testing.T) { + tmpdir, err := ioutil.TempDir("", "") + if err != nil { + panic(err) + } + defer os.RemoveAll(tmpdir) + + configFile, config, err := makeRuntimeConfig(tmpdir) + assert.NoError(t, err) + + _, err = getExpectedSettings(config, tmpdir, configFile) + assert.NoError(t, err) + + m := map[string]interface{}{ + "configFile": configFile, + "runtimeConfig": config, + } + + tmpfile, err := ioutil.TempFile("", "") + assert.NoError(t, err) + defer os.Remove(tmpfile.Name()) + + err = handleSettings(tmpfile, m) + assert.NoError(t, err) + + var env EnvInfo + + _, err = toml.DecodeFile(tmpfile.Name(), &env) + assert.NoError(t, err) +} + +func TestEnvHandleSettingsInvalidShimConfig(t *testing.T) { + assert := assert.New(t) + + tmpdir, err := ioutil.TempDir("", "") + assert.NoError(err) + defer os.RemoveAll(tmpdir) + + configFile, config, err := makeRuntimeConfig(tmpdir) + assert.NoError(err) + + _, err = getExpectedSettings(config, tmpdir, configFile) + assert.NoError(err) + + config.ShimConfig = "invalid shim config" + + m := map[string]interface{}{ + "configFile": configFile, + "runtimeConfig": config, + } + + tmpfile, err := ioutil.TempFile("", "") + assert.NoError(err) + defer os.Remove(tmpfile.Name()) + + err = handleSettings(tmpfile, m) + assert.Error(err) +} + +func TestEnvHandleSettingsInvalidParams(t *testing.T) { + err := handleSettings(nil, map[string]interface{}{}) + assert.Error(t, err) +} + +func TestEnvHandleSettingsEmptyMap(t *testing.T) { + err := handleSettings(os.Stdout, map[string]interface{}{}) + assert.Error(t, err) +} + +func TestEnvHandleSettingsInvalidFile(t *testing.T) { + m := map[string]interface{}{ + "configFile": "foo", + "runtimeConfig": oci.RuntimeConfig{}, + } + + err := handleSettings(nil, m) + assert.Error(t, err) +} + +func TestEnvHandleSettingsInvalidConfigFileType(t *testing.T) { + m := map[string]interface{}{ + "configFile": 123, + "runtimeConfig": oci.RuntimeConfig{}, + } + + err := handleSettings(os.Stderr, m) + assert.Error(t, err) +} + +func TestEnvHandleSettingsInvalidRuntimeConfigType(t *testing.T) { + m := map[string]interface{}{ + "configFile": "/some/where", + "runtimeConfig": true, + } + + err := handleSettings(os.Stderr, m) + assert.Error(t, err) +} + +func TestEnvCLIFunction(t *testing.T) { + tmpdir, err := ioutil.TempDir("", "") + if err != nil { + panic(err) + } + defer os.RemoveAll(tmpdir) + + configFile, config, err := makeRuntimeConfig(tmpdir) + assert.NoError(t, err) + + _, err = getExpectedSettings(config, tmpdir, configFile) + assert.NoError(t, err) + + app := cli.NewApp() + ctx := cli.NewContext(app, nil, nil) + app.Name = "foo" + + ctx.App.Metadata = map[string]interface{}{ + "configFile": configFile, + "runtimeConfig": config, + } + + fn, ok := kataEnvCLICommand.Action.(func(context *cli.Context) error) + assert.True(t, ok) + + devNull, err := os.OpenFile(os.DevNull, os.O_WRONLY, 0666) + assert.NoError(t, err) + + // throw away output + savedOutputFile := defaultOutputFile + defaultOutputFile = devNull + + defer func() { + defaultOutputFile = savedOutputFile + }() + + err = fn(ctx) + assert.NoError(t, err) +} + +func TestEnvCLIFunctionFail(t *testing.T) { + tmpdir, err := ioutil.TempDir("", "") + if err != nil { + panic(err) + } + defer os.RemoveAll(tmpdir) + + configFile, config, err := makeRuntimeConfig(tmpdir) + assert.NoError(t, err) + + _, err = getExpectedSettings(config, tmpdir, configFile) + assert.NoError(t, err) + + app := cli.NewApp() + ctx := cli.NewContext(app, nil, nil) + app.Name = "foo" + + ctx.App.Metadata = map[string]interface{}{ + "configFile": configFile, + "runtimeConfig": config, + } + + fn, ok := kataEnvCLICommand.Action.(func(context *cli.Context) error) + assert.True(t, ok) + + savedOutputFile := defaultOutputFile + // invalidate + defaultOutputFile = nil + + defer func() { + defaultOutputFile = savedOutputFile + }() + + err = fn(ctx) + assert.Error(t, err) +} + +func TestGetHypervisorInfo(t *testing.T) { + assert := assert.New(t) + + tmpdir, err := ioutil.TempDir("", "") + assert.NoError(err) + defer os.RemoveAll(tmpdir) + + _, config, err := makeRuntimeConfig(tmpdir) + assert.NoError(err) + + info := getHypervisorInfo(config) + assert.Equal(info.Version, testHypervisorVersion) + + err = os.Remove(config.HypervisorConfig.HypervisorPath) + assert.NoError(err) + + info = getHypervisorInfo(config) + assert.Equal(info.Version, unknown) +} diff --git a/cli/kill.go b/cli/kill.go new file mode 100644 index 0000000000..fc274b429d --- /dev/null +++ b/cli/kill.go @@ -0,0 +1,159 @@ +// Copyright (c) 2014,2015,2016 Docker, Inc. +// Copyright (c) 2017 Intel Corporation +// +// 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. + +package main + +import ( + "fmt" + "strconv" + "syscall" + + vc "github.com/kata-containers/runtime/virtcontainers" + "github.com/urfave/cli" +) + +var killCLICommand = cli.Command{ + Name: "kill", + Usage: "Kill sends signals to the container's init process", + ArgsUsage: ` [signal] + + is the name for the instance of the container + [signal] is the signal to be sent to the init process (default: SIGTERM) + +EXAMPLE: + If the container id is "ubuntu01" the following will send a "KILL" signal + to the init process of the "ubuntu01" container: + + # ` + name + ` kill ubuntu01 KILL`, + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "all, a", + Usage: "send the specified signal to all processes inside the container", + }, + }, + Action: func(context *cli.Context) error { + args := context.Args() + if args.Present() == false { + return fmt.Errorf("Missing container ID") + } + + // If signal is provided, it has to be the second argument. + signal := args.Get(1) + if signal == "" { + signal = "SIGTERM" + } + + return kill(args.First(), signal, context.Bool("all")) + }, +} + +var signals = map[string]syscall.Signal{ + "SIGABRT": syscall.SIGABRT, + "SIGALRM": syscall.SIGALRM, + "SIGBUS": syscall.SIGBUS, + "SIGCHLD": syscall.SIGCHLD, + "SIGCLD": syscall.SIGCLD, + "SIGCONT": syscall.SIGCONT, + "SIGFPE": syscall.SIGFPE, + "SIGHUP": syscall.SIGHUP, + "SIGILL": syscall.SIGILL, + "SIGINT": syscall.SIGINT, + "SIGIO": syscall.SIGIO, + "SIGIOT": syscall.SIGIOT, + "SIGKILL": syscall.SIGKILL, + "SIGPIPE": syscall.SIGPIPE, + "SIGPOLL": syscall.SIGPOLL, + "SIGPROF": syscall.SIGPROF, + "SIGPWR": syscall.SIGPWR, + "SIGQUIT": syscall.SIGQUIT, + "SIGSEGV": syscall.SIGSEGV, + "SIGSTKFLT": syscall.SIGSTKFLT, + "SIGSTOP": syscall.SIGSTOP, + "SIGSYS": syscall.SIGSYS, + "SIGTERM": syscall.SIGTERM, + "SIGTRAP": syscall.SIGTRAP, + "SIGTSTP": syscall.SIGTSTP, + "SIGTTIN": syscall.SIGTTIN, + "SIGTTOU": syscall.SIGTTOU, + "SIGUNUSED": syscall.SIGUNUSED, + "SIGURG": syscall.SIGURG, + "SIGUSR1": syscall.SIGUSR1, + "SIGUSR2": syscall.SIGUSR2, + "SIGVTALRM": syscall.SIGVTALRM, + "SIGWINCH": syscall.SIGWINCH, + "SIGXCPU": syscall.SIGXCPU, + "SIGXFSZ": syscall.SIGXFSZ, +} + +func kill(containerID, signal string, all bool) error { + // Checks the MUST and MUST NOT from OCI runtime specification + status, podID, err := getExistingContainerInfo(containerID) + if err != nil { + return err + } + + containerID = status.ID + + signum, err := processSignal(signal) + if err != nil { + return err + } + + // container MUST be created or running + if status.State.State != vc.StateReady && status.State.State != vc.StateRunning { + return fmt.Errorf("Container %s not ready or running, cannot send a signal", containerID) + } + + if err := vci.KillContainer(podID, containerID, signum, all); err != nil { + return err + } + + if signum != syscall.SIGKILL && signum != syscall.SIGTERM { + return nil + } + + _, err = vci.StopContainer(podID, containerID) + return err +} + +func processSignal(signal string) (syscall.Signal, error) { + signum, signalOk := signals[signal] + if signalOk { + return signum, nil + } + + // Support for short name signals (INT) + signum, signalOk = signals["SIG"+signal] + if signalOk { + return signum, nil + } + + // Support for numeric signals + s, err := strconv.Atoi(signal) + if err != nil { + return 0, fmt.Errorf("Failed to convert signal %s to int", signal) + } + + signum = syscall.Signal(s) + // Check whether signal is valid or not + for _, sig := range signals { + if sig == signum { + // signal is a valid signal + return signum, nil + } + } + + return 0, fmt.Errorf("Signal %s is not supported", signal) +} diff --git a/cli/kill_test.go b/cli/kill_test.go new file mode 100644 index 0000000000..cc81c1298f --- /dev/null +++ b/cli/kill_test.go @@ -0,0 +1,280 @@ +// Copyright (c) 2017 Intel Corporation +// +// 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. + +package main + +import ( + "flag" + "fmt" + "syscall" + "testing" + + vc "github.com/kata-containers/runtime/virtcontainers" + "github.com/kata-containers/runtime/virtcontainers/pkg/vcMock" + "github.com/stretchr/testify/assert" +) + +var ( + testKillContainerFuncReturnNil = func(podID, containerID string, signal syscall.Signal, all bool) error { + return nil + } + + testStopContainerFuncReturnNil = func(podID, containerID string) (vc.VCContainer, error) { + return &vcMock.Container{}, nil + } +) + +func TestProcessSignal(t *testing.T) { + tests := []struct { + signal string + valid bool + signum syscall.Signal + }{ + {"SIGDCKBY", false, 0}, //invalid signal + {"DCKBY", false, 0}, //invalid signal + {"99999", false, 0}, //invalid signal + {"SIGTERM", true, syscall.SIGTERM}, + {"TERM", true, syscall.SIGTERM}, + {"15", true, syscall.SIGTERM}, + } + + for _, test := range tests { + signum, err := processSignal(test.signal) + if signum != test.signum { + t.Fatalf("signal received: %d expected signal: %d\n", signum, test.signum) + } + if test.valid && err != nil { + t.Fatalf("signal %s is a valid but a error was received: %s\n", test.signal, err) + } + if !test.valid && err == nil { + t.Fatalf("signal %s is not a valid signal and no error was reported\n", test.signal) + } + } +} + +func testKillCLIFunctionTerminationSignalSuccessful(t *testing.T, sig string) { + assert := assert.New(t) + + state := vc.State{ + State: vc.StateRunning, + } + + testingImpl.KillContainerFunc = testKillContainerFuncReturnNil + testingImpl.StopContainerFunc = testStopContainerFuncReturnNil + testingImpl.ListPodFunc = func() ([]vc.PodStatus, error) { + return newSingleContainerPodStatusList(testPodID, testContainerID, state, state, map[string]string{}), nil + } + defer func() { + testingImpl.KillContainerFunc = nil + testingImpl.ListPodFunc = nil + }() + + set := flag.NewFlagSet("", 0) + set.Parse([]string{testContainerID, sig}) + + execCLICommandFunc(assert, killCLICommand, set, false) +} + +func TestKillCLIFunctionSigkillSuccessful(t *testing.T) { + testKillCLIFunctionTerminationSignalSuccessful(t, "SIGKILL") +} + +func TestKillCLIFunctionSigtermSuccessful(t *testing.T) { + testKillCLIFunctionTerminationSignalSuccessful(t, "SIGTERM") +} + +func TestKillCLIFunctionNotTerminationSignalSuccessful(t *testing.T) { + assert := assert.New(t) + + state := vc.State{ + State: vc.StateRunning, + } + + testingImpl.KillContainerFunc = testKillContainerFuncReturnNil + testingImpl.ListPodFunc = func() ([]vc.PodStatus, error) { + return newSingleContainerPodStatusList(testPodID, testContainerID, state, state, map[string]string{}), nil + } + defer func() { + testingImpl.KillContainerFunc = nil + testingImpl.ListPodFunc = nil + }() + + set := flag.NewFlagSet("", 0) + set.Parse([]string{testContainerID, "SIGUSR1"}) + + execCLICommandFunc(assert, killCLICommand, set, false) +} + +func TestKillCLIFunctionNoSignalSuccessful(t *testing.T) { + assert := assert.New(t) + + state := vc.State{ + State: vc.StateRunning, + } + + testingImpl.KillContainerFunc = testKillContainerFuncReturnNil + testingImpl.ListPodFunc = func() ([]vc.PodStatus, error) { + return newSingleContainerPodStatusList(testPodID, testContainerID, state, state, map[string]string{}), nil + } + defer func() { + testingImpl.KillContainerFunc = nil + testingImpl.ListPodFunc = nil + }() + + set := flag.NewFlagSet("", 0) + set.Parse([]string{testContainerID}) + + execCLICommandFunc(assert, killCLICommand, set, false) +} + +func TestKillCLIFunctionEnableAllSuccessful(t *testing.T) { + assert := assert.New(t) + + state := vc.State{ + State: vc.StateRunning, + } + + testingImpl.KillContainerFunc = func(podID, containerID string, signal syscall.Signal, all bool) error { + if !all { + return fmt.Errorf("Expecting -all flag = true, Got false") + } + + return nil + } + testingImpl.ListPodFunc = func() ([]vc.PodStatus, error) { + return newSingleContainerPodStatusList(testPodID, testContainerID, state, state, map[string]string{}), nil + } + defer func() { + testingImpl.KillContainerFunc = nil + testingImpl.ListPodFunc = nil + }() + + set := flag.NewFlagSet("", 0) + set.Bool("all", true, "") + set.Parse([]string{testContainerID}) + + execCLICommandFunc(assert, killCLICommand, set, false) +} + +func TestKillCLIFunctionNoContainerIDFailure(t *testing.T) { + assert := assert.New(t) + + set := flag.NewFlagSet("", 0) + + execCLICommandFunc(assert, killCLICommand, set, true) +} + +func TestKillCLIFunctionContainerNotExistFailure(t *testing.T) { + assert := assert.New(t) + + testingImpl.KillContainerFunc = testKillContainerFuncReturnNil + testingImpl.ListPodFunc = func() ([]vc.PodStatus, error) { + return []vc.PodStatus{}, nil + } + defer func() { + testingImpl.KillContainerFunc = nil + testingImpl.ListPodFunc = nil + }() + + set := flag.NewFlagSet("", 0) + set.Parse([]string{testContainerID}) + + execCLICommandFunc(assert, killCLICommand, set, true) +} + +func TestKillCLIFunctionInvalidSignalFailure(t *testing.T) { + assert := assert.New(t) + + state := vc.State{ + State: vc.StateRunning, + } + + testingImpl.KillContainerFunc = testKillContainerFuncReturnNil + testingImpl.ListPodFunc = func() ([]vc.PodStatus, error) { + return newSingleContainerPodStatusList(testPodID, testContainerID, state, state, map[string]string{}), nil + } + defer func() { + testingImpl.KillContainerFunc = nil + testingImpl.ListPodFunc = nil + }() + + set := flag.NewFlagSet("", 0) + set.Parse([]string{testContainerID, "SIGINVALID"}) + + execCLICommandFunc(assert, killCLICommand, set, true) +} + +func TestKillCLIFunctionInvalidStatePausedFailure(t *testing.T) { + assert := assert.New(t) + + state := vc.State{ + State: vc.StatePaused, + } + + testingImpl.KillContainerFunc = testKillContainerFuncReturnNil + testingImpl.ListPodFunc = func() ([]vc.PodStatus, error) { + return newSingleContainerPodStatusList(testPodID, testContainerID, state, state, map[string]string{}), nil + } + defer func() { + testingImpl.KillContainerFunc = nil + testingImpl.ListPodFunc = nil + }() + + set := flag.NewFlagSet("", 0) + set.Parse([]string{testContainerID}) + + execCLICommandFunc(assert, killCLICommand, set, true) +} + +func TestKillCLIFunctionInvalidStateStoppedFailure(t *testing.T) { + assert := assert.New(t) + + state := vc.State{ + State: vc.StateStopped, + } + + testingImpl.KillContainerFunc = testKillContainerFuncReturnNil + testingImpl.ListPodFunc = func() ([]vc.PodStatus, error) { + return newSingleContainerPodStatusList(testPodID, testContainerID, state, state, map[string]string{}), nil + } + defer func() { + testingImpl.KillContainerFunc = nil + testingImpl.ListPodFunc = nil + }() + + set := flag.NewFlagSet("", 0) + set.Parse([]string{testContainerID}) + + execCLICommandFunc(assert, killCLICommand, set, true) +} + +func TestKillCLIFunctionKillContainerFailure(t *testing.T) { + assert := assert.New(t) + + state := vc.State{ + State: vc.StateRunning, + } + + testingImpl.ListPodFunc = func() ([]vc.PodStatus, error) { + return newSingleContainerPodStatusList(testPodID, testContainerID, state, state, map[string]string{}), nil + } + defer func() { + testingImpl.ListPodFunc = nil + }() + + set := flag.NewFlagSet("", 0) + set.Parse([]string{testContainerID}) + + execCLICommandFunc(assert, killCLICommand, set, true) +} diff --git a/cli/list.go b/cli/list.go new file mode 100644 index 0000000000..831bb3d064 --- /dev/null +++ b/cli/list.go @@ -0,0 +1,392 @@ +// Copyright (c) 2014,2015,2016,2017 Docker, Inc. +// Copyright (c) 2017 Intel Corporation +// +// 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. + +package main + +import ( + "encoding/json" + "errors" + "fmt" + "os" + "strings" + "syscall" + "text/tabwriter" + "time" + + "github.com/urfave/cli" + + vc "github.com/kata-containers/runtime/virtcontainers" + oci "github.com/kata-containers/runtime/virtcontainers/pkg/oci" +) + +const formatOptions = `table or json` + +// containerState represents the platform agnostic pieces relating to a +// running container's status and state +type containerState struct { + // Version is the OCI version for the container + Version string `json:"ociVersion"` + // ID is the container ID + ID string `json:"id"` + // InitProcessPid is the init process id in the parent namespace + InitProcessPid int `json:"pid"` + // Status is the current status of the container, running, paused, ... + Status string `json:"status"` + // Bundle is the path on the filesystem to the bundle + Bundle string `json:"bundle"` + // Rootfs is a path to a directory containing the container's root filesystem. + Rootfs string `json:"rootfs"` + // Created is the unix timestamp for the creation time of the container in UTC + Created time.Time `json:"created"` + // Annotations is the user defined annotations added to the config. + Annotations map[string]string `json:"annotations,omitempty"` + // The owner of the state directory (the owner of the container). + Owner string `json:"owner"` +} + +type asset struct { + Path string `json:"path"` + Custom bool `json:"bool"` +} + +// hypervisorDetails stores details of the hypervisor used to host +// the container +type hypervisorDetails struct { + HypervisorAsset asset `json:"hypervisorAsset"` + ImageAsset asset `json:"imageAsset"` + KernelAsset asset `json:"kernelAsset"` +} + +// fullContainerState specifies the core state plus the hypervisor +// details +type fullContainerState struct { + containerState + CurrentHypervisorDetails hypervisorDetails `json:"currentHypervisor"` + LatestHypervisorDetails hypervisorDetails `json:"latestHypervisor"` + StaleAssets []string +} + +type formatState interface { + Write(state []fullContainerState, showAll bool, file *os.File) error +} + +type formatJSON struct{} +type formatIDList struct{} +type formatTabular struct{} + +var listCLICommand = cli.Command{ + Name: "list", + Usage: "lists containers started by " + name + " with the given root", + ArgsUsage: ` + +Where the given root is specified via the global option "--root" +(default: "` + defaultRootDirectory + `"). + +EXAMPLE 1: +To list containers created via the default "--root": + # ` + name + ` list + +EXAMPLE 2: +To list containers created using a non-default value for "--root": + # ` + name + ` --root value list`, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "format, f", + Value: "table", + Usage: `select one of: ` + formatOptions, + }, + cli.BoolFlag{ + Name: "quiet, q", + Usage: "display only container IDs", + }, + cli.BoolFlag{ + Name: "kata-all", + Usage: "display all available " + project + " information", + }, + }, + Action: func(context *cli.Context) error { + s, err := getContainers(context) + if err != nil { + return err + } + + file := defaultOutputFile + showAll := context.Bool("kata-all") + + var fs formatState = formatIDList{} + + if context.Bool("quiet") { + fs = formatIDList{} + } else { + + switch context.String("format") { + case "table": + fs = formatTabular{} + + case "json": + fs = formatJSON{} + + default: + return fmt.Errorf("invalid format option") + } + } + + return fs.Write(s, showAll, file) + }, +} + +// getStaleAssetsreturns compares the two specified hypervisorDetails objects +// and returns a list of strings representing which assets in "old" are not +// current compared to "new". If old and new are identical, the empty string +// will be returned. +// +// Notes: +// +// - This function is trivial because it relies upon the fact that new +// containers are always created with the latest versions of all assets. +// +// - WARNING: Since this function only compares local values, it is unable to +// determine if newer (remote) assets are available. +func getStaleAssets(old, new hypervisorDetails) []string { + var stale []string + + if old.KernelAsset.Path != new.KernelAsset.Path { + if old.KernelAsset.Custom { + // The workload kernel asset is a custom one, i.e. it's not coming + // from the runtime configuration file. Thus it does not make sense + // to compare it against the configured kernel asset. + // We assume a custom kernel asset has been updated if the + // corresponding path no longer exists, i.e. it's been replaced by + // a new kernel, e.g. with a new version name. + // Replacing a custom kernel asset binary with exactly the same + // binary name won't allow us to detect if it's staled or not. + if _, err := os.Stat(old.KernelAsset.Path); os.IsNotExist(err) { + stale = append(stale, "kernel") + } + } else { + stale = append(stale, "kernel") + } + } + + if old.ImageAsset.Path != new.ImageAsset.Path { + if old.ImageAsset.Custom { + // The workload image asset is a custom one, i.e. it's not coming + // from the runtime configuration file. Thus it does not make sense + // to compare it against the configured image asset. + // We assume a custom image asset has been updated if the + // corresponding path no longer exists, i.e. it's been replaced by + // a new image, e.g. with a new version name. + // Replacing a custom image asset binary with exactly the same + // binary name won't allow us to detect if it's staled or not. + if _, err := os.Stat(old.ImageAsset.Path); os.IsNotExist(err) { + stale = append(stale, "image") + } + } else { + stale = append(stale, "image") + } + } + + return stale +} + +func (f formatIDList) Write(state []fullContainerState, showAll bool, file *os.File) error { + for _, item := range state { + _, err := fmt.Fprintln(file, item.ID) + if err != nil { + return err + } + } + + return nil +} + +func (f formatTabular) Write(state []fullContainerState, showAll bool, file *os.File) error { + // values used by runc + flags := uint(0) + minWidth := 12 + tabWidth := 1 + padding := 3 + + w := tabwriter.NewWriter(file, minWidth, tabWidth, padding, ' ', flags) + + fmt.Fprint(w, "ID\tPID\tSTATUS\tBUNDLE\tCREATED\tOWNER") + + if showAll { + fmt.Fprint(w, "\tHYPERVISOR\tKERNEL\tIMAGE\tLATEST-KERNEL\tLATEST-IMAGE\tSTALE\n") + } else { + fmt.Fprintf(w, "\n") + } + + for _, item := range state { + fmt.Fprintf(w, "%s\t%d\t%s\t%s\t%s\t%s", + item.ID, + item.InitProcessPid, + item.Status, + item.Bundle, + item.Created.Format(time.RFC3339Nano), + item.Owner) + + if showAll { + stale := strings.Join(item.StaleAssets, ",") + if stale == "" { + stale = "-" + } + + current := item.CurrentHypervisorDetails + latest := item.LatestHypervisorDetails + + all := fmt.Sprintf("\t%s\t%s\t%s", + current.HypervisorAsset.Path, + current.KernelAsset.Path, + current.ImageAsset.Path) + + if !current.KernelAsset.Custom { + all += fmt.Sprintf("\t%s", latest.KernelAsset.Path) + } else { + all += fmt.Sprintf("\t%s", current.KernelAsset.Path) + } + + if !current.ImageAsset.Custom { + all += fmt.Sprintf("\t%s", latest.ImageAsset.Path) + } else { + all += fmt.Sprintf("\t%s", current.ImageAsset.Path) + } + + all += fmt.Sprintf("\t%s\n", stale) + + fmt.Fprint(w, all) + } else { + fmt.Fprint(w, "\n") + } + } + + return w.Flush() +} + +func (f formatJSON) Write(state []fullContainerState, showAll bool, file *os.File) error { + return json.NewEncoder(file).Encode(state) +} + +// getDirOwner returns the UID of the specified directory +func getDirOwner(dir string) (uint32, error) { + if dir == "" { + return 0, errors.New("BUG: need directory") + } + st, err := os.Stat(dir) + if err != nil { + return 0, err + } + + if !st.IsDir() { + return 0, fmt.Errorf("%q is not a directory", dir) + } + + statType, ok := st.Sys().(*syscall.Stat_t) + if !ok { + return 0, fmt.Errorf("cannot convert %+v to stat type for directory %q", st, dir) + } + + return statType.Uid, nil +} + +func getContainers(context *cli.Context) ([]fullContainerState, error) { + runtimeConfig, ok := context.App.Metadata["runtimeConfig"].(oci.RuntimeConfig) + if !ok { + return nil, errors.New("invalid runtime config") + } + + latestHypervisorDetails := getHypervisorDetails(&runtimeConfig.HypervisorConfig) + + podList, err := vci.ListPod() + if err != nil { + return nil, err + } + + var s []fullContainerState + + for _, pod := range podList { + if len(pod.ContainersStatus) == 0 { + // ignore empty pods + continue + } + + currentHypervisorDetails := getHypervisorDetails(&pod.HypervisorConfig) + + for _, container := range pod.ContainersStatus { + ociState := oci.StatusToOCIState(container) + staleAssets := getStaleAssets(currentHypervisorDetails, latestHypervisorDetails) + + uid, err := getDirOwner(container.RootFs) + if err != nil { + return nil, err + } + + owner := fmt.Sprintf("#%v", uid) + + s = append(s, fullContainerState{ + containerState: containerState{ + Version: ociState.Version, + ID: ociState.ID, + InitProcessPid: ociState.Pid, + Status: ociState.Status, + Bundle: ociState.Bundle, + Rootfs: container.RootFs, + Created: container.StartTime, + Annotations: ociState.Annotations, + Owner: owner, + }, + CurrentHypervisorDetails: currentHypervisorDetails, + LatestHypervisorDetails: latestHypervisorDetails, + StaleAssets: staleAssets, + }) + } + } + + return s, nil +} + +// getHypervisorDetails returns details of the latest version of the +// hypervisor and the associated assets. +func getHypervisorDetails(hypervisorConfig *vc.HypervisorConfig) hypervisorDetails { + hypervisorPath, err := hypervisorConfig.HypervisorAssetPath() + if err != nil { + hypervisorPath = hypervisorConfig.HypervisorPath + } + + kernelPath, err := hypervisorConfig.KernelAssetPath() + if err != nil { + kernelPath = hypervisorConfig.KernelPath + } + + imagePath, err := hypervisorConfig.ImageAssetPath() + if err != nil { + imagePath = hypervisorConfig.ImagePath + } + + return hypervisorDetails{ + HypervisorAsset: asset{ + Path: hypervisorPath, + Custom: hypervisorConfig.CustomHypervisorAsset(), + }, + KernelAsset: asset{ + Path: kernelPath, + Custom: hypervisorConfig.CustomKernelAsset(), + }, + ImageAsset: asset{ + Path: imagePath, + Custom: hypervisorConfig.CustomImageAsset(), + }, + } +} diff --git a/cli/list_test.go b/cli/list_test.go new file mode 100644 index 0000000000..9460a65cda --- /dev/null +++ b/cli/list_test.go @@ -0,0 +1,754 @@ +// Copyright (c) 2017 Intel Corporation +// +// 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. + +package main + +import ( + "encoding/json" + "flag" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "regexp" + "strings" + "testing" + "time" + + vc "github.com/kata-containers/runtime/virtcontainers" + vcAnnotations "github.com/kata-containers/runtime/virtcontainers/pkg/annotations" + "github.com/kata-containers/runtime/virtcontainers/pkg/vcMock" + "github.com/stretchr/testify/assert" + "github.com/urfave/cli" +) + +type TestFileWriter struct { + Name string + File *os.File +} + +var hypervisorDetails1 = hypervisorDetails{ + HypervisorAsset: asset{ + Path: "/hypervisor/path", + }, + ImageAsset: asset{ + Path: "/image/path", + }, + KernelAsset: asset{ + Path: "/kernel/path", + }, +} + +var hypervisorDetails2 = hypervisorDetails{ + HypervisorAsset: asset{ + Path: "/hypervisor/path2", + }, + ImageAsset: asset{ + Path: "/image/path2", + }, + KernelAsset: asset{ + Path: "/kernel/path2", + }, +} + +var hypervisorDetails3 = hypervisorDetails{ + HypervisorAsset: asset{ + Path: "/hypervisor/path3", + }, + ImageAsset: asset{ + Path: "/image/path3", + }, + KernelAsset: asset{ + Path: "/kernel/path3", + }, +} + +var testStatuses = []fullContainerState{ + { + containerState: containerState{ + Version: "", + ID: "1", + InitProcessPid: 1234, + Status: "running", + Bundle: "/somewhere/over/the/rainbow", + Created: time.Now().UTC(), + Annotations: map[string]string(nil), + Owner: "#0", + }, + + CurrentHypervisorDetails: hypervisorDetails1, + LatestHypervisorDetails: hypervisorDetails1, + StaleAssets: []string{}, + }, + { + containerState: containerState{ + Version: "", + ID: "2", + InitProcessPid: 2345, + Status: "stopped", + Bundle: "/this/path/is/invalid", + Created: time.Now().UTC(), + Annotations: map[string]string(nil), + Owner: "#0", + }, + + CurrentHypervisorDetails: hypervisorDetails2, + LatestHypervisorDetails: hypervisorDetails2, + StaleAssets: []string{}, + }, + { + containerState: containerState{ + Version: "", + ID: "3", + InitProcessPid: 9999, + Status: "ready", + Bundle: "/foo/bar/baz", + Created: time.Now().UTC(), + Annotations: map[string]string(nil), + Owner: "#0", + }, + + CurrentHypervisorDetails: hypervisorDetails3, + LatestHypervisorDetails: hypervisorDetails3, + StaleAssets: []string{}, + }, +} + +// Implement the io.Writer interface +func (w *TestFileWriter) Write(bytes []byte) (n int, err error) { + return w.File.Write(bytes) +} + +func formatListDataAsBytes(formatter formatState, state []fullContainerState, showAll bool) (bytes []byte, err error) { + tmpfile, err := ioutil.TempFile("", "formatListData-") + if err != nil { + return nil, err + } + + defer os.Remove(tmpfile.Name()) + + err = formatter.Write(state, showAll, tmpfile) + if err != nil { + return nil, err + } + + tmpfile.Close() + + return ioutil.ReadFile(tmpfile.Name()) +} + +func formatListDataAsString(formatter formatState, state []fullContainerState, showAll bool) (lines []string, err error) { + bytes, err := formatListDataAsBytes(formatter, state, showAll) + if err != nil { + return nil, err + } + + lines = strings.Split(string(bytes), "\n") + + // Remove last line if empty + length := len(lines) + last := lines[length-1] + if last == "" { + lines = lines[:length-1] + } + + return lines, nil +} + +func TestStateToIDList(t *testing.T) { + + // no header + expectedLength := len(testStatuses) + + // showAll should not affect the output + for _, showAll := range []bool{true, false} { + lines, err := formatListDataAsString(&formatIDList{}, testStatuses, showAll) + if err != nil { + t.Fatal(err) + } + + var expected []string + for _, s := range testStatuses { + expected = append(expected, s.ID) + } + + length := len(lines) + + if length != expectedLength { + t.Fatalf("Expected %d lines, got %d: %v", expectedLength, length, lines) + } + + assert.Equal(t, lines, expected, "lines + expected") + } +} + +func TestStateToTabular(t *testing.T) { + // +1 for header line + expectedLength := len(testStatuses) + 1 + + expectedDefaultHeaderPattern := `\AID\s+PID\s+STATUS\s+BUNDLE\s+CREATED\s+OWNER` + expectedExtendedHeaderPattern := `HYPERVISOR\s+KERNEL\s+IMAGE\s+LATEST-KERNEL\s+LATEST-IMAGE\s+STALE` + endingPattern := `\s*\z` + + lines, err := formatListDataAsString(&formatTabular{}, testStatuses, false) + if err != nil { + t.Fatal(err) + } + + length := len(lines) + + expectedHeaderPattern := expectedDefaultHeaderPattern + endingPattern + expectedHeaderRE := regexp.MustCompile(expectedHeaderPattern) + + if length != expectedLength { + t.Fatalf("Expected %d lines, got %d", expectedLength, length) + } + + header := lines[0] + + matches := expectedHeaderRE.FindAllStringSubmatch(header, -1) + if matches == nil { + t.Fatalf("Header line failed to match:\n"+ + "pattern : %v\n"+ + "line : %v\n", + expectedDefaultHeaderPattern, + header) + } + + for i, status := range testStatuses { + lineIndex := i + 1 + line := lines[lineIndex] + + expectedLinePattern := fmt.Sprintf(`\A%s\s+%d\s+%s\s+%s\s+%s\s+%s\s*\z`, + regexp.QuoteMeta(status.ID), + status.InitProcessPid, + regexp.QuoteMeta(status.Status), + regexp.QuoteMeta(status.Bundle), + regexp.QuoteMeta(status.Created.Format(time.RFC3339Nano)), + regexp.QuoteMeta(status.Owner)) + + expectedLineRE := regexp.MustCompile(expectedLinePattern) + + matches := expectedLineRE.FindAllStringSubmatch(line, -1) + if matches == nil { + t.Fatalf("Data line failed to match:\n"+ + "pattern : %v\n"+ + "line : %v\n", + expectedLinePattern, + line) + } + } + + // Try again with full details this time + lines, err = formatListDataAsString(&formatTabular{}, testStatuses, true) + if err != nil { + t.Fatal(err) + } + + length = len(lines) + + expectedHeaderPattern = expectedDefaultHeaderPattern + `\s+` + expectedExtendedHeaderPattern + endingPattern + expectedHeaderRE = regexp.MustCompile(expectedHeaderPattern) + + if length != expectedLength { + t.Fatalf("Expected %d lines, got %d", expectedLength, length) + } + + header = lines[0] + + matches = expectedHeaderRE.FindAllStringSubmatch(header, -1) + if matches == nil { + t.Fatalf("Header line failed to match:\n"+ + "pattern : %v\n"+ + "line : %v\n", + expectedDefaultHeaderPattern, + header) + } + + for i, status := range testStatuses { + lineIndex := i + 1 + line := lines[lineIndex] + + expectedLinePattern := fmt.Sprintf(`\A%s\s+%d\s+%s\s+%s\s+%s\s+%s\s+%s\s+%s\s+%s\s+%s\s+%s\s+%s\s*\z`, + regexp.QuoteMeta(status.ID), + status.InitProcessPid, + regexp.QuoteMeta(status.Status), + regexp.QuoteMeta(status.Bundle), + regexp.QuoteMeta(status.Created.Format(time.RFC3339Nano)), + regexp.QuoteMeta(status.Owner), + regexp.QuoteMeta(status.CurrentHypervisorDetails.HypervisorAsset.Path), + regexp.QuoteMeta(status.CurrentHypervisorDetails.KernelAsset.Path), + regexp.QuoteMeta(status.CurrentHypervisorDetails.ImageAsset.Path), + regexp.QuoteMeta(status.LatestHypervisorDetails.KernelAsset.Path), + regexp.QuoteMeta(status.LatestHypervisorDetails.ImageAsset.Path), + regexp.QuoteMeta("-")) + + expectedLineRE := regexp.MustCompile(expectedLinePattern) + + matches := expectedLineRE.FindAllStringSubmatch(line, -1) + if matches == nil { + t.Fatalf("Data line failed to match:\n"+ + "pattern : %v\n"+ + "line : %v\n", + expectedLinePattern, + line) + } + } +} + +func TestStateToJSON(t *testing.T) { + expectedLength := len(testStatuses) + + // showAll should not affect the output + for _, showAll := range []bool{true, false} { + bytes, err := formatListDataAsBytes(&formatJSON{}, testStatuses, showAll) + if err != nil { + t.Fatal(err) + } + + // Force capacity to match the original otherwise assert.Equal() complains. + states := make([]fullContainerState, 0, len(testStatuses)) + + err = json.Unmarshal(bytes, &states) + if err != nil { + t.Fatal(err) + } + + length := len(states) + + if length != expectedLength { + t.Fatalf("Expected %d lines, got %d", expectedLength, length) + } + + // golang tip (what will presumably become v1.9) now + // stores a monotonic clock value as part of time.Time's + // internal representation (this is shown by a suffix in + // the form "m=±ddd.nnnnnnnnn" when calling String() on + // the time.Time object). However, this monotonic value + // is stripped out when marshaling. + // + // This behaviour change makes comparing the original + // object and the marshaled-and-then-unmarshaled copy of + // the object doomed to failure. + // + // The solution? Manually strip the monotonic time out + // of the original before comparison (yuck!) + // + // See: + // + // - https://go-review.googlesource.com/c/36255/7/src/time/time.go#54 + // + for i := 0; i < expectedLength; i++ { + // remove monotonic time part + testStatuses[i].Created = testStatuses[i].Created.Truncate(0) + } + + assert.Equal(t, states, testStatuses, "states + testStatuses") + } +} + +func TestListCLIFunctionNoContainers(t *testing.T) { + app := cli.NewApp() + ctx := cli.NewContext(app, nil, nil) + app.Name = "foo" + ctx.App.Metadata = map[string]interface{}{ + "foo": "bar", + } + + fn, ok := listCLICommand.Action.(func(context *cli.Context) error) + assert.True(t, ok) + + err := fn(ctx) + + // no config in the Metadata + assert.Error(t, err) +} + +func TestListGetContainersListPodFail(t *testing.T) { + assert := assert.New(t) + + tmpdir, err := ioutil.TempDir(testDir, "") + assert.NoError(err) + defer os.RemoveAll(tmpdir) + + app := cli.NewApp() + ctx := cli.NewContext(app, nil, nil) + app.Name = "foo" + + runtimeConfig, err := newTestRuntimeConfig(tmpdir, testConsole, true) + assert.NoError(err) + + ctx.App.Metadata = map[string]interface{}{ + "runtimeConfig": runtimeConfig, + } + + _, err = getContainers(ctx) + assert.Error(err) + assert.True(vcMock.IsMockError(err)) +} + +func TestListGetContainers(t *testing.T) { + assert := assert.New(t) + + testingImpl.ListPodFunc = func() ([]vc.PodStatus, error) { + // No pre-existing pods + return []vc.PodStatus{}, nil + } + + defer func() { + testingImpl.ListPodFunc = nil + }() + + tmpdir, err := ioutil.TempDir(testDir, "") + assert.NoError(err) + defer os.RemoveAll(tmpdir) + + app := cli.NewApp() + ctx := cli.NewContext(app, nil, nil) + app.Name = "foo" + + runtimeConfig, err := newTestRuntimeConfig(tmpdir, testConsole, true) + assert.NoError(err) + + ctx.App.Metadata = map[string]interface{}{ + "runtimeConfig": runtimeConfig, + } + + state, err := getContainers(ctx) + assert.NoError(err) + assert.Equal(state, []fullContainerState(nil)) +} + +func TestListGetContainersPodWithoutContainers(t *testing.T) { + assert := assert.New(t) + + pod := &vcMock.Pod{ + MockID: testPodID, + } + + testingImpl.ListPodFunc = func() ([]vc.PodStatus, error) { + return []vc.PodStatus{ + { + ID: pod.ID(), + ContainersStatus: []vc.ContainerStatus(nil), + }, + }, nil + } + + defer func() { + testingImpl.ListPodFunc = nil + }() + + tmpdir, err := ioutil.TempDir(testDir, "") + assert.NoError(err) + defer os.RemoveAll(tmpdir) + + app := cli.NewApp() + ctx := cli.NewContext(app, nil, nil) + app.Name = "foo" + + runtimeConfig, err := newTestRuntimeConfig(tmpdir, testConsole, true) + assert.NoError(err) + + ctx.App.Metadata = map[string]interface{}{ + "runtimeConfig": runtimeConfig, + } + + state, err := getContainers(ctx) + assert.NoError(err) + assert.Equal(state, []fullContainerState(nil)) +} + +func TestListGetContainersPodWithContainer(t *testing.T) { + assert := assert.New(t) + + tmpdir, err := ioutil.TempDir(testDir, "") + assert.NoError(err) + defer os.RemoveAll(tmpdir) + + pod := &vcMock.Pod{ + MockID: testPodID, + } + + rootfs := filepath.Join(tmpdir, "rootfs") + err = os.MkdirAll(rootfs, testDirMode) + assert.NoError(err) + + testingImpl.ListPodFunc = func() ([]vc.PodStatus, error) { + return []vc.PodStatus{ + { + ID: pod.ID(), + ContainersStatus: []vc.ContainerStatus{ + { + ID: pod.ID(), + Annotations: map[string]string{}, + RootFs: rootfs, + }, + }, + }, + }, nil + } + + defer func() { + testingImpl.ListPodFunc = nil + }() + + app := cli.NewApp() + ctx := cli.NewContext(app, nil, nil) + app.Name = "foo" + + runtimeConfig, err := newTestRuntimeConfig(tmpdir, testConsole, true) + assert.NoError(err) + + ctx.App.Metadata = map[string]interface{}{ + "runtimeConfig": runtimeConfig, + } + + _, err = getContainers(ctx) + assert.NoError(err) +} + +func TestListCLIFunctionFormatFail(t *testing.T) { + assert := assert.New(t) + + tmpdir, err := ioutil.TempDir(testDir, "") + assert.NoError(err) + defer os.RemoveAll(tmpdir) + + quietFlags := flag.NewFlagSet("test", 0) + quietFlags.Bool("quiet", true, "") + + tableFlags := flag.NewFlagSet("test", 0) + tableFlags.String("format", "table", "") + + jsonFlags := flag.NewFlagSet("test", 0) + jsonFlags.String("format", "json", "") + + invalidFlags := flag.NewFlagSet("test", 0) + invalidFlags.String("format", "not-a-valid-format", "") + + type testData struct { + format string + flags *flag.FlagSet + } + + data := []testData{ + {"quiet", quietFlags}, + {"table", tableFlags}, + {"json", jsonFlags}, + {"invalid", invalidFlags}, + } + + pod := &vcMock.Pod{ + MockID: testPodID, + } + + rootfs := filepath.Join(tmpdir, "rootfs") + + testingImpl.ListPodFunc = func() ([]vc.PodStatus, error) { + return []vc.PodStatus{ + { + ID: pod.ID(), + ContainersStatus: []vc.ContainerStatus{ + { + ID: pod.ID(), + Annotations: map[string]string{ + vcAnnotations.ContainerTypeKey: string(vc.PodSandbox), + }, + RootFs: rootfs, + }, + }, + }, + }, nil + } + + defer func() { + testingImpl.ListPodFunc = nil + }() + + savedOutputFile := defaultOutputFile + defer func() { + defaultOutputFile = savedOutputFile + }() + + // purposely invalid + var invalidFile *os.File + + for _, d := range data { + // start off with an invalid output file + defaultOutputFile = invalidFile + + app := cli.NewApp() + ctx := cli.NewContext(app, d.flags, nil) + app.Name = "foo" + ctx.App.Metadata = map[string]interface{}{ + "foo": "bar", + } + + fn, ok := listCLICommand.Action.(func(context *cli.Context) error) + assert.True(ok, d) + + err = fn(ctx) + + // no config in the Metadata + assert.Error(err, d) + + runtimeConfig, err := newTestRuntimeConfig(tmpdir, testConsole, true) + assert.NoError(err, d) + + ctx.App.Metadata["runtimeConfig"] = runtimeConfig + + _ = os.Remove(rootfs) + + err = fn(ctx) + assert.Error(err) + + err = os.MkdirAll(rootfs, testDirMode) + assert.NoError(err) + + err = fn(ctx) + + // invalid output file + assert.Error(err, d) + assert.False(vcMock.IsMockError(err), d) + + output := filepath.Join(tmpdir, "output") + f, err := os.OpenFile(output, os.O_WRONLY|os.O_CREATE, testFileMode) + assert.NoError(err) + defer f.Close() + + // output file is now valid + defaultOutputFile = f + + err = fn(ctx) + if d.format == "invalid" { + assert.Error(err) + assert.False(vcMock.IsMockError(err), d) + } else { + assert.NoError(err) + } + } +} + +func TestListCLIFunctionQuiet(t *testing.T) { + assert := assert.New(t) + + tmpdir, err := ioutil.TempDir(testDir, "") + assert.NoError(err) + defer os.RemoveAll(tmpdir) + + runtimeConfig, err := newTestRuntimeConfig(tmpdir, testConsole, true) + assert.NoError(err) + + pod := &vcMock.Pod{ + MockID: testPodID, + } + + rootfs := filepath.Join(tmpdir, "rootfs") + err = os.MkdirAll(rootfs, testDirMode) + assert.NoError(err) + + testingImpl.ListPodFunc = func() ([]vc.PodStatus, error) { + return []vc.PodStatus{ + { + ID: pod.ID(), + ContainersStatus: []vc.ContainerStatus{ + { + ID: pod.ID(), + Annotations: map[string]string{ + vcAnnotations.ContainerTypeKey: string(vc.PodSandbox), + }, + RootFs: rootfs, + }, + }, + }, + }, nil + } + + defer func() { + testingImpl.ListPodFunc = nil + }() + + set := flag.NewFlagSet("test", 0) + set.Bool("quiet", true, "") + + app := cli.NewApp() + ctx := cli.NewContext(app, set, nil) + app.Name = "foo" + ctx.App.Metadata = map[string]interface{}{ + "runtimeConfig": runtimeConfig, + } + + savedOutputFile := defaultOutputFile + defer func() { + defaultOutputFile = savedOutputFile + }() + + output := filepath.Join(tmpdir, "output") + f, err := os.OpenFile(output, os.O_CREATE|os.O_WRONLY|os.O_SYNC, testFileMode) + assert.NoError(err) + defer f.Close() + + defaultOutputFile = f + + fn, ok := listCLICommand.Action.(func(context *cli.Context) error) + assert.True(ok) + + err = fn(ctx) + assert.NoError(err) + f.Close() + + text, err := getFileContents(output) + assert.NoError(err) + + trimmed := strings.TrimSpace(text) + assert.Equal(testPodID, trimmed) +} + +func TestListGetDirOwner(t *testing.T) { + assert := assert.New(t) + + tmpdir, err := ioutil.TempDir(testDir, "") + assert.NoError(err) + defer os.RemoveAll(tmpdir) + + _, err = getDirOwner("") + // invalid parameter + assert.Error(err) + + dir := filepath.Join(tmpdir, "dir") + + _, err = getDirOwner(dir) + // ENOENT + assert.Error(err) + + err = createEmptyFile(dir) + assert.NoError(err) + + _, err = getDirOwner(dir) + // wrong file type + assert.Error(err) + + err = os.Remove(dir) + assert.NoError(err) + + err = os.MkdirAll(dir, testDirMode) + assert.NoError(err) + + uid := uint32(os.Getuid()) + + dirUID, err := getDirOwner(dir) + assert.NoError(err) + assert.Equal(dirUID, uid) +} diff --git a/cli/logger.go b/cli/logger.go new file mode 100644 index 0000000000..0763306992 --- /dev/null +++ b/cli/logger.go @@ -0,0 +1,79 @@ +// Copyright (c) 2017 Intel Corporation +// +// 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. + +package main + +import ( + "log/syslog" + "time" + + "github.com/sirupsen/logrus" + lSyslog "github.com/sirupsen/logrus/hooks/syslog" +) + +// sysLogHook wraps a syslog logrus hook and a formatter to be used for all +// syslog entries. +// +// This is necessary to allow the main logger (for "--log=") to use a custom +// formatter ("--log-format=") whilst allowing the system logger to use a +// different formatter. +type sysLogHook struct { + shook *lSyslog.SyslogHook + formatter logrus.Formatter +} + +func (h *sysLogHook) Levels() []logrus.Level { + return h.shook.Levels() +} + +// Fire is responsible for adding a log entry to the system log. It switches +// formatter before adding the system log entry, then reverts the original log +// formatter. +func (h *sysLogHook) Fire(e *logrus.Entry) (err error) { + formatter := e.Logger.Formatter + + e.Logger.Formatter = h.formatter + + err = h.shook.Fire(e) + + e.Logger.Formatter = formatter + + return err +} + +func newSystemLogHook(network, raddr string) (*sysLogHook, error) { + hook, err := lSyslog.NewSyslogHook(network, raddr, syslog.LOG_INFO, name) + if err != nil { + return nil, err + } + + return &sysLogHook{ + formatter: &logrus.TextFormatter{ + TimestampFormat: time.RFC3339Nano, + }, + shook: hook, + }, nil +} + +// handleSystemLog sets up the system-level logger. +func handleSystemLog(network, raddr string) error { + hook, err := newSystemLogHook(network, raddr) + if err != nil { + return err + } + + kataLog.Logger.Hooks.Add(hook) + + return nil +} diff --git a/cli/logger_test.go b/cli/logger_test.go new file mode 100644 index 0000000000..495f2a0960 --- /dev/null +++ b/cli/logger_test.go @@ -0,0 +1,163 @@ +// Copyright (c) 2017 Intel Corporation +// +// 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. + +package main + +import ( + "fmt" + "io/ioutil" + "regexp" + "strings" + "testing" + "time" + + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" +) + +type testData struct { + network string + raddr string + expectError bool +} + +func init() { + // Ensure all log levels are logged + kataLog.Logger.Level = logrus.DebugLevel + + // Discard log output + kataLog.Logger.Out = ioutil.Discard +} + +func TestHandleSystemLog(t *testing.T) { + assert := assert.New(t) + + data := []testData{ + {"invalid-net-type", "999.999.999.999", true}, + {"invalid net-type", "a a ", true}, + {"invalid-net-type", ".", true}, + {"moo", "999.999.999.999", true}, + {"moo", "999.999.999.999:99999999999999999", true}, + {"qwerty", "uiop:ftw!", true}, + {"", "", false}, + } + + for _, d := range data { + err := handleSystemLog(d.network, d.raddr) + if d.expectError { + assert.Error(err, fmt.Sprintf("%+v", d)) + } else { + assert.NoError(err, fmt.Sprintf("%+v", d)) + } + } +} + +func TestNewSystemLogHook(t *testing.T) { + assert := assert.New(t) + + hook, err := newSystemLogHook("", "") + assert.NoError(err) + + msg := "wibble" + level := logrus.DebugLevel + + logger := logrus.New() + + // ensure all output is displayed + logger.Level = logrus.DebugLevel + + // throw away all stdout so that the Format() call + // below returns the data in structured form. + logger.Out = ioutil.Discard + + entry := &logrus.Entry{ + Logger: logger, + + // UTC for time.Parse() + Time: time.Now().UTC(), + + Message: msg, + Level: level, + } + + // call the formatting function directly and see if the output + // matches what we expect. + bytes, err := hook.formatter.Format(entry) + assert.NoError(err) + + output := string(bytes) + output = strings.TrimSpace(output) + output = strings.Replace(output, `"`, "", -1) + + fields := strings.Fields(output) + + msgFound := "" + timeFound := "" + levelFound := "" + + // look for the expected fields + for _, field := range fields { + + // split each structured field into name and value fields + f := strings.Split(field, "=") + assert.True(len(f) >= 2) + + switch f[0] { + case "level": + levelFound = f[1] + case "msg": + msgFound = f[1] + case "time": + timeFound = f[1] + } + } + + assert.Equal(levelFound, level.String()) + assert.Equal(msgFound, msg) + assert.NotEqual(timeFound, "") + + // Tell time.Parse() how to handle the timestamps by putting it into + // the standard golang time format equivalent to: + // + // "Mon Jan 2 15:04:05 -0700 MST 2006". + // + expectedTimeFormat := "2006-01-02T15:04:05.999999999Z" + + // Note that time.Parse() assumes a UTC time. + _, err = time.Parse(expectedTimeFormat, timeFound) + assert.NoError(err) + + // time.Parse() is "clever" but also doesn't check anything more + // granular than a second, so let's be completely paranoid and check + // via regular expression too. + expectedPattern := + // YYYY-MM-DD + `\d{4}-\d{2}-\d{2}` + + // time separator + `T` + + // HH:MM:SS + `\d{2}:\d{2}:\d{2}` + + // high-precision separator + `.` + + // nano-seconds. Note that the quantifier range is + // required because the time.RFC3339Nano format + // trunctates trailing zeros. + `\d{1,9}` + + // UTC timezone specifier + `Z` + + expectedRE := regexp.MustCompile(expectedPattern) + matched := expectedRE.FindAllStringSubmatch(timeFound, -1) + assert.NotNil(matched, "expected time in format %q, got %q", expectedPattern, timeFound) +} diff --git a/cli/main.go b/cli/main.go new file mode 100644 index 0000000000..5dbfdb3e13 --- /dev/null +++ b/cli/main.go @@ -0,0 +1,386 @@ +// Copyright (c) 2014,2015,2016 Docker, Inc. +// Copyright (c) 2017-2018 Intel Corporation +// +// 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. + +package main + +import ( + "fmt" + "io" + "os" + "os/signal" + goruntime "runtime" + "strings" + "syscall" + + vc "github.com/kata-containers/runtime/virtcontainers" + "github.com/kata-containers/runtime/virtcontainers/pkg/oci" + specs "github.com/opencontainers/runtime-spec/specs-go" + "github.com/sirupsen/logrus" + "github.com/urfave/cli" +) + +// specConfig is the name of the file holding the containers configuration +const specConfig = "config.json" + +// arch is the architecture for the running program +const arch = goruntime.GOARCH + +var usage = fmt.Sprintf(`%s runtime + +%s is a command line program for running applications packaged +according to the Open Container Initiative (OCI).`, name, name) + +var notes = fmt.Sprintf(` +NOTES: + +- Commands starting "%s-" and options starting "--%s-" are `+project+` extensions. + +URL: + + The canonical URL for this project is: %s + +`, projectPrefix, projectPrefix, projectURL) + +// kataLog is the logger used to record all messages +var kataLog *logrus.Entry + +// originalLoggerLevel is the default log level. It is used to revert the +// current log level back to its original value if debug output is not +// required. +var originalLoggerLevel logrus.Level + +// if true, coredump when an internal error occurs or a fatal signal is received +var crashOnError = false + +// concrete virtcontainer implementation +var virtcontainersImpl = &vc.VCImpl{} + +// vci is used to access a particular virtcontainers implementation. +// Normally, it refers to the official package, but is re-assigned in +// the tests to allow virtcontainers to be mocked. +var vci vc.VC = virtcontainersImpl + +// defaultOutputFile is the default output file to write the gathered +// information to. +var defaultOutputFile = os.Stdout + +// defaultErrorFile is the default output file to write error +// messages to. +var defaultErrorFile = os.Stderr + +// runtimeFlags is the list of supported global command-line flags +var runtimeFlags = []cli.Flag{ + cli.StringFlag{ + Name: configFilePathOption, + Usage: project + " config file path", + }, + cli.StringFlag{ + Name: "log", + Value: "/dev/null", + Usage: "set the log file path where internal debug information is written", + }, + cli.StringFlag{ + Name: "log-format", + Value: "text", + Usage: "set the format used by logs ('text' (default), or 'json')", + }, + cli.StringFlag{ + Name: "root", + Value: defaultRootDirectory, + Usage: "root directory for storage of container state (this should be located in tmpfs)", + }, + cli.BoolFlag{ + Name: showConfigPathsOption, + Usage: "show config file paths that will be checked for (in order)", + }, +} + +// runtimeCommands is the list of supported command-line (sub-) +// commands. +var runtimeCommands = []cli.Command{ + createCLICommand, + deleteCLICommand, + execCLICommand, + killCLICommand, + listCLICommand, + pauseCLICommand, + psCLICommand, + resumeCLICommand, + runCLICommand, + startCLICommand, + stateCLICommand, + versionCLICommand, + + // Kata Containers specific extensions + kataCheckCLICommand, + kataEnvCLICommand, +} + +// runtimeBeforeSubcommands is the function to run before command-line +// parsing occurs. +var runtimeBeforeSubcommands = beforeSubcommands + +// runtimeCommandNotFound is the function to handle an invalid sub-command. +var runtimeCommandNotFound = commandNotFound + +// runtimeVersion is the function that returns the full version +// string describing the runtime. +var runtimeVersion = makeVersionString + +// saved default cli package values (for testing). +var savedCLIAppHelpTemplate = cli.AppHelpTemplate +var savedCLIVersionPrinter = cli.VersionPrinter +var savedCLIErrWriter = cli.ErrWriter + +func init() { + kataLog = logrus.WithFields(logrus.Fields{ + "name": name, + "source": "runtime", + "pid": os.Getpid(), + }) + + // Save the original log level and then set to debug level to ensure + // that any problems detected before the config file is parsed are + // logged. This is required since the config file determines the true + // log level for the runtime: once parsed the log level is set + // appropriately but for issues between now and completion of the + // config file parsing, it is prudent to operate in verbose mode. + originalLoggerLevel = kataLog.Logger.Level + kataLog.Logger.Level = logrus.DebugLevel +} + +func setupSignalHandler() { + sigCh := make(chan os.Signal, 8) + + for _, sig := range fatalSignals() { + signal.Notify(sigCh, sig) + } + + go func() { + sig := <-sigCh + + nativeSignal, ok := sig.(syscall.Signal) + if ok { + if fatalSignal(nativeSignal) { + kataLog.WithField("signal", sig).Error("received fatal signal") + die() + } + } + }() +} + +// beforeSubcommands is the function to perform preliminary checks +// before command-line parsing occurs. +func beforeSubcommands(context *cli.Context) error { + if context.GlobalBool(showConfigPathsOption) { + files := getDefaultConfigFilePaths() + + for _, file := range files { + fmt.Fprintf(defaultOutputFile, "%s\n", file) + } + + exit(0) + } + + if userWantsUsage(context) || (context.NArg() == 1 && (context.Args()[0] == checkCmd)) { + // No setup required if the user just + // wants to see the usage statement or are + // running a command that does not manipulate + // containers. + return nil + } + + if path := context.GlobalString("log"); path != "" { + f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND|os.O_SYNC, 0640) + if err != nil { + return err + } + kataLog.Logger.Out = f + } + + switch context.GlobalString("log-format") { + case "text": + // retain logrus's default. + case "json": + kataLog.Logger.Formatter = new(logrus.JSONFormatter) + default: + return fmt.Errorf("unknown log-format %q", context.GlobalString("log-format")) + } + + // Set virtcontainers logger. + vci.SetLogger(kataLog) + + // Set the OCI package logger. + oci.SetLogger(kataLog) + + ignoreLogging := false + + // Add the name of the sub-command to each log entry for easier + // debugging. + cmdName := context.Args().First() + if context.App.Command(cmdName) != nil { + kataLog = kataLog.WithField("command", cmdName) + } + + if context.NArg() == 1 && context.Args()[0] == envCmd { + // simply report the logging setup + ignoreLogging = true + } + + configFile, runtimeConfig, err := loadConfiguration(context.GlobalString(configFilePathOption), ignoreLogging) + if err != nil { + fatal(err) + } + + args := strings.Join(context.Args(), " ") + + fields := logrus.Fields{ + "version": version, + "commit": commit, + "arguments": `"` + args + `"`, + } + + kataLog.WithFields(fields).Info() + + // make the data accessible to the sub-commands. + context.App.Metadata = map[string]interface{}{ + "runtimeConfig": runtimeConfig, + "configFile": configFile, + } + + return nil +} + +// function called when an invalid command is specified which causes the +// runtime to error. +func commandNotFound(c *cli.Context, command string) { + err := fmt.Errorf("Invalid command %q", command) + fatal(err) +} + +// makeVersionString returns a multi-line string describing the runtime +// version along with the version of the OCI specification it supports. +func makeVersionString() string { + v := make([]string, 0, 3) + + versionStr := version + if versionStr == "" { + versionStr = unknown + } + + v = append(v, name+" : "+versionStr) + + commitStr := commit + if commitStr == "" { + commitStr = unknown + } + + v = append(v, " commit : "+commitStr) + + specVersionStr := specs.Version + if specVersionStr == "" { + specVersionStr = unknown + } + + v = append(v, " OCI specs: "+specVersionStr) + + return strings.Join(v, "\n") +} + +// setCLIGlobals modifies various cli package global variables +func setCLIGlobals() { + cli.AppHelpTemplate = fmt.Sprintf(`%s%s`, cli.AppHelpTemplate, notes) + + // Override the default function to display version details to + // ensure the "--version" option and "version" command are identical. + cli.VersionPrinter = func(c *cli.Context) { + fmt.Fprintln(defaultOutputFile, c.App.Version) + } + + // If the command returns an error, cli takes upon itself to print + // the error on cli.ErrWriter and exit. + // Use our own writer here to ensure the log gets sent to the right + // location. + cli.ErrWriter = &fatalWriter{cli.ErrWriter} +} + +// createRuntimeApp creates an application to process the command-line +// arguments and invoke the requested runtime command. +func createRuntimeApp(args []string) error { + app := cli.NewApp() + + app.Name = name + app.Writer = defaultOutputFile + app.Usage = usage + app.CommandNotFound = runtimeCommandNotFound + app.Version = runtimeVersion() + app.Flags = runtimeFlags + app.Commands = runtimeCommands + app.Before = runtimeBeforeSubcommands + app.EnableBashCompletion = true + + return app.Run(args) +} + +// userWantsUsage determines if the user only wishes to see the usage +// statement. +func userWantsUsage(context *cli.Context) bool { + if context.NArg() == 0 { + return true + } + + if context.NArg() == 1 && (context.Args()[0] == "help" || context.Args()[0] == "version") { + return true + } + + if context.NArg() >= 2 && (context.Args()[1] == "-h" || context.Args()[1] == "--help") { + return true + } + + return false +} + +// fatal prints the error's details exits the program. +func fatal(err error) { + kataLog.Error(err) + fmt.Fprintln(defaultErrorFile, err) + exit(1) +} + +type fatalWriter struct { + cliErrWriter io.Writer +} + +func (f *fatalWriter) Write(p []byte) (n int, err error) { + // Ensure error is logged before displaying to the user + kataLog.Error(string(p)) + return f.cliErrWriter.Write(p) +} + +func createRuntime() { + setupSignalHandler() + + setCLIGlobals() + + err := createRuntimeApp(os.Args) + if err != nil { + fatal(err) + } +} + +func main() { + defer handlePanic() + createRuntime() +} diff --git a/cli/main_test.go b/cli/main_test.go new file mode 100644 index 0000000000..cb7470ba7c --- /dev/null +++ b/cli/main_test.go @@ -0,0 +1,1115 @@ +// +// Copyright (c) 2017 Intel Corporation +// +// 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. +// + +package main + +import ( + "bytes" + "encoding/json" + "errors" + "flag" + "fmt" + "io/ioutil" + "os" + "os/exec" + "path" + "path/filepath" + "regexp" + "strings" + "testing" + + "github.com/dlespiau/covertool/pkg/cover" + vc "github.com/kata-containers/runtime/virtcontainers" + "github.com/kata-containers/runtime/virtcontainers/pkg/oci" + "github.com/kata-containers/runtime/virtcontainers/pkg/vcMock" + specs "github.com/opencontainers/runtime-spec/specs-go" + "github.com/stretchr/testify/assert" + "github.com/urfave/cli" +) + +const ( + testDisabledNeedRoot = "Test disabled as requires root user" + testDisabledNeedNonRoot = "Test disabled as requires non-root user" + testDirMode = os.FileMode(0750) + testFileMode = os.FileMode(0640) + testExeFileMode = os.FileMode(0750) + + // small docker image used to create root filesystems from + testDockerImage = "busybox" + + testPodID = "99999999-9999-9999-99999999999999999" + testContainerID = "1" + testBundle = "bundle" +) + +var ( + // package variables set by calling TestMain() + testDir = "" + testBundleDir = "" +) + +// testingImpl is a concrete mock RVC implementation used for testing +var testingImpl = &vcMock.VCMock{} + +func init() { + if version == "" { + panic("ERROR: invalid build: version not set") + } + + if commit == "" { + panic("ERROR: invalid build: commit not set") + } + + if defaultSysConfRuntimeConfiguration == "" { + panic("ERROR: invalid build: defaultSysConfRuntimeConfiguration not set") + } + + if defaultRuntimeConfiguration == "" { + panic("ERROR: invalid build: defaultRuntimeConfiguration not set") + } + + fmt.Printf("INFO: running as actual user %v (effective %v), actual group %v (effective %v)\n", + os.Getuid(), os.Geteuid(), os.Getgid(), os.Getegid()) + + fmt.Printf("INFO: switching to fake virtcontainers implementation for testing\n") + vci = testingImpl + + var err error + + fmt.Printf("INFO: creating test directory\n") + testDir, err = ioutil.TempDir("", fmt.Sprintf("%s-", name)) + if err != nil { + panic(fmt.Sprintf("ERROR: failed to create test directory: %v", err)) + } + + fmt.Printf("INFO: test directory is %v\n", testDir) + + fmt.Printf("INFO: ensuring docker is running\n") + output, err := runCommandFull([]string{"docker", "version"}, true) + if err != nil { + panic(fmt.Sprintf("ERROR: docker daemon is not installed, not running, or not accessible to current user: %v (error %v)", + output, err)) + } + + // Do this now to avoid hitting the test timeout value due to + // slow network response. + fmt.Printf("INFO: ensuring required docker image (%v) is available\n", testDockerImage) + + // Only hit the network if the image doesn't exist locally + _, err = runCommand([]string{"docker", "image", "inspect", testDockerImage}) + if err == nil { + fmt.Printf("INFO: docker image %v already exists locally\n", testDockerImage) + } else { + _, err = runCommand([]string{"docker", "pull", testDockerImage}) + if err != nil { + panic(err) + } + } + + testBundleDir = filepath.Join(testDir, testBundle) + err = os.MkdirAll(testBundleDir, testDirMode) + if err != nil { + panic(fmt.Sprintf("ERROR: failed to create bundle directory %v: %v", testBundleDir, err)) + } + + fmt.Printf("INFO: creating OCI bundle in %v for tests to use\n", testBundleDir) + err = realMakeOCIBundle(testBundleDir) + if err != nil { + panic(fmt.Sprintf("ERROR: failed to create OCI bundle: %v", err)) + } +} + +// resetCLIGlobals undoes the effects of setCLIGlobals(), restoring the original values +func resetCLIGlobals() { + cli.AppHelpTemplate = savedCLIAppHelpTemplate + cli.VersionPrinter = savedCLIVersionPrinter + cli.ErrWriter = savedCLIErrWriter +} + +func runUnitTests(m *testing.M) { + ret := m.Run() + + os.RemoveAll(testDir) + + os.Exit(ret) +} + +// Read fail that should contain a CompatOCISpec and +// return its JSON representation on success +func readOCIConfigJSON(configFile string) (string, error) { + bundlePath := filepath.Dir(configFile) + ociSpec, err := oci.ParseConfigJSON(bundlePath) + if err != nil { + return "", nil + } + ociSpecJSON, err := json.Marshal(ociSpec) + if err != nil { + return "", err + } + return string(ociSpecJSON), err +} + +// TestMain is the common main function used by ALL the test functions +// for this package. +func TestMain(m *testing.M) { + // Parse the command line using the stdlib flag package so the flags defined + // in the testing package get populated. + cover.ParseAndStripTestFlags() + + // Make sure we have the opportunity to flush the coverage report to disk when + // terminating the process. + atexit(cover.FlushProfiles) + + // If the test binary name is kata-runtime.coverage, we've are being asked to + // run the coverage-instrumented kata-runtime. + if path.Base(os.Args[0]) == name+".coverage" || + path.Base(os.Args[0]) == name { + main() + exit(0) + } + + runUnitTests(m) +} + +func createEmptyFile(path string) (err error) { + return ioutil.WriteFile(path, []byte(""), testFileMode) +} + +func grep(pattern, file string) error { + if file == "" { + return errors.New("need file") + } + + bytes, err := ioutil.ReadFile(file) + if err != nil { + return err + } + + re := regexp.MustCompile(pattern) + matches := re.FindAllStringSubmatch(string(bytes), -1) + + if matches == nil { + return fmt.Errorf("pattern %q not found in file %q", pattern, file) + } + + return nil +} + +// newTestHypervisorConfig creaets a new virtcontainers +// HypervisorConfig, ensuring that the required resources are also +// created. +// +// Note: no parameter validation in case caller wishes to create an invalid +// object. +func newTestHypervisorConfig(dir string, create bool) (vc.HypervisorConfig, error) { + kernelPath := path.Join(dir, "kernel") + imagePath := path.Join(dir, "image") + hypervisorPath := path.Join(dir, "hypervisor") + + if create { + for _, file := range []string{kernelPath, imagePath, hypervisorPath} { + err := createEmptyFile(file) + if err != nil { + return vc.HypervisorConfig{}, err + } + } + } + + return vc.HypervisorConfig{ + KernelPath: kernelPath, + ImagePath: imagePath, + HypervisorPath: hypervisorPath, + HypervisorMachineType: "pc-lite", + }, nil +} + +// newTestRuntimeConfig creates a new RuntimeConfig +func newTestRuntimeConfig(dir, consolePath string, create bool) (oci.RuntimeConfig, error) { + if dir == "" { + return oci.RuntimeConfig{}, errors.New("BUG: need directory") + } + + hypervisorConfig, err := newTestHypervisorConfig(dir, create) + if err != nil { + return oci.RuntimeConfig{}, err + } + + return oci.RuntimeConfig{ + HypervisorType: vc.QemuHypervisor, + HypervisorConfig: hypervisorConfig, + AgentType: vc.KataContainersAgent, + ProxyType: vc.CCProxyType, + ShimType: vc.CCShimType, + Console: consolePath, + }, nil +} + +// createOCIConfig creates an OCI configuration (spec) file in +// the bundle directory specified (which must exist). +func createOCIConfig(bundleDir string) error { + if bundleDir == "" { + return errors.New("BUG: Need bundle directory") + } + + if !fileExists(bundleDir) { + return fmt.Errorf("BUG: Bundle directory %s does not exist", bundleDir) + } + + var configCmd string + + // Search for a suitable version of runc to use to generate + // the OCI config file. + for _, cmd := range []string{"docker-runc", "runc"} { + fullPath, err := exec.LookPath(cmd) + if err == nil { + configCmd = fullPath + break + } + } + + if configCmd == "" { + return errors.New("Cannot find command to generate OCI config file") + } + + _, err := runCommand([]string{configCmd, "spec", "--bundle", bundleDir}) + if err != nil { + return err + } + + specFile := filepath.Join(bundleDir, specConfig) + if !fileExists(specFile) { + return fmt.Errorf("generated OCI config file does not exist: %v", specFile) + } + + return nil +} + +// createRootfs creates a minimal root filesystem below the specified +// directory. +func createRootfs(dir string) error { + err := os.MkdirAll(dir, testDirMode) + if err != nil { + return err + } + + container, err := runCommand([]string{"docker", "create", testDockerImage}) + if err != nil { + return err + } + + cmd1 := exec.Command("docker", "export", container) + cmd2 := exec.Command("tar", "-C", dir, "-xvf", "-") + + cmd1Stdout, err := cmd1.StdoutPipe() + if err != nil { + return err + } + + cmd2.Stdin = cmd1Stdout + + err = cmd2.Start() + if err != nil { + return err + } + + err = cmd1.Run() + if err != nil { + return err + } + + err = cmd2.Wait() + if err != nil { + return err + } + + // Clean up + _, err = runCommand([]string{"docker", "rm", container}) + if err != nil { + return err + } + + return nil +} + +// realMakeOCIBundle will create an OCI bundle (including the "config.json" +// config file) in the directory specified (which must already exist). +// +// XXX: Note that tests should *NOT* call this function - they should +// XXX: instead call makeOCIBundle(). +func realMakeOCIBundle(bundleDir string) error { + if bundleDir == "" { + return errors.New("BUG: Need bundle directory") + } + + if !fileExists(bundleDir) { + return fmt.Errorf("BUG: Bundle directory %v does not exist", bundleDir) + } + + err := createOCIConfig(bundleDir) + if err != nil { + return err + } + + // Note the unusual parameter (a directory, not the config + // file to parse!) + spec, err := oci.ParseConfigJSON(bundleDir) + if err != nil { + return err + } + + // Determine the rootfs directory name the OCI config refers to + ociRootPath := spec.Root.Path + + rootfsDir := filepath.Join(bundleDir, ociRootPath) + + if strings.HasPrefix(ociRootPath, "/") { + return fmt.Errorf("Cannot handle absolute rootfs as bundle must be unique to each test") + } + + err = createRootfs(rootfsDir) + if err != nil { + return err + } + + return nil +} + +// Create an OCI bundle in the specified directory. +// +// Note that the directory will be created, but it's parent is expected to exist. +// +// This function works by copying the already-created test bundle. Ideally, +// the bundle would be recreated for each test, but createRootfs() uses +// docker which on some systems is too slow, resulting in the tests timing +// out. +func makeOCIBundle(bundleDir string) error { + from := testBundleDir + to := bundleDir + + // only the basename of bundleDir needs to exist as bundleDir + // will get created by cp(1). + base := filepath.Dir(bundleDir) + + for _, dir := range []string{from, base} { + if !fileExists(dir) { + return fmt.Errorf("BUG: directory %v should exist", dir) + } + } + + output, err := runCommandFull([]string{"cp", "-a", from, to}, true) + if err != nil { + return fmt.Errorf("failed to copy test OCI bundle from %v to %v: %v (output: %v)", from, to, err, output) + } + + return nil +} + +// readOCIConfig returns an OCI spec. +func readOCIConfigFile(configPath string) (oci.CompatOCISpec, error) { + if configPath == "" { + return oci.CompatOCISpec{}, errors.New("BUG: need config file path") + } + + data, err := ioutil.ReadFile(configPath) + if err != nil { + return oci.CompatOCISpec{}, err + } + + var ociSpec oci.CompatOCISpec + if err := json.Unmarshal(data, &ociSpec); err != nil { + return oci.CompatOCISpec{}, err + } + + return ociSpec, nil +} + +func writeOCIConfigFile(spec oci.CompatOCISpec, configPath string) error { + if configPath == "" { + return errors.New("BUG: need config file path") + } + + bytes, err := json.MarshalIndent(spec, "", "\t") + if err != nil { + return err + } + + return ioutil.WriteFile(configPath, bytes, testFileMode) +} + +func newSingleContainerPodStatusList(podID, containerID string, podState, containerState vc.State, annotations map[string]string) []vc.PodStatus { + return []vc.PodStatus{ + { + ID: podID, + State: podState, + ContainersStatus: []vc.ContainerStatus{ + { + ID: containerID, + State: containerState, + Annotations: annotations, + }, + }, + }, + } +} + +func execCLICommandFunc(assertHandler *assert.Assertions, cliCommand cli.Command, set *flag.FlagSet, expectedErr bool) { + app := cli.NewApp() + ctx := cli.NewContext(app, set, nil) + app.Name = "foo" + + fn, ok := cliCommand.Action.(func(context *cli.Context) error) + assertHandler.True(ok) + + err := fn(ctx) + + if expectedErr { + assertHandler.Error(err) + } else { + assertHandler.Nil(err) + } +} + +func TestMakeOCIBundle(t *testing.T) { + assert := assert.New(t) + + tmpdir, err := ioutil.TempDir(testDir, "") + assert.NoError(err) + defer os.RemoveAll(tmpdir) + + bundleDir := filepath.Join(tmpdir, "bundle") + + err = makeOCIBundle(bundleDir) + assert.NoError(err) + + specFile := filepath.Join(bundleDir, specConfig) + assert.True(fileExists(specFile)) +} + +func TestCreateOCIConfig(t *testing.T) { + assert := assert.New(t) + + tmpdir, err := ioutil.TempDir(testDir, "") + assert.NoError(err) + defer os.RemoveAll(tmpdir) + + bundleDir := filepath.Join(tmpdir, "bundle") + + err = createOCIConfig(bundleDir) + // ENOENT + assert.Error(err) + + err = os.MkdirAll(bundleDir, testDirMode) + assert.NoError(err) + + err = createOCIConfig(bundleDir) + assert.NoError(err) + + specFile := filepath.Join(bundleDir, specConfig) + assert.True(fileExists(specFile)) +} + +func TestCreateRootfs(t *testing.T) { + assert := assert.New(t) + + tmpdir, err := ioutil.TempDir(testDir, "") + assert.NoError(err) + defer os.RemoveAll(tmpdir) + + rootfsDir := filepath.Join(tmpdir, "rootfs") + assert.False(fileExists(rootfsDir)) + + err = createRootfs(rootfsDir) + assert.NoError(err) + + // non-comprehensive list of expected directories + expectedDirs := []string{"bin", "dev", "etc", "usr", "var"} + + assert.True(fileExists(rootfsDir)) + + for _, dir := range expectedDirs { + dirPath := filepath.Join(rootfsDir, dir) + assert.True(fileExists(dirPath)) + } +} + +func TestMainUserWantsUsage(t *testing.T) { + assert := assert.New(t) + app := cli.NewApp() + + type testData struct { + arguments []string + expectTrue bool + } + + data := []testData{ + {[]string{}, true}, + {[]string{"help"}, true}, + {[]string{"version"}, true}, + {[]string{"sub-command", "-h"}, true}, + {[]string{"sub-command", "--help"}, true}, + + {[]string{""}, false}, + {[]string{"sub-command", "--foo"}, false}, + {[]string{"kata-check"}, false}, + {[]string{"haaaalp"}, false}, + {[]string{"wibble"}, false}, + {[]string{"versioned"}, false}, + } + + for i, d := range data { + set := flag.NewFlagSet("", 0) + set.Parse(d.arguments) + + ctx := cli.NewContext(app, set, nil) + result := userWantsUsage(ctx) + + if d.expectTrue { + assert.True(result, "test %d (%+v)", i, d) + } else { + assert.False(result, "test %d (%+v)", i, d) + } + } +} + +func TestMainBeforeSubCommands(t *testing.T) { + assert := assert.New(t) + app := cli.NewApp() + + type testData struct { + arguments []string + expectError bool + } + + data := []testData{ + {[]string{}, false}, + {[]string{"help"}, false}, + {[]string{"version"}, false}, + {[]string{"sub-command", "-h"}, false}, + {[]string{"sub-command", "--help"}, false}, + {[]string{"kata-check"}, false}, + } + + for i, d := range data { + set := flag.NewFlagSet("", 0) + set.Parse(d.arguments) + + ctx := cli.NewContext(app, set, nil) + err := beforeSubcommands(ctx) + + if d.expectError { + assert.Errorf(err, "test %d (%+v)", i, d) + } else { + assert.NoError(err, "test %d (%+v)", i, d) + } + } +} + +func TestMainBeforeSubCommandsInvalidLogFile(t *testing.T) { + assert := assert.New(t) + + tmpdir, err := ioutil.TempDir(testDir, "") + assert.NoError(err) + defer os.RemoveAll(tmpdir) + + logFile := filepath.Join(tmpdir, "log") + + // create the file as the wrong type to force a failure + err = os.MkdirAll(logFile, testDirMode) + assert.NoError(err) + + app := cli.NewApp() + + set := flag.NewFlagSet("", 0) + set.String("log", logFile, "") + set.Parse([]string{"create"}) + + ctx := cli.NewContext(app, set, nil) + + err = beforeSubcommands(ctx) + assert.Error(err) +} + +func TestMainBeforeSubCommandsInvalidLogFormat(t *testing.T) { + assert := assert.New(t) + + tmpdir, err := ioutil.TempDir(testDir, "") + assert.NoError(err) + defer os.RemoveAll(tmpdir) + + logFile := filepath.Join(tmpdir, "log") + + app := cli.NewApp() + + set := flag.NewFlagSet("", 0) + set.Bool("debug", true, "") + set.String("log", logFile, "") + set.String("log-format", "captain-barnacles", "") + set.Parse([]string{"create"}) + + logOut := kataLog.Logger.Out + kataLog.Logger.Out = nil + + defer func() { + kataLog.Logger.Out = logOut + }() + + ctx := cli.NewContext(app, set, nil) + + err = beforeSubcommands(ctx) + assert.Error(err) + assert.NotNil(kataLog.Logger.Out) +} + +func TestMainBeforeSubCommandsLoadConfigurationFail(t *testing.T) { + assert := assert.New(t) + + tmpdir, err := ioutil.TempDir(testDir, "") + assert.NoError(err) + defer os.RemoveAll(tmpdir) + + logFile := filepath.Join(tmpdir, "log") + configFile := filepath.Join(tmpdir, "config") + + app := cli.NewApp() + + for _, logFormat := range []string{"json", "text"} { + set := flag.NewFlagSet("", 0) + set.Bool("debug", true, "") + set.String("log", logFile, "") + set.String("log-format", logFormat, "") + set.String("kata-config", configFile, "") + set.Parse([]string{"kata-env"}) + + ctx := cli.NewContext(app, set, nil) + + savedExitFunc := exitFunc + + exitStatus := 0 + exitFunc = func(status int) { exitStatus = status } + + defer func() { + exitFunc = savedExitFunc + }() + + // calls fatal() so no return + _ = beforeSubcommands(ctx) + assert.NotEqual(exitStatus, 0) + } +} + +func TestMainBeforeSubCommandsShowCCConfigPaths(t *testing.T) { + assert := assert.New(t) + + tmpdir, err := ioutil.TempDir(testDir, "") + assert.NoError(err) + defer os.RemoveAll(tmpdir) + + app := cli.NewApp() + + set := flag.NewFlagSet("", 0) + set.Bool("kata-show-default-config-paths", true, "") + + ctx := cli.NewContext(app, set, nil) + + savedExitFunc := exitFunc + + exitStatus := 99 + exitFunc = func(status int) { exitStatus = status } + + defer func() { + exitFunc = savedExitFunc + }() + + savedOutputFile := defaultOutputFile + + defer func() { + resetCLIGlobals() + defaultOutputFile = savedOutputFile + }() + + output := filepath.Join(tmpdir, "output") + f, err := os.OpenFile(output, os.O_CREATE|os.O_WRONLY|os.O_SYNC, testFileMode) + assert.NoError(err) + defer f.Close() + + defaultOutputFile = f + + setCLIGlobals() + + _ = beforeSubcommands(ctx) + assert.Equal(exitStatus, 0) + + text, err := getFileContents(output) + assert.NoError(err) + + lines := strings.Split(text, "\n") + + // Remove last line if empty + length := len(lines) + last := lines[length-1] + if last == "" { + lines = lines[:length-1] + } + + assert.Equal(len(lines), 2) + + for i, line := range lines { + switch i { + case 0: + assert.Equal(line, defaultSysConfRuntimeConfiguration) + case 1: + assert.Equal(line, defaultRuntimeConfiguration) + } + } +} + +func TestMainFatal(t *testing.T) { + assert := assert.New(t) + + tmpdir, err := ioutil.TempDir(testDir, "") + assert.NoError(err) + defer os.RemoveAll(tmpdir) + + var exitStatus int + savedExitFunc := exitFunc + + exitFunc = func(status int) { exitStatus = status } + + savedErrorFile := defaultErrorFile + + output := filepath.Join(tmpdir, "output") + f, err := os.OpenFile(output, os.O_CREATE|os.O_WRONLY|os.O_SYNC, testFileMode) + assert.NoError(err) + defaultErrorFile = f + + defer func() { + f.Close() + defaultErrorFile = savedErrorFile + exitFunc = savedExitFunc + }() + + exitError := errors.New("hello world") + + fatal(exitError) + assert.Equal(exitStatus, 1) + + text, err := getFileContents(output) + assert.NoError(err) + + trimmed := strings.TrimSpace(text) + assert.Equal(exitError.Error(), trimmed) +} + +func testVersionString(assert *assert.Assertions, versionString, expectedVersion, expectedCommit, expectedOCIVersion string) { + foundVersion := false + foundCommit := false + foundOCIVersion := false + + versionRE := regexp.MustCompile(fmt.Sprintf(`%s\s*:\s*%v`, name, expectedVersion)) + commitRE := regexp.MustCompile(fmt.Sprintf(`%s\s*:\s*%v`, "commit", expectedCommit)) + + ociRE := regexp.MustCompile(fmt.Sprintf(`%s\s*:\s*%v`, "OCI specs", expectedOCIVersion)) + + lines := strings.Split(versionString, "\n") + assert.True(len(lines) > 0) + + for _, line := range lines { + vMatches := versionRE.FindAllStringSubmatch(line, -1) + if vMatches != nil { + foundVersion = true + } + + cMatches := commitRE.FindAllStringSubmatch(line, -1) + if cMatches != nil { + foundCommit = true + } + + oMatches := ociRE.FindAllStringSubmatch(line, -1) + if oMatches != nil { + foundOCIVersion = true + } + } + + args := fmt.Sprintf("versionString: %q, expectedVersion: %q, expectedCommit: %v, expectedOCIVersion: %v\n", + versionString, expectedVersion, expectedCommit, expectedOCIVersion) + + assert.True(foundVersion, args) + assert.True(foundCommit, args) + assert.True(foundOCIVersion, args) +} + +func TestMainMakeVersionString(t *testing.T) { + assert := assert.New(t) + + v := makeVersionString() + + testVersionString(assert, v, version, commit, specs.Version) +} + +func TestMainMakeVersionStringNoVersion(t *testing.T) { + assert := assert.New(t) + + savedVersion := version + version = "" + + defer func() { + version = savedVersion + }() + + v := makeVersionString() + + testVersionString(assert, v, unknown, commit, specs.Version) +} + +func TestMainMakeVersionStringNoCommit(t *testing.T) { + assert := assert.New(t) + + savedCommit := commit + commit = "" + + defer func() { + commit = savedCommit + }() + + v := makeVersionString() + + testVersionString(assert, v, version, unknown, specs.Version) +} + +func TestMainMakeVersionStringNoOCIVersion(t *testing.T) { + assert := assert.New(t) + + savedVersion := specs.Version + specs.Version = "" + + defer func() { + specs.Version = savedVersion + }() + + v := makeVersionString() + + testVersionString(assert, v, version, commit, unknown) +} + +func TestMainCreateRuntimeApp(t *testing.T) { + assert := assert.New(t) + + savedBefore := runtimeBeforeSubcommands + savedOutputFile := defaultOutputFile + + // disable + runtimeBeforeSubcommands = nil + + devNull, err := os.OpenFile("/dev/null", os.O_RDWR, 0640) + assert.NoError(err) + defer devNull.Close() + + defaultOutputFile = devNull + + setCLIGlobals() + + defer func() { + resetCLIGlobals() + runtimeBeforeSubcommands = savedBefore + defaultOutputFile = savedOutputFile + }() + + args := []string{name} + + err = createRuntimeApp(args) + assert.NoError(err, "%v", args) +} + +func TestMainCreateRuntimeAppInvalidSubCommand(t *testing.T) { + assert := assert.New(t) + + exitStatus := 0 + + savedBefore := runtimeBeforeSubcommands + savedExitFunc := exitFunc + + exitFunc = func(status int) { exitStatus = status } + + // disable + runtimeBeforeSubcommands = nil + + defer func() { + runtimeBeforeSubcommands = savedBefore + exitFunc = savedExitFunc + }() + + // calls fatal() so no return + _ = createRuntimeApp([]string{name, "i-am-an-invalid-sub-command"}) + + assert.NotEqual(exitStatus, 0) +} + +func TestMainCreateRuntime(t *testing.T) { + assert := assert.New(t) + + const cmd = "foo" + const msg = "moo FAILURE" + + resetCLIGlobals() + + exitStatus := 0 + + savedOSArgs := os.Args + savedExitFunc := exitFunc + savedBefore := runtimeBeforeSubcommands + savedCommands := runtimeCommands + + os.Args = []string{name, cmd} + exitFunc = func(status int) { exitStatus = status } + + // disable + runtimeBeforeSubcommands = nil + + // override sub-commands + runtimeCommands = []cli.Command{ + { + Name: cmd, + Action: func(context *cli.Context) error { + return errors.New(msg) + }, + }, + } + + defer func() { + os.Args = savedOSArgs + exitFunc = savedExitFunc + runtimeBeforeSubcommands = savedBefore + runtimeCommands = savedCommands + }() + + assert.Equal(exitStatus, 0) + createRuntime() + assert.NotEqual(exitStatus, 0) +} + +func TestMainVersionPrinter(t *testing.T) { + assert := assert.New(t) + + tmpdir, err := ioutil.TempDir("", "") + assert.NoError(err) + defer os.RemoveAll(tmpdir) + + savedOutputFile := defaultOutputFile + + defer func() { + resetCLIGlobals() + defaultOutputFile = savedOutputFile + }() + + output := filepath.Join(tmpdir, "output") + f, err := os.OpenFile(output, os.O_CREATE|os.O_WRONLY|os.O_SYNC, testFileMode) + assert.NoError(err) + defer f.Close() + + defaultOutputFile = f + + setCLIGlobals() + + err = createRuntimeApp([]string{name, "--version"}) + assert.NoError(err) + + err = grep(fmt.Sprintf(`%s\s*:\s*%s`, name, version), output) + assert.NoError(err) +} + +func TestMainFatalWriter(t *testing.T) { + assert := assert.New(t) + + const cmd = "foo" + const msg = "moo FAILURE" + + // create buffer to save logger output + buf := &bytes.Buffer{} + + savedBefore := runtimeBeforeSubcommands + savedLogOutput := kataLog.Logger.Out + savedCLIExiter := cli.OsExiter + savedCommands := runtimeCommands + + // disable + runtimeBeforeSubcommands = nil + + // save all output + kataLog.Logger.Out = buf + + cli.OsExiter = func(status int) {} + + // override sub-commands + runtimeCommands = []cli.Command{ + { + Name: cmd, + Action: func(context *cli.Context) error { + return cli.NewExitError(msg, 42) + }, + }, + } + + defer func() { + runtimeBeforeSubcommands = savedBefore + kataLog.Logger.Out = savedLogOutput + cli.OsExiter = savedCLIExiter + runtimeCommands = savedCommands + }() + + setCLIGlobals() + + err := createRuntimeApp([]string{name, cmd}) + assert.Error(err) + + re := regexp.MustCompile( + fmt.Sprintf(`\blevel\b.*\berror\b.*\b%s\b`, msg)) + matches := re.FindAllStringSubmatch(buf.String(), -1) + assert.NotEmpty(matches) +} + +func TestMainSetCLIGlobals(t *testing.T) { + assert := assert.New(t) + + defer resetCLIGlobals() + + cli.AppHelpTemplate = "" + cli.VersionPrinter = nil + cli.ErrWriter = nil + + setCLIGlobals() + + assert.NotEqual(cli.AppHelpTemplate, "") + assert.NotNil(cli.VersionPrinter) + assert.NotNil(cli.ErrWriter) +} + +func TestMainResetCLIGlobals(t *testing.T) { + assert := assert.New(t) + + assert.NotEqual(cli.AppHelpTemplate, "") + assert.NotNil(savedCLIVersionPrinter) + assert.NotNil(savedCLIErrWriter) + + cli.AppHelpTemplate = "" + cli.VersionPrinter = nil + cli.ErrWriter = nil + + resetCLIGlobals() + + assert.Equal(cli.AppHelpTemplate, savedCLIAppHelpTemplate) + assert.NotNil(cli.VersionPrinter) + assert.NotNil(savedCLIVersionPrinter) +} diff --git a/cli/oci.go b/cli/oci.go new file mode 100644 index 0000000000..c2c9a055dc --- /dev/null +++ b/cli/oci.go @@ -0,0 +1,351 @@ +// Copyright (c) 2017 Intel Corporation +// +// 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. + +package main + +import ( + "bufio" + "errors" + "fmt" + "net" + "os" + "path/filepath" + "strings" + "syscall" + + vc "github.com/kata-containers/runtime/virtcontainers" + "github.com/kata-containers/runtime/virtcontainers/pkg/oci" + "github.com/opencontainers/runc/libcontainer/utils" + specs "github.com/opencontainers/runtime-spec/specs-go" + "github.com/sirupsen/logrus" +) + +// Contants related to cgroup memory directory +const ( + cgroupsTasksFile = "tasks" + cgroupsProcsFile = "cgroup.procs" + cgroupsDirMode = os.FileMode(0750) + cgroupsFileMode = os.FileMode(0640) + + // Filesystem type corresponding to CGROUP_SUPER_MAGIC as listed + // here: http://man7.org/linux/man-pages/man2/statfs.2.html + cgroupFsType = 0x27e0eb +) + +var errNeedLinuxResource = errors.New("Linux resource cannot be empty") + +var cgroupsDirPath string + +var procMountInfo = "/proc/self/mountinfo" + +// getContainerInfo returns the container status and its pod ID. +// It internally expands the container ID from the prefix provided. +func getContainerInfo(containerID string) (vc.ContainerStatus, string, error) { + // container ID MUST be provided. + if containerID == "" { + return vc.ContainerStatus{}, "", fmt.Errorf("Missing container ID") + } + + podStatusList, err := vci.ListPod() + if err != nil { + return vc.ContainerStatus{}, "", err + } + + for _, podStatus := range podStatusList { + for _, containerStatus := range podStatus.ContainersStatus { + if containerStatus.ID == containerID { + return containerStatus, podStatus.ID, nil + } + } + } + + // Not finding a container should not trigger an error as + // getContainerInfo is used for checking the existence and + // the absence of a container ID. + return vc.ContainerStatus{}, "", nil +} + +func getExistingContainerInfo(containerID string) (vc.ContainerStatus, string, error) { + cStatus, podID, err := getContainerInfo(containerID) + if err != nil { + return vc.ContainerStatus{}, "", err + } + + // container ID MUST exist. + if cStatus.ID == "" { + return vc.ContainerStatus{}, "", fmt.Errorf("Container ID (%v) does not exist", containerID) + } + + return cStatus, podID, nil +} + +func validCreateParams(containerID, bundlePath string) (string, error) { + // container ID MUST be provided. + if containerID == "" { + return "", fmt.Errorf("Missing container ID") + } + + // container ID MUST be unique. + cStatus, _, err := getContainerInfo(containerID) + if err != nil { + return "", err + } + + if cStatus.ID != "" { + return "", fmt.Errorf("ID already in use, unique ID should be provided") + } + + // bundle path MUST be provided. + if bundlePath == "" { + return "", fmt.Errorf("Missing bundle path") + } + + // bundle path MUST be valid. + fileInfo, err := os.Stat(bundlePath) + if err != nil { + return "", fmt.Errorf("Invalid bundle path '%s': %s", bundlePath, err) + } + if fileInfo.IsDir() == false { + return "", fmt.Errorf("Invalid bundle path '%s', it should be a directory", bundlePath) + } + + resolved, err := resolvePath(bundlePath) + if err != nil { + return "", err + } + + return resolved, nil +} + +// processCgroupsPath process the cgroups path as expected from the +// OCI runtime specification. It returns a list of complete paths +// that should be created and used for every specified resource. +func processCgroupsPath(ociSpec oci.CompatOCISpec, isPod bool) ([]string, error) { + var cgroupsPathList []string + + if ociSpec.Linux.CgroupsPath == "" { + return []string{}, nil + } + + if ociSpec.Linux.Resources == nil { + return []string{}, nil + } + + if ociSpec.Linux.Resources.Memory != nil { + memCgroupsPath, err := processCgroupsPathForResource(ociSpec, "memory", isPod) + if err != nil { + return []string{}, err + } + + if memCgroupsPath != "" { + cgroupsPathList = append(cgroupsPathList, memCgroupsPath) + } + } + + if ociSpec.Linux.Resources.CPU != nil { + cpuCgroupsPath, err := processCgroupsPathForResource(ociSpec, "cpu", isPod) + if err != nil { + return []string{}, err + } + + if cpuCgroupsPath != "" { + cgroupsPathList = append(cgroupsPathList, cpuCgroupsPath) + } + } + + if ociSpec.Linux.Resources.Pids != nil { + pidsCgroupsPath, err := processCgroupsPathForResource(ociSpec, "pids", isPod) + if err != nil { + return []string{}, err + } + + if pidsCgroupsPath != "" { + cgroupsPathList = append(cgroupsPathList, pidsCgroupsPath) + } + } + + if ociSpec.Linux.Resources.BlockIO != nil { + blkIOCgroupsPath, err := processCgroupsPathForResource(ociSpec, "blkio", isPod) + if err != nil { + return []string{}, err + } + + if blkIOCgroupsPath != "" { + cgroupsPathList = append(cgroupsPathList, blkIOCgroupsPath) + } + } + + return cgroupsPathList, nil +} + +func processCgroupsPathForResource(ociSpec oci.CompatOCISpec, resource string, isPod bool) (string, error) { + if resource == "" { + return "", errNeedLinuxResource + } + + var err error + cgroupsDirPath, err = getCgroupsDirPath(procMountInfo) + if err != nil { + return "", fmt.Errorf("get CgroupsDirPath error: %s", err) + } + + // Relative cgroups path provided. + if filepath.IsAbs(ociSpec.Linux.CgroupsPath) == false { + return filepath.Join(cgroupsDirPath, resource, ociSpec.Linux.CgroupsPath), nil + } + + // Absolute cgroups path provided. + var cgroupMount specs.Mount + cgroupMountFound := false + for _, mount := range ociSpec.Mounts { + if mount.Type == "cgroup" { + cgroupMount = mount + cgroupMountFound = true + break + } + } + + if !cgroupMountFound { + // According to the OCI spec, an absolute path should be + // interpreted as relative to the system cgroup mount point + // when there is no cgroup mount point. + return filepath.Join(cgroupsDirPath, resource, ociSpec.Linux.CgroupsPath), nil + } + + if cgroupMount.Destination == "" { + return "", fmt.Errorf("cgroupsPath is absolute, cgroup mount destination cannot be empty") + } + + cgroupPath := filepath.Join(cgroupMount.Destination, resource) + + // It is not an error to have this cgroup not mounted. It is usually + // due to an old kernel version with missing support for specific + // cgroups. + fields := logrus.Fields{ + "path": cgroupPath, + "type": "cgroup", + } + + if !isCgroupMounted(cgroupPath) { + kataLog.WithFields(fields).Info("path not mounted") + return "", nil + } + + kataLog.WithFields(fields).Info("path mounted") + + return filepath.Join(cgroupPath, ociSpec.Linux.CgroupsPath), nil +} + +func isCgroupMounted(cgroupPath string) bool { + var statFs syscall.Statfs_t + + if err := syscall.Statfs(cgroupPath, &statFs); err != nil { + return false + } + + if statFs.Type != int64(cgroupFsType) { + return false + } + + return true +} + +func setupConsole(consolePath, consoleSockPath string) (string, error) { + if consolePath != "" { + return consolePath, nil + } + + if consoleSockPath == "" { + return "", nil + } + + console, err := newConsole() + if err != nil { + return "", err + } + defer console.master.Close() + + // Open the socket path provided by the caller + conn, err := net.Dial("unix", consoleSockPath) + if err != nil { + return "", err + } + + uConn, ok := conn.(*net.UnixConn) + if !ok { + return "", fmt.Errorf("casting to *net.UnixConn failed") + } + + socket, err := uConn.File() + if err != nil { + return "", err + } + + // Send the parent fd through the provided socket + if err := utils.SendFd(socket, console.master.Name(), console.master.Fd()); err != nil { + return "", err + } + + return console.slavePath, nil +} + +func noNeedForOutput(detach bool, tty bool) bool { + if !detach { + return false + } + + if !tty { + return false + } + + return true +} + +func getCgroupsDirPath(mountInfoFile string) (string, error) { + if cgroupsDirPath != "" { + return cgroupsDirPath, nil + } + + f, err := os.Open(mountInfoFile) + if err != nil { + return "", err + } + defer f.Close() + + var cgroupRootPath string + scanner := bufio.NewScanner(f) + for scanner.Scan() { + text := scanner.Text() + index := strings.Index(text, " - ") + if index < 0 { + continue + } + fields := strings.Split(text, " ") + postSeparatorFields := strings.Fields(text[index+3:]) + numPostFields := len(postSeparatorFields) + + if len(fields) < 5 || postSeparatorFields[0] != "cgroup" || numPostFields < 3 { + continue + } + + cgroupRootPath = filepath.Dir(fields[4]) + break + } + + if _, err = os.Stat(cgroupRootPath); err != nil { + return "", err + } + + return cgroupRootPath, nil +} diff --git a/cli/oci_test.go b/cli/oci_test.go new file mode 100644 index 0000000000..02c11f39aa --- /dev/null +++ b/cli/oci_test.go @@ -0,0 +1,623 @@ +// Copyright (c) 2017 Intel Corporation +// +// 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. + +package main + +import ( + "fmt" + "io/ioutil" + "math/rand" + "net" + "os" + "path/filepath" + "reflect" + "syscall" + "testing" + "time" + + vc "github.com/kata-containers/runtime/virtcontainers" + vcAnnotations "github.com/kata-containers/runtime/virtcontainers/pkg/annotations" + "github.com/kata-containers/runtime/virtcontainers/pkg/oci" + "github.com/kata-containers/runtime/virtcontainers/pkg/vcMock" + "github.com/opencontainers/runc/libcontainer/utils" + specs "github.com/opencontainers/runtime-spec/specs-go" + "github.com/stretchr/testify/assert" +) + +var ( + consolePathTest = "console-test" + consoleSocketPathTest = "console-socket-test" +) + +type cgroupTestDataType struct { + resource string + linuxSpec *specs.LinuxResources +} + +var cgroupTestData = []cgroupTestDataType{ + { + "memory", + &specs.LinuxResources{ + Memory: &specs.LinuxMemory{}, + }, + }, + { + "cpu", + &specs.LinuxResources{ + CPU: &specs.LinuxCPU{}, + }, + }, + { + "pids", + &specs.LinuxResources{ + Pids: &specs.LinuxPids{}, + }, + }, + { + "blkio", + &specs.LinuxResources{ + BlockIO: &specs.LinuxBlockIO{}, + }, + }, +} + +func TestGetContainerInfoContainerIDEmptyFailure(t *testing.T) { + assert := assert.New(t) + status, _, err := getContainerInfo("") + + assert.Error(err, "This test should fail because containerID is empty") + assert.Empty(status.ID, "Expected blank fullID, but got %v", status.ID) +} + +func TestGetContainerInfo(t *testing.T) { + assert := assert.New(t) + + pod := &vcMock.Pod{ + MockID: testPodID, + } + + containerID := testContainerID + + containerStatus := vc.ContainerStatus{ + ID: containerID, + Annotations: map[string]string{ + vcAnnotations.ContainerTypeKey: string(vc.PodSandbox), + }, + } + + testingImpl.ListPodFunc = func() ([]vc.PodStatus, error) { + return []vc.PodStatus{ + { + ID: pod.ID(), + ContainersStatus: []vc.ContainerStatus{containerStatus}, + }, + }, nil + } + + defer func() { + testingImpl.ListPodFunc = nil + }() + + status, podID, err := getContainerInfo(testContainerID) + assert.NoError(err) + assert.Equal(podID, pod.ID()) + assert.Equal(status, containerStatus) +} + +func TestGetContainerInfoMismatch(t *testing.T) { + assert := assert.New(t) + + pod := &vcMock.Pod{ + MockID: testPodID, + } + + containerID := testContainerID + testContainerID + + containerStatus := vc.ContainerStatus{ + ID: containerID, + Annotations: map[string]string{ + vcAnnotations.ContainerTypeKey: string(vc.PodSandbox), + }, + } + + testingImpl.ListPodFunc = func() ([]vc.PodStatus, error) { + return []vc.PodStatus{ + { + ID: pod.ID(), + ContainersStatus: []vc.ContainerStatus{containerStatus}, + }, + }, nil + } + + defer func() { + testingImpl.ListPodFunc = nil + }() + + _, podID, err := getContainerInfo(testContainerID) + assert.NoError(err) + assert.Equal(podID, "") +} + +func TestValidCreateParamsContainerIDEmptyFailure(t *testing.T) { + assert := assert.New(t) + _, err := validCreateParams("", "") + + assert.Error(err, "This test should fail because containerID is empty") + assert.False(vcMock.IsMockError(err)) +} + +func TestGetExistingContainerInfoContainerIDEmptyFailure(t *testing.T) { + assert := assert.New(t) + status, _, err := getExistingContainerInfo("") + + assert.Error(err, "This test should fail because containerID is empty") + assert.Empty(status.ID, "Expected blank fullID, but got %v", status.ID) +} + +func TestValidCreateParamsContainerIDNotUnique(t *testing.T) { + assert := assert.New(t) + + containerID := testContainerID + testContainerID + + pod := &vcMock.Pod{ + MockID: testPodID, + } + + testingImpl.ListPodFunc = func() ([]vc.PodStatus, error) { + return []vc.PodStatus{ + { + ID: pod.ID(), + ContainersStatus: []vc.ContainerStatus{ + // 2 containers with same ID + { + ID: containerID, + Annotations: map[string]string{ + vcAnnotations.ContainerTypeKey: string(vc.PodSandbox), + }, + }, + { + ID: containerID, + Annotations: map[string]string{ + vcAnnotations.ContainerTypeKey: string(vc.PodSandbox), + }, + }, + }, + }, + }, nil + } + + defer func() { + testingImpl.ListPodFunc = nil + }() + + _, err := validCreateParams(testContainerID, "") + + assert.Error(err) + assert.False(vcMock.IsMockError(err)) +} + +func TestValidCreateParamsContainerIDNotUnique2(t *testing.T) { + assert := assert.New(t) + + containerID := testContainerID + testContainerID + + pod := &vcMock.Pod{ + MockID: testPodID, + } + + testingImpl.ListPodFunc = func() ([]vc.PodStatus, error) { + return []vc.PodStatus{ + { + ID: pod.ID(), + ContainersStatus: []vc.ContainerStatus{ + { + ID: containerID, + Annotations: map[string]string{ + vcAnnotations.ContainerTypeKey: string(vc.PodSandbox), + }, + }, + }, + }, + }, nil + } + + defer func() { + testingImpl.ListPodFunc = nil + }() + + _, err := validCreateParams(testContainerID, "") + assert.Error(err) + assert.False(vcMock.IsMockError(err)) +} + +func TestValidCreateParamsInvalidBundle(t *testing.T) { + assert := assert.New(t) + + tmpdir, err := ioutil.TempDir("", "") + assert.NoError(err) + defer os.RemoveAll(tmpdir) + + bundlePath := filepath.Join(tmpdir, "bundle") + + testingImpl.ListPodFunc = func() ([]vc.PodStatus, error) { + return []vc.PodStatus{}, nil + } + + defer func() { + testingImpl.ListPodFunc = nil + }() + + _, err = validCreateParams(testContainerID, bundlePath) + // bundle is ENOENT + assert.Error(err) + assert.False(vcMock.IsMockError(err)) +} + +func TestValidCreateParamsBundleIsAFile(t *testing.T) { + assert := assert.New(t) + + tmpdir, err := ioutil.TempDir("", "") + assert.NoError(err) + defer os.RemoveAll(tmpdir) + + bundlePath := filepath.Join(tmpdir, "bundle") + err = createEmptyFile(bundlePath) + assert.NoError(err) + + testingImpl.ListPodFunc = func() ([]vc.PodStatus, error) { + return []vc.PodStatus{}, nil + } + + defer func() { + testingImpl.ListPodFunc = nil + }() + + _, err = validCreateParams(testContainerID, bundlePath) + // bundle exists as a file, not a directory + assert.Error(err) + assert.False(vcMock.IsMockError(err)) +} + +func testProcessCgroupsPath(t *testing.T, ociSpec oci.CompatOCISpec, expected []string) { + assert := assert.New(t) + result, err := processCgroupsPath(ociSpec, true) + + assert.NoError(err) + + if reflect.DeepEqual(result, expected) == false { + assert.FailNow("DeepEqual failed", "Result path %q should match the expected one %q", result, expected) + } +} + +func TestProcessCgroupsPathEmptyPathSuccessful(t *testing.T) { + ociSpec := oci.CompatOCISpec{} + + ociSpec.Linux = &specs.Linux{ + CgroupsPath: "", + } + + testProcessCgroupsPath(t, ociSpec, []string{}) +} + +func TestProcessCgroupsPathEmptyResources(t *testing.T) { + ociSpec := oci.CompatOCISpec{} + + ociSpec.Linux = &specs.Linux{ + CgroupsPath: "foo", + } + + testProcessCgroupsPath(t, ociSpec, []string{}) +} + +func TestProcessCgroupsPathRelativePathSuccessful(t *testing.T) { + relativeCgroupsPath := "relative/cgroups/path" + cgroupsDirPath = "/foo/runtime/base" + + ociSpec := oci.CompatOCISpec{} + + ociSpec.Linux = &specs.Linux{ + CgroupsPath: relativeCgroupsPath, + } + + for _, d := range cgroupTestData { + ociSpec.Linux.Resources = d.linuxSpec + + p := filepath.Join(cgroupsDirPath, d.resource, relativeCgroupsPath) + + testProcessCgroupsPath(t, ociSpec, []string{p}) + } +} + +func TestProcessCgroupsPathAbsoluteNoCgroupMountSuccessful(t *testing.T) { + absoluteCgroupsPath := "/absolute/cgroups/path" + cgroupsDirPath = "/foo/runtime/base" + + ociSpec := oci.CompatOCISpec{} + + ociSpec.Linux = &specs.Linux{ + CgroupsPath: absoluteCgroupsPath, + } + + for _, d := range cgroupTestData { + ociSpec.Linux.Resources = d.linuxSpec + + p := filepath.Join(cgroupsDirPath, d.resource, absoluteCgroupsPath) + + testProcessCgroupsPath(t, ociSpec, []string{p}) + } +} + +func TestProcessCgroupsPathAbsoluteNoCgroupMountDestinationFailure(t *testing.T) { + assert := assert.New(t) + absoluteCgroupsPath := "/absolute/cgroups/path" + + ociSpec := oci.CompatOCISpec{} + + ociSpec.Mounts = []specs.Mount{ + { + Type: "cgroup", + }, + } + + ociSpec.Linux = &specs.Linux{ + CgroupsPath: absoluteCgroupsPath, + } + + for _, d := range cgroupTestData { + ociSpec.Linux.Resources = d.linuxSpec + for _, isPod := range []bool{true, false} { + _, err := processCgroupsPath(ociSpec, isPod) + assert.Error(err, "This test should fail because no cgroup mount destination provided") + } + } +} + +func TestProcessCgroupsPathAbsoluteSuccessful(t *testing.T) { + assert := assert.New(t) + + if os.Geteuid() != 0 { + t.Skip(testDisabledNeedRoot) + } + + memoryResource := "memory" + absoluteCgroupsPath := "/cgroup/mount/destination" + + cgroupMountDest, err := ioutil.TempDir("", "cgroup-memory-") + assert.NoError(err) + defer os.RemoveAll(cgroupMountDest) + + resourceMountPath := filepath.Join(cgroupMountDest, memoryResource) + err = os.MkdirAll(resourceMountPath, cgroupsDirMode) + assert.NoError(err) + + err = syscall.Mount("go-test", resourceMountPath, "cgroup", 0, memoryResource) + assert.NoError(err) + defer syscall.Unmount(resourceMountPath, 0) + + ociSpec := oci.CompatOCISpec{} + + ociSpec.Linux = &specs.Linux{ + Resources: &specs.LinuxResources{ + Memory: &specs.LinuxMemory{}, + }, + CgroupsPath: absoluteCgroupsPath, + } + + ociSpec.Mounts = []specs.Mount{ + { + Type: "cgroup", + Destination: cgroupMountDest, + }, + } + + testProcessCgroupsPath(t, ociSpec, []string{filepath.Join(resourceMountPath, absoluteCgroupsPath)}) +} + +func TestSetupConsoleExistingConsolePathSuccessful(t *testing.T) { + assert := assert.New(t) + console, err := setupConsole(consolePathTest, "") + + assert.NoError(err) + assert.Equal(console, consolePathTest, "Got %q, Expecting %q", console, consolePathTest) +} + +func TestSetupConsoleExistingConsolePathAndConsoleSocketPathSuccessful(t *testing.T) { + assert := assert.New(t) + console, err := setupConsole(consolePathTest, consoleSocketPathTest) + + assert.NoError(err) + assert.Equal(console, consolePathTest, "Got %q, Expecting %q", console, consolePathTest) +} + +func TestSetupConsoleEmptyPathsSuccessful(t *testing.T) { + assert := assert.New(t) + + console, err := setupConsole("", "") + assert.NoError(err) + assert.Empty(console, "Console path should be empty, got %q instead", console) +} + +func TestSetupConsoleExistingConsoleSocketPath(t *testing.T) { + assert := assert.New(t) + + dir, err := ioutil.TempDir("", "test-socket") + assert.NoError(err) + defer os.RemoveAll(dir) + + sockName := filepath.Join(dir, "console.sock") + + l, err := net.Listen("unix", sockName) + assert.NoError(err) + + console, err := setupConsole("", sockName) + assert.NoError(err) + + waitCh := make(chan error) + go func() { + conn, err1 := l.Accept() + if err != nil { + waitCh <- err1 + } + + uConn, ok := conn.(*net.UnixConn) + if !ok { + waitCh <- fmt.Errorf("casting to *net.UnixConn failed") + } + + f, err1 := uConn.File() + if err != nil { + waitCh <- err1 + } + + _, err1 = utils.RecvFd(f) + waitCh <- err1 + }() + + assert.NotEmpty(console, "Console socket path should not be empty") + + err = <-waitCh + assert.NoError(err) +} + +func TestSetupConsoleNotExistingSocketPathFailure(t *testing.T) { + assert := assert.New(t) + + console, err := setupConsole("", "unknown-sock-path") + assert.Error(err, "This test should fail because the console socket path does not exist") + assert.Empty(console, "This test should fail because the console socket path does not exist") +} + +func testNoNeedForOutput(t *testing.T, detach bool, tty bool, expected bool) { + assert := assert.New(t) + result := noNeedForOutput(detach, tty) + + assert.Equal(result, expected) +} + +func TestNoNeedForOutputDetachTrueTtyTrue(t *testing.T) { + testNoNeedForOutput(t, true, true, true) +} + +func TestNoNeedForOutputDetachFalseTtyTrue(t *testing.T) { + testNoNeedForOutput(t, false, true, false) +} + +func TestNoNeedForOutputDetachFalseTtyFalse(t *testing.T) { + testNoNeedForOutput(t, false, false, false) +} + +func TestNoNeedForOutputDetachTrueTtyFalse(t *testing.T) { + testNoNeedForOutput(t, true, false, false) +} + +func TestIsCgroupMounted(t *testing.T) { + assert := assert.New(t) + + r := rand.New(rand.NewSource(time.Now().Unix())) + randPath := fmt.Sprintf("/path/to/random/%d", r.Int63()) + + assert.False(isCgroupMounted(randPath), "%s does not exist", randPath) + + assert.False(isCgroupMounted(os.TempDir()), "%s is not a cgroup", os.TempDir()) + + cgroupsDirPath = "" + cgroupRootPath, err := getCgroupsDirPath(procMountInfo) + if err != nil { + assert.NoError(err) + } + memoryCgroupPath := filepath.Join(cgroupRootPath, "memory") + if _, err := os.Stat(memoryCgroupPath); os.IsNotExist(err) { + t.Skipf("memory cgroup does not exist: %s", memoryCgroupPath) + } + + assert.True(isCgroupMounted(memoryCgroupPath), "%s is a cgroup", memoryCgroupPath) +} + +func TestProcessCgroupsPathForResource(t *testing.T) { + assert := assert.New(t) + + tmpdir, err := ioutil.TempDir("", "") + assert.NoError(err) + defer os.RemoveAll(tmpdir) + + bundlePath := filepath.Join(tmpdir, "bundle") + + err = makeOCIBundle(bundlePath) + assert.NoError(err) + + ociConfigFile := filepath.Join(bundlePath, specConfig) + assert.True(fileExists(ociConfigFile)) + + spec, err := readOCIConfigFile(ociConfigFile) + assert.NoError(err) + + for _, isPod := range []bool{true, false} { + _, err := processCgroupsPathForResource(spec, "", isPod) + assert.Error(err) + assert.False(vcMock.IsMockError(err)) + } +} + +func TestGetCgroupsDirPath(t *testing.T) { + assert := assert.New(t) + + type testData struct { + contents string + expectedResult string + expectError bool + } + + dir, err := ioutil.TempDir("", "") + if err != nil { + assert.NoError(err) + } + defer os.RemoveAll(dir) + + // make sure tested cgroupsDirPath is existed + testedCgroupDir := filepath.Join(dir, "weirdCgroup") + err = os.Mkdir(testedCgroupDir, testDirMode) + assert.NoError(err) + + weirdCgroupPath := filepath.Join(testedCgroupDir, "memory") + + data := []testData{ + {fmt.Sprintf("num1 num2 num3 / %s num6 num7 - cgroup cgroup rw,memory", weirdCgroupPath), testedCgroupDir, false}, + // cgroup mount is not properly formated, if fields post - less than 3 + {fmt.Sprintf("num1 num2 num3 / %s num6 num7 - cgroup cgroup ", weirdCgroupPath), "", true}, + {"a a a a a a a - b c d", "", true}, + {"a \na b \na b c\na b c d", "", true}, + {"", "", true}, + } + + file := filepath.Join(dir, "mountinfo") + + //file does not exist, should error here + _, err = getCgroupsDirPath(file) + assert.Error(err) + + for _, d := range data { + err := ioutil.WriteFile(file, []byte(d.contents), testFileMode) + assert.NoError(err) + + cgroupsDirPath = "" + path, err := getCgroupsDirPath(file) + if d.expectError { + assert.Error(err, fmt.Sprintf("got %q, test data: %+v", path, d)) + } else { + assert.NoError(err, fmt.Sprintf("got %q, test data: %+v", path, d)) + } + + assert.Equal(d.expectedResult, path) + } +} diff --git a/cli/pause.go b/cli/pause.go new file mode 100644 index 0000000000..807943f76c --- /dev/null +++ b/cli/pause.go @@ -0,0 +1,66 @@ +// Copyright (c) 2014,2015,2016 Docker, Inc. +// Copyright (c) 2017 Intel Corporation +// +// 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. + +package main + +import ( + "github.com/urfave/cli" +) + +var noteText = `Use "` + name + ` list" to identify container statuses.` + +var pauseCLICommand = cli.Command{ + Name: "pause", + Usage: "suspend all processes in a container", + ArgsUsage: ` + +Where "" is the container name to be paused.`, + Description: `The pause command suspends all processes in a container. + + ` + noteText, + Action: func(context *cli.Context) error { + return toggleContainerPause(context.Args().First(), true) + }, +} + +var resumeCLICommand = cli.Command{ + Name: "resume", + Usage: "unpause all previously paused processes in a container", + ArgsUsage: ` + +Where "" is the container name to be resumed.`, + Description: `The resume command unpauses all processes in a container. + + ` + noteText, + Action: func(context *cli.Context) error { + return toggleContainerPause(context.Args().First(), false) + }, +} + +func toggleContainerPause(containerID string, pause bool) (err error) { + // Checks the MUST and MUST NOT from OCI runtime specification + _, podID, err := getExistingContainerInfo(containerID) + if err != nil { + return err + } + + if pause { + _, err = vci.PausePod(podID) + } else { + _, err = vci.ResumePod(podID) + } + + return err +} diff --git a/cli/pause_test.go b/cli/pause_test.go new file mode 100644 index 0000000000..3fcdd4a6f0 --- /dev/null +++ b/cli/pause_test.go @@ -0,0 +1,154 @@ +// Copyright (c) 2017 Intel Corporation +// +// 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. + +package main + +import ( + "flag" + "testing" + + vc "github.com/kata-containers/runtime/virtcontainers" + "github.com/kata-containers/runtime/virtcontainers/pkg/vcMock" + "github.com/stretchr/testify/assert" +) + +var ( + testPausePodFuncReturnNil = func(podID string) (vc.VCPod, error) { + return &vcMock.Pod{}, nil + } + + testResumePodFuncReturnNil = func(podID string) (vc.VCPod, error) { + return &vcMock.Pod{}, nil + } +) + +func TestPauseCLIFunctionSuccessful(t *testing.T) { + assert := assert.New(t) + + state := vc.State{ + State: vc.StateRunning, + } + + testingImpl.PausePodFunc = testPausePodFuncReturnNil + testingImpl.ListPodFunc = func() ([]vc.PodStatus, error) { + return newSingleContainerPodStatusList(testPodID, testContainerID, state, state, map[string]string{}), nil + } + defer func() { + testingImpl.PausePodFunc = nil + testingImpl.ListPodFunc = nil + }() + + set := flag.NewFlagSet("", 0) + set.Parse([]string{testContainerID}) + + execCLICommandFunc(assert, pauseCLICommand, set, false) +} + +func TestPauseCLIFunctionContainerNotExistFailure(t *testing.T) { + assert := assert.New(t) + + testingImpl.PausePodFunc = testPausePodFuncReturnNil + testingImpl.ListPodFunc = func() ([]vc.PodStatus, error) { + return []vc.PodStatus{}, nil + } + defer func() { + testingImpl.PausePodFunc = nil + testingImpl.ListPodFunc = nil + }() + + set := flag.NewFlagSet("", 0) + set.Parse([]string{testContainerID}) + + execCLICommandFunc(assert, pauseCLICommand, set, true) +} + +func TestPauseCLIFunctionPausePodFailure(t *testing.T) { + assert := assert.New(t) + + state := vc.State{ + State: vc.StateRunning, + } + + testingImpl.ListPodFunc = func() ([]vc.PodStatus, error) { + return newSingleContainerPodStatusList(testPodID, testContainerID, state, state, map[string]string{}), nil + } + defer func() { + testingImpl.ListPodFunc = nil + }() + + set := flag.NewFlagSet("", 0) + set.Parse([]string{testContainerID}) + + execCLICommandFunc(assert, pauseCLICommand, set, true) +} + +func TestResumeCLIFunctionSuccessful(t *testing.T) { + assert := assert.New(t) + + state := vc.State{ + State: vc.StateRunning, + } + + testingImpl.ResumePodFunc = testResumePodFuncReturnNil + testingImpl.ListPodFunc = func() ([]vc.PodStatus, error) { + return newSingleContainerPodStatusList(testPodID, testContainerID, state, state, map[string]string{}), nil + } + defer func() { + testingImpl.ResumePodFunc = nil + testingImpl.ListPodFunc = nil + }() + + set := flag.NewFlagSet("", 0) + set.Parse([]string{testContainerID}) + + execCLICommandFunc(assert, resumeCLICommand, set, false) +} + +func TestResumeCLIFunctionContainerNotExistFailure(t *testing.T) { + assert := assert.New(t) + + testingImpl.ResumePodFunc = testResumePodFuncReturnNil + testingImpl.ListPodFunc = func() ([]vc.PodStatus, error) { + return []vc.PodStatus{}, nil + } + defer func() { + testingImpl.ResumePodFunc = nil + testingImpl.ListPodFunc = nil + }() + + set := flag.NewFlagSet("", 0) + set.Parse([]string{testContainerID}) + + execCLICommandFunc(assert, resumeCLICommand, set, true) +} + +func TestResumeCLIFunctionPausePodFailure(t *testing.T) { + assert := assert.New(t) + + state := vc.State{ + State: vc.StateRunning, + } + + testingImpl.ListPodFunc = func() ([]vc.PodStatus, error) { + return newSingleContainerPodStatusList(testPodID, testContainerID, state, state, map[string]string{}), nil + } + defer func() { + testingImpl.ListPodFunc = nil + }() + + set := flag.NewFlagSet("", 0) + set.Parse([]string{testContainerID}) + + execCLICommandFunc(assert, resumeCLICommand, set, true) +} diff --git a/cli/ps.go b/cli/ps.go new file mode 100644 index 0000000000..280f605c7e --- /dev/null +++ b/cli/ps.go @@ -0,0 +1,89 @@ +// Copyright (c) 2014,2015,2016 Docker, Inc. +// Copyright (c) 2017 Intel Corporation +// +// 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. + +package main + +import ( + "fmt" + + vc "github.com/kata-containers/runtime/virtcontainers" + "github.com/urfave/cli" +) + +var psCLICommand = cli.Command{ + Name: "ps", + Usage: "ps displays the processes running inside a container", + ArgsUsage: ` [ps options]`, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "format, f", + Value: "table", + Usage: `select one of: ` + formatOptions, + }, + }, + Action: func(context *cli.Context) error { + if context.Args().Present() == false { + return fmt.Errorf("Missing container ID, should at least provide one") + } + + var args []string + if len(context.Args()) > 1 { + // [1:] is to remove container_id: + // context.Args(): [container_id ps_arg1 ps_arg2 ...] + // args: [ps_arg1 ps_arg2 ...] + args = context.Args()[1:] + } + + return ps(context.Args().First(), context.String("format"), args) + }, + SkipArgReorder: true, +} + +func ps(containerID, format string, args []string) error { + if containerID == "" { + return fmt.Errorf("Missing container ID") + } + + // Checks the MUST and MUST NOT from OCI runtime specification + status, podID, err := getExistingContainerInfo(containerID) + if err != nil { + return err + } + + containerID = status.ID + + // container MUST be running + if status.State.State != vc.StateRunning { + return fmt.Errorf("Container %s is not running", containerID) + } + + var options vc.ProcessListOptions + + options.Args = args + if len(options.Args) == 0 { + options.Args = []string{"-ef"} + } + + options.Format = format + + msg, err := vci.ProcessListContainer(containerID, podID, options) + if err != nil { + return err + } + + fmt.Print(string(msg)) + + return nil +} diff --git a/cli/ps_test.go b/cli/ps_test.go new file mode 100644 index 0000000000..ca64b4f7c5 --- /dev/null +++ b/cli/ps_test.go @@ -0,0 +1,134 @@ +// Copyright (c) 2017 Intel Corporation +// +// 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. + +package main + +import ( + "flag" + "testing" + + vc "github.com/kata-containers/runtime/virtcontainers" + vcAnnotations "github.com/kata-containers/runtime/virtcontainers/pkg/annotations" + "github.com/kata-containers/runtime/virtcontainers/pkg/vcMock" + "github.com/stretchr/testify/assert" + "github.com/urfave/cli" +) + +func TestPSCLIAction(t *testing.T) { + assert := assert.New(t) + + flagSet := flag.NewFlagSet("flag", flag.ContinueOnError) + flagSet.Parse([]string{"runtime"}) + + // create a new fake context + ctx := cli.NewContext(&cli.App{Metadata: map[string]interface{}{}}, flagSet, nil) + + // get Action function + actionFunc, ok := psCLICommand.Action.(func(ctx *cli.Context) error) + assert.True(ok) + + err := actionFunc(ctx) + assert.Error(err, "Missing container ID") +} + +func TestPSFailure(t *testing.T) { + assert := assert.New(t) + + pod := &vcMock.Pod{ + MockID: testContainerID, + } + + pod.MockContainers = []*vcMock.Container{ + { + MockID: pod.ID(), + MockPod: pod, + }, + } + + testingImpl.ListPodFunc = func() ([]vc.PodStatus, error) { + // return a podStatus with the container status + return []vc.PodStatus{ + { + ID: pod.ID(), + ContainersStatus: []vc.ContainerStatus{ + { + ID: pod.ID(), + Annotations: map[string]string{ + vcAnnotations.ContainerTypeKey: string(vc.PodContainer), + }, + }, + }, + }, + }, nil + } + + defer func() { + testingImpl.ListPodFunc = nil + }() + + // inexistent container + err := ps("xyz123abc", "json", []string{"-ef"}) + assert.Error(err) + + // container is not running + err = ps(pod.ID(), "json", []string{"-ef"}) + assert.Error(err) +} + +func TestPSSuccessful(t *testing.T) { + assert := assert.New(t) + + pod := &vcMock.Pod{ + MockID: testContainerID, + } + + pod.MockContainers = []*vcMock.Container{ + { + MockID: pod.ID(), + MockPod: pod, + }, + } + + testingImpl.ListPodFunc = func() ([]vc.PodStatus, error) { + // return a podStatus with the container status + return []vc.PodStatus{ + { + ID: pod.ID(), + ContainersStatus: []vc.ContainerStatus{ + { + State: vc.State{ + State: vc.StateRunning, + }, + ID: pod.ID(), + Annotations: map[string]string{ + vcAnnotations.ContainerTypeKey: string(vc.PodContainer), + }, + }, + }, + }, + }, nil + } + + testingImpl.ProcessListContainerFunc = func(podID, containerID string, options vc.ProcessListOptions) (vc.ProcessList, error) { + return []byte("echo,sleep,grep"), nil + } + + defer func() { + testingImpl.ListPodFunc = nil + testingImpl.ProcessListContainerFunc = nil + }() + + err := ps(pod.ID(), "json", []string{}) + assert.NoError(err) +} diff --git a/cli/run.go b/cli/run.go new file mode 100644 index 0000000000..9792508138 --- /dev/null +++ b/cli/run.go @@ -0,0 +1,124 @@ +// Copyright (c) 2014,2015,2016 Docker, Inc. +// Copyright (c) 2017 Intel Corporation +// +// 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. + +package main + +import ( + "errors" + "fmt" + "os" + "syscall" + + "github.com/kata-containers/runtime/virtcontainers/pkg/oci" + "github.com/urfave/cli" +) + +var runCLICommand = cli.Command{ + Name: "run", + Usage: "create and run a container", + ArgsUsage: ` + + is your name for the instance of the container that you + are starting. The name you provide for the container instance must be unique + on your host.`, + Description: `The run command creates an instance of a container for a bundle. The bundle + is a directory with a specification file named "config.json" and a root + filesystem.`, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "bundle, b", + Value: "", + Usage: `path to the root of the bundle directory, defaults to the current directory`, + }, + cli.StringFlag{ + Name: "console", + Value: "", + Usage: "path to a pseudo terminal", + }, + cli.StringFlag{ + Name: "console-socket", + Value: "", + Usage: "path to an AF_UNIX socket which will receive a file descriptor referencing the master end of the console's pseudoterminal", + }, + cli.StringFlag{ + Name: "pid-file", + Value: "", + Usage: "specify the file to write the process id to", + }, + cli.BoolFlag{ + Name: "detach, d", + Usage: "detach from the container's process", + }, + }, + Action: func(context *cli.Context) error { + runtimeConfig, ok := context.App.Metadata["runtimeConfig"].(oci.RuntimeConfig) + if !ok { + return errors.New("invalid runtime config") + } + + return run(context.Args().First(), + context.String("bundle"), + context.String("console"), + context.String("console-socket"), + context.String("pid-file"), + context.Bool("detach"), + runtimeConfig) + }, +} + +func run(containerID, bundle, console, consoleSocket, pidFile string, detach bool, + runtimeConfig oci.RuntimeConfig) error { + + consolePath, err := setupConsole(console, consoleSocket) + if err != nil { + return err + } + + if err := create(containerID, bundle, consolePath, pidFile, detach, runtimeConfig); err != nil { + return err + } + + pod, err := start(containerID) + if err != nil { + return err + } + + if detach { + return nil + } + + containers := pod.GetAllContainers() + if len(containers) == 0 { + return fmt.Errorf("There are no containers running in the pod: %s", pod.ID()) + } + + p, err := os.FindProcess(containers[0].GetPid()) + if err != nil { + return err + } + + ps, err := p.Wait() + if err != nil { + return fmt.Errorf("Process state %s: %s", ps.String(), err) + } + + // delete container's resources + if err := delete(pod.ID(), true); err != nil { + return err + } + + //runtime should forward container exit code to the system + return cli.NewExitError("", ps.Sys().(syscall.WaitStatus).ExitStatus()) +} diff --git a/cli/run_test.go b/cli/run_test.go new file mode 100644 index 0000000000..c267e3d87d --- /dev/null +++ b/cli/run_test.go @@ -0,0 +1,655 @@ +// Copyright (c) 2017 Intel Corporation +// +// 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. + +package main + +import ( + "flag" + "fmt" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "testing" + + vc "github.com/kata-containers/runtime/virtcontainers" + vcAnnotations "github.com/kata-containers/runtime/virtcontainers/pkg/annotations" + "github.com/kata-containers/runtime/virtcontainers/pkg/oci" + "github.com/kata-containers/runtime/virtcontainers/pkg/vcMock" + "github.com/stretchr/testify/assert" + "github.com/urfave/cli" +) + +func TestRunCliAction(t *testing.T) { + assert := assert.New(t) + + flagSet := flag.NewFlagSet("flag", flag.ContinueOnError) + flagSet.Parse([]string{"runtime"}) + + // create a new fake context + ctx := cli.NewContext(&cli.App{Metadata: map[string]interface{}{}}, flagSet, nil) + + // get Action function + actionFunc, ok := runCLICommand.Action.(func(ctx *cli.Context) error) + assert.True(ok) + + err := actionFunc(ctx) + assert.Error(err, "missing runtime configuration") + + // temporal dir to place container files + tmpdir, err := ioutil.TempDir("", "") + assert.NoError(err) + defer os.RemoveAll(tmpdir) + + // create a new runtime config + runtimeConfig, err := newTestRuntimeConfig(tmpdir, "/dev/ptmx", true) + assert.NoError(err) + + ctx.App.Metadata = map[string]interface{}{ + "runtimeConfig": runtimeConfig, + } + + err = actionFunc(ctx) + assert.Error(err, "run without args") +} + +func TestRunInvalidArgs(t *testing.T) { + assert := assert.New(t) + + pod := &vcMock.Pod{ + MockID: testPodID, + MockContainers: []*vcMock.Container{ + {MockID: testContainerID}, + }, + } + + // fake functions used to run containers + testingImpl.CreatePodFunc = func(podConfig vc.PodConfig) (vc.VCPod, error) { + return pod, nil + } + + testingImpl.StartPodFunc = func(podID string) (vc.VCPod, error) { + return pod, nil + } + + testingImpl.ListPodFunc = func() ([]vc.PodStatus, error) { + return []vc.PodStatus{}, nil + } + + defer func() { + testingImpl.CreatePodFunc = nil + testingImpl.StartPodFunc = nil + testingImpl.ListPodFunc = nil + }() + + // temporal dir to place container files + tmpdir, err := ioutil.TempDir("", "") + assert.NoError(err) + defer os.RemoveAll(tmpdir) + + // create a new bundle + bundlePath := filepath.Join(tmpdir, "bundle") + + err = os.MkdirAll(bundlePath, testDirMode) + assert.NoError(err) + + err = makeOCIBundle(bundlePath) + assert.NoError(err) + + // pid file + pidFilePath := filepath.Join(tmpdir, "pid") + + // console file + consolePath := "/dev/ptmx" + + // inexistent path + inexistentPath := "/this/path/does/not/exist" + + runtimeConfig, err := newTestRuntimeConfig(tmpdir, consolePath, true) + assert.NoError(err) + + type testArgs struct { + containerID string + bundle string + console string + consoleSocket string + pidFile string + detach bool + runtimeConfig oci.RuntimeConfig + } + + args := []testArgs{ + {"", "", "", "", "", true, oci.RuntimeConfig{}}, + {"", "", "", "", "", false, oci.RuntimeConfig{}}, + {"", "", "", "", "", true, runtimeConfig}, + {"", "", "", "", "", false, runtimeConfig}, + {"", "", "", "", pidFilePath, false, runtimeConfig}, + {"", "", "", "", inexistentPath, false, runtimeConfig}, + {"", "", "", "", pidFilePath, false, runtimeConfig}, + {"", "", "", inexistentPath, pidFilePath, false, runtimeConfig}, + {"", "", inexistentPath, inexistentPath, pidFilePath, false, runtimeConfig}, + {"", "", inexistentPath, "", pidFilePath, false, runtimeConfig}, + {"", "", consolePath, "", pidFilePath, false, runtimeConfig}, + {"", bundlePath, consolePath, "", pidFilePath, false, runtimeConfig}, + {testContainerID, inexistentPath, consolePath, "", pidFilePath, false, oci.RuntimeConfig{}}, + {testContainerID, inexistentPath, consolePath, "", inexistentPath, false, oci.RuntimeConfig{}}, + {testContainerID, bundlePath, consolePath, "", pidFilePath, false, oci.RuntimeConfig{}}, + {testContainerID, inexistentPath, consolePath, "", pidFilePath, false, runtimeConfig}, + {testContainerID, inexistentPath, consolePath, "", inexistentPath, false, runtimeConfig}, + {testContainerID, bundlePath, consolePath, "", pidFilePath, false, runtimeConfig}, + } + + for i, a := range args { + err := run(a.containerID, a.bundle, a.console, a.consoleSocket, a.pidFile, a.detach, a.runtimeConfig) + assert.Errorf(err, "test %d (%+v)", i, a) + } +} + +type runContainerData struct { + pidFilePath string + consolePath string + bundlePath string + configJSON string + pod *vcMock.Pod + runtimeConfig oci.RuntimeConfig + process *os.Process + tmpDir string +} + +func testRunContainerSetup(t *testing.T) runContainerData { + assert := assert.New(t) + + // create a fake container workload + workload := []string{"/bin/sleep", "10"} + cmd := exec.Command(workload[0], workload[1:]...) + err := cmd.Start() + assert.NoError(err, "unable to start fake container workload %+v: %s", workload, err) + + // temporal dir to place container files + tmpdir, err := ioutil.TempDir("", "") + assert.NoError(err) + + // pid file + pidFilePath := filepath.Join(tmpdir, "pid") + + // console file + consolePath := "/dev/ptmx" + + // create a new bundle + bundlePath := filepath.Join(tmpdir, "bundle") + + err = makeOCIBundle(bundlePath) + assert.NoError(err) + + // config json path + configPath := filepath.Join(bundlePath, specConfig) + + // pod id and container id must be the same otherwise delete will not works + pod := &vcMock.Pod{ + MockID: testContainerID, + } + + pod.MockContainers = []*vcMock.Container{ + { + MockID: testContainerID, + MockPid: cmd.Process.Pid, + MockPod: pod, + }, + } + + // create a new runtime config + runtimeConfig, err := newTestRuntimeConfig(tmpdir, consolePath, true) + assert.NoError(err) + + configJSON, err := readOCIConfigJSON(configPath) + assert.NoError(err) + + return runContainerData{ + pidFilePath: pidFilePath, + consolePath: consolePath, + bundlePath: bundlePath, + configJSON: configJSON, + pod: pod, + runtimeConfig: runtimeConfig, + process: cmd.Process, + tmpDir: tmpdir, + } +} + +func TestRunContainerSuccessful(t *testing.T) { + assert := assert.New(t) + + d := testRunContainerSetup(t) + defer os.RemoveAll(d.tmpDir) + + // this flags is used to detect if createPodFunc was called + flagCreate := false + + // fake functions used to run containers + testingImpl.CreatePodFunc = func(podConfig vc.PodConfig) (vc.VCPod, error) { + flagCreate = true + return d.pod, nil + } + + testingImpl.StartPodFunc = func(podID string) (vc.VCPod, error) { + return d.pod, nil + } + + testingImpl.ListPodFunc = func() ([]vc.PodStatus, error) { + // return an empty list on create + if !flagCreate { + return []vc.PodStatus{}, nil + } + + // return a podStatus with the container status + return []vc.PodStatus{ + { + ID: d.pod.ID(), + ContainersStatus: []vc.ContainerStatus{ + { + ID: d.pod.ID(), + Annotations: map[string]string{ + vcAnnotations.ContainerTypeKey: string(vc.PodContainer), + vcAnnotations.ConfigJSONKey: d.configJSON, + }, + }, + }, + }, + }, nil + } + + testingImpl.StartContainerFunc = func(podID, containerID string) (vc.VCContainer, error) { + // now we can kill the fake container workload + err := d.process.Kill() + assert.NoError(err) + + return d.pod.MockContainers[0], nil + } + + testingImpl.DeletePodFunc = func(podID string) (vc.VCPod, error) { + return d.pod, nil + } + + testingImpl.DeleteContainerFunc = func(podID, containerID string) (vc.VCContainer, error) { + return d.pod.MockContainers[0], nil + } + + defer func() { + testingImpl.CreatePodFunc = nil + testingImpl.StartPodFunc = nil + testingImpl.ListPodFunc = nil + testingImpl.StartContainerFunc = nil + testingImpl.DeletePodFunc = nil + testingImpl.DeleteContainerFunc = nil + }() + + err := run(d.pod.ID(), d.bundlePath, d.consolePath, "", d.pidFilePath, false, d.runtimeConfig) + + // should return ExitError with the message and exit code + e, ok := err.(*cli.ExitError) + assert.True(ok, "error should be a cli.ExitError: %s", err) + assert.Empty(e.Error()) + assert.NotZero(e.ExitCode()) +} + +func TestRunContainerDetachSuccessful(t *testing.T) { + assert := assert.New(t) + + d := testRunContainerSetup(t) + defer os.RemoveAll(d.tmpDir) + + // this flags is used to detect if createPodFunc was called + flagCreate := false + + // fake functions used to run containers + testingImpl.CreatePodFunc = func(podConfig vc.PodConfig) (vc.VCPod, error) { + flagCreate = true + return d.pod, nil + } + + testingImpl.StartPodFunc = func(podID string) (vc.VCPod, error) { + return d.pod, nil + } + + testingImpl.ListPodFunc = func() ([]vc.PodStatus, error) { + // return an empty list on create + if !flagCreate { + return []vc.PodStatus{}, nil + } + + // return a podStatus with the container status + return []vc.PodStatus{ + { + ID: d.pod.ID(), + ContainersStatus: []vc.ContainerStatus{ + { + ID: d.pod.ID(), + Annotations: map[string]string{ + vcAnnotations.ContainerTypeKey: string(vc.PodContainer), + vcAnnotations.ConfigJSONKey: d.configJSON, + }, + }, + }, + }, + }, nil + } + + testingImpl.StartContainerFunc = func(podID, containerID string) (vc.VCContainer, error) { + // now we can kill the fake container workload + err := d.process.Kill() + assert.NoError(err) + + return d.pod.MockContainers[0], nil + } + + testingImpl.DeletePodFunc = func(podID string) (vc.VCPod, error) { + return d.pod, nil + } + + testingImpl.DeleteContainerFunc = func(podID, containerID string) (vc.VCContainer, error) { + return d.pod.MockContainers[0], nil + } + + defer func() { + testingImpl.CreatePodFunc = nil + testingImpl.StartPodFunc = nil + testingImpl.ListPodFunc = nil + testingImpl.StartContainerFunc = nil + testingImpl.DeletePodFunc = nil + testingImpl.DeleteContainerFunc = nil + }() + + err := run(d.pod.ID(), d.bundlePath, d.consolePath, "", d.pidFilePath, true, d.runtimeConfig) + + // should not return ExitError + assert.NoError(err) +} + +func TestRunContainerDeleteFail(t *testing.T) { + assert := assert.New(t) + + d := testRunContainerSetup(t) + defer os.RemoveAll(d.tmpDir) + + // this flags is used to detect if createPodFunc was called + flagCreate := false + + // fake functions used to run containers + testingImpl.CreatePodFunc = func(podConfig vc.PodConfig) (vc.VCPod, error) { + flagCreate = true + return d.pod, nil + } + + testingImpl.StartPodFunc = func(podID string) (vc.VCPod, error) { + return d.pod, nil + } + + testingImpl.ListPodFunc = func() ([]vc.PodStatus, error) { + // return an empty list on create + if !flagCreate { + return []vc.PodStatus{}, nil + } + + // return a podStatus with the container status + return []vc.PodStatus{ + { + ID: d.pod.ID(), + ContainersStatus: []vc.ContainerStatus{ + { + ID: d.pod.ID(), + Annotations: map[string]string{ + vcAnnotations.ContainerTypeKey: string(vc.PodContainer), + vcAnnotations.ConfigJSONKey: d.configJSON, + }, + }, + }, + }, + }, nil + } + + testingImpl.StartContainerFunc = func(podID, containerID string) (vc.VCContainer, error) { + // now we can kill the fake container workload + err := d.process.Kill() + assert.NoError(err) + + return d.pod.MockContainers[0], nil + } + + testingImpl.DeletePodFunc = func(podID string) (vc.VCPod, error) { + // return an error to provoke a failure in delete + return nil, fmt.Errorf("DeletePodFunc") + } + + testingImpl.DeleteContainerFunc = func(podID, containerID string) (vc.VCContainer, error) { + // return an error to provoke a failure in delete + return d.pod.MockContainers[0], fmt.Errorf("DeleteContainerFunc") + } + + defer func() { + testingImpl.CreatePodFunc = nil + testingImpl.StartPodFunc = nil + testingImpl.ListPodFunc = nil + testingImpl.StartContainerFunc = nil + testingImpl.DeletePodFunc = nil + testingImpl.DeleteContainerFunc = nil + }() + + err := run(d.pod.ID(), d.bundlePath, d.consolePath, "", d.pidFilePath, false, d.runtimeConfig) + + // should not return ExitError + err, ok := err.(*cli.ExitError) + assert.False(ok, "error should not be a cli.ExitError: %s", err) +} + +func TestRunContainerWaitFail(t *testing.T) { + assert := assert.New(t) + + d := testRunContainerSetup(t) + defer os.RemoveAll(d.tmpDir) + + // this flags is used to detect if createPodFunc was called + flagCreate := false + + // fake functions used to run containers + testingImpl.CreatePodFunc = func(podConfig vc.PodConfig) (vc.VCPod, error) { + flagCreate = true + return d.pod, nil + } + + testingImpl.StartPodFunc = func(podID string) (vc.VCPod, error) { + return d.pod, nil + } + + testingImpl.ListPodFunc = func() ([]vc.PodStatus, error) { + // return an empty list on create + if !flagCreate { + return []vc.PodStatus{}, nil + } + + // return a podStatus with the container status + return []vc.PodStatus{ + { + ID: d.pod.ID(), + ContainersStatus: []vc.ContainerStatus{ + { + ID: d.pod.ID(), + Annotations: map[string]string{ + vcAnnotations.ContainerTypeKey: string(vc.PodContainer), + vcAnnotations.ConfigJSONKey: d.configJSON, + }, + }, + }, + }, + }, nil + } + + testingImpl.StartContainerFunc = func(podID, containerID string) (vc.VCContainer, error) { + // now we can kill the fake container workload + err := d.process.Kill() + assert.NoError(err) + + // change PID to provoke a failure in Wait + d.pod.MockContainers[0].MockPid = -1 + + return d.pod.MockContainers[0], nil + } + + testingImpl.DeletePodFunc = func(podID string) (vc.VCPod, error) { + // return an error to provoke a failure in delete + return nil, fmt.Errorf("DeletePodFunc") + } + + testingImpl.DeleteContainerFunc = func(podID, containerID string) (vc.VCContainer, error) { + // return an error to provoke a failure in delete + return d.pod.MockContainers[0], fmt.Errorf("DeleteContainerFunc") + } + + defer func() { + testingImpl.CreatePodFunc = nil + testingImpl.StartPodFunc = nil + testingImpl.ListPodFunc = nil + testingImpl.StartContainerFunc = nil + testingImpl.DeletePodFunc = nil + testingImpl.DeleteContainerFunc = nil + }() + + err := run(d.pod.ID(), d.bundlePath, d.consolePath, "", d.pidFilePath, false, d.runtimeConfig) + + // should not return ExitError + err, ok := err.(*cli.ExitError) + assert.False(ok, "error should not be a cli.ExitError: %s", err) +} + +func TestRunContainerStartFail(t *testing.T) { + assert := assert.New(t) + + d := testRunContainerSetup(t) + defer os.RemoveAll(d.tmpDir) + + // now we can kill the fake container workload + err := d.process.Kill() + assert.NoError(err) + + // this flags is used to detect if createPodFunc was called + flagCreate := false + + // fake functions used to run containers + testingImpl.CreatePodFunc = func(podConfig vc.PodConfig) (vc.VCPod, error) { + flagCreate = true + return d.pod, nil + } + + testingImpl.StartPodFunc = func(podID string) (vc.VCPod, error) { + // start fails + return nil, fmt.Errorf("StartPod") + } + + testingImpl.ListPodFunc = func() ([]vc.PodStatus, error) { + // return an empty list on create + if !flagCreate { + return []vc.PodStatus{}, nil + } + + // return a podStatus with the container status + return []vc.PodStatus{ + { + ID: d.pod.ID(), + ContainersStatus: []vc.ContainerStatus{ + { + ID: d.pod.ID(), + Annotations: map[string]string{ + vcAnnotations.ContainerTypeKey: string(vc.PodContainer), + vcAnnotations.ConfigJSONKey: d.configJSON, + }, + }, + }, + }, + }, nil + } + + defer func() { + testingImpl.CreatePodFunc = nil + testingImpl.StartPodFunc = nil + testingImpl.ListPodFunc = nil + }() + + err = run(d.pod.ID(), d.bundlePath, d.consolePath, "", d.pidFilePath, false, d.runtimeConfig) + + // should not return ExitError + err, ok := err.(*cli.ExitError) + assert.False(ok, "error should not be a cli.ExitError: %s", err) +} + +func TestRunContainerStartFailNoContainers(t *testing.T) { + assert := assert.New(t) + + listCallCount := 0 + + d := testRunContainerSetup(t) + defer os.RemoveAll(d.tmpDir) + + pod := &vcMock.Pod{ + MockID: testPodID, + } + + pod.MockContainers = []*vcMock.Container{ + { + MockID: testContainerID, + MockPod: pod, + }, + } + + testingImpl.ListPodFunc = func() ([]vc.PodStatus, error) { + listCallCount++ + + if listCallCount == 1 { + return []vc.PodStatus{}, nil + } + + return []vc.PodStatus{ + { + ID: pod.ID(), + ContainersStatus: []vc.ContainerStatus{ + { + ID: testContainerID, + Annotations: map[string]string{ + vcAnnotations.ContainerTypeKey: string(vc.PodSandbox), + }, + }, + }, + }, + }, nil + } + + testingImpl.CreatePodFunc = func(podConfig vc.PodConfig) (vc.VCPod, error) { + return pod, nil + } + + testingImpl.StartPodFunc = func(podID string) (vc.VCPod, error) { + // force no containers + pod.MockContainers = nil + + return pod, nil + } + + defer func() { + testingImpl.ListPodFunc = nil + testingImpl.CreatePodFunc = nil + testingImpl.StartPodFunc = nil + }() + + err := run(d.pod.ID(), d.bundlePath, d.consolePath, "", d.pidFilePath, false, d.runtimeConfig) + assert.Error(err) + assert.False(vcMock.IsMockError(err)) +} diff --git a/cli/start.go b/cli/start.go new file mode 100644 index 0000000000..a80096107a --- /dev/null +++ b/cli/start.go @@ -0,0 +1,75 @@ +// Copyright (c) 2014,2015,2016 Docker, Inc. +// Copyright (c) 2017 Intel Corporation +// +// 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. + +package main + +import ( + "fmt" + + vc "github.com/kata-containers/runtime/virtcontainers" + "github.com/kata-containers/runtime/virtcontainers/pkg/oci" + "github.com/urfave/cli" +) + +var startCLICommand = cli.Command{ + Name: "start", + Usage: "executes the user defined process in a created container", + ArgsUsage: ` [container-id...] + + is your name for the instance of the container that you + are starting. The name you provide for the container instance must be + unique on your host.`, + Description: `The start command executes the user defined process in a created container .`, + Action: func(context *cli.Context) error { + args := context.Args() + if args.Present() == false { + return fmt.Errorf("Missing container ID, should at least provide one") + } + + for _, cID := range []string(args) { + if _, err := start(cID); err != nil { + return err + } + } + + return nil + }, +} + +func start(containerID string) (vc.VCPod, error) { + // Checks the MUST and MUST NOT from OCI runtime specification + status, podID, err := getExistingContainerInfo(containerID) + if err != nil { + return nil, err + } + + containerID = status.ID + + containerType, err := oci.GetContainerType(status.Annotations) + if err != nil { + return nil, err + } + + if containerType.IsPod() { + return vci.StartPod(podID) + } + + c, err := vci.StartContainer(podID, containerID) + if err != nil { + return nil, err + } + + return c.Pod(), nil +} diff --git a/cli/start_test.go b/cli/start_test.go new file mode 100644 index 0000000000..14756097d6 --- /dev/null +++ b/cli/start_test.go @@ -0,0 +1,254 @@ +// Copyright (c) 2017 Intel Corporation +// +// 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. + +package main + +import ( + "flag" + "testing" + + vc "github.com/kata-containers/runtime/virtcontainers" + vcAnnotations "github.com/kata-containers/runtime/virtcontainers/pkg/annotations" + "github.com/kata-containers/runtime/virtcontainers/pkg/vcMock" + "github.com/stretchr/testify/assert" + "github.com/urfave/cli" +) + +func TestStartInvalidArgs(t *testing.T) { + assert := assert.New(t) + + // Missing container id + _, err := start("") + assert.Error(err) + assert.False(vcMock.IsMockError(err)) + + // Mock Listpod error + _, err = start(testContainerID) + assert.Error(err) + assert.True(vcMock.IsMockError(err)) + + testingImpl.ListPodFunc = func() ([]vc.PodStatus, error) { + return []vc.PodStatus{}, nil + } + + defer func() { + testingImpl.ListPodFunc = nil + }() + + // Container missing in ListPod + _, err = start(testContainerID) + assert.Error(err) + assert.False(vcMock.IsMockError(err)) +} + +func TestStartPod(t *testing.T) { + assert := assert.New(t) + + pod := &vcMock.Pod{ + MockID: testPodID, + } + + testingImpl.ListPodFunc = func() ([]vc.PodStatus, error) { + return []vc.PodStatus{ + { + ID: pod.ID(), + ContainersStatus: []vc.ContainerStatus{ + { + ID: pod.ID(), + Annotations: map[string]string{ + vcAnnotations.ContainerTypeKey: string(vc.PodSandbox), + }, + }, + }, + }, + }, nil + } + + defer func() { + testingImpl.ListPodFunc = nil + }() + + _, err := start(pod.ID()) + assert.Error(err) + assert.True(vcMock.IsMockError(err)) + + testingImpl.StartPodFunc = func(podID string) (vc.VCPod, error) { + return pod, nil + } + + defer func() { + testingImpl.StartPodFunc = nil + }() + + _, err = start(pod.ID()) + assert.Nil(err) +} + +func TestStartMissingAnnotation(t *testing.T) { + assert := assert.New(t) + + pod := &vcMock.Pod{ + MockID: testPodID, + } + + testingImpl.ListPodFunc = func() ([]vc.PodStatus, error) { + return []vc.PodStatus{ + { + ID: pod.ID(), + ContainersStatus: []vc.ContainerStatus{ + { + ID: pod.ID(), + Annotations: map[string]string{}, + }, + }, + }, + }, nil + } + + defer func() { + testingImpl.ListPodFunc = nil + }() + + _, err := start(pod.ID()) + assert.Error(err) + assert.False(vcMock.IsMockError(err)) +} + +func TestStartContainerSucessFailure(t *testing.T) { + assert := assert.New(t) + + pod := &vcMock.Pod{ + MockID: testPodID, + } + + pod.MockContainers = []*vcMock.Container{ + { + MockID: testContainerID, + MockPod: pod, + }, + } + + testingImpl.ListPodFunc = func() ([]vc.PodStatus, error) { + return []vc.PodStatus{ + { + ID: pod.ID(), + ContainersStatus: []vc.ContainerStatus{ + { + ID: testContainerID, + Annotations: map[string]string{ + vcAnnotations.ContainerTypeKey: string(vc.PodContainer), + }, + }, + }, + }, + }, nil + } + + defer func() { + testingImpl.ListPodFunc = nil + }() + + _, err := start(testContainerID) + assert.Error(err) + assert.True(vcMock.IsMockError(err)) + + testingImpl.StartContainerFunc = func(podID, containerID string) (vc.VCContainer, error) { + return pod.MockContainers[0], nil + } + + defer func() { + testingImpl.StartContainerFunc = nil + }() + + _, err = start(testContainerID) + assert.Nil(err) +} + +func TestStartCLIFunction(t *testing.T) { + assert := assert.New(t) + + flagSet := &flag.FlagSet{} + app := cli.NewApp() + + ctx := cli.NewContext(app, flagSet, nil) + + fn, ok := startCLICommand.Action.(func(context *cli.Context) error) + assert.True(ok) + + // no container id in the Metadata + err := fn(ctx) + assert.Error(err) + assert.False(vcMock.IsMockError(err)) + + flagSet = flag.NewFlagSet("container-id", flag.ContinueOnError) + flagSet.Parse([]string{"xyz"}) + ctx = cli.NewContext(app, flagSet, nil) + + err = fn(ctx) + assert.Error(err) + assert.True(vcMock.IsMockError(err)) +} + +func TestStartCLIFunctionSuccess(t *testing.T) { + assert := assert.New(t) + + pod := &vcMock.Pod{ + MockID: testPodID, + } + + pod.MockContainers = []*vcMock.Container{ + { + MockID: testContainerID, + MockPod: pod, + }, + } + + testingImpl.ListPodFunc = func() ([]vc.PodStatus, error) { + return []vc.PodStatus{ + { + ID: pod.ID(), + ContainersStatus: []vc.ContainerStatus{ + { + ID: testContainerID, + Annotations: map[string]string{ + vcAnnotations.ContainerTypeKey: string(vc.PodContainer), + }, + }, + }, + }, + }, nil + } + + testingImpl.StartContainerFunc = func(podID, containerID string) (vc.VCContainer, error) { + return pod.MockContainers[0], nil + } + + defer func() { + testingImpl.ListPodFunc = nil + testingImpl.StartContainerFunc = nil + }() + + app := cli.NewApp() + + fn, ok := startCLICommand.Action.(func(context *cli.Context) error) + assert.True(ok) + + flagSet := flag.NewFlagSet("test", 0) + flagSet.Parse([]string{testContainerID}) + ctx := cli.NewContext(app, flagSet, nil) + assert.NotNil(ctx) + + err := fn(ctx) + assert.NoError(err) +} diff --git a/cli/state.go b/cli/state.go new file mode 100644 index 0000000000..e47dfd68a9 --- /dev/null +++ b/cli/state.go @@ -0,0 +1,64 @@ +// Copyright (c) 2014,2015,2016 Docker, Inc. +// Copyright (c) 2017 Intel Corporation +// +// 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. + +package main + +import ( + "encoding/json" + "fmt" + "os" + + "github.com/kata-containers/runtime/virtcontainers/pkg/oci" + "github.com/urfave/cli" +) + +var stateCLICommand = cli.Command{ + Name: "state", + Usage: "output the state of a container", + ArgsUsage: ` + + is your name for the instance of the container`, + Description: `The state command outputs current state information for the +instance of a container.`, + Action: func(context *cli.Context) error { + args := context.Args() + if len(args) != 1 { + return fmt.Errorf("Expecting only one container ID, got %d: %v", len(args), []string(args)) + } + + return state(args.First()) + }, +} + +func state(containerID string) error { + // Checks the MUST and MUST NOT from OCI runtime specification + status, _, err := getExistingContainerInfo(containerID) + if err != nil { + return err + } + + // Convert the status to the expected State structure + state := oci.StatusToOCIState(status) + + stateJSON, err := json.MarshalIndent(state, "", " ") + if err != nil { + return err + } + + // Print stateJSON to stdout + fmt.Fprintf(os.Stdout, "%s", stateJSON) + + return nil +} diff --git a/cli/state_test.go b/cli/state_test.go new file mode 100644 index 0000000000..f5a6fb4d55 --- /dev/null +++ b/cli/state_test.go @@ -0,0 +1,90 @@ +// Copyright (c) 2017 Intel Corporation +// +// 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. + +package main + +import ( + "flag" + "testing" + + vc "github.com/kata-containers/runtime/virtcontainers" + vcAnnotations "github.com/kata-containers/runtime/virtcontainers/pkg/annotations" + "github.com/kata-containers/runtime/virtcontainers/pkg/vcMock" + "github.com/stretchr/testify/assert" + "github.com/urfave/cli" +) + +func TestStateCliAction(t *testing.T) { + assert := assert.New(t) + + actionFunc, ok := stateCLICommand.Action.(func(ctx *cli.Context) error) + assert.True(ok) + + flagSet := flag.NewFlagSet("flag", flag.ContinueOnError) + + // without container id + flagSet.Parse([]string{"runtime"}) + ctx := cli.NewContext(&cli.App{}, flagSet, nil) + err := actionFunc(ctx) + assert.Error(err) + + // with container id + flagSet.Parse([]string{"runtime", testContainerID}) + ctx = cli.NewContext(&cli.App{}, flagSet, nil) + err = actionFunc(ctx) + assert.Error(err) +} + +func TestStateSuccessful(t *testing.T) { + assert := assert.New(t) + + pod := &vcMock.Pod{ + MockID: testContainerID, + } + + pod.MockContainers = []*vcMock.Container{ + { + MockID: pod.ID(), + MockPod: pod, + }, + } + + testingImpl.ListPodFunc = func() ([]vc.PodStatus, error) { + // return a podStatus with the container status + return []vc.PodStatus{ + { + ID: pod.ID(), + ContainersStatus: []vc.ContainerStatus{ + { + ID: pod.ID(), + Annotations: map[string]string{ + vcAnnotations.ContainerTypeKey: string(vc.PodContainer), + }, + }, + }, + }, + }, nil + } + + defer func() { + testingImpl.ListPodFunc = nil + }() + + // trying with an inexistent id + err := state("123456789") + assert.Error(err) + + err = state(pod.ID()) + assert.NoError(err) +} diff --git a/cli/utils.go b/cli/utils.go new file mode 100644 index 0000000000..766624a73b --- /dev/null +++ b/cli/utils.go @@ -0,0 +1,207 @@ +// Copyright (c) 2017 Intel Corporation +// +// 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. + +package main + +import ( + "bytes" + "fmt" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "strings" +) + +const unknown = "<>" + +// variables to allow tests to modify the values +var ( + procVersion = "/proc/version" + osRelease = "/etc/os-release" + + // Clear Linux has a different path (for stateless support) + osReleaseClr = "/usr/lib/os-release" +) + +func fileExists(path string) bool { + if _, err := os.Stat(path); os.IsNotExist(err) { + return false + } + + return true +} + +func getFileContents(file string) (string, error) { + bytes, err := ioutil.ReadFile(file) + if err != nil { + return "", err + } + + return string(bytes), nil +} + +func getKernelVersion() (string, error) { + contents, err := getFileContents(procVersion) + if err != nil { + return "", err + } + + fields := strings.Fields(contents) + + if len(fields) < 3 { + return "", fmt.Errorf("unexpected contents in %v", procVersion) + } + + version := fields[2] + + return version, nil +} + +// getDistroDetails returns the distributions name and version string. +// If it is not possible to determine both values an error is +// returned. +func getDistroDetails() (name, version string, err error) { + files := []string{osRelease, osReleaseClr} + + for _, file := range files { + contents, err := getFileContents(file) + if err != nil { + if os.IsNotExist(err) { + continue + } + + return "", "", err + } + + lines := strings.Split(contents, "\n") + + for _, line := range lines { + if strings.HasPrefix(line, "NAME=") { + fields := strings.Split(line, "=") + name = strings.Trim(fields[1], `"`) + } else if strings.HasPrefix(line, "VERSION_ID=") { + fields := strings.Split(line, "=") + version = strings.Trim(fields[1], `"`) + } + } + + if name != "" && version != "" { + return name, version, nil + } + } + + return "", "", fmt.Errorf("failed to find expected fields in one of %v", files) +} + +// getCPUDetails returns the vendor and model of the CPU. +// If it is not possible to determine both values an error is +// returned. +func getCPUDetails() (vendor, model string, err error) { + cpuinfo, err := getCPUInfo(procCPUInfo) + if err != nil { + return "", "", err + } + + lines := strings.Split(cpuinfo, "\n") + + for _, line := range lines { + if strings.HasPrefix(line, "vendor_id") { + fields := strings.Split(line, ":") + if len(fields) > 1 { + vendor = strings.TrimSpace(fields[1]) + } + } else if strings.HasPrefix(line, "model name") { + fields := strings.Split(line, ":") + if len(fields) > 1 { + model = strings.TrimSpace(fields[1]) + } + } + } + + if vendor != "" && model != "" { + return vendor, model, nil + } + + return "", "", fmt.Errorf("failed to find expected fields in file %v", procCPUInfo) +} + +// resolvePath returns the fully resolved and expanded value of the +// specified path. +func resolvePath(path string) (string, error) { + if path == "" { + return "", fmt.Errorf("path must be specified") + } + + absolute, err := filepath.Abs(path) + if err != nil { + return "", err + } + + resolved, err := filepath.EvalSymlinks(absolute) + if err != nil { + if os.IsNotExist(err) { + // Make the error clearer than the default + return "", fmt.Errorf("file %v does not exist", absolute) + } + + return "", err + } + + return resolved, nil +} + +// runCommandFull returns the commands space-trimmed standard output and +// error on success. Note that if the command fails, the requested output will +// still be returned, along with an error. +func runCommandFull(args []string, includeStderr bool) (string, error) { + cmd := exec.Command(args[0], args[1:]...) + var err error + var bytes []byte + + if includeStderr { + bytes, err = cmd.CombinedOutput() + } else { + bytes, err = cmd.Output() + } + + trimmed := strings.TrimSpace(string(bytes)) + + return trimmed, err +} + +// runCommand returns the commands space-trimmed standard output on success +func runCommand(args []string) (string, error) { + return runCommandFull(args, false) +} + +// writeFile write data into specified file +func writeFile(filePath string, data string, fileMode os.FileMode) error { + // Normally dir should not be empty, one case is that cgroup subsystem + // is not mounted, we will get empty dir, and we want it fail here. + if filePath == "" { + return fmt.Errorf("no such file for %s", filePath) + } + + if err := ioutil.WriteFile(filePath, []byte(data), fileMode); err != nil { + return fmt.Errorf("failed to write %v to %v: %v", data, filePath, err) + } + + return nil +} + +// isEmptyString return if string is empty +func isEmptyString(b []byte) bool { + return len(bytes.Trim(b, "\n")) == 0 +} diff --git a/cli/utils_test.go b/cli/utils_test.go new file mode 100644 index 0000000000..cee4eba2d7 --- /dev/null +++ b/cli/utils_test.go @@ -0,0 +1,422 @@ +// Copyright (c) 2017 Intel Corporation +// +// 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. + +package main + +import ( + "fmt" + "io/ioutil" + "os" + "path" + "path/filepath" + "syscall" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestFileExists(t *testing.T) { + dir, err := ioutil.TempDir(testDir, "") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + + file := filepath.Join(dir, "foo") + + assert.False(t, fileExists(file), + fmt.Sprintf("File %q should not exist", file)) + + err = createEmptyFile(file) + if err != nil { + t.Fatal(err) + } + + assert.True(t, fileExists(file), + fmt.Sprintf("File %q should exist", file)) +} + +func TestGetFileContents(t *testing.T) { + type testData struct { + contents string + } + + data := []testData{ + {""}, + {" "}, + {"\n"}, + {"\n\n"}, + {"\n\n\n"}, + {"foo"}, + {"foo\nbar"}, + {"processor : 0\nvendor_id : GenuineIntel\n"}, + } + + dir, err := ioutil.TempDir(testDir, "") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + + file := filepath.Join(dir, "foo") + + // file doesn't exist + _, err = getFileContents(file) + assert.Error(t, err) + + for _, d := range data { + // create the file + err = ioutil.WriteFile(file, []byte(d.contents), testFileMode) + if err != nil { + t.Fatal(err) + } + defer os.Remove(file) + + contents, err := getFileContents(file) + assert.NoError(t, err) + assert.Equal(t, contents, d.contents) + } +} + +func TestGetKernelVersion(t *testing.T) { + type testData struct { + contents string + expectedVersion string + expectError bool + } + + const validVersion = "1.2.3-4.5.x86_64" + validContents := fmt.Sprintf("Linux version %s blah blah blah ...", validVersion) + + data := []testData{ + {"", "", true}, + {"invalid contents", "", true}, + {"a b c", "c", false}, + {validContents, validVersion, false}, + } + + tmpdir, err := ioutil.TempDir("", "") + if err != nil { + panic(err) + } + defer os.RemoveAll(tmpdir) + + subDir := filepath.Join(tmpdir, "subdir") + err = os.MkdirAll(subDir, testDirMode) + assert.NoError(t, err) + + _, err = getKernelVersion() + assert.Error(t, err) + + file := filepath.Join(tmpdir, "proc-version") + + // override + procVersion = file + + _, err = getKernelVersion() + // ENOENT + assert.Error(t, err) + assert.True(t, os.IsNotExist(err)) + + for _, d := range data { + err := createFile(file, d.contents) + assert.NoError(t, err) + + version, err := getKernelVersion() + if d.expectError { + assert.Error(t, err, fmt.Sprintf("%+v", d)) + continue + } else { + assert.NoError(t, err, fmt.Sprintf("%+v", d)) + assert.Equal(t, d.expectedVersion, version) + } + } +} + +func TestGetDistroDetails(t *testing.T) { + type testData struct { + clrContents string + nonClrContents string + expectedName string + expectedVersion string + expectError bool + } + + tmpdir, err := ioutil.TempDir("", "") + if err != nil { + panic(err) + } + defer os.RemoveAll(tmpdir) + + testOSRelease := filepath.Join(tmpdir, "os-release") + testOSReleaseClr := filepath.Join(tmpdir, "os-release-clr") + + const clrExpectedName = "clr" + const clrExpectedVersion = "1.2.3-4" + clrContents := fmt.Sprintf(` +HELLO=world +NAME="%s" +FOO=bar +VERSION_ID="%s" +`, clrExpectedName, clrExpectedVersion) + + const nonClrExpectedName = "not-clr" + const nonClrExpectedVersion = "999" + nonClrContents := fmt.Sprintf(` +HELLO=world +NAME="%s" +FOO=bar +VERSION_ID="%s" +`, nonClrExpectedName, nonClrExpectedVersion) + + subDir := filepath.Join(tmpdir, "subdir") + err = os.MkdirAll(subDir, testDirMode) + assert.NoError(t, err) + + // override + osRelease = subDir + + _, _, err = getDistroDetails() + assert.Error(t, err) + + // override + osRelease = testOSRelease + osReleaseClr = testOSReleaseClr + + _, _, err = getDistroDetails() + // ENOENT + assert.Error(t, err) + + data := []testData{ + {"", "", "", "", true}, + {"invalid", "", "", "", true}, + {clrContents, "", clrExpectedName, clrExpectedVersion, false}, + {"", nonClrContents, nonClrExpectedName, nonClrExpectedVersion, false}, + {clrContents, nonClrContents, nonClrExpectedName, nonClrExpectedVersion, false}, + } + + for _, d := range data { + err := createFile(osRelease, d.nonClrContents) + assert.NoError(t, err) + + err = createFile(osReleaseClr, d.clrContents) + assert.NoError(t, err) + + name, version, err := getDistroDetails() + if d.expectError { + assert.Error(t, err, fmt.Sprintf("%+v", d)) + continue + } else { + assert.NoError(t, err, fmt.Sprintf("%+v", d)) + assert.Equal(t, d.expectedName, name) + assert.Equal(t, d.expectedVersion, version) + } + } +} + +func TestGetCPUDetails(t *testing.T) { + type testData struct { + contents string + expectedVendor string + expectedModel string + expectError bool + } + + const validVendorName = "a vendor" + validVendor := fmt.Sprintf(`vendor_id : %s`, validVendorName) + + const validModelName = "some CPU model" + validModel := fmt.Sprintf(`model name : %s`, validModelName) + + validContents := fmt.Sprintf(` +a : b +%s +foo : bar +%s +`, validVendor, validModel) + + data := []testData{ + {"", "", "", true}, + {"invalid", "", "", true}, + {"vendor_id", "", "", true}, + {validVendor, "", "", true}, + {validModel, "", "", true}, + {validContents, validVendorName, validModelName, false}, + } + + tmpdir, err := ioutil.TempDir("", "") + if err != nil { + panic(err) + } + defer os.RemoveAll(tmpdir) + + testProcCPUInfo := filepath.Join(tmpdir, "cpuinfo") + + // override + procCPUInfo = testProcCPUInfo + + _, _, err = getCPUDetails() + // ENOENT + assert.Error(t, err) + assert.True(t, os.IsNotExist(err)) + + for _, d := range data { + err := createFile(procCPUInfo, d.contents) + assert.NoError(t, err) + + vendor, model, err := getCPUDetails() + + if d.expectError { + assert.Error(t, err, fmt.Sprintf("%+v", d)) + continue + } else { + assert.NoError(t, err, fmt.Sprintf("%+v", d)) + assert.Equal(t, d.expectedVendor, vendor) + assert.Equal(t, d.expectedModel, model) + } + } +} + +func TestUtilsResolvePathEmptyPath(t *testing.T) { + _, err := resolvePath("") + assert.Error(t, err) +} + +func TestUtilsResolvePathValidPath(t *testing.T) { + dir, err := ioutil.TempDir("", "") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + + target := path.Join(dir, "target") + linkDir := path.Join(dir, "a/b/c") + linkFile := path.Join(linkDir, "link") + + err = createEmptyFile(target) + assert.NoError(t, err) + + absolute, err := filepath.Abs(target) + assert.NoError(t, err) + + resolvedTarget, err := filepath.EvalSymlinks(absolute) + assert.NoError(t, err) + + err = os.MkdirAll(linkDir, testDirMode) + assert.NoError(t, err) + + err = syscall.Symlink(target, linkFile) + assert.NoError(t, err) + + resolvedLink, err := resolvePath(linkFile) + assert.NoError(t, err) + + assert.Equal(t, resolvedTarget, resolvedLink) +} + +func TestUtilsResolvePathENOENT(t *testing.T) { + dir, err := ioutil.TempDir("", "") + if err != nil { + t.Fatal(err) + } + + target := path.Join(dir, "target") + linkDir := path.Join(dir, "a/b/c") + linkFile := path.Join(linkDir, "link") + + err = createEmptyFile(target) + assert.NoError(t, err) + + err = os.MkdirAll(linkDir, testDirMode) + assert.NoError(t, err) + + err = syscall.Symlink(target, linkFile) + assert.NoError(t, err) + + cwd, err := os.Getwd() + assert.NoError(t, err) + defer os.Chdir(cwd) + + err = os.Chdir(dir) + assert.NoError(t, err) + + err = os.RemoveAll(dir) + assert.NoError(t, err) + + _, err = resolvePath(filepath.Base(linkFile)) + assert.Error(t, err) +} + +func TestUtilsRunCommand(t *testing.T) { + output, err := runCommand([]string{"true"}) + assert.NoError(t, err) + assert.Equal(t, "", output) +} + +func TestUtilsRunCommandCaptureStdout(t *testing.T) { + output, err := runCommand([]string{"echo", "hello"}) + assert.NoError(t, err) + assert.Equal(t, "hello", output) +} + +func TestUtilsRunCommandIgnoreStderr(t *testing.T) { + args := []string{"/bin/sh", "-c", "echo foo >&2;exit 0"} + + output, err := runCommand(args) + assert.NoError(t, err) + assert.Equal(t, "", output) +} + +func TestUtilsRunCommandInvalidCmds(t *testing.T) { + invalidCommands := [][]string{ + {""}, + {"", ""}, + {" "}, + {" ", " "}, + {" ", ""}, + {"\\"}, + {"/"}, + {"/.."}, + {"../"}, + {"/tmp"}, + {"\t"}, + {"\n"}, + {"false"}, + } + + for _, args := range invalidCommands { + output, err := runCommand(args) + assert.Error(t, err) + assert.Equal(t, "", output) + } +} + +func TestWriteFileErrWriteFail(t *testing.T) { + assert := assert.New(t) + + err := writeFile("", "", 0000) + assert.Error(err) +} + +func TestWriteFileErrNoPath(t *testing.T) { + assert := assert.New(t) + + dir, err := ioutil.TempDir(testDir, "") + assert.NoError(err) + defer os.RemoveAll(dir) + + // attempt to write a file over an existing directory + err = writeFile(dir, "", 0000) + assert.Error(err) +} diff --git a/cli/version.go b/cli/version.go new file mode 100644 index 0000000000..0ccd600bce --- /dev/null +++ b/cli/version.go @@ -0,0 +1,28 @@ +// Copyright (c) 2017 Intel Corporation +// +// 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. + +package main + +import ( + "github.com/urfave/cli" +) + +var versionCLICommand = cli.Command{ + Name: "version", + Usage: "display version details", + Action: func(context *cli.Context) error { + cli.VersionPrinter(context) + return nil + }, +} diff --git a/cli/version_test.go b/cli/version_test.go new file mode 100644 index 0000000000..edcc37c94d --- /dev/null +++ b/cli/version_test.go @@ -0,0 +1,65 @@ +// +// Copyright (c) 2017 Intel Corporation +// +// 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. +// + +package main + +import ( + "fmt" + "io/ioutil" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/urfave/cli" +) + +func TestVersion(t *testing.T) { + const testAppName = "foo" + const testAppVersion = "0.1.0" + + resetCLIGlobals() + + savedRuntimeVersionFunc := runtimeVersion + + defer func() { + runtimeVersion = savedRuntimeVersionFunc + }() + + runtimeVersion := func() string { + return testAppVersion + } + + app := cli.NewApp() + ctx := cli.NewContext(app, nil, nil) + app.Name = testAppName + app.Version = runtimeVersion() + + fn, ok := versionCLICommand.Action.(func(context *cli.Context) error) + assert.True(t, ok) + + tmpfile, err := ioutil.TempFile("", "") + assert.NoError(t, err) + defer os.Remove(tmpfile.Name()) + + ctx.App.Writer = tmpfile + + err = fn(ctx) + assert.NoError(t, err) + + pattern := fmt.Sprintf("%s.*version.*%s", testAppName, testAppVersion) + err = grep(pattern, tmpfile.Name()) + assert.NoError(t, err) +}