virtcontainers: Initial import

This is a virtcontainers 1.0.8 import into Kata Containers runtime.

virtcontainers is a Go library designed to manage hardware virtualized
pods and containers. It is the core Clear Containers framework and will
become the core Kata Containers framework, as discussed at
https://github.com/kata-containers/runtime/issues/33

Some more more pointers:

virtcontainers README, including some design and architecure notes:
https://github.com/containers/virtcontainers/blob/master/README.md

virtcontainers 1.0 API:
https://github.com/containers/virtcontainers/blob/master/documentation/api/1.0/api.md

Fixes #40

Signed-off-by: Samuel Ortiz <sameo@linux.intel.com>
This commit is contained in:
Samuel Ortiz
2018-02-23 10:35:08 +01:00
parent ad9cef49c6
commit 24eff72d82
854 changed files with 441776 additions and 0 deletions

46
virtcontainers/.ci/go-lint.sh Executable file
View File

@@ -0,0 +1,46 @@
# 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.
#
#!/bin/bash
if [ ! $(command -v gometalinter) ]
then
go get github.com/alecthomas/gometalinter
gometalinter --install --vendor
fi
linter_args="--tests --vendor"
# When running the linters in a CI environment we need to disable them all
# by default and then explicitly enable the ones we are care about. This is
# necessary since *if* gometalinter adds a new linter, that linter may cause
# the CI build to fail when it really shouldn't. However, when this script is
# run locally, all linters should be run to allow the developer to review any
# failures (and potentially decide whether we need to explicitly enable a new
# linter in the CI).
if [ "$CI" = true ]; then
linter_args+=" --disable-all"
fi
linter_args+=" --enable=misspell"
linter_args+=" --enable=vet"
linter_args+=" --enable=ineffassign"
linter_args+=" --enable=gofmt"
linter_args+=" --enable=gocyclo"
linter_args+=" --cyclo-over=15"
linter_args+=" --enable=golint"
linter_args+=" --deadline=600s"
eval gometalinter "${linter_args}" ./...

31
virtcontainers/.ci/go-test.sh Executable file
View File

@@ -0,0 +1,31 @@
# 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.
#
#!/bin/bash
set -e
test_packages=$(go list ./... | grep -v vendor)
test_ldflags="-X github.com/containers/virtcontainers/pkg/mock.DefaultMockCCShimBinPath=$1 \
-X github.com/containers/virtcontainers/pkg/mock.DefaultMockKataShimBinPath=$2 \
-X github.com/containers/virtcontainers/pkg/mock.DefaultMockHookBinPath=$3"
echo "Run go test and generate coverage:"
for pkg in $test_packages; do
if [ "$pkg" = "github.com/containers/virtcontainers" ]; then
sudo env GOPATH=$GOPATH GOROOT=$GOROOT PATH=$PATH go test -ldflags "$test_ldflags" -cover -coverprofile=profile.cov $pkg
else
sudo env GOPATH=$GOPATH GOROOT=$GOROOT PATH=$PATH go test -ldflags "$test_ldflags" -cover $pkg
fi
done

View File

@@ -0,0 +1,55 @@
#!/bin/bash
#
# 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.
set -e
vc_repo="github.com/containers/virtcontainers"
# Export all environment variables needed.
export GOROOT="/usr/local/go"
export GOPATH=${HOME}/go
export PATH=${GOPATH}/bin:/usr/local/go/bin:/usr/sbin:${PATH}
export CI=true
# Download and build goveralls binary in case we need to submit the code
# coverage.
if [ ${COVERALLS_REPO_TOKEN} ]
then
go get github.com/mattn/goveralls
fi
# Get the repository and move HEAD to the appropriate commit.
go get ${vc_repo} || true
cd "${GOPATH}/src/${vc_repo}"
if [ "${ghprbPullId}" ] && [ "${ghprbTargetBranch}" ]
then
git fetch origin "pull/${ghprbPullId}/head" && git checkout master && git reset --hard FETCH_HEAD && git rebase "origin/${ghprbTargetBranch}"
export AUTHOR_REPO_GIT_URL="${ghprbAuthorRepoGitUrl}"
export COMMIT_REVISION="${ghprbActualCommit}"
else
git fetch origin && git checkout master && git reset --hard origin/master
fi
# Setup environment and run the tests
sudo -E PATH=$PATH bash .ci/setup.sh
sudo -E PATH=$PATH bash .ci/run.sh
# Publish the code coverage if needed.
if [ ${COVERALLS_REPO_TOKEN} ]
then
sudo -E PATH=${PATH} bash -c "${GOPATH}/bin/goveralls -repotoken=${COVERALLS_REPO_TOKEN} -coverprofile=profile.cov"
fi

33
virtcontainers/.ci/run.sh Executable file
View File

@@ -0,0 +1,33 @@
#!/bin/bash
#
# 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.
#
set -e
export CI=true
tests_repo="github.com/clearcontainers/tests"
make check
sudo -E PATH=$PATH sh -c "go test -bench=."
sudo -E PATH=$PATH sh -c "go test -bench=CreateStartStopDeletePodQemuHypervisorNoopAgentNetworkCNI -benchtime=60s"
sudo -E PATH=$PATH sh -c "go test -bench=CreateStartStopDeletePodQemuHypervisorHyperstartAgentNetworkCNI -benchtime=60s"
pushd "${GOPATH}/src/${tests_repo}"
.ci/run.sh
popd

41
virtcontainers/.ci/setup.sh Executable file
View File

@@ -0,0 +1,41 @@
#!/bin/bash
#
# 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.
#
set -e
cidir=$(dirname "$0")
tests_repo="github.com/clearcontainers/tests"
# Clone Tests repository.
go get "$tests_repo"
tests_repo_dir="${GOPATH}/src/${tests_repo}"
echo "Update proxy and runtime vendoring"
sudo -E PATH=$PATH bash -c "${cidir}/update-vendoring.sh"
pushd "${tests_repo_dir}"
echo "Setup Clear Containers"
sudo -E PATH=$PATH bash -c ".ci/setup.sh"
popd
echo "Setup virtcontainers environment"
chronic sudo -E PATH=$PATH bash -c "${cidir}/../utils/virtcontainers-setup.sh"
echo "Install virtcontainers"
chronic make
chronic sudo make install

View File

@@ -0,0 +1,89 @@
#!/bin/bash
#
# 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.
#
# This script is used to update the virtcontainers code in the vendor
# directories of the proxy and runtime repositories.
set -e
proxy_repo="github.com/clearcontainers/proxy"
runtime_repo="github.com/clearcontainers/runtime"
virtcontainers_repo="github.com/containers/virtcontainers"
function apply_depends_on(){
pushd "${GOPATH}/src/${virtcontainers_repo}"
label_lines=$(git log --format=%s%b -n 1 | grep "Depends-on:" || true)
if [ "${label_lines}" == "" ]; then
return 0
fi
nb_lines=$(echo ${label_lines} | wc -l)
for i in `seq 1 ${nb_lines}`
do
label_line=$(echo $label_lines | sed "${i}q;d")
label_str=$(echo "${label_line}" | cut -d' ' -f2)
repo=$(echo "${label_str}" | cut -d'#' -f1)
pr_id=$(echo "${label_str}" | cut -d'#' -f2)
if [ ! -d "${GOPATH}/src/${repo}" ]; then
go get -d "$repo" || true
fi
pushd "${GOPATH}/src/${repo}"
git fetch origin "pull/${pr_id}/head" && git checkout FETCH_HEAD && git rebase origin/master
popd
done
popd
}
function install_dep(){
go get -u github.com/golang/dep/cmd/dep
}
function update_repo(){
if [ "${AUTHOR_REPO_GIT_URL}" ] && [ "${COMMIT_REVISION}" ]
then
repo="$1"
if [ ! -d "${GOPATH}/src/${repo}" ]; then
go get -d "$repo" || true
fi
pushd "${GOPATH}/src/${repo}"
# Update Gopkg.toml
cat >> Gopkg.toml <<EOF
[[override]]
name = "${virtcontainers_repo}"
source = "${AUTHOR_REPO_GIT_URL}"
revision = "${COMMIT_REVISION}"
EOF
# Update the whole vendoring
dep ensure && dep ensure -update "${virtcontainers_repo}" && dep prune
popd
fi
}
apply_depends_on
install_dep
update_repo "${proxy_repo}"
update_repo "${runtime_repo}"

10
virtcontainers/.gitignore vendored Normal file
View File

@@ -0,0 +1,10 @@
*.patch
*.o
*.swp
/hack/virtc/virtc
/hook/mock/hook
/shim/mock/shim
/shim/mock/cc-shim/cc-shim
/shim/mock/kata-shim/kata-shim
/utils/supportfiles
profile.cov

View File

@@ -0,0 +1,52 @@
version: 2
requirements:
signed_off_by:
required: true
# Disallow approval of PRs still under development
always_pending:
title_regex: 'WIP'
labels:
- do-not-merge
- wip
explanation: 'Work in progress - do not merge'
group_defaults:
approve_by_comment:
enabled: true
approve_regex: '^(LGTM|lgtm|Approved|\+1|:\+1:)'
reject_regex: '^(Rejected|-1|:-1:)'
reset_on_push:
enabled: false
reset_on_reopened:
enabled: false
author_approval:
ignored: true
groups:
networking:
conditions:
files:
- pkg/cni/*
- cni*
- cnm*
required: 1
users:
- mcastelino
- sboeuf
approvers:
required: 1
users:
- sameo
- sboeuf
- amshinde
- jodh-intel
reviewers:
required: 1
teams:
- virtcontainers-maintainers
users:
- amshinde

View File

@@ -0,0 +1,74 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, gender identity and expression, level of experience,
nationality, personal appearance, race, religion, or sexual identity and
orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at
clear-containers-code-of-conduct@clearlinux.org. All complaints will be
reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct/
[homepage]: https://www.contributor-covenant.org

View File

@@ -0,0 +1,127 @@
# Contributing to virtcontainers
virtcontainers is an open source project licensed under the [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0).
## Coding Style (Go)
The usual Go style, enforced by `gofmt`, should be used. Additionally, the [Go
Code Review](https://github.com/golang/go/wiki/CodeReviewComments) document
contains a few common errors to be mindful of.
## Certificate of Origin
In order to get a clear contribution chain of trust we use the [signed-off-by language](https://01.org/community/signed-process)
used by the Linux kernel project.
## Patch format
Beside the signed-off-by footer, we expect each patch to comply with the following format:
```
Subsystem: Change summary (no longer than 75 characters)
More detailed explanation of your changes: Why and how.
Wrap it to 72 characters.
See:
http://chris.beams.io/posts/git-commit/
for some more good advice, and the Linux Kernel document:
https://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/tree/Documentation/SubmittingPatches
Signed-off-by: <contributor@foo.com>
```
For example:
```
pod: Remove token from Cmd structure
The token and pid data will be hold by the new Process structure and
they are related to a container.
Signed-off-by: Sebastien Boeuf <sebastien.boeuf@intel.com>
```
Note, that the body of the message should not just be a continuation of the subject line, and is not used to extend the subject line beyond its length limit. They should stand alone as complete sentence and paragraphs.
It is recommended that each of your patches fixes one thing. Smaller patches are easier to review, and are thus more likely to be accepted and merged, and problems are more likely to be picked up during review.
### Breaking compatibility
In case the patch you submit will break virtcontainers CI, because Clear Containers runtime compatibility is tested through the CI, you have to specify which repository and which pull request it depends on.
Using a simple tag `Depends-on:` in your commit message will allow virtcontainers CI to run properly. Notice that this tag is parsed from the latest commit of the pull request.
For example:
```
pod: Remove token from Cmd structure
The token and pid data will be hold by the new Process structure and
they are related to a container.
Depends-on: github.com/clearcontainers/runtime#75
Signed-off-by: Sebastien Boeuf <sebastien.boeuf@intel.com>
```
In this example, we want our CI scripts to fetch the pull request 75 of the runtime repository.
## Pull requests
We accept [github pull requests](https://github.com/containers/virtcontainers/pulls).
Github has a basic introduction to the process [here](https://help.github.com/articles/using-pull-requests/).
When submitting your Pull Request (PR), treat the Pull Request message the same you would a patch message, including pre-fixing the title with a subsystem name. Github by default seems to copy the message from your first patch, which many times is appropriate, but please ensure your message is accurate and complete for the whole Pull Request, as it ends up in the git log as the merge message.
Your pull request may get some feedback and comments, and require some rework. The recommended procedure for reworking is to rework your branch to a new clean state and 'force push' it to your github. GitHub understands this action, and does sensible things in the online comment history. Do not pile patches on patches to rework your branch. Any relevant information from the github comments section should be re-worked into your patch set, as the ultimate place where your patches are documented is in the git log, and not in the github comments section.
For more information on github 'force push' workflows see [here](http://blog.adamspiers.org/2015/03/24/why-and-how-to-correctly-amend-github-pull-requests/).
It is perfectly fine for your Pull Request to contain more than one patch - use as many patches as you need to implement the Request (see the previously mentioned 'small patch' thoughts). Each Pull Request should only cover one topic - if you mix up different items in your patches or pull requests then you will most likely be asked to rework them.
## Reviews
Before your Pull Requests are merged into the main code base, they will be reviewed. Anybody can review any Pull Request and leave feedback (in fact, it is encouraged).
We use an 'acknowledge' system for people to note if they agree, or disagree, with a Pull Request. We utilise some automated systems that can spot common acknowledge patterns, which include placing any of these at the beginning of a comment line:
- LGTM
- lgtm
- +1
- Approve
### Project maintainers
The virtcontainers maintainers will be the ones accepting or rejecting any pull request. They are listed in the OWNERS files, and there can be one OWNERS file per directory.
The OWNERS files split maintainership into 2 categories: reviewers and approvers. All approvers also belong to the reviewers list and there must be one approval from one member of each list for a pull request to be merged.
Since approvers are also reviewers, they technically can approve a pull request without getting another reviewer's approval. However, it is their due diligence to rely on reviewers and should use their approval power only in very specific cases.
## Issue tracking
To report a bug that is not already documented, please [open an
issue in github](https://github.com/containers/virtcontainers/issues/new) so we all get
visibility on the problem and work toward resolution.
## Closing issues
You can either close issues manually by adding the fixing commit SHA1 to the issue
comments or by adding the `Fixes` keyword to your commit message:
```
pod: Remove token from Cmd structure
The token and pid data will be hold by the new Process structure and
they are related to a container.
Fixes #123
Signed-off-by: Sebastien Boeuf <sebastien.boeuf@intel.com>
```
Github will then automatically close that issue when parsing the
[commit message](https://help.github.com/articles/closing-issues-via-commit-messages/).

226
virtcontainers/Gopkg.lock generated Normal file
View File

@@ -0,0 +1,226 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
name = "github.com/clearcontainers/proxy"
packages = [
"api",
"client"
]
revision = "1d2a6a3ea132a86abd0731408b7dc34f2fc17d55"
[[projects]]
name = "github.com/containerd/cri-containerd"
packages = ["pkg/annotations"]
revision = "3d382e2f5dabe3bae62ceb9ded56bdee847008ee"
[[projects]]
name = "github.com/containernetworking/cni"
packages = [
"libcni",
"pkg/invoke",
"pkg/types",
"pkg/types/020",
"pkg/types/current",
"pkg/version"
]
revision = "384d8c0b5288c25b9f1da901c66ea5155e6c567d"
[[projects]]
name = "github.com/containernetworking/plugins"
packages = ["pkg/ns"]
revision = "7f98c94613021d8b57acfa1a2f0c8d0f6fd7ae5a"
[[projects]]
name = "github.com/davecgh/go-spew"
packages = ["spew"]
revision = "346938d642f2ec3594ed81d874461961cd0faa76"
version = "v1.1.0"
[[projects]]
name = "github.com/go-ini/ini"
packages = ["."]
revision = "20b96f641a5ea98f2f8619ff4f3e061cff4833bd"
[[projects]]
name = "github.com/gogo/protobuf"
packages = [
"gogoproto",
"proto",
"protoc-gen-gogo/descriptor",
"sortkeys",
"types"
]
revision = "342cbe0a04158f6dcb03ca0079991a51a4248c02"
[[projects]]
name = "github.com/golang/protobuf"
packages = [
"proto",
"ptypes",
"ptypes/any",
"ptypes/duration",
"ptypes/timestamp"
]
revision = "925541529c1fa6821df4e44ce2723319eb2be768"
version = "v1.0.0"
[[projects]]
name = "github.com/intel/govmm"
packages = ["qemu"]
revision = "e87160f8ea39dfe558bf6761f74717d191d82493"
[[projects]]
name = "github.com/kata-containers/agent"
packages = [
"protocols/client",
"protocols/grpc"
]
revision = "33eecb2a445f906811a5bc9713d2dafd10768d18"
[[projects]]
name = "github.com/kubernetes-incubator/cri-o"
packages = ["pkg/annotations"]
revision = "3394b3b2d6af0e41d185bb695c6378be5dd4d61d"
[[projects]]
name = "github.com/mdlayher/vsock"
packages = ["."]
revision = "738c88d6e4cfd60e8124a5344fa10d205fd828b9"
[[projects]]
name = "github.com/mitchellh/mapstructure"
packages = ["."]
revision = "d0303fe809921458f417bcf828397a65db30a7e4"
[[projects]]
name = "github.com/opencontainers/runc"
packages = ["libcontainer/configs"]
revision = "0351df1c5a66838d0c392b4ac4cf9450de844e2d"
[[projects]]
name = "github.com/opencontainers/runtime-spec"
packages = ["specs-go"]
revision = "4e3b9264a330d094b0386c3703c5f379119711e8"
[[projects]]
name = "github.com/pmezard/go-difflib"
packages = ["difflib"]
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
version = "v1.0.0"
[[projects]]
name = "github.com/sirupsen/logrus"
packages = ["."]
revision = "89742aefa4b206dcf400792f3bd35b542998eb3b"
[[projects]]
name = "github.com/stretchr/testify"
packages = ["assert"]
revision = "890a5c3458b43e6104ff5da8dfa139d013d77544"
[[projects]]
name = "github.com/urfave/cli"
packages = ["."]
revision = "ac249472b7de27a9e8990819566d9be95ab5b816"
[[projects]]
name = "github.com/vishvananda/netlink"
packages = [
".",
"nl"
]
revision = "c2a3de3b38bd00f07290c3c5e12b4dbc04ec8666"
[[projects]]
name = "github.com/vishvananda/netns"
packages = ["."]
revision = "86bef332bfc3b59b7624a600bd53009ce91a9829"
[[projects]]
branch = "master"
name = "golang.org/x/crypto"
packages = ["ssh/terminal"]
revision = "91a49db82a88618983a78a06c1cbd4e00ab749ab"
[[projects]]
name = "golang.org/x/net"
packages = [
"context",
"http2",
"http2/hpack",
"idna",
"internal/timeseries",
"lex/httplex",
"trace"
]
revision = "a8b9294777976932365dabb6640cf1468d95c70f"
[[projects]]
name = "golang.org/x/sys"
packages = [
"unix",
"windows"
]
revision = "1d2aa6dbdea45adaaebb9905d0666e4537563829"
[[projects]]
name = "golang.org/x/text"
packages = [
"collate",
"collate/build",
"internal/colltab",
"internal/gen",
"internal/tag",
"internal/triegen",
"internal/ucd",
"language",
"secure/bidirule",
"transform",
"unicode/bidi",
"unicode/cldr",
"unicode/norm",
"unicode/rangetable"
]
revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0"
version = "v0.3.0"
[[projects]]
branch = "master"
name = "google.golang.org/genproto"
packages = ["googleapis/rpc/status"]
revision = "2c5e7ac708aaa719366570dd82bda44541ca2a63"
[[projects]]
name = "google.golang.org/grpc"
packages = [
".",
"balancer",
"balancer/roundrobin",
"codes",
"connectivity",
"credentials",
"encoding",
"grpclb/grpc_lb_v1/messages",
"grpclog",
"internal",
"keepalive",
"metadata",
"naming",
"peer",
"resolver",
"resolver/dns",
"resolver/passthrough",
"stats",
"status",
"tap",
"transport"
]
revision = "5a9f7b402fe85096d2e1d0383435ee1876e863d0"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "590b7d31900883c6782b596fa607426a77d71565516cfe8395379308a6285771"
solver-name = "gps-cdcl"
solver-version = 1

72
virtcontainers/Gopkg.toml Normal file
View File

@@ -0,0 +1,72 @@
[[constraint]]
name = "github.com/clearcontainers/proxy"
revision = "1d2a6a3ea132a86abd0731408b7dc34f2fc17d55"
[[constraint]]
name = "github.com/containernetworking/cni"
revision = "384d8c0b5288c25b9f1da901c66ea5155e6c567d"
[[constraint]]
name = "github.com/containernetworking/plugins"
revision = "7f98c94613021d8b57acfa1a2f0c8d0f6fd7ae5a"
[[constraint]]
name = "github.com/go-ini/ini"
revision = "20b96f641a5ea98f2f8619ff4f3e061cff4833bd"
[[constraint]]
name = "github.com/kubernetes-incubator/cri-o"
revision = "3394b3b2d6af0e41d185bb695c6378be5dd4d61d"
[[constraint]]
name = "github.com/mitchellh/mapstructure"
revision = "d0303fe809921458f417bcf828397a65db30a7e4"
[[constraint]]
name = "github.com/opencontainers/runc"
revision = "0351df1c5a66838d0c392b4ac4cf9450de844e2d"
[[constraint]]
name = "github.com/opencontainers/runtime-spec"
revision = "4e3b9264a330d094b0386c3703c5f379119711e8"
[[constraint]]
name = "github.com/stretchr/testify"
revision = "890a5c3458b43e6104ff5da8dfa139d013d77544"
[[constraint]]
name = "github.com/urfave/cli"
revision = "ac249472b7de27a9e8990819566d9be95ab5b816"
[[constraint]]
name = "github.com/vishvananda/netlink"
revision = "c2a3de3b38bd00f07290c3c5e12b4dbc04ec8666"
[[constraint]]
name = "github.com/vishvananda/netns"
revision = "86bef332bfc3b59b7624a600bd53009ce91a9829"
[[constraint]]
name = "golang.org/x/sys"
revision = "1d2aa6dbdea45adaaebb9905d0666e4537563829"
[[constraint]]
name = "github.com/sirupsen/logrus"
revision = "89742aefa4b206dcf400792f3bd35b542998eb3b"
[[constraint]]
name = "github.com/intel/govmm"
revision = "e87160f8ea39dfe558bf6761f74717d191d82493"
[[constraint]]
name = "github.com/kata-containers/agent"
revision = "33eecb2a445f906811a5bc9713d2dafd10768d18"
[[constraint]]
name = "github.com/containerd/cri-containerd"
revision = "3d382e2f5dabe3bae62ceb9ded56bdee847008ee"
[prune]
non-go = true
go-tests = true
unused-packages = true

201
virtcontainers/LICENSE Normal file
View File

@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

128
virtcontainers/Makefile Normal file
View File

@@ -0,0 +1,128 @@
PREFIX := /usr
BIN_DIR := $(PREFIX)/bin
VC_BIN_DIR := $(BIN_DIR)/virtcontainers/bin
TEST_BIN_DIR := $(VC_BIN_DIR)/test
VIRTC_DIR := hack/virtc
VIRTC_BIN := virtc
HOOK_DIR := hook/mock
HOOK_BIN := hook
CC_SHIM_DIR := shim/mock/cc-shim
CC_SHIM_BIN := cc-shim
KATA_SHIM_DIR := shim/mock/kata-shim
KATA_SHIM_BIN := kata-shim
#
# Pretty printing
#
V = @
Q = $(V:1=)
QUIET_GOBUILD = $(Q:@=@echo ' GOBUILD '$@;)
#
# Build
#
all: build binaries
build:
$(QUIET_GOBUILD)go build $(go list ./... | grep -v /vendor/)
virtc:
$(QUIET_GOBUILD)go build -o $(VIRTC_DIR)/$@ $(VIRTC_DIR)/*.go
hook:
$(QUIET_GOBUILD)go build -o $(HOOK_DIR)/$@ $(HOOK_DIR)/*.go
cc-shim:
$(QUIET_GOBUILD)go build -o $(CC_SHIM_DIR)/$@ $(CC_SHIM_DIR)/*.go
kata-shim:
$(QUIET_GOBUILD)go build -o $(KATA_SHIM_DIR)/$@ $(KATA_SHIM_DIR)/*.go
binaries: virtc hook cc-shim kata-shim
#
# Tests
#
check: check-go-static check-go-test
check-go-static:
bash .ci/go-lint.sh
check-go-test:
bash .ci/go-test.sh \
$(TEST_BIN_DIR)/$(CC_SHIM_BIN) \
$(TEST_BIN_DIR)/$(KATA_SHIM_BIN) \
$(TEST_BIN_DIR)/$(HOOK_BIN)
#
# Install
#
define INSTALL_EXEC
install -D $1 $(VC_BIN_DIR)/ || exit 1;
endef
define INSTALL_TEST_EXEC
install -D $1 $(TEST_BIN_DIR)/ || exit 1;
endef
install:
@mkdir -p $(VC_BIN_DIR)
$(call INSTALL_EXEC,$(VIRTC_DIR)/$(VIRTC_BIN))
@mkdir -p $(TEST_BIN_DIR)
$(call INSTALL_TEST_EXEC,$(HOOK_DIR)/$(HOOK_BIN))
$(call INSTALL_TEST_EXEC,$(CC_SHIM_DIR)/$(CC_SHIM_BIN))
$(call INSTALL_TEST_EXEC,$(KATA_SHIM_DIR)/$(KATA_SHIM_BIN))
#
# Uninstall
#
define UNINSTALL_EXEC
rm -f $(call FILE_SAFE_TO_REMOVE,$(VC_BIN_DIR)/$1) || exit 1;
endef
define UNINSTALL_TEST_EXEC
rm -f $(call FILE_SAFE_TO_REMOVE,$(TEST_BIN_DIR)/$1) || exit 1;
endef
uninstall:
$(call UNINSTALL_EXEC,$(VIRTC_BIN))
$(call UNINSTALL_TEST_EXEC,$(HOOK_BIN))
$(call UNINSTALL_TEST_EXEC,$(CC_SHIM_BIN))
$(call UNINSTALL_TEST_EXEC,$(KATA_SHIM_BIN))
#
# Clean
#
# Input: filename to check.
# Output: filename, assuming the file exists and is safe to delete.
define FILE_SAFE_TO_REMOVE =
$(shell test -e "$(1)" && test "$(1)" != "/" && echo "$(1)")
endef
CLEAN_FILES += $(VIRTC_DIR)/$(VIRTC_BIN)
CLEAN_FILES += $(HOOK_DIR)/$(HOOK_BIN)
CLEAN_FILES += $(SHIM_DIR)/$(CC_SHIM_BIN)
CLEAN_FILES += $(SHIM_DIR)/$(KATA_SHIM_BIN)
clean:
rm -f $(foreach f,$(CLEAN_FILES),$(call FILE_SAFE_TO_REMOVE,$(f)))
.PHONY: \
all \
build \
virtc \
hook \
shim \
binaries \
check \
check-go-static \
check-go-test \
install \
uninstall \
clean

138
virtcontainers/NEWS Normal file
View File

@@ -0,0 +1,138 @@
1.0.8:
Added support for CPU hotplug
Added SCSI support for drives and volumes
Added networking support for Kata Containers
Added block device hotplug support for Kata Containers
Added support for starting shim inside their own PID namespace
1.0.7:
Added ARM64 support
Added Kata Containers support
Added dockershim support
Added govmm support
Added support for the one proxy per VM model
Added 1.0 API documentation
Added hotplug support for QEMU's Q35 machine type
Fixed the agent, proxy and shim hierarchy
Fixed the state storage implementation
1.0.6:
Added SRIOV support
Added Multi-OS support
Removed Travis CI
1.0.5:
Added full CI (unit, functional and integration tests)
Added docker ps command support
Added multi-queue macvtap to the list of networking models
Added --device support for block devices (emulated)
Added pod shared locks for more efficient locking
Removed the ciao uuid package dependency
1.0.4:
Added shim debug output
Added support for custom qemu accelerators
Added structured logging
Added drive hotplug support for all containers
Fixed pod block index accounting
Fixed vendoring model
1.0.3:
Added initial device assignment framework
Added VFIO devices assignment initial support
Fixed pod locking
Fixed unconfigured interfaces handling
1.0.2:
Added James Hunt as maintainer
Added huge pages support
Added option for disabling nesting optimizations
Fixed the rootfs readonly support
1.0.1:
Added QEMU memory locking support
1.0.0:
Added support for memory and cpu reservation
Added stroage hotplug support
Added code of conduct
Added memory pre-allocation, realtime and memory locking support
Fixed nested virtualization support
Fixed log verbosity
Fixed pod cleanup
1.0.0-rc.4:
Added new project approvers
Fixed gometalinter
Increased unit test coverage
Added "pc" machine type for Qemu hypervisor
Fixed "kill" and "stop" container
1.0.0-rc.3:
Added support for assigning the container rootfs underlying block device to the VM (when using devicemapper)
Changed qemu to use virtio-net vhost by default
Changed qemu parameters to not be Clear Containers specific
Fixed MTU passing to hyperstart
1.0.0-rc.2:
Added support for transitioning from Ready to Stopped
Fixed pod container creation
Fixed CRI-O support for sandbox identifiers
1.0.0-rc.1:
Added standard error for invalid pod resources
Fixed docker gateway routing
1.0.0-rc.0:
Added pod pause and resume support
Added bind mounts support
Added CRI-O support
Added QEMU Q35 machine type support
Added readonly rootfs support
Added annotations passing to the OCI config file
Fixed hyperstart users and groups management
Fixed the installation and build scripts
Fixed networking MAC address configuration
0.7.0:
Added Clear Containers 3.0 shim support
Added Clear Containers 3.0 proxy protocol support
Added self contained hyperstart package
Added full build scripts
Added hypervisor and pod debug options
Added command line and function arguments validity checks
Added logrus support, replacing glog
Added OCI runtime vendoring
Added structured and separated error definitions
Fixed networking support and unit testing
Fixed code layering by keeping generic code out of interface implementations
Fixed benchmarks
0.6.0:
Added pullapprove support
Added Slack integration
Added KillContainer API
Added networking documentation
Added a Process structure for describing container processes
Added a more complete pod and container status structure
Fixed hyperstart implementation for stopping containers
0.5.0:
Changed the CreatePod/StartPod logic
Added an OCI config file conversion package
Added initial benchmarks
Added more CNI network unit tests
Moved all non virtcontainers packages under pkg/
Fixed netns race conditions
0.4.0:
Added CNM network model support
Added hyperstart asynchronous events support
Added low level CTL routines
Added OCI compatible hooks support
Added Travis code coverage report
0.3.0:
Added support for the latest hyperstart changes
Added proxy interface and cc-proxy implementation
Added CNI unit tests

7
virtcontainers/OWNERS Normal file
View File

@@ -0,0 +1,7 @@
reviewers:
- virtcontainers-maintainers
approvers:
- sameo
- sboeuf
- jodh-intel

354
virtcontainers/README.md Normal file
View File

@@ -0,0 +1,354 @@
[![Build Status](https://travis-ci.org/containers/virtcontainers.svg?branch=master)](https://travis-ci.org/containers/virtcontainers)
[![Build Status](http://cc-jenkins-ci.westus2.cloudapp.azure.com/job/virtcontainers-ubuntu-16-04-master/badge/icon)](http://cc-jenkins-ci.westus2.cloudapp.azure.com/job/virtcontainers-ubuntu-16-04-master)
[![Build Status](http://cc-jenkins-ci.westus2.cloudapp.azure.com/job/virtcontainers-ubuntu-17-04-master/badge/icon)](http://cc-jenkins-ci.westus2.cloudapp.azure.com/job/virtcontainers-ubuntu-17-04-master)
[![Build Status](http://cc-jenkins-ci.westus2.cloudapp.azure.com/job/virtcontainers-fedora-26-master/badge/icon)](http://cc-jenkins-ci.westus2.cloudapp.azure.com/job/virtcontainers-fedora-26-master)
[![Go Report Card](https://goreportcard.com/badge/github.com/containers/virtcontainers)](https://goreportcard.com/report/github.com/containers/virtcontainers)
[![Coverage Status](https://coveralls.io/repos/github/containers/virtcontainers/badge.svg?branch=master)](https://coveralls.io/github/containers/virtcontainers?branch=master)
[![GoDoc](https://godoc.org/github.com/containers/virtcontainers?status.svg)](https://godoc.org/github.com/containers/virtcontainers)
Table of Contents
=================
* [What is it ?](#what-is-it-)
* [Background](#background)
* [Out of scope](#out-of-scope)
* [virtcontainers and Kubernetes CRI](#virtcontainers-and-kubernetes-cri)
* [Design](#design)
* [Pods](#pods)
* [Hypervisors](#hypervisors)
* [Agents](#agents)
* [Shim](#shim)
* [Proxy](#proxy)
* [API](#api)
* [Pod API](#pod-api)
* [Container API](#container-api)
* [Networking](#networking)
* [CNM](#cnm)
* [CNI](#cni)
* [Storage](#storage)
* [How to check if container uses devicemapper block device as its rootfs](#how-to-check-if-container-uses-devicemapper-block-device-as-its-rootfs)
* [Devices](#devices)
* [How to pass a device using VFIO-passthrough](#how-to-pass-a-device-using-vfio-passthrough)
* [Developers](#developers)
# What is it ?
`virtcontainers` is a Go library that can be used to build hardware-virtualized container
runtimes.
# Background
The few existing VM-based container runtimes (Clear Containers, runv, rkt's
kvm stage 1) all share the same hardware virtualization semantics but use different
code bases to implement them. `virtcontainers`'s goal is to factorize this code into
a common Go library.
Ideally, VM-based container runtime implementations would become translation
layers from the runtime specification they implement (e.g. the [OCI runtime-spec][oci]
or the [Kubernetes CRI][cri]) to the `virtcontainers` API.
`virtcontainers` is [Clear Containers][cc]'s runtime foundational package for their
[runtime][cc-runtime] implementation
[oci]: https://github.com/opencontainers/runtime-spec
[cri]: https://github.com/kubernetes/kubernetes/blob/master/docs/proposals/container-runtime-interface-v1.md
[cc]: https://github.com/clearcontainers/
[cc-runtime]: https://github.com/clearcontainers/runtime/
# Out of scope
Implementing a container runtime is out of scope for this project. Any
tools or executables in this repository are only provided for demonstration or
testing purposes.
## virtcontainers and Kubernetes CRI
`virtcontainers`'s API is loosely inspired by the Kubernetes [CRI][cri] because
we believe it provides the right level of abstractions for containerized pods.
However, despite the API similarities between the two projects, the goal of
`virtcontainers` is _not_ to build a CRI implementation, but instead to provide a
generic, runtime-specification agnostic, hardware-virtualized containers
library that other projects could leverage to implement CRI themselves.
# Design
## Pods
The `virtcontainers` execution unit is a _pod_, i.e. `virtcontainers` users start pods where
containers will be running.
`virtcontainers` creates a pod by starting a virtual machine and setting the pod
up within that environment. Starting a pod means launching all containers with
the VM pod runtime environment.
## Hypervisors
The `virtcontainers` package relies on hypervisors to start and stop virtual machine where
pods will be running. An hypervisor is defined by an Hypervisor interface implementation,
and the default implementation is the QEMU one.
## Agents
During the lifecycle of a container, the runtime running on the host needs to interact with
the virtual machine guest OS in order to start new commands to be executed as part of a given
container workload, set new networking routes or interfaces, fetch a container standard or
error output, and so on.
There are many existing and potential solutions to resolve that problem and `virtcontainers` abstracts
this through the Agent interface.
## Shim
In some cases the runtime will need a translation shim between the higher level container
stack (e.g. Docker) and the virtual machine holding the container workload. This is needed
for container stacks that make strong assumptions on the nature of the container they're
monitoring. In cases where they assume containers are simply regular host processes, a shim
layer is needed to translate host specific semantics into e.g. agent controlled virtual
machine ones.
## Proxy
When hardware virtualized containers have limited I/O multiplexing capabilities,
runtimes may decide to rely on an external host proxy to support cases where several
runtime instances are talking to the same container.
# API
The high level `virtcontainers` API is the following one:
## Pod API
* `CreatePod(podConfig PodConfig)` creates a Pod.
The virtual machine is started and the Pod is prepared.
* `DeletePod(podID string)` deletes a Pod.
The virtual machine is shut down and all information related to the Pod are removed.
The function will fail if the Pod is running. In that case `StopPod()` has to be called first.
* `StartPod(podID string)` starts an already created Pod.
The Pod and all its containers are started.
* `RunPod(podConfig PodConfig)` creates and starts a Pod.
This performs `CreatePod()` + `StartPod()`.
* `StopPod(podID string)` stops an already running Pod.
The Pod and all its containers are stopped.
* `PausePod(podID string)` pauses an existing Pod.
* `ResumePod(podID string)` resume a paused Pod.
* `StatusPod(podID string)` returns a detailed Pod status.
* `ListPod()` lists all Pods on the host.
It returns a detailed status for every Pod.
## Container API
* `CreateContainer(podID string, containerConfig ContainerConfig)` creates a Container on an existing Pod.
* `DeleteContainer(podID, containerID string)` deletes a Container from a Pod.
If the Container is running it has to be stopped first.
* `StartContainer(podID, containerID string)` starts an already created Container.
The Pod has to be running.
* `StopContainer(podID, containerID string)` stops an already running Container.
* `EnterContainer(podID, containerID string, cmd Cmd)` enters an already running Container and runs a given command.
* `StatusContainer(podID, containerID string)` returns a detailed Container status.
* `KillContainer(podID, containerID string, signal syscall.Signal, all bool)` sends a signal to all or one container inside a Pod.
An example tool using the `virtcontainers` API is provided in the `hack/virtc` package.
# Networking
`virtcontainers` supports the 2 major container networking models: the [Container Network Model (CNM)][cnm] and the [Container Network Interface (CNI)][cni].
Typically the former is the Docker default networking model while the later is used on Kubernetes deployments.
`virtcontainers` callers can select one or the other, on a per pod basis, by setting their `PodConfig`'s `NetworkModel` field properly.
[cnm]: https://github.com/docker/libnetwork/blob/master/docs/design.md
[cni]: https://github.com/containernetworking/cni/
## CNM
![High-level CNM Diagram](documentation/network/CNM_overall_diagram.png)
__CNM lifecycle__
1. RequestPool
2. CreateNetwork
3. RequestAddress
4. CreateEndPoint
5. CreateContainer
6. Create config.json
7. Create PID and network namespace
8. ProcessExternalKey
9. JoinEndPoint
10. LaunchContainer
11. Launch
12. Run container
![Detailed CNM Diagram](documentation/network/CNM_detailed_diagram.png)
__Runtime network setup with CNM__
1. Read config.json
2. Create the network namespace ([code](https://github.com/containers/virtcontainers/blob/0.5.0/cnm.go#L108-L120))
3. Call the prestart hook (from inside the netns) ([code](https://github.com/containers/virtcontainers/blob/0.5.0/api.go#L46-L49))
4. Scan network interfaces inside netns and get the name of the interface created by prestart hook ([code](https://github.com/containers/virtcontainers/blob/0.5.0/cnm.go#L70-L106))
5. Create bridge, TAP, and link all together with network interface previously created ([code](https://github.com/containers/virtcontainers/blob/0.5.0/network.go#L123-L205))
6. Start VM inside the netns and start the container ([code](https://github.com/containers/virtcontainers/blob/0.5.0/api.go#L66-L70))
__Drawbacks of CNM__
There are three drawbacks about using CNM instead of CNI:
* The way we call into it is not very explicit: Have to re-exec dockerd binary so that it can accept parameters and execute the prestart hook related to network setup.
* Implicit way to designate the network namespace: Instead of explicitely giving the netns to dockerd, we give it the PID of our runtime so that it can find the netns from this PID. This means we have to make sure being in the right netns while calling the hook, otherwise the veth pair will be created with the wrong netns.
* No results are back from the hook: We have to scan the network interfaces to discover which one has been created inside the netns. This introduces more latency in the code because it forces us to scan the network in the CreatePod path, which is critical for starting the VM as quick as possible.
## CNI
![CNI Diagram](documentation/network/CNI_diagram.png)
__Runtime network setup with CNI__
1. Create the network namespace ([code](https://github.com/containers/virtcontainers/blob/0.5.0/cni.go#L64-L76))
2. Get CNI plugin information ([code](https://github.com/containers/virtcontainers/blob/0.5.0/cni.go#L29-L32))
3. Start the plugin (providing previously created netns) to add a network described into /etc/cni/net.d/ directory. At that time, the CNI plugin will create the cni0 network interface and a veth pair between the host and the created netns. It links cni0 to the veth pair before to exit. ([code](https://github.com/containers/virtcontainers/blob/0.5.0/cni.go#L34-L45))
4. Create bridge, TAP, and link all together with network interface previously created ([code](https://github.com/containers/virtcontainers/blob/0.5.0/network.go#L123-L205))
5. Start VM inside the netns and start the container ([code](https://github.com/containers/virtcontainers/blob/0.5.0/api.go#L66-L70))
# Storage
Container workloads are shared with the virtualized environment through 9pfs.
The devicemapper storage driver is a special case. The driver uses dedicated block devices rather than formatted filesystems, and operates at the block level rather than the file level. This knowledge has been used to directly use the underlying block device instead of the overlay file system for the container root file system. The block device maps to the top read-write layer for the overlay. This approach gives much better I/O performance compared to using 9pfs to share the container file system.
The approach above does introduce a limitation in terms of dynamic file copy in/out of the container via `docker cp` operations.
The copy operation from host to container accesses the mounted file system on the host side. This is not expected to work and may lead to inconsistencies as the block device will be simultaneously written to, from two different mounts.
The copy operation from container to host will work, provided the user calls `sync(1)` from within the container prior to the copy to make sure any outstanding cached data is written to the block device.
```
docker cp [OPTIONS] CONTAINER:SRC_PATH HOST:DEST_PATH
docker cp [OPTIONS] HOST:SRC_PATH CONTAINER:DEST_PATH
```
Ability to hotplug block devices has been added, which makes it possible to use block devices for containers started after the VM has been launched.
## How to check if container uses devicemapper block device as its rootfs
Start a container. Call mount(8) within the container. You should see '/' mounted on /dev/vda device.
# Devices
Support has been added to pass [VFIO](https://www.kernel.org/doc/Documentation/vfio.txt)
assigned devices on the docker command line with --device.
Support for passing other devices including block devices with --device has
not been added added yet.
## How to pass a device using VFIO-passthrough
1. Requirements
IOMMU group represents the smallest set of devices for which the IOMMU has
visibility and which is isolated from other groups. VFIO uses this information
to enforce safe ownership of devices for userspace.
You will need Intel VT-d capable hardware. Check if IOMMU is enabled in your host
kernel by verifying `CONFIG_VFIO_NOIOMMU` is not in the kernel config. If it is set,
you will need to rebuild your kernel.
The following kernel configs need to be enabled:
```
CONFIG_VFIO_IOMMU_TYPE1=m
CONFIG_VFIO=m
CONFIG_VFIO_PCI=m
```
In addition, you need to pass `intel_iommu=on` on the kernel command line.
2. Identify BDF(Bus-Device-Function) of the PCI device to be assigned.
```
$ lspci -D | grep -e Ethernet -e Network
0000:01:00.0 Ethernet controller: Intel Corporation Ethernet Controller 10-Gigabit X540-AT2 (rev 01)
$ BDF=0000:01:00.0
```
3. Find vendor and device id.
```
$ lspci -n -s $BDF
01:00.0 0200: 8086:1528 (rev 01)
```
4. Find IOMMU group.
```
$ readlink /sys/bus/pci/devices/$BDF/iommu_group
../../../../kernel/iommu_groups/16
```
5. Unbind the device from host driver.
```
$ echo $BDF | sudo tee /sys/bus/pci/devices/$BDF/driver/unbind
```
6. Bind the device to vfio-pci.
```
$ sudo modprobe vfio-pci
$ echo 8086 1528 | sudo tee /sys/bus/pci/drivers/vfio-pci/new_id
$ echo $BDF | sudo tee --append /sys/bus/pci/drivers/vfio-pci/bind
```
7. Check /dev/vfio
```
$ ls /dev/vfio
16 vfio
```
8. Start a Clear Containers container passing the VFIO group on the docker command line.
```
docker run -it --device=/dev/vfio/16 centos/tools bash
```
9. Running `lspci` within the container should show the device among the
PCI devices. The driver for the device needs to be present within the
Clear Containers kernel. If the driver is missing, you can add it to your
custom container kernel using the [osbuilder](https://github.com/clearcontainers/osbuilder)
tooling.
# Developers
For information on how to build, develop and test `virtcontainers`, see the
[developer documentation](documentation/Developers.md).

175
virtcontainers/agent.go Normal file
View File

@@ -0,0 +1,175 @@
//
// Copyright (c) 2016 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 virtcontainers
import (
"fmt"
"syscall"
"github.com/mitchellh/mapstructure"
)
// AgentType describes the type of guest agent a Pod should run.
type AgentType string
// ProcessListOptions contains the options used to list running
// processes inside the container
type ProcessListOptions struct {
// Format describes the output format to list the running processes.
// Formats are unrelated to ps(1) formats, only two formats can be specified:
// "json" and "table"
Format string
// Args contains the list of arguments to run ps(1) command.
// If Args is empty the agent will use "-ef" as options to ps(1).
Args []string
}
// ProcessList represents the list of running processes inside the container
type ProcessList []byte
const (
// NoopAgentType is the No-Op agent.
NoopAgentType AgentType = "noop"
// HyperstartAgent is the Hyper hyperstart agent.
HyperstartAgent AgentType = "hyperstart"
// KataContainersAgent is the Kata Containers agent.
KataContainersAgent AgentType = "kata"
// SocketTypeVSOCK is a VSOCK socket type for talking to an agent.
SocketTypeVSOCK = "vsock"
// SocketTypeUNIX is a UNIX socket type for talking to an agent.
// It typically means the agent is living behind a host proxy.
SocketTypeUNIX = "unix"
)
// Set sets an agent type based on the input string.
func (agentType *AgentType) Set(value string) error {
switch value {
case "noop":
*agentType = NoopAgentType
return nil
case "hyperstart":
*agentType = HyperstartAgent
return nil
case "kata":
*agentType = KataContainersAgent
return nil
default:
return fmt.Errorf("Unknown agent type %s", value)
}
}
// String converts an agent type to a string.
func (agentType *AgentType) String() string {
switch *agentType {
case NoopAgentType:
return string(NoopAgentType)
case HyperstartAgent:
return string(HyperstartAgent)
case KataContainersAgent:
return string(KataContainersAgent)
default:
return ""
}
}
// newAgent returns an agent from an agent type.
func newAgent(agentType AgentType) agent {
switch agentType {
case NoopAgentType:
return &noopAgent{}
case HyperstartAgent:
return &hyper{}
case KataContainersAgent:
return &kataAgent{}
default:
return &noopAgent{}
}
}
// newAgentConfig returns an agent config from a generic PodConfig interface.
func newAgentConfig(config PodConfig) interface{} {
switch config.AgentType {
case NoopAgentType:
return nil
case HyperstartAgent:
var hyperConfig HyperConfig
err := mapstructure.Decode(config.AgentConfig, &hyperConfig)
if err != nil {
return err
}
return hyperConfig
case KataContainersAgent:
var kataAgentConfig KataAgentConfig
err := mapstructure.Decode(config.AgentConfig, &kataAgentConfig)
if err != nil {
return err
}
return kataAgentConfig
default:
return nil
}
}
// agent is the virtcontainers agent interface.
// Agents are running in the guest VM and handling
// communications between the host and guest.
type agent interface {
// init is used to pass agent specific configuration to the agent implementation.
// agent implementations also will typically start listening for agent events from
// init().
// After init() is called, agent implementations should be initialized and ready
// to handle all other Agent interface methods.
init(pod *Pod, config interface{}) error
// capabilities should return a structure that specifies the capabilities
// supported by the agent.
capabilities() capabilities
// createPod will tell the agent to perform necessary setup for a Pod.
createPod(pod *Pod) error
// exec will tell the agent to run a command in an already running container.
exec(pod *Pod, c Container, cmd Cmd) (*Process, error)
// startPod will tell the agent to start all containers related to the Pod.
startPod(pod Pod) error
// stopPod will tell the agent to stop all containers related to the Pod.
stopPod(pod Pod) error
// createContainer will tell the agent to create a container related to a Pod.
createContainer(pod *Pod, c *Container) (*Process, error)
// startContainer will tell the agent to start a container related to a Pod.
startContainer(pod Pod, c *Container) error
// stopContainer will tell the agent to stop a container related to a Pod.
stopContainer(pod Pod, c Container) error
// killContainer will tell the agent to send a signal to a
// container related to a Pod. If all is true, all processes in
// the container will be sent the signal.
killContainer(pod Pod, c Container, signal syscall.Signal, all bool) error
// processListContainer will list the processes running inside the container
processListContainer(pod Pod, c Container, options ProcessListOptions) (ProcessList, error)
}

View File

@@ -0,0 +1,156 @@
//
// Copyright (c) 2016 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 virtcontainers
import (
"reflect"
"testing"
)
func testSetAgentType(t *testing.T, value string, expected AgentType) {
var agentType AgentType
err := (&agentType).Set(value)
if err != nil {
t.Fatal(err)
}
if agentType != expected {
t.Fatal(err)
}
}
func TestSetNoopAgentType(t *testing.T) {
testSetAgentType(t, "noop", NoopAgentType)
}
func TestSetHyperstartAgentType(t *testing.T) {
testSetAgentType(t, "hyperstart", HyperstartAgent)
}
func TestSetKataAgentType(t *testing.T) {
testSetAgentType(t, "kata", KataContainersAgent)
}
func TestSetUnknownAgentType(t *testing.T) {
var agentType AgentType
err := (&agentType).Set("unknown")
if err == nil {
t.Fatal()
}
if agentType == NoopAgentType ||
agentType == HyperstartAgent {
t.Fatal()
}
}
func testStringFromAgentType(t *testing.T, agentType AgentType, expected string) {
agentTypeStr := (&agentType).String()
if agentTypeStr != expected {
t.Fatal()
}
}
func TestStringFromNoopAgentType(t *testing.T) {
testStringFromAgentType(t, NoopAgentType, "noop")
}
func TestStringFromHyperstartAgentType(t *testing.T) {
testStringFromAgentType(t, HyperstartAgent, "hyperstart")
}
func TestStringFromKataAgentType(t *testing.T) {
testStringFromAgentType(t, KataContainersAgent, "kata")
}
func TestStringFromUnknownAgentType(t *testing.T) {
var agentType AgentType
testStringFromAgentType(t, agentType, "")
}
func testNewAgentFromAgentType(t *testing.T, agentType AgentType, expected agent) {
ag := newAgent(agentType)
if reflect.DeepEqual(ag, expected) == false {
t.Fatal()
}
}
func TestNewAgentFromNoopAgentType(t *testing.T) {
testNewAgentFromAgentType(t, NoopAgentType, &noopAgent{})
}
func TestNewAgentFromHyperstartAgentType(t *testing.T) {
testNewAgentFromAgentType(t, HyperstartAgent, &hyper{})
}
func TestNewAgentFromKataAgentType(t *testing.T) {
testNewAgentFromAgentType(t, KataContainersAgent, &kataAgent{})
}
func TestNewAgentFromUnknownAgentType(t *testing.T) {
var agentType AgentType
testNewAgentFromAgentType(t, agentType, &noopAgent{})
}
func testNewAgentConfig(t *testing.T, config PodConfig, expected interface{}) {
agentConfig := newAgentConfig(config)
if reflect.DeepEqual(agentConfig, expected) == false {
t.Fatal()
}
}
func TestNewAgentConfigFromNoopAgentType(t *testing.T) {
var agentConfig interface{}
podConfig := PodConfig{
AgentType: NoopAgentType,
AgentConfig: agentConfig,
}
testNewAgentConfig(t, podConfig, agentConfig)
}
func TestNewAgentConfigFromHyperstartAgentType(t *testing.T) {
agentConfig := HyperConfig{}
podConfig := PodConfig{
AgentType: HyperstartAgent,
AgentConfig: agentConfig,
}
testNewAgentConfig(t, podConfig, agentConfig)
}
func TestNewAgentConfigFromKataAgentType(t *testing.T) {
agentConfig := KataAgentConfig{}
podConfig := PodConfig{
AgentType: KataContainersAgent,
AgentConfig: agentConfig,
}
testNewAgentConfig(t, podConfig, agentConfig)
}
func TestNewAgentConfigFromUnknownAgentType(t *testing.T) {
var agentConfig interface{}
testNewAgentConfig(t, PodConfig{}, agentConfig)
}

655
virtcontainers/api.go Normal file
View File

@@ -0,0 +1,655 @@
//
// Copyright (c) 2016 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 virtcontainers
import (
"os"
"runtime"
"syscall"
"github.com/sirupsen/logrus"
)
func init() {
runtime.LockOSThread()
}
var virtLog = logrus.FieldLogger(logrus.New())
// SetLogger sets the logger for virtcontainers package.
func SetLogger(logger logrus.FieldLogger) {
fields := logrus.Fields{
"source": "virtcontainers",
"arch": runtime.GOARCH,
}
virtLog = logger.WithFields(fields)
}
// CreatePod is the virtcontainers pod creation entry point.
// CreatePod creates a pod and its containers. It does not start them.
func CreatePod(podConfig PodConfig) (VCPod, error) {
return createPodFromConfig(podConfig)
}
func createPodFromConfig(podConfig PodConfig) (*Pod, error) {
// Create the pod.
p, err := createPod(podConfig)
if err != nil {
return nil, err
}
// Create the pod network
if err := p.createNetwork(); err != nil {
return nil, err
}
// Start the VM
if err := p.startVM(); err != nil {
return nil, err
}
// Create Containers
if err := p.createContainers(); err != nil {
return nil, err
}
// The pod is completely created now, we can store it.
if err := p.storePod(); err != nil {
return nil, err
}
return p, nil
}
// DeletePod is the virtcontainers pod deletion entry point.
// DeletePod will stop an already running container and then delete it.
func DeletePod(podID string) (VCPod, error) {
if podID == "" {
return nil, errNeedPodID
}
lockFile, err := rwLockPod(podID)
if err != nil {
return nil, err
}
defer unlockPod(lockFile)
// Fetch the pod from storage and create it.
p, err := fetchPod(podID)
if err != nil {
return nil, err
}
// Delete it.
if err := p.delete(); err != nil {
return nil, err
}
return p, nil
}
// StartPod is the virtcontainers pod starting entry point.
// StartPod will talk to the given hypervisor to start an existing
// pod and all its containers.
// It returns the pod ID.
func StartPod(podID string) (VCPod, error) {
if podID == "" {
return nil, errNeedPodID
}
lockFile, err := rwLockPod(podID)
if err != nil {
return nil, err
}
defer unlockPod(lockFile)
// Fetch the pod from storage and create it.
p, err := fetchPod(podID)
if err != nil {
return nil, err
}
return startPod(p)
}
func startPod(p *Pod) (*Pod, error) {
// Start it
err := p.start()
if err != nil {
return nil, err
}
// Execute poststart hooks.
if err := p.config.Hooks.postStartHooks(); err != nil {
return nil, err
}
return p, nil
}
// StopPod is the virtcontainers pod stopping entry point.
// StopPod will talk to the given agent to stop an existing pod and destroy all containers within that pod.
func StopPod(podID string) (VCPod, error) {
if podID == "" {
return nil, errNeedPod
}
lockFile, err := rwLockPod(podID)
if err != nil {
return nil, err
}
defer unlockPod(lockFile)
// Fetch the pod from storage and create it.
p, err := fetchPod(podID)
if err != nil {
return nil, err
}
// Stop it.
err = p.stop()
if err != nil {
p.delete()
return nil, err
}
// Remove the network.
if err := p.removeNetwork(); err != nil {
return nil, err
}
// Execute poststop hooks.
if err := p.config.Hooks.postStopHooks(); err != nil {
return nil, err
}
return p, nil
}
// RunPod is the virtcontainers pod running entry point.
// RunPod creates a pod and its containers and then it starts them.
func RunPod(podConfig PodConfig) (VCPod, error) {
p, err := createPodFromConfig(podConfig)
if err != nil {
return nil, err
}
lockFile, err := rwLockPod(p.id)
if err != nil {
return nil, err
}
defer unlockPod(lockFile)
return startPod(p)
}
// ListPod is the virtcontainers pod listing entry point.
func ListPod() ([]PodStatus, error) {
dir, err := os.Open(configStoragePath)
if err != nil {
if os.IsNotExist(err) {
// No pod directory is not an error
return []PodStatus{}, nil
}
return []PodStatus{}, err
}
defer dir.Close()
podsID, err := dir.Readdirnames(0)
if err != nil {
return []PodStatus{}, err
}
var podStatusList []PodStatus
for _, podID := range podsID {
podStatus, err := StatusPod(podID)
if err != nil {
continue
}
podStatusList = append(podStatusList, podStatus)
}
return podStatusList, nil
}
// StatusPod is the virtcontainers pod status entry point.
func StatusPod(podID string) (PodStatus, error) {
if podID == "" {
return PodStatus{}, errNeedPodID
}
lockFile, err := rLockPod(podID)
if err != nil {
return PodStatus{}, err
}
pod, err := fetchPod(podID)
if err != nil {
unlockPod(lockFile)
return PodStatus{}, err
}
// We need to potentially wait for a separate container.stop() routine
// that needs to be terminated before we return from this function.
// Deferring the synchronization here is very important since we want
// to avoid a deadlock. Indeed, the goroutine started by statusContainer
// will need to lock an exclusive lock, meaning that all other locks have
// to be released to let this happen. This call ensures this will be the
// last operation executed by this function.
defer pod.wg.Wait()
defer unlockPod(lockFile)
var contStatusList []ContainerStatus
for _, container := range pod.containers {
contStatus, err := statusContainer(pod, container.id)
if err != nil {
return PodStatus{}, err
}
contStatusList = append(contStatusList, contStatus)
}
podStatus := PodStatus{
ID: pod.id,
State: pod.state,
Hypervisor: pod.config.HypervisorType,
HypervisorConfig: pod.config.HypervisorConfig,
Agent: pod.config.AgentType,
ContainersStatus: contStatusList,
Annotations: pod.config.Annotations,
}
return podStatus, nil
}
// CreateContainer is the virtcontainers container creation entry point.
// CreateContainer creates a container on a given pod.
func CreateContainer(podID string, containerConfig ContainerConfig) (VCPod, VCContainer, error) {
if podID == "" {
return nil, nil, errNeedPodID
}
lockFile, err := rwLockPod(podID)
if err != nil {
return nil, nil, err
}
defer unlockPod(lockFile)
p, err := fetchPod(podID)
if err != nil {
return nil, nil, err
}
// Create the container.
c, err := createContainer(p, containerConfig)
if err != nil {
return nil, nil, err
}
// Add the container to the containers list in the pod.
if err := p.addContainer(c); err != nil {
return nil, nil, err
}
// Store it.
err = c.storeContainer()
if err != nil {
return nil, nil, err
}
// Update pod config.
p.config.Containers = append(p.config.Containers, containerConfig)
err = p.storage.storePodResource(podID, configFileType, *(p.config))
if err != nil {
return nil, nil, err
}
return p, c, nil
}
// DeleteContainer is the virtcontainers container deletion entry point.
// DeleteContainer deletes a Container from a Pod. If the container is running,
// it needs to be stopped first.
func DeleteContainer(podID, containerID string) (VCContainer, error) {
if podID == "" {
return nil, errNeedPodID
}
if containerID == "" {
return nil, errNeedContainerID
}
lockFile, err := rwLockPod(podID)
if err != nil {
return nil, err
}
defer unlockPod(lockFile)
p, err := fetchPod(podID)
if err != nil {
return nil, err
}
// Fetch the container.
c, err := p.findContainer(containerID)
if err != nil {
return nil, err
}
// Delete it.
err = c.delete()
if err != nil {
return nil, err
}
// Update pod config
for idx, contConfig := range p.config.Containers {
if contConfig.ID == containerID {
p.config.Containers = append(p.config.Containers[:idx], p.config.Containers[idx+1:]...)
break
}
}
err = p.storage.storePodResource(podID, configFileType, *(p.config))
if err != nil {
return nil, err
}
return c, nil
}
// StartContainer is the virtcontainers container starting entry point.
// StartContainer starts an already created container.
func StartContainer(podID, containerID string) (VCContainer, error) {
if podID == "" {
return nil, errNeedPodID
}
if containerID == "" {
return nil, errNeedContainerID
}
lockFile, err := rwLockPod(podID)
if err != nil {
return nil, err
}
defer unlockPod(lockFile)
p, err := fetchPod(podID)
if err != nil {
return nil, err
}
// Fetch the container.
c, err := p.findContainer(containerID)
if err != nil {
return nil, err
}
// Start it.
err = c.start()
if err != nil {
c.delete()
return nil, err
}
return c, nil
}
// StopContainer is the virtcontainers container stopping entry point.
// StopContainer stops an already running container.
func StopContainer(podID, containerID string) (VCContainer, error) {
if podID == "" {
return nil, errNeedPodID
}
if containerID == "" {
return nil, errNeedContainerID
}
lockFile, err := rwLockPod(podID)
if err != nil {
return nil, err
}
defer unlockPod(lockFile)
p, err := fetchPod(podID)
if err != nil {
return nil, err
}
// Fetch the container.
c, err := p.findContainer(containerID)
if err != nil {
return nil, err
}
// Stop it.
err = c.stop()
if err != nil {
c.delete()
return nil, err
}
return c, nil
}
// EnterContainer is the virtcontainers container command execution entry point.
// EnterContainer enters an already running container and runs a given command.
func EnterContainer(podID, containerID string, cmd Cmd) (VCPod, VCContainer, *Process, error) {
if podID == "" {
return nil, nil, nil, errNeedPodID
}
if containerID == "" {
return nil, nil, nil, errNeedContainerID
}
lockFile, err := rLockPod(podID)
if err != nil {
return nil, nil, nil, err
}
defer unlockPod(lockFile)
p, err := fetchPod(podID)
if err != nil {
return nil, nil, nil, err
}
// Fetch the container.
c, err := p.findContainer(containerID)
if err != nil {
return nil, nil, nil, err
}
// Enter it.
process, err := c.enter(cmd)
if err != nil {
return nil, nil, nil, err
}
return p, c, process, nil
}
// StatusContainer is the virtcontainers container status entry point.
// StatusContainer returns a detailed container status.
func StatusContainer(podID, containerID string) (ContainerStatus, error) {
if podID == "" {
return ContainerStatus{}, errNeedPodID
}
if containerID == "" {
return ContainerStatus{}, errNeedContainerID
}
lockFile, err := rLockPod(podID)
if err != nil {
return ContainerStatus{}, err
}
pod, err := fetchPod(podID)
if err != nil {
unlockPod(lockFile)
return ContainerStatus{}, err
}
// We need to potentially wait for a separate container.stop() routine
// that needs to be terminated before we return from this function.
// Deferring the synchronization here is very important since we want
// to avoid a deadlock. Indeed, the goroutine started by statusContainer
// will need to lock an exclusive lock, meaning that all other locks have
// to be released to let this happen. This call ensures this will be the
// last operation executed by this function.
defer pod.wg.Wait()
defer unlockPod(lockFile)
return statusContainer(pod, containerID)
}
// This function is going to spawn a goroutine and it needs to be waited for
// by the caller.
func statusContainer(pod *Pod, containerID string) (ContainerStatus, error) {
for _, container := range pod.containers {
if container.id == containerID {
// We have to check for the process state to make sure
// we update the status in case the process is supposed
// to be running but has been killed or terminated.
if (container.state.State == StateReady ||
container.state.State == StateRunning ||
container.state.State == StatePaused) &&
container.process.Pid > 0 {
running, err := isShimRunning(container.process.Pid)
if err != nil {
return ContainerStatus{}, err
}
if !running {
pod.wg.Add(1)
go func() {
defer pod.wg.Done()
lockFile, err := rwLockPod(pod.id)
if err != nil {
return
}
defer unlockPod(lockFile)
if err := container.stop(); err != nil {
return
}
}()
}
}
return ContainerStatus{
ID: container.id,
State: container.state,
PID: container.process.Pid,
StartTime: container.process.StartTime,
RootFs: container.config.RootFs,
Annotations: container.config.Annotations,
}, nil
}
}
// No matching containers in the pod
return ContainerStatus{}, nil
}
// KillContainer is the virtcontainers entry point to send a signal
// to a container running inside a pod. If all is true, all processes in
// the container will be sent the signal.
func KillContainer(podID, containerID string, signal syscall.Signal, all bool) error {
if podID == "" {
return errNeedPodID
}
if containerID == "" {
return errNeedContainerID
}
lockFile, err := rwLockPod(podID)
if err != nil {
return err
}
defer unlockPod(lockFile)
p, err := fetchPod(podID)
if err != nil {
return err
}
// Fetch the container.
c, err := p.findContainer(containerID)
if err != nil {
return err
}
// Send a signal to the process.
err = c.kill(signal, all)
if err != nil {
return err
}
return nil
}
// PausePod is the virtcontainers pausing entry point which pauses an
// already running pod.
func PausePod(podID string) (VCPod, error) {
return togglePausePod(podID, true)
}
// ResumePod is the virtcontainers resuming entry point which resumes
// (or unpauses) and already paused pod.
func ResumePod(podID string) (VCPod, error) {
return togglePausePod(podID, false)
}
// ProcessListContainer is the virtcontainers entry point to list
// processes running inside a container
func ProcessListContainer(podID, containerID string, options ProcessListOptions) (ProcessList, error) {
if podID == "" {
return nil, errNeedPodID
}
if containerID == "" {
return nil, errNeedContainerID
}
lockFile, err := rLockPod(podID)
if err != nil {
return nil, err
}
defer unlockPod(lockFile)
p, err := fetchPod(podID)
if err != nil {
return nil, err
}
// Fetch the container.
c, err := p.findContainer(containerID)
if err != nil {
return nil, err
}
return c.processList(options)
}

2231
virtcontainers/api_test.go Normal file

File diff suppressed because it is too large Load Diff

158
virtcontainers/asset.go Normal file
View File

@@ -0,0 +1,158 @@
//
// 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 virtcontainers
import (
"crypto/sha512"
"encoding/hex"
"fmt"
"io/ioutil"
"path/filepath"
"github.com/containers/virtcontainers/pkg/annotations"
)
type assetType string
func (t assetType) annotations() (string, string, error) {
switch t {
case kernelAsset:
return annotations.KernelPath, annotations.KernelHash, nil
case imageAsset:
return annotations.ImagePath, annotations.ImageHash, nil
case hypervisorAsset:
return annotations.HypervisorPath, annotations.HypervisorHash, nil
case firmwareAsset:
return annotations.FirmwarePath, annotations.FirmwareHash, nil
}
return "", "", fmt.Errorf("Wrong asset type %s", t)
}
const (
kernelAsset assetType = "kernel"
imageAsset assetType = "image"
hypervisorAsset assetType = "hypervisor"
firmwareAsset assetType = "firmware"
)
type asset struct {
path string
computedHash string
kind assetType
}
func (a *asset) valid() bool {
if !filepath.IsAbs(a.path) {
return false
}
switch a.kind {
case kernelAsset:
return true
case imageAsset:
return true
case hypervisorAsset:
return true
case firmwareAsset:
return true
}
return false
}
// hash returns the hex encoded string for the asset hash
func (a *asset) hash(hashType string) (string, error) {
var hashEncodedLen int
var hash string
// We read the actual asset content
bytes, err := ioutil.ReadFile(a.path)
if err != nil {
return "", err
}
if len(bytes) == 0 {
return "", fmt.Errorf("Empty asset file at %s", a.path)
}
// Build the asset hash and convert it to a string.
// We only support SHA512 for now.
switch hashType {
case annotations.SHA512:
virtLog.Debugf("Computing %v hash", a.path)
hashComputed := sha512.Sum512(bytes)
hashEncodedLen = hex.EncodedLen(len(hashComputed))
hashEncoded := make([]byte, hashEncodedLen)
hex.Encode(hashEncoded, hashComputed[:])
hash = string(hashEncoded[:])
virtLog.Debugf("%v hash: %s", a.path, hash)
default:
return "", fmt.Errorf("Invalid hash type %s", hashType)
}
a.computedHash = hash
return hash, nil
}
// newAsset returns a new asset from the pod annotations.
func newAsset(podConfig *PodConfig, t assetType) (*asset, error) {
pathAnnotation, hashAnnotation, err := t.annotations()
if err != nil {
return nil, err
}
if pathAnnotation == "" || hashAnnotation == "" {
return nil, fmt.Errorf("Missing annotation paths for %s", t)
}
path, ok := podConfig.Annotations[pathAnnotation]
if !ok || path == "" {
return nil, nil
}
if !filepath.IsAbs(path) {
return nil, fmt.Errorf("%s is not an absolute path", path)
}
a := &asset{path: path, kind: t}
hash, ok := podConfig.Annotations[hashAnnotation]
if !ok || hash == "" {
return a, nil
}
// We have a hash annotation, we need to verify the asset against it.
hashType, ok := podConfig.Annotations[annotations.AssetHashType]
if !ok {
virtLog.Warningf("Unrecognized hash type: %s, switching to %s", hashType, annotations.SHA512)
hashType = annotations.SHA512
}
hashComputed, err := a.hash(hashType)
if err != nil {
return a, err
}
// If our computed asset hash does not match the passed annotation, we must exit.
if hashComputed != hash {
return nil, fmt.Errorf("Invalid hash for %s: computed %s, expecting %s]", a.path, hashComputed, hash)
}
return a, nil
}

View File

@@ -0,0 +1,117 @@
//
// 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 virtcontainers
import (
"io/ioutil"
"os"
"testing"
"github.com/containers/virtcontainers/pkg/annotations"
"github.com/stretchr/testify/assert"
)
var assetContent = []byte("FakeAsset fake asset FAKE ASSET")
var assetContentHash = "92549f8d2018a95a294d28a65e795ed7d1a9d150009a28cea108ae10101178676f04ab82a6950d0099e4924f9c5e41dcba8ece56b75fc8b4e0a7492cb2a8c880"
var assetContentWrongHash = "92549f8d2018a95a294d28a65e795ed7d1a9d150009a28cea108ae10101178676f04ab82a6950d0099e4924f9c5e41dcba8ece56b75fc8b4e0a7492cb2a8c881"
func TestAssetWrongHashType(t *testing.T) {
assert := assert.New(t)
tmpfile, err := ioutil.TempFile("", "virtcontainers-test-")
assert.Nil(err)
defer func() {
tmpfile.Close()
os.Remove(tmpfile.Name()) // clean up
}()
_, err = tmpfile.Write(assetContent)
assert.Nil(err)
a := &asset{
path: tmpfile.Name(),
}
h, err := a.hash("shafoo")
assert.Equal(h, "")
assert.NotNil(err)
}
func TestAssetHash(t *testing.T) {
assert := assert.New(t)
tmpfile, err := ioutil.TempFile("", "virtcontainers-test-")
assert.Nil(err)
defer func() {
tmpfile.Close()
os.Remove(tmpfile.Name()) // clean up
}()
_, err = tmpfile.Write(assetContent)
assert.Nil(err)
a := &asset{
path: tmpfile.Name(),
}
hash, err := a.hash(annotations.SHA512)
assert.Nil(err)
assert.Equal(assetContentHash, hash)
assert.Equal(assetContentHash, a.computedHash)
}
func TestAssetNew(t *testing.T) {
assert := assert.New(t)
tmpfile, err := ioutil.TempFile("", "virtcontainers-test-")
assert.Nil(err)
defer func() {
tmpfile.Close()
os.Remove(tmpfile.Name()) // clean up
}()
_, err = tmpfile.Write(assetContent)
assert.Nil(err)
p := &PodConfig{
Annotations: map[string]string{
annotations.KernelPath: tmpfile.Name(),
annotations.KernelHash: assetContentHash,
},
}
a, err := newAsset(p, imageAsset)
assert.Nil(err)
assert.Nil(a)
a, err = newAsset(p, kernelAsset)
assert.Nil(err)
assert.Equal(assetContentHash, a.computedHash)
p = &PodConfig{
Annotations: map[string]string{
annotations.KernelPath: tmpfile.Name(),
annotations.KernelHash: assetContentWrongHash,
},
}
a, err = newAsset(p, kernelAsset)
assert.NotNil(err)
}

77
virtcontainers/bridge.go Normal file
View File

@@ -0,0 +1,77 @@
//
// 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 virtcontainers
import "fmt"
type bridgeType string
const (
pciBridge bridgeType = "pci"
pcieBridge = "pcie"
)
const pciBridgeMaxCapacity = 30
// Bridge is a bridge where devices can be hot plugged
type Bridge struct {
// Address contains information about devices plugged and its address in the bridge
Address map[uint32]string
// Type is the type of the bridge (pci, pcie, etc)
Type bridgeType
//ID is used to identify the bridge in the hypervisor
ID string
}
// addDevice on success adds the device ID to the bridge and return the address
// where the device was added, otherwise an error is returned
func (b *Bridge) addDevice(ID string) (uint32, error) {
var addr uint32
// looking for the first available address
for i := uint32(1); i <= pciBridgeMaxCapacity; i++ {
if _, ok := b.Address[i]; !ok {
addr = i
break
}
}
if addr == 0 {
return 0, fmt.Errorf("Unable to hot plug device on bridge: there are not empty slots")
}
// save address and device
b.Address[addr] = ID
return addr, nil
}
// removeDevice on success removes the device ID from the bridge and return nil,
// otherwise an error is returned
func (b *Bridge) removeDevice(ID string) error {
// check if the device was hot plugged in the bridge
for addr, devID := range b.Address {
if devID == ID {
// free address to re-use the same slot with other devices
delete(b.Address, addr)
return nil
}
}
return fmt.Errorf("Unable to hot unplug device %s: not present on bridge", ID)
}

View File

@@ -0,0 +1,58 @@
//
// 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 virtcontainers
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
func TestAddRemoveDevice(t *testing.T) {
assert := assert.New(t)
// create a bridge
bridges := []*Bridge{{make(map[uint32]string), pciBridge, "rgb123"}}
// add device
devID := "abc123"
b := bridges[0]
addr, err := b.addDevice(devID)
assert.NoError(err)
if addr < 1 {
assert.Fail("address cannot be less than 1")
}
// remove device
err = b.removeDevice("")
assert.Error(err)
err = b.removeDevice(devID)
assert.NoError(err)
// add device when the bridge is full
bridges[0].Address = make(map[uint32]string)
for i := uint32(1); i <= pciBridgeMaxCapacity; i++ {
bridges[0].Address[i] = fmt.Sprintf("%d", i)
}
addr, err = b.addDevice(devID)
assert.Error(err)
if addr != 0 {
assert.Fail("address should be 0")
}
}

View File

@@ -0,0 +1,48 @@
//
// 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 virtcontainers
const (
blockDeviceSupport = 1 << iota
blockDeviceHotplugSupport
)
type capabilities struct {
flags uint
}
func (caps *capabilities) isBlockDeviceSupported() bool {
if caps.flags&blockDeviceSupport != 0 {
return true
}
return false
}
func (caps *capabilities) setBlockDeviceSupport() {
caps.flags = caps.flags | blockDeviceSupport
}
func (caps *capabilities) isBlockDeviceHotplugSupported() bool {
if caps.flags&blockDeviceHotplugSupport != 0 {
return true
}
return false
}
func (caps *capabilities) setBlockDeviceHotplugSupport() {
caps.flags |= blockDeviceHotplugSupport
}

View File

@@ -0,0 +1,47 @@
//
// 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 virtcontainers
import "testing"
func TestBlockDeviceCapability(t *testing.T) {
var caps capabilities
if caps.isBlockDeviceSupported() {
t.Fatal()
}
caps.setBlockDeviceSupport()
if !caps.isBlockDeviceSupported() {
t.Fatal()
}
}
func TestBlockDeviceHotplugCapability(t *testing.T) {
var caps capabilities
if caps.isBlockDeviceHotplugSupported() {
t.Fatal()
}
caps.setBlockDeviceHotplugSupport()
if !caps.isBlockDeviceHotplugSupported() {
t.Fatal()
}
}

View File

@@ -0,0 +1,54 @@
//
// 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 virtcontainers
import (
"os/exec"
)
type ccProxy struct {
}
// start is the proxy start implementation for ccProxy.
func (p *ccProxy) start(pod Pod, params proxyParams) (int, string, error) {
config, err := newProxyConfig(pod.config)
if err != nil {
return -1, "", err
}
// construct the socket path the proxy instance will use
proxyURL, err := defaultProxyURL(pod, SocketTypeUNIX)
if err != nil {
return -1, "", err
}
args := []string{config.Path, "-uri", proxyURL}
if config.Debug {
args = append(args, "-log", "debug")
}
cmd := exec.Command(args[0], args[1:]...)
if err := cmd.Start(); err != nil {
return -1, "", err
}
return cmd.Process.Pid, proxyURL, nil
}
func (p *ccProxy) stop(pod Pod, pid int) error {
return nil
}

View File

@@ -0,0 +1,100 @@
// 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 virtcontainers
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
)
func TestCCProxyStart(t *testing.T) {
assert := assert.New(t)
tmpdir, err := ioutil.TempDir("", "")
assert.NoError(err)
defer os.RemoveAll(tmpdir)
proxy := &ccProxy{}
type testData struct {
pod Pod
expectedURI string
expectError bool
}
invalidPath := filepath.Join(tmpdir, "enoent")
expectedSocketPath := filepath.Join(runStoragePath, testPodID, "proxy.sock")
expectedURI := fmt.Sprintf("unix://%s", expectedSocketPath)
data := []testData{
{Pod{}, "", true},
{
Pod{
config: &PodConfig{
ProxyType: "invalid",
},
}, "", true,
},
{
Pod{
config: &PodConfig{
ProxyType: CCProxyType,
ProxyConfig: ProxyConfig{
// invalid - no path
},
},
}, "", true,
},
{
Pod{
config: &PodConfig{
ProxyType: CCProxyType,
ProxyConfig: ProxyConfig{
Path: invalidPath,
},
},
}, "", true,
},
{
Pod{
id: testPodID,
config: &PodConfig{
ProxyType: CCProxyType,
ProxyConfig: ProxyConfig{
Path: "echo",
},
},
}, expectedURI, false,
},
}
for _, d := range data {
pid, uri, err := proxy.start(d.pod, proxyParams{})
if d.expectError {
assert.Error(err)
continue
}
assert.NoError(err)
assert.True(pid > 0)
assert.Equal(d.expectedURI, uri)
}
}

60
virtcontainers/cc_shim.go Normal file
View File

@@ -0,0 +1,60 @@
//
// 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 virtcontainers
import (
"fmt"
)
type ccShim struct{}
// start is the ccShim start implementation.
// It starts the cc-shim binary with URL and token flags provided by
// the proxy.
func (s *ccShim) start(pod Pod, params ShimParams) (int, error) {
if pod.config == nil {
return -1, fmt.Errorf("Pod config cannot be nil")
}
config, ok := newShimConfig(*(pod.config)).(ShimConfig)
if !ok {
return -1, fmt.Errorf("Wrong shim config type, should be CCShimConfig type")
}
if config.Path == "" {
return -1, fmt.Errorf("Shim path cannot be empty")
}
if params.Token == "" {
return -1, fmt.Errorf("Token cannot be empty")
}
if params.URL == "" {
return -1, fmt.Errorf("URL cannot be empty")
}
if params.Container == "" {
return -1, fmt.Errorf("Container cannot be empty")
}
args := []string{config.Path, "-c", params.Container, "-t", params.Token, "-u", params.URL}
if config.Debug {
args = append(args, "-d")
}
return startShim(args, params)
}

View File

@@ -0,0 +1,368 @@
//
// 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 virtcontainers
import (
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
"syscall"
"testing"
"time"
"unsafe"
. "github.com/containers/virtcontainers/pkg/mock"
)
// These tests don't care about the format of the container ID
const testContainer = "testContainer"
var testCCShimPath = "/usr/bin/virtcontainers/bin/test/cc-shim"
var testProxyURL = "foo:///foo/clear-containers/proxy.sock"
var testWrongConsolePath = "/foo/wrong-console"
var testConsolePath = "tty-console"
func getMockCCShimBinPath() string {
if DefaultMockCCShimBinPath == "" {
return testCCShimPath
}
return DefaultMockCCShimBinPath
}
func testCCShimStart(t *testing.T, pod Pod, params ShimParams, expectFail bool) {
s := &ccShim{}
pid, err := s.start(pod, params)
if expectFail {
if err == nil || pid != -1 {
t.Fatalf("This test should fail (pod %+v, params %+v, expectFail %t)",
pod, params, expectFail)
}
} else {
if err != nil {
t.Fatalf("This test should pass (pod %+v, params %+v, expectFail %t): %s",
pod, params, expectFail, err)
}
if pid == -1 {
t.Fatalf("This test should pass (pod %+v, params %+v, expectFail %t)",
pod, params, expectFail)
}
}
}
func TestCCShimStartNilPodConfigFailure(t *testing.T) {
testCCShimStart(t, Pod{}, ShimParams{}, true)
}
func TestCCShimStartNilShimConfigFailure(t *testing.T) {
pod := Pod{
config: &PodConfig{},
}
testCCShimStart(t, pod, ShimParams{}, true)
}
func TestCCShimStartShimPathEmptyFailure(t *testing.T) {
pod := Pod{
config: &PodConfig{
ShimType: CCShimType,
ShimConfig: ShimConfig{},
},
}
testCCShimStart(t, pod, ShimParams{}, true)
}
func TestCCShimStartShimTypeInvalid(t *testing.T) {
pod := Pod{
config: &PodConfig{
ShimType: "foo",
ShimConfig: ShimConfig{},
},
}
testCCShimStart(t, pod, ShimParams{}, true)
}
func TestCCShimStartParamsTokenEmptyFailure(t *testing.T) {
pod := Pod{
config: &PodConfig{
ShimType: CCShimType,
ShimConfig: ShimConfig{
Path: getMockCCShimBinPath(),
},
},
}
testCCShimStart(t, pod, ShimParams{}, true)
}
func TestCCShimStartParamsURLEmptyFailure(t *testing.T) {
pod := Pod{
config: &PodConfig{
ShimType: CCShimType,
ShimConfig: ShimConfig{
Path: getMockCCShimBinPath(),
},
},
}
params := ShimParams{
Token: "testToken",
}
testCCShimStart(t, pod, params, true)
}
func TestCCShimStartParamsContainerEmptyFailure(t *testing.T) {
pod := Pod{
config: &PodConfig{
ShimType: CCShimType,
ShimConfig: ShimConfig{
Path: getMockCCShimBinPath(),
},
},
}
params := ShimParams{
Token: "testToken",
URL: "unix://is/awesome",
}
testCCShimStart(t, pod, params, true)
}
func TestCCShimStartParamsInvalidCommand(t *testing.T) {
dir, err := ioutil.TempDir("", "")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(dir)
cmd := filepath.Join(dir, "does-not-exist")
pod := Pod{
config: &PodConfig{
ShimType: CCShimType,
ShimConfig: ShimConfig{
Path: cmd,
},
},
}
params := ShimParams{
Token: "testToken",
URL: "http://foo",
}
testCCShimStart(t, pod, params, true)
}
func startCCShimStartWithoutConsoleSuccessful(t *testing.T, detach bool) (*os.File, *os.File, *os.File, Pod, ShimParams, error) {
saveStdout := os.Stdout
rStdout, wStdout, err := os.Pipe()
if err != nil {
return nil, nil, nil, Pod{}, ShimParams{}, err
}
os.Stdout = wStdout
pod := Pod{
config: &PodConfig{
ShimType: CCShimType,
ShimConfig: ShimConfig{
Path: getMockCCShimBinPath(),
},
},
}
params := ShimParams{
Container: testContainer,
Token: "testToken",
URL: testProxyURL,
Detach: detach,
}
return rStdout, wStdout, saveStdout, pod, params, nil
}
func TestCCShimStartSuccessful(t *testing.T) {
rStdout, wStdout, saveStdout, pod, params, err := startCCShimStartWithoutConsoleSuccessful(t, false)
if err != nil {
t.Fatal(err)
}
defer func() {
os.Stdout = saveStdout
rStdout.Close()
wStdout.Close()
}()
testCCShimStart(t, pod, params, false)
bufStdout := make([]byte, 1024)
if _, err := rStdout.Read(bufStdout); err != nil {
t.Fatal(err)
}
if !strings.Contains(string(bufStdout), ShimStdoutOutput) {
t.Fatalf("Substring %q not found in %q", ShimStdoutOutput, string(bufStdout))
}
}
func TestCCShimStartDetachSuccessful(t *testing.T) {
rStdout, wStdout, saveStdout, pod, params, err := startCCShimStartWithoutConsoleSuccessful(t, true)
if err != nil {
t.Fatal(err)
}
defer func() {
os.Stdout = saveStdout
wStdout.Close()
rStdout.Close()
}()
testCCShimStart(t, pod, params, false)
readCh := make(chan error)
go func() {
bufStdout := make([]byte, 1024)
n, err := rStdout.Read(bufStdout)
if err != nil && err != io.EOF {
readCh <- err
return
}
if n > 0 {
readCh <- fmt.Errorf("Not expecting to read anything, Got %q", string(bufStdout))
return
}
readCh <- nil
}()
select {
case err := <-readCh:
if err != nil {
t.Fatal(err)
}
case <-time.After(time.Duration(20) * time.Millisecond):
return
}
}
func TestCCShimStartWithConsoleNonExistingFailure(t *testing.T) {
pod := Pod{
config: &PodConfig{
ShimType: CCShimType,
ShimConfig: ShimConfig{
Path: getMockCCShimBinPath(),
},
},
}
params := ShimParams{
Token: "testToken",
URL: testProxyURL,
Console: testWrongConsolePath,
}
testCCShimStart(t, pod, params, true)
}
func ioctl(fd uintptr, flag, data uintptr) error {
if _, _, err := syscall.Syscall(syscall.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.
func unlockpt(f *os.File) error {
var u int32
return ioctl(f.Fd(), syscall.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(), syscall.TIOCGPTN, uintptr(unsafe.Pointer(&n))); err != nil {
return "", err
}
return fmt.Sprintf("/dev/pts/%d", n), nil
}
func newConsole() (*os.File, string, error) {
master, err := os.OpenFile("/dev/ptmx", syscall.O_RDWR|syscall.O_NOCTTY|syscall.O_CLOEXEC, 0)
if err != nil {
return nil, "", err
}
console, err := ptsname(master)
if err != nil {
return nil, "", err
}
if err := unlockpt(master); err != nil {
return nil, "", err
}
if err := os.Chmod(console, 0600); err != nil {
return nil, "", err
}
return master, console, nil
}
func TestCCShimStartWithConsoleSuccessful(t *testing.T) {
cleanUp()
master, console, err := newConsole()
t.Logf("Console created for tests:%s\n", console)
if err != nil {
t.Fatal(err)
}
pod := Pod{
config: &PodConfig{
ShimType: CCShimType,
ShimConfig: ShimConfig{
Path: getMockCCShimBinPath(),
},
},
}
params := ShimParams{
Container: testContainer,
Token: "testToken",
URL: testProxyURL,
Console: console,
}
testCCShimStart(t, pod, params, false)
master.Close()
}

179
virtcontainers/cni.go Normal file
View File

@@ -0,0 +1,179 @@
//
// Copyright (c) 2016 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 virtcontainers
import (
"fmt"
cniTypes "github.com/containernetworking/cni/pkg/types"
cniV2Types "github.com/containernetworking/cni/pkg/types/020"
cniLatestTypes "github.com/containernetworking/cni/pkg/types/current"
cniPlugin "github.com/containers/virtcontainers/pkg/cni"
"github.com/sirupsen/logrus"
)
// CniPrimaryInterface Name chosen for the primary interface
// If CNI ever support multiple primary interfaces this should be revisited
const CniPrimaryInterface = "eth0"
// cni is a network implementation for the CNI plugin.
type cni struct{}
// Logger returns a logrus logger appropriate for logging cni messages
func (n *cni) Logger() *logrus.Entry {
return virtLog.WithField("subsystem", "cni")
}
func cniDNSToDNSInfo(cniDNS cniTypes.DNS) DNSInfo {
return DNSInfo{
Servers: cniDNS.Nameservers,
Domain: cniDNS.Domain,
Searches: cniDNS.Search,
Options: cniDNS.Options,
}
}
func convertLatestCNIResult(result *cniLatestTypes.Result) NetworkInfo {
return NetworkInfo{
DNS: cniDNSToDNSInfo(result.DNS),
}
}
func convertV2CNIResult(result *cniV2Types.Result) NetworkInfo {
return NetworkInfo{
DNS: cniDNSToDNSInfo(result.DNS),
}
}
func convertCNIResult(cniResult cniTypes.Result) (NetworkInfo, error) {
switch result := cniResult.(type) {
case *cniLatestTypes.Result:
return convertLatestCNIResult(result), nil
case *cniV2Types.Result:
return convertV2CNIResult(result), nil
default:
return NetworkInfo{}, fmt.Errorf("Unknown CNI result type %T", result)
}
}
func (n *cni) invokePluginsAdd(pod Pod, networkNS *NetworkNamespace) (*NetworkInfo, error) {
netPlugin, err := cniPlugin.NewNetworkPlugin()
if err != nil {
return nil, err
}
// Note: In the case of multus or cni-genie this will return only the results
// corresponding to the primary interface. The remaining results need to be
// derived
result, err := netPlugin.AddNetwork(pod.id, networkNS.NetNsPath, CniPrimaryInterface)
if err != nil {
return nil, err
}
netInfo, err := convertCNIResult(result)
if err != nil {
return nil, err
}
// We do not care about this for now but
// If present, the CNI DNS result has to be updated in resolv.conf
// if the kubelet has not supplied it already
n.Logger().Infof("AddNetwork results %s", result.String())
return &netInfo, nil
}
func (n *cni) invokePluginsDelete(pod Pod, networkNS NetworkNamespace) error {
netPlugin, err := cniPlugin.NewNetworkPlugin()
if err != nil {
return err
}
err = netPlugin.RemoveNetwork(pod.id, networkNS.NetNsPath, CniPrimaryInterface)
if err != nil {
return err
}
return nil
}
func (n *cni) updateEndpointsFromScan(networkNS *NetworkNamespace, netInfo *NetworkInfo, config NetworkConfig) error {
endpoints, err := createEndpointsFromScan(networkNS.NetNsPath, config)
if err != nil {
return err
}
for _, endpoint := range endpoints {
if CniPrimaryInterface == endpoint.Name() {
prop := endpoint.Properties()
prop.DNS = netInfo.DNS
endpoint.SetProperties(prop)
break
}
}
networkNS.Endpoints = endpoints
return nil
}
// init initializes the network, setting a new network namespace for the CNI network.
func (n *cni) init(config NetworkConfig) (string, bool, error) {
return initNetworkCommon(config)
}
// run runs a callback in the specified network namespace.
func (n *cni) run(networkNSPath string, cb func() error) error {
return runNetworkCommon(networkNSPath, cb)
}
// add adds all needed interfaces inside the network namespace for the CNI network.
func (n *cni) add(pod Pod, config NetworkConfig, netNsPath string, netNsCreated bool) (NetworkNamespace, error) {
networkNS := NetworkNamespace{
NetNsPath: netNsPath,
NetNsCreated: netNsCreated,
}
netInfo, err := n.invokePluginsAdd(pod, &networkNS)
if err != nil {
return NetworkNamespace{}, err
}
if err := n.updateEndpointsFromScan(&networkNS, netInfo, config); err != nil {
return NetworkNamespace{}, err
}
if err := addNetworkCommon(pod, &networkNS); err != nil {
return NetworkNamespace{}, err
}
return networkNS, nil
}
// remove unbridges and deletes TAP interfaces. It also removes virtual network
// interfaces and deletes the network namespace for the CNI network.
func (n *cni) remove(pod Pod, networkNS NetworkNamespace) error {
if err := removeNetworkCommon(networkNS); err != nil {
return err
}
if err := n.invokePluginsDelete(pod, networkNS); err != nil {
return err
}
return deleteNetNS(networkNS.NetNsPath, true)
}

70
virtcontainers/cnm.go Normal file
View File

@@ -0,0 +1,70 @@
//
// Copyright (c) 2016 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 virtcontainers
import (
"github.com/sirupsen/logrus"
)
// cnm is a network implementation for the CNM plugin.
type cnm struct {
config NetworkConfig
}
func cnmLogger() *logrus.Entry {
return virtLog.WithField("subsystem", "cnm")
}
// init initializes the network, setting a new network namespace for the CNM network.
func (n *cnm) init(config NetworkConfig) (string, bool, error) {
return initNetworkCommon(config)
}
// run runs a callback in the specified network namespace.
func (n *cnm) run(networkNSPath string, cb func() error) error {
return runNetworkCommon(networkNSPath, cb)
}
// add adds all needed interfaces inside the network namespace for the CNM network.
func (n *cnm) add(pod Pod, config NetworkConfig, netNsPath string, netNsCreated bool) (NetworkNamespace, error) {
endpoints, err := createEndpointsFromScan(netNsPath, config)
if err != nil {
return NetworkNamespace{}, err
}
networkNS := NetworkNamespace{
NetNsPath: netNsPath,
NetNsCreated: netNsCreated,
Endpoints: endpoints,
}
if err := addNetworkCommon(pod, &networkNS); err != nil {
return NetworkNamespace{}, err
}
return networkNS, nil
}
// remove unbridges and deletes TAP interfaces. It also removes virtual network
// interfaces and deletes the network namespace for the CNM network.
func (n *cnm) remove(pod Pod, networkNS NetworkNamespace) error {
if err := removeNetworkCommon(networkNS); err != nil {
return err
}
return deleteNetNS(networkNS.NetNsPath, true)
}

View File

@@ -0,0 +1,30 @@
//
// 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 virtcontainers
type mockAddr struct {
network string
ipAddr string
}
func (m mockAddr) Network() string {
return m.network
}
func (m mockAddr) String() string {
return m.ipAddr
}

814
virtcontainers/container.go Normal file
View File

@@ -0,0 +1,814 @@
//
// Copyright (c) 2016 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 virtcontainers
import (
"encoding/hex"
"fmt"
"os"
"path/filepath"
"syscall"
"time"
"github.com/sirupsen/logrus"
)
// Process gathers data related to a container process.
type Process struct {
// Token is the process execution context ID. It must be
// unique per pod.
// Token is used to manipulate processes for containers
// that have not started yet, and later identify them
// uniquely within a pod.
Token string
// Pid is the process ID as seen by the host software
// stack, e.g. CRI-O, containerd. This is typically the
// shim PID.
Pid int
StartTime time.Time
}
// ContainerStatus describes a container status.
type ContainerStatus struct {
ID string
State State
PID int
StartTime time.Time
RootFs string
// Annotations allow clients to store arbitrary values,
// for example to add additional status values required
// to support particular specifications.
Annotations map[string]string
}
// ContainerResources describes container resources
type ContainerResources struct {
// CPUQuota specifies the total amount of time in microseconds
// The number of microseconds per CPUPeriod that the container is guaranteed CPU access
CPUQuota int64
// CPUPeriod specifies the CPU CFS scheduler period of time in microseconds
CPUPeriod uint64
// CPUShares specifies container's weight vs. other containers
CPUShares uint64
}
// ContainerConfig describes one container runtime configuration.
type ContainerConfig struct {
ID string
// RootFs is the container workload image on the host.
RootFs string
// ReadOnlyRootfs indicates if the rootfs should be mounted readonly
ReadonlyRootfs bool
// Cmd specifies the command to run on a container
Cmd Cmd
// Annotations allow clients to store arbitrary values,
// for example to add additional status values required
// to support particular specifications.
Annotations map[string]string
Mounts []Mount
// Device configuration for devices that must be available within the container.
DeviceInfos []DeviceInfo
// Resources container resources
Resources ContainerResources
}
// valid checks that the container configuration is valid.
func (c *ContainerConfig) valid() bool {
if c == nil {
return false
}
if c.ID == "" {
return false
}
return true
}
// SystemMountsInfo describes additional information for system mounts that the agent
// needs to handle
type SystemMountsInfo struct {
// Indicates if /dev has been passed as a bind mount for the host /dev
BindMountDev bool
// Size of /dev/shm assigned on the host.
DevShmSize uint
}
// Container is composed of a set of containers and a runtime environment.
// A Container can be created, deleted, started, stopped, listed, entered, paused and restored.
type Container struct {
id string
podID string
rootFs string
config *ContainerConfig
pod *Pod
runPath string
configPath string
containerPath string
state State
process Process
mounts []Mount
devices []Device
systemMountsInfo SystemMountsInfo
}
// ID returns the container identifier string.
func (c *Container) ID() string {
return c.id
}
// Logger returns a logrus logger appropriate for logging Container messages
func (c *Container) Logger() *logrus.Entry {
return virtLog.WithFields(logrus.Fields{
"subsystem": "container",
"container-id": c.id,
"pod-id": c.podID,
})
}
// Pod returns the pod handler related to this container.
func (c *Container) Pod() VCPod {
return c.pod
}
// Process returns the container process.
func (c *Container) Process() Process {
return c.process
}
// GetToken returns the token related to this container's process.
func (c *Container) GetToken() string {
return c.process.Token
}
// GetPid returns the pid related to this container's process.
func (c *Container) GetPid() int {
return c.process.Pid
}
// SetPid sets and stores the given pid as the pid of container's process.
func (c *Container) SetPid(pid int) error {
c.process.Pid = pid
return c.storeProcess()
}
func (c *Container) setStateBlockIndex(index int) error {
c.state.BlockIndex = index
err := c.pod.storage.storeContainerResource(c.pod.id, c.id, stateFileType, c.state)
if err != nil {
return err
}
return nil
}
func (c *Container) setStateFstype(fstype string) error {
c.state.Fstype = fstype
err := c.pod.storage.storeContainerResource(c.pod.id, c.id, stateFileType, c.state)
if err != nil {
return err
}
return nil
}
func (c *Container) setStateHotpluggedDrive(hotplugged bool) error {
c.state.HotpluggedDrive = hotplugged
err := c.pod.storage.storeContainerResource(c.pod.id, c.id, stateFileType, c.state)
if err != nil {
return err
}
return nil
}
// GetAnnotations returns container's annotations
func (c *Container) GetAnnotations() map[string]string {
return c.config.Annotations
}
func (c *Container) storeProcess() error {
return c.pod.storage.storeContainerProcess(c.podID, c.id, c.process)
}
func (c *Container) fetchProcess() (Process, error) {
return c.pod.storage.fetchContainerProcess(c.podID, c.id)
}
func (c *Container) storeMounts() error {
return c.pod.storage.storeContainerMounts(c.podID, c.id, c.mounts)
}
func (c *Container) fetchMounts() ([]Mount, error) {
return c.pod.storage.fetchContainerMounts(c.podID, c.id)
}
func (c *Container) storeDevices() error {
return c.pod.storage.storeContainerDevices(c.podID, c.id, c.devices)
}
func (c *Container) fetchDevices() ([]Device, error) {
return c.pod.storage.fetchContainerDevices(c.podID, c.id)
}
// storeContainer stores a container config.
func (c *Container) storeContainer() error {
fs := filesystem{}
err := fs.storeContainerResource(c.pod.id, c.id, configFileType, *(c.config))
if err != nil {
return err
}
return nil
}
// setContainerState sets both the in-memory and on-disk state of the
// container.
func (c *Container) setContainerState(state stateString) error {
if state == "" {
return errNeedState
}
// update in-memory state
c.state.State = state
// update on-disk state
err := c.pod.storage.storeContainerResource(c.pod.id, c.id, stateFileType, c.state)
if err != nil {
return err
}
return nil
}
func (c *Container) createContainersDirs() error {
err := os.MkdirAll(c.runPath, dirMode)
if err != nil {
return err
}
err = os.MkdirAll(c.configPath, dirMode)
if err != nil {
c.pod.storage.deleteContainerResources(c.podID, c.id, nil)
return err
}
return nil
}
// mountSharedDirMounts handles bind-mounts by bindmounting to the host shared
// directory which is mounted through 9pfs in the VM.
// It also updates the container mount list with the HostPath info, and store
// container mounts to the storage. This way, we will have the HostPath info
// available when we will need to unmount those mounts.
func (c *Container) mountSharedDirMounts(hostSharedDir, guestSharedDir string) ([]Mount, error) {
var sharedDirMounts []Mount
for idx, m := range c.mounts {
if isSystemMount(m.Destination) || m.Type != "bind" {
continue
}
randBytes, err := generateRandomBytes(8)
if err != nil {
return nil, err
}
// These mounts are created in the shared dir
filename := fmt.Sprintf("%s-%s-%s", c.id, hex.EncodeToString(randBytes), filepath.Base(m.Destination))
mountDest := filepath.Join(hostSharedDir, c.pod.id, filename)
if err := bindMount(m.Source, mountDest, false); err != nil {
return nil, err
}
// Save HostPath mount value into the mount list of the container.
c.mounts[idx].HostPath = mountDest
// Check if mount is readonly, let the agent handle the readonly mount
// within the VM.
readonly := false
for _, flag := range m.Options {
if flag == "ro" {
readonly = true
}
}
sharedDirMount := Mount{
Source: filepath.Join(guestSharedDir, filename),
Destination: m.Destination,
Type: m.Type,
Options: m.Options,
ReadOnly: readonly,
}
sharedDirMounts = append(sharedDirMounts, sharedDirMount)
}
if err := c.storeMounts(); err != nil {
return nil, err
}
return sharedDirMounts, nil
}
func (c *Container) unmountHostMounts() error {
for _, m := range c.mounts {
if m.HostPath != "" {
if err := syscall.Unmount(m.HostPath, 0); err != nil {
c.Logger().WithFields(logrus.Fields{
"host-path": m.HostPath,
"error": err,
}).Warn("Could not umount")
return err
}
}
}
return nil
}
// newContainer creates a Container structure from a pod and a container configuration.
func newContainer(pod *Pod, contConfig ContainerConfig) (*Container, error) {
if contConfig.valid() == false {
return &Container{}, fmt.Errorf("Invalid container configuration")
}
c := &Container{
id: contConfig.ID,
podID: pod.id,
rootFs: contConfig.RootFs,
config: &contConfig,
pod: pod,
runPath: filepath.Join(runStoragePath, pod.id, contConfig.ID),
configPath: filepath.Join(configStoragePath, pod.id, contConfig.ID),
containerPath: filepath.Join(pod.id, contConfig.ID),
state: State{},
process: Process{},
mounts: contConfig.Mounts,
}
state, err := c.pod.storage.fetchContainerState(c.podID, c.id)
if err == nil {
c.state = state
}
process, err := c.pod.storage.fetchContainerProcess(c.podID, c.id)
if err == nil {
c.process = process
}
mounts, err := c.fetchMounts()
if err == nil {
c.mounts = mounts
}
// Devices will be found in storage after create stage has completed.
// We fetch devices from storage at all other stages.
storedDevices, err := c.fetchDevices()
if err == nil {
c.devices = storedDevices
} else {
// If devices were not found in storage, create Device implementations
// from the configuration. This should happen at create.
devices, err := newDevices(contConfig.DeviceInfos)
if err != nil {
return &Container{}, err
}
c.devices = devices
}
return c, nil
}
// createContainer creates and start a container inside a Pod. It has to be
// called only when a new container, not known by the pod, has to be created.
func createContainer(pod *Pod, contConfig ContainerConfig) (*Container, error) {
if pod == nil {
return nil, errNeedPod
}
c, err := newContainer(pod, contConfig)
if err != nil {
return nil, err
}
if err := c.createContainersDirs(); err != nil {
return nil, err
}
if !c.pod.config.HypervisorConfig.DisableBlockDeviceUse {
agentCaps := c.pod.agent.capabilities()
hypervisorCaps := c.pod.hypervisor.capabilities()
if agentCaps.isBlockDeviceSupported() && hypervisorCaps.isBlockDeviceHotplugSupported() {
if err := c.hotplugDrive(); err != nil {
return nil, err
}
}
}
// Attach devices
if err := c.attachDevices(); err != nil {
return nil, err
}
if err := c.addResources(); err != nil {
return nil, err
}
// Deduce additional system mount info that should be handled by the agent
// inside the VM
c.getSystemMountInfo()
if err := c.storeDevices(); err != nil {
return nil, err
}
process, err := pod.agent.createContainer(c.pod, c)
if err != nil {
return nil, err
}
c.process = *process
// Store the container process returned by the agent.
if err := c.storeProcess(); err != nil {
return nil, err
}
if err := c.setContainerState(StateReady); err != nil {
return nil, err
}
return c, nil
}
func (c *Container) delete() error {
if c.state.State != StateReady &&
c.state.State != StateStopped {
return fmt.Errorf("Container not ready or stopped, impossible to delete")
}
// Remove the container from pod structure
if err := c.pod.removeContainer(c.id); err != nil {
return err
}
return c.pod.storage.deleteContainerResources(c.podID, c.id, nil)
}
// checkPodRunning validates the container state.
//
// cmd specifies the operation (or verb) that the retrieval is destined
// for and is only used to make the returned error as descriptive as
// possible.
func (c *Container) checkPodRunning(cmd string) error {
if cmd == "" {
return fmt.Errorf("Cmd cannot be empty")
}
if c.pod.state.State != StateRunning {
return fmt.Errorf("Pod not running, impossible to %s the container", cmd)
}
return nil
}
func (c *Container) getSystemMountInfo() {
// check if /dev needs to be bind mounted from host /dev
c.systemMountsInfo.BindMountDev = false
for _, m := range c.mounts {
if m.Source == "/dev" && m.Destination == "/dev" && m.Type == "bind" {
c.systemMountsInfo.BindMountDev = true
}
}
// TODO Deduce /dev/shm size. See https://github.com/clearcontainers/runtime/issues/138
}
func (c *Container) start() error {
if err := c.checkPodRunning("start"); err != nil {
return err
}
if c.state.State != StateReady &&
c.state.State != StateStopped {
return fmt.Errorf("Container not ready or stopped, impossible to start")
}
if err := c.state.validTransition(c.state.State, StateRunning); err != nil {
return err
}
if err := c.pod.agent.startContainer(*(c.pod), c); err != nil {
c.Logger().WithError(err).Error("Failed to start container")
if err := c.stop(); err != nil {
c.Logger().WithError(err).Warn("Failed to stop container")
}
return err
}
return c.setContainerState(StateRunning)
}
func (c *Container) stop() error {
// In case the container status has been updated implicitly because
// the container process has terminated, it might be possible that
// someone try to stop the container, and we don't want to issue an
// error in that case. This should be a no-op.
//
// This has to be handled before the transition validation since this
// is an exception.
if c.state.State == StateStopped {
c.Logger().Info("Container already stopped")
return nil
}
if c.pod.state.State != StateReady && c.pod.state.State != StateRunning {
return fmt.Errorf("Pod not ready or running, impossible to stop the container")
}
if err := c.state.validTransition(c.state.State, StateStopped); err != nil {
return err
}
defer func() {
// If shim is still running something went wrong
// Make sure we stop the shim process
if running, _ := isShimRunning(c.process.Pid); running {
l := c.Logger()
l.Error("Failed to stop container so stopping dangling shim")
if err := stopShim(c.process.Pid); err != nil {
l.WithError(err).Warn("failed to stop shim")
}
}
}()
// Here we expect that stop() has been called because the container
// process returned or because it received a signal. In case of a
// signal, we want to give it some time to end the container process.
// However, if the signal didn't reach its goal, the caller still
// expects this container to be stopped, that's why we should not
// return an error, but instead try to kill it forcefully.
if err := waitForShim(c.process.Pid); err != nil {
// Force the container to be killed.
if err := c.pod.agent.killContainer(*(c.pod), *c, syscall.SIGKILL, true); err != nil {
return err
}
// Wait for the end of container process. We expect this call
// to succeed. Indeed, we have already given a second chance
// to the container by trying to kill it with SIGKILL, there
// is no reason to try to go further if we got an error.
if err := waitForShim(c.process.Pid); err != nil {
return err
}
}
if err := c.pod.agent.stopContainer(*(c.pod), *c); err != nil {
return err
}
if err := c.removeResources(); err != nil {
return err
}
if err := c.detachDevices(); err != nil {
return err
}
if err := c.removeDrive(); err != nil {
return err
}
return c.setContainerState(StateStopped)
}
func (c *Container) enter(cmd Cmd) (*Process, error) {
if err := c.checkPodRunning("enter"); err != nil {
return nil, err
}
if c.state.State != StateRunning {
return nil, fmt.Errorf("Container not running, impossible to enter")
}
process, err := c.pod.agent.exec(c.pod, *c, cmd)
if err != nil {
return nil, err
}
return process, nil
}
func (c *Container) kill(signal syscall.Signal, all bool) error {
if c.pod.state.State != StateReady && c.pod.state.State != StateRunning {
return fmt.Errorf("Pod not ready or running, impossible to signal the container")
}
if c.state.State != StateReady && c.state.State != StateRunning {
return fmt.Errorf("Container not ready or running, impossible to signal the container")
}
return c.pod.agent.killContainer(*(c.pod), *c, signal, all)
}
func (c *Container) processList(options ProcessListOptions) (ProcessList, error) {
if err := c.checkPodRunning("ps"); err != nil {
return nil, err
}
if c.state.State != StateRunning {
return nil, fmt.Errorf("Container not running, impossible to list processes")
}
return c.pod.agent.processListContainer(*(c.pod), *c, options)
}
func (c *Container) hotplugDrive() error {
dev, err := getDeviceForPath(c.rootFs)
if err == errMountPointNotFound {
return nil
}
if err != nil {
return err
}
c.Logger().WithFields(logrus.Fields{
"device-major": dev.major,
"device-minor": dev.minor,
"mount-point": dev.mountPoint,
}).Info("device details")
isDM, err := checkStorageDriver(dev.major, dev.minor)
if err != nil {
return err
}
if !isDM {
return nil
}
// If device mapper device, then fetch the full path of the device
devicePath, fsType, err := getDevicePathAndFsType(dev.mountPoint)
if err != nil {
return err
}
c.Logger().WithFields(logrus.Fields{
"device-path": devicePath,
"fs-type": fsType,
}).Info("Block device detected")
driveIndex, err := c.pod.getAndSetPodBlockIndex()
if err != nil {
return err
}
// Add drive with id as container id
devID := makeNameID("drive", c.id)
drive := Drive{
File: devicePath,
Format: "raw",
ID: devID,
Index: driveIndex,
}
if err := c.pod.hypervisor.hotplugAddDevice(drive, blockDev); err != nil {
return err
}
c.setStateHotpluggedDrive(true)
if err := c.setStateBlockIndex(driveIndex); err != nil {
return err
}
return c.setStateFstype(fsType)
}
// isDriveUsed checks if a drive has been used for container rootfs
func (c *Container) isDriveUsed() bool {
if c.state.Fstype == "" {
return false
}
return true
}
func (c *Container) removeDrive() (err error) {
if c.isDriveUsed() && c.state.HotpluggedDrive {
c.Logger().Info("unplugging block device")
devID := makeNameID("drive", c.id)
drive := Drive{
ID: devID,
}
l := c.Logger().WithField("device-id", devID)
l.Info("Unplugging block device")
if err := c.pod.hypervisor.hotplugRemoveDevice(drive, blockDev); err != nil {
l.WithError(err).Info("Failed to unplug block device")
return err
}
}
return nil
}
func (c *Container) attachDevices() error {
for _, device := range c.devices {
if err := device.attach(c.pod.hypervisor, c); err != nil {
return err
}
}
return nil
}
func (c *Container) detachDevices() error {
for _, device := range c.devices {
if err := device.detach(c.pod.hypervisor); err != nil {
return err
}
}
return nil
}
func (c *Container) addResources() error {
//TODO add support for memory, Issue: https://github.com/containers/virtcontainers/issues/578
if c.config == nil {
return nil
}
vCPUs := ConstraintsToVCPUs(c.config.Resources.CPUQuota, c.config.Resources.CPUPeriod)
if vCPUs != 0 {
virtLog.Debugf("hot adding %d vCPUs", vCPUs)
if err := c.pod.hypervisor.hotplugAddDevice(uint32(vCPUs), cpuDev); err != nil {
return err
}
}
return nil
}
func (c *Container) removeResources() error {
//TODO add support for memory, Issue: https://github.com/containers/virtcontainers/issues/578
if c.config == nil {
return nil
}
vCPUs := ConstraintsToVCPUs(c.config.Resources.CPUQuota, c.config.Resources.CPUPeriod)
if vCPUs != 0 {
virtLog.Debugf("hot removing %d vCPUs", vCPUs)
if err := c.pod.hypervisor.hotplugRemoveDevice(uint32(vCPUs), cpuDev); err != nil {
return err
}
}
return nil
}

View File

@@ -0,0 +1,334 @@
//
// 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 virtcontainers
import (
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"reflect"
"strings"
"syscall"
"testing"
vcAnnotations "github.com/containers/virtcontainers/pkg/annotations"
"github.com/stretchr/testify/assert"
)
func TestGetAnnotations(t *testing.T) {
annotations := map[string]string{
"annotation1": "abc",
"annotation2": "xyz",
"annotation3": "123",
}
container := Container{
config: &ContainerConfig{
Annotations: annotations,
},
}
containerAnnotations := container.GetAnnotations()
for k, v := range containerAnnotations {
if annotations[k] != v {
t.Fatalf("Expecting ['%s']='%s', Got ['%s']='%s'\n", k, annotations[k], k, v)
}
}
}
func TestContainerSystemMountsInfo(t *testing.T) {
mounts := []Mount{
{
Source: "/dev",
Destination: "/dev",
Type: "bind",
},
{
Source: "procfs",
Destination: "/proc",
Type: "procfs",
},
}
c := Container{
mounts: mounts,
}
assert.False(t, c.systemMountsInfo.BindMountDev)
c.getSystemMountInfo()
assert.True(t, c.systemMountsInfo.BindMountDev)
c.mounts[0].Type = "tmpfs"
c.getSystemMountInfo()
assert.False(t, c.systemMountsInfo.BindMountDev)
}
func TestContainerPod(t *testing.T) {
expectedPod := &Pod{}
container := Container{
pod: expectedPod,
}
pod := container.Pod()
if !reflect.DeepEqual(pod, expectedPod) {
t.Fatalf("Expecting %+v\nGot %+v", expectedPod, pod)
}
}
func TestContainerRemoveDrive(t *testing.T) {
pod := &Pod{}
container := Container{
pod: pod,
id: "testContainer",
}
container.state.Fstype = ""
err := container.removeDrive()
// hotplugRemoveDevice for hypervisor should not be called.
// test should pass without a hypervisor created for the container's pod.
if err != nil {
t.Fatal("")
}
container.state.Fstype = "xfs"
container.state.HotpluggedDrive = false
err = container.removeDrive()
// hotplugRemoveDevice for hypervisor should not be called.
if err != nil {
t.Fatal("")
}
container.state.HotpluggedDrive = true
pod.hypervisor = &mockHypervisor{}
err = container.removeDrive()
if err != nil {
t.Fatal()
}
}
func testSetupFakeRootfs(t *testing.T) (testRawFile, loopDev, mntDir string, err error) {
tmpDir, err := ioutil.TempDir("", "")
if err != nil {
t.Fatal(err)
}
testRawFile = filepath.Join(tmpDir, "raw.img")
if _, err := os.Stat(testRawFile); !os.IsNotExist(err) {
os.Remove(testRawFile)
}
output, err := exec.Command("losetup", "-f").CombinedOutput()
if err != nil {
t.Fatalf("Skipping test since no loop device available for tests : %s, %s", output, err)
return
}
loopDev = strings.TrimSpace(string(output[:]))
output, err = exec.Command("fallocate", "-l", "256K", testRawFile).CombinedOutput()
if err != nil {
t.Fatalf("fallocate failed %s %s", output, err)
}
output, err = exec.Command("mkfs.ext4", "-F", testRawFile).CombinedOutput()
if err != nil {
t.Fatalf("mkfs.ext4 failed for %s: %s, %s", testRawFile, output, err)
}
output, err = exec.Command("losetup", loopDev, testRawFile).CombinedOutput()
if err != nil {
t.Fatalf("Losetup for %s at %s failed : %s, %s ", loopDev, testRawFile, output, err)
return
}
mntDir = filepath.Join(tmpDir, "rootfs")
err = os.Mkdir(mntDir, dirMode)
if err != nil {
t.Fatalf("Error creating dir %s: %s", mntDir, err)
}
err = syscall.Mount(loopDev, mntDir, "ext4", uintptr(0), "")
if err != nil {
t.Fatalf("Error while mounting loop device %s at %s: %s", loopDev, mntDir, err)
}
return
}
func cleanupFakeRootfsSetup(testRawFile, loopDev, mntDir string) {
// unmount loop device
if mntDir != "" {
syscall.Unmount(mntDir, 0)
}
// detach loop device
if loopDev != "" {
exec.Command("losetup", "-d", loopDev).CombinedOutput()
}
if _, err := os.Stat(testRawFile); err == nil {
tmpDir := filepath.Dir(testRawFile)
os.RemoveAll(tmpDir)
}
}
func TestContainerAddDriveDir(t *testing.T) {
testRawFile, loopDev, fakeRootfs, err := testSetupFakeRootfs(t)
defer cleanupFakeRootfsSetup(testRawFile, loopDev, fakeRootfs)
if err != nil {
t.Fatalf("Error while setting up fake rootfs: %v, Skipping test", err)
}
fs := &filesystem{}
pod := &Pod{
id: testPodID,
storage: fs,
hypervisor: &mockHypervisor{},
}
contID := "100"
container := Container{
pod: pod,
id: contID,
rootFs: fakeRootfs,
}
// create state file
path := filepath.Join(runStoragePath, testPodID, container.ID())
err = os.MkdirAll(path, dirMode)
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(path)
stateFilePath := filepath.Join(path, stateFile)
os.Remove(stateFilePath)
_, err = os.Create(stateFilePath)
if err != nil {
t.Fatal(err)
}
defer os.Remove(stateFilePath)
// Make the checkStorageDriver func variable point to a fake check function
savedFunc := checkStorageDriver
checkStorageDriver = func(major, minor int) (bool, error) {
return true, nil
}
defer func() {
checkStorageDriver = savedFunc
}()
container.state.Fstype = ""
container.state.HotpluggedDrive = false
err = container.hotplugDrive()
if err != nil {
t.Fatalf("Error with hotplugDrive :%v", err)
}
if container.state.Fstype == "" || !container.state.HotpluggedDrive {
t.Fatal()
}
}
func TestCheckPodRunningEmptyCmdFailure(t *testing.T) {
c := &Container{}
err := c.checkPodRunning("")
assert.NotNil(t, err, "Should fail because provided command is empty")
}
func TestCheckPodRunningNotRunningFailure(t *testing.T) {
c := &Container{
pod: &Pod{},
}
err := c.checkPodRunning("test_cmd")
assert.NotNil(t, err, "Should fail because pod state is empty")
}
func TestCheckPodRunningSuccessful(t *testing.T) {
c := &Container{
pod: &Pod{
state: State{
State: StateRunning,
},
},
}
err := c.checkPodRunning("test_cmd")
assert.Nil(t, err, "%v", err)
}
func TestContainerAddResources(t *testing.T) {
assert := assert.New(t)
c := &Container{}
err := c.addResources()
assert.Nil(err)
c.config = &ContainerConfig{Annotations: make(map[string]string)}
c.config.Annotations[vcAnnotations.ContainerTypeKey] = string(PodSandbox)
err = c.addResources()
assert.Nil(err)
c.config.Annotations[vcAnnotations.ContainerTypeKey] = string(PodContainer)
err = c.addResources()
assert.Nil(err)
c.config.Resources = ContainerResources{
CPUQuota: 5000,
CPUPeriod: 1000,
}
c.pod = &Pod{hypervisor: &mockHypervisor{}}
err = c.addResources()
assert.Nil(err)
}
func TestContainerRemoveResources(t *testing.T) {
assert := assert.New(t)
c := &Container{}
err := c.addResources()
assert.Nil(err)
c.config = &ContainerConfig{Annotations: make(map[string]string)}
c.config.Annotations[vcAnnotations.ContainerTypeKey] = string(PodSandbox)
err = c.removeResources()
assert.Nil(err)
c.config.Annotations[vcAnnotations.ContainerTypeKey] = string(PodContainer)
err = c.removeResources()
assert.Nil(err)
c.config.Resources = ContainerResources{
CPUQuota: 5000,
CPUPeriod: 1000,
}
c.pod = &Pod{hypervisor: &mockHypervisor{}}
err = c.removeResources()
assert.Nil(err)
}

652
virtcontainers/device.go Normal file
View File

@@ -0,0 +1,652 @@
//
// 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 virtcontainers
import (
"encoding/hex"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/go-ini/ini"
"github.com/sirupsen/logrus"
)
const (
// DeviceVFIO is the VFIO device type
DeviceVFIO = "vfio"
// DeviceBlock is the block device type
DeviceBlock = "block"
// DeviceGeneric is a generic device type
DeviceGeneric = "generic"
)
// Defining this as a variable instead of a const, to allow
// overriding this in the tests.
var sysIOMMUPath = "/sys/kernel/iommu_groups"
var sysDevPrefix = "/sys/dev"
var blockPaths = []string{
"/dev/sd", //SCSI block device
"/dev/hd", //IDE block device
"/dev/vd", //Virtual Block device
"/dev/ida/", //Compaq Intelligent Drive Array devices
}
const (
vfioPath = "/dev/vfio/"
)
// Device is the virtcontainers device interface.
type Device interface {
attach(hypervisor, *Container) error
detach(hypervisor) error
deviceType() string
}
// DeviceInfo is an embedded type that contains device data common to all types of devices.
type DeviceInfo struct {
// Device path on host
HostPath string
// Device path inside the container
ContainerPath string
// Type of device: c, b, u or p
// c , u - character(unbuffered)
// p - FIFO
// b - block(buffered) special file
// More info in mknod(1).
DevType string
// Major, minor numbers for device.
Major int64
Minor int64
// FileMode permission bits for the device.
FileMode os.FileMode
// id of the device owner.
UID uint32
// id of the device group.
GID uint32
// Hotplugged is used to store device state indicating if the
// device was hotplugged.
Hotplugged bool
// ID for the device that is passed to the hypervisor.
ID string
}
func deviceLogger() *logrus.Entry {
return virtLog.WithField("subsystem", "device")
}
// VFIODevice is a vfio device meant to be passed to the hypervisor
// to be used by the Virtual Machine.
type VFIODevice struct {
DeviceType string
DeviceInfo DeviceInfo
BDF string
}
func newVFIODevice(devInfo DeviceInfo) *VFIODevice {
return &VFIODevice{
DeviceType: DeviceVFIO,
DeviceInfo: devInfo,
}
}
func (device *VFIODevice) attach(h hypervisor, c *Container) error {
vfioGroup := filepath.Base(device.DeviceInfo.HostPath)
iommuDevicesPath := filepath.Join(sysIOMMUPath, vfioGroup, "devices")
deviceFiles, err := ioutil.ReadDir(iommuDevicesPath)
if err != nil {
return err
}
// Pass all devices in iommu group
for _, deviceFile := range deviceFiles {
//Get bdf of device eg 0000:00:1c.0
deviceBDF, err := getBDF(deviceFile.Name())
if err != nil {
return err
}
device.BDF = deviceBDF
if err := h.addDevice(*device, vfioDev); err != nil {
deviceLogger().WithError(err).Error("Failed to add device")
return err
}
deviceLogger().WithFields(logrus.Fields{
"device-group": device.DeviceInfo.HostPath,
"device-type": "vfio-passthrough",
}).Info("Device group attached")
}
return nil
}
func (device *VFIODevice) detach(h hypervisor) error {
return nil
}
func (device *VFIODevice) deviceType() string {
return device.DeviceType
}
// VhostUserDeviceType - represents a vhost-user device type
// Currently support just VhostUserNet
type VhostUserDeviceType string
const (
//VhostUserSCSI - SCSI based vhost-user type
VhostUserSCSI = "vhost-user-scsi-pci"
//VhostUserNet - net based vhost-user type
VhostUserNet = "virtio-net-pci"
//VhostUserBlk represents a block vhostuser device type
VhostUserBlk = "vhost-user-blk-pci"
)
// VhostUserDevice represents a vhost-user device. Shared
// attributes of a vhost-user device can be retrieved using
// the Attrs() method. Unique data can be obtained by casting
// the object to the proper type.
type VhostUserDevice interface {
Attrs() *VhostUserDeviceAttrs
Type() string
}
// VhostUserDeviceAttrs represents data shared by most vhost-user devices
type VhostUserDeviceAttrs struct {
DeviceType string
DeviceInfo DeviceInfo
SocketPath string
ID string
}
// VhostUserNetDevice is a network vhost-user based device
type VhostUserNetDevice struct {
VhostUserDeviceAttrs
MacAddress string
}
// Attrs returns the VhostUserDeviceAttrs associated with the vhost-user device
func (vhostUserNetDevice *VhostUserNetDevice) Attrs() *VhostUserDeviceAttrs {
return &vhostUserNetDevice.VhostUserDeviceAttrs
}
// Type returns the type associated with the vhost-user device
func (vhostUserNetDevice *VhostUserNetDevice) Type() string {
return VhostUserNet
}
// VhostUserSCSIDevice is a SCSI vhost-user based device
type VhostUserSCSIDevice struct {
VhostUserDeviceAttrs
}
// Attrs returns the VhostUserDeviceAttrs associated with the vhost-user device
func (vhostUserSCSIDevice *VhostUserSCSIDevice) Attrs() *VhostUserDeviceAttrs {
return &vhostUserSCSIDevice.VhostUserDeviceAttrs
}
// Type returns the type associated with the vhost-user device
func (vhostUserSCSIDevice *VhostUserSCSIDevice) Type() string {
return VhostUserSCSI
}
// VhostUserBlkDevice is a block vhost-user based device
type VhostUserBlkDevice struct {
VhostUserDeviceAttrs
}
// Attrs returns the VhostUserDeviceAttrs associated with the vhost-user device
func (vhostUserBlkDevice *VhostUserBlkDevice) Attrs() *VhostUserDeviceAttrs {
return &vhostUserBlkDevice.VhostUserDeviceAttrs
}
// Type returns the type associated with the vhost-user device
func (vhostUserBlkDevice *VhostUserBlkDevice) Type() string {
return VhostUserBlk
}
// vhostUserAttach handles the common logic among all of the vhost-user device's
// attach functions
func vhostUserAttach(device VhostUserDevice, h hypervisor, c *Container) (err error) {
// generate a unique ID to be used for hypervisor commandline fields
randBytes, err := generateRandomBytes(8)
if err != nil {
return err
}
id := hex.EncodeToString(randBytes)
device.Attrs().ID = id
return h.addDevice(device, vhostuserDev)
}
//
// VhostUserNetDevice's implementation of the device interface:
//
func (vhostUserNetDevice *VhostUserNetDevice) attach(h hypervisor, c *Container) (err error) {
return vhostUserAttach(vhostUserNetDevice, h, c)
}
func (vhostUserNetDevice *VhostUserNetDevice) detach(h hypervisor) error {
return nil
}
func (vhostUserNetDevice *VhostUserNetDevice) deviceType() string {
return vhostUserNetDevice.DeviceType
}
//
// VhostUserBlkDevice's implementation of the device interface:
//
func (vhostUserBlkDevice *VhostUserBlkDevice) attach(h hypervisor, c *Container) (err error) {
return vhostUserAttach(vhostUserBlkDevice, h, c)
}
func (vhostUserBlkDevice *VhostUserBlkDevice) detach(h hypervisor) error {
return nil
}
func (vhostUserBlkDevice *VhostUserBlkDevice) deviceType() string {
return vhostUserBlkDevice.DeviceType
}
//
// VhostUserSCSIDevice's implementation of the device interface:
//
func (vhostUserSCSIDevice *VhostUserSCSIDevice) attach(h hypervisor, c *Container) (err error) {
return vhostUserAttach(vhostUserSCSIDevice, h, c)
}
func (vhostUserSCSIDevice *VhostUserSCSIDevice) detach(h hypervisor) error {
return nil
}
func (vhostUserSCSIDevice *VhostUserSCSIDevice) deviceType() string {
return vhostUserSCSIDevice.DeviceType
}
// Long term, this should be made more configurable. For now matching path
// provided by CNM VPP and OVS-DPDK plugins, available at github.com/clearcontainers/vpp and
// github.com/clearcontainers/ovsdpdk. The plugins create the socket on the host system
// using this path.
const hostSocketSearchPath = "/tmp/vhostuser_%s/vhu.sock"
// findVhostUserNetSocketPath checks if an interface is a dummy placeholder
// for a vhost-user socket, and if it is it returns the path to the socket
func findVhostUserNetSocketPath(netInfo NetworkInfo) (string, error) {
if netInfo.Iface.Name == "lo" {
return "", nil
}
// check for socket file existence at known location.
for _, addr := range netInfo.Addrs {
socketPath := fmt.Sprintf(hostSocketSearchPath, addr.IPNet.IP)
if _, err := os.Stat(socketPath); err == nil {
return socketPath, nil
}
}
return "", nil
}
// vhostUserSocketPath returns the path of the socket discovered. This discovery
// will vary depending on the type of vhost-user socket.
// Today only VhostUserNetDevice is supported.
func vhostUserSocketPath(info interface{}) (string, error) {
switch v := info.(type) {
case NetworkInfo:
return findVhostUserNetSocketPath(v)
default:
return "", nil
}
}
// BlockDevice refers to a block storage device implementation.
type BlockDevice struct {
DeviceType string
DeviceInfo DeviceInfo
// SCSI Address of the block device, in case the device is attached using SCSI driver
// SCSI address is in the format SCSI-Id:LUN
SCSIAddr string
// Path at which the device appears inside the VM, outside of the container mount namespace
VirtPath string
}
func newBlockDevice(devInfo DeviceInfo) *BlockDevice {
return &BlockDevice{
DeviceType: DeviceBlock,
DeviceInfo: devInfo,
}
}
func (device *BlockDevice) attach(h hypervisor, c *Container) (err error) {
randBytes, err := generateRandomBytes(8)
if err != nil {
return err
}
device.DeviceInfo.ID = hex.EncodeToString(randBytes)
// Increment the block index for the pod. This is used to determine the name
// for the block device in the case where the block device is used as container
// rootfs and the predicted block device name needs to be provided to the agent.
index, err := c.pod.getAndSetPodBlockIndex()
defer func() {
if err != nil {
c.pod.decrementPodBlockIndex()
}
}()
if err != nil {
return err
}
drive := Drive{
File: device.DeviceInfo.HostPath,
Format: "raw",
ID: makeNameID("drive", device.DeviceInfo.ID),
Index: index,
}
driveName, err := getVirtDriveName(index)
if err != nil {
return err
}
deviceLogger().WithField("device", device.DeviceInfo.HostPath).Info("Attaching block device")
if err = h.hotplugAddDevice(drive, blockDev); err != nil {
return err
}
device.DeviceInfo.Hotplugged = true
if c.pod.config.HypervisorConfig.BlockDeviceDriver == VirtioBlock {
device.VirtPath = filepath.Join("/dev", driveName)
} else {
scsiAddr, err := getSCSIAddress(index)
if err != nil {
return err
}
device.SCSIAddr = scsiAddr
}
return nil
}
func (device BlockDevice) detach(h hypervisor) error {
if device.DeviceInfo.Hotplugged {
deviceLogger().WithField("device", device.DeviceInfo.HostPath).Info("Unplugging block device")
drive := Drive{
ID: makeNameID("drive", device.DeviceInfo.ID),
}
if err := h.hotplugRemoveDevice(drive, blockDev); err != nil {
deviceLogger().WithError(err).Error("Failed to unplug block device")
return err
}
}
return nil
}
func (device *BlockDevice) deviceType() string {
return device.DeviceType
}
// GenericDevice refers to a device that is neither a VFIO device or block device.
type GenericDevice struct {
DeviceType string
DeviceInfo DeviceInfo
}
func newGenericDevice(devInfo DeviceInfo) *GenericDevice {
return &GenericDevice{
DeviceType: DeviceGeneric,
DeviceInfo: devInfo,
}
}
func (device *GenericDevice) attach(h hypervisor, c *Container) error {
return nil
}
func (device *GenericDevice) detach(h hypervisor) error {
return nil
}
func (device *GenericDevice) deviceType() string {
return device.DeviceType
}
// isVFIO checks if the device provided is a vfio group.
func isVFIO(hostPath string) bool {
// Ignore /dev/vfio/vfio character device
if strings.HasPrefix(hostPath, filepath.Join(vfioPath, "vfio")) {
return false
}
if strings.HasPrefix(hostPath, vfioPath) && len(hostPath) > len(vfioPath) {
return true
}
return false
}
// isBlock checks if the device is a block device.
func isBlock(devInfo DeviceInfo) bool {
if devInfo.DevType == "b" {
return true
}
return false
}
func createDevice(devInfo DeviceInfo) Device {
path := devInfo.HostPath
if isVFIO(path) {
return newVFIODevice(devInfo)
} else if isBlock(devInfo) {
return newBlockDevice(devInfo)
} else {
deviceLogger().WithField("device", path).Info("Device has not been passed to the container")
return newGenericDevice(devInfo)
}
}
// GetHostPath is used to fetcg the host path for the device.
// The path passed in the spec refers to the path that should appear inside the container.
// We need to find the actual device path on the host based on the major-minor numbers of the device.
func GetHostPath(devInfo DeviceInfo) (string, error) {
if devInfo.ContainerPath == "" {
return "", fmt.Errorf("Empty path provided for device")
}
var pathComp string
switch devInfo.DevType {
case "c", "u":
pathComp = "char"
case "b":
pathComp = "block"
default:
// Unsupported device types. Return nil error to ignore devices
// that cannot be handled currently.
return "", nil
}
format := strconv.FormatInt(devInfo.Major, 10) + ":" + strconv.FormatInt(devInfo.Minor, 10)
sysDevPath := filepath.Join(sysDevPrefix, pathComp, format, "uevent")
if _, err := os.Stat(sysDevPath); err != nil {
// Some devices(eg. /dev/fuse, /dev/cuse) do not always implement sysfs interface under /sys/dev
// These devices are passed by default by docker.
//
// Simply return the path passed in the device configuration, this does mean that no device renames are
// supported for these devices.
if os.IsNotExist(err) {
return devInfo.ContainerPath, nil
}
return "", err
}
content, err := ini.Load(sysDevPath)
if err != nil {
return "", err
}
devName, err := content.Section("").GetKey("DEVNAME")
if err != nil {
return "", err
}
return filepath.Join("/dev", devName.String()), nil
}
// GetHostPathFunc is function pointer used to mock GetHostPath in tests.
var GetHostPathFunc = GetHostPath
func newDevices(devInfos []DeviceInfo) ([]Device, error) {
var devices []Device
for _, devInfo := range devInfos {
hostPath, err := GetHostPathFunc(devInfo)
if err != nil {
return nil, err
}
devInfo.HostPath = hostPath
device := createDevice(devInfo)
devices = append(devices, device)
}
return devices, nil
}
// getBDF returns the BDF of pci device
// Expected input strng format is [<domain>]:[<bus>][<slot>].[<func>] eg. 0000:02:10.0
func getBDF(deviceSysStr string) (string, error) {
tokens := strings.Split(deviceSysStr, ":")
if len(tokens) != 3 {
return "", fmt.Errorf("Incorrect number of tokens found while parsing bdf for device : %s", deviceSysStr)
}
tokens = strings.SplitN(deviceSysStr, ":", 2)
return tokens[1], nil
}
// bind/unbind paths to aid in SRIOV VF bring-up/restore
var pciDriverUnbindPath = "/sys/bus/pci/devices/%s/driver/unbind"
var pciDriverBindPath = "/sys/bus/pci/drivers/%s/bind"
var vfioRemoveIDPath = "/sys/bus/pci/drivers/vfio-pci/remove_id"
var vfioNewIDPath = "/sys/bus/pci/drivers/vfio-pci/new_id"
// bindDevicetoVFIO binds the device to vfio driver after unbinding from host.
// Will be called by a network interface or a generic pcie device.
func bindDevicetoVFIO(bdf, hostDriver, vendorDeviceID string) error {
// Unbind from the host driver
unbindDriverPath := fmt.Sprintf(pciDriverUnbindPath, bdf)
deviceLogger().WithFields(logrus.Fields{
"device-bdf": bdf,
"driver-path": unbindDriverPath,
}).Info("Unbinding device from driver")
if err := writeToFile(unbindDriverPath, []byte(bdf)); err != nil {
return err
}
// Add device id to vfio driver.
deviceLogger().WithFields(logrus.Fields{
"vendor-device-id": vendorDeviceID,
"vfio-new-id-path": vfioNewIDPath,
}).Info("Writing vendor-device-id to vfio new-id path")
if err := writeToFile(vfioNewIDPath, []byte(vendorDeviceID)); err != nil {
return err
}
// Bind to vfio-pci driver.
bindDriverPath := fmt.Sprintf(pciDriverBindPath, "vfio-pci")
deviceLogger().WithFields(logrus.Fields{
"device-bdf": bdf,
"driver-path": bindDriverPath,
}).Info("Binding device to vfio driver")
// Device may be already bound at this time because of earlier write to new_id, ignore error
writeToFile(bindDriverPath, []byte(bdf))
return nil
}
// bindDevicetoHost binds the device to the host driver driver after unbinding from vfio-pci.
func bindDevicetoHost(bdf, hostDriver, vendorDeviceID string) error {
// Unbind from vfio-pci driver
unbindDriverPath := fmt.Sprintf(pciDriverUnbindPath, bdf)
deviceLogger().WithFields(logrus.Fields{
"device-bdf": bdf,
"driver-path": unbindDriverPath,
}).Info("Unbinding device from driver")
if err := writeToFile(unbindDriverPath, []byte(bdf)); err != nil {
return err
}
// To prevent new VFs from binding to VFIO-PCI, remove_id
if err := writeToFile(vfioRemoveIDPath, []byte(vendorDeviceID)); err != nil {
return err
}
// Bind back to host driver
bindDriverPath := fmt.Sprintf(pciDriverBindPath, hostDriver)
deviceLogger().WithFields(logrus.Fields{
"device-bdf": bdf,
"driver-path": bindDriverPath,
}).Info("Binding back device to host driver")
return writeToFile(bindDriverPath, []byte(bdf))
}

View File

@@ -0,0 +1,402 @@
//
// 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 virtcontainers
import (
"fmt"
"io/ioutil"
"net"
"os"
"path/filepath"
"strconv"
"testing"
"github.com/stretchr/testify/assert"
"github.com/vishvananda/netlink"
)
const fileMode0640 = os.FileMode(0640)
func TestVhostUserSocketPath(t *testing.T) {
// First test case: search for existing:
addresses := []netlink.Addr{
{
IPNet: &net.IPNet{
IP: net.IPv4(192, 168, 0, 2),
Mask: net.IPv4Mask(192, 168, 0, 2),
},
},
{
IPNet: &net.IPNet{
IP: net.IPv4(192, 168, 0, 1),
Mask: net.IPv4Mask(192, 168, 0, 1),
},
},
}
expectedPath := "/tmp/vhostuser_192.168.0.1"
expectedFileName := "vhu.sock"
expectedResult := fmt.Sprintf("%s/%s", expectedPath, expectedFileName)
err := os.Mkdir(expectedPath, 0777)
if err != nil {
t.Fatal(err)
}
_, err = os.Create(expectedResult)
if err != nil {
t.Fatal(err)
}
netinfo := NetworkInfo{
Addrs: addresses,
}
path, _ := vhostUserSocketPath(netinfo)
if path != expectedResult {
t.Fatalf("Got %+v\nExpecting %+v", path, expectedResult)
}
// Second test case: search doesn't include matching vsock:
addressesFalse := []netlink.Addr{
{
IPNet: &net.IPNet{
IP: net.IPv4(192, 168, 0, 4),
Mask: net.IPv4Mask(192, 168, 0, 4),
},
},
}
netinfoFail := NetworkInfo{
Addrs: addressesFalse,
}
path, _ = vhostUserSocketPath(netinfoFail)
if path != "" {
t.Fatalf("Got %+v\nExpecting %+v", path, "")
}
err = os.Remove(expectedResult)
if err != nil {
t.Fatal(err)
}
err = os.Remove(expectedPath)
if err != nil {
t.Fatal(err)
}
}
func TestIsVFIO(t *testing.T) {
type testData struct {
path string
expected bool
}
data := []testData{
{"/dev/vfio/16", true},
{"/dev/vfio/1", true},
{"/dev/vfio/", false},
{"/dev/vfio", false},
{"/dev/vf", false},
{"/dev", false},
{"/dev/vfio/vfio", false},
{"/dev/vfio/vfio/12", false},
}
for _, d := range data {
isVFIO := isVFIO(d.path)
assert.Equal(t, d.expected, isVFIO)
}
}
func TestIsBlock(t *testing.T) {
type testData struct {
devType string
expected bool
}
data := []testData{
{"b", true},
{"c", false},
{"u", false},
}
for _, d := range data {
isBlock := isBlock(DeviceInfo{DevType: d.devType})
assert.Equal(t, d.expected, isBlock)
}
}
func TestCreateDevice(t *testing.T) {
devInfo := DeviceInfo{
HostPath: "/dev/vfio/8",
DevType: "b",
}
device := createDevice(devInfo)
_, ok := device.(*VFIODevice)
assert.True(t, ok)
devInfo.HostPath = "/dev/sda"
device = createDevice(devInfo)
_, ok = device.(*BlockDevice)
assert.True(t, ok)
devInfo.HostPath = "/dev/tty"
devInfo.DevType = "c"
device = createDevice(devInfo)
_, ok = device.(*GenericDevice)
assert.True(t, ok)
}
func TestNewDevices(t *testing.T) {
savedSysDevPrefix := sysDevPrefix
major := int64(252)
minor := int64(3)
tmpDir, err := ioutil.TempDir("", "")
assert.Nil(t, err)
sysDevPrefix = tmpDir
defer func() {
os.RemoveAll(tmpDir)
sysDevPrefix = savedSysDevPrefix
}()
path := "/dev/vfio/2"
deviceInfo := DeviceInfo{
ContainerPath: "",
Major: major,
Minor: minor,
UID: 2,
GID: 2,
DevType: "c",
}
_, err = newDevices([]DeviceInfo{deviceInfo})
assert.NotNil(t, err)
format := strconv.FormatInt(major, 10) + ":" + strconv.FormatInt(minor, 10)
ueventPathPrefix := filepath.Join(sysDevPrefix, "char", format)
ueventPath := filepath.Join(ueventPathPrefix, "uevent")
// Return true for non-existent /sys/dev path.
deviceInfo.ContainerPath = path
_, err = newDevices([]DeviceInfo{deviceInfo})
assert.Nil(t, err)
err = os.MkdirAll(ueventPathPrefix, dirMode)
assert.Nil(t, err)
// Should return error for bad data in uevent file
content := []byte("nonkeyvaluedata")
err = ioutil.WriteFile(ueventPath, content, fileMode0640)
assert.Nil(t, err)
_, err = newDevices([]DeviceInfo{deviceInfo})
assert.NotNil(t, err)
content = []byte("MAJOR=252\nMINOR=3\nDEVNAME=vfio/2")
err = ioutil.WriteFile(ueventPath, content, fileMode0640)
assert.Nil(t, err)
devices, err := newDevices([]DeviceInfo{deviceInfo})
assert.Nil(t, err)
assert.Equal(t, len(devices), 1)
vfioDev, ok := devices[0].(*VFIODevice)
assert.True(t, ok)
assert.Equal(t, vfioDev.DeviceInfo.HostPath, path)
assert.Equal(t, vfioDev.DeviceInfo.ContainerPath, path)
assert.Equal(t, vfioDev.DeviceInfo.DevType, "c")
assert.Equal(t, vfioDev.DeviceInfo.Major, major)
assert.Equal(t, vfioDev.DeviceInfo.Minor, minor)
assert.Equal(t, vfioDev.DeviceInfo.UID, uint32(2))
assert.Equal(t, vfioDev.DeviceInfo.GID, uint32(2))
}
func TestGetBDF(t *testing.T) {
type testData struct {
deviceStr string
expectedBDF string
}
data := []testData{
{"0000:02:10.0", "02:10.0"},
{"0000:0210.0", ""},
{"test", ""},
{"", ""},
}
for _, d := range data {
deviceBDF, err := getBDF(d.deviceStr)
assert.Equal(t, d.expectedBDF, deviceBDF)
if d.expectedBDF == "" {
assert.NotNil(t, err)
} else {
assert.Nil(t, err)
}
}
}
func TestAttachVFIODevice(t *testing.T) {
tmpDir, err := ioutil.TempDir("", "")
assert.Nil(t, err)
os.RemoveAll(tmpDir)
testFDIOGroup := "2"
testDeviceBDFPath := "0000:00:1c.0"
devicesDir := filepath.Join(tmpDir, testFDIOGroup, "devices")
err = os.MkdirAll(devicesDir, dirMode)
assert.Nil(t, err)
deviceFile := filepath.Join(devicesDir, testDeviceBDFPath)
_, err = os.Create(deviceFile)
assert.Nil(t, err)
savedIOMMUPath := sysIOMMUPath
sysIOMMUPath = tmpDir
defer func() {
sysIOMMUPath = savedIOMMUPath
}()
path := filepath.Join(vfioPath, testFDIOGroup)
deviceInfo := DeviceInfo{
HostPath: path,
ContainerPath: path,
DevType: "c",
}
device := createDevice(deviceInfo)
_, ok := device.(*VFIODevice)
assert.True(t, ok)
hypervisor := &mockHypervisor{}
err = device.attach(hypervisor, &Container{})
assert.Nil(t, err)
err = device.detach(hypervisor)
assert.Nil(t, err)
}
func TestAttachGenericDevice(t *testing.T) {
path := "/dev/tty2"
deviceInfo := DeviceInfo{
HostPath: path,
ContainerPath: path,
DevType: "c",
}
device := createDevice(deviceInfo)
_, ok := device.(*GenericDevice)
assert.True(t, ok)
hypervisor := &mockHypervisor{}
err := device.attach(hypervisor, &Container{})
assert.Nil(t, err)
err = device.detach(hypervisor)
assert.Nil(t, err)
}
func TestAttachBlockDevice(t *testing.T) {
fs := &filesystem{}
hypervisor := &mockHypervisor{}
hConfig := HypervisorConfig{
BlockDeviceDriver: VirtioBlock,
}
config := &PodConfig{
HypervisorConfig: hConfig,
}
pod := &Pod{
id: testPodID,
storage: fs,
hypervisor: hypervisor,
config: config,
}
contID := "100"
container := Container{
pod: pod,
id: contID,
}
// create state file
path := filepath.Join(runStoragePath, testPodID, container.ID())
err := os.MkdirAll(path, dirMode)
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(path)
stateFilePath := filepath.Join(path, stateFile)
os.Remove(stateFilePath)
_, err = os.Create(stateFilePath)
if err != nil {
t.Fatal(err)
}
defer os.Remove(stateFilePath)
path = "/dev/hda"
deviceInfo := DeviceInfo{
HostPath: path,
ContainerPath: path,
DevType: "b",
}
device := createDevice(deviceInfo)
_, ok := device.(*BlockDevice)
assert.True(t, ok)
container.state.State = ""
err = device.attach(hypervisor, &container)
assert.Nil(t, err)
err = device.detach(hypervisor)
assert.Nil(t, err)
container.state.State = StateReady
err = device.attach(hypervisor, &container)
assert.Nil(t, err)
err = device.detach(hypervisor)
assert.Nil(t, err)
container.pod.config.HypervisorConfig.BlockDeviceDriver = VirtioSCSI
err = device.attach(hypervisor, &container)
assert.Nil(t, err)
err = device.detach(hypervisor)
assert.Nil(t, err)
container.state.State = StateReady
err = device.attach(hypervisor, &container)
assert.Nil(t, err)
err = device.detach(hypervisor)
assert.Nil(t, err)
}

28
virtcontainers/doc.go Normal file
View File

@@ -0,0 +1,28 @@
//
// Copyright (c) 2016 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 virtcontainers manages hardware virtualized containers.
Each container belongs to a set of containers sharing the same networking
namespace and storage, also known as a pod.
Virtcontainers pods are hardware virtualized, i.e. they run on virtual machines.
Virtcontainers will create one VM per pod, and containers will be created as
processes within the pod VM.
The virtcontainers package manages both pods and containers lifecycles.
*/
package virtcontainers

View File

@@ -0,0 +1,56 @@
Table of Contents
=================
* [Prerequisites](#prerequisites)
* [Building](#building)
* [Testing](#testing)
* [Submitting changes](#submitting-changes)
# Prerequisites
`virtcontainers` has a few prerequisites for development:
- docker
- CNI
- golang
- gometalinter
A number of these can be installed using the
[virtcontainers-setup.sh](../utils/virtcontainers-setup.sh) script.
# Building
To build `virtcontainers`, at the top level directory run:
```bash
# make
```
# Testing
Before testing `virtcontainers`, ensure you have met the [prerequisites](#prerequisites).
Before testing you need to install virtcontainers. The following command will install
`virtcontainers` into its own area (`/usr/bin/virtcontainers/bin/` by default).
```
# sudo -E PATH=$PATH make install
```
To test `virtcontainers`, at the top level run:
```
# make check
```
This will:
- run static code checks on the code base.
- run `go test` unit tests from the code base.
# Submitting changes
For details on the format and how to submit changes, refer to the
[Contributing](CONTRIBUTING.md) document.

View File

@@ -0,0 +1,932 @@
# virtcontainers 1.0 API
The virtcontainers 1.0 API operates on two high level objects:
[Pods](#pod-api) and [containers](#container-api):
* [Pod API](#pod-api)
* [Container API](#container-api)
* [Examples](#examples)
## Pod API
The virtcontainers 1.0 pod API manages hardware virtualized
[pod lifecycles](#pod-functions). The virtcontainers pod
semantics strictly follow the
[Kubernetes](https://kubernetes.io/docs/concepts/workloads/pods/pod/) ones.
The pod API allows callers to [create](#createpod), [delete](#deletepod),
[start](#startpod), [stop](#stoppod), [run](#runpod), [pause](#pausepod),
[resume](resumepod) and [list](#listpod) VM (Virtual Machine) based pods.
To initially create a pod, the API caller must prepare a
[`PodConfig`](#podconfig) and pass it to either [`CreatePod`](#createpod)
or [`RunPod`](#runpod). Upon successful pod creation, the virtcontainers
API will return a [`VCPod`](#vcpod) interface back to the caller.
The `VCPod` interface is a pod abstraction hiding the internal and private
virtcontainers pod structure. It is a handle for API callers to manage the
pod lifecycle through the rest of the [pod API](#pod-functions).
* [Structures](#pod-structures)
* [Functions](#pod-functions)
### Pod Structures
* [PodConfig](#podconfig)
* [Resources](#resources)
* [HypervisorType](#hypervisortype)
* [HypervisorConfig](#hypervisorconfig)
* [AgentType](#agenttype)
* [ProxyType](#proxytype)
* [ProxyConfig](#proxyconfig)
* [ShimType](#shimtype)
* [NetworkModel](#networkmodel)
* [NetworkConfig](#networkconfig)
* [NetInterworkingModel](#netinterworkingmodel)
* [Volume](#volume)
* [ContainerConfig](#containerconfig)
* [Cmd](#cmd)
* [Mount](#mount)
* [DeviceInfo](#deviceinfo)
* [VCPod](#vcpod)
#### `PodConfig`
```Go
// PodConfig is a Pod configuration.
type PodConfig struct {
ID string
Hostname string
// Field specific to OCI specs, needed to setup all the hooks
Hooks Hooks
// VMConfig is the VM configuration to set for this pod.
VMConfig Resources
HypervisorType HypervisorType
HypervisorConfig HypervisorConfig
AgentType AgentType
AgentConfig interface{}
ProxyType ProxyType
ProxyConfig ProxyConfig
ShimType ShimType
ShimConfig interface{}
NetworkModel NetworkModel
NetworkConfig NetworkConfig
// Volumes is a list of shared volumes between the host and the Pod.
Volumes []Volume
// Containers describe the list of containers within a Pod.
// This list can be empty and populated by adding containers
// to the Pod a posteriori.
Containers []ContainerConfig
// Annotations keys must be unique strings and must be name-spaced
// with e.g. reverse domain notation (org.clearlinux.key).
Annotations map[string]string
}
```
##### `Resources`
```Go
// Resources describes VM resources configuration.
type Resources struct {
// VCPUs is the number of available virtual CPUs.
VCPUs uint
// Memory is the amount of available memory in MiB.
Memory uint
}
```
##### `HypervisorType`
```Go
// HypervisorType describes an hypervisor type.
type HypervisorType string
const (
// QemuHypervisor is the QEMU hypervisor.
QemuHypervisor HypervisorType = "qemu"
// MockHypervisor is a mock hypervisor for testing purposes
MockHypervisor HypervisorType = "mock"
)
```
##### `HypervisorConfig`
```Go
// HypervisorConfig is the hypervisor configuration.
type HypervisorConfig struct {
// KernelPath is the guest kernel host path.
KernelPath string
// ImagePath is the guest image host path.
ImagePath string
// FirmwarePath is the bios host path
FirmwarePath string
// MachineAccelerators are machine specific accelerators
MachineAccelerators string
// HypervisorPath is the hypervisor executable host path.
HypervisorPath string
// DisableBlockDeviceUse disallows a block device from being used.
DisableBlockDeviceUse bool
// KernelParams are additional guest kernel parameters.
KernelParams []Param
// HypervisorParams are additional hypervisor parameters.
HypervisorParams []Param
// HypervisorMachineType specifies the type of machine being
// emulated.
HypervisorMachineType string
// Debug changes the default hypervisor and kernel parameters to
// enable debug output where available.
Debug bool
// DefaultVCPUs specifies default number of vCPUs for the VM.
// Pod configuration VMConfig.VCPUs overwrites this.
DefaultVCPUs uint32
// DefaultMem specifies default memory size in MiB for the VM.
// Pod configuration VMConfig.Memory overwrites this.
DefaultMemSz uint32
// DefaultBridges specifies default number of bridges for the VM.
// Bridges can be used to hot plug devices
DefaultBridges uint32
// MemPrealloc specifies if the memory should be pre-allocated
MemPrealloc bool
// HugePages specifies if the memory should be pre-allocated from huge pages
HugePages bool
// Realtime Used to enable/disable realtime
Realtime bool
// Mlock is used to control memory locking when Realtime is enabled
// Realtime=true and Mlock=false, allows for swapping out of VM memory
// enabling higher density
Mlock bool
// DisableNestingChecks is used to override customizations performed
// when running on top of another VMM.
DisableNestingChecks bool
}
```
##### `AgentType`
```Go
// AgentType describes the type of guest agent a Pod should run.
type AgentType string
const (
// NoopAgentType is the No-Op agent.
NoopAgentType AgentType = "noop"
// HyperstartAgent is the Hyper hyperstart agent.
HyperstartAgent AgentType = "hyperstart"
// KataContainersAgent is the Kata Containers agent.
KataContainersAgent AgentType = "kata"
// SocketTypeVSOCK is a VSOCK socket type for talking to an agent.
SocketTypeVSOCK = "vsock"
// SocketTypeUNIX is a UNIX socket type for talking to an agent.
// It typically means the agent is living behind a host proxy.
SocketTypeUNIX = "unix"
)
```
##### `ProxyType`
```Go
// ProxyType describes a proxy type.
type ProxyType string
const (
// NoopProxyType is the noopProxy.
NoopProxyType ProxyType = "noopProxy"
// NoProxyType is the noProxy.
NoProxyType ProxyType = "noProxy"
// CCProxyType is the ccProxy.
CCProxyType ProxyType = "ccProxy"
// KataProxyType is the kataProxy.
KataProxyType ProxyType = "kataProxy"
)
```
##### `ProxyConfig`
```Go
// ProxyConfig is a structure storing information needed from any
// proxy in order to be properly initialized.
type ProxyConfig struct {
Path string
Debug bool
}
```
##### `ShimType`
```Go
// ShimType describes a shim type.
type ShimType string
const (
// CCShimType is the ccShim.
CCShimType ShimType = "ccShim"
// NoopShimType is the noopShim.
NoopShimType ShimType = "noopShim"
// KataShimType is the Kata Containers shim type.
KataShimType ShimType = "kataShim"
)
```
##### `NetworkModel`
```Go
// NetworkModel describes the type of network specification.
type NetworkModel string
const (
// NoopNetworkModel is the No-Op network.
NoopNetworkModel NetworkModel = "noop"
// CNINetworkModel is the CNI network.
CNINetworkModel NetworkModel = "CNI"
// CNMNetworkModel is the CNM network.
CNMNetworkModel NetworkModel = "CNM"
)
```
##### `NetworkConfig`
```Go
// NetworkConfig is the network configuration related to a network.
type NetworkConfig struct {
NetNSPath string
NumInterfaces int
InterworkingModel NetInterworkingModel
}
```
###### `NetInterworkingModel`
```Go
// NetInterworkingModel defines the network model connecting
// the network interface to the virtual machine.
type NetInterworkingModel int
const (
// NetXConnectDefaultModel Ask to use DefaultNetInterworkingModel
NetXConnectDefaultModel NetInterworkingModel = iota
// NetXConnectBridgedModel uses a linux bridge to interconnect
// the container interface to the VM. This is the
// safe default that works for most cases except
// macvlan and ipvlan
NetXConnectBridgedModel
// NetXConnectMacVtapModel can be used when the Container network
// interface can be bridged using macvtap
NetXConnectMacVtapModel
// NetXConnectEnlightenedModel can be used when the Network plugins
// are enlightened to create VM native interfaces
// when requested by the runtime
// This will be used for vethtap, macvtap, ipvtap
NetXConnectEnlightenedModel
// NetXConnectInvalidModel is the last item to check valid values by IsValid()
NetXConnectInvalidModel
)
```
##### `Volume`
```Go
// Volume is a shared volume between the host and the VM,
// defined by its mount tag and its host path.
type Volume struct {
// MountTag is a label used as a hint to the guest.
MountTag string
// HostPath is the host filesystem path for this volume.
HostPath string
}
```
##### `ContainerConfig`
```Go
// ContainerConfig describes one container runtime configuration.
type ContainerConfig struct {
ID string
// RootFs is the container workload image on the host.
RootFs string
// ReadOnlyRootfs indicates if the rootfs should be mounted readonly
ReadonlyRootfs bool
// Cmd specifies the command to run on a container
Cmd Cmd
// Annotations allow clients to store arbitrary values,
// for example to add additional status values required
// to support particular specifications.
Annotations map[string]string
Mounts []Mount
// Device configuration for devices that must be available within the container.
DeviceInfos []DeviceInfo
}
```
###### `Cmd`
```Go
// Cmd represents a command to execute in a running container.
type Cmd struct {
Args []string
Envs []EnvVar
WorkDir string
// Note that these fields *MUST* remain as strings.
//
// The reason being that we want runtimes to be able to support CLI
// operations like "exec --user=". That option allows the
// specification of a user (either as a string username or a numeric
// UID), and may optionally also include a group (groupame or GID).
//
// Since this type is the interface to allow the runtime to specify
// the user and group the workload can run as, these user and group
// fields cannot be encoded as integer values since that would imply
// the runtime itself would need to perform a UID/GID lookup on the
// user-specified username/groupname. But that isn't practically
// possible given that to do so would require the runtime to access
// the image to allow it to interrogate the appropriate databases to
// convert the username/groupnames to UID/GID values.
//
// Note that this argument applies solely to the _runtime_ supporting
// a "--user=" option when running in a "standalone mode" - there is
// no issue when the runtime is called by a container manager since
// all the user and group mapping is handled by the container manager
// and specified to the runtime in terms of UID/GID's in the
// configuration file generated by the container manager.
User string
PrimaryGroup string
SupplementaryGroups []string
Interactive bool
Console string
Detach bool
NoNewPrivileges bool
Capabilities LinuxCapabilities
}
```
###### `Mount`
```Go
// Mount describes a container mount.
type Mount struct {
Source string
Destination string
// Type specifies the type of filesystem to mount.
Type string
// Options list all the mount options of the filesystem.
Options []string
// HostPath used to store host side bind mount path
HostPath string
// ReadOnly specifies if the mount should be read only or not
ReadOnly bool
}
```
###### `DeviceInfo`
```Go
// DeviceInfo is an embedded type that contains device data common to all types of devices.
type DeviceInfo struct {
// Device path on host
HostPath string
// Device path inside the container
ContainerPath string
// Type of device: c, b, u or p
// c , u - character(unbuffered)
// p - FIFO
// b - block(buffered) special file
// More info in mknod(1).
DevType string
// Major, minor numbers for device.
Major int64
Minor int64
// FileMode permission bits for the device.
FileMode os.FileMode
// id of the device owner.
UID uint32
// id of the device group.
GID uint32
// Hotplugged is used to store device state indicating if the
// device was hotplugged.
Hotplugged bool
// ID for the device that is passed to the hypervisor.
ID string
}
```
#### `VCPod`
```Go
// VCPod is the Pod interface
// (required since virtcontainers.Pod only contains private fields)
type VCPod interface {
Annotations(key string) (string, error)
GetAllContainers() []VCContainer
GetAnnotations() map[string]string
GetContainer(containerID string) VCContainer
ID() string
SetAnnotations(annotations map[string]string) error
}
```
### Pod Functions
* [CreatePod](#createpod)
* [DeletePod](#deletepod)
* [StartPod](#startpod)
* [StopPod](#stoppod)
* [RunPod](#runpod)
* [ListPod](#listpod)
* [StatusPod](#statuspod)
* [PausePod](#pausepod)
* [ResumePod](#resumepod)
#### `CreatePod`
```Go
// CreatePod is the virtcontainers pod creation entry point.
// CreatePod creates a pod and its containers. It does not start them.
func CreatePod(podConfig PodConfig) (VCPod, error)
```
#### `DeletePod`
```Go
// DeletePod is the virtcontainers pod deletion entry point.
// DeletePod will stop an already running container and then delete it.
func DeletePod(podID string) (VCPod, error)
```
#### `StartPod`
```Go
// StartPod is the virtcontainers pod starting entry point.
// StartPod will talk to the given hypervisor to start an existing
// pod and all its containers.
func StartPod(podID string) (VCPod, error)
```
#### `StopPod`
```Go
// StopPod is the virtcontainers pod stopping entry point.
// StopPod will talk to the given agent to stop an existing pod
// and destroy all containers within that pod.
func StopPod(podID string) (VCPod, error)
```
#### `RunPod`
```Go
// RunPod is the virtcontainers pod running entry point.
// RunPod creates a pod and its containers and then it starts them.
func RunPod(podConfig PodConfig) (VCPod, error)
```
#### `ListPod`
```Go
// ListPod is the virtcontainers pod listing entry point.
func ListPod() ([]PodStatus, error)
```
#### `StatusPod`
```Go
// StatusPod is the virtcontainers pod status entry point.
func StatusPod(podID string) (PodStatus, error)
```
#### `PausePod`
```Go
// PausePod is the virtcontainers pausing entry point which pauses an
// already running pod.
func PausePod(podID string) (VCPod, error)
```
#### `ResumePod`
```Go
// ResumePod is the virtcontainers resuming entry point which resumes
// (or unpauses) and already paused pod.
func ResumePod(podID string) (VCPod, error)
```
## Container API
The virtcontainers 1.0 container API manages pod
[container lifecycles](#container-functions).
A virtcontainers container is process running inside a containerized
environment, as part of a hardware virtualized context. In other words,
a virtcontainers container is just a regular container running inside a
virtual machine's guest OS.
A virtcontainers container always belong to one and only one
virtcontainers pod, again following the
[Kubernetes](https://kubernetes.io/docs/concepts/workloads/pods/pod-overview/)
logic and semantics.
The container API allows callers to [create](#createcontainer),
[delete](#deletecontainer), [start](#startcontainer), [stop](#stopcontainer),
[kill](#killcontainer) and [observe](#statuscontainer) containers. It also
allows for running [additional processes](#entercontainer) inside a
specific container.
As a virtcontainers container is always linked to a pod, the entire container
API always takes a pod ID as its first argument.
To create a container, the API caller must prepare a
[`ContainerConfig`](#containerconfig) and pass it to
[`CreateContainer`](#createcontainer) together with a pod ID. Upon successful
container creation, the virtcontainers API will return a
[`VCContainer`](#vccontainer) interface back to the caller.
The `VCContainer` interface is a container abstraction hiding the internal
and private virtcontainers container structure. It is a handle for API callers
to manage the container lifecycle through the rest of the
[container API](#container-functions).
* [Structures](#container-structures)
* [Functions](#container-functions)
### Container Structures
* [ContainerConfig](#containerconfig-1)
* [Cmd](#cmd-1)
* [Mount](#mount-1)
* [DeviceInfo](#deviceinfo-1)
* [Process](#process)
* [ContainerStatus](#containerstatus)
* [ProcessListOptions](#processlistoptions)
* [VCContainer](#vccontainer)
#### `ContainerConfig`
```Go
// ContainerConfig describes one container runtime configuration.
type ContainerConfig struct {
ID string
// RootFs is the container workload image on the host.
RootFs string
// ReadOnlyRootfs indicates if the rootfs should be mounted readonly
ReadonlyRootfs bool
// Cmd specifies the command to run on a container
Cmd Cmd
// Annotations allow clients to store arbitrary values,
// for example to add additional status values required
// to support particular specifications.
Annotations map[string]string
Mounts []Mount
// Device configuration for devices that must be available within the container.
DeviceInfos []DeviceInfo
}
```
##### `Cmd`
```Go
// Cmd represents a command to execute in a running container.
type Cmd struct {
Args []string
Envs []EnvVar
WorkDir string
// Note that these fields *MUST* remain as strings.
//
// The reason being that we want runtimes to be able to support CLI
// operations like "exec --user=". That option allows the
// specification of a user (either as a string username or a numeric
// UID), and may optionally also include a group (groupame or GID).
//
// Since this type is the interface to allow the runtime to specify
// the user and group the workload can run as, these user and group
// fields cannot be encoded as integer values since that would imply
// the runtime itself would need to perform a UID/GID lookup on the
// user-specified username/groupname. But that isn't practically
// possible given that to do so would require the runtime to access
// the image to allow it to interrogate the appropriate databases to
// convert the username/groupnames to UID/GID values.
//
// Note that this argument applies solely to the _runtime_ supporting
// a "--user=" option when running in a "standalone mode" - there is
// no issue when the runtime is called by a container manager since
// all the user and group mapping is handled by the container manager
// and specified to the runtime in terms of UID/GID's in the
// configuration file generated by the container manager.
User string
PrimaryGroup string
SupplementaryGroups []string
Interactive bool
Console string
Detach bool
NoNewPrivileges bool
Capabilities LinuxCapabilities
}
```
##### `Mount`
```Go
// Mount describes a container mount.
type Mount struct {
Source string
Destination string
// Type specifies the type of filesystem to mount.
Type string
// Options list all the mount options of the filesystem.
Options []string
// HostPath used to store host side bind mount path
HostPath string
// ReadOnly specifies if the mount should be read only or not
ReadOnly bool
}
```
##### `DeviceInfo`
```Go
// DeviceInfo is an embedded type that contains device data common to all types of devices.
type DeviceInfo struct {
// Device path on host
HostPath string
// Device path inside the container
ContainerPath string
// Type of device: c, b, u or p
// c , u - character(unbuffered)
// p - FIFO
// b - block(buffered) special file
// More info in mknod(1).
DevType string
// Major, minor numbers for device.
Major int64
Minor int64
// FileMode permission bits for the device.
FileMode os.FileMode
// id of the device owner.
UID uint32
// id of the device group.
GID uint32
// Hotplugged is used to store device state indicating if the
// device was hotplugged.
Hotplugged bool
// ID for the device that is passed to the hypervisor.
ID string
}
```
#### `Process`
```Go
// Process gathers data related to a container process.
type Process struct {
// Token is the process execution context ID. It must be
// unique per pod.
// Token is used to manipulate processes for containers
// that have not started yet, and later identify them
// uniquely within a pod.
Token string
// Pid is the process ID as seen by the host software
// stack, e.g. CRI-O, containerd. This is typically the
// shim PID.
Pid int
StartTime time.Time
}
```
#### `ContainerStatus`
```Go
// ContainerStatus describes a container status.
type ContainerStatus struct {
ID string
State State
PID int
StartTime time.Time
RootFs string
// Annotations allow clients to store arbitrary values,
// for example to add additional status values required
// to support particular specifications.
Annotations map[string]string
}
```
#### `ProcessListOptions`
```Go
// ProcessListOptions contains the options used to list running
// processes inside the container
type ProcessListOptions struct {
// Format describes the output format to list the running processes.
// Formats are unrelated to ps(1) formats, only two formats can be specified:
// "json" and "table"
Format string
// Args contains the list of arguments to run ps(1) command.
// If Args is empty the agent will use "-ef" as options to ps(1).
Args []string
}
```
#### `VCContainer`
```Go
// VCContainer is the Container interface
// (required since virtcontainers.Container only contains private fields)
type VCContainer interface {
GetAnnotations() map[string]string
GetPid() int
GetToken() string
ID() string
Pod() VCPod
Process() Process
SetPid(pid int) error
}
```
### Container Functions
* [CreateContainer](#createcontainer)
* [DeleteContainer](#deletecontainer)
* [StartContainer](#startcontainer)
* [StopContainer](#stopcontainer)
* [EnterContainer](#entercontainer)
* [StatusContainer](#statuscontainer)
* [KillContainer](#killcontainer)
* [ProcessListContainer](#processlistcontainer)
#### `CreateContainer`
```Go
// CreateContainer is the virtcontainers container creation entry point.
// CreateContainer creates a container on a given pod.
func CreateContainer(podID string, containerConfig ContainerConfig) (VCPod, VCContainer, error)
```
#### `DeleteContainer`
```Go
// DeleteContainer is the virtcontainers container deletion entry point.
// DeleteContainer deletes a Container from a Pod. If the container is running,
// it needs to be stopped first.
func DeleteContainer(podID, containerID string) (VCContainer, error)
```
#### `StartContainer`
```Go
// StartContainer is the virtcontainers container starting entry point.
// StartContainer starts an already created container.
func StartContainer(podID, containerID string) (VCContainer, error)
```
#### `StopContainer`
```Go
// StopContainer is the virtcontainers container stopping entry point.
// StopContainer stops an already running container.
func StopContainer(podID, containerID string) (VCContainer, error)
```
#### `EnterContainer`
```Go
// EnterContainer is the virtcontainers container command execution entry point.
// EnterContainer enters an already running container and runs a given command.
func EnterContainer(podID, containerID string, cmd Cmd) (VCPod, VCContainer, *Process, error)
```
#### `StatusContainer`
```Go
// StatusContainer is the virtcontainers container status entry point.
// StatusContainer returns a detailed container status.
func StatusContainer(podID, containerID string) (ContainerStatus, error)
```
#### `KillContainer`
```Go
// KillContainer is the virtcontainers entry point to send a signal
// to a container running inside a pod. If all is true, all processes in
// the container will be sent the signal.
func KillContainer(podID, containerID string, signal syscall.Signal, all bool) error
```
#### `ProcessListContainer`
```Go
// ProcessListContainer is the virtcontainers entry point to list
// processes running inside a container
func ProcessListContainer(podID, containerID string, options ProcessListOptions) (ProcessList, error)
```
## Examples
### Preparing and running a pod
```Go
// This example creates and starts a single container pod,
// using qemu as the hypervisor and hyperstart as the VM agent.
func Example_createAndStartPod() {
envs := []vc.EnvVar{
{
Var: "PATH",
Value: "/bin:/usr/bin:/sbin:/usr/sbin",
},
}
cmd := vc.Cmd{
Args: strings.Split("/bin/sh", " "),
Envs: envs,
WorkDir: "/",
}
// Define the container command and bundle.
container := vc.ContainerConfig{
ID: "1",
RootFs: containerRootfs,
Cmd: cmd,
}
// Sets the hypervisor configuration.
hypervisorConfig := vc.HypervisorConfig{
KernelPath: "/usr/share/clear-containers/vmlinux.container",
ImagePath: "/usr/share/clear-containers/clear-containers.img",
HypervisorPath: "/usr/bin/qemu-lite-system-x86_64",
}
// Use hyperstart default values for the agent.
agConfig := vc.HyperConfig{}
// VM resources
vmConfig := vc.Resources{
VCPUs: 4,
Memory: 1024,
}
// The pod configuration:
// - One container
// - Hypervisor is QEMU
// - Agent is hyperstart
podConfig := vc.PodConfig{
VMConfig: vmConfig,
HypervisorType: vc.QemuHypervisor,
HypervisorConfig: hypervisorConfig,
AgentType: vc.HyperstartAgent,
AgentConfig: agConfig,
Containers: []vc.ContainerConfig{container},
}
_, err := vc.RunPod(podConfig)
if err != nil {
fmt.Printf("Could not run pod: %s", err)
}
return
}
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

31
virtcontainers/errors.go Normal file
View File

@@ -0,0 +1,31 @@
//
// 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 virtcontainers
import (
"errors"
)
// common error objects used for argument checking
var (
errNeedPod = errors.New("Pod must be specified")
errNeedPodID = errors.New("Pod ID cannot be empty")
errNeedContainerID = errors.New("Container ID cannot be empty")
errNeedFile = errors.New("File cannot be empty")
errNeedState = errors.New("State cannot be empty")
errInvalidResource = errors.New("Invalid pod resource")
)

View File

@@ -0,0 +1,88 @@
//
// Copyright (c) 2016 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 virtcontainers_test
import (
"fmt"
"strings"
vc "github.com/containers/virtcontainers"
)
const containerRootfs = "/var/lib/container/bundle/"
// This example creates and starts a single container pod,
// using qemu as the hypervisor and hyperstart as the VM agent.
func Example_createAndStartPod() {
envs := []vc.EnvVar{
{
Var: "PATH",
Value: "/bin:/usr/bin:/sbin:/usr/sbin",
},
}
cmd := vc.Cmd{
Args: strings.Split("/bin/sh", " "),
Envs: envs,
WorkDir: "/",
}
// Define the container command and bundle.
container := vc.ContainerConfig{
ID: "1",
RootFs: containerRootfs,
Cmd: cmd,
}
// Sets the hypervisor configuration.
hypervisorConfig := vc.HypervisorConfig{
KernelPath: "/usr/share/clear-containers/vmlinux.container",
ImagePath: "/usr/share/clear-containers/clear-containers.img",
HypervisorPath: "/usr/bin/qemu-lite-system-x86_64",
}
// Use hyperstart default values for the agent.
agConfig := vc.HyperConfig{}
// VM resources
vmConfig := vc.Resources{
Memory: 1024,
}
// The pod configuration:
// - One container
// - Hypervisor is QEMU
// - Agent is hyperstart
podConfig := vc.PodConfig{
VMConfig: vmConfig,
HypervisorType: vc.QemuHypervisor,
HypervisorConfig: hypervisorConfig,
AgentType: vc.HyperstartAgent,
AgentConfig: agConfig,
Containers: []vc.ContainerConfig{container},
}
_, err := vc.RunPod(podConfig)
if err != nil {
fmt.Printf("Could not run pod: %s", err)
}
return
}

View File

@@ -0,0 +1,767 @@
//
// Copyright (c) 2016 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 virtcontainers
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"github.com/sirupsen/logrus"
)
// podResource is an int representing a pod resource type.
//
// Note that some are specific to the pod itself and others can apply to
// pods and containers.
type podResource int
const (
// configFileType represents a configuration file type
configFileType podResource = iota
// stateFileType represents a state file type
stateFileType
// networkFileType represents a network file type (pod only)
networkFileType
// hypervisorFileType represents a hypervisor file type (pod only)
hypervisorFileType
// agentFileType represents an agent file type (pod only)
agentFileType
// processFileType represents a process file type
processFileType
// lockFileType represents a lock file type (pod only)
lockFileType
// mountsFileType represents a mount file type
mountsFileType
// devicesFileType represents a device file type
devicesFileType
)
// configFile is the file name used for every JSON pod configuration.
const configFile = "config.json"
// stateFile is the file name storing a pod state.
const stateFile = "state.json"
// networkFile is the file name storing a pod network.
const networkFile = "network.json"
// hypervisorFile is the file name storing a hypervisor's state.
const hypervisorFile = "hypervisor.json"
// agentFile is the file name storing an agent's state.
const agentFile = "agent.json"
// processFile is the file name storing a container process.
const processFile = "process.json"
// lockFile is the file name locking the usage of a pod.
const lockFileName = "lock"
const mountsFile = "mounts.json"
// devicesFile is the file name storing a container's devices.
const devicesFile = "devices.json"
// dirMode is the permission bits used for creating a directory
const dirMode = os.FileMode(0750) | os.ModeDir
// storagePathSuffix is the suffix used for all storage paths
const storagePathSuffix = "/virtcontainers/pods"
// configStoragePath is the pod configuration directory.
// It will contain one config.json file for each created pod.
var configStoragePath = filepath.Join("/var/lib", storagePathSuffix)
// runStoragePath is the pod runtime directory.
// It will contain one state.json and one lock file for each created pod.
var runStoragePath = filepath.Join("/run", storagePathSuffix)
// resourceStorage is the virtcontainers resources (configuration, state, etc...)
// storage interface.
// The default resource storage implementation is filesystem.
type resourceStorage interface {
// Create all resources for a pod
createAllResources(pod Pod) error
// Resources URIs functions return both the URI
// for the actual resource and the URI base.
containerURI(podID, containerID string, resource podResource) (string, string, error)
podURI(podID string, resource podResource) (string, string, error)
// Pod resources
storePodResource(podID string, resource podResource, data interface{}) error
deletePodResources(podID string, resources []podResource) error
fetchPodConfig(podID string) (PodConfig, error)
fetchPodState(podID string) (State, error)
fetchPodNetwork(podID string) (NetworkNamespace, error)
storePodNetwork(podID string, networkNS NetworkNamespace) error
// Hypervisor resources
fetchHypervisorState(podID string, state interface{}) error
storeHypervisorState(podID string, state interface{}) error
// Agent resources
fetchAgentState(podID string, state interface{}) error
storeAgentState(podID string, state interface{}) error
// Container resources
storeContainerResource(podID, containerID string, resource podResource, data interface{}) error
deleteContainerResources(podID, containerID string, resources []podResource) error
fetchContainerConfig(podID, containerID string) (ContainerConfig, error)
fetchContainerState(podID, containerID string) (State, error)
fetchContainerProcess(podID, containerID string) (Process, error)
storeContainerProcess(podID, containerID string, process Process) error
fetchContainerMounts(podID, containerID string) ([]Mount, error)
storeContainerMounts(podID, containerID string, mounts []Mount) error
fetchContainerDevices(podID, containerID string) ([]Device, error)
storeContainerDevices(podID, containerID string, devices []Device) error
}
// filesystem is a resourceStorage interface implementation for a local filesystem.
type filesystem struct {
}
// Logger returns a logrus logger appropriate for logging filesystem messages
func (fs *filesystem) Logger() *logrus.Entry {
return virtLog.WithField("subsystem", "filesystem")
}
func (fs *filesystem) createAllResources(pod Pod) (err error) {
for _, resource := range []podResource{stateFileType, configFileType} {
_, path, _ := fs.podURI(pod.id, resource)
err = os.MkdirAll(path, dirMode)
if err != nil {
return err
}
}
for _, container := range pod.containers {
for _, resource := range []podResource{stateFileType, configFileType} {
_, path, _ := fs.containerURI(pod.id, container.id, resource)
err = os.MkdirAll(path, dirMode)
if err != nil {
fs.deletePodResources(pod.id, nil)
return err
}
}
}
podlockFile, _, err := fs.podURI(pod.id, lockFileType)
if err != nil {
fs.deletePodResources(pod.id, nil)
return err
}
_, err = os.Stat(podlockFile)
if err != nil {
lockFile, err := os.Create(podlockFile)
if err != nil {
fs.deletePodResources(pod.id, nil)
return err
}
lockFile.Close()
}
return nil
}
func (fs *filesystem) storeFile(file string, data interface{}) error {
if file == "" {
return errNeedFile
}
f, err := os.Create(file)
if err != nil {
return err
}
defer f.Close()
jsonOut, err := json.Marshal(data)
if err != nil {
return fmt.Errorf("Could not marshall data: %s", err)
}
f.Write(jsonOut)
return nil
}
// TypedDevice is used as an intermediate representation for marshalling
// and unmarshalling Device implementations.
type TypedDevice struct {
Type string
// Data is assigned the Device object.
// This being declared as RawMessage prevents it from being marshalled/unmarshalled.
// We do that explicitly depending on Type.
Data json.RawMessage
}
// storeDeviceFile is used to provide custom marshalling for Device objects.
// Device is first marshalled into TypedDevice to include the type
// of the Device object.
func (fs *filesystem) storeDeviceFile(file string, data interface{}) error {
if file == "" {
return errNeedFile
}
f, err := os.Create(file)
if err != nil {
return err
}
defer f.Close()
devices, ok := data.([]Device)
if !ok {
return fmt.Errorf("Incorrect data type received, Expected []Device")
}
var typedDevices []TypedDevice
for _, d := range devices {
tempJSON, _ := json.Marshal(d)
typedDevice := TypedDevice{
Type: d.deviceType(),
Data: tempJSON,
}
typedDevices = append(typedDevices, typedDevice)
}
jsonOut, err := json.Marshal(typedDevices)
if err != nil {
return fmt.Errorf("Could not marshal devices: %s", err)
}
if _, err := f.Write(jsonOut); err != nil {
return err
}
return nil
}
func (fs *filesystem) fetchFile(file string, resource podResource, data interface{}) error {
if file == "" {
return errNeedFile
}
fileData, err := ioutil.ReadFile(file)
if err != nil {
return err
}
switch resource {
case devicesFileType:
devices, ok := data.(*[]Device)
if !ok {
return fmt.Errorf("Could not cast %v into *[]Device type", data)
}
return fs.fetchDeviceFile(fileData, devices)
}
return json.Unmarshal(fileData, data)
}
// fetchDeviceFile is used for custom unmarshalling of device interface objects.
func (fs *filesystem) fetchDeviceFile(fileData []byte, devices *[]Device) error {
var typedDevices []TypedDevice
if err := json.Unmarshal(fileData, &typedDevices); err != nil {
return err
}
var tempDevices []Device
for _, d := range typedDevices {
l := fs.Logger().WithField("device-type", d.Type)
l.Info("Device type found")
switch d.Type {
case DeviceVFIO:
var device VFIODevice
if err := json.Unmarshal(d.Data, &device); err != nil {
return err
}
tempDevices = append(tempDevices, &device)
l.Infof("VFIO device unmarshalled [%v]", device)
case DeviceBlock:
var device BlockDevice
if err := json.Unmarshal(d.Data, &device); err != nil {
return err
}
tempDevices = append(tempDevices, &device)
l.Infof("Block Device unmarshalled [%v]", device)
case DeviceGeneric:
var device GenericDevice
if err := json.Unmarshal(d.Data, &device); err != nil {
return err
}
tempDevices = append(tempDevices, &device)
l.Infof("Generic device unmarshalled [%v]", device)
default:
return fmt.Errorf("Unknown device type, could not unmarshal")
}
}
*devices = tempDevices
return nil
}
// resourceNeedsContainerID determines if the specified
// podResource needs a containerID. Since some podResources can
// be used for both pods and containers, it is necessary to specify
// whether the resource is being used in a pod-specific context using
// the podSpecific parameter.
func resourceNeedsContainerID(podSpecific bool, resource podResource) bool {
switch resource {
case lockFileType, networkFileType, hypervisorFileType, agentFileType:
// pod-specific resources
return false
default:
return !podSpecific
}
}
func resourceDir(podSpecific bool, podID, containerID string, resource podResource) (string, error) {
if podID == "" {
return "", errNeedPodID
}
if resourceNeedsContainerID(podSpecific, resource) == true && containerID == "" {
return "", errNeedContainerID
}
var path string
switch resource {
case configFileType:
path = configStoragePath
break
case stateFileType, networkFileType, processFileType, lockFileType, mountsFileType, devicesFileType, hypervisorFileType, agentFileType:
path = runStoragePath
break
default:
return "", errInvalidResource
}
dirPath := filepath.Join(path, podID, containerID)
return dirPath, nil
}
// If podSpecific is true, the resource is being applied for an empty
// pod (meaning containerID may be blank).
// Note that this function defers determining if containerID can be
// blank to resourceDIR()
func (fs *filesystem) resourceURI(podSpecific bool, podID, containerID string, resource podResource) (string, string, error) {
if podID == "" {
return "", "", errNeedPodID
}
var filename string
dirPath, err := resourceDir(podSpecific, podID, containerID, resource)
if err != nil {
return "", "", err
}
switch resource {
case configFileType:
filename = configFile
break
case stateFileType:
filename = stateFile
case networkFileType:
filename = networkFile
case hypervisorFileType:
filename = hypervisorFile
case agentFileType:
filename = agentFile
case processFileType:
filename = processFile
case lockFileType:
filename = lockFileName
break
case mountsFileType:
filename = mountsFile
break
case devicesFileType:
filename = devicesFile
break
default:
return "", "", errInvalidResource
}
filePath := filepath.Join(dirPath, filename)
return filePath, dirPath, nil
}
func (fs *filesystem) containerURI(podID, containerID string, resource podResource) (string, string, error) {
if podID == "" {
return "", "", errNeedPodID
}
if containerID == "" {
return "", "", errNeedContainerID
}
return fs.resourceURI(false, podID, containerID, resource)
}
func (fs *filesystem) podURI(podID string, resource podResource) (string, string, error) {
return fs.resourceURI(true, podID, "", resource)
}
// commonResourceChecks performs basic checks common to both setting and
// getting a podResource.
func (fs *filesystem) commonResourceChecks(podSpecific bool, podID, containerID string, resource podResource) error {
if podID == "" {
return errNeedPodID
}
if resourceNeedsContainerID(podSpecific, resource) == true && containerID == "" {
return errNeedContainerID
}
switch resource {
case configFileType:
case stateFileType:
case networkFileType:
case hypervisorFileType:
case agentFileType:
case processFileType:
case mountsFileType:
case devicesFileType:
default:
return errInvalidResource
}
return nil
}
func (fs *filesystem) storePodAndContainerConfigResource(podSpecific bool, podID, containerID string, resource podResource, file interface{}) error {
if resource != configFileType {
return errInvalidResource
}
configFile, _, err := fs.resourceURI(podSpecific, podID, containerID, configFileType)
if err != nil {
return err
}
return fs.storeFile(configFile, file)
}
func (fs *filesystem) storeStateResource(podSpecific bool, podID, containerID string, resource podResource, file interface{}) error {
if resource != stateFileType {
return errInvalidResource
}
stateFile, _, err := fs.resourceURI(podSpecific, podID, containerID, stateFileType)
if err != nil {
return err
}
return fs.storeFile(stateFile, file)
}
func (fs *filesystem) storeNetworkResource(podSpecific bool, podID, containerID string, resource podResource, file interface{}) error {
if resource != networkFileType {
return errInvalidResource
}
// pod only resource
networkFile, _, err := fs.resourceURI(true, podID, containerID, networkFileType)
if err != nil {
return err
}
return fs.storeFile(networkFile, file)
}
func (fs *filesystem) storeProcessResource(podSpecific bool, podID, containerID string, resource podResource, file interface{}) error {
if resource != processFileType {
return errInvalidResource
}
processFile, _, err := fs.resourceURI(podSpecific, podID, containerID, processFileType)
if err != nil {
return err
}
return fs.storeFile(processFile, file)
}
func (fs *filesystem) storeMountResource(podSpecific bool, podID, containerID string, resource podResource, file interface{}) error {
if resource != mountsFileType {
return errInvalidResource
}
mountsFile, _, err := fs.resourceURI(podSpecific, podID, containerID, mountsFileType)
if err != nil {
return err
}
return fs.storeFile(mountsFile, file)
}
func (fs *filesystem) storeDeviceResource(podSpecific bool, podID, containerID string, resource podResource, file interface{}) error {
if resource != devicesFileType {
return errInvalidResource
}
devicesFile, _, err := fs.resourceURI(podSpecific, podID, containerID, devicesFileType)
if err != nil {
return err
}
return fs.storeDeviceFile(devicesFile, file)
}
func (fs *filesystem) storeResource(podSpecific bool, podID, containerID string, resource podResource, data interface{}) error {
if err := fs.commonResourceChecks(podSpecific, podID, containerID, resource); err != nil {
return err
}
switch file := data.(type) {
case PodConfig, ContainerConfig:
return fs.storePodAndContainerConfigResource(podSpecific, podID, containerID, resource, file)
case State:
return fs.storeStateResource(podSpecific, podID, containerID, resource, file)
case NetworkNamespace:
return fs.storeNetworkResource(podSpecific, podID, containerID, resource, file)
case Process:
return fs.storeProcessResource(podSpecific, podID, containerID, resource, file)
case []Mount:
return fs.storeMountResource(podSpecific, podID, containerID, resource, file)
case []Device:
return fs.storeDeviceResource(podSpecific, podID, containerID, resource, file)
default:
return fmt.Errorf("Invalid resource data type")
}
}
func (fs *filesystem) fetchResource(podSpecific bool, podID, containerID string, resource podResource, data interface{}) error {
if err := fs.commonResourceChecks(podSpecific, podID, containerID, resource); err != nil {
return err
}
path, _, err := fs.resourceURI(podSpecific, podID, containerID, resource)
if err != nil {
return err
}
return fs.fetchFile(path, resource, data)
}
func (fs *filesystem) storePodResource(podID string, resource podResource, data interface{}) error {
return fs.storeResource(true, podID, "", resource, data)
}
func (fs *filesystem) fetchPodConfig(podID string) (PodConfig, error) {
var podConfig PodConfig
if err := fs.fetchResource(true, podID, "", configFileType, &podConfig); err != nil {
return PodConfig{}, err
}
return podConfig, nil
}
func (fs *filesystem) fetchPodState(podID string) (State, error) {
var state State
if err := fs.fetchResource(true, podID, "", stateFileType, &state); err != nil {
return State{}, err
}
return state, nil
}
func (fs *filesystem) fetchPodNetwork(podID string) (NetworkNamespace, error) {
var networkNS NetworkNamespace
if err := fs.fetchResource(true, podID, "", networkFileType, &networkNS); err != nil {
return NetworkNamespace{}, err
}
return networkNS, nil
}
func (fs *filesystem) fetchHypervisorState(podID string, state interface{}) error {
return fs.fetchResource(true, podID, "", hypervisorFileType, state)
}
func (fs *filesystem) fetchAgentState(podID string, state interface{}) error {
return fs.fetchResource(true, podID, "", agentFileType, state)
}
func (fs *filesystem) storePodNetwork(podID string, networkNS NetworkNamespace) error {
return fs.storePodResource(podID, networkFileType, networkNS)
}
func (fs *filesystem) storeHypervisorState(podID string, state interface{}) error {
hypervisorFile, _, err := fs.resourceURI(true, podID, "", hypervisorFileType)
if err != nil {
return err
}
return fs.storeFile(hypervisorFile, state)
}
func (fs *filesystem) storeAgentState(podID string, state interface{}) error {
agentFile, _, err := fs.resourceURI(true, podID, "", agentFileType)
if err != nil {
return err
}
return fs.storeFile(agentFile, state)
}
func (fs *filesystem) deletePodResources(podID string, resources []podResource) error {
if resources == nil {
resources = []podResource{configFileType, stateFileType}
}
for _, resource := range resources {
_, dir, err := fs.podURI(podID, resource)
if err != nil {
return err
}
err = os.RemoveAll(dir)
if err != nil {
return err
}
}
return nil
}
func (fs *filesystem) storeContainerResource(podID, containerID string, resource podResource, data interface{}) error {
if podID == "" {
return errNeedPodID
}
if containerID == "" {
return errNeedContainerID
}
return fs.storeResource(false, podID, containerID, resource, data)
}
func (fs *filesystem) fetchContainerConfig(podID, containerID string) (ContainerConfig, error) {
var config ContainerConfig
if err := fs.fetchResource(false, podID, containerID, configFileType, &config); err != nil {
return ContainerConfig{}, err
}
return config, nil
}
func (fs *filesystem) fetchContainerState(podID, containerID string) (State, error) {
var state State
if err := fs.fetchResource(false, podID, containerID, stateFileType, &state); err != nil {
return State{}, err
}
return state, nil
}
func (fs *filesystem) fetchContainerProcess(podID, containerID string) (Process, error) {
var process Process
if err := fs.fetchResource(false, podID, containerID, processFileType, &process); err != nil {
return Process{}, err
}
return process, nil
}
func (fs *filesystem) storeContainerProcess(podID, containerID string, process Process) error {
return fs.storeContainerResource(podID, containerID, processFileType, process)
}
func (fs *filesystem) fetchContainerMounts(podID, containerID string) ([]Mount, error) {
var mounts []Mount
if err := fs.fetchResource(false, podID, containerID, mountsFileType, &mounts); err != nil {
return []Mount{}, err
}
return mounts, nil
}
func (fs *filesystem) fetchContainerDevices(podID, containerID string) ([]Device, error) {
var devices []Device
if err := fs.fetchResource(false, podID, containerID, devicesFileType, &devices); err != nil {
return []Device{}, err
}
return devices, nil
}
func (fs *filesystem) storeContainerMounts(podID, containerID string, mounts []Mount) error {
return fs.storeContainerResource(podID, containerID, mountsFileType, mounts)
}
func (fs *filesystem) storeContainerDevices(podID, containerID string, devices []Device) error {
return fs.storeContainerResource(podID, containerID, devicesFileType, devices)
}
func (fs *filesystem) deleteContainerResources(podID, containerID string, resources []podResource) error {
if resources == nil {
resources = []podResource{configFileType, stateFileType}
}
for _, resource := range resources {
_, dir, err := fs.podURI(podID, resource)
if err != nil {
return err
}
containerDir := filepath.Join(dir, containerID, "/")
err = os.RemoveAll(containerDir)
if err != nil {
return err
}
}
return nil
}

View File

@@ -0,0 +1,571 @@
//
// Copyright (c) 2016 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 virtcontainers
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"reflect"
"testing"
)
func TestFilesystemCreateAllResourcesSuccessful(t *testing.T) {
fs := &filesystem{}
contConfigs := []ContainerConfig{
{ID: "1"},
{ID: "10"},
{ID: "100"},
}
podConfig := &PodConfig{
Containers: contConfigs,
}
pod := Pod{
id: testPodID,
storage: fs,
config: podConfig,
}
if err := pod.newContainers(); err != nil {
t.Fatal(err)
}
podConfigPath := filepath.Join(configStoragePath, testPodID)
podRunPath := filepath.Join(runStoragePath, testPodID)
os.RemoveAll(podConfigPath)
os.RemoveAll(podRunPath)
for _, container := range contConfigs {
configPath := filepath.Join(configStoragePath, testPodID, container.ID)
os.RemoveAll(configPath)
runPath := filepath.Join(runStoragePath, testPodID, container.ID)
os.RemoveAll(runPath)
}
err := fs.createAllResources(pod)
if err != nil {
t.Fatal(err)
}
// Check resources
_, err = os.Stat(podConfigPath)
if err != nil {
t.Fatal(err)
}
_, err = os.Stat(podRunPath)
if err != nil {
t.Fatal(err)
}
for _, container := range contConfigs {
configPath := filepath.Join(configStoragePath, testPodID, container.ID)
s, err := os.Stat(configPath)
if err != nil {
t.Fatal(err)
}
// Check we created the dirs with the correct mode
if s.Mode() != dirMode {
t.Fatal(fmt.Errorf("dirmode [%v] != expected [%v]", s.Mode(), dirMode))
}
runPath := filepath.Join(runStoragePath, testPodID, container.ID)
s, err = os.Stat(runPath)
if err != nil {
t.Fatal(err)
}
// Check we created the dirs with the correct mode
if s.Mode() != dirMode {
t.Fatal(fmt.Errorf("dirmode [%v] != expected [%v]", s.Mode(), dirMode))
}
}
}
func TestFilesystemCreateAllResourcesFailingPodIDEmpty(t *testing.T) {
fs := &filesystem{}
pod := Pod{}
err := fs.createAllResources(pod)
if err == nil {
t.Fatal()
}
}
func TestFilesystemCreateAllResourcesFailingContainerIDEmpty(t *testing.T) {
fs := &filesystem{}
containers := []*Container{
{id: ""},
}
pod := Pod{
id: testPodID,
containers: containers,
}
err := fs.createAllResources(pod)
if err == nil {
t.Fatal()
}
}
type TestNoopStructure struct {
Field1 string
Field2 string
}
func TestFilesystemStoreFileSuccessfulNotExisting(t *testing.T) {
fs := &filesystem{}
path := filepath.Join(testDir, "testFilesystem")
os.Remove(path)
data := TestNoopStructure{
Field1: "value1",
Field2: "value2",
}
expected := "{\"Field1\":\"value1\",\"Field2\":\"value2\"}"
err := fs.storeFile(path, data)
if err != nil {
t.Fatal(err)
}
fileData, err := ioutil.ReadFile(path)
if err != nil {
t.Fatal(err)
}
if string(fileData) != expected {
t.Fatal()
}
}
func TestFilesystemStoreFileSuccessfulExisting(t *testing.T) {
fs := &filesystem{}
path := filepath.Join(testDir, "testFilesystem")
os.Remove(path)
f, err := os.Create(path)
if err != nil {
t.Fatal(err)
}
f.Close()
data := TestNoopStructure{
Field1: "value1",
Field2: "value2",
}
expected := "{\"Field1\":\"value1\",\"Field2\":\"value2\"}"
err = fs.storeFile(path, data)
if err != nil {
t.Fatal(err)
}
fileData, err := ioutil.ReadFile(path)
if err != nil {
t.Fatal(err)
}
if string(fileData) != expected {
t.Fatal()
}
}
func TestFilesystemStoreFileFailingMarshalling(t *testing.T) {
fs := &filesystem{}
path := filepath.Join(testDir, "testFilesystem")
os.Remove(path)
data := make(chan bool)
err := fs.storeFile(path, data)
if err == nil {
t.Fatal()
}
}
func TestFilesystemFetchFileSuccessful(t *testing.T) {
fs := &filesystem{}
data := TestNoopStructure{}
path := filepath.Join(testDir, "testFilesystem")
os.Remove(path)
f, err := os.Create(path)
if err != nil {
t.Fatal(err)
}
dataToWrite := "{\"Field1\":\"value1\",\"Field2\":\"value2\"}"
n, err := f.WriteString(dataToWrite)
if err != nil || n != len(dataToWrite) {
f.Close()
t.Fatal(err)
}
f.Close()
err = fs.fetchFile(path, podResource(-1), &data)
if err != nil {
t.Fatal(err)
}
expected := TestNoopStructure{
Field1: "value1",
Field2: "value2",
}
if reflect.DeepEqual(data, expected) == false {
t.Fatal()
}
}
func TestFilesystemFetchFileFailingNoFile(t *testing.T) {
fs := &filesystem{}
data := TestNoopStructure{}
path := filepath.Join(testDir, "testFilesystem")
os.Remove(path)
err := fs.fetchFile(path, podResource(-1), &data)
if err == nil {
t.Fatal()
}
}
func TestFilesystemFetchFileFailingUnMarshalling(t *testing.T) {
fs := &filesystem{}
data := TestNoopStructure{}
path := filepath.Join(testDir, "testFilesystem")
os.Remove(path)
f, err := os.Create(path)
if err != nil {
t.Fatal(err)
}
f.Close()
err = fs.fetchFile(path, podResource(-1), data)
if err == nil {
t.Fatal()
}
}
func TestFilesystemFetchContainerConfigSuccessful(t *testing.T) {
fs := &filesystem{}
contID := "100"
rootFs := "rootfs"
contConfigDir := filepath.Join(configStoragePath, testPodID, contID)
os.MkdirAll(contConfigDir, dirMode)
path := filepath.Join(contConfigDir, configFile)
os.Remove(path)
f, err := os.Create(path)
if err != nil {
t.Fatal(err)
}
configData := fmt.Sprintf("{\"ID\":\"%s\",\"RootFs\":\"%s\"}", contID, rootFs)
n, err := f.WriteString(configData)
if err != nil || n != len(configData) {
f.Close()
t.Fatal(err)
}
f.Close()
data, err := fs.fetchContainerConfig(testPodID, contID)
if err != nil {
t.Fatal(err)
}
expected := ContainerConfig{
ID: contID,
RootFs: rootFs,
}
if reflect.DeepEqual(data, expected) == false {
t.Fatal()
}
}
func TestFilesystemFetchContainerConfigFailingContIDEmpty(t *testing.T) {
fs := &filesystem{}
_, err := fs.fetchContainerConfig(testPodID, "")
if err == nil {
t.Fatal()
}
}
func TestFilesystemFetchContainerConfigFailingPodIDEmpty(t *testing.T) {
fs := &filesystem{}
_, err := fs.fetchContainerConfig("", "100")
if err == nil {
t.Fatal()
}
}
func TestFilesystemFetchContainerMountsSuccessful(t *testing.T) {
fs := &filesystem{}
contID := "100"
contMountsDir := filepath.Join(runStoragePath, testPodID, contID)
os.MkdirAll(contMountsDir, dirMode)
path := filepath.Join(contMountsDir, mountsFile)
os.Remove(path)
f, err := os.Create(path)
if err != nil {
t.Fatal(err)
}
source := "/dev/sda1"
dest := "/root"
mntType := "ext4"
options := "rw"
hostPath := "/tmp/root"
mountData := fmt.Sprintf(`
[
{
"Source":"%s",
"Destination":"%s",
"Type":"%s",
"Options": ["%s"],
"HostPath":"%s"
}
]
`, source, dest, mntType, options, hostPath)
n, err := f.WriteString(mountData)
if err != nil || n != len(mountData) {
f.Close()
t.Fatal(err)
}
f.Close()
data, err := fs.fetchContainerMounts(testPodID, contID)
if err != nil {
data, _ := ioutil.ReadFile(path)
t.Logf("Data from file : %s", string(data[:]))
t.Fatal(err)
}
expected := []Mount{
{
Source: source,
Destination: dest,
Type: mntType,
Options: []string{"rw"},
HostPath: hostPath,
},
}
if reflect.DeepEqual(data, expected) == false {
t.Fatalf("Expected : [%v]\n, Got : [%v]\n", expected, data)
}
}
func TestFilesystemFetchContainerMountsInvalidType(t *testing.T) {
fs := &filesystem{}
contID := "100"
contMountsDir := filepath.Join(runStoragePath, testPodID, contID)
os.MkdirAll(contMountsDir, dirMode)
path := filepath.Join(contMountsDir, mountsFile)
os.Remove(path)
f, err := os.Create(path)
if err != nil {
t.Fatal(err)
}
configData := fmt.Sprintf("{\"ID\":\"%s\",\"RootFs\":\"rootfs\"}", contID)
n, err := f.WriteString(configData)
if err != nil || n != len(configData) {
f.Close()
t.Fatal(err)
}
f.Close()
_, err = fs.fetchContainerMounts(testPodID, contID)
if err == nil {
t.Fatal()
}
}
func TestFilesystemFetchContainerMountsFailingContIDEmpty(t *testing.T) {
fs := &filesystem{}
_, err := fs.fetchContainerMounts(testPodID, "")
if err == nil {
t.Fatal()
}
}
func TestFilesystemFetchContainerMountsFailingPodIDEmpty(t *testing.T) {
fs := &filesystem{}
_, err := fs.fetchContainerMounts("", "100")
if err == nil {
t.Fatal()
}
}
func TestFilesystemResourceDirFailingPodIDEmpty(t *testing.T) {
for _, b := range []bool{true, false} {
_, err := resourceDir(b, "", "", configFileType)
if err == nil {
t.Fatal()
}
}
}
func TestFilesystemResourceDirFailingInvalidResource(t *testing.T) {
for _, b := range []bool{true, false} {
_, err := resourceDir(b, testPodID, "100", podResource(-1))
if err == nil {
t.Fatal()
}
}
}
func TestFilesystemResourceURIFailingResourceDir(t *testing.T) {
fs := &filesystem{}
for _, b := range []bool{true, false} {
_, _, err := fs.resourceURI(b, testPodID, "100", podResource(-1))
if err == nil {
t.Fatal()
}
}
}
func TestFilesystemStoreResourceFailingPodConfigStateFileType(t *testing.T) {
fs := &filesystem{}
data := PodConfig{}
for _, b := range []bool{true, false} {
err := fs.storeResource(b, testPodID, "100", stateFileType, data)
if err == nil {
t.Fatal()
}
}
}
func TestFilesystemStoreResourceFailingContainerConfigStateFileType(t *testing.T) {
fs := &filesystem{}
data := ContainerConfig{}
for _, b := range []bool{true, false} {
err := fs.storeResource(b, testPodID, "100", stateFileType, data)
if err == nil {
t.Fatal()
}
}
}
func TestFilesystemStoreResourceFailingPodConfigResourceURI(t *testing.T) {
fs := &filesystem{}
data := PodConfig{}
for _, b := range []bool{true, false} {
err := fs.storeResource(b, "", "100", configFileType, data)
if err == nil {
t.Fatal()
}
}
}
func TestFilesystemStoreResourceFailingContainerConfigResourceURI(t *testing.T) {
fs := &filesystem{}
data := ContainerConfig{}
for _, b := range []bool{true, false} {
err := fs.storeResource(b, "", "100", configFileType, data)
if err == nil {
t.Fatal()
}
}
}
func TestFilesystemStoreResourceFailingStateConfigFileType(t *testing.T) {
fs := &filesystem{}
data := State{}
for _, b := range []bool{true, false} {
err := fs.storeResource(b, testPodID, "100", configFileType, data)
if err == nil {
t.Fatal()
}
}
}
func TestFilesystemStoreResourceFailingStateResourceURI(t *testing.T) {
fs := &filesystem{}
data := State{}
for _, b := range []bool{true, false} {
err := fs.storeResource(b, "", "100", stateFileType, data)
if err == nil {
t.Fatal()
}
}
}
func TestFilesystemStoreResourceFailingWrongDataType(t *testing.T) {
fs := &filesystem{}
data := TestNoopStructure{}
for _, b := range []bool{true, false} {
err := fs.storeResource(b, testPodID, "100", configFileType, data)
if err == nil {
t.Fatal()
}
}
}
func TestFilesystemFetchResourceFailingWrongResourceType(t *testing.T) {
fs := &filesystem{}
for _, b := range []bool{true, false} {
if err := fs.fetchResource(b, testPodID, "100", lockFileType, nil); err == nil {
t.Fatal()
}
}
}

View File

@@ -0,0 +1,240 @@
# virtc
`virtc` is a simple command-line tool that serves to demonstrate typical usage of the virtcontainers API.
This is example software; unlike other projects like runc, runv, or rkt, virtcontainers is not a full container runtime.
## Virtc example
Here we explain how to use the pod and container API from `virtc` command line.
### Prepare your environment
#### Get your kernel
_Fedora_
```
$ sudo -E dnf config-manager --add-repo http://download.opensuse.org/repositories/home:clearlinux:preview:clear-containers-2.1/Fedora_25/home:clearlinux:preview:clear-containers-2.1.repo
$ sudo dnf install linux-container
```
_Ubuntu_
```
$ sudo sh -c "echo 'deb http://download.opensuse.org/repositories/home:/clearlinux:/preview:/clear-containers-2.1/xUbuntu_16.10/ /' >> /etc/apt/sources.list.d/cc-oci-runtime.list"
$ sudo apt install linux-container
```
#### Get your image
Retrieve a recent Clear Containers image to make sure it contains a recent version of hyperstart agent.
To download and install the latest image:
```
$ latest_version=$(curl -sL https://download.clearlinux.org/latest)
$ curl -LO "https://download.clearlinux.org/current/clear-${latest_version}-containers.img.xz"
$ unxz clear-${latest_version}-containers.img.xz
$ sudo mkdir -p /usr/share/clear-containers/
$ sudo install --owner root --group root --mode 0644 clear-${latest_version}-containers.img /usr/share/clear-containers/
$ sudo ln -fs /usr/share/clear-containers/clear-${latest_version}-containers.img /usr/share/clear-containers/clear-containers.img
```
#### Get virtc
_Download virtcontainers project_
```
$ go get github.com/containers/virtcontainers
```
_Build and setup your environment_
```
$ cd $GOPATH/src/github.com/containers/virtcontainers
$ go build -o virtc hack/virtc/main.go
$ sudo -E bash ./utils/virtcontainers-setup.sh
```
`virtcontainers-setup.sh` setup your environment performing different tasks. Particularly, it creates a __busybox__ bundle, and it creates CNI configuration files needed to run `virtc` with CNI plugins.
### Get cc-proxy (optional)
If you plan to start `virtc` with the hyperstart agent, you will have to use [cc-proxy](https://github.com/clearcontainers/proxy) as a proxy, meaning you have to perform extra steps to setup your environment.
```
$ go get github.com/clearcontainers/proxy
$ cd $GOPATH/src/github.com/clearcontainers/proxy
$ make
$ sudo make install
```
If you want to see the traces from the proxy when `virtc` will run, you can manually start it with appropriate debug level:
```
$ sudo /usr/libexec/clearcontainers/cc-proxy -v 3
```
This will generate output similar to the following:
```
I0410 08:58:49.058881 5384 proxy.go:521] listening on /var/run/clearcontainers/proxy.sock
I0410 08:58:49.059044 5384 proxy.go:566] proxy started
```
The proxy socket specified in the example log output has to be used as `virtc`'s `--proxy-url` option.
### Get cc-shim (optional)
If you plan to start `virtc` with the hyperstart agent (implying the use of `cc-proxy` as a proxy), you will have to rely on [cc-shim](https://github.com/clearcontainers/shim) in order to interact with the process running inside your container.
First, you will have to perform extra steps to setup your environment.
```
$ go get github.com/clearcontainers/shim
$ cd $GOPATH/src/github.com/clearcontainers/shim && ./autogen.sh
$ make
$ sudo make install
```
The shim will be installed at the following location: `/usr/libexec/clear-containers/cc-shim`. There will be three cases where you will be able to interact with your container's process through `cc-shim`:
_Start a new container_
```
# ./virtc container start --id=1 --pod-id=306ecdcf-0a6f-4a06-a03e-86a7b868ffc8
```
_Execute a new process on a running container_
```
# ./virtc container enter --id=1 --pod-id=306ecdcf-0a6f-4a06-a03e-86a7b868ffc8
```
_Start a pod with container(s) previously created_
```
# ./virtc pod start --id=306ecdcf-0a6f-4a06-a03e-86a7b868ffc8
```
Notice that in both cases, the `--pod-id` and `--id` options have been defined when previously creating a pod and a container.
### Run virtc
All following commands __MUST__ be run as root. By default, and unless you decide to modify it and rebuild it, `virtc` starts empty pods (no container started).
#### Run a new pod (Create + Start)
```
# ./virtc pod run --agent="hyperstart" --network="CNI" --proxy="ccProxy" --proxy-url="unix:///var/run/clearcontainers/proxy.sock" --shim="ccShim" --shim-path="/usr/libexec/cc-shim"
```
#### Create a new pod
```
# ./virtc pod run --agent="hyperstart" --network="CNI" --proxy="ccProxy" --proxy-url="unix:///var/run/clearcontainers/proxy.sock" --shim="ccShim" --shim-path="/usr/libexec/cc-shim"
```
This will generate output similar to the following:
```
Pod 306ecdcf-0a6f-4a06-a03e-86a7b868ffc8 created
```
#### Start an existing pod
```
# ./virtc pod start --id=306ecdcf-0a6f-4a06-a03e-86a7b868ffc8
```
This will generate output similar to the following:
```
Pod 306ecdcf-0a6f-4a06-a03e-86a7b868ffc8 started
```
#### Stop an existing pod
```
# ./virtc pod stop --id=306ecdcf-0a6f-4a06-a03e-86a7b868ffc8
```
This will generate output similar to the following:
```
Pod 306ecdcf-0a6f-4a06-a03e-86a7b868ffc8 stopped
```
#### Get the status of an existing pod and its containers
```
# ./virtc pod status --id=306ecdcf-0a6f-4a06-a03e-86a7b868ffc8
```
This will generate output similar to the following (assuming the pod has been started):
```
POD ID STATE HYPERVISOR AGENT
306ecdcf-0a6f-4a06-a03e-86a7b868ffc8 running qemu hyperstart
CONTAINER ID STATE
```
#### Delete an existing pod
```
# ./virtc pod delete --id=306ecdcf-0a6f-4a06-a03e-86a7b868ffc8
```
This will generate output similar to the following:
```
Pod 306ecdcf-0a6f-4a06-a03e-86a7b868ffc8 deleted
```
#### List all existing pods
```
# ./virtc pod list
```
This should generate that kind of output
```
POD ID STATE HYPERVISOR AGENT
306ecdcf-0a6f-4a06-a03e-86a7b868ffc8 running qemu hyperstart
92d73f74-4514-4a0d-81df-db1cc4c59100 running qemu hyperstart
7088148c-049b-4be7-b1be-89b3ae3c551c ready qemu hyperstart
6d57654e-4804-4a91-b72d-b5fe375ed3e1 ready qemu hyperstart
```
#### Create a new container
```
# ./virtc container create --id=1 --pod-id=306ecdcf-0a6f-4a06-a03e-86a7b868ffc8 --rootfs="/tmp/bundles/busybox/rootfs" --cmd="/bin/ifconfig" --console="/dev/pts/30"
```
This will generate output similar to the following:
```
Container 1 created
```
__Note:__ The option `--console` can be any existing console.
Don't try to provide `$(tty)` as it is your current console, and you would not be
able to get your console back as the shim would be listening to this indefinitely.
Instead, you would prefer to open a new shell and get the `$(tty)` from this shell.
That way, you make sure you have a dedicated input/output terminal.
#### Start an existing container
```
# ./virtc container start --id=1 --pod-id=306ecdcf-0a6f-4a06-a03e-86a7b868ffc8
```
This will generate output similar to the following:
```
Container 1 started
```
#### Run a new process on an existing container
```
# ./virtc container enter --id=1 --pod-id=306ecdcf-0a6f-4a06-a03e-86a7b868ffc8 --cmd="/bin/ps" --console="/dev/pts/30"
```
This will generate output similar to the following:
```
Container 1 entered
```
__Note:__ The option `--console` can be any existing console.
Don't try to provide `$(tty)` as it is your current console, and you would not be
able to get your console back as the shim would be listening to this indefinitely.
Instead, you would prefer to open a new shell and get the `$(tty)` from this shell.
That way, you make sure you have a dedicated input/output terminal.
#### Stop an existing container
```
# ./virtc container stop --id=1 --pod-id=306ecdcf-0a6f-4a06-a03e-86a7b868ffc8
```
This will generate output similar to the following:
```
Container 1 stopped
```
#### Delete an existing container
```
# ./virtc container delete --id=1 --pod-id=306ecdcf-0a6f-4a06-a03e-86a7b868ffc8
```
This will generate output similar to the following:
```
Container 1 deleted
```
#### Get the status of an existing container
```
# ./virtc container status --id=1 --pod-id=306ecdcf-0a6f-4a06-a03e-86a7b868ffc8
```
This will generate output similar to the following (assuming the container has been started):
```
CONTAINER ID STATE
1 running
```

View File

@@ -0,0 +1,976 @@
//
// Copyright (c) 2016 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"
"os/exec"
"strings"
"text/tabwriter"
"github.com/containers/virtcontainers/pkg/uuid"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
vc "github.com/containers/virtcontainers"
)
var virtcLog = logrus.New()
var listFormat = "%s\t%s\t%s\t%s\n"
var statusFormat = "%s\t%s\n"
var (
errNeedContainerID = errors.New("Container ID cannot be empty")
errNeedPodID = errors.New("Pod ID cannot be empty")
)
var podConfigFlags = []cli.Flag{
cli.GenericFlag{
Name: "agent",
Value: new(vc.AgentType),
Usage: "the guest agent",
},
cli.StringFlag{
Name: "id",
Value: "",
Usage: "the pod identifier (default: auto-generated)",
},
cli.StringFlag{
Name: "machine-type",
Value: vc.QemuPC,
Usage: "hypervisor machine type",
},
cli.GenericFlag{
Name: "network",
Value: new(vc.NetworkModel),
Usage: "the network model",
},
cli.GenericFlag{
Name: "proxy",
Value: new(vc.ProxyType),
Usage: "the agent's proxy",
},
cli.StringFlag{
Name: "proxy-path",
Value: "",
Usage: "path to proxy binary",
},
cli.GenericFlag{
Name: "shim",
Value: new(vc.ShimType),
Usage: "the shim type",
},
cli.StringFlag{
Name: "shim-path",
Value: "",
Usage: "the shim binary path",
},
cli.StringFlag{
Name: "hyper-ctl-sock-name",
Value: "",
Usage: "the hyperstart control socket name",
},
cli.StringFlag{
Name: "hyper-tty-sock-name",
Value: "",
Usage: "the hyperstart tty socket name",
},
cli.UintFlag{
Name: "cpus",
Value: 0,
Usage: "the number of virtual cpus available for this pod",
},
cli.UintFlag{
Name: "memory",
Value: 0,
Usage: "the amount of memory available for this pod in MiB",
},
}
var ccKernelParams = []vc.Param{
{
Key: "init",
Value: "/usr/lib/systemd/systemd",
},
{
Key: "systemd.unit",
Value: "clear-containers.target",
},
{
Key: "systemd.mask",
Value: "systemd-networkd.service",
},
{
Key: "systemd.mask",
Value: "systemd-networkd.socket",
},
}
func buildKernelParams(config *vc.HypervisorConfig) error {
for _, p := range ccKernelParams {
if err := config.AddKernelParam(p); err != nil {
return err
}
}
return nil
}
func buildPodConfig(context *cli.Context) (vc.PodConfig, error) {
var agConfig interface{}
hyperCtlSockName := context.String("hyper-ctl-sock-name")
hyperTtySockName := context.String("hyper-tty-sock-name")
proxyPath := context.String("proxy-path")
shimPath := context.String("shim-path")
machineType := context.String("machine-type")
vmMemory := context.Uint("vm-memory")
agentType, ok := context.Generic("agent").(*vc.AgentType)
if ok != true {
return vc.PodConfig{}, fmt.Errorf("Could not convert agent type")
}
networkModel, ok := context.Generic("network").(*vc.NetworkModel)
if ok != true {
return vc.PodConfig{}, fmt.Errorf("Could not convert network model")
}
proxyType, ok := context.Generic("proxy").(*vc.ProxyType)
if ok != true {
return vc.PodConfig{}, fmt.Errorf("Could not convert proxy type")
}
shimType, ok := context.Generic("shim").(*vc.ShimType)
if ok != true {
return vc.PodConfig{}, fmt.Errorf("Could not convert shim type")
}
kernelPath := "/usr/share/clear-containers/vmlinuz.container"
if machineType == vc.QemuPCLite {
kernelPath = "/usr/share/clear-containers/vmlinux.container"
}
hypervisorConfig := vc.HypervisorConfig{
KernelPath: kernelPath,
ImagePath: "/usr/share/clear-containers/clear-containers.img",
HypervisorMachineType: machineType,
}
if err := buildKernelParams(&hypervisorConfig); err != nil {
return vc.PodConfig{}, err
}
netConfig := vc.NetworkConfig{
NumInterfaces: 1,
}
switch *agentType {
case vc.HyperstartAgent:
agConfig = vc.HyperConfig{
SockCtlName: hyperCtlSockName,
SockTtyName: hyperTtySockName,
}
default:
agConfig = nil
}
proxyConfig := getProxyConfig(*proxyType, proxyPath)
shimConfig := getShimConfig(*shimType, shimPath)
vmConfig := vc.Resources{
Memory: vmMemory,
}
id := context.String("id")
if id == "" {
// auto-generate pod name
id = uuid.Generate().String()
}
podConfig := vc.PodConfig{
ID: id,
VMConfig: vmConfig,
HypervisorType: vc.QemuHypervisor,
HypervisorConfig: hypervisorConfig,
AgentType: *agentType,
AgentConfig: agConfig,
NetworkModel: *networkModel,
NetworkConfig: netConfig,
ProxyType: *proxyType,
ProxyConfig: proxyConfig,
ShimType: *shimType,
ShimConfig: shimConfig,
Containers: []vc.ContainerConfig{},
}
return podConfig, nil
}
func getProxyConfig(proxyType vc.ProxyType, path string) vc.ProxyConfig {
var proxyConfig vc.ProxyConfig
switch proxyType {
case vc.KataProxyType:
fallthrough
case vc.CCProxyType:
proxyConfig = vc.ProxyConfig{
Path: path,
}
}
return proxyConfig
}
func getShimConfig(shimType vc.ShimType, path string) interface{} {
var shimConfig interface{}
switch shimType {
case vc.CCShimType, vc.KataShimType:
shimConfig = vc.ShimConfig{
Path: path,
}
default:
shimConfig = nil
}
return shimConfig
}
// checkRequiredPodArgs checks to ensure the required command-line
// arguments have been specified for the pod sub-command specified by
// the context argument.
func checkRequiredPodArgs(context *cli.Context) error {
if context == nil {
return fmt.Errorf("BUG: need Context")
}
// sub-sub-command name
name := context.Command.Name
switch name {
case "create":
fallthrough
case "list":
fallthrough
case "run":
// these commands don't require any arguments
return nil
}
id := context.String("id")
if id == "" {
return errNeedPodID
}
return nil
}
// checkRequiredContainerArgs checks to ensure the required command-line
// arguments have been specified for the container sub-command specified
// by the context argument.
func checkRequiredContainerArgs(context *cli.Context) error {
if context == nil {
return fmt.Errorf("BUG: need Context")
}
// sub-sub-command name
name := context.Command.Name
podID := context.String("pod-id")
if podID == "" {
return errNeedPodID
}
rootfs := context.String("rootfs")
if name == "create" && rootfs == "" {
return fmt.Errorf("%s: need rootfs", name)
}
id := context.String("id")
if id == "" {
return errNeedContainerID
}
return nil
}
func runPod(context *cli.Context) error {
podConfig, err := buildPodConfig(context)
if err != nil {
return fmt.Errorf("Could not build pod config: %s", err)
}
_, err = vc.RunPod(podConfig)
if err != nil {
return fmt.Errorf("Could not run pod: %s", err)
}
return nil
}
func createPod(context *cli.Context) error {
podConfig, err := buildPodConfig(context)
if err != nil {
return fmt.Errorf("Could not build pod config: %s", err)
}
p, err := vc.CreatePod(podConfig)
if err != nil {
return fmt.Errorf("Could not create pod: %s", err)
}
fmt.Printf("Pod %s created\n", p.ID())
return nil
}
func checkPodArgs(context *cli.Context, f func(context *cli.Context) error) error {
if err := checkRequiredPodArgs(context); err != nil {
return err
}
return f(context)
}
func checkContainerArgs(context *cli.Context, f func(context *cli.Context) error) error {
if err := checkRequiredContainerArgs(context); err != nil {
return err
}
return f(context)
}
func deletePod(context *cli.Context) error {
p, err := vc.DeletePod(context.String("id"))
if err != nil {
return fmt.Errorf("Could not delete pod: %s", err)
}
fmt.Printf("Pod %s deleted\n", p.ID())
return nil
}
func startPod(context *cli.Context) error {
p, err := vc.StartPod(context.String("id"))
if err != nil {
return fmt.Errorf("Could not start pod: %s", err)
}
fmt.Printf("Pod %s started\n", p.ID())
return nil
}
func stopPod(context *cli.Context) error {
p, err := vc.StopPod(context.String("id"))
if err != nil {
return fmt.Errorf("Could not stop pod: %s", err)
}
fmt.Printf("Pod %s stopped\n", p.ID())
return nil
}
func pausePod(context *cli.Context) error {
p, err := vc.PausePod(context.String("id"))
if err != nil {
return fmt.Errorf("Could not pause pod: %s", err)
}
fmt.Printf("Pod %s paused\n", p.ID())
return nil
}
func resumePod(context *cli.Context) error {
p, err := vc.ResumePod(context.String("id"))
if err != nil {
return fmt.Errorf("Could not resume pod: %s", err)
}
fmt.Printf("Pod %s resumed\n", p.ID())
return nil
}
func listPods(context *cli.Context) error {
podStatusList, err := vc.ListPod()
if err != nil {
return fmt.Errorf("Could not list pod: %s", err)
}
w := tabwriter.NewWriter(os.Stdout, 2, 8, 1, '\t', 0)
fmt.Fprintf(w, listFormat, "POD ID", "STATE", "HYPERVISOR", "AGENT")
for _, podStatus := range podStatusList {
fmt.Fprintf(w, listFormat,
podStatus.ID, podStatus.State.State, podStatus.Hypervisor, podStatus.Agent)
}
w.Flush()
return nil
}
func statusPod(context *cli.Context) error {
podStatus, err := vc.StatusPod(context.String("id"))
if err != nil {
return fmt.Errorf("Could not get pod status: %s", err)
}
w := tabwriter.NewWriter(os.Stdout, 2, 8, 1, '\t', 0)
fmt.Fprintf(w, listFormat, "POD ID", "STATE", "HYPERVISOR", "AGENT")
fmt.Fprintf(w, listFormat+"\n",
podStatus.ID, podStatus.State.State, podStatus.Hypervisor, podStatus.Agent)
fmt.Fprintf(w, statusFormat, "CONTAINER ID", "STATE")
for _, contStatus := range podStatus.ContainersStatus {
fmt.Fprintf(w, statusFormat, contStatus.ID, contStatus.State.State)
}
w.Flush()
return nil
}
var runPodCommand = cli.Command{
Name: "run",
Usage: "run a pod",
Flags: podConfigFlags,
Action: func(context *cli.Context) error {
return checkPodArgs(context, runPod)
},
}
var createPodCommand = cli.Command{
Name: "create",
Usage: "create a pod",
Flags: podConfigFlags,
Action: func(context *cli.Context) error {
return checkPodArgs(context, createPod)
},
}
var deletePodCommand = cli.Command{
Name: "delete",
Usage: "delete an existing pod",
Flags: []cli.Flag{
cli.StringFlag{
Name: "id",
Value: "",
Usage: "the pod identifier",
},
},
Action: func(context *cli.Context) error {
return checkPodArgs(context, deletePod)
},
}
var startPodCommand = cli.Command{
Name: "start",
Usage: "start an existing pod",
Flags: []cli.Flag{
cli.StringFlag{
Name: "id",
Value: "",
Usage: "the pod identifier",
},
},
Action: func(context *cli.Context) error {
return checkPodArgs(context, startPod)
},
}
var stopPodCommand = cli.Command{
Name: "stop",
Usage: "stop an existing pod",
Flags: []cli.Flag{
cli.StringFlag{
Name: "id",
Value: "",
Usage: "the pod identifier",
},
},
Action: func(context *cli.Context) error {
return checkPodArgs(context, stopPod)
},
}
var listPodsCommand = cli.Command{
Name: "list",
Usage: "list all existing pods",
Action: func(context *cli.Context) error {
return checkPodArgs(context, listPods)
},
}
var statusPodCommand = cli.Command{
Name: "status",
Usage: "returns a detailed pod status",
Flags: []cli.Flag{
cli.StringFlag{
Name: "id",
Value: "",
Usage: "the pod identifier",
},
},
Action: func(context *cli.Context) error {
return checkPodArgs(context, statusPod)
},
}
var pausePodCommand = cli.Command{
Name: "pause",
Usage: "pause an existing pod",
Flags: []cli.Flag{
cli.StringFlag{
Name: "id",
Value: "",
Usage: "the pod identifier",
},
},
Action: func(context *cli.Context) error {
return checkPodArgs(context, pausePod)
},
}
var resumePodCommand = cli.Command{
Name: "resume",
Usage: "unpause a paused pod",
Flags: []cli.Flag{
cli.StringFlag{
Name: "id",
Value: "",
Usage: "the pod identifier",
},
},
Action: func(context *cli.Context) error {
return checkPodArgs(context, resumePod)
},
}
func createContainer(context *cli.Context) error {
console := context.String("console")
interactive := false
if console != "" {
interactive = true
}
envs := []vc.EnvVar{
{
Var: "PATH",
Value: "/bin:/usr/bin:/sbin:/usr/sbin",
},
}
cmd := vc.Cmd{
Args: strings.Split(context.String("cmd"), " "),
Envs: envs,
WorkDir: "/",
Interactive: interactive,
Console: console,
}
id := context.String("id")
if id == "" {
// auto-generate container name
id = uuid.Generate().String()
}
containerConfig := vc.ContainerConfig{
ID: id,
RootFs: context.String("rootfs"),
Cmd: cmd,
}
_, c, err := vc.CreateContainer(context.String("pod-id"), containerConfig)
if err != nil {
return fmt.Errorf("Could not create container: %s", err)
}
fmt.Printf("Container %s created\n", c.ID())
return nil
}
func deleteContainer(context *cli.Context) error {
c, err := vc.DeleteContainer(context.String("pod-id"), context.String("id"))
if err != nil {
return fmt.Errorf("Could not delete container: %s", err)
}
fmt.Printf("Container %s deleted\n", c.ID())
return nil
}
func startContainer(context *cli.Context) error {
c, err := vc.StartContainer(context.String("pod-id"), context.String("id"))
if err != nil {
return fmt.Errorf("Could not start container: %s", err)
}
fmt.Printf("Container %s started\n", c.ID())
return nil
}
func stopContainer(context *cli.Context) error {
c, err := vc.StopContainer(context.String("pod-id"), context.String("id"))
if err != nil {
return fmt.Errorf("Could not stop container: %s", err)
}
fmt.Printf("Container %s stopped\n", c.ID())
return nil
}
func enterContainer(context *cli.Context) error {
console := context.String("console")
interactive := false
if console != "" {
interactive = true
}
envs := []vc.EnvVar{
{
Var: "PATH",
Value: "/bin:/usr/bin:/sbin:/usr/sbin",
},
}
cmd := vc.Cmd{
Args: strings.Split(context.String("cmd"), " "),
Envs: envs,
WorkDir: "/",
Interactive: interactive,
Console: console,
}
_, c, _, err := vc.EnterContainer(context.String("pod-id"), context.String("id"), cmd)
if err != nil {
return fmt.Errorf("Could not enter container: %s", err)
}
fmt.Printf("Container %s entered\n", c.ID())
return nil
}
func statusContainer(context *cli.Context) error {
contStatus, err := vc.StatusContainer(context.String("pod-id"), context.String("id"))
if err != nil {
return fmt.Errorf("Could not get container status: %s", err)
}
w := tabwriter.NewWriter(os.Stdout, 2, 8, 1, '\t', 0)
fmt.Fprintf(w, statusFormat, "CONTAINER ID", "STATE")
fmt.Fprintf(w, statusFormat, contStatus.ID, contStatus.State.State)
w.Flush()
return nil
}
var createContainerCommand = cli.Command{
Name: "create",
Usage: "create a container",
Flags: []cli.Flag{
cli.StringFlag{
Name: "id",
Value: "",
Usage: "the container identifier (default: auto-generated)",
},
cli.StringFlag{
Name: "pod-id",
Value: "",
Usage: "the pod identifier",
},
cli.StringFlag{
Name: "rootfs",
Value: "",
Usage: "the container rootfs directory",
},
cli.StringFlag{
Name: "cmd",
Value: "",
Usage: "the command executed inside the container",
},
cli.StringFlag{
Name: "console",
Value: "",
Usage: "the container console",
},
},
Action: func(context *cli.Context) error {
return checkContainerArgs(context, createContainer)
},
}
var deleteContainerCommand = cli.Command{
Name: "delete",
Usage: "delete an existing container",
Flags: []cli.Flag{
cli.StringFlag{
Name: "id",
Value: "",
Usage: "the container identifier",
},
cli.StringFlag{
Name: "pod-id",
Value: "",
Usage: "the pod identifier",
},
},
Action: func(context *cli.Context) error {
return checkContainerArgs(context, deleteContainer)
},
}
var startContainerCommand = cli.Command{
Name: "start",
Usage: "start an existing container",
Flags: []cli.Flag{
cli.StringFlag{
Name: "id",
Value: "",
Usage: "the container identifier",
},
cli.StringFlag{
Name: "pod-id",
Value: "",
Usage: "the pod identifier",
},
},
Action: func(context *cli.Context) error {
return checkContainerArgs(context, startContainer)
},
}
var stopContainerCommand = cli.Command{
Name: "stop",
Usage: "stop an existing container",
Flags: []cli.Flag{
cli.StringFlag{
Name: "id",
Value: "",
Usage: "the container identifier",
},
cli.StringFlag{
Name: "pod-id",
Value: "",
Usage: "the pod identifier",
},
},
Action: func(context *cli.Context) error {
return checkContainerArgs(context, stopContainer)
},
}
var enterContainerCommand = cli.Command{
Name: "enter",
Usage: "enter an existing container",
Flags: []cli.Flag{
cli.StringFlag{
Name: "id",
Value: "",
Usage: "the container identifier",
},
cli.StringFlag{
Name: "pod-id",
Value: "",
Usage: "the pod identifier",
},
cli.StringFlag{
Name: "cmd",
Value: "echo",
Usage: "the command executed inside the container",
},
cli.StringFlag{
Name: "console",
Value: "",
Usage: "the process console",
},
},
Action: func(context *cli.Context) error {
return checkContainerArgs(context, enterContainer)
},
}
var statusContainerCommand = cli.Command{
Name: "status",
Usage: "returns detailed container status",
Flags: []cli.Flag{
cli.StringFlag{
Name: "id",
Value: "",
Usage: "the container identifier",
},
cli.StringFlag{
Name: "pod-id",
Value: "",
Usage: "the pod identifier",
},
},
Action: func(context *cli.Context) error {
return checkContainerArgs(context, statusContainer)
},
}
func startCCShim(process *vc.Process, shimPath, url string) error {
if process.Token == "" {
return fmt.Errorf("Token cannot be empty")
}
if url == "" {
return fmt.Errorf("URL cannot be empty")
}
if shimPath == "" {
return fmt.Errorf("Shim path cannot be empty")
}
cmd := exec.Command(shimPath, "-t", process.Token, "-u", url)
cmd.Env = os.Environ()
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
func main() {
cli.VersionFlag = cli.BoolFlag{
Name: "version",
Usage: "print the version",
}
virtc := cli.NewApp()
virtc.Name = "VirtContainers CLI"
virtc.Version = "0.0.1"
virtc.Flags = []cli.Flag{
cli.BoolFlag{
Name: "debug",
Usage: "enable debug output for logging",
},
cli.StringFlag{
Name: "log",
Value: "",
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')",
},
}
virtc.Commands = []cli.Command{
{
Name: "pod",
Usage: "pod commands",
Subcommands: []cli.Command{
createPodCommand,
deletePodCommand,
listPodsCommand,
pausePodCommand,
resumePodCommand,
runPodCommand,
startPodCommand,
stopPodCommand,
statusPodCommand,
},
},
{
Name: "container",
Usage: "container commands",
Subcommands: []cli.Command{
createContainerCommand,
deleteContainerCommand,
startContainerCommand,
stopContainerCommand,
enterContainerCommand,
statusContainerCommand,
},
},
}
virtc.Before = func(context *cli.Context) error {
if context.GlobalBool("debug") {
virtcLog.Level = logrus.DebugLevel
}
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
}
virtcLog.Out = f
}
switch context.GlobalString("log-format") {
case "text":
// retain logrus's default.
case "json":
virtcLog.Formatter = new(logrus.JSONFormatter)
default:
return fmt.Errorf("unknown log-format %q", context.GlobalString("log-format"))
}
// Set virtcontainers logger.
vc.SetLogger(virtcLog)
return nil
}
err := virtc.Run(os.Args)
if err != nil {
virtcLog.Fatal(err)
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}

160
virtcontainers/hook.go Normal file
View File

@@ -0,0 +1,160 @@
//
// 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 virtcontainers
import (
"bytes"
"encoding/json"
"fmt"
"os"
"os/exec"
"time"
specs "github.com/opencontainers/runtime-spec/specs-go"
"github.com/sirupsen/logrus"
)
// Hook represents an OCI hook, including its required parameters.
type Hook struct {
Path string
Args []string
Env []string
Timeout int
}
// Hooks gathers all existing OCI hooks list.
type Hooks struct {
PreStartHooks []Hook
PostStartHooks []Hook
PostStopHooks []Hook
}
// Logger returns a logrus logger appropriate for logging Hooks messages
func (h *Hooks) Logger() *logrus.Entry {
return virtLog.WithField("subsystem", "hooks")
}
func buildHookState(processID int) specs.State {
return specs.State{
Pid: processID,
}
}
func (h *Hook) runHook() error {
state := buildHookState(os.Getpid())
stateJSON, err := json.Marshal(state)
if err != nil {
return err
}
var stdout, stderr bytes.Buffer
cmd := &exec.Cmd{
Path: h.Path,
Args: h.Args,
Env: h.Env,
Stdin: bytes.NewReader(stateJSON),
Stdout: &stdout,
Stderr: &stderr,
}
err = cmd.Start()
if err != nil {
return err
}
if h.Timeout == 0 {
err = cmd.Wait()
if err != nil {
return fmt.Errorf("%s: stdout: %s, stderr: %s", err, stdout.String(), stderr.String())
}
} else {
done := make(chan error)
go func() { done <- cmd.Wait() }()
select {
case err := <-done:
if err != nil {
return fmt.Errorf("%s: stdout: %s, stderr: %s", err, stdout.String(), stderr.String())
}
case <-time.After(time.Duration(h.Timeout) * time.Second):
return fmt.Errorf("Hook timeout")
}
}
return nil
}
func (h *Hooks) preStartHooks() error {
if len(h.PreStartHooks) == 0 {
return nil
}
for _, hook := range h.PreStartHooks {
err := hook.runHook()
if err != nil {
h.Logger().WithFields(logrus.Fields{
"hook-type": "pre-start",
"error": err,
}).Error("hook error")
return err
}
}
return nil
}
func (h *Hooks) postStartHooks() error {
if len(h.PostStartHooks) == 0 {
return nil
}
for _, hook := range h.PostStartHooks {
err := hook.runHook()
if err != nil {
// In case of post start hook, the error is not fatal,
// just need to be logged.
h.Logger().WithFields(logrus.Fields{
"hook-type": "post-start",
"error": err,
}).Info("hook error")
}
}
return nil
}
func (h *Hooks) postStopHooks() error {
if len(h.PostStopHooks) == 0 {
return nil
}
for _, hook := range h.PostStopHooks {
err := hook.runHook()
if err != nil {
// In case of post stop hook, the error is not fatal,
// just need to be logged.
h.Logger().WithFields(logrus.Fields{
"hook-type": "post-stop",
"error": err,
}).Info("hook error")
}
}
return nil
}

View File

@@ -0,0 +1,101 @@
//
// 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"
"strconv"
"time"
"github.com/opencontainers/runc/libcontainer/configs"
)
var logFile = "/tmp/mock_hook.log"
var testKey = "test-key"
var testContainerID = "test-container-id"
var testControllerID = "test-controller-id"
func main() {
err := os.RemoveAll(logFile)
if err != nil {
os.Exit(1)
}
f, err := os.Create(logFile)
if err != nil {
os.Exit(1)
}
defer f.Close()
fmt.Fprintf(f, "args = %s\n", os.Args)
if len(os.Args) < 3 {
fmt.Fprintf(f, "At least 3 args expected, only %d received\n", len(os.Args))
os.Exit(1)
}
if os.Args[0] != testKey {
fmt.Fprintf(f, "args[0] should be \"%s\", received \"%s\" instead\n", testKey, os.Args[0])
os.Exit(1)
}
if os.Args[1] != testContainerID {
fmt.Fprintf(f, "argv[1] should be \"%s\", received \"%s\" instead\n", testContainerID, os.Args[1])
os.Exit(1)
}
if os.Args[2] != testControllerID {
fmt.Fprintf(f, "argv[2] should be \"%s\", received \"%s\" instead\n", testControllerID, os.Args[2])
os.Exit(1)
}
stateBuf, err := ioutil.ReadAll(os.Stdin)
if err != nil {
fmt.Fprintf(f, "Could not read on stdin: %s\n", err)
os.Exit(1)
}
var state configs.HookState
err = json.Unmarshal(stateBuf, &state)
if err != nil {
fmt.Fprintf(f, "Could not unmarshal HookState json: %s\n", err)
os.Exit(1)
}
if state.Pid < 1 {
fmt.Fprintf(f, "Invalid PID: %d\n", state.Pid)
os.Exit(1)
}
// Intended to sleep, so as to make the test passing/failing.
if len(os.Args) >= 4 {
timeout, err := strconv.Atoi(os.Args[3])
if err != nil {
fmt.Fprintf(f, "Could not retrieve timeout %s from args[3]\n", os.Args[3])
os.Exit(1)
}
time.Sleep(time.Duration(timeout) * time.Second)
if len(os.Args) == 5 {
msg := "panicking at user request"
fmt.Fprintln(f, msg)
panic(msg)
}
}
}

239
virtcontainers/hook_test.go Normal file
View File

@@ -0,0 +1,239 @@
//
// 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 virtcontainers
import (
"io/ioutil"
"os"
"path/filepath"
"reflect"
"testing"
. "github.com/containers/virtcontainers/pkg/mock"
specs "github.com/opencontainers/runtime-spec/specs-go"
)
// Important to keep these values in sync with hook test binary
var testKeyHook = "test-key"
var testContainerIDHook = "test-container-id"
var testControllerIDHook = "test-controller-id"
var testProcessIDHook = 12345
var testBinHookPath = "/usr/bin/virtcontainers/bin/test/hook"
func getMockHookBinPath() string {
if DefaultMockHookBinPath == "" {
return testBinHookPath
}
return DefaultMockHookBinPath
}
func TestBuildHookState(t *testing.T) {
expected := specs.State{
Pid: testProcessIDHook,
}
hookState := buildHookState(testProcessIDHook)
if reflect.DeepEqual(hookState, expected) == false {
t.Fatal()
}
}
func createHook(timeout int) *Hook {
return &Hook{
Path: getMockHookBinPath(),
Args: []string{testKeyHook, testContainerIDHook, testControllerIDHook},
Env: os.Environ(),
Timeout: timeout,
}
}
func createWrongHook() *Hook {
return &Hook{
Path: getMockHookBinPath(),
Args: []string{"wrong-args"},
Env: os.Environ(),
}
}
func testRunHookFull(t *testing.T, timeout int, expectFail bool) {
hook := createHook(timeout)
err := hook.runHook()
if expectFail {
if err == nil {
t.Fatal("unexpected success")
}
} else {
if err != nil {
t.Fatalf("unexpected failure: %v", err)
}
}
}
func testRunHook(t *testing.T, timeout int) {
testRunHookFull(t, timeout, false)
}
func TestRunHook(t *testing.T) {
cleanUp()
testRunHook(t, 0)
}
func TestRunHookTimeout(t *testing.T) {
testRunHook(t, 1)
}
func TestRunHookExitFailure(t *testing.T) {
hook := createWrongHook()
err := hook.runHook()
if err == nil {
t.Fatal()
}
}
func TestRunHookTimeoutFailure(t *testing.T) {
hook := createHook(1)
hook.Args = append(hook.Args, "2")
err := hook.runHook()
if err == nil {
t.Fatal()
}
}
func TestRunHookWaitFailure(t *testing.T) {
hook := createHook(60)
hook.Args = append(hook.Args, "1", "panic")
err := hook.runHook()
if err == nil {
t.Fatal()
}
}
func testRunHookInvalidCommand(t *testing.T, timeout int) {
cleanUp()
dir, err := ioutil.TempDir("", "")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(dir)
cmd := filepath.Join(dir, "does-not-exist")
savedDefaultMockHookBinPath := DefaultMockHookBinPath
DefaultMockHookBinPath = cmd
defer func() {
DefaultMockHookBinPath = savedDefaultMockHookBinPath
}()
testRunHookFull(t, timeout, true)
}
func TestRunHookInvalidCommand(t *testing.T) {
testRunHookInvalidCommand(t, 0)
}
func TestRunHookTimeoutInvalidCommand(t *testing.T) {
testRunHookInvalidCommand(t, 1)
}
func testHooks(t *testing.T, hook *Hook) {
hooks := &Hooks{
PreStartHooks: []Hook{*hook},
PostStartHooks: []Hook{*hook},
PostStopHooks: []Hook{*hook},
}
err := hooks.preStartHooks()
if err != nil {
t.Fatal(err)
}
err = hooks.postStartHooks()
if err != nil {
t.Fatal(err)
}
err = hooks.postStopHooks()
if err != nil {
t.Fatal(err)
}
}
func testFailingHooks(t *testing.T, hook *Hook) {
hooks := &Hooks{
PreStartHooks: []Hook{*hook},
PostStartHooks: []Hook{*hook},
PostStopHooks: []Hook{*hook},
}
err := hooks.preStartHooks()
if err == nil {
t.Fatal(err)
}
err = hooks.postStartHooks()
if err != nil {
t.Fatal(err)
}
err = hooks.postStopHooks()
if err != nil {
t.Fatal(err)
}
}
func TestHooks(t *testing.T) {
testHooks(t, createHook(0))
}
func TestHooksTimeout(t *testing.T) {
testHooks(t, createHook(1))
}
func TestFailingHooks(t *testing.T) {
testFailingHooks(t, createWrongHook())
}
func TestEmptyHooks(t *testing.T) {
hooks := &Hooks{}
err := hooks.preStartHooks()
if err != nil {
t.Fatal(err)
}
err = hooks.postStartHooks()
if err != nil {
t.Fatal(err)
}
err = hooks.postStopHooks()
if err != nil {
t.Fatal(err)
}
}

View File

@@ -0,0 +1,794 @@
//
// Copyright (c) 2016 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 virtcontainers
import (
"fmt"
"net"
"net/url"
"os"
"path/filepath"
"syscall"
"time"
proxyClient "github.com/clearcontainers/proxy/client"
"github.com/containers/virtcontainers/pkg/hyperstart"
ns "github.com/containers/virtcontainers/pkg/nsenter"
"github.com/sirupsen/logrus"
"github.com/vishvananda/netlink"
)
var defaultSockPathTemplates = []string{"%s/%s/hyper.sock", "%s/%s/tty.sock"}
var defaultChannelTemplate = "sh.hyper.channel.%d"
var defaultDeviceIDTemplate = "channel%d"
var defaultIDTemplate = "charch%d"
var defaultSharedDir = "/run/hyper/shared/pods/"
var mountTag = "hyperShared"
var maxHostnameLen = 64
const (
unixSocket = "unix"
)
// HyperConfig is a structure storing information needed for
// hyperstart agent initialization.
type HyperConfig struct {
SockCtlName string
SockTtyName string
}
func (h *hyper) generateSockets(pod Pod, c HyperConfig) {
podSocketPaths := []string{
fmt.Sprintf(defaultSockPathTemplates[0], runStoragePath, pod.id),
fmt.Sprintf(defaultSockPathTemplates[1], runStoragePath, pod.id),
}
if c.SockCtlName != "" {
podSocketPaths[0] = c.SockCtlName
}
if c.SockTtyName != "" {
podSocketPaths[1] = c.SockTtyName
}
for i := 0; i < len(podSocketPaths); i++ {
s := Socket{
DeviceID: fmt.Sprintf(defaultDeviceIDTemplate, i),
ID: fmt.Sprintf(defaultIDTemplate, i),
HostPath: podSocketPaths[i],
Name: fmt.Sprintf(defaultChannelTemplate, i),
}
h.sockets = append(h.sockets, s)
}
}
// HyperAgentState is the structure describing the data stored from this
// agent implementation.
type HyperAgentState struct {
ProxyPid int
URL string
}
// hyper is the Agent interface implementation for hyperstart.
type hyper struct {
pod Pod
shim shim
proxy proxy
client *proxyClient.Client
state HyperAgentState
sockets []Socket
}
type hyperstartProxyCmd struct {
cmd string
message interface{}
token string
}
// Logger returns a logrus logger appropriate for logging hyper messages
func (h *hyper) Logger() *logrus.Entry {
return virtLog.WithField("subsystem", "hyper")
}
func (h *hyper) buildHyperContainerProcess(cmd Cmd) (*hyperstart.Process, error) {
var envVars []hyperstart.EnvironmentVar
for _, e := range cmd.Envs {
envVar := hyperstart.EnvironmentVar{
Env: e.Var,
Value: e.Value,
}
envVars = append(envVars, envVar)
}
process := &hyperstart.Process{
Terminal: cmd.Interactive,
Args: cmd.Args,
Envs: envVars,
Workdir: cmd.WorkDir,
User: cmd.User,
Group: cmd.PrimaryGroup,
AdditionalGroups: cmd.SupplementaryGroups,
NoNewPrivileges: cmd.NoNewPrivileges,
}
process.Capabilities = hyperstart.Capabilities{
Bounding: cmd.Capabilities.Bounding,
Effective: cmd.Capabilities.Effective,
Inheritable: cmd.Capabilities.Inheritable,
Permitted: cmd.Capabilities.Permitted,
Ambient: cmd.Capabilities.Ambient,
}
return process, nil
}
func (h *hyper) processHyperRoute(route netlink.Route, deviceName string) *hyperstart.Route {
gateway := route.Gw.String()
if gateway == "<nil>" {
gateway = ""
}
var destination string
if route.Dst == nil {
destination = ""
} else {
destination = route.Dst.String()
if destination == defaultRouteDest {
destination = defaultRouteLabel
}
// Skip IPv6 because not supported by hyperstart
if route.Dst.IP.To4() == nil {
h.Logger().WithFields(logrus.Fields{
"unsupported-route-type": "ipv6",
"destination": destination,
}).Warn("unsupported route")
return nil
}
}
return &hyperstart.Route{
Dest: destination,
Gateway: gateway,
Device: deviceName,
}
}
func (h *hyper) buildNetworkInterfacesAndRoutes(pod Pod) ([]hyperstart.NetworkIface, []hyperstart.Route, error) {
if pod.networkNS.NetNsPath == "" {
return []hyperstart.NetworkIface{}, []hyperstart.Route{}, nil
}
var ifaces []hyperstart.NetworkIface
var routes []hyperstart.Route
for _, endpoint := range pod.networkNS.Endpoints {
var ipAddresses []hyperstart.IPAddress
for _, addr := range endpoint.Properties().Addrs {
// Skip IPv6 because not supported by hyperstart.
// Skip localhost interface.
if addr.IP.To4() == nil || addr.IP.IsLoopback() {
continue
}
netMask, _ := addr.Mask.Size()
ipAddress := hyperstart.IPAddress{
IPAddress: addr.IP.String(),
NetMask: fmt.Sprintf("%d", netMask),
}
ipAddresses = append(ipAddresses, ipAddress)
}
iface := hyperstart.NetworkIface{
NewDevice: endpoint.Name(),
IPAddresses: ipAddresses,
MTU: endpoint.Properties().Iface.MTU,
MACAddr: endpoint.HardwareAddr(),
}
ifaces = append(ifaces, iface)
for _, r := range endpoint.Properties().Routes {
route := h.processHyperRoute(r, endpoint.Name())
if route == nil {
continue
}
routes = append(routes, *route)
}
}
return ifaces, routes, nil
}
func fsMapFromMounts(mounts []Mount) []*hyperstart.FsmapDescriptor {
var fsmap []*hyperstart.FsmapDescriptor
for _, m := range mounts {
fsmapDesc := &hyperstart.FsmapDescriptor{
Source: m.Source,
Path: m.Destination,
ReadOnly: m.ReadOnly,
DockerVolume: false,
}
fsmap = append(fsmap, fsmapDesc)
}
return fsmap
}
// init is the agent initialization implementation for hyperstart.
func (h *hyper) init(pod *Pod, config interface{}) (err error) {
switch c := config.(type) {
case HyperConfig:
// Create agent sockets from paths provided through
// configuration, or generate them from scratch.
h.generateSockets(*pod, c)
h.pod = *pod
default:
return fmt.Errorf("Invalid config type")
}
h.proxy, err = newProxy(pod.config.ProxyType)
if err != nil {
return err
}
h.shim, err = newShim(pod.config.ShimType)
if err != nil {
return err
}
// Fetch agent runtime info.
if err := pod.storage.fetchAgentState(pod.id, &h.state); err != nil {
h.Logger().Debug("Could not retrieve anything from storage")
}
return nil
}
func (h *hyper) createPod(pod *Pod) (err error) {
for _, socket := range h.sockets {
err := pod.hypervisor.addDevice(socket, serialPortDev)
if err != nil {
return err
}
}
// Adding the hyper shared volume.
// This volume contains all bind mounted container bundles.
sharedVolume := Volume{
MountTag: mountTag,
HostPath: filepath.Join(defaultSharedDir, pod.id),
}
if err := os.MkdirAll(sharedVolume.HostPath, dirMode); err != nil {
return err
}
return pod.hypervisor.addDevice(sharedVolume, fsDev)
}
func (h *hyper) capabilities() capabilities {
var caps capabilities
// add all capabilities supported by agent
caps.setBlockDeviceSupport()
return caps
}
// exec is the agent command execution implementation for hyperstart.
func (h *hyper) exec(pod *Pod, c Container, cmd Cmd) (*Process, error) {
token, err := h.attach()
if err != nil {
return nil, err
}
hyperProcess, err := h.buildHyperContainerProcess(cmd)
if err != nil {
return nil, err
}
execCommand := hyperstart.ExecCommand{
Container: c.id,
Process: *hyperProcess,
}
enterNSList := []ns.Namespace{
{
PID: c.process.Pid,
Type: ns.NSTypeNet,
},
{
PID: c.process.Pid,
Type: ns.NSTypePID,
},
}
process, err := prepareAndStartShim(pod, h.shim, c.id,
token, h.state.URL, cmd, []ns.NSType{}, enterNSList)
if err != nil {
return nil, err
}
proxyCmd := hyperstartProxyCmd{
cmd: hyperstart.ExecCmd,
message: execCommand,
token: process.Token,
}
if _, err := h.sendCmd(proxyCmd); err != nil {
return nil, err
}
return process, nil
}
// startPod is the agent Pod starting implementation for hyperstart.
func (h *hyper) startPod(pod Pod) error {
// Start the proxy here
pid, uri, err := h.proxy.start(pod, proxyParams{})
if err != nil {
return err
}
// Fill agent state with proxy information, and store them.
h.state.ProxyPid = pid
h.state.URL = uri
if err := pod.storage.storeAgentState(pod.id, h.state); err != nil {
return err
}
h.Logger().WithField("proxy-pid", pid).Info("proxy started")
if err := h.register(); err != nil {
return err
}
ifaces, routes, err := h.buildNetworkInterfacesAndRoutes(pod)
if err != nil {
return err
}
hostname := pod.config.Hostname
if len(hostname) > maxHostnameLen {
hostname = hostname[:maxHostnameLen]
}
hyperPod := hyperstart.Pod{
Hostname: hostname,
Containers: []hyperstart.Container{},
Interfaces: ifaces,
Routes: routes,
ShareDir: mountTag,
}
proxyCmd := hyperstartProxyCmd{
cmd: hyperstart.StartPod,
message: hyperPod,
}
_, err = h.sendCmd(proxyCmd)
return err
}
// stopPod is the agent Pod stopping implementation for hyperstart.
func (h *hyper) stopPod(pod Pod) error {
proxyCmd := hyperstartProxyCmd{
cmd: hyperstart.DestroyPod,
message: nil,
}
if _, err := h.sendCmd(proxyCmd); err != nil {
return err
}
if err := h.unregister(); err != nil {
return err
}
return h.proxy.stop(pod, h.state.ProxyPid)
}
func (h *hyper) startOneContainer(pod Pod, c *Container) error {
process, err := h.buildHyperContainerProcess(c.config.Cmd)
if err != nil {
return err
}
container := hyperstart.Container{
ID: c.id,
Image: c.id,
Rootfs: rootfsDir,
Process: process,
}
if c.config.Resources.CPUQuota != 0 && c.config.Resources.CPUPeriod != 0 {
container.Constraints = hyperstart.Constraints{
CPUQuota: c.config.Resources.CPUQuota,
CPUPeriod: c.config.Resources.CPUPeriod,
}
}
if c.config.Resources.CPUShares != 0 {
container.Constraints.CPUShares = c.config.Resources.CPUShares
}
container.SystemMountsInfo.BindMountDev = c.systemMountsInfo.BindMountDev
if c.state.Fstype != "" {
// Pass a drive name only in case of block driver
if pod.config.HypervisorConfig.BlockDeviceDriver == VirtioBlock {
driveName, err := getVirtDriveName(c.state.BlockIndex)
if err != nil {
return err
}
container.Image = driveName
} else {
scsiAddr, err := getSCSIAddress(c.state.BlockIndex)
if err != nil {
return err
}
container.SCSIAddr = scsiAddr
}
container.Fstype = c.state.Fstype
} else {
if err := bindMountContainerRootfs(defaultSharedDir, pod.id, c.id, c.rootFs, false); err != nil {
bindUnmountAllRootfs(defaultSharedDir, pod)
return err
}
}
//TODO : Enter mount namespace
// Handle container mounts
newMounts, err := c.mountSharedDirMounts(defaultSharedDir, "")
if err != nil {
bindUnmountAllRootfs(defaultSharedDir, pod)
return err
}
fsmap := fsMapFromMounts(newMounts)
// Append container mounts for block devices passed with --device.
for _, device := range c.devices {
d, ok := device.(*BlockDevice)
if ok {
fsmapDesc := &hyperstart.FsmapDescriptor{
Source: d.VirtPath,
Path: d.DeviceInfo.ContainerPath,
AbsolutePath: true,
DockerVolume: false,
SCSIAddr: d.SCSIAddr,
}
fsmap = append(fsmap, fsmapDesc)
}
}
// Assign fsmap for hyperstart to mount these at the correct location within the container
container.Fsmap = fsmap
proxyCmd := hyperstartProxyCmd{
cmd: hyperstart.NewContainer,
message: container,
token: c.process.Token,
}
if _, err := h.sendCmd(proxyCmd); err != nil {
return err
}
return nil
}
// createContainer is the agent Container creation implementation for hyperstart.
func (h *hyper) createContainer(pod *Pod, c *Container) (*Process, error) {
token, err := h.attach()
if err != nil {
return nil, err
}
createNSList := []ns.NSType{ns.NSTypePID}
enterNSList := []ns.Namespace{
{
Path: pod.networkNS.NetNsPath,
Type: ns.NSTypeNet,
},
}
return prepareAndStartShim(pod, h.shim, c.id, token,
h.state.URL, c.config.Cmd, createNSList, enterNSList)
}
// startContainer is the agent Container starting implementation for hyperstart.
func (h *hyper) startContainer(pod Pod, c *Container) error {
return h.startOneContainer(pod, c)
}
// stopContainer is the agent Container stopping implementation for hyperstart.
func (h *hyper) stopContainer(pod Pod, c Container) error {
// Nothing to be done in case the container has not been started.
if c.state.State == StateReady {
return nil
}
return h.stopOneContainer(pod.id, c)
}
func (h *hyper) stopOneContainer(podID string, c Container) error {
removeCommand := hyperstart.RemoveCommand{
Container: c.id,
}
proxyCmd := hyperstartProxyCmd{
cmd: hyperstart.RemoveContainer,
message: removeCommand,
}
if _, err := h.sendCmd(proxyCmd); err != nil {
return err
}
if err := c.unmountHostMounts(); err != nil {
return err
}
if c.state.Fstype == "" {
if err := bindUnmountContainerRootfs(defaultSharedDir, podID, c.id); err != nil {
return err
}
}
return nil
}
// killContainer is the agent process signal implementation for hyperstart.
func (h *hyper) killContainer(pod Pod, c Container, signal syscall.Signal, all bool) error {
// Send the signal to the shim directly in case the container has not
// been started yet.
if c.state.State == StateReady {
return signalShim(c.process.Pid, signal)
}
return h.killOneContainer(c.id, signal, all)
}
func (h *hyper) killOneContainer(cID string, signal syscall.Signal, all bool) error {
killCmd := hyperstart.KillCommand{
Container: cID,
Signal: signal,
AllProcesses: all,
}
proxyCmd := hyperstartProxyCmd{
cmd: hyperstart.KillContainer,
message: killCmd,
}
if _, err := h.sendCmd(proxyCmd); err != nil {
return err
}
return nil
}
func (h *hyper) processListContainer(pod Pod, c Container, options ProcessListOptions) (ProcessList, error) {
return h.processListOneContainer(pod.id, c.id, options)
}
func (h *hyper) processListOneContainer(podID, cID string, options ProcessListOptions) (ProcessList, error) {
psCmd := hyperstart.PsCommand{
Container: cID,
Format: options.Format,
PsArgs: options.Args,
}
proxyCmd := hyperstartProxyCmd{
cmd: hyperstart.PsContainer,
message: psCmd,
}
response, err := h.sendCmd(proxyCmd)
if err != nil {
return nil, err
}
msg, ok := response.([]byte)
if !ok {
return nil, fmt.Errorf("failed to get response message from container %s pod %s", cID, podID)
}
return msg, nil
}
// connectProxyRetry repeatedly tries to connect to the proxy on the specified
// address until a timeout state is reached, when it will fail.
func (h *hyper) connectProxyRetry(scheme, address string) (conn net.Conn, err error) {
attempt := 1
timeoutSecs := time.Duration(waitForProxyTimeoutSecs * time.Second)
startTime := time.Now()
lastLogTime := startTime
for {
conn, err = net.Dial(scheme, address)
if err == nil {
// If the initial connection was unsuccessful,
// ensure a log message is generated when successfully
// connected.
if attempt > 1 {
h.Logger().WithField("attempt", fmt.Sprintf("%d", attempt)).Info("Connected to proxy")
}
return conn, nil
}
attempt++
now := time.Now()
delta := now.Sub(startTime)
remaining := timeoutSecs - delta
if remaining <= 0 {
return nil, fmt.Errorf("failed to connect to proxy after %v: %v", timeoutSecs, err)
}
logDelta := now.Sub(lastLogTime)
logDeltaSecs := logDelta / time.Second
if logDeltaSecs >= 1 {
h.Logger().WithError(err).WithFields(logrus.Fields{
"attempt": fmt.Sprintf("%d", attempt),
"proxy-network": scheme,
"proxy-address": address,
"remaining-time-secs": fmt.Sprintf("%2.2f", remaining.Seconds()),
}).Warning("Retrying proxy connection")
lastLogTime = now
}
time.Sleep(time.Duration(100) * time.Millisecond)
}
}
func (h *hyper) connect() error {
if h.client != nil {
return nil
}
u, err := url.Parse(h.state.URL)
if err != nil {
return err
}
if u.Scheme == "" {
return fmt.Errorf("URL scheme cannot be empty")
}
address := u.Host
if address == "" {
if u.Path == "" {
return fmt.Errorf("URL host and path cannot be empty")
}
address = u.Path
}
conn, err := h.connectProxyRetry(u.Scheme, address)
if err != nil {
return err
}
h.client = proxyClient.NewClient(conn)
return nil
}
func (h *hyper) disconnect() {
if h.client == nil {
return
}
h.client.Close()
h.client = nil
}
func (h *hyper) register() error {
if err := h.connect(); err != nil {
return err
}
defer h.disconnect()
registerVMOptions := &proxyClient.RegisterVMOptions{
Console: h.pod.hypervisor.getPodConsole(h.pod.id),
NumIOStreams: 0,
}
_, err := h.client.RegisterVM(h.pod.id, h.sockets[0].HostPath,
h.sockets[1].HostPath, registerVMOptions)
return err
}
func (h *hyper) unregister() error {
if err := h.connect(); err != nil {
return err
}
defer h.disconnect()
h.client.UnregisterVM(h.pod.id)
return nil
}
func (h *hyper) attach() (string, error) {
if err := h.connect(); err != nil {
return "", err
}
defer h.disconnect()
numTokens := 1
attachVMOptions := &proxyClient.AttachVMOptions{
NumIOStreams: numTokens,
}
attachVMReturn, err := h.client.AttachVM(h.pod.id, attachVMOptions)
if err != nil {
return "", err
}
if len(attachVMReturn.IO.Tokens) != numTokens {
return "", fmt.Errorf("%d tokens retrieved out of %d expected",
len(attachVMReturn.IO.Tokens), numTokens)
}
return attachVMReturn.IO.Tokens[0], nil
}
func (h *hyper) sendCmd(proxyCmd hyperstartProxyCmd) (interface{}, error) {
if err := h.connect(); err != nil {
return nil, err
}
defer h.disconnect()
attachVMOptions := &proxyClient.AttachVMOptions{
NumIOStreams: 0,
}
if _, err := h.client.AttachVM(h.pod.id, attachVMOptions); err != nil {
return nil, err
}
var tokens []string
if proxyCmd.token != "" {
tokens = append(tokens, proxyCmd.token)
}
return h.client.HyperWithTokens(proxyCmd.cmd, tokens, proxyCmd.message)
}

View File

@@ -0,0 +1,169 @@
//
// Copyright (c) 2016 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 virtcontainers
import (
"fmt"
"net"
"reflect"
"testing"
"github.com/containers/virtcontainers/pkg/hyperstart"
"github.com/vishvananda/netlink"
)
var testRouteDest = "192.168.10.1/32"
var testRouteGateway = "192.168.0.0"
var testRouteDeviceName = "test_eth0"
var testRouteDestIPv6 = "2001:db8::/32"
func TestHyperstartGenerateSocketsSuccessful(t *testing.T) {
config := HyperConfig{
SockCtlName: "ctlSock",
SockTtyName: "ttySock",
}
pod := Pod{
id: testPodID,
}
h := &hyper{}
h.generateSockets(pod, config)
expectedSockets := []Socket{
{
DeviceID: fmt.Sprintf(defaultDeviceIDTemplate, 0),
ID: fmt.Sprintf(defaultIDTemplate, 0),
HostPath: config.SockCtlName,
Name: fmt.Sprintf(defaultChannelTemplate, 0),
},
{
DeviceID: fmt.Sprintf(defaultDeviceIDTemplate, 1),
ID: fmt.Sprintf(defaultIDTemplate, 1),
HostPath: config.SockTtyName,
Name: fmt.Sprintf(defaultChannelTemplate, 1),
},
}
if !reflect.DeepEqual(expectedSockets, h.sockets) {
t.Fatalf("Expecting %+v, Got %+v", expectedSockets, h.sockets)
}
}
func TestHyperstartGenerateSocketsSuccessfulNoPathProvided(t *testing.T) {
config := HyperConfig{}
pod := Pod{
id: testPodID,
}
h := &hyper{}
h.generateSockets(pod, config)
expectedSockets := []Socket{
{
DeviceID: fmt.Sprintf(defaultDeviceIDTemplate, 0),
ID: fmt.Sprintf(defaultIDTemplate, 0),
HostPath: fmt.Sprintf(defaultSockPathTemplates[0], runStoragePath, pod.id),
Name: fmt.Sprintf(defaultChannelTemplate, 0),
},
{
DeviceID: fmt.Sprintf(defaultDeviceIDTemplate, 1),
ID: fmt.Sprintf(defaultIDTemplate, 1),
HostPath: fmt.Sprintf(defaultSockPathTemplates[1], runStoragePath, pod.id),
Name: fmt.Sprintf(defaultChannelTemplate, 1),
},
}
if !reflect.DeepEqual(expectedSockets, h.sockets) {
t.Fatalf("Expecting %+v, Got %+v", expectedSockets, h.sockets)
}
}
func testProcessHyperRoute(t *testing.T, route netlink.Route, deviceName string, expected *hyperstart.Route) {
h := &hyper{}
hyperRoute := h.processHyperRoute(route, deviceName)
if expected == nil {
if hyperRoute != nil {
t.Fatalf("Expecting route to be nil, Got %+v", hyperRoute)
} else {
return
}
}
// At this point, we know that "expected" != nil.
if !reflect.DeepEqual(*expected, *hyperRoute) {
t.Fatalf("Expecting %+v, Got %+v", *expected, *hyperRoute)
}
}
func TestProcessHyperRouteEmptyGWSuccessful(t *testing.T) {
expected := &hyperstart.Route{
Dest: testRouteDest,
Gateway: "",
Device: testRouteDeviceName,
}
_, dest, err := net.ParseCIDR(testRouteDest)
if err != nil {
t.Fatal(err)
}
route := netlink.Route{
Dst: dest,
Gw: net.IP{},
}
testProcessHyperRoute(t, route, testRouteDeviceName, expected)
}
func TestProcessHyperRouteEmptyDestSuccessful(t *testing.T) {
expected := &hyperstart.Route{
Dest: defaultRouteLabel,
Gateway: testRouteGateway,
Device: testRouteDeviceName,
}
_, dest, err := net.ParseCIDR(defaultRouteDest)
if err != nil {
t.Fatal(err)
}
route := netlink.Route{
Dst: dest,
Gw: net.ParseIP(testRouteGateway),
}
testProcessHyperRoute(t, route, testRouteDeviceName, expected)
}
func TestProcessHyperRouteDestIPv6Failure(t *testing.T) {
_, dest, err := net.ParseCIDR(testRouteDestIPv6)
if err != nil {
t.Fatal(err)
}
route := netlink.Route{
Dst: dest,
Gw: net.IP{},
}
testProcessHyperRoute(t, route, testRouteDeviceName, nil)
}

View File

@@ -0,0 +1,500 @@
//
// Copyright (c) 2016 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 virtcontainers
import (
"bufio"
"fmt"
"os"
"runtime"
"strconv"
"strings"
)
// HypervisorType describes an hypervisor type.
type HypervisorType string
const (
// QemuHypervisor is the QEMU hypervisor.
QemuHypervisor HypervisorType = "qemu"
// MockHypervisor is a mock hypervisor for testing purposes
MockHypervisor HypervisorType = "mock"
)
const (
procMemInfo = "/proc/meminfo"
procCPUInfo = "/proc/cpuinfo"
)
const (
defaultVCPUs = 1
// 2 GiB
defaultMemSzMiB = 2048
defaultBridges = 1
defaultBlockDriver = VirtioSCSI
)
// In some architectures the maximum number of vCPUs depends on the number of physical cores.
var defaultMaxQemuVCPUs = maxQemuVCPUs()
// deviceType describes a virtualized device type.
type deviceType int
const (
// ImgDev is the image device type.
imgDev deviceType = iota
// FsDev is the filesystem device type.
fsDev
// NetDev is the network device type.
netDev
// SerialDev is the serial device type.
serialDev
// BlockDev is the block device type.
blockDev
// ConsoleDev is the console device type.
consoleDev
// SerialPortDev is the serial port device type.
serialPortDev
// VFIODevice is VFIO device type
vfioDev
// vhostuserDev is a Vhost-user device type
vhostuserDev
// CPUDevice is CPU device type
cpuDev
)
// Set sets an hypervisor type based on the input string.
func (hType *HypervisorType) Set(value string) error {
switch value {
case "qemu":
*hType = QemuHypervisor
return nil
case "mock":
*hType = MockHypervisor
return nil
default:
return fmt.Errorf("Unknown hypervisor type %s", value)
}
}
// String converts an hypervisor type to a string.
func (hType *HypervisorType) String() string {
switch *hType {
case QemuHypervisor:
return string(QemuHypervisor)
case MockHypervisor:
return string(MockHypervisor)
default:
return ""
}
}
// newHypervisor returns an hypervisor from and hypervisor type.
func newHypervisor(hType HypervisorType) (hypervisor, error) {
switch hType {
case QemuHypervisor:
return &qemu{}, nil
case MockHypervisor:
return &mockHypervisor{}, nil
default:
return nil, fmt.Errorf("Unknown hypervisor type %s", hType)
}
}
//Generic function for creating a named-id for passing on the hypervisor commandline
func makeNameID(namedType string, id string) string {
nameID := fmt.Sprintf("%s-%s", namedType, id)
if len(nameID) > maxDevIDSize {
nameID = string(nameID[:maxDevIDSize])
}
return nameID
}
// Param is a key/value representation for hypervisor and kernel parameters.
type Param struct {
Key string
Value string
}
// HypervisorConfig is the hypervisor configuration.
type HypervisorConfig struct {
// KernelPath is the guest kernel host path.
KernelPath string
// ImagePath is the guest image host path.
ImagePath string
// FirmwarePath is the bios host path
FirmwarePath string
// MachineAccelerators are machine specific accelerators
MachineAccelerators string
// HypervisorPath is the hypervisor executable host path.
HypervisorPath string
// DisableBlockDeviceUse disallows a block device from being used.
DisableBlockDeviceUse bool
// BlockDeviceDriver specifies the driver to be used for block device
// either VirtioSCSI or VirtioBlock with the default driver being defaultBlockDriver
BlockDeviceDriver string
// KernelParams are additional guest kernel parameters.
KernelParams []Param
// HypervisorParams are additional hypervisor parameters.
HypervisorParams []Param
// HypervisorMachineType specifies the type of machine being
// emulated.
HypervisorMachineType string
// Debug changes the default hypervisor and kernel parameters to
// enable debug output where available.
Debug bool
// DefaultVCPUs specifies default number of vCPUs for the VM.
DefaultVCPUs uint32
//DefaultMaxVCPUs specifies the maximum number of vCPUs for the VM.
DefaultMaxVCPUs uint32
// DefaultMem specifies default memory size in MiB for the VM.
// Pod configuration VMConfig.Memory overwrites this.
DefaultMemSz uint32
// DefaultBridges specifies default number of bridges for the VM.
// Bridges can be used to hot plug devices
DefaultBridges uint32
// MemPrealloc specifies if the memory should be pre-allocated
MemPrealloc bool
// HugePages specifies if the memory should be pre-allocated from huge pages
HugePages bool
// Realtime Used to enable/disable realtime
Realtime bool
// Mlock is used to control memory locking when Realtime is enabled
// Realtime=true and Mlock=false, allows for swapping out of VM memory
// enabling higher density
Mlock bool
// DisableNestingChecks is used to override customizations performed
// when running on top of another VMM.
DisableNestingChecks bool
// customAssets is a map of assets.
// Each value in that map takes precedence over the configured assets.
// For example, if there is a value for the "kernel" key in this map,
// it will be used for the pod's kernel path instead of KernelPath.
customAssets map[assetType]*asset
}
func (conf *HypervisorConfig) valid() (bool, error) {
if conf.KernelPath == "" {
return false, fmt.Errorf("Missing kernel path")
}
if conf.ImagePath == "" {
return false, fmt.Errorf("Missing image path")
}
if conf.DefaultVCPUs == 0 {
conf.DefaultVCPUs = defaultVCPUs
}
if conf.DefaultMemSz == 0 {
conf.DefaultMemSz = defaultMemSzMiB
}
if conf.DefaultBridges == 0 {
conf.DefaultBridges = defaultBridges
}
if conf.BlockDeviceDriver == "" {
conf.BlockDeviceDriver = defaultBlockDriver
}
if conf.DefaultMaxVCPUs == 0 {
conf.DefaultMaxVCPUs = defaultMaxQemuVCPUs
}
return true, nil
}
// AddKernelParam allows the addition of new kernel parameters to an existing
// hypervisor configuration.
func (conf *HypervisorConfig) AddKernelParam(p Param) error {
if p.Key == "" {
return fmt.Errorf("Empty kernel parameter")
}
conf.KernelParams = append(conf.KernelParams, p)
return nil
}
func (conf *HypervisorConfig) addCustomAsset(a *asset) error {
if a == nil || a.path == "" {
// We did not get a custom asset, we will use the default one.
return nil
}
if !a.valid() {
return fmt.Errorf("Invalid %s at %s", a.kind, a.path)
}
virtLog.Debugf("Using custom %v asset %s", a.kind, a.path)
if conf.customAssets == nil {
conf.customAssets = make(map[assetType]*asset)
}
conf.customAssets[a.kind] = a
return nil
}
func (conf *HypervisorConfig) assetPath(t assetType) (string, error) {
// Custom assets take precedence over the configured ones
a, ok := conf.customAssets[t]
if ok {
return a.path, nil
}
// We could not find a custom asset for the given type, let's
// fall back to the configured ones.
switch t {
case kernelAsset:
return conf.KernelPath, nil
case imageAsset:
return conf.ImagePath, nil
case hypervisorAsset:
return conf.HypervisorPath, nil
case firmwareAsset:
return conf.FirmwarePath, nil
default:
return "", fmt.Errorf("Unknown asset type %v", t)
}
}
func (conf *HypervisorConfig) isCustomAsset(t assetType) bool {
_, ok := conf.customAssets[t]
if ok {
return true
}
return false
}
// KernelAssetPath returns the guest kernel path
func (conf *HypervisorConfig) KernelAssetPath() (string, error) {
return conf.assetPath(kernelAsset)
}
// CustomKernelAsset returns true if the kernel asset is a custom one, false otherwise.
func (conf *HypervisorConfig) CustomKernelAsset() bool {
return conf.isCustomAsset(kernelAsset)
}
// ImageAssetPath returns the guest image path
func (conf *HypervisorConfig) ImageAssetPath() (string, error) {
return conf.assetPath(imageAsset)
}
// CustomImageAsset returns true if the image asset is a custom one, false otherwise.
func (conf *HypervisorConfig) CustomImageAsset() bool {
return conf.isCustomAsset(imageAsset)
}
// HypervisorAssetPath returns the VM hypervisor path
func (conf *HypervisorConfig) HypervisorAssetPath() (string, error) {
return conf.assetPath(hypervisorAsset)
}
// CustomHypervisorAsset returns true if the hypervisor asset is a custom one, false otherwise.
func (conf *HypervisorConfig) CustomHypervisorAsset() bool {
return conf.isCustomAsset(hypervisorAsset)
}
// FirmwareAssetPath returns the guest firmware path
func (conf *HypervisorConfig) FirmwareAssetPath() (string, error) {
return conf.assetPath(firmwareAsset)
}
// CustomFirmwareAsset returns true if the firmware asset is a custom one, false otherwise.
func (conf *HypervisorConfig) CustomFirmwareAsset() bool {
return conf.isCustomAsset(firmwareAsset)
}
func appendParam(params []Param, parameter string, value string) []Param {
return append(params, Param{parameter, value})
}
// SerializeParams converts []Param to []string
func SerializeParams(params []Param, delim string) []string {
var parameters []string
for _, p := range params {
if p.Key == "" && p.Value == "" {
continue
} else if p.Key == "" {
parameters = append(parameters, fmt.Sprintf("%s", p.Value))
} else if p.Value == "" {
parameters = append(parameters, fmt.Sprintf("%s", p.Key))
} else if delim == "" {
parameters = append(parameters, fmt.Sprintf("%s", p.Key))
parameters = append(parameters, fmt.Sprintf("%s", p.Value))
} else {
parameters = append(parameters, fmt.Sprintf("%s%s%s", p.Key, delim, p.Value))
}
}
return parameters
}
// DeserializeParams converts []string to []Param
func DeserializeParams(parameters []string) []Param {
var params []Param
for _, param := range parameters {
if param == "" {
continue
}
p := strings.SplitN(param, "=", 2)
if len(p) == 2 {
params = append(params, Param{Key: p[0], Value: p[1]})
} else {
params = append(params, Param{Key: p[0], Value: ""})
}
}
return params
}
func getHostMemorySizeKb(memInfoPath string) (uint64, error) {
f, err := os.Open(memInfoPath)
if err != nil {
return 0, err
}
defer f.Close()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
// Expected format: ["MemTotal:", "1234", "kB"]
parts := strings.Fields(scanner.Text())
// Sanity checks: Skip malformed entries.
if len(parts) < 3 || parts[0] != "MemTotal:" || parts[2] != "kB" {
continue
}
sizeKb, err := strconv.ParseUint(parts[1], 0, 64)
if err != nil {
continue
}
return sizeKb, nil
}
// Handle errors that may have occurred during the reading of the file.
if err := scanner.Err(); err != nil {
return 0, err
}
return 0, fmt.Errorf("unable get MemTotal from %s", memInfoPath)
}
// RunningOnVMM checks if the system is running inside a VM.
func RunningOnVMM(cpuInfoPath string) (bool, error) {
if runtime.GOARCH == "arm64" {
virtLog.Debugf("Unable to know if the system is running inside a VM")
return false, nil
}
flagsField := "flags"
f, err := os.Open(cpuInfoPath)
if err != nil {
return false, err
}
defer f.Close()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
// Expected format: ["flags", ":", ...] or ["flags:", ...]
fields := strings.Fields(scanner.Text())
if len(fields) < 2 {
continue
}
if !strings.HasPrefix(fields[0], flagsField) {
continue
}
for _, field := range fields[1:] {
if field == "hypervisor" {
return true, nil
}
}
// As long as we have been able to analyze the fields from
// "flags", there is no reason to check what comes next from
// /proc/cpuinfo, because we already know we are not running
// on a VMM.
return false, nil
}
if err := scanner.Err(); err != nil {
return false, err
}
return false, fmt.Errorf("Couldn't find %q from %q output", flagsField, cpuInfoPath)
}
// hypervisor is the virtcontainers hypervisor interface.
// The default hypervisor implementation is Qemu.
type hypervisor interface {
init(pod *Pod) error
createPod(podConfig PodConfig) error
startPod() error
waitPod(timeout int) error
stopPod() error
pausePod() error
resumePod() error
addDevice(devInfo interface{}, devType deviceType) error
hotplugAddDevice(devInfo interface{}, devType deviceType) error
hotplugRemoveDevice(devInfo interface{}, devType deviceType) error
getPodConsole(podID string) string
capabilities() capabilities
}

View File

@@ -0,0 +1,502 @@
//
// Copyright (c) 2016 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 virtcontainers
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"reflect"
"testing"
)
func testSetHypervisorType(t *testing.T, value string, expected HypervisorType) {
var hypervisorType HypervisorType
err := (&hypervisorType).Set(value)
if err != nil {
t.Fatal(err)
}
if hypervisorType != expected {
t.Fatal()
}
}
func TestSetQemuHypervisorType(t *testing.T) {
testSetHypervisorType(t, "qemu", QemuHypervisor)
}
func TestSetMockHypervisorType(t *testing.T) {
testSetHypervisorType(t, "mock", MockHypervisor)
}
func TestSetUnknownHypervisorType(t *testing.T) {
var hypervisorType HypervisorType
err := (&hypervisorType).Set("unknown")
if err == nil {
t.Fatal()
}
if hypervisorType == QemuHypervisor ||
hypervisorType == MockHypervisor {
t.Fatal()
}
}
func testStringFromHypervisorType(t *testing.T, hypervisorType HypervisorType, expected string) {
hypervisorTypeStr := (&hypervisorType).String()
if hypervisorTypeStr != expected {
t.Fatal()
}
}
func TestStringFromQemuHypervisorType(t *testing.T) {
hypervisorType := QemuHypervisor
testStringFromHypervisorType(t, hypervisorType, "qemu")
}
func TestStringFromMockHypervisorType(t *testing.T) {
hypervisorType := MockHypervisor
testStringFromHypervisorType(t, hypervisorType, "mock")
}
func TestStringFromUnknownHypervisorType(t *testing.T) {
var hypervisorType HypervisorType
testStringFromHypervisorType(t, hypervisorType, "")
}
func testNewHypervisorFromHypervisorType(t *testing.T, hypervisorType HypervisorType, expected hypervisor) {
hy, err := newHypervisor(hypervisorType)
if err != nil {
t.Fatal(err)
}
if reflect.DeepEqual(hy, expected) == false {
t.Fatal()
}
}
func TestNewHypervisorFromQemuHypervisorType(t *testing.T) {
hypervisorType := QemuHypervisor
expectedHypervisor := &qemu{}
testNewHypervisorFromHypervisorType(t, hypervisorType, expectedHypervisor)
}
func TestNewHypervisorFromMockHypervisorType(t *testing.T) {
hypervisorType := MockHypervisor
expectedHypervisor := &mockHypervisor{}
testNewHypervisorFromHypervisorType(t, hypervisorType, expectedHypervisor)
}
func TestNewHypervisorFromUnknownHypervisorType(t *testing.T) {
var hypervisorType HypervisorType
hy, err := newHypervisor(hypervisorType)
if err == nil {
t.Fatal()
}
if hy != nil {
t.Fatal()
}
}
func testHypervisorConfigValid(t *testing.T, hypervisorConfig *HypervisorConfig, expected bool) {
ret, _ := hypervisorConfig.valid()
if ret != expected {
t.Fatal()
}
}
func TestHypervisorConfigNoKernelPath(t *testing.T) {
hypervisorConfig := &HypervisorConfig{
KernelPath: "",
ImagePath: fmt.Sprintf("%s/%s", testDir, testImage),
HypervisorPath: fmt.Sprintf("%s/%s", testDir, testHypervisor),
}
testHypervisorConfigValid(t, hypervisorConfig, false)
}
func TestHypervisorConfigNoImagePath(t *testing.T) {
hypervisorConfig := &HypervisorConfig{
KernelPath: fmt.Sprintf("%s/%s", testDir, testKernel),
ImagePath: "",
HypervisorPath: fmt.Sprintf("%s/%s", testDir, testHypervisor),
}
testHypervisorConfigValid(t, hypervisorConfig, false)
}
func TestHypervisorConfigNoHypervisorPath(t *testing.T) {
hypervisorConfig := &HypervisorConfig{
KernelPath: fmt.Sprintf("%s/%s", testDir, testKernel),
ImagePath: fmt.Sprintf("%s/%s", testDir, testImage),
HypervisorPath: "",
}
testHypervisorConfigValid(t, hypervisorConfig, true)
}
func TestHypervisorConfigIsValid(t *testing.T) {
hypervisorConfig := &HypervisorConfig{
KernelPath: fmt.Sprintf("%s/%s", testDir, testKernel),
ImagePath: fmt.Sprintf("%s/%s", testDir, testImage),
HypervisorPath: fmt.Sprintf("%s/%s", testDir, testHypervisor),
}
testHypervisorConfigValid(t, hypervisorConfig, true)
}
func TestHypervisorConfigDefaults(t *testing.T) {
hypervisorConfig := &HypervisorConfig{
KernelPath: fmt.Sprintf("%s/%s", testDir, testKernel),
ImagePath: fmt.Sprintf("%s/%s", testDir, testImage),
HypervisorPath: "",
}
testHypervisorConfigValid(t, hypervisorConfig, true)
hypervisorConfigDefaultsExpected := &HypervisorConfig{
KernelPath: fmt.Sprintf("%s/%s", testDir, testKernel),
ImagePath: fmt.Sprintf("%s/%s", testDir, testImage),
HypervisorPath: "",
DefaultVCPUs: defaultVCPUs,
DefaultMemSz: defaultMemSzMiB,
DefaultBridges: defaultBridges,
BlockDeviceDriver: defaultBlockDriver,
DefaultMaxVCPUs: defaultMaxQemuVCPUs,
}
if reflect.DeepEqual(hypervisorConfig, hypervisorConfigDefaultsExpected) == false {
t.Fatal()
}
}
func TestAppendParams(t *testing.T) {
paramList := []Param{
{
Key: "param1",
Value: "value1",
},
}
expectedParams := []Param{
{
Key: "param1",
Value: "value1",
},
{
Key: "param2",
Value: "value2",
},
}
paramList = appendParam(paramList, "param2", "value2")
if reflect.DeepEqual(paramList, expectedParams) == false {
t.Fatal()
}
}
func testSerializeParams(t *testing.T, params []Param, delim string, expected []string) {
result := SerializeParams(params, delim)
if reflect.DeepEqual(result, expected) == false {
t.Fatal()
}
}
func TestSerializeParamsNoParamNoValue(t *testing.T) {
params := []Param{
{
Key: "",
Value: "",
},
}
var expected []string
testSerializeParams(t, params, "", expected)
}
func TestSerializeParamsNoParam(t *testing.T) {
params := []Param{
{
Value: "value1",
},
}
expected := []string{"value1"}
testSerializeParams(t, params, "", expected)
}
func TestSerializeParamsNoValue(t *testing.T) {
params := []Param{
{
Key: "param1",
},
}
expected := []string{"param1"}
testSerializeParams(t, params, "", expected)
}
func TestSerializeParamsNoDelim(t *testing.T) {
params := []Param{
{
Key: "param1",
Value: "value1",
},
}
expected := []string{"param1", "value1"}
testSerializeParams(t, params, "", expected)
}
func TestSerializeParams(t *testing.T) {
params := []Param{
{
Key: "param1",
Value: "value1",
},
}
expected := []string{"param1=value1"}
testSerializeParams(t, params, "=", expected)
}
func testDeserializeParams(t *testing.T, parameters []string, expected []Param) {
result := DeserializeParams(parameters)
if reflect.DeepEqual(result, expected) == false {
t.Fatal()
}
}
func TestDeserializeParamsNil(t *testing.T) {
var parameters []string
var expected []Param
testDeserializeParams(t, parameters, expected)
}
func TestDeserializeParamsNoParamNoValue(t *testing.T) {
parameters := []string{
"",
}
var expected []Param
testDeserializeParams(t, parameters, expected)
}
func TestDeserializeParamsNoValue(t *testing.T) {
parameters := []string{
"param1",
}
expected := []Param{
{
Key: "param1",
},
}
testDeserializeParams(t, parameters, expected)
}
func TestDeserializeParams(t *testing.T) {
parameters := []string{
"param1=value1",
}
expected := []Param{
{
Key: "param1",
Value: "value1",
},
}
testDeserializeParams(t, parameters, expected)
}
func TestAddKernelParamValid(t *testing.T) {
var config HypervisorConfig
expected := []Param{
{"foo", "bar"},
}
err := config.AddKernelParam(expected[0])
if err != nil || reflect.DeepEqual(config.KernelParams, expected) == false {
t.Fatal()
}
}
func TestAddKernelParamInvalid(t *testing.T) {
var config HypervisorConfig
invalid := []Param{
{"", "bar"},
}
err := config.AddKernelParam(invalid[0])
if err == nil {
t.Fatal()
}
}
func TestGetHostMemorySizeKb(t *testing.T) {
type testData struct {
contents string
expectedResult int
expectError bool
}
data := []testData{
{
`
MemTotal: 1 kB
MemFree: 2 kB
SwapTotal: 3 kB
SwapFree: 4 kB
`,
1024,
false,
},
{
`
MemFree: 2 kB
SwapTotal: 3 kB
SwapFree: 4 kB
`,
0,
true,
},
}
dir, err := ioutil.TempDir("", "")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(dir)
file := filepath.Join(dir, "meminfo")
if _, err := getHostMemorySizeKb(file); err == nil {
t.Fatalf("expected failure as file %q does not exist", file)
}
for _, d := range data {
if err := ioutil.WriteFile(file, []byte(d.contents), os.FileMode(0640)); err != nil {
t.Fatal(err)
}
defer os.Remove(file)
hostMemKb, err := getHostMemorySizeKb(file)
if (d.expectError && err == nil) || (!d.expectError && err != nil) {
t.Fatalf("got %d, input %v", hostMemKb, d)
}
if reflect.DeepEqual(hostMemKb, d.expectedResult) {
t.Fatalf("got %d, input %v", hostMemKb, d)
}
}
}
var dataFlagsFieldWithoutHypervisor = []byte(`
fpu_exception : yes
cpuid level : 20
wp : yes
flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ss ht syscall nx pdpe1gb rdtscp lm constant_tsc rep_good nopl xtopology eagerfpu pni pclmulqdq vmx ssse3 fma cx16 sse4_1 sse4_2 movbe popcnt aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch tpr_shadow vnmi ept vpid fsgsbase bmi1 hle avx2 smep bmi2 erms rtm rdseed adx smap xsaveopt
bugs :
bogomips : 4589.35
`)
var dataFlagsFieldWithHypervisor = []byte(`
fpu_exception : yes
cpuid level : 20
wp : yes
flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ss ht syscall nx pdpe1gb rdtscp lm constant_tsc rep_good nopl xtopology eagerfpu pni pclmulqdq vmx ssse3 fma cx16 sse4_1 sse4_2 movbe popcnt aes xsave avx f16c rdrand hypervisor lahf_lm abm 3dnowprefetch tpr_shadow vnmi ept vpid fsgsbase bmi1 hle avx2 smep bmi2 erms rtm rdseed adx smap xsaveopt
bugs :
bogomips : 4589.35
`)
var dataWithoutFlagsField = []byte(`
fpu_exception : yes
cpuid level : 20
wp : yes
bugs :
bogomips : 4589.35
`)
func testRunningOnVMMSuccessful(t *testing.T, cpuInfoContent []byte, expectedErr bool, expected bool) {
f, err := ioutil.TempFile("", "cpuinfo")
if err != nil {
t.Fatal(err)
}
defer os.Remove(f.Name())
defer f.Close()
n, err := f.Write(cpuInfoContent)
if err != nil {
t.Fatal(err)
}
if n != len(cpuInfoContent) {
t.Fatalf("Only %d bytes written out of %d expected", n, len(cpuInfoContent))
}
running, err := RunningOnVMM(f.Name())
if !expectedErr && err != nil {
t.Fatalf("This test should succeed: %v", err)
} else if expectedErr && err == nil {
t.Fatalf("This test should fail")
}
if running != expected {
t.Fatalf("Expecting running on VMM = %t, Got %t", expected, running)
}
}
func TestRunningOnVMMFalseSuccessful(t *testing.T) {
testRunningOnVMMSuccessful(t, dataFlagsFieldWithoutHypervisor, false, false)
}
func TestRunningOnVMMTrueSuccessful(t *testing.T) {
testRunningOnVMMSuccessful(t, dataFlagsFieldWithHypervisor, false, true)
}
func TestRunningOnVMMNoFlagsFieldFailure(t *testing.T) {
testRunningOnVMMSuccessful(t, dataWithoutFlagsField, true, false)
}
func TestRunningOnVMMNotExistingCPUInfoPathFailure(t *testing.T) {
f, err := ioutil.TempFile("", "cpuinfo")
if err != nil {
t.Fatal(err)
}
filePath := f.Name()
f.Close()
os.Remove(filePath)
if _, err := RunningOnVMM(filePath); err == nil {
t.Fatalf("Should fail because %q file path does not exist", filePath)
}
}

View File

@@ -0,0 +1,119 @@
// 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.
// Description: The true virtcontainers function of the same name.
// This indirection is required to allow an alternative implemenation to be
// used for testing purposes.
package virtcontainers
import (
"syscall"
"github.com/sirupsen/logrus"
)
// VCImpl is the official virtcontainers function of the same name.
type VCImpl struct {
}
// SetLogger implements the VC function of the same name.
func (impl *VCImpl) SetLogger(logger logrus.FieldLogger) {
SetLogger(logger)
}
// CreatePod implements the VC function of the same name.
func (impl *VCImpl) CreatePod(podConfig PodConfig) (VCPod, error) {
return CreatePod(podConfig)
}
// DeletePod implements the VC function of the same name.
func (impl *VCImpl) DeletePod(podID string) (VCPod, error) {
return DeletePod(podID)
}
// StartPod implements the VC function of the same name.
func (impl *VCImpl) StartPod(podID string) (VCPod, error) {
return StartPod(podID)
}
// StopPod implements the VC function of the same name.
func (impl *VCImpl) StopPod(podID string) (VCPod, error) {
return StopPod(podID)
}
// RunPod implements the VC function of the same name.
func (impl *VCImpl) RunPod(podConfig PodConfig) (VCPod, error) {
return RunPod(podConfig)
}
// ListPod implements the VC function of the same name.
func (impl *VCImpl) ListPod() ([]PodStatus, error) {
return ListPod()
}
// StatusPod implements the VC function of the same name.
func (impl *VCImpl) StatusPod(podID string) (PodStatus, error) {
return StatusPod(podID)
}
// PausePod implements the VC function of the same name.
func (impl *VCImpl) PausePod(podID string) (VCPod, error) {
return PausePod(podID)
}
// ResumePod implements the VC function of the same name.
func (impl *VCImpl) ResumePod(podID string) (VCPod, error) {
return ResumePod(podID)
}
// CreateContainer implements the VC function of the same name.
func (impl *VCImpl) CreateContainer(podID string, containerConfig ContainerConfig) (VCPod, VCContainer, error) {
return CreateContainer(podID, containerConfig)
}
// DeleteContainer implements the VC function of the same name.
func (impl *VCImpl) DeleteContainer(podID, containerID string) (VCContainer, error) {
return DeleteContainer(podID, containerID)
}
// StartContainer implements the VC function of the same name.
func (impl *VCImpl) StartContainer(podID, containerID string) (VCContainer, error) {
return StartContainer(podID, containerID)
}
// StopContainer implements the VC function of the same name.
func (impl *VCImpl) StopContainer(podID, containerID string) (VCContainer, error) {
return StopContainer(podID, containerID)
}
// EnterContainer implements the VC function of the same name.
func (impl *VCImpl) EnterContainer(podID, containerID string, cmd Cmd) (VCPod, VCContainer, *Process, error) {
return EnterContainer(podID, containerID, cmd)
}
// StatusContainer implements the VC function of the same name.
func (impl *VCImpl) StatusContainer(podID, containerID string) (ContainerStatus, error) {
return StatusContainer(podID, containerID)
}
// KillContainer implements the VC function of the same name.
func (impl *VCImpl) KillContainer(podID, containerID string, signal syscall.Signal, all bool) error {
return KillContainer(podID, containerID, signal, all)
}
// ProcessListContainer implements the VC function of the same name.
func (impl *VCImpl) ProcessListContainer(podID, containerID string, options ProcessListOptions) (ProcessList, error) {
return ProcessListContainer(podID, containerID, options)
}

View File

@@ -0,0 +1,68 @@
// 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 virtcontainers
import (
"syscall"
"github.com/sirupsen/logrus"
)
// VC is the Virtcontainers interface
type VC interface {
SetLogger(logger logrus.FieldLogger)
CreatePod(podConfig PodConfig) (VCPod, error)
DeletePod(podID string) (VCPod, error)
ListPod() ([]PodStatus, error)
PausePod(podID string) (VCPod, error)
ResumePod(podID string) (VCPod, error)
RunPod(podConfig PodConfig) (VCPod, error)
StartPod(podID string) (VCPod, error)
StatusPod(podID string) (PodStatus, error)
StopPod(podID string) (VCPod, error)
CreateContainer(podID string, containerConfig ContainerConfig) (VCPod, VCContainer, error)
DeleteContainer(podID, containerID string) (VCContainer, error)
EnterContainer(podID, containerID string, cmd Cmd) (VCPod, VCContainer, *Process, error)
KillContainer(podID, containerID string, signal syscall.Signal, all bool) error
StartContainer(podID, containerID string) (VCContainer, error)
StatusContainer(podID, containerID string) (ContainerStatus, error)
StopContainer(podID, containerID string) (VCContainer, error)
ProcessListContainer(podID, containerID string, options ProcessListOptions) (ProcessList, error)
}
// VCPod is the Pod interface
// (required since virtcontainers.Pod only contains private fields)
type VCPod interface {
Annotations(key string) (string, error)
GetAllContainers() []VCContainer
GetAnnotations() map[string]string
GetContainer(containerID string) VCContainer
ID() string
SetAnnotations(annotations map[string]string) error
}
// VCContainer is the Container interface
// (required since virtcontainers.Container only contains private fields)
type VCContainer interface {
GetAnnotations() map[string]string
GetPid() int
GetToken() string
ID() string
Pod() VCPod
Process() Process
SetPid(pid int) error
}

View File

@@ -0,0 +1,833 @@
//
// 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 virtcontainers
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"syscall"
vcAnnotations "github.com/containers/virtcontainers/pkg/annotations"
ns "github.com/containers/virtcontainers/pkg/nsenter"
"github.com/containers/virtcontainers/pkg/uuid"
kataclient "github.com/kata-containers/agent/protocols/client"
"github.com/kata-containers/agent/protocols/grpc"
"github.com/opencontainers/runtime-spec/specs-go"
"github.com/sirupsen/logrus"
)
var (
defaultKataSockPathTemplate = "%s/%s/kata.sock"
defaultKataChannel = "agent.channel.0"
defaultKataDeviceID = "channel0"
defaultKataID = "charch0"
errorMissingProxy = errors.New("Missing proxy pointer")
errorMissingOCISpec = errors.New("Missing OCI specification")
kataHostSharedDir = "/run/kata-containers/shared/pods/"
kataGuestSharedDir = "/run/kata-containers/shared/containers/"
mountGuest9pTag = "kataShared"
type9pFs = "9p"
devPath = "/dev"
vsockSocketScheme = "vsock"
kataBlkDevType = "blk"
)
// KataAgentConfig is a structure storing information needed
// to reach the Kata Containers agent.
type KataAgentConfig struct {
GRPCSocket string
}
type kataVSOCK struct {
contextID uint32
port uint32
}
func (s *kataVSOCK) String() string {
return fmt.Sprintf("%s://%d:%d", vsockSocketScheme, s.contextID, s.port)
}
// KataAgentState is the structure describing the data stored from this
// agent implementation.
type KataAgentState struct {
ProxyPid int
URL string
}
type kataAgent struct {
shim shim
proxy proxy
client *kataclient.AgentClient
state KataAgentState
vmSocket interface{}
}
func (k *kataAgent) Logger() *logrus.Entry {
return virtLog.WithField("subsystem", "kata_agent")
}
func parseVSOCKAddr(sock string) (uint32, uint32, error) {
sp := strings.Split(sock, ":")
if len(sp) != 3 {
return 0, 0, fmt.Errorf("Invalid vsock address: %s", sock)
}
if sp[0] != vsockSocketScheme {
return 0, 0, fmt.Errorf("Invalid vsock URL scheme: %s", sp[0])
}
cid, err := strconv.ParseUint(sp[1], 10, 32)
if err != nil {
return 0, 0, fmt.Errorf("Invalid vsock cid: %s", sp[1])
}
port, err := strconv.ParseUint(sp[2], 10, 32)
if err != nil {
return 0, 0, fmt.Errorf("Invalid vsock port: %s", sp[2])
}
return uint32(cid), uint32(port), nil
}
func (k *kataAgent) generateVMSocket(pod Pod, c KataAgentConfig) error {
cid, port, err := parseVSOCKAddr(c.GRPCSocket)
if err != nil {
// We need to generate a host UNIX socket path for the emulated serial port.
k.vmSocket = Socket{
DeviceID: defaultKataDeviceID,
ID: defaultKataID,
HostPath: fmt.Sprintf(defaultKataSockPathTemplate, runStoragePath, pod.id),
Name: defaultKataChannel,
}
} else {
// We want to go through VSOCK. The VM VSOCK endpoint will be our gRPC.
k.vmSocket = kataVSOCK{
contextID: cid,
port: port,
}
}
return nil
}
func (k *kataAgent) init(pod *Pod, config interface{}) (err error) {
switch c := config.(type) {
case KataAgentConfig:
if err := k.generateVMSocket(*pod, c); err != nil {
return err
}
default:
return fmt.Errorf("Invalid config type")
}
k.proxy, err = newProxy(pod.config.ProxyType)
if err != nil {
return err
}
k.shim, err = newShim(pod.config.ShimType)
if err != nil {
return err
}
// Fetch agent runtime info.
if err := pod.storage.fetchAgentState(pod.id, &k.state); err != nil {
k.Logger().Debug("Could not retrieve anything from storage")
}
return nil
}
func (k *kataAgent) agentURL() (string, error) {
switch s := k.vmSocket.(type) {
case Socket:
return s.HostPath, nil
case kataVSOCK:
return s.String(), nil
default:
return "", fmt.Errorf("Invalid socket type")
}
}
func (k *kataAgent) capabilities() capabilities {
var caps capabilities
// add all capabilities supported by agent
caps.setBlockDeviceSupport()
return caps
}
func (k *kataAgent) createPod(pod *Pod) error {
switch s := k.vmSocket.(type) {
case Socket:
err := pod.hypervisor.addDevice(s, serialPortDev)
if err != nil {
return err
}
case kataVSOCK:
// TODO Add an hypervisor vsock
default:
return fmt.Errorf("Invalid config type")
}
// Adding the shared volume.
// This volume contains all bind mounted container bundles.
sharedVolume := Volume{
MountTag: mountGuest9pTag,
HostPath: filepath.Join(kataHostSharedDir, pod.id),
}
if err := os.MkdirAll(sharedVolume.HostPath, dirMode); err != nil {
return err
}
return pod.hypervisor.addDevice(sharedVolume, fsDev)
}
func cmdToKataProcess(cmd Cmd) (process *grpc.Process, err error) {
var i uint64
var extraGids []uint32
// Number of bits used to store user+group values in
// the gRPC "User" type.
const grpcUserBits = 32
// User can contain only the "uid" or it can contain "uid:gid".
parsedUser := strings.Split(cmd.User, ":")
if len(parsedUser) > 2 {
return nil, fmt.Errorf("cmd.User %q format is wrong", cmd.User)
}
i, err = strconv.ParseUint(parsedUser[0], 10, grpcUserBits)
if err != nil {
return nil, err
}
uid := uint32(i)
var gid uint32
if len(parsedUser) > 1 {
i, err = strconv.ParseUint(parsedUser[1], 10, grpcUserBits)
if err != nil {
return nil, err
}
gid = uint32(i)
}
if cmd.PrimaryGroup != "" {
i, err = strconv.ParseUint(cmd.PrimaryGroup, 10, grpcUserBits)
if err != nil {
return nil, err
}
gid = uint32(i)
}
for _, g := range cmd.SupplementaryGroups {
var extraGid uint64
extraGid, err = strconv.ParseUint(g, 10, grpcUserBits)
if err != nil {
return nil, err
}
extraGids = append(extraGids, uint32(extraGid))
}
process = &grpc.Process{
Terminal: cmd.Interactive,
User: grpc.User{
UID: uid,
GID: gid,
AdditionalGids: extraGids,
},
Args: cmd.Args,
Env: cmdEnvsToStringSlice(cmd.Envs),
Cwd: cmd.WorkDir,
}
return process, nil
}
func cmdEnvsToStringSlice(ev []EnvVar) []string {
var env []string
for _, e := range ev {
pair := []string{e.Var, e.Value}
env = append(env, strings.Join(pair, "="))
}
return env
}
func (k *kataAgent) exec(pod *Pod, c Container, cmd Cmd) (*Process, error) {
var kataProcess *grpc.Process
kataProcess, err := cmdToKataProcess(cmd)
if err != nil {
return nil, err
}
req := &grpc.ExecProcessRequest{
ContainerId: c.id,
ExecId: uuid.Generate().String(),
Process: kataProcess,
}
if _, err := k.sendReq(req); err != nil {
return nil, err
}
enterNSList := []ns.Namespace{
{
PID: c.process.Pid,
Type: ns.NSTypeNet,
},
{
PID: c.process.Pid,
Type: ns.NSTypePID,
},
}
return prepareAndStartShim(pod, k.shim, c.id, req.ExecId,
k.state.URL, cmd, []ns.NSType{}, enterNSList)
}
func (k *kataAgent) generateInterfacesAndRoutes(networkNS NetworkNamespace) ([]*grpc.Interface, []*grpc.Route, error) {
if networkNS.NetNsPath == "" {
return nil, nil, nil
}
var routes []*grpc.Route
var ifaces []*grpc.Interface
for _, endpoint := range networkNS.Endpoints {
var ipAddresses []*grpc.IPAddress
for _, addr := range endpoint.Properties().Addrs {
// Skip IPv6 because not supported
if addr.IP.To4() == nil {
// Skip IPv6 because not supported
k.Logger().WithFields(logrus.Fields{
"unsupported-address-type": "ipv6",
"address": addr,
}).Warn("unsupported address")
continue
}
// Skip localhost interface
if addr.IP.IsLoopback() {
continue
}
netMask, _ := addr.Mask.Size()
ipAddress := grpc.IPAddress{
Family: grpc.IPFamily_v4,
Address: addr.IP.String(),
Mask: fmt.Sprintf("%d", netMask),
}
ipAddresses = append(ipAddresses, &ipAddress)
}
ifc := grpc.Interface{
IPAddresses: ipAddresses,
Device: endpoint.Name(),
Name: endpoint.Name(),
Mtu: uint64(endpoint.Properties().Iface.MTU),
HwAddr: endpoint.HardwareAddr(),
}
ifaces = append(ifaces, &ifc)
for _, route := range endpoint.Properties().Routes {
var r grpc.Route
if route.Dst != nil {
r.Dest = route.Dst.String()
if route.Dst.IP.To4() == nil {
// Skip IPv6 because not supported
k.Logger().WithFields(logrus.Fields{
"unsupported-route-type": "ipv6",
"destination": r.Dest,
}).Warn("unsupported route")
continue
}
}
if route.Gw != nil {
r.Gateway = route.Gw.String()
}
if route.Src != nil {
r.Source = route.Src.String()
}
r.Device = endpoint.Name()
r.Scope = uint32(route.Scope)
routes = append(routes, &r)
}
}
return ifaces, routes, nil
}
func (k *kataAgent) startPod(pod Pod) error {
if k.proxy == nil {
return errorMissingProxy
}
// Get agent socket path to provide it to the proxy.
agentURL, err := k.agentURL()
if err != nil {
return err
}
proxyParams := proxyParams{
agentURL: agentURL,
}
// Start the proxy here
pid, uri, err := k.proxy.start(pod, proxyParams)
if err != nil {
return err
}
// Fill agent state with proxy information, and store them.
k.state.ProxyPid = pid
k.state.URL = uri
if err := pod.storage.storeAgentState(pod.id, k.state); err != nil {
return err
}
k.Logger().WithField("proxy-pid", pid).Info("proxy started")
hostname := pod.config.Hostname
if len(hostname) > maxHostnameLen {
hostname = hostname[:maxHostnameLen]
}
//
// Setup network interfaces and routes
//
interfaces, routes, err := k.generateInterfacesAndRoutes(pod.networkNS)
if err != nil {
return err
}
for _, ifc := range interfaces {
// send update interface request
ifcReq := &grpc.UpdateInterfaceRequest{
Interface: ifc,
}
resultingInterface, err := k.sendReq(ifcReq)
if err != nil {
k.Logger().WithFields(logrus.Fields{
"interface-requested": fmt.Sprintf("%+v", ifc),
"resulting-interface": fmt.Sprintf("%+v", resultingInterface),
}).WithError(err).Error("update interface request failed")
return err
}
}
if routes != nil {
routesReq := &grpc.UpdateRoutesRequest{
Routes: &grpc.Routes{
Routes: routes,
},
}
resultingRoutes, err := k.sendReq(routesReq)
if err != nil {
k.Logger().WithFields(logrus.Fields{
"routes-requested": fmt.Sprintf("%+v", routes),
"resulting-routes": fmt.Sprintf("%+v", resultingRoutes),
}).WithError(err).Error("update routes request failed")
return err
}
}
// We mount the shared directory in a predefined location
// in the guest.
// This is where at least some of the host config files
// (resolv.conf, etc...) and potentially all container
// rootfs will reside.
sharedVolume := &grpc.Storage{
Source: mountGuest9pTag,
MountPoint: kataGuestSharedDir,
Fstype: type9pFs,
Options: []string{"trans=virtio", "nodev"},
}
req := &grpc.CreateSandboxRequest{
Hostname: hostname,
Storages: []*grpc.Storage{sharedVolume},
SandboxPidns: false,
}
_, err = k.sendReq(req)
return err
}
func (k *kataAgent) stopPod(pod Pod) error {
if k.proxy == nil {
return errorMissingProxy
}
req := &grpc.DestroySandboxRequest{}
if _, err := k.sendReq(req); err != nil {
return err
}
return k.proxy.stop(pod, k.state.ProxyPid)
}
func appendStorageFromMounts(storage []*grpc.Storage, mounts []*Mount) []*grpc.Storage {
for _, m := range mounts {
s := &grpc.Storage{
Source: m.Source,
MountPoint: m.Destination,
Fstype: m.Type,
Options: m.Options,
}
storage = append(storage, s)
}
return storage
}
func (k *kataAgent) replaceOCIMountSource(spec *specs.Spec, guestMounts []Mount) error {
ociMounts := spec.Mounts
for index, m := range ociMounts {
for _, guestMount := range guestMounts {
if guestMount.Destination != m.Destination {
continue
}
k.Logger().Debugf("Replacing OCI mount (%s) source %s with %s", m.Destination, m.Source, guestMount.Source)
ociMounts[index].Source = guestMount.Source
}
}
return nil
}
func constraintGRPCSpec(grpcSpec *grpc.Spec) {
// Disable Hooks since they have been handled on the host and there is
// no reason to send them to the agent. It would make no sense to try
// to apply them on the guest.
grpcSpec.Hooks = nil
// Disable Seccomp since they cannot be handled properly by the agent
// until we provide a guest image with libseccomp support. More details
// here: https://github.com/kata-containers/agent/issues/104
grpcSpec.Linux.Seccomp = nil
// TODO: Remove this constraint as soon as the agent properly handles
// resources provided through the specification.
grpcSpec.Linux.Resources = nil
// Disable network namespace since it is already handled on the host by
// virtcontainers. The network is a complex part which cannot be simply
// passed to the agent.
// Every other namespaces's paths have to be emptied. This way, there
// is no confusion from the agent, trying to find an existing namespace
// on the guest.
var tmpNamespaces []grpc.LinuxNamespace
for _, ns := range grpcSpec.Linux.Namespaces {
switch ns.Type {
case specs.NetworkNamespace:
default:
ns.Path = ""
tmpNamespaces = append(tmpNamespaces, ns)
}
}
grpcSpec.Linux.Namespaces = tmpNamespaces
// Handle /dev/shm mount
for idx, mnt := range grpcSpec.Mounts {
if mnt.Destination == "/dev/shm" {
grpcSpec.Mounts[idx].Type = "tmpfs"
grpcSpec.Mounts[idx].Source = "shm"
grpcSpec.Mounts[idx].Options = []string{"noexec", "nosuid", "nodev", "mode=1777", "size=65536k"}
break
}
}
}
func (k *kataAgent) createContainer(pod *Pod, c *Container) (*Process, error) {
ociSpecJSON, ok := c.config.Annotations[vcAnnotations.ConfigJSONKey]
if !ok {
return nil, errorMissingOCISpec
}
var ctrStorages []*grpc.Storage
var ctrDevices []*grpc.Device
// The rootfs storage volume represents the container rootfs
// mount point inside the guest.
// It can be a block based device (when using block based container
// overlay on the host) mount or a 9pfs one (for all other overlay
// implementations).
rootfs := &grpc.Storage{}
// This is the guest absolute root path for that container.
rootPathParent := filepath.Join(kataGuestSharedDir, c.id)
rootPath := filepath.Join(rootPathParent, rootfsDir)
if c.state.Fstype != "" {
// This is a block based device rootfs.
// driveName is the predicted virtio-block guest name (the vd* in /dev/vd*).
driveName, err := getVirtDriveName(c.state.BlockIndex)
if err != nil {
return nil, err
}
virtPath := filepath.Join(devPath, driveName)
// Create a new device with empty ContainerPath so that we get
// the device being waited for by the agent inside the VM,
// without trying to match and update it into the OCI spec list
// of actual devices. The device corresponding to the rootfs is
// a very specific case.
rootfsDevice := &grpc.Device{
Type: kataBlkDevType,
VmPath: virtPath,
ContainerPath: "",
}
ctrDevices = append(ctrDevices, rootfsDevice)
rootfs.Source = virtPath
rootfs.MountPoint = rootPathParent
rootfs.Fstype = c.state.Fstype
if c.state.Fstype == "xfs" {
rootfs.Options = []string{"nouuid"}
}
// Add rootfs to the list of container storage.
// We only need to do this for block based rootfs, as we
// want the agent to mount it into the right location
// (kataGuestSharedDir/ctrID/
ctrStorages = append(ctrStorages, rootfs)
} else {
// This is not a block based device rootfs.
// We are going to bind mount it into the 9pfs
// shared drive between the host and the guest.
// With 9pfs we don't need to ask the agent to
// mount the rootfs as the shared directory
// (kataGuestSharedDir) is already mounted in the
// guest. We only need to mount the rootfs from
// the host and it will show up in the guest.
if err := bindMountContainerRootfs(kataHostSharedDir, pod.id, c.id, c.rootFs, false); err != nil {
bindUnmountAllRootfs(kataHostSharedDir, *pod)
return nil, err
}
}
ociSpec := &specs.Spec{}
if err := json.Unmarshal([]byte(ociSpecJSON), ociSpec); err != nil {
return nil, err
}
// Handle container mounts
newMounts, err := c.mountSharedDirMounts(kataHostSharedDir, kataGuestSharedDir)
if err != nil {
bindUnmountAllRootfs(kataHostSharedDir, *pod)
return nil, err
}
// We replace all OCI mount sources that match our container mount
// with the right source path (The guest one).
if err := k.replaceOCIMountSource(ociSpec, newMounts); err != nil {
return nil, err
}
grpcSpec, err := grpc.OCItoGRPC(ociSpec)
if err != nil {
return nil, err
}
// We need to give the OCI spec our absolute rootfs path in the guest.
grpcSpec.Root.Path = rootPath
// We need to constraint the spec to make sure we're not passing
// irrelevant information to the agent.
constraintGRPCSpec(grpcSpec)
// Append container mounts for block devices passed with --device.
for _, device := range c.devices {
d, ok := device.(*BlockDevice)
if !ok {
continue
}
kataDevice := &grpc.Device{
Type: kataBlkDevType,
VmPath: d.VirtPath,
ContainerPath: d.DeviceInfo.ContainerPath,
}
ctrDevices = append(ctrDevices, kataDevice)
}
req := &grpc.CreateContainerRequest{
ContainerId: c.id,
ExecId: c.id,
Storages: ctrStorages,
Devices: ctrDevices,
OCI: grpcSpec,
}
if _, err := k.sendReq(req); err != nil {
return nil, err
}
createNSList := []ns.NSType{ns.NSTypePID}
enterNSList := []ns.Namespace{
{
Path: pod.networkNS.NetNsPath,
Type: ns.NSTypeNet,
},
}
return prepareAndStartShim(pod, k.shim, c.id, req.ExecId,
k.state.URL, c.config.Cmd, createNSList, enterNSList)
}
func (k *kataAgent) startContainer(pod Pod, c *Container) error {
req := &grpc.StartContainerRequest{
ContainerId: c.id,
}
_, err := k.sendReq(req)
return err
}
func (k *kataAgent) stopContainer(pod Pod, c Container) error {
req := &grpc.RemoveContainerRequest{
ContainerId: c.id,
}
if _, err := k.sendReq(req); err != nil {
return err
}
if err := c.unmountHostMounts(); err != nil {
return err
}
if err := bindUnmountContainerRootfs(kataHostSharedDir, pod.id, c.id); err != nil {
return err
}
return nil
}
func (k *kataAgent) killContainer(pod Pod, c Container, signal syscall.Signal, all bool) error {
req := &grpc.SignalProcessRequest{
ContainerId: c.id,
ExecId: c.process.Token,
Signal: uint32(signal),
}
_, err := k.sendReq(req)
return err
}
func (k *kataAgent) processListContainer(pod Pod, c Container, options ProcessListOptions) (ProcessList, error) {
return nil, nil
}
func (k *kataAgent) connect() error {
if k.client != nil {
return nil
}
client, err := kataclient.NewAgentClient(k.state.URL)
if err != nil {
return err
}
k.client = client
return nil
}
func (k *kataAgent) disconnect() error {
if k.client == nil {
return nil
}
if err := k.client.Close(); err != nil {
return err
}
k.client = nil
return nil
}
func (k *kataAgent) sendReq(request interface{}) (interface{}, error) {
if err := k.connect(); err != nil {
return nil, err
}
defer k.disconnect()
switch req := request.(type) {
case *grpc.ExecProcessRequest:
_, err := k.client.ExecProcess(context.Background(), req)
return nil, err
case *grpc.CreateSandboxRequest:
_, err := k.client.CreateSandbox(context.Background(), req)
return nil, err
case *grpc.DestroySandboxRequest:
_, err := k.client.DestroySandbox(context.Background(), req)
return nil, err
case *grpc.CreateContainerRequest:
_, err := k.client.CreateContainer(context.Background(), req)
return nil, err
case *grpc.StartContainerRequest:
_, err := k.client.StartContainer(context.Background(), req)
return nil, err
case *grpc.RemoveContainerRequest:
_, err := k.client.RemoveContainer(context.Background(), req)
return nil, err
case *grpc.SignalProcessRequest:
_, err := k.client.SignalProcess(context.Background(), req)
return nil, err
case *grpc.UpdateRoutesRequest:
_, err := k.client.UpdateRoutes(context.Background(), req)
return nil, err
case *grpc.UpdateInterfaceRequest:
ifc, err := k.client.UpdateInterface(context.Background(), req)
return ifc, err
default:
return nil, fmt.Errorf("Unknown gRPC type %T", req)
}
}

View File

@@ -0,0 +1,338 @@
//
// 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 virtcontainers
import (
"fmt"
"io/ioutil"
"net"
"os"
"reflect"
"testing"
"github.com/containers/virtcontainers/pkg/mock"
gpb "github.com/gogo/protobuf/types"
pb "github.com/kata-containers/agent/protocols/grpc"
"github.com/stretchr/testify/assert"
"github.com/vishvananda/netlink"
"golang.org/x/net/context"
"google.golang.org/grpc"
)
var testKataProxyURLTempl = "unix://%s/kata-proxy-test.sock"
func proxyHandlerDiscard(c net.Conn) {
buf := make([]byte, 1024)
c.Read(buf)
}
func testGenerateKataProxySockDir() (string, error) {
dir, err := ioutil.TempDir("", "kata-proxy-test")
if err != nil {
return "", err
}
return dir, nil
}
func TestKataAgentConnect(t *testing.T) {
proxy := mock.ProxyUnixMock{
ClientHandler: proxyHandlerDiscard,
}
sockDir, err := testGenerateKataProxySockDir()
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(sockDir)
testKataProxyURL := fmt.Sprintf(testKataProxyURLTempl, sockDir)
if err := proxy.Start(testKataProxyURL); err != nil {
t.Fatal(err)
}
defer proxy.Stop()
k := &kataAgent{
state: KataAgentState{
URL: testKataProxyURL,
},
}
if err := k.connect(); err != nil {
t.Fatal(err)
}
if k.client == nil {
t.Fatal("Kata agent client is not properly initialized")
}
}
func TestKataAgentDisconnect(t *testing.T) {
proxy := mock.ProxyUnixMock{
ClientHandler: proxyHandlerDiscard,
}
sockDir, err := testGenerateKataProxySockDir()
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(sockDir)
testKataProxyURL := fmt.Sprintf(testKataProxyURLTempl, sockDir)
if err := proxy.Start(testKataProxyURL); err != nil {
t.Fatal(err)
}
defer proxy.Stop()
k := &kataAgent{
state: KataAgentState{
URL: testKataProxyURL,
},
}
if err := k.connect(); err != nil {
t.Fatal(err)
}
if err := k.disconnect(); err != nil {
t.Fatal(err)
}
if k.client != nil {
t.Fatal("Kata agent client pointer should be nil")
}
}
type gRPCProxy struct{}
var emptyResp = &gpb.Empty{}
func (p *gRPCProxy) CreateContainer(ctx context.Context, req *pb.CreateContainerRequest) (*gpb.Empty, error) {
return emptyResp, nil
}
func (p *gRPCProxy) StartContainer(ctx context.Context, req *pb.StartContainerRequest) (*gpb.Empty, error) {
return emptyResp, nil
}
func (p *gRPCProxy) ExecProcess(ctx context.Context, req *pb.ExecProcessRequest) (*gpb.Empty, error) {
return emptyResp, nil
}
func (p *gRPCProxy) SignalProcess(ctx context.Context, req *pb.SignalProcessRequest) (*gpb.Empty, error) {
return emptyResp, nil
}
func (p *gRPCProxy) WaitProcess(ctx context.Context, req *pb.WaitProcessRequest) (*pb.WaitProcessResponse, error) {
return &pb.WaitProcessResponse{}, nil
}
func (p *gRPCProxy) RemoveContainer(ctx context.Context, req *pb.RemoveContainerRequest) (*gpb.Empty, error) {
return emptyResp, nil
}
func (p *gRPCProxy) WriteStdin(ctx context.Context, req *pb.WriteStreamRequest) (*pb.WriteStreamResponse, error) {
return &pb.WriteStreamResponse{}, nil
}
func (p *gRPCProxy) ReadStdout(ctx context.Context, req *pb.ReadStreamRequest) (*pb.ReadStreamResponse, error) {
return &pb.ReadStreamResponse{}, nil
}
func (p *gRPCProxy) ReadStderr(ctx context.Context, req *pb.ReadStreamRequest) (*pb.ReadStreamResponse, error) {
return &pb.ReadStreamResponse{}, nil
}
func (p *gRPCProxy) CloseStdin(ctx context.Context, req *pb.CloseStdinRequest) (*gpb.Empty, error) {
return emptyResp, nil
}
func (p *gRPCProxy) TtyWinResize(ctx context.Context, req *pb.TtyWinResizeRequest) (*gpb.Empty, error) {
return emptyResp, nil
}
func (p *gRPCProxy) CreateSandbox(ctx context.Context, req *pb.CreateSandboxRequest) (*gpb.Empty, error) {
return emptyResp, nil
}
func (p *gRPCProxy) DestroySandbox(ctx context.Context, req *pb.DestroySandboxRequest) (*gpb.Empty, error) {
return emptyResp, nil
}
func (p *gRPCProxy) AddInterface(ctx context.Context, req *pb.AddInterfaceRequest) (*pb.Interface, error) {
return nil, nil
}
func (p *gRPCProxy) RemoveInterface(ctx context.Context, req *pb.RemoveInterfaceRequest) (*pb.Interface, error) {
return nil, nil
}
func (p *gRPCProxy) UpdateInterface(ctx context.Context, req *pb.UpdateInterfaceRequest) (*pb.Interface, error) {
return nil, nil
}
func (p *gRPCProxy) UpdateRoutes(ctx context.Context, req *pb.UpdateRoutesRequest) (*pb.Routes, error) {
return nil, nil
}
func (p *gRPCProxy) OnlineCPUMem(ctx context.Context, req *pb.OnlineCPUMemRequest) (*gpb.Empty, error) {
return emptyResp, nil
}
func gRPCRegister(s *grpc.Server, srv interface{}) {
switch g := srv.(type) {
case *gRPCProxy:
pb.RegisterAgentServiceServer(s, g)
}
}
var reqList = []interface{}{
&pb.CreateSandboxRequest{},
&pb.DestroySandboxRequest{},
&pb.ExecProcessRequest{},
&pb.CreateContainerRequest{},
&pb.StartContainerRequest{},
&pb.RemoveContainerRequest{},
&pb.SignalProcessRequest{},
}
func TestKataAgentSendReq(t *testing.T) {
impl := &gRPCProxy{}
proxy := mock.ProxyGRPCMock{
GRPCImplementer: impl,
GRPCRegister: gRPCRegister,
}
sockDir, err := testGenerateKataProxySockDir()
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(sockDir)
testKataProxyURL := fmt.Sprintf(testKataProxyURLTempl, sockDir)
if err := proxy.Start(testKataProxyURL); err != nil {
t.Fatal(err)
}
defer proxy.Stop()
k := &kataAgent{
state: KataAgentState{
URL: testKataProxyURL,
},
}
for _, req := range reqList {
if _, err := k.sendReq(req); err != nil {
t.Fatal(err)
}
}
}
func TestGenerateInterfacesAndRoutes(t *testing.T) {
impl := &gRPCProxy{}
proxy := mock.ProxyGRPCMock{
GRPCImplementer: impl,
GRPCRegister: gRPCRegister,
}
sockDir, err := testGenerateKataProxySockDir()
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(sockDir)
testKataProxyURL := fmt.Sprintf(testKataProxyURLTempl, sockDir)
if err := proxy.Start(testKataProxyURL); err != nil {
t.Fatal(err)
}
defer proxy.Stop()
k := &kataAgent{
state: KataAgentState{
URL: testKataProxyURL,
},
}
//
//Create a couple of addresses
//
address1 := &net.IPNet{IP: net.IPv4(172, 17, 0, 2), Mask: net.CIDRMask(16, 32)}
address2 := &net.IPNet{IP: net.IPv4(182, 17, 0, 2), Mask: net.CIDRMask(16, 32)}
addrs := []netlink.Addr{
{IPNet: address1, Label: "phyaddr1"},
{IPNet: address2, Label: "phyaddr2"},
}
// Create a couple of routes:
dst2 := &net.IPNet{IP: net.IPv4(172, 17, 0, 0), Mask: net.CIDRMask(16, 32)}
src2 := net.IPv4(172, 17, 0, 2)
gw2 := net.IPv4(172, 17, 0, 1)
routes := []netlink.Route{
{LinkIndex: 329, Dst: nil, Src: nil, Gw: net.IPv4(172, 17, 0, 1), Scope: netlink.Scope(254)},
{LinkIndex: 329, Dst: dst2, Src: src2, Gw: gw2},
}
networkInfo := NetworkInfo{
Iface: NetlinkIface{
LinkAttrs: netlink.LinkAttrs{MTU: 1500},
Type: "",
},
Addrs: addrs,
Routes: routes,
}
ep0 := &PhysicalEndpoint{
IfaceName: "eth0",
HardAddr: net.HardwareAddr{0x02, 0x00, 0xca, 0xfe, 0x00, 0x04}.String(),
EndpointProperties: networkInfo,
}
endpoints := []Endpoint{ep0}
nns := NetworkNamespace{NetNsPath: "foobar", NetNsCreated: true, Endpoints: endpoints}
resInterfaces, resRoutes, err := k.generateInterfacesAndRoutes(nns)
//
// Build expected results:
//
expectedAddresses := []*pb.IPAddress{
{Family: 0, Address: "172.17.0.2", Mask: "16"},
{Family: 0, Address: "182.17.0.2", Mask: "16"},
}
expectedInterfaces := []*pb.Interface{
{Device: "eth0", Name: "eth0", IPAddresses: expectedAddresses, Mtu: 1500, HwAddr: "02:00:ca:fe:00:04"},
}
expectedRoutes := []*pb.Route{
{Dest: "", Gateway: "172.17.0.1", Device: "eth0", Source: "", Scope: uint32(254)},
{Dest: "172.17.0.0/16", Gateway: "172.17.0.1", Device: "eth0", Source: "172.17.0.2"},
}
assert.Nil(t, err, "unexpected failure when calling generateKataInterfacesAndRoutes")
assert.True(t, reflect.DeepEqual(resInterfaces, expectedInterfaces),
"Interfaces returned didn't match: got %+v, expecting %+v", resInterfaces, expectedInterfaces)
assert.True(t, reflect.DeepEqual(resRoutes, expectedRoutes),
"Routes returned didn't match: got %+v, expecting %+v", resRoutes, expectedRoutes)
}

View File

@@ -0,0 +1,70 @@
//
// 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 virtcontainers
import (
"fmt"
"os/exec"
"syscall"
)
// This is the Kata Containers implementation of the proxy interface.
// This is pretty simple since it provides the same interface to both
// runtime and shim as if they were talking directly to the agent.
type kataProxy struct {
}
// start is kataProxy start implementation for proxy interface.
func (p *kataProxy) start(pod Pod, params proxyParams) (int, string, error) {
if pod.agent == nil {
return -1, "", fmt.Errorf("No agent")
}
if params.agentURL == "" {
return -1, "", fmt.Errorf("AgentURL cannot be empty")
}
config, err := newProxyConfig(pod.config)
if err != nil {
return -1, "", err
}
// construct the socket path the proxy instance will use
proxyURL, err := defaultProxyURL(pod, SocketTypeUNIX)
if err != nil {
return -1, "", err
}
args := []string{config.Path, "-listen-socket", proxyURL, "-mux-socket", params.agentURL}
if config.Debug {
args = append(args, "-log", "debug")
args = append(args, "-agent-logs-socket", pod.hypervisor.getPodConsole(pod.id))
}
cmd := exec.Command(args[0], args[1:]...)
if err := cmd.Start(); err != nil {
return -1, "", err
}
return cmd.Process.Pid, proxyURL, nil
}
// stop is kataProxy stop implementation for proxy interface.
func (p *kataProxy) stop(pod Pod, pid int) error {
// Signal the proxy with SIGTERM.
return syscall.Kill(pid, syscall.SIGTERM)
}

View File

@@ -0,0 +1,72 @@
//
// 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 virtcontainers
import (
"fmt"
)
type kataShim struct{}
// KataShimConfig is the structure providing specific configuration
// for kataShim implementation.
type KataShimConfig struct {
Path string
Debug bool
}
// start is the ccShim start implementation.
// It starts the cc-shim binary with URL and token flags provided by
// the proxy.
func (s *kataShim) start(pod Pod, params ShimParams) (int, error) {
if pod.config == nil {
return -1, fmt.Errorf("Pod config cannot be nil")
}
config, ok := newShimConfig(*(pod.config)).(ShimConfig)
if !ok {
return -1, fmt.Errorf("Wrong shim config type, should be KataShimConfig type")
}
if config.Path == "" {
return -1, fmt.Errorf("Shim path cannot be empty")
}
if params.URL == "" {
return -1, fmt.Errorf("URL cannot be empty")
}
if params.Container == "" {
return -1, fmt.Errorf("Container cannot be empty")
}
if params.Token == "" {
return -1, fmt.Errorf("Process token cannot be empty")
}
args := []string{config.Path, "-agent", params.URL, "-container", params.Container, "-exec-id", params.Token}
if params.Terminal {
args = append(args, "-terminal")
}
if config.Debug {
args = append(args, "-log", "debug")
}
return startShim(args, params)
}

View File

@@ -0,0 +1,316 @@
//
// 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 virtcontainers
import (
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
"testing"
"time"
. "github.com/containers/virtcontainers/pkg/mock"
)
// These tests don't care about the format of the container ID
const testKataContainer = "testContainer"
var testKataShimPath = "/usr/bin/virtcontainers/bin/test/kata-shim"
var testKataShimProxyURL = "foo:///foo/kata-containers/proxy.sock"
func getMockKataShimBinPath() string {
if DefaultMockKataShimBinPath == "" {
return testKataShimPath
}
return DefaultMockKataShimBinPath
}
func testKataShimStart(t *testing.T, pod Pod, params ShimParams, expectFail bool) {
s := &kataShim{}
pid, err := s.start(pod, params)
if expectFail {
if err == nil || pid != -1 {
t.Fatalf("This test should fail (pod %+v, params %+v, expectFail %t)",
pod, params, expectFail)
}
} else {
if err != nil {
t.Fatalf("This test should pass (pod %+v, params %+v, expectFail %t): %s",
pod, params, expectFail, err)
}
if pid == -1 {
t.Fatalf("This test should pass (pod %+v, params %+v, expectFail %t)",
pod, params, expectFail)
}
}
}
func TestKataShimStartNilPodConfigFailure(t *testing.T) {
testKataShimStart(t, Pod{}, ShimParams{}, true)
}
func TestKataShimStartNilShimConfigFailure(t *testing.T) {
pod := Pod{
config: &PodConfig{},
}
testKataShimStart(t, pod, ShimParams{}, true)
}
func TestKataShimStartShimPathEmptyFailure(t *testing.T) {
pod := Pod{
config: &PodConfig{
ShimType: KataShimType,
ShimConfig: ShimConfig{},
},
}
testKataShimStart(t, pod, ShimParams{}, true)
}
func TestKataShimStartShimTypeInvalid(t *testing.T) {
pod := Pod{
config: &PodConfig{
ShimType: "foo",
ShimConfig: ShimConfig{},
},
}
testKataShimStart(t, pod, ShimParams{}, true)
}
func TestKataShimStartParamsTokenEmptyFailure(t *testing.T) {
pod := Pod{
config: &PodConfig{
ShimType: KataShimType,
ShimConfig: ShimConfig{
Path: getMockKataShimBinPath(),
},
},
}
testKataShimStart(t, pod, ShimParams{}, true)
}
func TestKataShimStartParamsURLEmptyFailure(t *testing.T) {
pod := Pod{
config: &PodConfig{
ShimType: KataShimType,
ShimConfig: ShimConfig{
Path: getMockKataShimBinPath(),
},
},
}
params := ShimParams{
Token: "testToken",
}
testKataShimStart(t, pod, params, true)
}
func TestKataShimStartParamsContainerEmptyFailure(t *testing.T) {
pod := Pod{
config: &PodConfig{
ShimType: KataShimType,
ShimConfig: ShimConfig{
Path: getMockKataShimBinPath(),
},
},
}
params := ShimParams{
Token: "testToken",
URL: "unix://is/awesome",
}
testKataShimStart(t, pod, params, true)
}
func TestKataShimStartParamsInvalidCommand(t *testing.T) {
dir, err := ioutil.TempDir("", "")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(dir)
cmd := filepath.Join(dir, "does-not-exist")
pod := Pod{
config: &PodConfig{
ShimType: KataShimType,
ShimConfig: ShimConfig{
Path: cmd,
},
},
}
params := ShimParams{
Token: "testToken",
URL: "http://foo",
}
testKataShimStart(t, pod, params, true)
}
func startKataShimStartWithoutConsoleSuccessful(t *testing.T, detach bool) (*os.File, *os.File, *os.File, Pod, ShimParams, error) {
saveStdout := os.Stdout
rStdout, wStdout, err := os.Pipe()
if err != nil {
return nil, nil, nil, Pod{}, ShimParams{}, err
}
os.Stdout = wStdout
pod := Pod{
config: &PodConfig{
ShimType: KataShimType,
ShimConfig: ShimConfig{
Path: getMockKataShimBinPath(),
},
},
}
params := ShimParams{
Container: testContainer,
Token: "testToken",
URL: testKataShimProxyURL,
Detach: detach,
}
return rStdout, wStdout, saveStdout, pod, params, nil
}
func TestKataShimStartSuccessful(t *testing.T) {
rStdout, wStdout, saveStdout, pod, params, err := startKataShimStartWithoutConsoleSuccessful(t, false)
if err != nil {
t.Fatal(err)
}
defer func() {
os.Stdout = saveStdout
rStdout.Close()
wStdout.Close()
}()
testKataShimStart(t, pod, params, false)
bufStdout := make([]byte, 1024)
if _, err := rStdout.Read(bufStdout); err != nil {
t.Fatal(err)
}
if !strings.Contains(string(bufStdout), ShimStdoutOutput) {
t.Fatalf("Substring %q not found in %q", ShimStdoutOutput, string(bufStdout))
}
}
func TestKataShimStartDetachSuccessful(t *testing.T) {
rStdout, wStdout, saveStdout, pod, params, err := startKataShimStartWithoutConsoleSuccessful(t, true)
if err != nil {
t.Fatal(err)
}
defer func() {
os.Stdout = saveStdout
wStdout.Close()
rStdout.Close()
}()
testKataShimStart(t, pod, params, false)
readCh := make(chan error)
go func() {
bufStdout := make([]byte, 1024)
n, err := rStdout.Read(bufStdout)
if err != nil && err != io.EOF {
readCh <- err
return
}
if n > 0 {
readCh <- fmt.Errorf("Not expecting to read anything, Got %q", string(bufStdout))
return
}
readCh <- nil
}()
select {
case err := <-readCh:
if err != nil {
t.Fatal(err)
}
case <-time.After(time.Duration(20) * time.Millisecond):
return
}
}
func TestKataShimStartWithConsoleNonExistingFailure(t *testing.T) {
pod := Pod{
config: &PodConfig{
ShimType: KataShimType,
ShimConfig: ShimConfig{
Path: getMockKataShimBinPath(),
},
},
}
params := ShimParams{
Token: "testToken",
URL: testKataShimProxyURL,
Console: testWrongConsolePath,
}
testKataShimStart(t, pod, params, true)
}
func TestKataShimStartWithConsoleSuccessful(t *testing.T) {
cleanUp()
master, console, err := newConsole()
t.Logf("Console created for tests:%s\n", console)
if err != nil {
t.Fatal(err)
}
pod := Pod{
config: &PodConfig{
ShimType: KataShimType,
ShimConfig: ShimConfig{
Path: getMockKataShimBinPath(),
},
},
}
params := ShimParams{
Container: testContainer,
Token: "testToken",
URL: testKataShimProxyURL,
Console: console,
}
testKataShimStart(t, pod, params, false)
master.Close()
}

View File

@@ -0,0 +1,73 @@
//
// Copyright (c) 2016 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 virtcontainers
type mockHypervisor struct {
}
func (m *mockHypervisor) init(pod *Pod) error {
valid, err := pod.config.HypervisorConfig.valid()
if valid == false || err != nil {
return err
}
return nil
}
func (m *mockHypervisor) capabilities() capabilities {
return capabilities{}
}
func (m *mockHypervisor) createPod(podConfig PodConfig) error {
return nil
}
func (m *mockHypervisor) startPod() error {
return nil
}
func (m *mockHypervisor) waitPod(timeout int) error {
return nil
}
func (m *mockHypervisor) stopPod() error {
return nil
}
func (m *mockHypervisor) pausePod() error {
return nil
}
func (m *mockHypervisor) resumePod() error {
return nil
}
func (m *mockHypervisor) addDevice(devInfo interface{}, devType deviceType) error {
return nil
}
func (m *mockHypervisor) hotplugAddDevice(devInfo interface{}, devType deviceType) error {
return nil
}
func (m *mockHypervisor) hotplugRemoveDevice(devInfo interface{}, devType deviceType) error {
return nil
}
func (m *mockHypervisor) getPodConsole(podID string) string {
return ""
}

View File

@@ -0,0 +1,104 @@
//
// Copyright (c) 2016 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 virtcontainers
import (
"fmt"
"testing"
)
func TestMockHypervisorInit(t *testing.T) {
var m *mockHypervisor
pod := &Pod{
config: &PodConfig{
HypervisorConfig: HypervisorConfig{
KernelPath: "",
ImagePath: "",
HypervisorPath: "",
},
},
}
// wrong config
if err := m.init(pod); err == nil {
t.Fatal()
}
pod.config.HypervisorConfig = HypervisorConfig{
KernelPath: fmt.Sprintf("%s/%s", testDir, testKernel),
ImagePath: fmt.Sprintf("%s/%s", testDir, testImage),
HypervisorPath: fmt.Sprintf("%s/%s", testDir, testHypervisor),
}
// right config
if err := m.init(pod); err != nil {
t.Fatal(err)
}
}
func TestMockHypervisorCreatePod(t *testing.T) {
var m *mockHypervisor
config := PodConfig{}
if err := m.createPod(config); err != nil {
t.Fatal(err)
}
}
func TestMockHypervisorStartPod(t *testing.T) {
var m *mockHypervisor
if err := m.startPod(); err != nil {
t.Fatal(err)
}
}
func TestMockHypervisorWaitPod(t *testing.T) {
var m *mockHypervisor
if err := m.waitPod(0); err != nil {
t.Fatal(err)
}
}
func TestMockHypervisorStopPod(t *testing.T) {
var m *mockHypervisor
if err := m.stopPod(); err != nil {
t.Fatal(err)
}
}
func TestMockHypervisorAddDevice(t *testing.T) {
var m *mockHypervisor
if err := m.addDevice(nil, imgDev); err != nil {
t.Fatal(err)
}
}
func TestMockHypervisorGetPodConsole(t *testing.T) {
var m *mockHypervisor
expected := ""
if result := m.getPodConsole("testPodID"); result != expected {
t.Fatalf("Got %s\nExpecting %s", result, expected)
}
}

350
virtcontainers/mount.go Normal file
View File

@@ -0,0 +1,350 @@
//
// 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 virtcontainers
import (
"bufio"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"syscall"
"github.com/sirupsen/logrus"
)
var rootfsDir = "rootfs"
func mountLogger() *logrus.Entry {
return virtLog.WithField("subsystem", "mount")
}
// These mounts need to be created by the agent within the VM
var systemMounts = []string{"/proc", "/dev", "/dev/pts", "/dev/shm", "/dev/mqueue", "/sys", "/sys/fs/cgroup"}
var systemMountPrefixes = []string{"/proc", "/dev", "/sys"}
func isSystemMount(m string) bool {
for _, p := range systemMountPrefixes {
if m == p || strings.HasPrefix(m, p+"/") {
return true
}
}
return false
}
func major(dev uint64) int {
return int((dev >> 8) & 0xfff)
}
func minor(dev uint64) int {
return int((dev & 0xff) | ((dev >> 12) & 0xfff00))
}
type device struct {
major int
minor int
mountPoint string
}
var errMountPointNotFound = errors.New("Mount point not found")
// getDeviceForPath gets the underlying device containing the file specified by path.
// The device type constitutes the major-minor number of the device and the dest mountPoint for the device
//
// eg. if /dev/sda1 is mounted on /a/b/c, a call to getDeviceForPath("/a/b/c/file") would return
//
// device {
// major : major(/dev/sda1)
// manor : minor(/dev/sda1)
// mountPoint: /a/b/c
// }
func getDeviceForPath(path string) (device, error) {
if path == "" {
return device{}, fmt.Errorf("Path cannot be empty")
}
stat := syscall.Stat_t{}
err := syscall.Stat(path, &stat)
if err != nil {
return device{}, err
}
// stat.Dev points to the underlying device containing the file
major := major(stat.Dev)
minor := minor(stat.Dev)
path, err = filepath.Abs(path)
if err != nil {
return device{}, err
}
mountPoint := path
if path == "/" {
return device{
major: major,
minor: minor,
mountPoint: mountPoint,
}, nil
}
// We get the mount point by recursively peforming stat on the path
// The point where the device changes indicates the mountpoint
for {
if mountPoint == "/" {
return device{}, errMountPointNotFound
}
parentStat := syscall.Stat_t{}
parentDir := filepath.Dir(path)
err := syscall.Lstat(parentDir, &parentStat)
if err != nil {
return device{}, err
}
if parentStat.Dev != stat.Dev {
break
}
mountPoint = parentDir
stat = parentStat
path = parentDir
}
dev := device{
major: major,
minor: minor,
mountPoint: mountPoint,
}
return dev, nil
}
const (
procMountsFile = "/proc/mounts"
fieldsPerLine = 6
)
const (
procDeviceIndex = iota
procPathIndex
procTypeIndex
)
func getDevicePathAndFsType(mountPoint string) (devicePath, fsType string, err error) {
if mountPoint == "" {
err = fmt.Errorf("Mount point cannot be empty")
return
}
var file *os.File
file, err = os.Open(procMountsFile)
if err != nil {
return
}
defer file.Close()
reader := bufio.NewReader(file)
for {
var line string
line, err = reader.ReadString('\n')
if err == io.EOF {
err = fmt.Errorf("Mount %s not found", mountPoint)
return
}
fields := strings.Fields(line)
if len(fields) != fieldsPerLine {
err = fmt.Errorf("Incorrect no of fields (expected %d, got %d)) :%s", fieldsPerLine, len(fields), line)
return
}
if mountPoint == fields[procPathIndex] {
devicePath = fields[procDeviceIndex]
fsType = fields[procTypeIndex]
return
}
}
}
var blockFormatTemplate = "/sys/dev/block/%d:%d/dm"
var checkStorageDriver = isDeviceMapper
// isDeviceMapper checks if the device with the major and minor numbers is a devicemapper block device
func isDeviceMapper(major, minor int) (bool, error) {
//Check if /sys/dev/block/${major}-${minor}/dm exists
sysPath := fmt.Sprintf(blockFormatTemplate, major, minor)
_, err := os.Stat(sysPath)
if err == nil {
return true, nil
} else if os.IsNotExist(err) {
return false, nil
}
return false, err
}
// getVirtBlockDriveName returns the disk name format for virtio-blk
// Reference: https://github.com/torvalds/linux/blob/master/drivers/block/virtio_blk.c @c0aa3e0916d7e531e69b02e426f7162dfb1c6c0
func getVirtDriveName(index int) (string, error) {
if index < 0 {
return "", fmt.Errorf("Index cannot be negative for drive")
}
// Prefix used for virtio-block devices
const prefix = "vd"
//Refer to DISK_NAME_LEN: https://github.com/torvalds/linux/blob/08c521a2011ff492490aa9ed6cc574be4235ce2b/include/linux/genhd.h#L61
diskNameLen := 32
base := 26
suffLen := diskNameLen - len(prefix)
diskLetters := make([]byte, suffLen)
var i int
for i = 0; i < suffLen && index >= 0; i++ {
letter := byte('a' + (index % base))
diskLetters[i] = letter
index = index/base - 1
}
if index >= 0 {
return "", fmt.Errorf("Index not supported")
}
diskName := prefix + reverseString(string(diskLetters[:i]))
return diskName, nil
}
const mountPerm = os.FileMode(0755)
// bindMount bind mounts a source in to a destination. This will
// do some bookkeeping:
// * evaluate all symlinks
// * ensure the source exists
// * recursively create the destination
func bindMount(source, destination string, readonly bool) error {
if source == "" {
return fmt.Errorf("source must be specified")
}
if destination == "" {
return fmt.Errorf("destination must be specified")
}
absSource, err := filepath.EvalSymlinks(source)
if err != nil {
return fmt.Errorf("Could not resolve symlink for source %v", source)
}
if err := ensureDestinationExists(absSource, destination); err != nil {
return fmt.Errorf("Could not create destination mount point %v: %v", destination, err)
} else if err := syscall.Mount(absSource, destination, "bind", syscall.MS_BIND, ""); err != nil {
return fmt.Errorf("Could not bind mount %v to %v: %v", absSource, destination, err)
}
// For readonly bind mounts, we need to remount with the readonly flag.
// This is needed as only very recent versions of libmount/util-linux support "bind,ro"
if readonly {
return syscall.Mount(absSource, destination, "bind", uintptr(syscall.MS_BIND|syscall.MS_REMOUNT|syscall.MS_RDONLY), "")
}
return nil
}
// bindMountContainerRootfs bind mounts a container rootfs into a 9pfs shared
// directory between the guest and the host.
func bindMountContainerRootfs(sharedDir, podID, cID, cRootFs string, readonly bool) error {
rootfsDest := filepath.Join(sharedDir, podID, cID, rootfsDir)
return bindMount(cRootFs, rootfsDest, readonly)
}
// Mount describes a container mount.
type Mount struct {
Source string
Destination string
// Type specifies the type of filesystem to mount.
Type string
// Options list all the mount options of the filesystem.
Options []string
// HostPath used to store host side bind mount path
HostPath string
// ReadOnly specifies if the mount should be read only or not
ReadOnly bool
}
func bindUnmountContainerRootfs(sharedDir, podID, cID string) error {
rootfsDest := filepath.Join(sharedDir, podID, cID, rootfsDir)
syscall.Unmount(rootfsDest, 0)
return nil
}
func bindUnmountAllRootfs(sharedDir string, pod Pod) {
for _, c := range pod.containers {
c.unmountHostMounts()
if c.state.Fstype == "" {
// Need to check for error returned by this call.
// See: https://github.com/containers/virtcontainers/issues/295
bindUnmountContainerRootfs(sharedDir, pod.id, c.id)
}
}
}
const maxSCSIDevices = 65535
// getSCSIIdLun gets the SCSI id and lun, based on the index of the drive being inserted.
// qemu code suggests that scsi-id can take values from 0 to 255 inclusive, while lun can
// take values from 0 to 16383 inclusive. But lun values over 255 do not seem to follow
// consistent SCSI addressing. Hence we limit to 255.
func getSCSIIdLun(index int) (int, int, error) {
if index < 0 {
return -1, -1, fmt.Errorf("Index cannot be negative")
}
if index > maxSCSIDevices {
return -1, -1, fmt.Errorf("Index cannot be greater than %d, maximum of %d devices are supported", maxSCSIDevices, maxSCSIDevices)
}
return index / 256, index % 256, nil
}
func getSCSIAddress(index int) (string, error) {
scsiID, lun, err := getSCSIIdLun(index)
if err != nil {
return "", err
}
return fmt.Sprintf("%d:%d", scsiID, lun), nil
}

View File

@@ -0,0 +1,334 @@
//
// 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 virtcontainers
import (
"bytes"
"fmt"
"github.com/stretchr/testify/assert"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"syscall"
"testing"
)
func TestIsSystemMount(t *testing.T) {
tests := []struct {
mnt string
expected bool
}{
{"/sys", true},
{"/sys/", true},
{"/sys//", true},
{"/sys/fs", true},
{"/sys/fs/", true},
{"/sys/fs/cgroup", true},
{"/sysfoo", false},
{"/home", false},
{"/dev/block/", true},
}
for _, test := range tests {
result := isSystemMount(test.mnt)
if result != test.expected {
t.Fatalf("Expected result for path %s : %v, got %v", test.mnt, test.expected, result)
}
}
}
func TestMajorMinorNumber(t *testing.T) {
devices := []string{"/dev/zero", "/dev/net/tun"}
for _, device := range devices {
cmdStr := fmt.Sprintf("ls -l %s | awk '{print $5$6}'", device)
cmd := exec.Command("sh", "-c", cmdStr)
output, err := cmd.Output()
if err != nil {
t.Fatal(err)
}
data := bytes.Split(output, []byte(","))
if len(data) < 2 {
t.Fatal()
}
majorStr := strings.TrimSpace(string(data[0]))
minorStr := strings.TrimSpace(string(data[1]))
majorNo, err := strconv.Atoi(majorStr)
if err != nil {
t.Fatal(err)
}
minorNo, err := strconv.Atoi(minorStr)
if err != nil {
t.Fatal(err)
}
stat := syscall.Stat_t{}
err = syscall.Stat(device, &stat)
if err != nil {
t.Fatal(err)
}
// Get major and minor numbers for the device itself. Note the use of stat.Rdev instead of Dev.
major := major(stat.Rdev)
minor := minor(stat.Rdev)
if minor != minorNo {
t.Fatalf("Expected minor number for device %s: %d, Got :%d", device, minorNo, minor)
}
if major != majorNo {
t.Fatalf("Expected major number for device %s : %d, Got :%d", device, majorNo, major)
}
}
}
func TestGetDeviceForPathRoot(t *testing.T) {
dev, err := getDeviceForPath("/")
if err != nil {
t.Fatal(err)
}
expected := "/"
if dev.mountPoint != expected {
t.Fatalf("Expected %s mountpoint, got %s", expected, dev.mountPoint)
}
}
func TestGetDeviceForPathValidMount(t *testing.T) {
dev, err := getDeviceForPath("/proc")
if err != nil {
t.Fatal(err)
}
expected := "/proc"
if dev.mountPoint != expected {
t.Fatalf("Expected %s mountpoint, got %s", expected, dev.mountPoint)
}
}
func TestGetDeviceForPathEmptyPath(t *testing.T) {
_, err := getDeviceForPath("")
if err == nil {
t.Fatal()
}
}
func TestGetDeviceForPath(t *testing.T) {
dev, err := getDeviceForPath("///")
if err != nil {
t.Fatal(err)
}
if dev.mountPoint != "/" {
t.Fatal(err)
}
_, err = getDeviceForPath("/../../.././././../.")
if err != nil {
t.Fatal(err)
}
_, err = getDeviceForPath("/root/file with spaces")
if err == nil {
t.Fatal()
}
}
func TestGetDeviceForPathBindMount(t *testing.T) {
if os.Geteuid() != 0 {
t.Skip(testDisabledAsNonRoot)
}
source := filepath.Join(testDir, "testDeviceDirSrc")
dest := filepath.Join(testDir, "testDeviceDirDest")
syscall.Unmount(dest, 0)
os.Remove(source)
os.Remove(dest)
err := os.MkdirAll(source, mountPerm)
if err != nil {
t.Fatal(err)
}
defer os.Remove(source)
err = os.MkdirAll(dest, mountPerm)
if err != nil {
t.Fatal(err)
}
defer os.Remove(dest)
err = bindMount(source, dest, false)
if err != nil {
t.Fatal(err)
}
defer syscall.Unmount(dest, 0)
destFile := filepath.Join(dest, "test")
_, err = os.Create(destFile)
if err != nil {
fmt.Println("Could not create test file:", err)
t.Fatal(err)
}
defer os.Remove(destFile)
sourceDev, _ := getDeviceForPath(source)
destDev, _ := getDeviceForPath(destFile)
if sourceDev != destDev {
t.Fatal()
}
}
func TestGetDevicePathAndFsTypeEmptyMount(t *testing.T) {
_, _, err := getDevicePathAndFsType("")
if err == nil {
t.Fatal()
}
}
func TestGetDevicePathAndFsTypeSuccessful(t *testing.T) {
path, fstype, err := getDevicePathAndFsType("/proc")
if err != nil {
t.Fatal(err)
}
if path != "proc" || fstype != "proc" {
t.Fatal(err)
}
}
func TestIsDeviceMapper(t *testing.T) {
// known major, minor for /dev/tty
major := 5
minor := 0
isDM, err := isDeviceMapper(major, minor)
if err != nil {
t.Fatal(err)
}
if isDM {
t.Fatal()
}
// fake the block device format
blockFormatTemplate = "/sys/dev/char/%d:%d"
isDM, err = isDeviceMapper(major, minor)
if err != nil {
t.Fatal(err)
}
if !isDM {
t.Fatal()
}
}
func TestGetVirtDriveNameInvalidIndex(t *testing.T) {
_, err := getVirtDriveName(-1)
if err == nil {
t.Fatal(err)
}
}
func TestGetVirtDriveName(t *testing.T) {
tests := []struct {
index int
expectedDrive string
}{
{0, "vda"},
{25, "vdz"},
{27, "vdab"},
{704, "vdaac"},
{18277, "vdzzz"},
}
for _, test := range tests {
driveName, err := getVirtDriveName(test.index)
if err != nil {
t.Fatal(err)
}
if driveName != test.expectedDrive {
t.Fatalf("Incorrect drive Name: Got: %s, Expecting :%s", driveName, test.expectedDrive)
}
}
}
func TestGetSCSIIdLun(t *testing.T) {
tests := []struct {
index int
expectedScsiID int
expectedLun int
}{
{0, 0, 0},
{1, 0, 1},
{2, 0, 2},
{255, 0, 255},
{256, 1, 0},
{257, 1, 1},
{258, 1, 2},
{512, 2, 0},
{513, 2, 1},
}
for _, test := range tests {
scsiID, lun, err := getSCSIIdLun(test.index)
assert.Nil(t, err)
if scsiID != test.expectedScsiID && lun != test.expectedLun {
t.Fatalf("Expecting scsi-id:lun %d:%d, Got %d:%d", test.expectedScsiID, test.expectedLun, scsiID, lun)
}
}
_, _, err := getSCSIIdLun(maxSCSIDevices + 1)
assert.NotNil(t, err)
}
func TestGetSCSIAddress(t *testing.T) {
tests := []struct {
index int
expectedSCSIAddress string
}{
{0, "0:0"},
{200, "0:200"},
{255, "0:255"},
{258, "1:2"},
{512, "2:0"},
}
for _, test := range tests {
scsiAddr, err := getSCSIAddress(test.index)
assert.Nil(t, err)
assert.Equal(t, scsiAddr, test.expectedSCSIAddress)
}
}

1394
virtcontainers/network.go Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,410 @@
//
// Copyright (c) 2016 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 virtcontainers
import (
"net"
"os"
"reflect"
"testing"
"github.com/containernetworking/plugins/pkg/ns"
"github.com/vishvananda/netlink"
"github.com/vishvananda/netns"
)
func testNetworkModelSet(t *testing.T, value string, expected NetworkModel) {
var netModel NetworkModel
err := netModel.Set(value)
if err != nil {
t.Fatal(err)
}
if netModel != expected {
t.Fatal()
}
}
func TestNoopNetworkModelSet(t *testing.T) {
testNetworkModelSet(t, "noop", NoopNetworkModel)
}
func TestCNINetworkModelSet(t *testing.T) {
testNetworkModelSet(t, "CNI", CNINetworkModel)
}
func TestCNMNetworkModelSet(t *testing.T) {
testNetworkModelSet(t, "CNM", CNMNetworkModel)
}
func TestNetworkModelSetFailure(t *testing.T) {
var netModel NetworkModel
err := netModel.Set("wrong-value")
if err == nil {
t.Fatal(err)
}
}
func testNetworkModelString(t *testing.T, netModel *NetworkModel, expected string) {
result := netModel.String()
if result != expected {
t.Fatal()
}
}
func TestNoopNetworkModelString(t *testing.T) {
netModel := NoopNetworkModel
testNetworkModelString(t, &netModel, string(NoopNetworkModel))
}
func TestCNINetworkModelString(t *testing.T) {
netModel := CNINetworkModel
testNetworkModelString(t, &netModel, string(CNINetworkModel))
}
func TestCNMNetworkModelString(t *testing.T) {
netModel := CNMNetworkModel
testNetworkModelString(t, &netModel, string(CNMNetworkModel))
}
func TestWrongNetworkModelString(t *testing.T) {
var netModel NetworkModel
testNetworkModelString(t, &netModel, "")
}
func testNewNetworkFromNetworkModel(t *testing.T, netModel NetworkModel, expected interface{}) {
result := newNetwork(netModel)
if reflect.DeepEqual(result, expected) == false {
t.Fatal()
}
}
func TestNewNoopNetworkFromNetworkModel(t *testing.T) {
testNewNetworkFromNetworkModel(t, NoopNetworkModel, &noopNetwork{})
}
func TestNewCNINetworkFromNetworkModel(t *testing.T) {
testNewNetworkFromNetworkModel(t, CNINetworkModel, &cni{})
}
func TestNewCNMNetworkFromNetworkModel(t *testing.T) {
testNewNetworkFromNetworkModel(t, CNMNetworkModel, &cnm{})
}
func TestNewUnknownNetworkFromNetworkModel(t *testing.T) {
var netModel NetworkModel
testNewNetworkFromNetworkModel(t, netModel, &noopNetwork{})
}
func TestCreateDeleteNetNS(t *testing.T) {
if os.Geteuid() != 0 {
t.Skip(testDisabledAsNonRoot)
}
netNSPath, err := createNetNS()
if err != nil {
t.Fatal(err)
}
if netNSPath == "" {
t.Fatal()
}
_, err = os.Stat(netNSPath)
if err != nil {
t.Fatal(err)
}
err = deleteNetNS(netNSPath, true)
if err != nil {
t.Fatal(err)
}
}
func testEndpointTypeSet(t *testing.T, value string, expected EndpointType) {
//var netModel NetworkModel
var endpointType EndpointType
err := endpointType.Set(value)
if err != nil {
t.Fatal(err)
}
if endpointType != expected {
t.Fatal()
}
}
func TestPhysicalEndpointTypeSet(t *testing.T) {
testEndpointTypeSet(t, "physical", PhysicalEndpointType)
}
func TestVirtualEndpointTypeSet(t *testing.T) {
testEndpointTypeSet(t, "virtual", VirtualEndpointType)
}
func TestEndpointTypeSetFailure(t *testing.T) {
var endpointType EndpointType
err := endpointType.Set("wrong-value")
if err == nil {
t.Fatal(err)
}
}
func testEndpointTypeString(t *testing.T, endpointType *EndpointType, expected string) {
result := endpointType.String()
if result != expected {
t.Fatal()
}
}
func TestPhysicalEndpointTypeString(t *testing.T) {
endpointType := PhysicalEndpointType
testEndpointTypeString(t, &endpointType, string(PhysicalEndpointType))
}
func TestVirtualEndpointTypeString(t *testing.T) {
endpointType := VirtualEndpointType
testEndpointTypeString(t, &endpointType, string(VirtualEndpointType))
}
func TestIncorrectEndpointTypeString(t *testing.T) {
var endpointType EndpointType
testEndpointTypeString(t, &endpointType, "")
}
func TestCreateVhostUserEndpoint(t *testing.T) {
macAddr := net.HardwareAddr{0x02, 0x00, 0xCA, 0xFE, 0x00, 0x48}
ifcName := "vhost-deadbeef"
socket := "/tmp/vhu_192.168.0.1"
netinfo := NetworkInfo{
Iface: NetlinkIface{
LinkAttrs: netlink.LinkAttrs{
HardwareAddr: macAddr,
Name: ifcName,
},
},
}
expected := &VhostUserEndpoint{
SocketPath: socket,
HardAddr: macAddr.String(),
IfaceName: ifcName,
EndpointType: VhostUserEndpointType,
}
result, err := createVhostUserEndpoint(netinfo, socket)
if err != nil {
t.Fatal(err)
}
if reflect.DeepEqual(result, expected) == false {
t.Fatalf("\n\tGot %v\n\tExpecting %v", result, expected)
}
}
func TestCreateVirtualNetworkEndpoint(t *testing.T) {
macAddr := net.HardwareAddr{0x02, 0x00, 0xCA, 0xFE, 0x00, 0x04}
expected := &VirtualEndpoint{
NetPair: NetworkInterfacePair{
ID: "uniqueTestID-4",
Name: "br4",
VirtIface: NetworkInterface{
Name: "eth4",
HardAddr: macAddr.String(),
},
TAPIface: NetworkInterface{
Name: "tap4",
},
NetInterworkingModel: DefaultNetInterworkingModel,
},
EndpointType: VirtualEndpointType,
}
result, err := createVirtualNetworkEndpoint(4, "", DefaultNetInterworkingModel)
if err != nil {
t.Fatal(err)
}
// the resulting ID will be random - so let's overwrite to test the rest of the flow
result.NetPair.ID = "uniqueTestID-4"
if reflect.DeepEqual(result, expected) == false {
t.Fatal()
}
}
func TestCreateVirtualNetworkEndpointChooseIfaceName(t *testing.T) {
macAddr := net.HardwareAddr{0x02, 0x00, 0xCA, 0xFE, 0x00, 0x04}
expected := &VirtualEndpoint{
NetPair: NetworkInterfacePair{
ID: "uniqueTestID-4",
Name: "br4",
VirtIface: NetworkInterface{
Name: "eth1",
HardAddr: macAddr.String(),
},
TAPIface: NetworkInterface{
Name: "tap4",
},
NetInterworkingModel: DefaultNetInterworkingModel,
},
EndpointType: VirtualEndpointType,
}
result, err := createVirtualNetworkEndpoint(4, "eth1", DefaultNetInterworkingModel)
if err != nil {
t.Fatal(err)
}
// the resulting ID will be random - so let's overwrite to test the rest of the flow
result.NetPair.ID = "uniqueTestID-4"
if reflect.DeepEqual(result, expected) == false {
t.Fatal()
}
}
func TestCreateVirtualNetworkEndpointInvalidArgs(t *testing.T) {
type endpointValues struct {
idx int
ifName string
}
// all elements are expected to result in failure
failingValues := []endpointValues{
{-1, "bar"},
{-1, ""},
}
for _, d := range failingValues {
result, err := createVirtualNetworkEndpoint(d.idx, d.ifName, DefaultNetInterworkingModel)
if err == nil {
t.Fatalf("expected invalid endpoint for %v, got %v", d, result)
}
}
}
func TestIsPhysicalIface(t *testing.T) {
testNetIface := "testIface0"
testMTU := 1500
testMACAddr := "00:00:00:00:00:01"
hwAddr, err := net.ParseMAC(testMACAddr)
if err != nil {
t.Fatal(err)
}
link := &netlink.Bridge{
LinkAttrs: netlink.LinkAttrs{
Name: testNetIface,
MTU: testMTU,
HardwareAddr: hwAddr,
TxQLen: -1,
},
}
n, err := ns.NewNS()
if err != nil {
t.Fatal(err)
}
defer n.Close()
netnsHandle, err := netns.GetFromPath(n.Path())
if err != nil {
t.Fatal(err)
}
defer netnsHandle.Close()
netlinkHandle, err := netlink.NewHandleAt(netnsHandle)
if err != nil {
t.Fatal(err)
}
defer netlinkHandle.Delete()
if err := netlinkHandle.LinkAdd(link); err != nil {
t.Fatal(err)
}
var isPhysical bool
err = doNetNS(n.Path(), func(_ ns.NetNS) error {
var err error
isPhysical, err = isPhysicalIface(testNetIface)
return err
})
if err != nil {
t.Fatal(err)
}
if isPhysical == true {
t.Fatalf("Got %+v\nExpecting %+v", isPhysical, false)
}
}
func TestNetInterworkingModelIsValid(t *testing.T) {
tests := []struct {
name string
n NetInterworkingModel
want bool
}{
{"Invalid Model", NetXConnectInvalidModel, false},
{"Default Model", NetXConnectDefaultModel, true},
{"Bridged Model", NetXConnectBridgedModel, true},
{"Macvtap Model", NetXConnectMacVtapModel, true},
{"Enlightened Model", NetXConnectEnlightenedModel, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.n.IsValid(); got != tt.want {
t.Errorf("NetInterworkingModel.IsValid() = %v, want %v", got, tt.want)
}
})
}
}
func TestNetInterworkingModelSetModel(t *testing.T) {
var n NetInterworkingModel
tests := []struct {
name string
modelName string
wantErr bool
}{
{"Invalid Model", "Invalid", true},
{"default Model", "default", false},
{"bridged Model", "bridged", false},
{"macvtap Model", "macvtap", false},
{"enlightened Model", "enlightened", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := n.SetModel(tt.modelName); (err != nil) != tt.wantErr {
t.Errorf("NetInterworkingModel.SetModel() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}

View File

@@ -0,0 +1,48 @@
//
// 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 virtcontainers
import (
"fmt"
)
// This is the no proxy implementation of the proxy interface. This
// is a generic implementation for any case (basically any agent),
// where no actual proxy is needed. This happens when the combination
// of the VM and the agent can handle multiple connections without
// additional component to handle the multiplexing. Both the runtime
// and the shim will connect to the agent through the VM, bypassing
// the proxy model.
// That's why this implementation is very generic, and all it does
// is to provide both shim and runtime the correct URL to connect
// directly to the VM.
type noProxy struct {
}
// start is noProxy start implementation for proxy interface.
func (p *noProxy) start(pod Pod, params proxyParams) (int, string, error) {
if params.agentURL == "" {
return -1, "", fmt.Errorf("AgentURL cannot be empty")
}
return 0, params.agentURL, nil
}
// stop is noProxy stop implementation for proxy interface.
func (p *noProxy) stop(pod Pod, pid int) error {
return nil
}

View File

@@ -0,0 +1,53 @@
//
// 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 virtcontainers
import (
"testing"
)
var testNoProxyVMURL = "vmURL"
func TestNoProxyStart(t *testing.T) {
pod := Pod{
agent: newAgent(NoopAgentType),
}
p := &noProxy{}
agentURL := "agentURL"
pid, vmURL, err := p.start(pod, proxyParams{agentURL: agentURL})
if err != nil {
t.Fatal(err)
}
if vmURL != agentURL {
t.Fatalf("Got URL %q, expecting %q", vmURL, agentURL)
}
if pid != 0 {
t.Fatal("Failure since returned PID should be 0")
}
}
func TestNoProxyStop(t *testing.T) {
p := &noProxy{}
if err := p.stop(Pod{}, 0); err != nil {
t.Fatal(err)
}
}

View File

@@ -0,0 +1,81 @@
//
// Copyright (c) 2016 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 virtcontainers
import (
"syscall"
)
// noopAgent a.k.a. NO-OP Agent is an empty Agent implementation, for testing and
// mocking purposes.
type noopAgent struct {
}
// init initializes the Noop agent, i.e. it does nothing.
func (n *noopAgent) init(pod *Pod, config interface{}) error {
return nil
}
// createPod is the Noop agent pod creation implementation. It does nothing.
func (n *noopAgent) createPod(pod *Pod) error {
return nil
}
// capabilities returns empty capabilities, i.e no capabilties are supported.
func (n *noopAgent) capabilities() capabilities {
return capabilities{}
}
// exec is the Noop agent command execution implementation. It does nothing.
func (n *noopAgent) exec(pod *Pod, c Container, cmd Cmd) (*Process, error) {
return nil, nil
}
// startPod is the Noop agent Pod starting implementation. It does nothing.
func (n *noopAgent) startPod(pod Pod) error {
return nil
}
// stopPod is the Noop agent Pod stopping implementation. It does nothing.
func (n *noopAgent) stopPod(pod Pod) error {
return nil
}
// createContainer is the Noop agent Container creation implementation. It does nothing.
func (n *noopAgent) createContainer(pod *Pod, c *Container) (*Process, error) {
return &Process{}, nil
}
// startContainer is the Noop agent Container starting implementation. It does nothing.
func (n *noopAgent) startContainer(pod Pod, c *Container) error {
return nil
}
// stopContainer is the Noop agent Container stopping implementation. It does nothing.
func (n *noopAgent) stopContainer(pod Pod, c Container) error {
return nil
}
// killContainer is the Noop agent Container signaling implementation. It does nothing.
func (n *noopAgent) killContainer(pod Pod, c Container, signal syscall.Signal, all bool) error {
return nil
}
// processListContainer is the Noop agent Container ps implementation. It does nothing.
func (n *noopAgent) processListContainer(pod Pod, c Container, options ProcessListOptions) (ProcessList, error) {
return nil, nil
}

View File

@@ -0,0 +1,125 @@
//
// Copyright (c) 2016 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 virtcontainers
import (
"testing"
)
func testCreateNoopContainer() (*Pod, *Container, error) {
contID := "100"
config := newTestPodConfigNoop()
p, err := CreatePod(config)
if err != nil {
return nil, nil, err
}
contConfig := newTestContainerConfigNoop(contID)
p, c, err := CreateContainer(p.ID(), contConfig)
if err != nil {
return nil, nil, err
}
return p.(*Pod), c.(*Container), nil
}
func TestNoopAgentInit(t *testing.T) {
n := &noopAgent{}
pod := &Pod{}
err := n.init(pod, nil)
if err != nil {
t.Fatal(err)
}
}
func TestNoopAgentExec(t *testing.T) {
n := &noopAgent{}
cmd := Cmd{}
pod, container, err := testCreateNoopContainer()
if err != nil {
t.Fatal(err)
}
if _, err = n.exec(pod, *container, cmd); err != nil {
t.Fatal(err)
}
}
func TestNoopAgentStartPod(t *testing.T) {
n := &noopAgent{}
pod := Pod{}
err := n.startPod(pod)
if err != nil {
t.Fatal(err)
}
}
func TestNoopAgentStopPod(t *testing.T) {
n := &noopAgent{}
pod := Pod{}
err := n.stopPod(pod)
if err != nil {
t.Fatal(err)
}
}
func TestNoopAgentCreateContainer(t *testing.T) {
n := &noopAgent{}
pod, container, err := testCreateNoopContainer()
if err != nil {
t.Fatal(err)
}
if err := n.startPod(*pod); err != nil {
t.Fatal(err)
}
if _, err := n.createContainer(pod, container); err != nil {
t.Fatal(err)
}
}
func TestNoopAgentStartContainer(t *testing.T) {
n := &noopAgent{}
pod, container, err := testCreateNoopContainer()
if err != nil {
t.Fatal(err)
}
err = n.startContainer(*pod, container)
if err != nil {
t.Fatal(err)
}
}
func TestNoopAgentStopContainer(t *testing.T) {
n := &noopAgent{}
pod, container, err := testCreateNoopContainer()
if err != nil {
t.Fatal(err)
}
err = n.stopContainer(*pod, *container)
if err != nil {
t.Fatal(err)
}
}

View File

@@ -0,0 +1,48 @@
//
// Copyright (c) 2016 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 virtcontainers
// noopNetwork a.k.a. NO-OP Network is an empty network implementation, for
// testing and mocking purposes.
type noopNetwork struct {
}
// init initializes the network, setting a new network namespace for the Noop network.
// It does nothing.
func (n *noopNetwork) init(config NetworkConfig) (string, bool, error) {
return "", true, nil
}
// run runs a callback in the specified network namespace for
// the Noop network.
// It does nothing.
func (n *noopNetwork) run(networkNSPath string, cb func() error) error {
return cb()
}
// add adds all needed interfaces inside the network namespace the Noop network.
// It does nothing.
func (n *noopNetwork) add(pod Pod, config NetworkConfig, netNsPath string, netNsCreated bool) (NetworkNamespace, error) {
return NetworkNamespace{}, nil
}
// remove unbridges and deletes TAP interfaces. It also removes virtual network
// interfaces and deletes the network namespace for the Noop network.
// It does nothing.
func (n *noopNetwork) remove(pod Pod, networkNS NetworkNamespace) error {
return nil
}

View File

@@ -0,0 +1,35 @@
//
// 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 virtcontainers
// This is a dummy proxy implementation of the proxy interface, only
// used for testing purpose.
type noopProxy struct{}
var noopProxyURL = "noopProxyURL"
// register is the proxy start implementation for testing purpose.
// It does nothing.
func (p *noopProxy) start(pod Pod, params proxyParams) (int, string, error) {
return 0, noopProxyURL, nil
}
// stop is the proxy stop implementation for testing purpose.
// It does nothing.
func (p *noopProxy) stop(pod Pod, pid int) error {
return nil
}

View File

@@ -0,0 +1,25 @@
//
// 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 virtcontainers
type noopShim struct{}
// start is the noopShim start implementation for testing purpose.
// It does nothing.
func (s *noopShim) start(pod Pod, params ShimParams) (int, error) {
return 1000, nil
}

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 virtcontainers
import (
"testing"
)
func TestNoopShimStart(t *testing.T) {
s := &noopShim{}
pod := Pod{}
params := ShimParams{}
expected := 1000
pid, err := s.start(pod, params)
if err != nil {
t.Fatal(err)
}
if pid != expected {
t.Fatalf("PID should be %d", expected)
}
}

40
virtcontainers/nsenter.go Normal file
View File

@@ -0,0 +1,40 @@
//
// Copyright (c) 2016 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 virtcontainers
// nsenter is a spawner implementation for the nsenter util-linux command.
type nsenter struct {
ContConfig ContainerConfig
}
const (
// NsenterCmd is the command used to start nsenter.
nsenterCmd = "nsenter"
)
// formatArgs is the spawner command formatting implementation for nsenter.
func (n *nsenter) formatArgs(args []string) ([]string, error) {
var newArgs []string
pid := "-1"
// TODO: Retrieve container PID from container ID
newArgs = append(newArgs, nsenterCmd+" --target "+pid+" --mount --uts --ipc --net --pid")
newArgs = append(newArgs, args...)
return newArgs, nil
}

View File

@@ -0,0 +1,43 @@
//
// Copyright (c) 2016 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 virtcontainers
import (
"strings"
"testing"
)
func testNsEnterFormatArgs(t *testing.T, args []string, expected string) {
nsenter := &nsenter{}
cmd, err := nsenter.formatArgs(args)
if err != nil {
t.Fatal(err)
}
if strings.Join(cmd, " ") != expected {
t.Fatal()
}
}
func TestNsEnterFormatArgsHello(t *testing.T) {
expectedCmd := "nsenter --target -1 --mount --uts --ipc --net --pid echo hello"
args := []string{"echo", "hello"}
testNsEnterFormatArgs(t, args, expectedCmd)
}

View File

@@ -0,0 +1,62 @@
//
// 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 annotations
const (
vcAnnotationsPrefix = "com.github.containers.virtcontainers."
// KernelPath is a pod annotation for passing a per container path pointing at the kernel needed to boot the container VM.
KernelPath = vcAnnotationsPrefix + "KernelPath"
// ImagePath is a pod annotation for passing a per container path pointing at the guest image that will run in the container VM.
ImagePath = vcAnnotationsPrefix + "ImagePath"
// HypervisorPath is a pod annotation for passing a per container path pointing at the hypervisor that will run the container VM.
HypervisorPath = vcAnnotationsPrefix + "HypervisorPath"
// FirmwarePath is a pod annotation for passing a per container path pointing at the guest firmware that will run the container VM.
FirmwarePath = vcAnnotationsPrefix + "FirmwarePath"
// KernelHash is a pod annotation for passing a container kernel image SHA-512 hash value.
KernelHash = vcAnnotationsPrefix + "KernelHash"
// ImageHash is an pod annotation for passing a container guest image SHA-512 hash value.
ImageHash = vcAnnotationsPrefix + "ImageHash"
// HypervisorHash is an pod annotation for passing a container hypervisor binary SHA-512 hash value.
HypervisorHash = vcAnnotationsPrefix + "HypervisorHash"
// FirmwareHash is an pod annotation for passing a container guest firmware SHA-512 hash value.
FirmwareHash = vcAnnotationsPrefix + "FirmwareHash"
// AssetHashType is the hash type used for assets verification
AssetHashType = vcAnnotationsPrefix + "AssetHashType"
// ConfigJSONKey is the annotation key to fetch the OCI configuration.
ConfigJSONKey = vcAnnotationsPrefix + "pkg.oci.config"
// BundlePathKey is the annotation key to fetch the OCI configuration file path.
BundlePathKey = vcAnnotationsPrefix + "pkg.oci.bundle_path"
// ContainerTypeKey is the annotation key to fetch container type.
ContainerTypeKey = vcAnnotationsPrefix + "pkg.oci.container_type"
)
const (
// SHA512 is the SHA-512 (64) hash algorithm
SHA512 string = "sha512"
)

View File

@@ -0,0 +1,36 @@
//
// 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 dockershim
const (
// Copied from k8s.io/pkg/kubelet/dockershim/docker_service.go,
// used to identify whether a docker container is a sandbox or
// a regular container, will be removed after defining those as
// public fields in dockershim.
// ContainerTypeLabelKey is the container type (podsandbox or container) annotation
ContainerTypeLabelKey = "io.kubernetes.docker.type"
// ContainerTypeLabelSandbox represents a pod sandbox container
ContainerTypeLabelSandbox = "podsandbox"
// ContainerTypeLabelContainer represents a container running within a pod
ContainerTypeLabelContainer = "container"
// SandboxIDLabelKey is the sandbox ID annotation
SandboxIDLabelKey = "io.kubernetes.sandbox.id"
)

View File

@@ -0,0 +1,6 @@
reviewers:
- virtcontainers-maintainers
approvers:
- mcastelino
- sboeuf

View File

@@ -0,0 +1,167 @@
//
// Copyright (c) 2016 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 cni
import (
"fmt"
"sort"
"github.com/containernetworking/cni/libcni"
types "github.com/containernetworking/cni/pkg/types"
)
// CNI default values to find plugins and configurations.
const (
LocalNetName = "lo"
DefNetName = "net"
PluginConfDir = "/etc/cni/net.d"
PluginBinDir = "/opt/cni/bin"
)
var confExtensions = []string{".conf"}
// NetworkPlugin is the CNI network plugin handler.
type NetworkPlugin struct {
loNetwork *cniNetwork
defNetwork *cniNetwork
}
type cniNetwork struct {
name string
networkConfig *libcni.NetworkConfig
cniConfig libcni.CNI
}
// NewNetworkPlugin initialize the CNI network plugin and returns
// a handler to it.
func NewNetworkPlugin() (*NetworkPlugin, error) {
return NewNetworkPluginWithArgs(PluginConfDir, PluginBinDir)
}
// NewNetworkPluginWithArgs initialize the CNI network plugin, specifying the
// configuration and binary directories, and it returns a handler to it.
func NewNetworkPluginWithArgs(confDir, binDir string) (*NetworkPlugin, error) {
var err error
plugin := &NetworkPlugin{}
plugin.loNetwork, err = getLoNetwork(confDir, binDir)
if err != nil {
return nil, err
}
plugin.defNetwork, err = getDefNetwork(confDir, binDir)
if err != nil {
return nil, err
}
return plugin, nil
}
func getNetwork(confDir, binDir, defaultName string, local bool) (*cniNetwork, error) {
confFiles, err := libcni.ConfFiles(confDir, confExtensions)
if err != nil || confFiles == nil {
return nil, fmt.Errorf("Invalid configuration directory %s", confDir)
}
if len(confFiles) == 0 {
return nil, fmt.Errorf("Could not find networks in %s", confDir)
}
if local == true {
sort.Sort(sort.Reverse(sort.StringSlice(confFiles)))
} else {
sort.Sort(sort.StringSlice(confFiles))
}
for _, confFile := range confFiles {
conf, err := libcni.ConfFromFile(confFile)
if err != nil {
continue
}
cninet := &libcni.CNIConfig{
Path: []string{binDir},
}
name := defaultName
if conf.Network.Name != "" {
name = conf.Network.Name
}
network := &cniNetwork{
name: name,
networkConfig: conf,
cniConfig: cninet,
}
return network, nil
}
return nil, fmt.Errorf("No valid networks found in %s", confDir)
}
func getLoNetwork(confDir, binDir string) (*cniNetwork, error) {
return getNetwork(confDir, binDir, LocalNetName, true)
}
func getDefNetwork(confDir, binDir string) (*cniNetwork, error) {
return getNetwork(confDir, binDir, DefNetName, false)
}
func buildRuntimeConf(podID, podNetNSPath, ifName string) *libcni.RuntimeConf {
return &libcni.RuntimeConf{
ContainerID: podID,
NetNS: podNetNSPath,
IfName: ifName,
}
}
// AddNetwork calls the CNI plugin to create a network between the host and the network namespace.
func (plugin *NetworkPlugin) AddNetwork(podID, netNSPath, ifName string) (types.Result, error) {
rt := buildRuntimeConf(podID, netNSPath, ifName)
_, err := plugin.loNetwork.cniConfig.AddNetwork(plugin.loNetwork.networkConfig, rt)
if err != nil {
return nil, err
}
ifaceResult, err := plugin.defNetwork.cniConfig.AddNetwork(plugin.defNetwork.networkConfig, rt)
if err != nil {
return nil, err
}
return ifaceResult, nil
}
// RemoveNetwork calls the CNI plugin to remove a specific network previously created between
// the host and the network namespace.
func (plugin *NetworkPlugin) RemoveNetwork(podID, netNSPath, ifName string) error {
rt := buildRuntimeConf(podID, netNSPath, ifName)
err := plugin.defNetwork.cniConfig.DelNetwork(plugin.defNetwork.networkConfig, rt)
if err != nil {
return err
}
err = plugin.loNetwork.cniConfig.DelNetwork(plugin.loNetwork.networkConfig, rt)
if err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,408 @@
//
// Copyright (c) 2016 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 cni
import (
"fmt"
"os"
"path/filepath"
"reflect"
"testing"
"github.com/containernetworking/cni/libcni"
"github.com/containernetworking/plugins/pkg/ns"
)
const (
dirMode = os.FileMode(0750)
)
var testDirBase = "../../utils/supportfiles/cni"
var testConfDir = testDirBase + "/net.d"
var testBinDir = testDirBase + "/bin"
var testWrongConfDir = testDirBase + "/wrong"
var testDefFile = "10-test_network.conf"
var testLoFile = "99-test_loopback.conf"
var testWrongFile = "100-test_error.conf"
var testLoFileContent = []byte(`{
"cniVersion": "0.3.0",
"name": "testlonetwork",
"type": "loopback"
}`)
var testLoFileContentNoName = []byte(`{
"cniVersion": "0.3.0",
"type": "loopback"
}`)
var testDefFileContent = []byte(`{
"cniVersion": "0.3.0",
"name": "testdefnetwork",
"type": "cni-bridge",
"bridge": "cni0",
"isGateway": true,
"ipMasq": true,
"ipam": {
"type": "host-local",
"subnet": "10.88.0.0/16",
"routes": [
{ "dst": "0.0.0.0/0" }
]
}
}`)
var testDefFileContentNoName = []byte(`{
"cniVersion": "0.3.0",
"type": "cni-bridge",
"bridge": "cni0",
"isGateway": true,
"ipMasq": true,
"ipam": {
"type": "host-local",
"subnet": "10.88.0.0/16",
"routes": [
{ "dst": "0.0.0.0/0" }
]
}
}`)
var testWrongFileContent = []byte(`{
"cniVersion "0.3.0",
"type": "loopback"
}`)
func createLoNetwork(t *testing.T) {
loFile := filepath.Join(testConfDir, testLoFile)
f, err := os.Create(loFile)
if err != nil {
t.Fatal(err)
}
defer f.Close()
_, err = f.Write(testLoFileContent)
if err != nil {
t.Fatal(err)
}
}
func createLoNetworkNoName(t *testing.T) {
loFile := filepath.Join(testConfDir, testLoFile)
f, err := os.Create(loFile)
if err != nil {
t.Fatal(err)
}
defer f.Close()
_, err = f.Write(testLoFileContentNoName)
if err != nil {
t.Fatal(err)
}
}
func createDefNetwork(t *testing.T) {
defFile := filepath.Join(testConfDir, testDefFile)
f, err := os.Create(defFile)
if err != nil {
t.Fatal(err)
}
defer f.Close()
_, err = f.Write(testDefFileContent)
if err != nil {
t.Fatal(err)
}
}
func createDefNetworkNoName(t *testing.T) {
defFile := filepath.Join(testConfDir, testDefFile)
f, err := os.Create(defFile)
if err != nil {
t.Fatal(err)
}
defer f.Close()
_, err = f.Write(testDefFileContentNoName)
if err != nil {
t.Fatal(err)
}
}
func createWrongNetwork(t *testing.T) {
wrongFile := filepath.Join(testConfDir, testWrongFile)
f, err := os.Create(wrongFile)
if err != nil {
t.Fatal(err)
}
defer f.Close()
_, err = f.Write(testWrongFileContent)
if err != nil {
t.Fatal(err)
}
}
func removeLoNetwork(t *testing.T) {
loFile := filepath.Join(testConfDir, testLoFile)
err := os.Remove(loFile)
if err != nil {
t.Fatal(err)
}
}
func removeDefNetwork(t *testing.T) {
defFile := filepath.Join(testConfDir, testDefFile)
err := os.Remove(defFile)
if err != nil {
t.Fatal(err)
}
}
func removeWrongNetwork(t *testing.T) {
wrongFile := filepath.Join(testConfDir, testWrongFile)
err := os.Remove(wrongFile)
if err != nil {
t.Fatal(err)
}
}
func TestNewNetworkPluginSuccessful(t *testing.T) {
createLoNetwork(t)
defer removeLoNetwork(t)
createDefNetwork(t)
defer removeDefNetwork(t)
netPlugin, err := NewNetworkPluginWithArgs(testConfDir, testBinDir)
if err != nil {
t.Fatal(err)
}
if netPlugin.loNetwork == nil {
t.Fatal("Invalid local network")
}
if netPlugin.defNetwork == nil {
t.Fatal("Invalid default network")
}
if netPlugin.loNetwork.name != "testlonetwork" {
t.Fatal("Invalid local network name")
}
if netPlugin.defNetwork.name != "testdefnetwork" {
t.Fatal("Invalid default network name")
}
}
func TestNewNetworkPluginSuccessfulNoName(t *testing.T) {
createLoNetworkNoName(t)
defer removeLoNetwork(t)
createDefNetworkNoName(t)
defer removeDefNetwork(t)
netPlugin, err := NewNetworkPluginWithArgs(testConfDir, testBinDir)
if err != nil {
t.Fatal(err)
}
if netPlugin.loNetwork == nil {
t.Fatal("Invalid local network")
}
if netPlugin.defNetwork == nil {
t.Fatal("Invalid default network")
}
if netPlugin.loNetwork.name != "lo" {
t.Fatal("Invalid local network name")
}
if netPlugin.defNetwork.name != "net" {
t.Fatal("Invalid default network name")
}
}
func TestNewNetworkPluginFailureNoNetwork(t *testing.T) {
netPlugin, err := NewNetworkPluginWithArgs(testConfDir, testBinDir)
if err == nil || netPlugin != nil {
t.Fatal("Should fail because no available network")
}
}
func TestNewNetworkPluginFailureNoConfDir(t *testing.T) {
netPlugin, err := NewNetworkPluginWithArgs(testWrongConfDir, testBinDir)
if err == nil || netPlugin != nil {
t.Fatal("Should fail because configuration directory does not exist")
}
}
func TestNewNetworkPluginFailureWrongNetwork(t *testing.T) {
createWrongNetwork(t)
defer removeWrongNetwork(t)
netPlugin, err := NewNetworkPluginWithArgs(testConfDir, testBinDir)
if err == nil || netPlugin != nil {
t.Fatal("Should fail because of wrong network definition")
}
}
func TestBuildRuntimeConf(t *testing.T) {
expected := libcni.RuntimeConf{
ContainerID: "testPodID",
NetNS: "testPodNetNSPath",
IfName: "testIfName",
}
runtimeConf := buildRuntimeConf("testPodID", "testPodNetNSPath", "testIfName")
if reflect.DeepEqual(*runtimeConf, expected) == false {
t.Fatal("Runtime configuration different from expected one")
}
}
func TestAddNetworkSuccessful(t *testing.T) {
createLoNetworkNoName(t)
defer removeLoNetwork(t)
createDefNetworkNoName(t)
defer removeDefNetwork(t)
netNsHandle, err := ns.NewNS()
if err != nil {
t.Fatal(err)
}
defer netNsHandle.Close()
testNetNsPath := netNsHandle.Path()
netPlugin, err := NewNetworkPluginWithArgs(testConfDir, testBinDir)
if err != nil {
t.Fatal(err)
}
_, err = netPlugin.AddNetwork("testPodID", testNetNsPath, "testIfName")
if err != nil {
t.Fatal(err)
}
}
func TestAddNetworkFailureUnknownNetNs(t *testing.T) {
createLoNetworkNoName(t)
defer removeLoNetwork(t)
createDefNetworkNoName(t)
defer removeDefNetwork(t)
const invalidNetNsPath = "/this/path/does/not/exist"
// ensure it really is invalid
_, err := os.Stat(invalidNetNsPath)
if err == nil {
t.Fatalf("directory %v unexpectedly exists", invalidNetNsPath)
}
netPlugin, err := NewNetworkPluginWithArgs(testConfDir, testBinDir)
if err != nil {
t.Fatal(err)
}
_, err = netPlugin.AddNetwork("testPodID", invalidNetNsPath, "testIfName")
if err == nil {
t.Fatalf("Should fail because netns %s does not exist", invalidNetNsPath)
}
}
func TestRemoveNetworkSuccessful(t *testing.T) {
createLoNetworkNoName(t)
defer removeLoNetwork(t)
createDefNetworkNoName(t)
defer removeDefNetwork(t)
netNsHandle, err := ns.NewNS()
if err != nil {
t.Fatal(err)
}
defer netNsHandle.Close()
testNetNsPath := netNsHandle.Path()
netPlugin, err := NewNetworkPluginWithArgs(testConfDir, testBinDir)
if err != nil {
t.Fatal(err)
}
_, err = netPlugin.AddNetwork("testPodID", testNetNsPath, "testIfName")
if err != nil {
t.Fatal(err)
}
err = netPlugin.RemoveNetwork("testPodID", testNetNsPath, "testIfName")
if err != nil {
t.Fatal(err)
}
}
func TestRemoveNetworkSuccessfulNetworkDoesNotExist(t *testing.T) {
createLoNetworkNoName(t)
defer removeLoNetwork(t)
createDefNetworkNoName(t)
defer removeDefNetwork(t)
netNsHandle, err := ns.NewNS()
if err != nil {
t.Fatal(err)
}
defer netNsHandle.Close()
testNetNsPath := netNsHandle.Path()
netPlugin, err := NewNetworkPluginWithArgs(testConfDir, testBinDir)
if err != nil {
t.Fatal(err)
}
err = netPlugin.RemoveNetwork("testPodID", testNetNsPath, "testIfName")
if err != nil {
// CNI specification says that no error should be returned
// in case we try to tear down a non-existing network.
t.Fatalf("Should pass because network not previously added: %s", err)
}
}
func TestMain(m *testing.M) {
err := os.MkdirAll(testConfDir, dirMode)
if err != nil {
fmt.Println("Could not create test configuration directory:", err)
os.Exit(1)
}
_, err = os.Stat(testBinDir)
if err != nil {
fmt.Println("Test binary directory should exist:", err)
os.RemoveAll(testConfDir)
os.Exit(1)
}
ret := m.Run()
os.RemoveAll(testConfDir)
os.Exit(ret)
}

View File

@@ -0,0 +1,164 @@
/*
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 ethtool
import (
"bytes"
"syscall"
"unsafe"
)
// Maximum size of an interface name
const (
IFNAMSIZ = 16
)
// ioctl ethtool request
const (
SIOCETHTOOL = 0x8946
)
// ethtool stats related constants.
const (
ethGstringLen = 32
ethtoolGDrvInfo = 0x00000003
)
// maxGtrings maximum number of stats entries that ethtool can
// retrieve currently.
const (
maxGstrings = 1000
)
type ifreq struct {
ifrName [IFNAMSIZ]byte
ifrData uintptr
}
type ethtoolDrvInfo struct {
cmd uint32
driver [32]byte
version [32]byte
fwVersion [32]byte
busInfo [32]byte
eromVersion [32]byte
reserved2 [12]byte
nPrivFlags uint32
nStats uint32
testinfoLen uint32
eedumpLen uint32
regdumpLen uint32
}
type ethtoolGStrings struct {
cmd uint32
stringSet uint32
len uint32
data [maxGstrings * ethGstringLen]byte
}
type ethtoolStats struct {
cmd uint32
nStats uint32
data [maxGstrings]uint64
}
// Ethtool file descriptor.
type Ethtool struct {
fd int
}
// DriverName returns the driver name of the given interface.
func (e *Ethtool) DriverName(intf string) (string, error) {
info, err := e.getDriverInfo(intf)
if err != nil {
return "", err
}
return string(bytes.Trim(info.driver[:], "\x00")), nil
}
// BusInfo returns the bus info of the given interface.
func (e *Ethtool) BusInfo(intf string) (string, error) {
info, err := e.getDriverInfo(intf)
if err != nil {
return "", err
}
return string(bytes.Trim(info.busInfo[:], "\x00")), nil
}
func (e *Ethtool) getDriverInfo(intf string) (ethtoolDrvInfo, error) {
drvinfo := ethtoolDrvInfo{
cmd: ethtoolGDrvInfo,
}
var name [IFNAMSIZ]byte
copy(name[:], []byte(intf))
ifr := ifreq{
ifrName: name,
ifrData: uintptr(unsafe.Pointer(&drvinfo)),
}
_, _, ep := syscall.Syscall(syscall.SYS_IOCTL, uintptr(e.fd), SIOCETHTOOL, uintptr(unsafe.Pointer(&ifr)))
if ep != 0 {
return ethtoolDrvInfo{}, syscall.Errno(ep)
}
return drvinfo, nil
}
// Close closes the ethtool file descriptor.
func (e *Ethtool) Close() {
syscall.Close(e.fd)
}
// NewEthtool opens a ethtool socket.
func NewEthtool() (*Ethtool, error) {
fd, _, err := syscall.RawSyscall(syscall.SYS_SOCKET, syscall.AF_INET, syscall.SOCK_DGRAM, syscall.IPPROTO_IP)
if err != 0 {
return nil, syscall.Errno(err)
}
return &Ethtool{
fd: int(fd),
}, nil
}
// BusInfo returns the bus information of the network device.
func BusInfo(intf string) (string, error) {
e, err := NewEthtool()
if err != nil {
return "", err
}
defer e.Close()
return e.BusInfo(intf)
}
// DriverName returns the driver name of the network interface.
func DriverName(intf string) (string, error) {
e, err := NewEthtool()
if err != nil {
return "", err
}
defer e.Close()
return e.DriverName(intf)
}

View File

@@ -0,0 +1,67 @@
/*
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 ethtool
import (
"net"
"testing"
)
func TestDriverName(t *testing.T) {
intfs, err := net.Interfaces()
if err != nil {
t.Fatal(err)
}
// we expected to have at least one success
success := false
for _, intf := range intfs {
_, err := DriverName(intf.Name)
if err == nil {
success = true
}
}
if !success {
t.Fatal("Unable to retrieve driver from any interface of this system.")
}
}
func TestBusInfo(t *testing.T) {
intfs, err := net.Interfaces()
if err != nil {
t.Fatal(err)
}
// we expected to have at least one success
success := false
for _, intf := range intfs {
_, err := BusInfo(intf.Name)
if err == nil {
success = true
}
}
if !success {
t.Fatal("Unable to retrieve bus info from any interface of this system.")
}
}

View File

@@ -0,0 +1,545 @@
//
// Copyright (c) 2016 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 hyperstart
import (
"encoding/binary"
"encoding/json"
"fmt"
"math"
"net"
"sync"
"time"
"github.com/sirupsen/logrus"
)
// Control command IDs
// Need to be in sync with hyperstart/src/api.h
const (
Version = "version"
StartPod = "startpod"
DestroyPod = "destroypod"
ExecCmd = "execcmd"
Ready = "ready"
Ack = "ack"
Error = "error"
WinSize = "winsize"
Ping = "ping"
FinishPod = "finishpod"
Next = "next"
WriteFile = "writefile"
ReadFile = "readfile"
NewContainer = "newcontainer"
KillContainer = "killcontainer"
OnlineCPUMem = "onlinecpumem"
SetupInterface = "setupinterface"
SetupRoute = "setuproute"
RemoveContainer = "removecontainer"
PsContainer = "pscontainer"
)
// CodeList is the map making the relation between a string command
// and its corresponding code.
var CodeList = map[string]uint32{
Version: VersionCode,
StartPod: StartPodCode,
DestroyPod: DestroyPodCode,
ExecCmd: ExecCmdCode,
Ready: ReadyCode,
Ack: AckCode,
Error: ErrorCode,
WinSize: WinsizeCode,
Ping: PingCode,
Next: NextCode,
WriteFile: WriteFileCode,
ReadFile: ReadFileCode,
NewContainer: NewContainerCode,
KillContainer: KillContainerCode,
OnlineCPUMem: OnlineCPUMemCode,
SetupInterface: SetupInterfaceCode,
SetupRoute: SetupRouteCode,
RemoveContainer: RemoveContainerCode,
PsContainer: PsContainerCode,
}
// Values related to the communication on control channel.
const (
CtlHdrSize = 8
CtlHdrLenOffset = 4
)
// Values related to the communication on tty channel.
const (
TtyHdrSize = 12
TtyHdrLenOffset = 8
)
type connState struct {
sync.Mutex
opened bool
}
func (c *connState) close() {
c.Lock()
defer c.Unlock()
c.opened = false
}
func (c *connState) open() {
c.Lock()
defer c.Unlock()
c.opened = true
}
func (c *connState) closed() bool {
c.Lock()
defer c.Unlock()
return !c.opened
}
// Hyperstart is the base structure for hyperstart.
type Hyperstart struct {
ctlSerial, ioSerial string
sockType string
ctl, io net.Conn
ctlState, ioState connState
// ctl access is arbitrated by ctlMutex. We can only allow a single
// "transaction" (write command + read answer) at a time
ctlMutex sync.Mutex
ctlMulticast *multicast
ctlChDone chan interface{}
}
var hyperLog = logrus.FieldLogger(logrus.New())
// SetLogger sets the logger for hyperstart package.
func SetLogger(logger logrus.FieldLogger) {
hyperLog = logger.WithField("source", "virtcontainers/hyperstart")
}
// NewHyperstart returns a new hyperstart structure.
func NewHyperstart(ctlSerial, ioSerial, sockType string) *Hyperstart {
return &Hyperstart{
ctlSerial: ctlSerial,
ioSerial: ioSerial,
sockType: sockType,
}
}
// GetCtlSock returns the internal CTL sock.
func (h *Hyperstart) GetCtlSock() net.Conn {
return h.ctl
}
// GetIoSock returns the internal IO sock.
func (h *Hyperstart) GetIoSock() net.Conn {
return h.io
}
// GetCtlSockPath returns the internal CTL sock path.
func (h *Hyperstart) GetCtlSockPath() string {
return h.ctlSerial
}
// GetIoSockPath returns the internal IO sock path.
func (h *Hyperstart) GetIoSockPath() string {
return h.ioSerial
}
// GetSockType returns the internal sock type.
func (h *Hyperstart) GetSockType() string {
return h.sockType
}
// OpenSocketsNoMulticast opens both CTL and IO sockets, without
// starting the multicast.
func (h *Hyperstart) OpenSocketsNoMulticast() error {
var err error
h.ctl, err = net.Dial(h.sockType, h.ctlSerial)
if err != nil {
return err
}
h.ctlState.open()
h.io, err = net.Dial(h.sockType, h.ioSerial)
if err != nil {
h.ctl.Close()
return err
}
h.ioState.open()
return nil
}
// OpenSockets opens both CTL and IO sockets.
func (h *Hyperstart) OpenSockets() error {
if err := h.OpenSocketsNoMulticast(); err != nil {
return err
}
h.ctlChDone = make(chan interface{})
h.ctlMulticast = startCtlMonitor(h.ctl, h.ctlChDone)
return nil
}
// CloseSockets closes both CTL and IO sockets.
func (h *Hyperstart) CloseSockets() error {
if !h.ctlState.closed() {
if h.ctlChDone != nil {
// Wait for the CTL channel to be terminated.
select {
case <-h.ctlChDone:
break
case <-time.After(time.Duration(3) * time.Second):
return fmt.Errorf("CTL channel did not end as expected")
}
}
err := h.ctl.Close()
if err != nil {
return err
}
h.ctlState.close()
}
if !h.ioState.closed() {
err := h.io.Close()
if err != nil {
return err
}
h.ioState.close()
}
h.ctlMulticast = nil
return nil
}
// SetDeadline sets a timeout for CTL connection.
func (h *Hyperstart) SetDeadline(t time.Time) error {
err := h.ctl.SetDeadline(t)
if err != nil {
return err
}
return nil
}
// IsStarted returns about connection status.
func (h *Hyperstart) IsStarted() bool {
ret := false
timeoutDuration := 1 * time.Second
if h.ctlState.closed() {
return ret
}
h.SetDeadline(time.Now().Add(timeoutDuration))
_, err := h.SendCtlMessage(Ping, nil)
if err == nil {
ret = true
}
h.SetDeadline(time.Time{})
if ret == false {
h.CloseSockets()
}
return ret
}
// FormatMessage formats hyperstart messages.
func FormatMessage(payload interface{}) ([]byte, error) {
var payloadSlice []byte
var err error
if payload != nil {
switch p := payload.(type) {
case string:
payloadSlice = []byte(p)
default:
payloadSlice, err = json.Marshal(p)
if err != nil {
return nil, err
}
}
}
return payloadSlice, nil
}
// ReadCtlMessage reads an hyperstart message from conn and returns a decoded message.
//
// This is a low level function, for a full and safe transaction on the
// hyperstart control serial link, use SendCtlMessage.
func ReadCtlMessage(conn net.Conn) (*DecodedMessage, error) {
needRead := CtlHdrSize
length := 0
read := 0
buf := make([]byte, 512)
res := []byte{}
for read < needRead {
want := needRead - read
if want > 512 {
want = 512
}
nr, err := conn.Read(buf[:want])
if err != nil {
return nil, err
}
res = append(res, buf[:nr]...)
read = read + nr
if length == 0 && read >= CtlHdrSize {
length = int(binary.BigEndian.Uint32(res[CtlHdrLenOffset:CtlHdrSize]))
if length > CtlHdrSize {
needRead = length
}
}
}
return &DecodedMessage{
Code: binary.BigEndian.Uint32(res[:CtlHdrLenOffset]),
Message: res[CtlHdrSize:],
}, nil
}
// WriteCtlMessage writes an hyperstart message to conn.
//
// This is a low level function, for a full and safe transaction on the
// hyperstart control serial link, use SendCtlMessage.
func (h *Hyperstart) WriteCtlMessage(conn net.Conn, m *DecodedMessage) error {
length := len(m.Message) + CtlHdrSize
// XXX: Support sending messages by chunks to support messages over
// 10240 bytes. That limit is from hyperstart src/init.c,
// hyper_channel_ops, rbuf_size.
if length > 10240 {
return fmt.Errorf("message too long %d", length)
}
msg := make([]byte, length)
binary.BigEndian.PutUint32(msg[:], uint32(m.Code))
binary.BigEndian.PutUint32(msg[CtlHdrLenOffset:], uint32(length))
copy(msg[CtlHdrSize:], m.Message)
_, err := conn.Write(msg)
if err != nil {
return err
}
return nil
}
// ReadIoMessageWithConn returns data coming from the specified IO channel.
func ReadIoMessageWithConn(conn net.Conn) (*TtyMessage, error) {
needRead := TtyHdrSize
length := 0
read := 0
buf := make([]byte, 512)
res := []byte{}
for read < needRead {
want := needRead - read
if want > 512 {
want = 512
}
nr, err := conn.Read(buf[:want])
if err != nil {
return nil, err
}
res = append(res, buf[:nr]...)
read = read + nr
if length == 0 && read >= TtyHdrSize {
length = int(binary.BigEndian.Uint32(res[TtyHdrLenOffset:TtyHdrSize]))
if length > TtyHdrSize {
needRead = length
}
}
}
return &TtyMessage{
Session: binary.BigEndian.Uint64(res[:TtyHdrLenOffset]),
Message: res[TtyHdrSize:],
}, nil
}
// ReadIoMessage returns data coming from the IO channel.
func (h *Hyperstart) ReadIoMessage() (*TtyMessage, error) {
return ReadIoMessageWithConn(h.io)
}
// SendIoMessageWithConn sends data to the specified IO channel.
func SendIoMessageWithConn(conn net.Conn, ttyMsg *TtyMessage) error {
length := len(ttyMsg.Message) + TtyHdrSize
// XXX: Support sending messages by chunks to support messages over
// 10240 bytes. That limit is from hyperstart src/init.c,
// hyper_channel_ops, rbuf_size.
if length > 10240 {
return fmt.Errorf("message too long %d", length)
}
msg := make([]byte, length)
binary.BigEndian.PutUint64(msg[:], ttyMsg.Session)
binary.BigEndian.PutUint32(msg[TtyHdrLenOffset:], uint32(length))
copy(msg[TtyHdrSize:], ttyMsg.Message)
n, err := conn.Write(msg)
if err != nil {
return err
}
if n != length {
return fmt.Errorf("%d bytes written out of %d expected", n, length)
}
return nil
}
// SendIoMessage sends data to the IO channel.
func (h *Hyperstart) SendIoMessage(ttyMsg *TtyMessage) error {
return SendIoMessageWithConn(h.io, ttyMsg)
}
// CodeFromCmd translates a string command to its corresponding code.
func (h *Hyperstart) CodeFromCmd(cmd string) (uint32, error) {
_, ok := CodeList[cmd]
if ok == false {
return math.MaxUint32, fmt.Errorf("unknown command '%s'", cmd)
}
return CodeList[cmd], nil
}
// CheckReturnedCode ensures we did not receive an ERROR code.
func (h *Hyperstart) CheckReturnedCode(recvMsg *DecodedMessage, expectedCode uint32) error {
if recvMsg.Code != expectedCode {
if recvMsg.Code == ErrorCode {
return fmt.Errorf("ERROR received from VM agent, control msg received : %s", recvMsg.Message)
}
return fmt.Errorf("CMD ID received %d not matching expected %d, control msg received : %s", recvMsg.Code, expectedCode, recvMsg.Message)
}
return nil
}
// WaitForReady waits for a READY message on CTL channel.
func (h *Hyperstart) WaitForReady() error {
if h.ctlMulticast == nil {
return fmt.Errorf("No multicast available for CTL channel")
}
channel, err := h.ctlMulticast.listen("", "", replyType)
if err != nil {
return err
}
msg := <-channel
err = h.CheckReturnedCode(msg, ReadyCode)
if err != nil {
return err
}
return nil
}
// WaitForPAE waits for a PROCESSASYNCEVENT message on CTL channel.
func (h *Hyperstart) WaitForPAE(containerID, processID string) (*PAECommand, error) {
if h.ctlMulticast == nil {
return nil, fmt.Errorf("No multicast available for CTL channel")
}
channel, err := h.ctlMulticast.listen(containerID, processID, eventType)
if err != nil {
return nil, err
}
msg := <-channel
var paeData PAECommand
err = json.Unmarshal(msg.Message, paeData)
if err != nil {
return nil, err
}
return &paeData, nil
}
// SendCtlMessage sends a message to the CTL channel.
//
// This function does a full transaction over the CTL channel: it will rely on the
// multicaster to register a listener reading over the CTL channel. Then it writes
// a command and waits for the multicaster to send hyperstart's answer back before
// it can return.
// Several concurrent calls to SendCtlMessage are allowed, the function ensuring
// proper serialization of the communication by making the listener registration
// and the command writing an atomic operation protected by a mutex.
// Waiting for the reply from multicaster doesn't need to be protected by this mutex.
func (h *Hyperstart) SendCtlMessage(cmd string, data []byte) (*DecodedMessage, error) {
if h.ctlMulticast == nil {
return nil, fmt.Errorf("No multicast available for CTL channel")
}
h.ctlMutex.Lock()
channel, err := h.ctlMulticast.listen("", "", replyType)
if err != nil {
h.ctlMutex.Unlock()
return nil, err
}
code, err := h.CodeFromCmd(cmd)
if err != nil {
h.ctlMutex.Unlock()
return nil, err
}
msgSend := &DecodedMessage{
Code: code,
Message: data,
}
err = h.WriteCtlMessage(h.ctl, msgSend)
if err != nil {
h.ctlMutex.Unlock()
return nil, err
}
h.ctlMutex.Unlock()
msgRecv := <-channel
err = h.CheckReturnedCode(msgRecv, AckCode)
if err != nil {
return nil, err
}
return msgRecv, nil
}

View File

@@ -0,0 +1,583 @@
//
// Copyright (c) 2016 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 hyperstart_test
import (
"math"
"net"
"reflect"
"testing"
"time"
. "github.com/containers/virtcontainers/pkg/hyperstart"
"github.com/containers/virtcontainers/pkg/hyperstart/mock"
)
const (
testSockType = "unix"
testSequence = uint64(100)
testMessage = "test_message"
)
func connectHyperstartNoMulticast(h *Hyperstart) error {
return h.OpenSocketsNoMulticast()
}
func connectHyperstart(h *Hyperstart) error {
return h.OpenSockets()
}
func disconnectHyperstart(h *Hyperstart) {
h.CloseSockets()
}
func connectMockHyperstart(t *testing.T, multiCast bool) (*mock.Hyperstart, *Hyperstart, error) {
mockHyper := mock.NewHyperstart(t)
mockHyper.Start()
ctlSock, ioSock := mockHyper.GetSocketPaths()
h := NewHyperstart(ctlSock, ioSock, testSockType)
var err error
if multiCast {
err = connectHyperstart(h)
} else {
err = connectHyperstartNoMulticast(h)
}
if err != nil {
mockHyper.Stop()
return nil, nil, err
}
return mockHyper, h, nil
}
func TestNewHyperstart(t *testing.T) {
ctlSock := "/tmp/test_hyper.sock"
ioSock := "/tmp/test_tty.sock"
sockType := "test_unix"
h := NewHyperstart(ctlSock, ioSock, sockType)
resultCtlSockPath := h.GetCtlSockPath()
resultIoSockPath := h.GetIoSockPath()
resultSockType := h.GetSockType()
if resultCtlSockPath != ctlSock {
t.Fatalf("CTL sock result %s should be the same than %s", resultCtlSockPath, ctlSock)
}
if resultIoSockPath != ioSock {
t.Fatalf("IO sock result %s should be the same than %s", resultIoSockPath, ioSock)
}
if resultSockType != sockType {
t.Fatalf("Sock type result %s should be the same than %s", resultSockType, sockType)
}
}
func TestOpenSockets(t *testing.T) {
mockHyper := mock.NewHyperstart(t)
mockHyper.Start()
ctlSock, ioSock := mockHyper.GetSocketPaths()
h := NewHyperstart(ctlSock, ioSock, testSockType)
err := h.OpenSockets()
if err != nil {
mockHyper.Stop()
t.Fatal()
}
mockHyper.Stop()
disconnectHyperstart(h)
}
func TestCloseSockets(t *testing.T) {
mockHyper, h, err := connectMockHyperstart(t, true)
if err != nil {
t.Fatal()
}
mockHyper.Stop()
err = h.CloseSockets()
if err != nil {
t.Fatal()
}
}
func TestSetDeadline(t *testing.T) {
mockHyper, h, err := connectMockHyperstart(t, false)
if err != nil {
t.Fatal()
}
defer disconnectHyperstart(h)
defer mockHyper.Stop()
timeoutDuration := 1 * time.Second
err = h.SetDeadline(time.Now().Add(timeoutDuration))
if err != nil {
t.Fatal()
}
mockHyper.SendMessage(ReadyCode, []byte{})
buf := make([]byte, 512)
_, err = h.GetCtlSock().Read(buf)
if err != nil {
t.Fatal()
}
err = h.SetDeadline(time.Now().Add(timeoutDuration))
if err != nil {
t.Fatal()
}
time.Sleep(timeoutDuration)
_, err = h.GetCtlSock().Read(buf)
netErr, ok := err.(net.Error)
if ok && netErr.Timeout() == false {
t.Fatal()
}
}
func TestIsStartedFalse(t *testing.T) {
h := &Hyperstart{}
if h.IsStarted() == true {
t.Fatal()
}
}
func TestIsStartedTrue(t *testing.T) {
mockHyper, h, err := connectMockHyperstart(t, true)
if err != nil {
t.Fatal()
}
defer disconnectHyperstart(h)
defer mockHyper.Stop()
if h.IsStarted() == false {
t.Fatal()
}
}
func testFormatMessage(t *testing.T, payload interface{}, expected []byte) {
res, err := FormatMessage(payload)
if err != nil {
t.Fatal()
}
if reflect.DeepEqual(res, expected) == false {
t.Fatal()
}
}
func TestFormatMessageFromString(t *testing.T) {
payload := testMessage
expectedOut := []byte(payload)
testFormatMessage(t, payload, expectedOut)
}
type TestStruct struct {
FieldString string `json:"fieldString"`
FieldInt int `json:"fieldInt"`
}
func TestFormatMessageFromStruct(t *testing.T) {
payload := TestStruct{
FieldString: "test_string",
FieldInt: 100,
}
expectedOut := []byte("{\"fieldString\":\"test_string\",\"fieldInt\":100}")
testFormatMessage(t, payload, expectedOut)
}
func TestReadCtlMessage(t *testing.T) {
mockHyper, h, err := connectMockHyperstart(t, false)
if err != nil {
t.Fatal()
}
defer disconnectHyperstart(h)
defer mockHyper.Stop()
expected := &DecodedMessage{
Code: ReadyCode,
Message: []byte{},
}
mockHyper.SendMessage(int(expected.Code), expected.Message)
reply, err := ReadCtlMessage(h.GetCtlSock())
if err != nil {
t.Fatal()
}
if reflect.DeepEqual(reply, expected) == false {
t.Fatal()
}
}
func TestWriteCtlMessage(t *testing.T) {
mockHyper, h, err := connectMockHyperstart(t, false)
if err != nil {
t.Fatal()
}
defer disconnectHyperstart(h)
defer mockHyper.Stop()
msg := DecodedMessage{
Code: PingCode,
Message: []byte{},
}
err = h.WriteCtlMessage(h.GetCtlSock(), &msg)
if err != nil {
t.Fatal()
}
for {
reply, err := ReadCtlMessage(h.GetCtlSock())
if err != nil {
t.Fatal()
}
if reply.Code == NextCode {
continue
}
err = h.CheckReturnedCode(reply, AckCode)
if err != nil {
t.Fatal()
}
break
}
msgs := mockHyper.GetLastMessages()
if msgs == nil {
t.Fatal()
}
if msgs[0].Code != msg.Code || string(msgs[0].Message) != string(msg.Message) {
t.Fatal()
}
}
func TestReadIoMessage(t *testing.T) {
mockHyper, h, err := connectMockHyperstart(t, true)
if err != nil {
t.Fatal()
}
defer disconnectHyperstart(h)
defer mockHyper.Stop()
mockHyper.SendIo(testSequence, []byte(testMessage))
msg, err := h.ReadIoMessage()
if err != nil {
t.Fatal()
}
if msg.Session != testSequence || string(msg.Message) != testMessage {
t.Fatal()
}
}
func TestReadIoMessageWithConn(t *testing.T) {
mockHyper, h, err := connectMockHyperstart(t, true)
if err != nil {
t.Fatal()
}
defer disconnectHyperstart(h)
defer mockHyper.Stop()
mockHyper.SendIo(testSequence, []byte(testMessage))
msg, err := ReadIoMessageWithConn(h.GetIoSock())
if err != nil {
t.Fatal()
}
if msg.Session != testSequence || string(msg.Message) != testMessage {
t.Fatal()
}
}
func TestSendIoMessage(t *testing.T) {
mockHyper, h, err := connectMockHyperstart(t, true)
if err != nil {
t.Fatal()
}
defer disconnectHyperstart(h)
defer mockHyper.Stop()
msg := &TtyMessage{
Session: testSequence,
Message: []byte(testMessage),
}
err = h.SendIoMessage(msg)
if err != nil {
t.Fatal()
}
buf := make([]byte, 512)
n, seqRecv := mockHyper.ReadIo(buf)
if seqRecv != testSequence || string(buf[TtyHdrSize:n]) != testMessage {
t.Fatal()
}
}
func TestSendIoMessageWithConn(t *testing.T) {
mockHyper, h, err := connectMockHyperstart(t, true)
if err != nil {
t.Fatal()
}
defer disconnectHyperstart(h)
defer mockHyper.Stop()
msg := &TtyMessage{
Session: testSequence,
Message: []byte(testMessage),
}
err = SendIoMessageWithConn(h.GetIoSock(), msg)
if err != nil {
t.Fatal()
}
buf := make([]byte, 512)
n, seqRecv := mockHyper.ReadIo(buf)
if seqRecv != testSequence || string(buf[TtyHdrSize:n]) != testMessage {
t.Fatal()
}
}
func testCodeFromCmd(t *testing.T, cmd string, expected uint32) {
h := &Hyperstart{}
code, err := h.CodeFromCmd(cmd)
if err != nil || code != expected {
t.Fatal()
}
}
func TestCodeFromCmdVersion(t *testing.T) {
testCodeFromCmd(t, Version, VersionCode)
}
func TestCodeFromCmdStartPod(t *testing.T) {
testCodeFromCmd(t, StartPod, StartPodCode)
}
func TestCodeFromCmdDestroyPod(t *testing.T) {
testCodeFromCmd(t, DestroyPod, DestroyPodCode)
}
func TestCodeFromCmdExecCmd(t *testing.T) {
testCodeFromCmd(t, ExecCmd, ExecCmdCode)
}
func TestCodeFromCmdReady(t *testing.T) {
testCodeFromCmd(t, Ready, ReadyCode)
}
func TestCodeFromCmdAck(t *testing.T) {
testCodeFromCmd(t, Ack, AckCode)
}
func TestCodeFromCmdError(t *testing.T) {
testCodeFromCmd(t, Error, ErrorCode)
}
func TestCodeFromCmdWinSize(t *testing.T) {
testCodeFromCmd(t, WinSize, WinsizeCode)
}
func TestCodeFromCmdPing(t *testing.T) {
testCodeFromCmd(t, Ping, PingCode)
}
func TestCodeFromCmdNext(t *testing.T) {
testCodeFromCmd(t, Next, NextCode)
}
func TestCodeFromCmdWriteFile(t *testing.T) {
testCodeFromCmd(t, WriteFile, WriteFileCode)
}
func TestCodeFromCmdReadFile(t *testing.T) {
testCodeFromCmd(t, ReadFile, ReadFileCode)
}
func TestCodeFromCmdNewContainer(t *testing.T) {
testCodeFromCmd(t, NewContainer, NewContainerCode)
}
func TestCodeFromCmdKillContainer(t *testing.T) {
testCodeFromCmd(t, KillContainer, KillContainerCode)
}
func TestCodeFromCmdOnlineCPUMem(t *testing.T) {
testCodeFromCmd(t, OnlineCPUMem, OnlineCPUMemCode)
}
func TestCodeFromCmdSetupInterface(t *testing.T) {
testCodeFromCmd(t, SetupInterface, SetupInterfaceCode)
}
func TestCodeFromCmdSetupRoute(t *testing.T) {
testCodeFromCmd(t, SetupRoute, SetupRouteCode)
}
func TestCodeFromCmdRemoveContainer(t *testing.T) {
testCodeFromCmd(t, RemoveContainer, RemoveContainerCode)
}
func TestCodeFromCmdUnknown(t *testing.T) {
h := &Hyperstart{}
code, err := h.CodeFromCmd("unknown")
if err == nil || code != math.MaxUint32 {
t.Fatal()
}
}
func testCheckReturnedCode(t *testing.T, recvMsg *DecodedMessage, refCode uint32) {
h := &Hyperstart{}
err := h.CheckReturnedCode(recvMsg, refCode)
if err != nil {
t.Fatal()
}
}
func TestCheckReturnedCodeList(t *testing.T) {
for _, code := range CodeList {
recvMsg := DecodedMessage{Code: code}
testCheckReturnedCode(t, &recvMsg, code)
}
}
func testCheckReturnedCodeFailure(t *testing.T, recvMsg *DecodedMessage, refCode uint32) {
h := &Hyperstart{}
err := h.CheckReturnedCode(recvMsg, refCode)
if err == nil {
t.Fatal()
}
}
func TestCheckReturnedCodeListWrong(t *testing.T) {
for _, code := range CodeList {
msg := DecodedMessage{Code: code}
if code != ReadyCode {
testCheckReturnedCodeFailure(t, &msg, ReadyCode)
} else {
testCheckReturnedCodeFailure(t, &msg, PingCode)
}
}
}
func TestWaitForReady(t *testing.T) {
mockHyper, h, err := connectMockHyperstart(t, true)
if err != nil {
t.Fatal()
}
defer disconnectHyperstart(h)
defer mockHyper.Stop()
mockHyper.SendMessage(int(ReadyCode), []byte{})
err = h.WaitForReady()
if err != nil {
t.Fatal()
}
}
func TestWaitForReadyError(t *testing.T) {
mockHyper, h, err := connectMockHyperstart(t, true)
if err != nil {
t.Fatal()
}
defer disconnectHyperstart(h)
defer mockHyper.Stop()
mockHyper.SendMessage(int(ErrorCode), []byte{})
err = h.WaitForReady()
if err == nil {
t.Fatal()
}
}
var cmdList = []string{
Version,
StartPod,
DestroyPod,
ExecCmd,
Ready,
Ack,
Error,
WinSize,
Ping,
Next,
NewContainer,
KillContainer,
OnlineCPUMem,
SetupInterface,
SetupRoute,
RemoveContainer,
}
func testSendCtlMessage(t *testing.T, cmd string) {
mockHyper, h, err := connectMockHyperstart(t, true)
if err != nil {
t.Fatal()
}
defer disconnectHyperstart(h)
defer mockHyper.Stop()
msg, err := h.SendCtlMessage(cmd, []byte{})
if err != nil {
t.Fatal()
}
if msg.Code != AckCode {
t.Fatal()
}
}
func TestSendCtlMessage(t *testing.T) {
for _, cmd := range cmdList {
testSendCtlMessage(t, cmd)
}
}

View File

@@ -0,0 +1,408 @@
// Copyright (c) 2016 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 mock
import (
"encoding/binary"
"encoding/hex"
"fmt"
"net"
"os"
"path/filepath"
"sync"
"testing"
hyper "github.com/containers/virtcontainers/pkg/hyperstart"
"github.com/stretchr/testify/assert"
)
// Control command string IDs
const (
Version = "version"
StartPod = "startpod"
DestroyPod = "destroypod"
ExecCmd = "execcmd"
Ready = "ready"
Ack = "ack"
Error = "error"
WinSize = "winsize"
Ping = "ping"
FinishPod = "finishpod"
Next = "next"
WriteFile = "writefile"
ReadFile = "readfile"
NewContainer = "newcontainer"
KillContainer = "killcontainer"
RemoveContainer = "removecontainer"
OnlineCPUMem = "onlinecpumem"
SetupInterface = "setupinterface"
SetupRoute = "setuproute"
)
var codeList = map[int]string{
hyper.VersionCode: Version,
hyper.StartPodCode: StartPod,
hyper.DestroyPodCode: DestroyPod,
hyper.ExecCmdCode: ExecCmd,
hyper.ReadyCode: Ready,
hyper.AckCode: Ack,
hyper.ErrorCode: Error,
hyper.WinsizeCode: WinSize,
hyper.PingCode: Ping,
hyper.NextCode: Next,
hyper.WriteFileCode: WriteFile,
hyper.ReadFileCode: ReadFile,
hyper.NewContainerCode: NewContainer,
hyper.KillContainerCode: KillContainer,
hyper.OnlineCPUMemCode: OnlineCPUMem,
hyper.SetupInterfaceCode: SetupInterface,
hyper.SetupRouteCode: SetupRoute,
hyper.RemoveContainerCode: RemoveContainer,
}
// Hyperstart is an object mocking the hyperstart agent.
type Hyperstart struct {
t *testing.T
ctlSocketPath, ioSocketPath string
ctlListener, ioListener *net.UnixListener
ctl, io net.Conn
// Start() will launch two goroutines to accept connections on the ctl
// and io sockets. Those goroutine will exit once the first connection
// is accepted or when the listening socket is closed. wgConnected can
// be used to make sure we've accepted connections to both sockets
wgConnected sync.WaitGroup
// We then have two other goroutines to handle communication on those
// sockets.
wg sync.WaitGroup
// Keep the list of messages received by hyperstart, older first, for
// later inspection with GetLastMessages()
lastMessages []hyper.DecodedMessage
}
func newMessageList() []hyper.DecodedMessage {
return make([]hyper.DecodedMessage, 0, 10)
}
// NewHyperstart creates a new hyperstart instance.
func NewHyperstart(t *testing.T) *Hyperstart {
dir := os.TempDir()
ctlSocketPath := filepath.Join(dir, "mock.hyper."+nextSuffix()+".0.sock")
ioSocketPath := filepath.Join(dir, "mock.hyper."+nextSuffix()+".1.sock")
return &Hyperstart{
t: t,
ctlSocketPath: ctlSocketPath,
ioSocketPath: ioSocketPath,
lastMessages: newMessageList(),
}
}
// GetSocketPaths returns the ctl and io socket paths, respectively
func (h *Hyperstart) GetSocketPaths() (string, string) {
return h.ctlSocketPath, h.ioSocketPath
}
// GetLastMessages returns list of messages received by hyperstart, older
// first. This function only returns the messages:
// - since Start on the first invocation
// - since the last GetLastMessages for subsequent invocations
func (h *Hyperstart) GetLastMessages() []hyper.DecodedMessage {
msgs := h.lastMessages
h.lastMessages = newMessageList()
return msgs
}
func (h *Hyperstart) log(s string) {
h.logf("%s\n", s)
}
func (h *Hyperstart) logf(format string, args ...interface{}) {
h.t.Logf("[hyperstart] "+format, args...)
}
func (h *Hyperstart) logData(data []byte) {
h.t.Log(hex.Dump(data))
}
//
// ctl channel
//
const ctlHeaderSize = 8
func (h *Hyperstart) writeCtl(data []byte) error {
h.wgConnected.Wait()
n, err := h.ctl.Write(data)
if err != nil {
return fmt.Errorf("Connection broken, cannot send data")
}
assert.Equal(h.t, n, len(data))
return nil
}
// SendMessage makes hyperstart send the hyper command cmd along with optional
// data on the control channel
func (h *Hyperstart) SendMessage(cmd int, data []byte) {
length := ctlHeaderSize + len(data)
header := make([]byte, ctlHeaderSize)
binary.BigEndian.PutUint32(header[:], uint32(cmd))
binary.BigEndian.PutUint32(header[4:], uint32(length))
err := h.writeCtl(header)
if err != nil {
return
}
if len(data) == 0 {
return
}
h.writeCtl(data)
}
func (h *Hyperstart) readCtl(data []byte) error {
h.wgConnected.Wait()
n, err := h.ctl.Read(data)
if err != nil {
return err
}
assert.Equal(h.t, n, len(data))
return nil
}
func (h *Hyperstart) ackData(nBytes int) {
data := make([]byte, 4)
binary.BigEndian.PutUint32(data[:], uint32(nBytes))
h.SendMessage(hyper.NextCode, data)
}
func (h *Hyperstart) readMessage() (int, []byte, error) {
buf := make([]byte, ctlHeaderSize)
if err := h.readCtl(buf); err != nil {
return -1, buf, err
}
h.ackData(len(buf))
cmd := int(binary.BigEndian.Uint32(buf[:4]))
length := int(binary.BigEndian.Uint32(buf[4:8]))
assert.True(h.t, length >= 8)
length -= 8
if length == 0 {
return cmd, nil, nil
}
data := make([]byte, length)
if err := h.readCtl(data); err != nil {
return -1, buf, err
}
h.ackData(len(data))
return cmd, data, nil
}
func cmdToString(cmd int) (string, error) {
_, ok := codeList[cmd]
if ok == false {
return "", fmt.Errorf("unknown command '%d'", cmd)
}
return codeList[cmd], nil
}
func (h *Hyperstart) handleCtl() {
for {
cmd, data, err := h.readMessage()
if err != nil {
break
}
cmdName, err := cmdToString(cmd)
assert.Nil(h.t, err)
h.logf("ctl: --> command %s, payload_len=%d\n", cmdName, len(data))
if len(data) != 0 {
h.logData(data)
}
h.lastMessages = append(h.lastMessages, hyper.DecodedMessage{
Code: uint32(cmd),
Message: data,
})
// answer back with the message exit status
// XXX: may be interesting to be able to configure the mock
// hyperstart to fail and test the reaction of proxy/clients
h.logf("ctl: <-- command %s executed successfully\n", cmdName)
h.SendMessage(hyper.AckCode, nil)
}
h.wg.Done()
}
//
// io channel
//
const ioHeaderSize = 12
func (h *Hyperstart) writeIo(data []byte) {
h.wgConnected.Wait()
n, err := h.io.Write(data)
assert.Nil(h.t, err)
assert.Equal(h.t, n, len(data))
}
// SendIo sends a packet of I/O data to a client connected the I/O channel.
// Multiple I/O streams are multiplexed on that channel. seq specifies which
// steam the data belongs to.
func (h *Hyperstart) SendIo(seq uint64, data []byte) {
length := ioHeaderSize + len(data)
header := make([]byte, ioHeaderSize)
h.logf("io: <-- writing %d bytes for seq %d\n", len(data), seq)
binary.BigEndian.PutUint64(header[:], uint64(seq))
binary.BigEndian.PutUint32(header[8:], uint32(length))
h.writeIo(header)
if len(data) == 0 {
return
}
h.writeIo(data)
}
// SendIoString sends a string a client connected the I/O channel.
// Multiple I/O streams are multiplexed on that channel. seq specifies which
// steam the data belongs to.
func (h *Hyperstart) SendIoString(seq uint64, data string) {
h.SendIo(seq, []byte(data))
}
// CloseIo closes the I/O stream specified by seq.
func (h *Hyperstart) CloseIo(seq uint64) {
h.SendIo(seq, nil)
}
// SendExitStatus sends the exit status on the I/O streams specified by seq.
// The exit status should only be sent after the stream has been closed with
// CloseIo.
func (h *Hyperstart) SendExitStatus(seq uint64, exitStatus uint8) {
status := []byte{exitStatus}
h.SendIo(seq, status)
}
// ReadIo reads data that has been sent on the I/O channel by a client. It
// returns the full packet (header & data) as well as the seq number decoded
// from the header.
func (h *Hyperstart) ReadIo(buf []byte) (n int, seq uint64) {
h.wgConnected.Wait()
n, err := h.io.Read(buf)
assert.Nil(h.t, err)
seq = binary.BigEndian.Uint64(buf[:8])
return
}
type acceptCb func(c net.Conn)
func (h *Hyperstart) startListening(path string, cb acceptCb) *net.UnixListener {
addr := &net.UnixAddr{Name: path, Net: "unix"}
l, err := net.ListenUnix("unix", addr)
assert.Nil(h.t, err)
go func() {
h.logf("%s: waiting for connection\n", path)
c, err := l.Accept()
if err != nil {
cb(nil)
return
}
cb(c)
h.logf("%s: accepted connection\n", path)
}()
return l
}
// Start will
// Once finished with the Hyperstart object, Close must be called.
func (h *Hyperstart) Start() {
h.log("start")
h.wgConnected.Add(1)
h.wgConnected.Add(1)
h.ctlListener = h.startListening(h.ctlSocketPath, func(s net.Conn) {
// a client is now connected to the ctl socket
h.ctl = s
// Close() was called before we had a chance to accept a
// connection.
if s == nil {
h.wgConnected.Done()
return
}
// start the goroutine that will handle the ctl socket
h.wg.Add(1)
go h.handleCtl()
// we need signal wgConnected late, so wg.Add(1) is done before
// the wg.Wait() in Close()
// See https://golang.org/pkg/sync/#WaitGroup.Wait:
// "Note that calls with a positive delta that occur when the
// counter is zero must happen before a Wait"
h.wgConnected.Done()
})
h.ioListener = h.startListening(h.ioSocketPath, func(s net.Conn) {
// a client is now connected to the ctl socket
h.io = s
h.wgConnected.Done()
})
}
// Stop closes all internal resources and waits for goroutines started by Start
// to finish. Stop shouldn't be called if Start hasn't been called.
func (h *Hyperstart) Stop() {
h.wgConnected.Wait()
h.ctl.Close()
h.io.Close()
h.ctlListener.Close()
h.ioListener.Close()
h.wg.Wait()
os.Remove(h.ctlSocketPath)
os.Remove(h.ioSocketPath)
h.log("stopped")
}

View File

@@ -0,0 +1,37 @@
// Copyright (c) 2016 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 mock
import (
"fmt"
"os"
"path/filepath"
)
// GetTmpPath will return a filename suitable for a tempory file according to
// the format string given in argument. The format string must contain a single
// %s which will be replaced by a random string. Eg.:
//
// GetTmpPath("test.foo.%s.sock")
//
// will return something like:
//
// "/tmp/test.foo.832222621.sock"
func GetTmpPath(format string) string {
filename := fmt.Sprintf(format, nextSuffix())
dir := os.TempDir()
return filepath.Join(dir, filename)
}

View File

@@ -0,0 +1,42 @@
// Copyright (c) 2016 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 mock
import (
"os"
"strconv"
"sync"
"time"
)
// Taken from https://golang.org/src/io/ioutil/tempfile.go?s=#L19
var rand uint32
var randmu sync.Mutex
func reseed() uint32 {
return uint32(time.Now().UnixNano() + int64(os.Getpid()))
}
func nextSuffix() string {
randmu.Lock()
r := rand
if r == 0 {
r = reseed()
}
r = r*1664525 + 1013904223 // constants from Numerical Recipes
rand = r
randmu.Unlock()
return strconv.Itoa(int(1e9 + r%1e9))[1:]
}

View File

@@ -0,0 +1,175 @@
//
// 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 hyperstart
import (
"encoding/json"
"fmt"
"net"
"sync"
)
type ctlDataType string
const (
eventType ctlDataType = "ctlEvent"
replyType ctlDataType = "ctlReply"
)
type multicast struct {
bufReplies []*DecodedMessage
reply []chan *DecodedMessage
event map[string]chan *DecodedMessage
ctl net.Conn
sync.Mutex
}
func newMulticast(ctlConn net.Conn) *multicast {
return &multicast{
bufReplies: []*DecodedMessage{},
reply: []chan *DecodedMessage{},
event: make(map[string]chan *DecodedMessage),
ctl: ctlConn,
}
}
func startCtlMonitor(ctlConn net.Conn, done chan<- interface{}) *multicast {
ctlMulticast := newMulticast(ctlConn)
go func() {
for {
msg, err := ReadCtlMessage(ctlMulticast.ctl)
if err != nil {
hyperLog.Infof("Read on CTL channel ended: %s", err)
break
}
err = ctlMulticast.write(msg)
if err != nil {
hyperLog.Errorf("Multicaster write error: %s", err)
break
}
}
close(done)
}()
return ctlMulticast
}
func (m *multicast) buildEventID(containerID, processID string) string {
return fmt.Sprintf("%s-%s", containerID, processID)
}
func (m *multicast) sendEvent(msg *DecodedMessage) error {
var paeData PAECommand
err := json.Unmarshal(msg.Message, paeData)
if err != nil {
return err
}
uniqueID := m.buildEventID(paeData.Container, paeData.Process)
channel, exist := m.event[uniqueID]
if !exist {
return nil
}
channel <- msg
delete(m.event, uniqueID)
return nil
}
func (m *multicast) sendReply(msg *DecodedMessage) error {
m.Lock()
if len(m.reply) == 0 {
m.bufReplies = append(m.bufReplies, msg)
m.Unlock()
return nil
}
replyChannel := m.reply[0]
m.reply = m.reply[1:]
m.Unlock()
// The current reply channel has been removed from the list, that's why
// we can be out of the mutex to send through that channel. Indeed, there
// is no risk that someone else tries to write on this channel.
replyChannel <- msg
return nil
}
func (m *multicast) processBufferedReply(channel chan *DecodedMessage) {
m.Lock()
if len(m.bufReplies) == 0 {
m.reply = append(m.reply, channel)
m.Unlock()
return
}
msg := m.bufReplies[0]
m.bufReplies = m.bufReplies[1:]
m.Unlock()
// The current buffered reply message has been removed from the list, and
// the channel have not been added to the reply list, that's why we can be
// out of the mutex to send the buffered message through that channel.
// There is no risk that someone else tries to write this message on another
// channel, or another message on this channel.
channel <- msg
}
func (m *multicast) write(msg *DecodedMessage) error {
switch msg.Code {
case NextCode:
return nil
case ProcessAsyncEventCode:
return m.sendEvent(msg)
default:
return m.sendReply(msg)
}
}
func (m *multicast) listen(containerID, processID string, dataType ctlDataType) (chan *DecodedMessage, error) {
switch dataType {
case replyType:
newChan := make(chan *DecodedMessage)
go m.processBufferedReply(newChan)
return newChan, nil
case eventType:
uniqueID := m.buildEventID(containerID, processID)
_, exist := m.event[uniqueID]
if exist {
return nil, fmt.Errorf("Channel already assigned for ID %s", uniqueID)
}
m.event[uniqueID] = make(chan *DecodedMessage)
return m.event[uniqueID], nil
default:
return nil, fmt.Errorf("Unknown data type: %s", dataType)
}
}

View File

@@ -0,0 +1,265 @@
//
// 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 hyperstart
import (
"syscall"
)
// Defines all available commands to communicate with hyperstart agent.
const (
VersionCode = iota
StartPodCode
GetPodDeprecatedCode
StopPodDeprecatedCode
DestroyPodCode
RestartContainerDeprecatedCode
ExecCmdCode
FinishCmdDeprecatedCode
ReadyCode
AckCode
ErrorCode
WinsizeCode
PingCode
FinishPodDeprecatedCode
NextCode
WriteFileCode
ReadFileCode
NewContainerCode
KillContainerCode
OnlineCPUMemCode
SetupInterfaceCode
SetupRouteCode
RemoveContainerCode
PsContainerCode
ProcessAsyncEventCode
)
// FileCommand is the structure corresponding to the format expected by
// hyperstart to interact with files.
type FileCommand struct {
Container string `json:"container"`
File string `json:"file"`
}
// KillCommand is the structure corresponding to the format expected by
// hyperstart to kill a container on the guest.
type KillCommand struct {
Container string `json:"container"`
Signal syscall.Signal `json:"signal"`
AllProcesses bool `json:"allProcesses"`
}
// ExecCommand is the structure corresponding to the format expected by
// hyperstart to execute a command on the guest.
type ExecCommand struct {
Container string `json:"container,omitempty"`
Process Process `json:"process"`
}
// RemoveCommand is the structure corresponding to the format expected by
// hyperstart to remove a container on the guest.
type RemoveCommand struct {
Container string `json:"container"`
}
// PsCommand is the structure corresponding to the format expected by
// hyperstart to list processes of a container on the guest.
type PsCommand struct {
Container string `json:"container"`
Format string `json:"format"`
PsArgs []string `json:"psargs"`
}
// PAECommand is the structure hyperstart can expects to
// receive after a process has been started/executed on a container.
type PAECommand struct {
Container string `json:"container"`
Process string `json:"process"`
Event string `json:"event"`
Info string `json:"info,omitempty"`
Status int `json:"status,omitempty"`
}
// DecodedMessage is the structure holding messages coming from CTL channel.
type DecodedMessage struct {
Code uint32
Message []byte
}
// TtyMessage is the structure holding messages coming from TTY channel.
type TtyMessage struct {
Session uint64
Message []byte
}
// WindowSizeMessage is the structure corresponding to the format expected by
// hyperstart to resize a container's window.
type WindowSizeMessage struct {
Container string `json:"container"`
Process string `json:"process"`
Row uint16 `json:"row"`
Column uint16 `json:"column"`
}
// VolumeDescriptor describes a volume related to a container.
type VolumeDescriptor struct {
Device string `json:"device"`
Addr string `json:"addr,omitempty"`
Mount string `json:"mount"`
Fstype string `json:"fstype,omitempty"`
ReadOnly bool `json:"readOnly"`
DockerVolume bool `json:"dockerVolume"`
}
// FsmapDescriptor describes a filesystem map related to a container.
type FsmapDescriptor struct {
Source string `json:"source"`
Path string `json:"path"`
ReadOnly bool `json:"readOnly"`
DockerVolume bool `json:"dockerVolume"`
AbsolutePath bool `json:"absolutePath"`
SCSIAddr string `json:"scsiAddr"`
}
// EnvironmentVar holds an environment variable and its value.
type EnvironmentVar struct {
Env string `json:"env"`
Value string `json:"value"`
}
// Rlimit describes a resource limit.
type Rlimit struct {
// Type of the rlimit to set
Type string `json:"type"`
// Hard is the hard limit for the specified type
Hard uint64 `json:"hard"`
// Soft is the soft limit for the specified type
Soft uint64 `json:"soft"`
}
// Capabilities specify the capabilities to keep when executing the process inside the container.
type Capabilities struct {
// Bounding is the set of capabilities checked by the kernel.
Bounding []string `json:"bounding"`
// Effective is the set of capabilities checked by the kernel.
Effective []string `json:"effective"`
// Inheritable is the capabilities preserved across execve.
Inheritable []string `json:"inheritable"`
// Permitted is the limiting superset for effective capabilities.
Permitted []string `json:"permitted"`
// Ambient is the ambient set of capabilities that are kept.
Ambient []string `json:"ambient"`
}
// Process describes a process running on a container inside a pod.
type Process struct {
User string `json:"user,omitempty"`
Group string `json:"group,omitempty"`
AdditionalGroups []string `json:"additionalGroups,omitempty"`
// Terminal creates an interactive terminal for the process.
Terminal bool `json:"terminal"`
// Sequeue number for stdin and stdout
Stdio uint64 `json:"stdio,omitempty"`
// Sequeue number for stderr if it is not shared with stdout
Stderr uint64 `json:"stderr,omitempty"`
// Args specifies the binary and arguments for the application to execute.
Args []string `json:"args"`
// Envs populates the process environment for the process.
Envs []EnvironmentVar `json:"envs,omitempty"`
// Workdir is the current working directory for the process and must be
// relative to the container's root.
Workdir string `json:"workdir"`
// Rlimits specifies rlimit options to apply to the process.
Rlimits []Rlimit `json:"rlimits,omitempty"`
// NoNewPrivileges indicates that the process should not gain any additional privileges
NoNewPrivileges bool `json:"noNewPrivileges"`
// Capabilities specifies the sets of capabilities for the process(es) inside the container.
Capabilities Capabilities `json:"capabilities"`
}
// SystemMountsInfo describes additional information for system mounts that the agent
// needs to handle
type SystemMountsInfo struct {
// Indicates if /dev has been passed as a bind mount for the host /dev
BindMountDev bool `json:"bindMountDev"`
// Size of /dev/shm assigned on the host.
DevShmSize int `json:"devShmSize"`
}
// Constraints describes the constrains for a container
type Constraints struct {
// CPUQuota specifies the total amount of time in microseconds
// The number of microseconds per CPUPeriod that the container is guaranteed CPU access
CPUQuota int64
// CPUPeriod specifies the CPU CFS scheduler period of time in microseconds
CPUPeriod uint64
// CPUShares specifies container's weight vs. other containers
CPUShares uint64
}
// Container describes a container running on a pod.
type Container struct {
ID string `json:"id"`
Rootfs string `json:"rootfs"`
Fstype string `json:"fstype,omitempty"`
Image string `json:"image"`
SCSIAddr string `json:"scsiAddr,omitempty"`
Volumes []*VolumeDescriptor `json:"volumes,omitempty"`
Fsmap []*FsmapDescriptor `json:"fsmap,omitempty"`
Sysctl map[string]string `json:"sysctl,omitempty"`
Process *Process `json:"process"`
RestartPolicy string `json:"restartPolicy"`
Initialize bool `json:"initialize"`
SystemMountsInfo SystemMountsInfo `json:"systemMountsInfo"`
Constraints Constraints `json:"constraints"`
}
// IPAddress describes an IP address and its network mask.
type IPAddress struct {
IPAddress string `json:"ipAddress"`
NetMask string `json:"netMask"`
}
// NetworkIface describes a network interface to setup on the host.
type NetworkIface struct {
Device string `json:"device,omitempty"`
NewDevice string `json:"newDeviceName,omitempty"`
IPAddresses []IPAddress `json:"ipAddresses"`
MTU int `json:"mtu"`
MACAddr string `json:"macAddr"`
}
// Route describes a route to setup on the host.
type Route struct {
Dest string `json:"dest"`
Gateway string `json:"gateway,omitempty"`
Device string `json:"device,omitempty"`
}
// Pod describes the pod configuration to start inside the VM.
type Pod struct {
Hostname string `json:"hostname"`
Containers []Container `json:"containers,omitempty"`
Interfaces []NetworkIface `json:"interfaces,omitempty"`
DNS []string `json:"dns,omitempty"`
Routes []Route `json:"routes,omitempty"`
ShareDir string `json:"shareDir"`
}

View File

@@ -0,0 +1,482 @@
// 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 mock
import (
"encoding/json"
"errors"
"fmt"
"net"
"os"
"sync"
"testing"
"github.com/clearcontainers/proxy/api"
"github.com/stretchr/testify/assert"
)
const testContainerid = "123456789"
const testToken = "pF56IaDpuax6hihJ5PneB8JypqmOvjkqY-wKGVYqgIM="
// CCProxyMock is an object mocking clearcontainers Proxy
type CCProxyMock struct {
t *testing.T
wg sync.WaitGroup
connectionPath string
// proxy socket
listener net.Listener
// single client to serve
cl net.Conn
//token to be used for the connection
token string
lastStdinStream []byte
ShimConnected chan bool
Signal chan ShimSignal
ShimDisconnected chan bool
StdinReceived chan bool
}
// NewCCProxyMock creates a hyperstart instance
func NewCCProxyMock(t *testing.T, path string) *CCProxyMock {
return &CCProxyMock{
t: t,
connectionPath: path,
lastStdinStream: nil,
Signal: make(chan ShimSignal, 5),
ShimConnected: make(chan bool),
ShimDisconnected: make(chan bool),
StdinReceived: make(chan bool),
token: testToken,
}
}
// GetProxyToken returns the token that mock proxy uses
// to verify its client connection
func (proxy *CCProxyMock) GetProxyToken() string {
return proxy.token
}
func newSignalList() []ShimSignal {
return make([]ShimSignal, 0, 5)
}
// GetLastStdinStream returns the last received stdin stream
func (proxy *CCProxyMock) GetLastStdinStream() []byte {
return proxy.lastStdinStream
}
func (proxy *CCProxyMock) log(s string) {
proxy.logF("%s\n", s)
}
func (proxy *CCProxyMock) logF(format string, args ...interface{}) {
proxy.t.Logf("[CCProxyMock] "+format, args...)
}
type client struct {
proxy *CCProxyMock
conn net.Conn
}
// ConnectShim payload defined here, as it has not been defined
// in proxy api package yet
type ConnectShim struct {
Token string `json:"token"`
}
// ShimSignal is the struct used to represent the signal received from the shim
type ShimSignal struct {
Signal int `json:"signalNumber"`
Row int `json:"rows,omitempty"`
Column int `json:"columns,omitempty"`
}
func connectShimHandler(data []byte, userData interface{}, response *handlerResponse) {
client := userData.(*client)
proxy := client.proxy
payload := ConnectShim{}
err := json.Unmarshal(data, &payload)
assert.Nil(proxy.t, err)
if payload.Token != proxy.token {
response.SetErrorMsg("Invalid Token")
}
proxy.logF("ConnectShim(token=%s)", payload.Token)
response.AddResult("version", api.Version)
proxy.ShimConnected <- true
}
func signalShimHandler(data []byte, userData interface{}, response *handlerResponse) {
client := userData.(*client)
proxy := client.proxy
signalPayload := ShimSignal{}
err := json.Unmarshal(data, &signalPayload)
assert.Nil(proxy.t, err)
proxy.logF("CCProxyMock received signal: %v", signalPayload)
proxy.Signal <- signalPayload
}
func disconnectShimHandler(data []byte, userData interface{}, response *handlerResponse) {
client := userData.(*client)
proxy := client.proxy
proxy.log("Client sent DisconnectShim Command")
proxy.ShimDisconnected <- true
}
func stdinShimHandler(data []byte, userData interface{}, response *handlerResponse) {
client := userData.(*client)
proxy := client.proxy
proxy.lastStdinStream = data
proxy.StdinReceived <- true
}
func registerVMHandler(data []byte, userData interface{}, response *handlerResponse) {
client := userData.(*client)
proxy := client.proxy
proxy.log("Register VM")
payload := api.RegisterVM{}
if err := json.Unmarshal(data, &payload); err != nil {
response.SetError(err)
return
}
// Generate fake tokens
var tokens []string
for i := 0; i < payload.NumIOStreams; i++ {
tokens = append(tokens, fmt.Sprintf("%d", i))
}
io := &api.IOResponse{
Tokens: tokens,
}
response.AddResult("io", io)
}
func unregisterVMHandler(data []byte, userData interface{}, response *handlerResponse) {
client := userData.(*client)
proxy := client.proxy
proxy.log("Unregister VM")
}
func attachVMHandler(data []byte, userData interface{}, response *handlerResponse) {
client := userData.(*client)
proxy := client.proxy
proxy.log("Attach VM")
payload := api.AttachVM{}
if err := json.Unmarshal(data, &payload); err != nil {
response.SetError(err)
return
}
// Generate fake tokens
var tokens []string
for i := 0; i < payload.NumIOStreams; i++ {
tokens = append(tokens, fmt.Sprintf("%d", i))
}
io := &api.IOResponse{
Tokens: tokens,
}
response.AddResult("io", io)
}
func hyperCmdHandler(data []byte, userData interface{}, response *handlerResponse) {
client := userData.(*client)
proxy := client.proxy
proxy.log("Hyper command")
response.SetData([]byte{})
}
// SendStdoutStream sends a Stdout Stream Frame to connected client
func (proxy *CCProxyMock) SendStdoutStream(payload []byte) {
err := api.WriteStream(proxy.cl, api.StreamStdout, payload)
assert.Nil(proxy.t, err)
}
// SendStderrStream sends a Stderr Stream Frame to connected client
func (proxy *CCProxyMock) SendStderrStream(payload []byte) {
err := api.WriteStream(proxy.cl, api.StreamStderr, payload)
assert.Nil(proxy.t, err)
}
// SendExitNotification sends an Exit Notification Frame to connected client
func (proxy *CCProxyMock) SendExitNotification(payload []byte) {
err := api.WriteNotification(proxy.cl, api.NotificationProcessExited, payload)
assert.Nil(proxy.t, err)
}
func (proxy *CCProxyMock) startListening() {
l, err := net.ListenUnix("unix", &net.UnixAddr{Name: proxy.connectionPath, Net: "unix"})
assert.Nil(proxy.t, err)
proxy.logF("listening on %s", proxy.connectionPath)
proxy.listener = l
}
func (proxy *CCProxyMock) serveClient(proto *ccProxyProtocol, newConn net.Conn) {
newClient := &client{
proxy: proxy,
conn: newConn,
}
err := proto.Serve(newConn, newClient)
proxy.logF("Error serving client : %v\n", err)
newConn.Close()
proxy.log("Client closed connection")
proxy.wg.Done()
}
func (proxy *CCProxyMock) serve() {
proto := newCCProxyProtocol()
// shim handlers
proto.Handle(FrameKey{api.TypeCommand, int(api.CmdConnectShim)}, connectShimHandler)
proto.Handle(FrameKey{api.TypeCommand, int(api.CmdDisconnectShim)}, disconnectShimHandler)
proto.Handle(FrameKey{api.TypeStream, int(api.StreamStdin)}, stdinShimHandler)
// runtime handlers
proto.Handle(FrameKey{api.TypeCommand, int(api.CmdRegisterVM)}, registerVMHandler)
proto.Handle(FrameKey{api.TypeCommand, int(api.CmdUnregisterVM)}, unregisterVMHandler)
proto.Handle(FrameKey{api.TypeCommand, int(api.CmdAttachVM)}, attachVMHandler)
proto.Handle(FrameKey{api.TypeCommand, int(api.CmdHyper)}, hyperCmdHandler)
// Shared handler between shim and runtime
proto.Handle(FrameKey{api.TypeCommand, int(api.CmdSignal)}, signalShimHandler)
//Wait for a single client connection
conn, err := proxy.listener.Accept()
if err != nil {
// Ending up into this case when the listener is closed, which
// is still a valid case. We don't want to throw an error in
// this case.
return
}
assert.NotNil(proxy.t, conn)
proxy.log("Client connected")
proxy.wg.Add(1)
proxy.cl = conn
proxy.serveClient(proto, conn)
}
// Start invokes mock proxy instance to start listening.
func (proxy *CCProxyMock) Start() {
proxy.startListening()
go func() {
for {
proxy.serve()
}
}()
}
// Stop causes mock proxy instance to stop listening,
// close connection to client and close all channels
func (proxy *CCProxyMock) Stop() {
proxy.listener.Close()
if proxy.cl != nil {
proxy.log("Closing client connection")
proxy.cl.Close()
proxy.cl = nil
} else {
proxy.log("Client connection already closed")
}
proxy.wg.Wait()
close(proxy.ShimConnected)
close(proxy.Signal)
close(proxy.ShimDisconnected)
close(proxy.StdinReceived)
os.Remove(proxy.connectionPath)
proxy.log("Stopped")
}
// XXX: could do with its own package to remove that ugly namespacing
type ccProxyProtocolHandler func([]byte, interface{}, *handlerResponse)
// Encapsulates the different parts of what a handler can return.
type handlerResponse struct {
err error
results map[string]interface{}
data []byte
}
// SetError indicates sets error for the response.
func (r *handlerResponse) SetError(err error) {
r.err = err
}
// SetErrorMsg sets an error with the passed string for the response.
func (r *handlerResponse) SetErrorMsg(msg string) {
r.err = errors.New(msg)
}
// SetErrorf sets an error with the formatted string for the response.
func (r *handlerResponse) SetErrorf(format string, a ...interface{}) {
r.SetError(fmt.Errorf(format, a...))
}
// AddResult adds the given key/val to the response.
func (r *handlerResponse) AddResult(key string, value interface{}) {
if r.results == nil {
r.results = make(map[string]interface{})
}
r.results[key] = value
}
func (r *handlerResponse) SetData(data []byte) {
r.data = data
}
// FrameKey is a struct composed of the the frame type and opcode,
// used as a key for retrieving the handler for handling the frame.
type FrameKey struct {
ftype api.FrameType
opcode int
}
func newFrameKey(frameType api.FrameType, opcode int) FrameKey {
return FrameKey{
ftype: frameType,
opcode: opcode,
}
}
type ccProxyProtocol struct {
cmdHandlers map[FrameKey]ccProxyProtocolHandler
}
func newCCProxyProtocol() *ccProxyProtocol {
return &ccProxyProtocol{
cmdHandlers: make(map[FrameKey]ccProxyProtocolHandler),
}
}
// Handle retreives the handler for handling the frame
func (proto *ccProxyProtocol) Handle(key FrameKey, handler ccProxyProtocolHandler) bool {
if _, ok := proto.cmdHandlers[key]; ok {
return false
}
proto.cmdHandlers[key] = handler
return true
}
type clientCtx struct {
conn net.Conn
userData interface{}
}
func newErrorResponse(opcode int, errMsg string) *api.Frame {
frame, err := api.NewFrameJSON(api.TypeResponse, opcode, &api.ErrorResponse{
Message: errMsg,
})
if err != nil {
frame, err = api.NewFrameJSON(api.TypeResponse, opcode, &api.ErrorResponse{
Message: fmt.Sprintf("couldn't marshal response: %v", err),
})
if err != nil {
frame = api.NewFrame(api.TypeResponse, opcode, nil)
}
}
frame.Header.InError = true
return frame
}
func (proto *ccProxyProtocol) handleCommand(ctx *clientCtx, cmd *api.Frame) *api.Frame {
hr := handlerResponse{}
// cmd.Header.Opcode is guaranteed to be within the right bounds by
// ReadFrame().
handler := proto.cmdHandlers[FrameKey{cmd.Header.Type, int(cmd.Header.Opcode)}]
handler(cmd.Payload, ctx.userData, &hr)
if hr.err != nil {
return newErrorResponse(cmd.Header.Opcode, hr.err.Error())
}
var payload interface{}
if len(hr.results) > 0 {
payload = hr.results
}
frame, err := api.NewFrameJSON(api.TypeResponse, cmd.Header.Opcode, payload)
if err != nil {
return newErrorResponse(cmd.Header.Opcode, err.Error())
}
return frame
}
// Serve serves the client connection in a continuous loop.
func (proto *ccProxyProtocol) Serve(conn net.Conn, userData interface{}) error {
ctx := &clientCtx{
conn: conn,
userData: userData,
}
for {
frame, err := api.ReadFrame(conn)
if err != nil {
// EOF or the client isn't even sending proper JSON,
// just kill the connection
return err
}
if frame.Header.Type != api.TypeCommand && frame.Header.Type != api.TypeStream {
// EOF or the client isn't even sending proper JSON,
// just kill the connection
return fmt.Errorf("serve: expected a command got a %v", frame.Header.Type)
}
// Execute the corresponding handler
resp := proto.handleCommand(ctx, frame)
// Send the response back to the client.
if err = api.WriteFrame(conn, resp); err != nil {
// Something made us unable to write the response back
// to the client (could be a disconnection, ...).
return err
}
}
}

View File

@@ -0,0 +1,228 @@
//
// 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 mock
import (
"flag"
"fmt"
"io/ioutil"
"net"
"net/url"
"os"
"path/filepath"
"google.golang.org/grpc"
)
// DefaultMockCCShimBinPath is populated at link time.
var DefaultMockCCShimBinPath string
// DefaultMockKataShimBinPath is populated at link time.
var DefaultMockKataShimBinPath string
// DefaultMockHookBinPath is populated at link time.
var DefaultMockHookBinPath string
// ShimStdoutOutput is the expected output sent by the mock shim on stdout.
const ShimStdoutOutput = "Some output on stdout"
// ShimStderrOutput is the expected output sent by the mock shim on stderr.
const ShimStderrOutput = "Some output on stderr"
// ShimMockConfig is the configuration structure for all virtcontainers shim mock implementations.
type ShimMockConfig struct {
Name string
URLParamName string
ContainerParamName string
TokenParamName string
}
// StartShim is a common routine for starting a shim mock.
func StartShim(config ShimMockConfig) error {
logDirPath, err := ioutil.TempDir("", config.Name+"-")
if err != nil {
return err
}
logFilePath := filepath.Join(logDirPath, "mock_"+config.Name+".log")
f, err := os.Create(logFilePath)
if err != nil {
return err
}
defer f.Close()
tokenFlag := flag.String(config.TokenParamName, "", "Container token")
urlFlag := flag.String(config.URLParamName, "", "Agent URL")
containerFlag := flag.String(config.ContainerParamName, "", "Container ID")
flag.Parse()
fmt.Fprintf(f, "INFO: Token = %s\n", *tokenFlag)
fmt.Fprintf(f, "INFO: URL = %s\n", *urlFlag)
fmt.Fprintf(f, "INFO: Container = %s\n", *containerFlag)
if *tokenFlag == "" {
err := fmt.Errorf("token should not be empty")
fmt.Fprintf(f, "%s\n", err)
return err
}
if *urlFlag == "" {
err := fmt.Errorf("url should not be empty")
fmt.Fprintf(f, "%s\n", err)
return err
}
if _, err := url.Parse(*urlFlag); err != nil {
err2 := fmt.Errorf("could not parse the URL %q: %s", *urlFlag, err)
fmt.Fprintf(f, "%s\n", err2)
return err2
}
if *containerFlag == "" {
err := fmt.Errorf("container should not be empty")
fmt.Fprintf(f, "%s\n", err)
return err
}
// Print some traces to stdout
fmt.Fprintf(os.Stdout, ShimStdoutOutput)
os.Stdout.Close()
// Print some traces to stderr
fmt.Fprintf(os.Stderr, ShimStderrOutput)
os.Stderr.Close()
fmt.Fprintf(f, "INFO: Shim exited properly\n")
return nil
}
// ProxyMock is the proxy mock interface.
// It allows for implementing different kind
// of containers proxies front end.
type ProxyMock interface {
Start(URL string) error
Stop() error
}
// ProxyUnixMock is the UNIX proxy mock
type ProxyUnixMock struct {
ClientHandler func(c net.Conn)
listener net.Listener
}
// ProxyGRPCMock is the gRPC proxy mock
type ProxyGRPCMock struct {
// GRPCImplementer is the structure implementing
// the GRPC interface we want the proxy to serve.
GRPCImplementer interface{}
// GRPCRegister is the registration routine for
// the GRPC service.
GRPCRegister func(s *grpc.Server, srv interface{})
listener net.Listener
}
// Start starts the UNIX proxy mock
func (p *ProxyUnixMock) Start(URL string) error {
if p.ClientHandler == nil {
return fmt.Errorf("Missing client handler")
}
url, err := url.Parse(URL)
if err != nil {
return err
}
l, err := net.Listen(url.Scheme, url.Path)
if err != nil {
return err
}
p.listener = l
go func() {
defer func() {
l.Close()
}()
for {
conn, err := l.Accept()
if err != nil {
return
}
go p.ClientHandler(conn)
}
}()
return nil
}
// Stop stops the UNIX proxy mock
func (p *ProxyUnixMock) Stop() error {
if p.listener == nil {
return fmt.Errorf("Missing proxy listener")
}
return p.listener.Close()
}
// Start starts the gRPC proxy mock
func (p *ProxyGRPCMock) Start(URL string) error {
if p.GRPCImplementer == nil {
return fmt.Errorf("Missing gRPC handler")
}
if p.GRPCRegister == nil {
return fmt.Errorf("Missing gRPC registration routine")
}
url, err := url.Parse(URL)
if err != nil {
return err
}
l, err := net.Listen(url.Scheme, url.Path)
if err != nil {
return err
}
p.listener = l
grpcServer := grpc.NewServer()
p.GRPCRegister(grpcServer, p.GRPCImplementer)
go func() {
grpcServer.Serve(l)
}()
return nil
}
// Stop stops the gRPC proxy mock
func (p *ProxyGRPCMock) Stop() error {
if p.listener == nil {
return fmt.Errorf("Missing proxy listener")
}
return p.listener.Close()
}

View File

@@ -0,0 +1,194 @@
//
// Copyright (c) 2018 Intel Corporation
// Copyright 2015-2017 CNI authors
//
// 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 nsenter
import (
"fmt"
"os"
"path/filepath"
"runtime"
"strconv"
"sync"
"syscall"
"golang.org/x/sys/unix"
)
// Filesystems constants.
const (
// https://github.com/torvalds/linux/blob/master/include/uapi/linux/magic.h
nsFSMagic = 0x6e736673
procFSMagic = 0x9fa0
procRootPath = "/proc"
nsDirPath = "ns"
taskDirPath = "task"
)
// NSType defines a namespace type.
type NSType string
// List of namespace types.
// Notice that neither "mnt" nor "user" are listed into this list.
// Because Golang is multithreaded, we get some errors when trying
// to switch to those namespaces, getting "invalid argument".
// The solution is to reexec the current code so that it will call
// into a C constructor, making sure the namespace can be entered
// without multithreading issues.
const (
NSTypeCGroup NSType = "cgroup"
NSTypeIPC = "ipc"
NSTypeNet = "net"
NSTypePID = "pid"
NSTypeUTS = "uts"
)
// CloneFlagsTable is exported so that consumers of this package don't need
// to define this same table again.
var CloneFlagsTable = map[NSType]int{
NSTypeCGroup: unix.CLONE_NEWCGROUP,
NSTypeIPC: unix.CLONE_NEWIPC,
NSTypeNet: unix.CLONE_NEWNET,
NSTypePID: unix.CLONE_NEWPID,
NSTypeUTS: unix.CLONE_NEWUTS,
}
// Namespace describes a namespace that will be entered.
type Namespace struct {
Path string
PID int
Type NSType
}
type nsPair struct {
targetNS *os.File
threadNS *os.File
}
func getNSPathFromPID(pid int, nsType NSType) string {
return filepath.Join(procRootPath, strconv.Itoa(pid), nsDirPath, string(nsType))
}
func getCurrentThreadNSPath(nsType NSType) string {
return filepath.Join(procRootPath, strconv.Itoa(os.Getpid()),
taskDirPath, strconv.Itoa(unix.Gettid()), nsDirPath, string(nsType))
}
func setNS(nsFile *os.File, nsType NSType) error {
if nsFile == nil {
return fmt.Errorf("File handler cannot be nil")
}
nsFlag, exist := CloneFlagsTable[nsType]
if !exist {
return fmt.Errorf("Unknown namespace type %q", nsType)
}
if err := unix.Setns(int(nsFile.Fd()), nsFlag); err != nil {
return fmt.Errorf("Error switching to ns %v: %v", nsFile.Name(), err)
}
return nil
}
// getFileFromNS checks the provided file path actually matches a real
// namespace filesystem, and then opens it to return a handler to this
// file. This is needed since the system call setns() expects a file
// descriptor to enter the given namespace.
func getFileFromNS(nsPath string) (*os.File, error) {
stat := syscall.Statfs_t{}
if err := syscall.Statfs(nsPath, &stat); err != nil {
return nil, fmt.Errorf("failed to Statfs %q: %v", nsPath, err)
}
switch stat.Type {
case nsFSMagic, procFSMagic:
break
default:
return nil, fmt.Errorf("unknown FS magic on %q: %x", nsPath, stat.Type)
}
file, err := os.Open(nsPath)
if err != nil {
return nil, err
}
return file, nil
}
// NsEnter executes the passed closure under the given namespace,
// restoring the original namespace afterwards.
func NsEnter(nsList []Namespace, toRun func() error) error {
targetNSList := make(map[NSType]*nsPair)
// Open all targeted namespaces.
for _, ns := range nsList {
targetNSPath := ns.Path
if targetNSPath == "" {
targetNSPath = getNSPathFromPID(ns.PID, ns.Type)
}
targetNS, err := getFileFromNS(targetNSPath)
if err != nil {
return fmt.Errorf("failed to open target ns: %v", err)
}
defer targetNS.Close()
targetNSList[ns.Type] = &nsPair{
targetNS: targetNS,
}
}
containedCall := func() error {
for nsType := range targetNSList {
threadNS, err := getFileFromNS(getCurrentThreadNSPath(nsType))
if err != nil {
return fmt.Errorf("failed to open current ns: %v", err)
}
defer threadNS.Close()
targetNSList[nsType].threadNS = threadNS
}
// Switch to namespaces all at once.
for nsType, pair := range targetNSList {
// Switch to targeted namespace.
if err := setNS(pair.targetNS, nsType); err != nil {
return fmt.Errorf("error switching to ns %v: %v", pair.targetNS.Name(), err)
}
// Switch back to initial namespace after closure return.
defer setNS(pair.threadNS, nsType)
}
return toRun()
}
var wg sync.WaitGroup
wg.Add(1)
var innerError error
go func() {
defer wg.Done()
runtime.LockOSThread()
defer runtime.UnlockOSThread()
innerError = containedCall()
}()
wg.Wait()
return innerError
}

View File

@@ -0,0 +1,256 @@
//
// 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 nsenter
import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
"syscall"
"testing"
"github.com/stretchr/testify/assert"
"golang.org/x/sys/unix"
)
const (
testPID = 12345
testNSPath = "/foo/bar/ns"
)
func TestGetNSPathFromPID(t *testing.T) {
for nsType := range CloneFlagsTable {
expectedPath := fmt.Sprintf("/proc/%d/ns/%s", testPID, nsType)
path := getNSPathFromPID(testPID, nsType)
assert.Equal(t, path, expectedPath)
}
}
func TestGetCurrentThreadNSPath(t *testing.T) {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
currentPID := os.Getpid()
currentTID := unix.Gettid()
for nsType := range CloneFlagsTable {
expectedPath := fmt.Sprintf("/proc/%d/task/%d/ns/%s", currentPID, currentTID, nsType)
path := getCurrentThreadNSPath(nsType)
assert.Equal(t, path, expectedPath)
}
}
func TestGetFileFromNSEmptyNSPathFailure(t *testing.T) {
nsFile, err := getFileFromNS("")
assert.NotNil(t, err, "Empty path should result as a failure")
assert.Nil(t, nsFile, "The file handler returned should be nil")
}
func TestGetFileFromNSNotExistingNSPathFailure(t *testing.T) {
nsFile, err := ioutil.TempFile("", "not-existing-ns-path")
if err != nil {
t.Fatal(err)
}
nsFilePath := nsFile.Name()
nsFile.Close()
if err := os.Remove(nsFilePath); err != nil {
t.Fatal(err)
}
nsFile, err = getFileFromNS(nsFilePath)
assert.NotNil(t, err, "Not existing path should result as a failure")
assert.Nil(t, nsFile, "The file handler returned should be nil")
}
func TestGetFileFromNSWrongNSPathFailure(t *testing.T) {
nsFile, err := ioutil.TempFile("", "wrong-ns-path")
if err != nil {
t.Fatal(err)
}
nsFilePath := nsFile.Name()
nsFile.Close()
defer os.Remove(nsFilePath)
nsFile, err = getFileFromNS(nsFilePath)
assert.NotNil(t, err, "Should fail because wrong filesystem")
assert.Nil(t, nsFile, "The file handler returned should be nil")
}
func TestGetFileFromNSSuccessful(t *testing.T) {
for nsType := range CloneFlagsTable {
nsFilePath := fmt.Sprintf("/proc/self/ns/%s", string(nsType))
nsFile, err := getFileFromNS(nsFilePath)
assert.Nil(t, err, "Should have succeeded: %v", err)
assert.NotNil(t, nsFile, "The file handler should not be nil")
if nsFile != nil {
nsFile.Close()
}
}
}
func startSleepBinary(duration int, cloneFlags int) (int, error) {
sleepBinName := "sleep"
sleepPath, err := exec.LookPath(sleepBinName)
if err != nil {
return -1, fmt.Errorf("Could not find %q: %v", sleepBinName, err)
}
cmd := exec.Command(sleepPath, strconv.Itoa(duration))
cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: uintptr(cloneFlags),
}
if err := cmd.Start(); err != nil {
return -1, err
}
return cmd.Process.Pid, nil
}
func TestSetNSNilFileHandlerFailure(t *testing.T) {
err := setNS(nil, "")
assert.NotNil(t, err, "Should fail because file handler is nil")
}
func TestSetNSUnknownNSTypeFailure(t *testing.T) {
file := &os.File{}
err := setNS(file, "")
assert.NotNil(t, err, "Should fail because unknown ns type")
}
func TestSetNSWrongFileFailure(t *testing.T) {
nsFile, err := ioutil.TempFile("", "wrong-ns-path")
if err != nil {
t.Fatal(err)
}
defer func() {
nsFilePath := nsFile.Name()
nsFile.Close()
os.Remove(nsFilePath)
}()
err = setNS(nsFile, NSTypeIPC)
assert.NotNil(t, err, "Should fail because file is not a namespace")
}
var testNamespaceList = []Namespace{
{
Type: NSTypeCGroup,
},
{
Type: NSTypeIPC,
},
{
Type: NSTypeNet,
},
{
Type: NSTypePID,
},
{
Type: NSTypeUTS,
},
}
func testToRunNil() error {
return nil
}
func TestNsEnterEmptyPathAndPIDFromNSListFailure(t *testing.T) {
err := NsEnter(testNamespaceList, testToRunNil)
assert.NotNil(t, err, "Should fail because neither a path nor a PID"+
" has been provided by every namespace of the list")
}
func TestNsEnterEmptyNamespaceListSuccess(t *testing.T) {
err := NsEnter([]Namespace{}, testToRunNil)
assert.Nil(t, err, "Should not fail since closure should return nil: %v", err)
}
func TestNsEnterSuccessful(t *testing.T) {
nsList := testNamespaceList
sleepDuration := 60
cloneFlags := 0
for _, ns := range nsList {
cloneFlags |= CloneFlagsTable[ns.Type]
}
sleepPID, err := startSleepBinary(sleepDuration, cloneFlags)
if err != nil {
t.Fatal(err)
}
defer func() {
if sleepPID > 1 {
unix.Kill(sleepPID, syscall.SIGKILL)
}
}()
for idx := range nsList {
nsList[idx].Path = getNSPathFromPID(sleepPID, nsList[idx].Type)
nsList[idx].PID = sleepPID
}
var sleepPIDFromNsEnter int
testToRun := func() error {
sleepPIDFromNsEnter, err = startSleepBinary(sleepDuration, 0)
if err != nil {
return err
}
return nil
}
err = NsEnter(nsList, testToRun)
assert.Nil(t, err, "%v", err)
defer func() {
if sleepPIDFromNsEnter > 1 {
unix.Kill(sleepPIDFromNsEnter, syscall.SIGKILL)
}
}()
for _, ns := range nsList {
nsPathEntered := getNSPathFromPID(sleepPIDFromNsEnter, ns.Type)
// Here we are trying to resolve the path but it fails because
// namespaces links don't really exist. For this reason, the
// call to EvalSymlinks will fail when it will try to stat the
// resolved path found. As we only care about the path, we can
// retrieve it from the PathError structure.
evalExpectedNSPath, err := filepath.EvalSymlinks(ns.Path)
if err != nil {
evalExpectedNSPath = err.(*os.PathError).Path
}
// Same thing here, resolving the namespace path.
evalNSEnteredPath, err := filepath.EvalSymlinks(nsPathEntered)
if err != nil {
evalNSEnteredPath = err.(*os.PathError).Path
}
_, evalExpectedNS := filepath.Split(evalExpectedNSPath)
_, evalNSEntered := filepath.Split(evalNSEnteredPath)
assert.Equal(t, evalExpectedNS, evalNSEntered)
}
}

View File

@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -0,0 +1,680 @@
// 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 oci
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"path/filepath"
"strconv"
"strings"
criContainerdAnnotations "github.com/containerd/cri-containerd/pkg/annotations"
vc "github.com/containers/virtcontainers"
vcAnnotations "github.com/containers/virtcontainers/pkg/annotations"
dockershimAnnotations "github.com/containers/virtcontainers/pkg/annotations/dockershim"
crioAnnotations "github.com/kubernetes-incubator/cri-o/pkg/annotations"
spec "github.com/opencontainers/runtime-spec/specs-go"
"github.com/sirupsen/logrus"
)
type annotationContainerType struct {
annotation string
containerType vc.ContainerType
}
var (
// ErrNoLinux is an error for missing Linux sections in the OCI configuration file.
ErrNoLinux = errors.New("missing Linux section")
// CRIContainerTypeKeyList lists all the CRI keys that could define
// the container type from annotations in the config.json.
CRIContainerTypeKeyList = []string{criContainerdAnnotations.ContainerType, crioAnnotations.ContainerType, dockershimAnnotations.ContainerTypeLabelKey}
// CRISandboxNameKeyList lists all the CRI keys that could define
// the sandbox ID (pod ID) from annotations in the config.json.
CRISandboxNameKeyList = []string{criContainerdAnnotations.SandboxID, crioAnnotations.SandboxID, dockershimAnnotations.SandboxIDLabelKey}
// CRIContainerTypeList lists all the maps from CRI ContainerTypes annotations
// to a virtcontainers ContainerType.
CRIContainerTypeList = []annotationContainerType{
{crioAnnotations.ContainerTypeSandbox, vc.PodSandbox},
{crioAnnotations.ContainerTypeContainer, vc.PodContainer},
{criContainerdAnnotations.ContainerTypeSandbox, vc.PodSandbox},
{criContainerdAnnotations.ContainerTypeContainer, vc.PodContainer},
{dockershimAnnotations.ContainerTypeLabelSandbox, vc.PodSandbox},
{dockershimAnnotations.ContainerTypeLabelContainer, vc.PodContainer},
}
)
const (
// StateCreated represents a container that has been created and is
// ready to be run.
StateCreated = "created"
// StateRunning represents a container that's currently running.
StateRunning = "running"
// StateStopped represents a container that has been stopped.
StateStopped = "stopped"
)
// CompatOCIProcess is a structure inheriting from spec.Process defined
// in runtime-spec/specs-go package. The goal is to be compatible with
// both v1.0.0-rc4 and v1.0.0-rc5 since the latter introduced a change
// about the type of the Capabilities field.
// Refer to: https://github.com/opencontainers/runtime-spec/commit/37391fb
type CompatOCIProcess struct {
spec.Process
Capabilities interface{} `json:"capabilities,omitempty" platform:"linux"`
}
// CompatOCISpec is a structure inheriting from spec.Spec defined
// in runtime-spec/specs-go package. It relies on the CompatOCIProcess
// structure declared above, in order to be compatible with both
// v1.0.0-rc4 and v1.0.0-rc5.
// Refer to: https://github.com/opencontainers/runtime-spec/commit/37391fb
type CompatOCISpec struct {
spec.Spec
Process *CompatOCIProcess `json:"process,omitempty"`
}
// RuntimeConfig aggregates all runtime specific settings
type RuntimeConfig struct {
VMConfig vc.Resources
HypervisorType vc.HypervisorType
HypervisorConfig vc.HypervisorConfig
AgentType vc.AgentType
AgentConfig interface{}
ProxyType vc.ProxyType
ProxyConfig vc.ProxyConfig
ShimType vc.ShimType
ShimConfig interface{}
Console string
//Determines how the VM should be connected to the
//the container network interface
InterNetworkModel vc.NetInterworkingModel
}
// AddKernelParam allows the addition of new kernel parameters to an existing
// hypervisor configuration stored inside the current runtime configuration.
func (config *RuntimeConfig) AddKernelParam(p vc.Param) error {
return config.HypervisorConfig.AddKernelParam(p)
}
var ociLog = logrus.FieldLogger(logrus.New())
// SetLogger sets the logger for oci package.
func SetLogger(logger logrus.FieldLogger) {
ociLog = logger.WithField("source", "virtcontainers/oci")
}
func cmdEnvs(spec CompatOCISpec, envs []vc.EnvVar) []vc.EnvVar {
for _, env := range spec.Process.Env {
kv := strings.Split(env, "=")
if len(kv) < 2 {
continue
}
envs = append(envs,
vc.EnvVar{
Var: kv[0],
Value: kv[1],
})
}
return envs
}
func newHook(h spec.Hook) vc.Hook {
timeout := 0
if h.Timeout != nil {
timeout = *h.Timeout
}
return vc.Hook{
Path: h.Path,
Args: h.Args,
Env: h.Env,
Timeout: timeout,
}
}
func containerHooks(spec CompatOCISpec) vc.Hooks {
ociHooks := spec.Hooks
if ociHooks == nil {
return vc.Hooks{}
}
var hooks vc.Hooks
for _, h := range ociHooks.Prestart {
hooks.PreStartHooks = append(hooks.PreStartHooks, newHook(h))
}
for _, h := range ociHooks.Poststart {
hooks.PostStartHooks = append(hooks.PostStartHooks, newHook(h))
}
for _, h := range ociHooks.Poststop {
hooks.PostStopHooks = append(hooks.PostStopHooks, newHook(h))
}
return hooks
}
func newMount(m spec.Mount) vc.Mount {
return vc.Mount{
Source: m.Source,
Destination: m.Destination,
Type: m.Type,
Options: m.Options,
}
}
func containerMounts(spec CompatOCISpec) []vc.Mount {
ociMounts := spec.Spec.Mounts
if ociMounts == nil {
return []vc.Mount{}
}
var mnts []vc.Mount
for _, m := range ociMounts {
mnts = append(mnts, newMount(m))
}
return mnts
}
func contains(s []string, e string) bool {
for _, a := range s {
if a == e {
return true
}
}
return false
}
func newLinuxDeviceInfo(d spec.LinuxDevice) (*vc.DeviceInfo, error) {
allowedDeviceTypes := []string{"c", "b", "u", "p"}
if !contains(allowedDeviceTypes, d.Type) {
return nil, fmt.Errorf("Unexpected Device Type %s for device %s", d.Type, d.Path)
}
if d.Path == "" {
return nil, fmt.Errorf("Path cannot be empty for device")
}
deviceInfo := vc.DeviceInfo{
ContainerPath: d.Path,
DevType: d.Type,
Major: d.Major,
Minor: d.Minor,
}
if d.UID != nil {
deviceInfo.UID = *d.UID
}
if d.GID != nil {
deviceInfo.GID = *d.GID
}
if d.FileMode != nil {
deviceInfo.FileMode = *d.FileMode
}
return &deviceInfo, nil
}
func containerDeviceInfos(spec CompatOCISpec) ([]vc.DeviceInfo, error) {
ociLinuxDevices := spec.Spec.Linux.Devices
if ociLinuxDevices == nil {
return []vc.DeviceInfo{}, nil
}
var devices []vc.DeviceInfo
for _, d := range ociLinuxDevices {
linuxDeviceInfo, err := newLinuxDeviceInfo(d)
if err != nil {
return []vc.DeviceInfo{}, err
}
devices = append(devices, *linuxDeviceInfo)
}
return devices, nil
}
func containerCapabilities(s CompatOCISpec) (vc.LinuxCapabilities, error) {
capabilities := s.Process.Capabilities
var c vc.LinuxCapabilities
// In spec v1.0.0-rc4, capabilities was a list of strings. This was changed
// to an object with v1.0.0-rc5.
// Check for the interface type to support both the versions.
switch caps := capabilities.(type) {
case map[string]interface{}:
for key, value := range caps {
switch val := value.(type) {
case []interface{}:
var list []string
for _, str := range val {
list = append(list, str.(string))
}
switch key {
case "bounding":
c.Bounding = list
case "effective":
c.Effective = list
case "inheritable":
c.Inheritable = list
case "ambient":
c.Ambient = list
case "permitted":
c.Permitted = list
}
default:
return c, fmt.Errorf("Unexpected format for capabilities: %v", caps)
}
}
case []interface{}:
var list []string
for _, str := range caps {
list = append(list, str.(string))
}
c = vc.LinuxCapabilities{
Bounding: list,
Effective: list,
Inheritable: list,
Ambient: list,
Permitted: list,
}
case nil:
ociLog.Debug("Empty capabilities have been passed")
return c, nil
default:
return c, fmt.Errorf("Unexpected format for capabilities: %v", caps)
}
return c, nil
}
func networkConfig(ocispec CompatOCISpec, config RuntimeConfig) (vc.NetworkConfig, error) {
linux := ocispec.Linux
if linux == nil {
return vc.NetworkConfig{}, ErrNoLinux
}
var netConf vc.NetworkConfig
for _, n := range linux.Namespaces {
if n.Type != spec.NetworkNamespace {
continue
}
// Bug: This is not the interface count
// It is just an indication that you need networking
netConf.NumInterfaces = 1
if n.Path != "" {
netConf.NetNSPath = n.Path
}
}
netConf.InterworkingModel = config.InterNetworkModel
return netConf, nil
}
// getConfigPath returns the full config path from the bundle
// path provided.
func getConfigPath(bundlePath string) string {
return filepath.Join(bundlePath, "config.json")
}
// ParseConfigJSON unmarshals the config.json file.
func ParseConfigJSON(bundlePath string) (CompatOCISpec, error) {
configPath := getConfigPath(bundlePath)
ociLog.Debugf("converting %s", configPath)
configByte, err := ioutil.ReadFile(configPath)
if err != nil {
return CompatOCISpec{}, err
}
var ocispec CompatOCISpec
if err := json.Unmarshal(configByte, &ocispec); err != nil {
return CompatOCISpec{}, err
}
return ocispec, nil
}
// GetContainerType determines which type of container matches the annotations
// table provided.
func GetContainerType(annotations map[string]string) (vc.ContainerType, error) {
if containerType, ok := annotations[vcAnnotations.ContainerTypeKey]; ok {
return vc.ContainerType(containerType), nil
}
ociLog.Errorf("Annotations[%s] not found, cannot determine the container type",
vcAnnotations.ContainerTypeKey)
return vc.UnknownContainerType, fmt.Errorf("Could not find container type")
}
// ContainerType returns the type of container and if the container type was
// found from CRI servers annotations.
func (spec *CompatOCISpec) ContainerType() (vc.ContainerType, error) {
for _, key := range CRIContainerTypeKeyList {
containerTypeVal, ok := spec.Annotations[key]
if !ok {
continue
}
for _, t := range CRIContainerTypeList {
if t.annotation == containerTypeVal {
return t.containerType, nil
}
}
return vc.UnknownContainerType, fmt.Errorf("Unknown container type %s", containerTypeVal)
}
return vc.PodSandbox, nil
}
// PodID determines the pod ID related to an OCI configuration. This function
// is expected to be called only when the container type is "PodContainer".
func (spec *CompatOCISpec) PodID() (string, error) {
for _, key := range CRISandboxNameKeyList {
podID, ok := spec.Annotations[key]
if ok {
return podID, nil
}
}
return "", fmt.Errorf("Could not find pod ID")
}
func vmConfig(ocispec CompatOCISpec, config RuntimeConfig) (vc.Resources, error) {
resources := config.VMConfig
if ocispec.Linux == nil || ocispec.Linux.Resources == nil {
return resources, nil
}
if ocispec.Linux.Resources.Memory != nil &&
ocispec.Linux.Resources.Memory.Limit != nil {
memBytes := *ocispec.Linux.Resources.Memory.Limit
if memBytes <= 0 {
return vc.Resources{}, fmt.Errorf("Invalid OCI memory limit %d", memBytes)
}
// Use some math magic to round up to the nearest Mb.
// This has the side effect that we can never have <1Mb assigned.
resources.Memory = uint((memBytes + (1024*1024 - 1)) / (1024 * 1024))
}
return resources, nil
}
func addAssetAnnotations(ocispec CompatOCISpec, config *vc.PodConfig) {
assetAnnotations := []string{
vcAnnotations.KernelPath,
vcAnnotations.ImagePath,
vcAnnotations.KernelHash,
vcAnnotations.ImageHash,
vcAnnotations.AssetHashType,
}
for _, a := range assetAnnotations {
value, ok := ocispec.Annotations[a]
if !ok {
continue
}
config.Annotations[a] = value
}
}
// PodConfig converts an OCI compatible runtime configuration file
// to a virtcontainers pod configuration structure.
func PodConfig(ocispec CompatOCISpec, runtime RuntimeConfig, bundlePath, cid, console string, detach bool) (vc.PodConfig, error) {
containerConfig, err := ContainerConfig(ocispec, bundlePath, cid, console, detach)
if err != nil {
return vc.PodConfig{}, err
}
networkConfig, err := networkConfig(ocispec, runtime)
if err != nil {
return vc.PodConfig{}, err
}
resources, err := vmConfig(ocispec, runtime)
if err != nil {
return vc.PodConfig{}, err
}
ociSpecJSON, err := json.Marshal(ocispec)
if err != nil {
return vc.PodConfig{}, err
}
podConfig := vc.PodConfig{
ID: cid,
Hostname: ocispec.Hostname,
Hooks: containerHooks(ocispec),
VMConfig: resources,
HypervisorType: runtime.HypervisorType,
HypervisorConfig: runtime.HypervisorConfig,
AgentType: runtime.AgentType,
AgentConfig: runtime.AgentConfig,
ProxyType: runtime.ProxyType,
ProxyConfig: runtime.ProxyConfig,
ShimType: runtime.ShimType,
ShimConfig: runtime.ShimConfig,
NetworkModel: vc.CNMNetworkModel,
NetworkConfig: networkConfig,
Containers: []vc.ContainerConfig{containerConfig},
Annotations: map[string]string{
vcAnnotations.ConfigJSONKey: string(ociSpecJSON),
vcAnnotations.BundlePathKey: bundlePath,
},
}
addAssetAnnotations(ocispec, &podConfig)
return podConfig, nil
}
// ContainerConfig converts an OCI compatible runtime configuration
// file to a virtcontainers container configuration structure.
func ContainerConfig(ocispec CompatOCISpec, bundlePath, cid, console string, detach bool) (vc.ContainerConfig, error) {
ociSpecJSON, err := json.Marshal(ocispec)
if err != nil {
return vc.ContainerConfig{}, err
}
rootfs := ocispec.Root.Path
if !filepath.IsAbs(rootfs) {
rootfs = filepath.Join(bundlePath, ocispec.Root.Path)
}
ociLog.Debugf("container rootfs: %s", rootfs)
cmd := vc.Cmd{
Args: ocispec.Process.Args,
Envs: cmdEnvs(ocispec, []vc.EnvVar{}),
WorkDir: ocispec.Process.Cwd,
User: strconv.FormatUint(uint64(ocispec.Process.User.UID), 10),
PrimaryGroup: strconv.FormatUint(uint64(ocispec.Process.User.GID), 10),
Interactive: ocispec.Process.Terminal,
Console: console,
Detach: detach,
NoNewPrivileges: ocispec.Process.NoNewPrivileges,
}
cmd.SupplementaryGroups = []string{}
for _, gid := range ocispec.Process.User.AdditionalGids {
cmd.SupplementaryGroups = append(cmd.SupplementaryGroups, strconv.FormatUint(uint64(gid), 10))
}
deviceInfos, err := containerDeviceInfos(ocispec)
if err != nil {
return vc.ContainerConfig{}, err
}
cmd.Capabilities, err = containerCapabilities(ocispec)
if err != nil {
return vc.ContainerConfig{}, err
}
var resources vc.ContainerResources
if ocispec.Linux.Resources.CPU != nil {
if ocispec.Linux.Resources.CPU.Quota != nil &&
ocispec.Linux.Resources.CPU.Period != nil {
resources.CPUQuota = *ocispec.Linux.Resources.CPU.Quota
resources.CPUPeriod = *ocispec.Linux.Resources.CPU.Period
}
if ocispec.Linux.Resources.CPU.Shares != nil {
resources.CPUShares = *ocispec.Linux.Resources.CPU.Shares
}
}
containerConfig := vc.ContainerConfig{
ID: cid,
RootFs: rootfs,
ReadonlyRootfs: ocispec.Spec.Root.Readonly,
Cmd: cmd,
Annotations: map[string]string{
vcAnnotations.ConfigJSONKey: string(ociSpecJSON),
vcAnnotations.BundlePathKey: bundlePath,
},
Mounts: containerMounts(ocispec),
DeviceInfos: deviceInfos,
Resources: resources,
}
cType, err := ocispec.ContainerType()
if err != nil {
return vc.ContainerConfig{}, err
}
containerConfig.Annotations[vcAnnotations.ContainerTypeKey] = string(cType)
return containerConfig, nil
}
// StatusToOCIState translates a virtcontainers container status into an OCI state.
func StatusToOCIState(status vc.ContainerStatus) spec.State {
return spec.State{
Version: spec.Version,
ID: status.ID,
Status: StateToOCIState(status.State),
Pid: status.PID,
Bundle: status.Annotations[vcAnnotations.BundlePathKey],
Annotations: status.Annotations,
}
}
// StateToOCIState translates a virtcontainers container state into an OCI one.
func StateToOCIState(state vc.State) string {
switch state.State {
case vc.StateReady:
return StateCreated
case vc.StateRunning:
return StateRunning
case vc.StateStopped:
return StateStopped
default:
return ""
}
}
// EnvVars converts an OCI process environment variables slice
// into a virtcontainers EnvVar slice.
func EnvVars(envs []string) ([]vc.EnvVar, error) {
var envVars []vc.EnvVar
envDelimiter := "="
expectedEnvLen := 2
for _, env := range envs {
envSlice := strings.SplitN(env, envDelimiter, expectedEnvLen)
if len(envSlice) < expectedEnvLen {
return []vc.EnvVar{}, fmt.Errorf("Wrong string format: %s, expecting only %v parameters separated with %q",
env, expectedEnvLen, envDelimiter)
}
if envSlice[0] == "" {
return []vc.EnvVar{}, fmt.Errorf("Environment variable cannot be empty")
}
envSlice[1] = strings.Trim(envSlice[1], "' ")
if envSlice[1] == "" {
return []vc.EnvVar{}, fmt.Errorf("Environment value cannot be empty")
}
envVar := vc.EnvVar{
Var: envSlice[0],
Value: envSlice[1],
}
envVars = append(envVars, envVar)
}
return envVars, nil
}
// GetOCIConfig returns an OCI spec configuration from the annotation
// stored into the container status.
func GetOCIConfig(status vc.ContainerStatus) (CompatOCISpec, error) {
ociConfigStr, ok := status.Annotations[vcAnnotations.ConfigJSONKey]
if !ok {
return CompatOCISpec{}, fmt.Errorf("Annotation[%s] not found", vcAnnotations.ConfigJSONKey)
}
var ociSpec CompatOCISpec
if err := json.Unmarshal([]byte(ociConfigStr), &ociSpec); err != nil {
return CompatOCISpec{}, err
}
return ociSpec, nil
}

Some files were not shown because too many files have changed in this diff Show More