cli: Add initial cli implementation.

- Add kata-runtime
- Add unit test
- Add Makefile to build cli

Fixes: #33

Signed-off-by: Julio Montes <julio.montes@intel.com>
Signed-off-by: James O. D. Hunt <james.o.hunt@intel.com>
Signed-off-by: Jose Carlos Venegas Munoz <jose.carlos.venegas.munoz@intel.com>
This commit is contained in:
Julio Montes 2018-03-14 11:25:41 -06:00 committed by Jose Carlos Venegas Munoz
parent 65b9936798
commit e84a9a70b0
53 changed files with 16033 additions and 0 deletions

23
.ci/go-no-os-exit.sh Executable file
View File

@ -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 '\<os\.Exit\>' $files; then
echo "Direct calls to os.Exit() are forbidden, please use exit() so atexit() works"
exit 1
fi

99
.ci/go-static-checks.sh Executable file
View File

@ -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!"

96
.ci/go-test.sh Executable file
View File

@ -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

8
cli/.gitignore vendored Normal file
View File

@ -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

584
cli/Makefile Normal file
View File

@ -0,0 +1,584 @@
# 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 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 <prev_head> <new_head> <file_or_branch_checkout>
# post-commit # no parameters
# post-merge <squash_or_not>
# post-rewrite <amend_or_rebase>
#
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

1
cli/VERSION Normal file
View File

@ -0,0 +1 @@
0.1.0

27
cli/arch/amd64-options.mk Normal file
View File

@ -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

21
cli/arch/arm64-options.mk Normal file
View File

@ -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

511
cli/config.go Normal file
View File

@ -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:
//
// [<component>.<type>]
//
// 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, ", "))
}

View File

@ -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@"

1054
cli/config_test.go Normal file

File diff suppressed because it is too large Load Diff

147
cli/console.go Normal file
View File

@ -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
}

131
cli/console_test.go Normal file
View File

@ -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)
}

382
cli/create.go Normal file
View File

@ -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: `<container-id>
<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 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
}

1197
cli/create_test.go Normal file

File diff suppressed because it is too large Load Diff

155
cli/delete.go Normal file
View File

@ -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> [container-id...]
<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
}

597
cli/delete_test.go Normal file
View File

@ -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)
}

273
cli/exec.go Normal file
View File

@ -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: `<container-id> <command> [command options] || -p process.json <container-id>
<container-id> is the name for the instance of the container and <command>
is the command to be executed in the container. <command> 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 + ` <container-id> 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: <uid>[:<gid>])",
},
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())
}

696
cli/exec_test.go Normal file
View File

@ -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")
}

37
cli/exit.go Normal file
View File

@ -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)
}

51
cli/exit_test.go Normal file
View File

@ -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)
}

80
cli/fatal.go Normal file
View File

@ -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)
}

304
cli/kata-check.go Normal file
View File

@ -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
},
}

132
cli/kata-check_amd64.go Normal file
View File

@ -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 <linux/kvm.h>
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
}

View File

@ -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)
}

View File

@ -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:
`

698
cli/kata-check_test.go Normal file
View File

@ -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))
}

365
cli/kata-env.go Normal file
View File

@ -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)
},
}

969
cli/kata-env_test.go Normal file
View File

@ -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)
}

159
cli/kill.go Normal file
View File

@ -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: `<container-id> [signal]
<container-id> 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)
}

280
cli/kill_test.go Normal file
View File

@ -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)
}

392
cli/list.go Normal file
View File

@ -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(),
},
}
}

754
cli/list_test.go Normal file
View File

@ -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)
}

79
cli/logger.go Normal file
View File

@ -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
}

163
cli/logger_test.go Normal file
View File

@ -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)
}

386
cli/main.go Normal file
View File

@ -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()
}

1115
cli/main_test.go Normal file

File diff suppressed because it is too large Load Diff

351
cli/oci.go Normal file
View File

@ -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
}

623
cli/oci_test.go Normal file
View File

@ -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)
}
}

66
cli/pause.go Normal file
View File

@ -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: `<container-id>
Where "<container-id>" 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: `<container-id>
Where "<container-id>" 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
}

154
cli/pause_test.go Normal file
View File

@ -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)
}

89
cli/ps.go Normal file
View File

@ -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: `<container-id> [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
}

134
cli/ps_test.go Normal file
View File

@ -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)
}

124
cli/run.go Normal file
View File

@ -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: `<container-id>
<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 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())
}

655
cli/run_test.go Normal file
View File

@ -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))
}

75
cli/start.go Normal file
View File

@ -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> [container-id...]
<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
}

254
cli/start_test.go Normal file
View File

@ -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)
}

64
cli/state.go Normal file
View File

@ -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: `<container-id>
<container-id> 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
}

90
cli/state_test.go Normal file
View File

@ -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)
}

207
cli/utils.go Normal file
View File

@ -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 = "<<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
}

422
cli/utils_test.go Normal file
View File

@ -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)
}

28
cli/version.go Normal file
View File

@ -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
},
}

65
cli/version_test.go Normal file
View File

@ -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)
}