Compare commits
517 Commits
Author | SHA1 | Date | |
---|---|---|---|
e13bab99e5 | |||
3689d53adf | |||
680c6dd0b1 | |||
be19d786a0 | |||
001abd4f55 | |||
8d52c42f90 | |||
3ae85c1093 | |||
25704f9372 | |||
7e59bac059 | |||
eb31403ac7 | |||
336ba52542 | |||
b47d178ae0 | |||
509d645ee9 | |||
108c2aebd4 | |||
cccf5395e8 | |||
c41c78b600 | |||
8aad9739d8 | |||
b7af044cdc | |||
95248d8490 | |||
50a3aa6536 | |||
98c621abe6 | |||
b34402abd3 | |||
ccd872bd7a | |||
5fc309a699 | |||
440dcc331b | |||
362f5d626a | |||
8d0d8a9547 | |||
a9abbaf19b | |||
c4b8bccd2a | |||
1a7f1bd8b1 | |||
8a6e96b3f0 | |||
6df03d7937 | |||
e1517e2498 | |||
322790226b | |||
59baaa1546 | |||
9ce99d3f07 | |||
e78e6aa5b9 | |||
08ff3b6413 | |||
1ea19f9213 | |||
9b8de6a613 | |||
dacb671328 | |||
799d3cbf4c | |||
bd589992fb | |||
839d918e35 | |||
c50490eb76 | |||
01a8de9997 | |||
8a88f90f94 | |||
2b6cd5467f | |||
3aadb402e4 | |||
021462563b | |||
d713ec692c | |||
813f541d30 | |||
6eb8e31d21 | |||
051452cdcf | |||
877602d627 | |||
c90b165c6e | |||
28773dc925 | |||
2bd04cb92f | |||
d4775ecff5 | |||
e1f8f9bee5 | |||
68a80bcf9b | |||
1fb9793607 | |||
5cb3a5e897 | |||
b76fdd7c03 | |||
67175607ad | |||
b9560fd5c1 | |||
79192cb1f1 | |||
ad10b6fa91 | |||
219eb9e046 | |||
f7a2fc97e4 | |||
6957f6ca4e | |||
02bfece2e9 | |||
5af9ff493e | |||
44d92c19de | |||
5e0fbd8374 | |||
a78853f29f | |||
2d2583ee33 | |||
f4332fec59 | |||
ed16760739 | |||
30776ff858 | |||
2a48d68937 | |||
117e30ff21 | |||
486ef96e6f | |||
8a0e3fe10e | |||
ca419073e4 | |||
47a9fd80c8 | |||
112288ecb2 | |||
32fc3ee9d3 | |||
c7e2cf7602 | |||
53854dd948 | |||
5c512194eb | |||
a9b4e04bc4 | |||
f5c3d1b1ba | |||
8bf6a7b362 | |||
66e0aaf9c1 | |||
45fd949465 | |||
2ff84a481e | |||
37207f05b4 | |||
832f2699c2 | |||
d1360b82ab | |||
1f33fb729a | |||
754e153b03 | |||
0edf8a4208 | |||
db7e6cfabf | |||
963aaf86e6 | |||
cd9d6b28da | |||
0452c1dd10 | |||
d671d29ad5 | |||
cc6154603e | |||
62b36d2fbc | |||
e5fdd449dd | |||
8db5e4d41b | |||
ec8f6c99d0 | |||
7dea2a4c1b | |||
5a02c5bc61 | |||
bf8f171041 | |||
3603738c6a | |||
d8b1289098 | |||
6551165853 | |||
10a01b09ae | |||
497560f35f | |||
58dd90b996 | |||
d5efdfe1f6 | |||
05f121a406 | |||
825fbd8a95 | |||
1a30688da0 | |||
bee8d6cf30 | |||
a16232968d | |||
1880421389 | |||
a2ed3d9a69 | |||
7bcaae263f | |||
e1f955d9bf | |||
2583a0b4ad | |||
85083ea434 | |||
2290fc8d8a | |||
411d060b81 | |||
5915b49b38 | |||
c25c62742b | |||
b7ffa24326 | |||
15894b36a0 | |||
77b51f0bc9 | |||
bd63528b0b | |||
cf187287af | |||
0dff883769 | |||
d0eeb27494 | |||
e70558cbe1 | |||
0a1421a08c | |||
0f19aa2f8d | |||
e91889678b | |||
8ec6bd6a42 | |||
fc7059c1ae | |||
a96c469e62 | |||
291ab6cc84 | |||
90125f40ba | |||
23d5525ec3 | |||
fd42109a06 | |||
4bb288193c | |||
e8365e126d | |||
7e68430081 | |||
f81a529ebd | |||
630a4d8db6 | |||
3d56f7504d | |||
659a09f34e | |||
b76ace9c64 | |||
0d0dcfc02f | |||
485be65581 | |||
ca82120019 | |||
c9e1c0c1ed | |||
2d6d4b260a | |||
ad7c1d189b | |||
a069a5f1a3 | |||
ccd683e1a3 | |||
a11cb626b0 | |||
f36dbc2031 | |||
e9d511c5bc | |||
91a68d56f9 | |||
8902d2614a | |||
df9af9ab41 | |||
5e2e365291 | |||
4b68f56820 | |||
ded2f17577 | |||
57650a1e5b | |||
7ba2bcfeab | |||
3fb8dcfd4c | |||
966bbcb8a5 | |||
7d76537d4a | |||
f3b1ffc960 | |||
ce9560712e | |||
e2984e7840 | |||
eb1ff18c4c | |||
e8771b36a2 | |||
7f8ea631e5 | |||
0eddc554c0 | |||
e8a25e33cd | |||
96bd10f679 | |||
303299b7b2 | |||
d42007865a | |||
ce60e8eb3d | |||
addbcd34b4 | |||
e8c953999e | |||
13fbc4afdf | |||
545a77f4bb | |||
c204dbd47c | |||
660685a8af | |||
2b6808807f | |||
869d5ec873 | |||
93919752fb | |||
fe60fcddb8 | |||
e308211d34 | |||
29a431f1fc | |||
7d75ab66a4 | |||
d5c8b4b206 | |||
5a4085f1fa | |||
37d2ee1d5d | |||
7f9b1844b8 | |||
2753b9af8f | |||
d35c96dda6 | |||
344d343431 | |||
0950a3607b | |||
a6a8a818e4 | |||
02ccf1ef9b | |||
a03e6b505e | |||
6bceb68143 | |||
e2187efec1 | |||
5a78120e24 | |||
071907f867 | |||
2d6d46d308 | |||
af692de1b8 | |||
ae0b03787f | |||
b199dc3ffe | |||
dbc86e4db2 | |||
29801c45a7 | |||
12b426a921 | |||
7efec9ea46 | |||
79b1c402c4 | |||
44297f6ba3 | |||
cc4976a43c | |||
827a4bd843 | |||
697550083d | |||
214bcb8c8b | |||
635968aaff | |||
7df5acee0f | |||
7924ef76da | |||
5e25b0e40c | |||
688a87a055 | |||
229ec3602d | |||
8c52f50849 | |||
6ea047fe09 | |||
72f2a1ffd4 | |||
6733d30762 | |||
d47387c6fe | |||
95be5da5e2 | |||
b46e1a0138 | |||
9d6f1e9975 | |||
eb66fc201c | |||
d096a4df48 | |||
e9e1d37309 | |||
b69a2dd3d7 | |||
0d6198bac4 | |||
914f2bc740 | |||
e028b70b49 | |||
fbd9accd14 | |||
8b53f80fa8 | |||
dc536993e2 | |||
18874aac7d | |||
74a2596573 | |||
8a3d149a60 | |||
9c9a8e991d | |||
f55d8d02ce | |||
d577d9cc24 | |||
4ec62ac295 | |||
82a0651d0a | |||
9fab520c37 | |||
b1814d7f9a | |||
b71e8db683 | |||
afd7391938 | |||
a95d48bb01 | |||
57b42a7b99 | |||
4bca3e76bf | |||
1865a0701e | |||
29928cff4d | |||
c74e0e9967 | |||
9226c9642d | |||
fd71dae5a7 | |||
d29d56bc4d | |||
f891fd9e9a | |||
a5378f2ccf | |||
9a429d8d25 | |||
a686cc4bd8 | |||
f5c451f719 | |||
f0208611ad | |||
ee819c71a1 | |||
4371665606 | |||
d0a28ce5f7 | |||
34b1b702dc | |||
35451e67e2 | |||
59a746bd52 | |||
3fb464786f | |||
72251a67b5 | |||
b75d14ab84 | |||
bf47e9aa1b | |||
251a00e6b4 | |||
0af31fc4d0 | |||
227a4c15fa | |||
073df9a04d | |||
71e026426f | |||
5fc8209482 | |||
534bfafe99 | |||
44757b95ef | |||
6f3332e9fe | |||
ddbf22f7f9 | |||
4e1f7802db | |||
d3284f7c5e | |||
ee57f675ad | |||
f006155b66 | |||
2d03820ebb | |||
e3ec4a74d0 | |||
b93d284d18 | |||
3129152706 | |||
9abd20edd6 | |||
2955d63c95 | |||
a326f9d3f8 | |||
3072177d0e | |||
d2ebd0a4c2 | |||
f879dad626 | |||
22b11bb367 | |||
396f51afab | |||
73106f0ece | |||
bf31f08981 | |||
75e35b6c01 | |||
37715a0f87 | |||
a17cadda88 | |||
1e8f9525a6 | |||
ef913eadd5 | |||
3d349e4645 | |||
3de323f3f0 | |||
5fd849ac6d | |||
6d3215a256 | |||
726759b29b | |||
66837d6f3b | |||
1e4d47fc35 | |||
0fbc611121 | |||
9959f787e8 | |||
646dbbace1 | |||
9b86f52791 | |||
8a579a7fbc | |||
a8ad12dd7a | |||
b56ca2fe45 | |||
e1d29e9fe4 | |||
7b72f2afd4 | |||
93178bf026 | |||
f5f787057d | |||
8c61a2c2f3 | |||
1a3f49c7ae | |||
947a5881fc | |||
26834c3e63 | |||
f1ee2cc614 | |||
d22e75316f | |||
094c903932 | |||
61a412ea77 | |||
61c136126f | |||
35b87a34db | |||
9048a61dda | |||
321467bf1b | |||
60a99ca331 | |||
7d329215b0 | |||
6a12a375cd | |||
220499db6b | |||
5532950dce | |||
fb7a24405e | |||
9689522b4f | |||
9425d24c28 | |||
635cb22f12 | |||
75d3585862 | |||
3a7f254a63 | |||
2b819b5571 | |||
316489903b | |||
3a7ee332be | |||
cc3ad26691 | |||
4b296ba330 | |||
1562a1e60e | |||
6dc16b9132 | |||
21e3e4ec01 | |||
427a38e2d7 | |||
f970542036 | |||
9201a5a433 | |||
3b3622db67 | |||
dc899ac0e0 | |||
1f77018b60 | |||
7571169160 | |||
696b1f9ab1 | |||
b2fc336833 | |||
6da1cb7876 | |||
1f6e6ef198 | |||
68b4efb405 | |||
e4fdb6cd18 | |||
2b8b1ac0af | |||
475fdb8a0a | |||
50d626fe02 | |||
1d973f59d2 | |||
79e7b27494 | |||
dfbaeccc12 | |||
731298003c | |||
eb410208cb | |||
1df359a210 | |||
adba9ec16e | |||
4a0971bcd8 | |||
7cf02869ec | |||
aade7b93ee | |||
bb1e8d10a7 | |||
0dba9daee0 | |||
72b62babee | |||
e4f13535ab | |||
26dafaa097 | |||
13e6a4b2ba | |||
00b072dd0b | |||
a0eac8d7d9 | |||
263ff063a6 | |||
9c36007eea | |||
9bdf37ff9a | |||
e590fca5ae | |||
00fbba085e | |||
1fb94a4222 | |||
fe0cf201f8 | |||
d2f6472474 | |||
56989e2380 | |||
d5bdfe4cbd | |||
2793dd11cb | |||
90252c30fb | |||
59fa37252f | |||
dce91d11d6 | |||
b78e535055 | |||
372bb5e826 | |||
961b53bf2f | |||
c82fb1bbe2 | |||
c850d4514d | |||
fa2bf4c210 | |||
6cb23dc489 | |||
8e0f961576 | |||
69cc860821 | |||
0536605966 | |||
b16633bbe9 | |||
8da8088d43 | |||
dd8ff8a5cf | |||
08ec2995ba | |||
fdc602c0f0 | |||
136399f078 | |||
596480eadc | |||
61c3c42107 | |||
b522ed6aae | |||
26ef6e312d | |||
461d433911 | |||
84a01001be | |||
e2f063b534 | |||
9e5836047c | |||
808d4e20ae | |||
3468364f7e | |||
412b6d3128 | |||
9604565b22 | |||
2a0736c748 | |||
2eba56ad52 | |||
d228f980e1 | |||
6aa21c431e | |||
c42470bc79 | |||
ffc591e242 | |||
59f9976017 | |||
c26961a990 | |||
8ebea58550 | |||
97664d8a6a | |||
5c7e7c0913 | |||
b03d23a4fa | |||
03e316b07b | |||
ecdd827d3a | |||
1f02326d56 | |||
92c634042c | |||
2c05055101 | |||
5e830efb20 | |||
73fdc87395 | |||
d07d2aaf71 | |||
6c2ef734c2 | |||
47668f6d64 | |||
fbced0cccb | |||
99f6be0319 | |||
b09e0d28a7 | |||
5576f3120e | |||
449700f7ea | |||
4779f1d2bf | |||
7f98c94613 | |||
596b44301b | |||
0063a1b9d0 | |||
cc71426592 | |||
e256564546 | |||
c238c93b5e | |||
25ca6ccb52 | |||
5e46a66c89 | |||
6be2e8a0e2 | |||
b24225fc17 | |||
d8f2fd7a3c | |||
1396ab0bab | |||
92babd4a3d | |||
7a62515407 | |||
008024125a | |||
556e509097 | |||
dda9c2b1b0 | |||
0e3df2961c | |||
e1ea7f5ecb | |||
8fe8460c72 | |||
92e62b9f4d | |||
2f957864ea | |||
b49379d284 | |||
9769434a13 | |||
a124fb36e6 | |||
9fb22524a1 | |||
2d7d680874 | |||
ca3f28fa9e | |||
f2faf549b4 | |||
6099d8c84c |
7
.github/actions/retest-action/Dockerfile
vendored
Normal file
7
.github/actions/retest-action/Dockerfile
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
FROM alpine:3.10
|
||||
|
||||
RUN apk add --no-cache curl jq
|
||||
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
11
.github/actions/retest-action/action.yml
vendored
Normal file
11
.github/actions/retest-action/action.yml
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
name: 'Re-Test'
|
||||
description: 'Re-Runs the last workflow for a PR'
|
||||
inputs:
|
||||
token:
|
||||
description: 'GitHub API Token'
|
||||
required: true
|
||||
runs:
|
||||
using: 'docker'
|
||||
image: 'Dockerfile'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ inputs.token }}
|
45
.github/actions/retest-action/entrypoint.sh
vendored
Executable file
45
.github/actions/retest-action/entrypoint.sh
vendored
Executable file
@ -0,0 +1,45 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -ex
|
||||
|
||||
if ! jq -e '.issue.pull_request' ${GITHUB_EVENT_PATH}; then
|
||||
echo "Not a PR... Exiting."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$(jq -r '.comment.body' ${GITHUB_EVENT_PATH})" != "/retest" ]; then
|
||||
echo "Nothing to do... Exiting."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
PR_URL=$(jq -r '.issue.pull_request.url' ${GITHUB_EVENT_PATH})
|
||||
|
||||
curl --request GET \
|
||||
--url "${PR_URL}" \
|
||||
--header "authorization: Bearer ${GITHUB_TOKEN}" \
|
||||
--header "content-type: application/json" > pr.json
|
||||
|
||||
ACTOR=$(jq -r '.user.login' pr.json)
|
||||
BRANCH=$(jq -r '.head.ref' pr.json)
|
||||
|
||||
curl --request GET \
|
||||
--url "https://api.github.com/repos/${GITHUB_REPOSITORY}/actions/runs?event=pull_request&actor=${ACTOR}&branch=${BRANCH}" \
|
||||
--header "authorization: Bearer ${GITHUB_TOKEN}" \
|
||||
--header "content-type: application/json" | jq '.workflow_runs | max_by(.run_number)' > run.json
|
||||
|
||||
RERUN_URL=$(jq -r '.rerun_url' run.json)
|
||||
|
||||
curl --request POST \
|
||||
--url "${RERUN_URL}" \
|
||||
--header "authorization: Bearer ${GITHUB_TOKEN}" \
|
||||
--header "content-type: application/json"
|
||||
|
||||
|
||||
REACTION_URL="$(jq -r '.comment.url' ${GITHUB_EVENT_PATH})/reactions"
|
||||
|
||||
curl --request POST \
|
||||
--url "${REACTION_URL}" \
|
||||
--header "authorization: Bearer ${GITHUB_TOKEN}" \
|
||||
--header "accept: application/vnd.github.squirrel-girl-preview+json" \
|
||||
--header "content-type: application/json" \
|
||||
--data '{ "content" : "rocket" }'
|
17
.github/workflows/commands.yml
vendored
Normal file
17
.github/workflows/commands.yml
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
name: commands
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
retest:
|
||||
if: github.repository == 'containernetworking/plugins'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Re-Test Action
|
||||
uses: ./.github/actions/retest-action
|
||||
with:
|
||||
token: ${{ secrets.REPO_ACCESS_TOKEN }}
|
69
.github/workflows/test.yaml
vendored
Normal file
69
.github/workflows/test.yaml
vendored
Normal file
@ -0,0 +1,69 @@
|
||||
---
|
||||
name: test
|
||||
|
||||
on: ["push", "pull_request"]
|
||||
|
||||
env:
|
||||
GO_VERSION: "1.15"
|
||||
LINUX_ARCHES: "amd64 386 arm arm64 s390x mips64le ppc64le"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build all linux architectures
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: setup go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Build on all supported architectures
|
||||
run: |
|
||||
set -e
|
||||
for arch in ${LINUX_ARCHES}; do
|
||||
echo "Building for arch $arch"
|
||||
GOARCH=$arch ./build_linux.sh
|
||||
rm bin/*
|
||||
done
|
||||
|
||||
test-linux:
|
||||
name: Run tests on Linux amd64
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: setup go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Install test binaries
|
||||
env:
|
||||
GO111MODULE: off
|
||||
run: |
|
||||
go get github.com/containernetworking/cni/cnitool
|
||||
go get github.com/mattn/goveralls
|
||||
go get github.com/modocache/gover
|
||||
|
||||
- name: test
|
||||
run: PATH=$PATH:$(go env GOPATH)/bin COVERALLS=1 ./test_linux.sh
|
||||
|
||||
- name: Send coverage to coveralls
|
||||
env:
|
||||
COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
PATH=$PATH:$(go env GOPATH)/bin
|
||||
gover
|
||||
goveralls -coverprofile=gover.coverprofile -service=github
|
||||
|
||||
test-win:
|
||||
name: Build and run tests on Windows
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: setup go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
- uses: actions/checkout@v2
|
||||
- name: test
|
||||
run: bash ./test_windows.sh
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -26,3 +26,5 @@ _testmain.go
|
||||
bin/
|
||||
gopath/
|
||||
.vagrant
|
||||
.idea
|
||||
/release-*
|
||||
|
34
.travis.yml
34
.travis.yml
@ -1,34 +0,0 @@
|
||||
language: go
|
||||
sudo: required
|
||||
dist: trusty
|
||||
|
||||
go:
|
||||
- 1.7.x
|
||||
- 1.8.x
|
||||
|
||||
env:
|
||||
global:
|
||||
- PATH=$GOROOT/bin:$PATH
|
||||
matrix:
|
||||
- TARGET=amd64
|
||||
- TARGET=arm
|
||||
- TARGET=arm64
|
||||
- TARGET=ppc64le
|
||||
- TARGET=s390x
|
||||
|
||||
matrix:
|
||||
fast_finish: true
|
||||
|
||||
script:
|
||||
- |
|
||||
if [ "${TARGET}" == "amd64" ]; then
|
||||
GOARCH="${TARGET}" ./test.sh
|
||||
else
|
||||
GOARCH="${TARGET}" ./build.sh
|
||||
fi
|
||||
|
||||
notifications:
|
||||
email: false
|
||||
|
||||
git:
|
||||
depth: 9999999
|
@ -26,9 +26,14 @@ are very busy and read the mailing lists.
|
||||
## Getting Started
|
||||
|
||||
- Fork the repository on GitHub
|
||||
- Read the [README](README.md) for build and test instructions
|
||||
- Play with the project, submit bugs, submit pull requests!
|
||||
|
||||
|
||||
## Building
|
||||
|
||||
Each plugin is compiled simply with `go build`. Two scripts, `build_linux.sh` and `build_windows.sh`,
|
||||
are supplied which will build all the plugins for their respective OS.
|
||||
|
||||
## Contribution workflow
|
||||
|
||||
This is a rough outline of how to prepare a contribution:
|
||||
@ -61,14 +66,17 @@ git clone https://github.com/containernetworking/plugins
|
||||
Next, boot the virtual machine and SSH in to run the tests:
|
||||
|
||||
```bash
|
||||
cd ~/workspace/plugins
|
||||
vagrant up
|
||||
vagrant ssh
|
||||
# you're now in a shell in a virtual machine
|
||||
sudo su
|
||||
go get github.com/onsi/ginkgo/ginkgo
|
||||
go install github.com/containernetworking/cni/cnitool
|
||||
cd /go/src/github.com/containernetworking/plugins
|
||||
|
||||
# to run the full test suite
|
||||
./test.sh
|
||||
./test_linux.sh
|
||||
|
||||
# to focus on a particular test suite
|
||||
cd plugins/main/loopback
|
||||
|
36
DCO
Normal file
36
DCO
Normal file
@ -0,0 +1,36 @@
|
||||
Developer Certificate of Origin
|
||||
Version 1.1
|
||||
|
||||
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
|
||||
660 York Street, Suite 102,
|
||||
San Francisco, CA 94110 USA
|
||||
|
||||
Everyone is permitted to copy and distribute verbatim copies of this
|
||||
license document, but changing it is not allowed.
|
||||
|
||||
|
||||
Developer's Certificate of Origin 1.1
|
||||
|
||||
By making a contribution to this project, I certify that:
|
||||
|
||||
(a) The contribution was created in whole or in part by me and I
|
||||
have the right to submit it under the open source license
|
||||
indicated in the file; or
|
||||
|
||||
(b) The contribution is based upon previous work that, to the best
|
||||
of my knowledge, is covered under an appropriate open source
|
||||
license and I have the right under that license to submit that
|
||||
work with modifications, whether created in whole or in part
|
||||
by me, under the same open source license (unless I am
|
||||
permitted to submit under a different license), as indicated
|
||||
in the file; or
|
||||
|
||||
(c) The contribution was provided directly to me by some other
|
||||
person who certified (a), (b) or (c) and I have not modified
|
||||
it.
|
||||
|
||||
(d) I understand and agree that this project and the contribution
|
||||
are public and that a record of the contribution (including all
|
||||
personal information I submit with it, including my sign-off) is
|
||||
maintained indefinitely and may be redistributed consistent with
|
||||
this project or the open source license(s) involved.
|
238
Godeps/Godeps.json
generated
238
Godeps/Godeps.json
generated
@ -1,238 +0,0 @@
|
||||
{
|
||||
"ImportPath": "github.com/containernetworking/plugins",
|
||||
"GoVersion": "go1.7",
|
||||
"GodepVersion": "v79",
|
||||
"Packages": [
|
||||
"./..."
|
||||
],
|
||||
"Deps": [
|
||||
{
|
||||
"ImportPath": "github.com/containernetworking/cni/libcni",
|
||||
"Comment": "v0.6.0-rc1",
|
||||
"Rev": "a2da8f8d7fd8e6dc25f336408a8ac86f050fbd88"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/containernetworking/cni/pkg/invoke",
|
||||
"Comment": "v0.6.0-rc1",
|
||||
"Rev": "a2da8f8d7fd8e6dc25f336408a8ac86f050fbd88"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/containernetworking/cni/pkg/skel",
|
||||
"Comment": "v0.6.0-rc1",
|
||||
"Rev": "a2da8f8d7fd8e6dc25f336408a8ac86f050fbd88"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/containernetworking/cni/pkg/types",
|
||||
"Comment": "v0.6.0-rc1",
|
||||
"Rev": "a2da8f8d7fd8e6dc25f336408a8ac86f050fbd88"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/containernetworking/cni/pkg/types/020",
|
||||
"Comment": "v0.6.0-rc1",
|
||||
"Rev": "a2da8f8d7fd8e6dc25f336408a8ac86f050fbd88"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/containernetworking/cni/pkg/types/current",
|
||||
"Comment": "v0.6.0-rc1",
|
||||
"Rev": "a2da8f8d7fd8e6dc25f336408a8ac86f050fbd88"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/containernetworking/cni/pkg/version",
|
||||
"Comment": "v0.6.0-rc1",
|
||||
"Rev": "a2da8f8d7fd8e6dc25f336408a8ac86f050fbd88"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/go-iptables/iptables",
|
||||
"Comment": "v0.2.0",
|
||||
"Rev": "259c8e6a4275d497442c721fa52204d7a58bde8b"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/go-systemd/activation",
|
||||
"Comment": "v2-53-g2688e91",
|
||||
"Rev": "2688e91251d9d8e404e86dd8f096e23b2f086958"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/d2g/dhcp4",
|
||||
"Rev": "f0e4d29ff0231dce36e250b2ed9ff08412584bca"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/d2g/dhcp4client",
|
||||
"Rev": "bed07e1bc5b85f69c6f0fd73393aa35ec68ed892"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/j-keck/arping",
|
||||
"Rev": "2cf9dc699c5640a7e2c81403a44127bf28033600"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/mattn/go-shellwords",
|
||||
"Comment": "v1.0.3",
|
||||
"Rev": "02e3cf038dcea8290e44424da473dd12be796a8a"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/onsi/ginkgo",
|
||||
"Comment": "v1.2.0-29-g7f8ab55",
|
||||
"Rev": "7f8ab55aaf3b86885aa55b762e803744d1674700"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/onsi/ginkgo/config",
|
||||
"Comment": "v1.2.0-29-g7f8ab55",
|
||||
"Rev": "7f8ab55aaf3b86885aa55b762e803744d1674700"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/onsi/ginkgo/extensions/table",
|
||||
"Comment": "v1.2.0-29-g7f8ab55",
|
||||
"Rev": "7f8ab55aaf3b86885aa55b762e803744d1674700"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/onsi/ginkgo/internal/codelocation",
|
||||
"Comment": "v1.2.0-29-g7f8ab55",
|
||||
"Rev": "7f8ab55aaf3b86885aa55b762e803744d1674700"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/onsi/ginkgo/internal/containernode",
|
||||
"Comment": "v1.2.0-29-g7f8ab55",
|
||||
"Rev": "7f8ab55aaf3b86885aa55b762e803744d1674700"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/onsi/ginkgo/internal/failer",
|
||||
"Comment": "v1.2.0-29-g7f8ab55",
|
||||
"Rev": "7f8ab55aaf3b86885aa55b762e803744d1674700"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/onsi/ginkgo/internal/leafnodes",
|
||||
"Comment": "v1.2.0-29-g7f8ab55",
|
||||
"Rev": "7f8ab55aaf3b86885aa55b762e803744d1674700"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/onsi/ginkgo/internal/remote",
|
||||
"Comment": "v1.2.0-29-g7f8ab55",
|
||||
"Rev": "7f8ab55aaf3b86885aa55b762e803744d1674700"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/onsi/ginkgo/internal/spec",
|
||||
"Comment": "v1.2.0-29-g7f8ab55",
|
||||
"Rev": "7f8ab55aaf3b86885aa55b762e803744d1674700"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/onsi/ginkgo/internal/specrunner",
|
||||
"Comment": "v1.2.0-29-g7f8ab55",
|
||||
"Rev": "7f8ab55aaf3b86885aa55b762e803744d1674700"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/onsi/ginkgo/internal/suite",
|
||||
"Comment": "v1.2.0-29-g7f8ab55",
|
||||
"Rev": "7f8ab55aaf3b86885aa55b762e803744d1674700"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/onsi/ginkgo/internal/testingtproxy",
|
||||
"Comment": "v1.2.0-29-g7f8ab55",
|
||||
"Rev": "7f8ab55aaf3b86885aa55b762e803744d1674700"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/onsi/ginkgo/internal/writer",
|
||||
"Comment": "v1.2.0-29-g7f8ab55",
|
||||
"Rev": "7f8ab55aaf3b86885aa55b762e803744d1674700"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/onsi/ginkgo/reporters",
|
||||
"Comment": "v1.2.0-29-g7f8ab55",
|
||||
"Rev": "7f8ab55aaf3b86885aa55b762e803744d1674700"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/onsi/ginkgo/reporters/stenographer",
|
||||
"Comment": "v1.2.0-29-g7f8ab55",
|
||||
"Rev": "7f8ab55aaf3b86885aa55b762e803744d1674700"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/onsi/ginkgo/types",
|
||||
"Comment": "v1.2.0-29-g7f8ab55",
|
||||
"Rev": "7f8ab55aaf3b86885aa55b762e803744d1674700"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/onsi/gomega",
|
||||
"Comment": "v1.0-71-g2152b45",
|
||||
"Rev": "2152b45fa28a361beba9aab0885972323a444e28"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/onsi/gomega/format",
|
||||
"Comment": "v1.0-71-g2152b45",
|
||||
"Rev": "2152b45fa28a361beba9aab0885972323a444e28"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/onsi/gomega/gbytes",
|
||||
"Comment": "v1.0-71-g2152b45",
|
||||
"Rev": "2152b45fa28a361beba9aab0885972323a444e28"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/onsi/gomega/gexec",
|
||||
"Comment": "v1.0-71-g2152b45",
|
||||
"Rev": "2152b45fa28a361beba9aab0885972323a444e28"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/onsi/gomega/internal/assertion",
|
||||
"Comment": "v1.0-71-g2152b45",
|
||||
"Rev": "2152b45fa28a361beba9aab0885972323a444e28"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/onsi/gomega/internal/asyncassertion",
|
||||
"Comment": "v1.0-71-g2152b45",
|
||||
"Rev": "2152b45fa28a361beba9aab0885972323a444e28"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/onsi/gomega/internal/oraclematcher",
|
||||
"Comment": "v1.0-71-g2152b45",
|
||||
"Rev": "2152b45fa28a361beba9aab0885972323a444e28"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/onsi/gomega/internal/testingtsupport",
|
||||
"Comment": "v1.0-71-g2152b45",
|
||||
"Rev": "2152b45fa28a361beba9aab0885972323a444e28"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/onsi/gomega/matchers",
|
||||
"Comment": "v1.0-71-g2152b45",
|
||||
"Rev": "2152b45fa28a361beba9aab0885972323a444e28"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/onsi/gomega/matchers/support/goraph/bipartitegraph",
|
||||
"Comment": "v1.0-71-g2152b45",
|
||||
"Rev": "2152b45fa28a361beba9aab0885972323a444e28"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/onsi/gomega/matchers/support/goraph/edge",
|
||||
"Comment": "v1.0-71-g2152b45",
|
||||
"Rev": "2152b45fa28a361beba9aab0885972323a444e28"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/onsi/gomega/matchers/support/goraph/node",
|
||||
"Comment": "v1.0-71-g2152b45",
|
||||
"Rev": "2152b45fa28a361beba9aab0885972323a444e28"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/onsi/gomega/matchers/support/goraph/util",
|
||||
"Comment": "v1.0-71-g2152b45",
|
||||
"Rev": "2152b45fa28a361beba9aab0885972323a444e28"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/onsi/gomega/types",
|
||||
"Comment": "v1.0-71-g2152b45",
|
||||
"Rev": "2152b45fa28a361beba9aab0885972323a444e28"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/vishvananda/netlink",
|
||||
"Rev": "6e453822d85ef5721799774b654d4d02fed62afb"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/vishvananda/netlink/nl",
|
||||
"Rev": "6e453822d85ef5721799774b654d4d02fed62afb"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/vishvananda/netns",
|
||||
"Rev": "54f0e4339ce73702a0607f49922aaa1e749b418d"
|
||||
},
|
||||
{
|
||||
"ImportPath": "golang.org/x/sys/unix",
|
||||
"Rev": "076b546753157f758b316e59bcb51e6807c04057"
|
||||
}
|
||||
]
|
||||
}
|
5
Godeps/Readme
generated
5
Godeps/Readme
generated
@ -1,5 +0,0 @@
|
||||
This directory tree is generated automatically by godep.
|
||||
|
||||
Please do not edit.
|
||||
|
||||
See https://github.com/tools/godep for more information.
|
10
OWNERS.md
Normal file
10
OWNERS.md
Normal file
@ -0,0 +1,10 @@
|
||||
# Owners
|
||||
This is the official list of the CNI network plugins owners:
|
||||
- Bruce Ma <brucema19901024@gmail.com> (@mars1024)
|
||||
- Bryan Boreham <bryan@weave.works> (@bboreham)
|
||||
- Casey Callendrello <cdc@redhat.com> (@squeed)
|
||||
- Dan Williams <dcbw@redhat.com> (@dcbw)
|
||||
- Gabe Rosenhouse <grosenhouse@pivotal.io> (@rosenhouse)
|
||||
- Matt Dupre <matt@tigera.io> (@matthewdupre)
|
||||
- Michael Cambria <mcambria@redhat.com> (@mccv1r0)
|
||||
- Piotr Skarmuk <piotr.skarmuk@gmail.com> (@jellonek)
|
31
README.md
31
README.md
@ -1,25 +1,42 @@
|
||||
[](https://travis-ci.org/containernetworking/plugins)
|
||||
|
||||
# plugins
|
||||
Some CNI network plugins, maintained by the containernetworking team. For more information, see the individual READMEs.
|
||||
Some CNI network plugins, maintained by the containernetworking team. For more information, see the [CNI website](https://www.cni.dev).
|
||||
|
||||
Read [CONTRIBUTING](CONTRIBUTING.md) for build and test instructions.
|
||||
|
||||
## Plugins supplied:
|
||||
### Main: interface-creating
|
||||
* `bridge`: Creates a bridge, adds the host and the container to it.
|
||||
* `ipvlan`: Adds an [ipvlan](https://www.kernel.org/doc/Documentation/networking/ipvlan.txt) interface in the container
|
||||
* `loopback`: Creates a loopback interface
|
||||
* `macvlan`: Creates a new MAC address, forwards all traffic to that to the container
|
||||
* `ipvlan`: Adds an [ipvlan](https://www.kernel.org/doc/Documentation/networking/ipvlan.txt) interface in the container.
|
||||
* `loopback`: Set the state of loopback interface to up.
|
||||
* `macvlan`: Creates a new MAC address, forwards all traffic to that to the container.
|
||||
* `ptp`: Creates a veth pair.
|
||||
* `vlan`: Allocates a vlan device.
|
||||
|
||||
* `host-device`: Move an already-existing device into a container.
|
||||
#### Windows: windows specific
|
||||
* `win-bridge`: Creates a bridge, adds the host and the container to it.
|
||||
* `win-overlay`: Creates an overlay interface to the container.
|
||||
### IPAM: IP address allocation
|
||||
* `dhcp`: Runs a daemon on the host to make DHCP requests on behalf of the container
|
||||
* `host-local`: maintains a local database of allocated IPs
|
||||
* `host-local`: Maintains a local database of allocated IPs
|
||||
* `static`: Allocate a static IPv4/IPv6 addresses to container and it's useful in debugging purpose.
|
||||
|
||||
### Meta: other plugins
|
||||
* `flannel`: generates an interface corresponding to a flannel config file
|
||||
* `flannel`: Generates an interface corresponding to a flannel config file
|
||||
* `tuning`: Tweaks sysctl parameters of an existing interface
|
||||
* `portmap`: An iptables-based portmapping plugin. Maps ports from the host's address space to the container.
|
||||
* `bandwidth`: Allows bandwidth-limiting through use of traffic control tbf (ingress/egress).
|
||||
* `sbr`: A plugin that configures source based routing for an interface (from which it is chained).
|
||||
* `firewall`: A firewall plugin which uses iptables or firewalld to add rules to allow traffic to/from the container.
|
||||
|
||||
### Sample
|
||||
The sample plugin provides an example for building your own plugin.
|
||||
|
||||
## Contact
|
||||
|
||||
For any questions about CNI, please reach out via:
|
||||
- Email: [cni-dev](https://groups.google.com/forum/#!forum/cni-dev)
|
||||
- Slack: #cni on the [CNCF slack](https://slack.cncf.io/).
|
||||
|
||||
If you have a _security_ issue to report, please do so privately to the email addresses listed in the [OWNERS](OWNERS.md) file.
|
||||
|
21
Vagrantfile
vendored
21
Vagrantfile
vendored
@ -1,21 +0,0 @@
|
||||
# -*- mode: ruby -*-
|
||||
# vi: set ft=ruby :
|
||||
|
||||
Vagrant.configure(2) do |config|
|
||||
config.vm.box = "bento/ubuntu-16.04"
|
||||
|
||||
config.vm.synced_folder "..", "/go/src/github.com/containernetworking"
|
||||
|
||||
config.vm.provision "shell", inline: <<-SHELL
|
||||
set -e -x -u
|
||||
|
||||
apt-get update -y || (sleep 40 && apt-get update -y)
|
||||
apt-get install -y git
|
||||
|
||||
wget -qO- https://storage.googleapis.com/golang/go1.8.3.linux-amd64.tar.gz | tar -C /usr/local -xz
|
||||
|
||||
echo 'export GOPATH=/go' >> /root/.bashrc
|
||||
echo 'export PATH=$PATH:/usr/local/go/bin:$GOPATH/bin' >> /root/.bashrc
|
||||
cd /go/src/github.com/containernetworking/plugins
|
||||
SHELL
|
||||
end
|
35
build.sh
35
build.sh
@ -1,35 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
if [ "$(uname)" == "Darwin" ]; then
|
||||
export GOOS=linux
|
||||
fi
|
||||
|
||||
ORG_PATH="github.com/containernetworking"
|
||||
export REPO_PATH="${ORG_PATH}/plugins"
|
||||
|
||||
if [ ! -h gopath/src/${REPO_PATH} ]; then
|
||||
mkdir -p gopath/src/${ORG_PATH}
|
||||
ln -s ../../../.. gopath/src/${REPO_PATH} || exit 255
|
||||
fi
|
||||
|
||||
export GO15VENDOREXPERIMENT=1
|
||||
export GOPATH=${PWD}/gopath
|
||||
|
||||
mkdir -p "${PWD}/bin"
|
||||
|
||||
echo "Building plugins"
|
||||
PLUGINS="plugins/meta/* plugins/main/* plugins/ipam/* plugins/sample"
|
||||
for d in $PLUGINS; do
|
||||
if [ -d "$d" ]; then
|
||||
plugin="$(basename "$d")"
|
||||
echo " $plugin"
|
||||
# use go install so we don't duplicate work
|
||||
if [ -n "$FASTBUILD" ]
|
||||
then
|
||||
GOBIN=${PWD}/bin go install -pkgdir $GOPATH/pkg "$@" $REPO_PATH/$d
|
||||
else
|
||||
go build -o "${PWD}/bin/$plugin" -pkgdir "$GOPATH/pkg" "$@" "$REPO_PATH/$d"
|
||||
fi
|
||||
fi
|
||||
done
|
23
build_linux.sh
Executable file
23
build_linux.sh
Executable file
@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
if [ "$(uname)" == "Darwin" ]; then
|
||||
export GOOS="${GOOS:-linux}"
|
||||
fi
|
||||
|
||||
export GOFLAGS="${GOFLAGS} -mod=vendor"
|
||||
|
||||
mkdir -p "${PWD}/bin"
|
||||
|
||||
echo "Building plugins ${GOOS}"
|
||||
PLUGINS="plugins/meta/* plugins/main/* plugins/ipam/*"
|
||||
for d in $PLUGINS; do
|
||||
if [ -d "$d" ]; then
|
||||
plugin="$(basename "$d")"
|
||||
if [ "${plugin}" != "windows" ]; then
|
||||
echo " $plugin"
|
||||
${GO:-go} build -o "${PWD}/bin/$plugin" "$@" ./"$d"
|
||||
fi
|
||||
fi
|
||||
done
|
15
build_windows.sh
Executable file
15
build_windows.sh
Executable file
@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
export GO="${GO:-go}"
|
||||
export GOOS=windows
|
||||
export GOFLAGS="${GOFLAGS} -mod=vendor"
|
||||
echo "$GOFLAGS"
|
||||
|
||||
PLUGINS=$(cat plugins/windows_only.txt | dos2unix )
|
||||
for d in $PLUGINS; do
|
||||
plugin="$(basename "$d").exe"
|
||||
echo "building $plugin"
|
||||
$GO build -o "${PWD}/bin/$plugin" "$@" ./"${d}"
|
||||
done
|
29
go.mod
Normal file
29
go.mod
Normal file
@ -0,0 +1,29 @@
|
||||
module github.com/containernetworking/plugins
|
||||
|
||||
go 1.14
|
||||
|
||||
require (
|
||||
github.com/Microsoft/go-winio v0.4.11 // indirect
|
||||
github.com/Microsoft/hcsshim v0.8.6
|
||||
github.com/alexflint/go-filemutex v0.0.0-20171022225611-72bdc8eae2ae
|
||||
github.com/buger/jsonparser v0.0.0-20180808090653-f4dd9f5a6b44
|
||||
github.com/containernetworking/cni v0.8.0
|
||||
github.com/coreos/go-iptables v0.4.5
|
||||
github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7
|
||||
github.com/d2g/dhcp4 v0.0.0-20170904100407-a1d1b6c41b1c
|
||||
github.com/d2g/dhcp4client v1.0.0
|
||||
github.com/d2g/dhcp4server v0.0.0-20181031114812-7d4a0a7f59a5
|
||||
github.com/d2g/hardwareaddr v0.0.0-20190221164911-e7d9fbe030e4 // indirect
|
||||
github.com/godbus/dbus v0.0.0-20180201030542-885f9cc04c9c
|
||||
github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56
|
||||
github.com/mattn/go-shellwords v1.0.3
|
||||
github.com/onsi/ginkgo v1.12.1
|
||||
github.com/onsi/gomega v1.10.3
|
||||
github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8
|
||||
github.com/sirupsen/logrus v1.0.6 // indirect
|
||||
github.com/stretchr/testify v1.3.0 // indirect
|
||||
github.com/vishvananda/netlink v1.1.1-0.20201029203352-d40f9887b852
|
||||
golang.org/x/sys v0.0.0-20201117170446-d9b008d0a637
|
||||
gopkg.in/airbrake/gobrake.v2 v2.0.9 // indirect
|
||||
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2 // indirect
|
||||
)
|
109
go.sum
Normal file
109
go.sum
Normal file
@ -0,0 +1,109 @@
|
||||
github.com/Microsoft/go-winio v0.4.11 h1:zoIOcVf0xPN1tnMVbTtEdI+P8OofVk3NObnwOQ6nK2Q=
|
||||
github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
|
||||
github.com/Microsoft/hcsshim v0.8.6 h1:ZfF0+zZeYdzMIVMZHKtDKJvLHj76XCuVae/jNkjj0IA=
|
||||
github.com/Microsoft/hcsshim v0.8.6/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg=
|
||||
github.com/alexflint/go-filemutex v0.0.0-20171022225611-72bdc8eae2ae h1:AMzIhMUqU3jMrZiTuW0zkYeKlKDAFD+DG20IoO421/Y=
|
||||
github.com/alexflint/go-filemutex v0.0.0-20171022225611-72bdc8eae2ae/go.mod h1:CgnQgUtFrFz9mxFNtED3jI5tLDjKlOM+oUF/sTk6ps0=
|
||||
github.com/buger/jsonparser v0.0.0-20180808090653-f4dd9f5a6b44 h1:y853v6rXx+zefEcjET3JuKAqvhj+FKflQijjeaSv2iA=
|
||||
github.com/buger/jsonparser v0.0.0-20180808090653-f4dd9f5a6b44/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
|
||||
github.com/containernetworking/cni v0.8.0 h1:BT9lpgGoH4jw3lFC7Odz2prU5ruiYKcgAjMCbgybcKI=
|
||||
github.com/containernetworking/cni v0.8.0/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY=
|
||||
github.com/coreos/go-iptables v0.4.5 h1:DpHb9vJrZQEFMcVLFKAAGMUVX0XoRC0ptCthinRYm38=
|
||||
github.com/coreos/go-iptables v0.4.5/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU=
|
||||
github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7 h1:u9SHYsPQNyt5tgDm3YN7+9dYrpK96E5wFilTFWIDZOM=
|
||||
github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/d2g/dhcp4 v0.0.0-20170904100407-a1d1b6c41b1c h1:Xo2rK1pzOm0jO6abTPIQwbAmqBIOj132otexc1mmzFc=
|
||||
github.com/d2g/dhcp4 v0.0.0-20170904100407-a1d1b6c41b1c/go.mod h1:Ct2BUK8SB0YC1SMSibvLzxjeJLnrYEVLULFNiHY9YfQ=
|
||||
github.com/d2g/dhcp4client v1.0.0 h1:suYBsYZIkSlUMEz4TAYCczKf62IA2UWC+O8+KtdOhCo=
|
||||
github.com/d2g/dhcp4client v1.0.0/go.mod h1:j0hNfjhrt2SxUOw55nL0ATM/z4Yt3t2Kd1mW34z5W5s=
|
||||
github.com/d2g/dhcp4server v0.0.0-20181031114812-7d4a0a7f59a5 h1:+CpLbZIeUn94m02LdEKPcgErLJ347NUwxPKs5u8ieiY=
|
||||
github.com/d2g/dhcp4server v0.0.0-20181031114812-7d4a0a7f59a5/go.mod h1:Eo87+Kg/IX2hfWJfwxMzLyuSZyxSoAug2nGa1G2QAi8=
|
||||
github.com/d2g/hardwareaddr v0.0.0-20190221164911-e7d9fbe030e4 h1:itqmmf1PFpC4n5JW+j4BU7X4MTfVurhYRTjODoPb2Y8=
|
||||
github.com/d2g/hardwareaddr v0.0.0-20190221164911-e7d9fbe030e4/go.mod h1:bMl4RjIciD2oAxI7DmWRx6gbeqrkoLqv3MV0vzNad+I=
|
||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/godbus/dbus v0.0.0-20180201030542-885f9cc04c9c h1:RBUpb2b14UnmRHNd2uHz20ZHLDK+SW5Us/vWF5IHRaY=
|
||||
github.com/godbus/dbus v0.0.0-20180201030542-885f9cc04c9c/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56 h1:742eGXur0715JMq73aD95/FU0XpVKXqNuTnEfXsLOYQ=
|
||||
github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56/go.mod h1:ymszkNOg6tORTn+6F6j+Jc8TOr5osrynvN6ivFWZ2GA=
|
||||
github.com/mattn/go-shellwords v1.0.3 h1:K/VxK7SZ+cvuPgFSLKi5QPI9Vr/ipOf4C1gN+ntueUk=
|
||||
github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
|
||||
github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.12.1 h1:mFwc4LvZ0xpSvDZ3E+k8Yte0hLOMxXUlP+yXtJqkYfQ=
|
||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.10.3 h1:gph6h/qe9GSUw1NhH1gp+qb+h8rXD8Cy60Z32Qw3ELA=
|
||||
github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8 h1:2c1EFnZHIPCW8qKWgHMH/fX2PkSabFc5mrVzfUNdg5U=
|
||||
github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4=
|
||||
github.com/sirupsen/logrus v1.0.6 h1:hcP1GmhGigz/O7h1WVUM5KklBp1JoNS9FggWKdj/j3s=
|
||||
github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/vishvananda/netlink v1.1.1-0.20201029203352-d40f9887b852 h1:cPXZWzzG0NllBLdjWoD1nDfaqu98YMv+OneaKc8sPOA=
|
||||
github.com/vishvananda/netlink v1.1.1-0.20201029203352-d40f9887b852/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho=
|
||||
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae h1:4hwBBUfQCFe3Cym0ZtKyq7L16eZUtYKs+BaHDN6mAns=
|
||||
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0 h1:wBouT66WTYFXdxfVdz9sVWARVd/2vfGcmI45D2gj45M=
|
||||
golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201117170446-d9b008d0a637 h1:O5hKNaGxIT4A8OTMnuh6UpmBdI3SAPxlZ3g0olDrJVM=
|
||||
golang.org/x/sys v0.0.0-20201117170446-d9b008d0a637/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
gopkg.in/airbrake/gobrake.v2 v2.0.9 h1:7z2uVWwn7oVeeugY1DtlPAy5H+KYgB1KeKTnqjNatLo=
|
||||
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2 h1:OAj3g0cR6Dx/R07QgQe8wkA9RNjB2u4i700xBkIT4e0=
|
||||
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
269
integration/integration_linux_test.go
Normal file
269
integration/integration_linux_test.go
Normal file
@ -0,0 +1,269 @@
|
||||
// Copyright 2018 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 integration_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
|
||||
"bytes"
|
||||
"io"
|
||||
"net"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/onsi/gomega/gbytes"
|
||||
"github.com/onsi/gomega/gexec"
|
||||
)
|
||||
|
||||
var _ = Describe("Basic PTP using cnitool", func() {
|
||||
var (
|
||||
cnitoolBin string
|
||||
cniPath string
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
var err error
|
||||
cniPath, err = filepath.Abs("../bin")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
cnitoolBin, err = exec.LookPath("cnitool")
|
||||
Expect(err).NotTo(HaveOccurred(), "expected to find cnitool in your PATH")
|
||||
})
|
||||
|
||||
Context("basic cases", func() {
|
||||
var (
|
||||
env TestEnv
|
||||
hostNS Namespace
|
||||
contNS Namespace
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
var err error
|
||||
|
||||
netConfPath, err := filepath.Abs("./testdata")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
env = TestEnv([]string{
|
||||
"CNI_PATH=" + cniPath,
|
||||
"NETCONFPATH=" + netConfPath,
|
||||
"PATH=" + os.Getenv("PATH"),
|
||||
})
|
||||
|
||||
hostNS = Namespace(fmt.Sprintf("cni-test-host-%x", rand.Int31()))
|
||||
hostNS.Add()
|
||||
|
||||
contNS = Namespace(fmt.Sprintf("cni-test-cont-%x", rand.Int31()))
|
||||
contNS.Add()
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
contNS.Del()
|
||||
hostNS.Del()
|
||||
})
|
||||
|
||||
basicAssertion := func(netName, expectedIPPrefix string) {
|
||||
env.runInNS(hostNS, cnitoolBin, "add", netName, contNS.LongName())
|
||||
|
||||
addrOutput := env.runInNS(contNS, "ip", "addr")
|
||||
Expect(addrOutput).To(ContainSubstring(expectedIPPrefix))
|
||||
|
||||
env.runInNS(hostNS, cnitoolBin, "del", netName, contNS.LongName())
|
||||
}
|
||||
|
||||
It("supports basic network add and del operations", func() {
|
||||
basicAssertion("basic-ptp", "10.1.2.")
|
||||
})
|
||||
|
||||
It("supports add and del with ptp + bandwidth", func() {
|
||||
basicAssertion("chained-ptp-bandwidth", "10.9.2.")
|
||||
})
|
||||
})
|
||||
|
||||
Context("when the bandwidth plugin is chained with a plugin that returns multiple adapters", func() {
|
||||
var (
|
||||
hostNS Namespace
|
||||
contNS1 Namespace
|
||||
contNS2 Namespace
|
||||
basicBridgeEnv TestEnv
|
||||
chainedBridgeBandwidthEnv TestEnv
|
||||
chainedBridgeBandwidthSession, basicBridgeSession *gexec.Session
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
hostNS = Namespace(fmt.Sprintf("cni-test-host-%x", rand.Int31()))
|
||||
hostNS.Add()
|
||||
|
||||
contNS1 = Namespace(fmt.Sprintf("cni-test-cont1-%x", rand.Int31()))
|
||||
contNS1.Add()
|
||||
|
||||
contNS2 = Namespace(fmt.Sprintf("cni-test-cont2-%x", rand.Int31()))
|
||||
contNS2.Add()
|
||||
|
||||
basicBridgeNetConfPath, err := filepath.Abs("./testdata/basic-bridge")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
basicBridgeEnv = TestEnv([]string{
|
||||
"CNI_PATH=" + cniPath,
|
||||
"NETCONFPATH=" + basicBridgeNetConfPath,
|
||||
"PATH=" + os.Getenv("PATH"),
|
||||
})
|
||||
|
||||
chainedBridgeBandwidthNetConfPath, err := filepath.Abs("./testdata/chained-bridge-bandwidth")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
chainedBridgeBandwidthEnv = TestEnv([]string{
|
||||
"CNI_PATH=" + cniPath,
|
||||
"NETCONFPATH=" + chainedBridgeBandwidthNetConfPath,
|
||||
"PATH=" + os.Getenv("PATH"),
|
||||
})
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
if chainedBridgeBandwidthSession != nil {
|
||||
chainedBridgeBandwidthSession.Kill()
|
||||
}
|
||||
if basicBridgeSession != nil {
|
||||
basicBridgeSession.Kill()
|
||||
}
|
||||
|
||||
chainedBridgeBandwidthEnv.runInNS(hostNS, cnitoolBin, "del", "network-chain-test", contNS1.LongName())
|
||||
basicBridgeEnv.runInNS(hostNS, cnitoolBin, "del", "network-chain-test", contNS2.LongName())
|
||||
})
|
||||
|
||||
Measure("limits traffic only on the restricted bandwith veth device", func(b Benchmarker) {
|
||||
ipRegexp := regexp.MustCompile("10\\.1[12]\\.2\\.\\d{1,3}")
|
||||
|
||||
By(fmt.Sprintf("adding %s to %s\n\n", "chained-bridge-bandwidth", contNS1.ShortName()))
|
||||
chainedBridgeBandwidthEnv.runInNS(hostNS, cnitoolBin, "add", "network-chain-test", contNS1.LongName())
|
||||
chainedBridgeIP := ipRegexp.FindString(chainedBridgeBandwidthEnv.runInNS(contNS1, "ip", "addr"))
|
||||
Expect(chainedBridgeIP).To(ContainSubstring("10.12.2."))
|
||||
|
||||
By(fmt.Sprintf("adding %s to %s\n\n", "basic-bridge", contNS2.ShortName()))
|
||||
basicBridgeEnv.runInNS(hostNS, cnitoolBin, "add", "network-chain-test", contNS2.LongName())
|
||||
basicBridgeIP := ipRegexp.FindString(basicBridgeEnv.runInNS(contNS2, "ip", "addr"))
|
||||
Expect(basicBridgeIP).To(ContainSubstring("10.11.2."))
|
||||
|
||||
var chainedBridgeBandwidthPort, basicBridgePort int
|
||||
var err error
|
||||
|
||||
By(fmt.Sprintf("starting echo server in %s\n\n", contNS1.ShortName()))
|
||||
chainedBridgeBandwidthPort, chainedBridgeBandwidthSession, err = startEchoServerInNamespace(contNS1)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
By(fmt.Sprintf("starting echo server in %s\n\n", contNS2.ShortName()))
|
||||
basicBridgePort, basicBridgeSession, err = startEchoServerInNamespace(contNS2)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
packetInBytes := 20000 // The shaper needs to 'warm'. Send enough to cause it to throttle,
|
||||
// balanced by run time.
|
||||
|
||||
By(fmt.Sprintf("sending tcp traffic to the chained, bridged, traffic shaped container on ip address '%s:%d'\n\n", chainedBridgeIP, chainedBridgeBandwidthPort))
|
||||
runtimeWithLimit := b.Time("with chained bridge and bandwidth plugins", func() {
|
||||
makeTcpClientInNS(hostNS.ShortName(), chainedBridgeIP, chainedBridgeBandwidthPort, packetInBytes)
|
||||
})
|
||||
|
||||
By(fmt.Sprintf("sending tcp traffic to the basic bridged container on ip address '%s:%d'\n\n", basicBridgeIP, basicBridgePort))
|
||||
runtimeWithoutLimit := b.Time("with basic bridged plugin", func() {
|
||||
makeTcpClientInNS(hostNS.ShortName(), basicBridgeIP, basicBridgePort, packetInBytes)
|
||||
})
|
||||
|
||||
Expect(runtimeWithLimit).To(BeNumerically(">", runtimeWithoutLimit+1000*time.Millisecond))
|
||||
}, 1)
|
||||
})
|
||||
})
|
||||
|
||||
type TestEnv []string
|
||||
|
||||
func (e TestEnv) run(bin string, args ...string) string {
|
||||
cmd := exec.Command(bin, args...)
|
||||
cmd.Env = e
|
||||
session, err := gexec.Start(cmd, GinkgoWriter, GinkgoWriter)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Eventually(session, "5s").Should(gexec.Exit(0))
|
||||
return string(session.Out.Contents())
|
||||
}
|
||||
|
||||
func (e TestEnv) runInNS(nsShortName Namespace, bin string, args ...string) string {
|
||||
a := append([]string{"netns", "exec", string(nsShortName), bin}, args...)
|
||||
return e.run("ip", a...)
|
||||
}
|
||||
|
||||
type Namespace string
|
||||
|
||||
func (n Namespace) LongName() string {
|
||||
return fmt.Sprintf("/var/run/netns/%s", n)
|
||||
}
|
||||
|
||||
func (n Namespace) ShortName() string {
|
||||
return string(n)
|
||||
}
|
||||
|
||||
func (n Namespace) Add() {
|
||||
(TestEnv{}).run("ip", "netns", "add", string(n))
|
||||
}
|
||||
|
||||
func (n Namespace) Del() {
|
||||
(TestEnv{}).run("ip", "netns", "del", string(n))
|
||||
}
|
||||
|
||||
func makeTcpClientInNS(netns string, address string, port int, numBytes int) {
|
||||
payload := bytes.Repeat([]byte{'a'}, numBytes)
|
||||
message := string(payload)
|
||||
|
||||
var cmd *exec.Cmd
|
||||
if netns != "" {
|
||||
netns = filepath.Base(netns)
|
||||
cmd = exec.Command("ip", "netns", "exec", netns, echoClientBinaryPath, "--target", fmt.Sprintf("%s:%d", address, port), "--message", message)
|
||||
} else {
|
||||
cmd = exec.Command(echoClientBinaryPath, "--target", fmt.Sprintf("%s:%d", address, port), "--message", message)
|
||||
}
|
||||
cmd.Stdin = bytes.NewBuffer([]byte(message))
|
||||
cmd.Stderr = GinkgoWriter
|
||||
out, err := cmd.Output()
|
||||
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(string(out)).To(Equal(message))
|
||||
}
|
||||
|
||||
func startEchoServerInNamespace(netNS Namespace) (int, *gexec.Session, error) {
|
||||
session, err := startInNetNS(echoServerBinaryPath, netNS)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// wait for it to print it's address on stdout
|
||||
Eventually(session.Out).Should(gbytes.Say("\n"))
|
||||
_, portString, err := net.SplitHostPort(strings.TrimSpace(string(session.Out.Contents())))
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
port, err := strconv.Atoi(portString)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
go func() {
|
||||
// print out echoserver output to ginkgo to capture any errors that might be occurring.
|
||||
io.Copy(GinkgoWriter, io.MultiReader(session.Out, session.Err))
|
||||
}()
|
||||
|
||||
return port, session, nil
|
||||
}
|
||||
|
||||
func startInNetNS(binPath string, namespace Namespace) (*gexec.Session, error) {
|
||||
cmd := exec.Command("ip", "netns", "exec", namespace.ShortName(), binPath)
|
||||
return gexec.Start(cmd, GinkgoWriter, GinkgoWriter)
|
||||
}
|
46
integration/integration_suite_test.go
Normal file
46
integration/integration_suite_test.go
Normal file
@ -0,0 +1,46 @@
|
||||
// Copyright 2018 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 integration_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/onsi/gomega/gexec"
|
||||
)
|
||||
|
||||
func TestIntegration(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "integration")
|
||||
}
|
||||
|
||||
var echoServerBinaryPath, echoClientBinaryPath string
|
||||
|
||||
var _ = SynchronizedBeforeSuite(func() []byte {
|
||||
serverBinaryPath, err := gexec.Build("github.com/containernetworking/plugins/pkg/testutils/echo/server")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
clientBinaryPath, err := gexec.Build("github.com/containernetworking/plugins/pkg/testutils/echo/client")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
return []byte(strings.Join([]string{serverBinaryPath, clientBinaryPath}, ","))
|
||||
}, func(data []byte) {
|
||||
binaries := strings.Split(string(data), ",")
|
||||
echoServerBinaryPath = binaries[0]
|
||||
echoClientBinaryPath = binaries[1]
|
||||
})
|
||||
|
||||
var _ = SynchronizedAfterSuite(func() {}, func() {
|
||||
gexec.CleanupBuildArtifacts()
|
||||
})
|
12
integration/testdata/basic-bridge/network-chain-test.json
vendored
Normal file
12
integration/testdata/basic-bridge/network-chain-test.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"cniVersion": "0.3.1",
|
||||
"name": "network-chain-test",
|
||||
"type": "bridge",
|
||||
"bridge": "test-bridge-0",
|
||||
"isDefaultGateway": true,
|
||||
"ipam": {
|
||||
"type": "host-local",
|
||||
"subnet": "10.11.2.0/24",
|
||||
"dataDir": "/tmp/foo"
|
||||
}
|
||||
}
|
11
integration/testdata/basic-ptp.json
vendored
Normal file
11
integration/testdata/basic-ptp.json
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"cniVersion": "0.3.0",
|
||||
"name": "basic-ptp",
|
||||
"type": "ptp",
|
||||
"ipMasq": true,
|
||||
"mtu": 512,
|
||||
"ipam": {
|
||||
"type": "host-local",
|
||||
"subnet": "10.1.2.0/24"
|
||||
}
|
||||
}
|
27
integration/testdata/chained-bridge-bandwidth/network-chain-test.conflist
vendored
Normal file
27
integration/testdata/chained-bridge-bandwidth/network-chain-test.conflist
vendored
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"cniVersion": "0.3.1",
|
||||
"name": "network-chain-test",
|
||||
"plugins": [
|
||||
{
|
||||
"type": "bridge",
|
||||
"bridge": "test-bridge-1",
|
||||
"isDefaultGateway": true,
|
||||
"ipam": {
|
||||
"type": "host-local",
|
||||
"subnet": "10.12.2.0/24",
|
||||
"dataDir": "/tmp/bar"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "bandwidth",
|
||||
"runtimeConfig": {
|
||||
"bandWidth": {
|
||||
"ingressRate": 8000,
|
||||
"ingressBurst": 16000,
|
||||
"egressRate": 8000,
|
||||
"egressBurst": 16000
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
26
integration/testdata/chained-ptp-bandwidth.conflist
vendored
Normal file
26
integration/testdata/chained-ptp-bandwidth.conflist
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"cniVersion": "0.3.1",
|
||||
"name": "chained-ptp-bandwidth",
|
||||
"plugins": [
|
||||
{
|
||||
"type": "ptp",
|
||||
"ipMasq": true,
|
||||
"mtu": 512,
|
||||
"ipam": {
|
||||
"type": "host-local",
|
||||
"subnet": "10.9.2.0/24"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "bandwidth",
|
||||
"runtimeConfig": {
|
||||
"bandWidth": {
|
||||
"ingressRate": 800,
|
||||
"ingressBurst": 200,
|
||||
"egressRate": 800,
|
||||
"egressBurst": 200
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
37
pkg/errors/errors.go
Normal file
37
pkg/errors/errors.go
Normal file
@ -0,0 +1,37 @@
|
||||
// Copyright 2020 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 errors
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Annotate is used to add extra context to an existing error. The return will be
|
||||
// a new error which carries error message from both context message and existing error.
|
||||
func Annotate(err error, message string) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("%s: %v", message, err)
|
||||
}
|
||||
|
||||
// Annotatef is used to add extra context with args to an existing error. The return will be
|
||||
// a new error which carries error message from both context message and existing error.
|
||||
func Annotatef(err error, message string, args ...interface{}) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("%s: %v", fmt.Sprintf(message, args...), err)
|
||||
}
|
96
pkg/errors/errors_test.go
Normal file
96
pkg/errors/errors_test.go
Normal file
@ -0,0 +1,96 @@
|
||||
// Copyright 2020 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 errors
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAnnotate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
existingErr error
|
||||
contextMessage string
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
"nil error",
|
||||
nil,
|
||||
"context",
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"normal case",
|
||||
errors.New("existing error"),
|
||||
"context",
|
||||
errors.New("context: existing error"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
if !reflect.DeepEqual(Annotatef(test.existingErr, test.contextMessage), test.expectedErr) {
|
||||
t.Errorf("test case %s fails", test.name)
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnnotatef(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
existingErr error
|
||||
contextMessage string
|
||||
contextArgs []interface{}
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
"nil error",
|
||||
nil,
|
||||
"context",
|
||||
nil,
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"normal case",
|
||||
errors.New("existing error"),
|
||||
"context",
|
||||
nil,
|
||||
errors.New("context: existing error"),
|
||||
},
|
||||
{
|
||||
"normal case with args",
|
||||
errors.New("existing error"),
|
||||
"context %s %d",
|
||||
[]interface{}{
|
||||
"arg",
|
||||
100,
|
||||
},
|
||||
errors.New("context arg 100: existing error"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
if !reflect.DeepEqual(Annotatef(test.existingErr, test.contextMessage, test.contextArgs...), test.expectedErr) {
|
||||
t.Errorf("test case %s fails", test.name)
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
370
pkg/hns/endpoint_windows.go
Normal file
370
pkg/hns/endpoint_windows.go
Normal file
@ -0,0 +1,370 @@
|
||||
// Copyright 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 hns
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/Microsoft/hcsshim"
|
||||
"github.com/Microsoft/hcsshim/hcn"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
"github.com/containernetworking/cni/pkg/types/current"
|
||||
"github.com/containernetworking/plugins/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
pauseContainerNetNS = "none"
|
||||
)
|
||||
|
||||
type EndpointInfo struct {
|
||||
EndpointName string
|
||||
DNS types.DNS
|
||||
NetworkName string
|
||||
NetworkId string
|
||||
Gateway net.IP
|
||||
IpAddress net.IP
|
||||
}
|
||||
|
||||
// GetSandboxContainerID returns the sandbox ID of this pod
|
||||
func GetSandboxContainerID(containerID string, netNs string) string {
|
||||
if len(netNs) != 0 && netNs != pauseContainerNetNS {
|
||||
splits := strings.SplitN(netNs, ":", 2)
|
||||
if len(splits) == 2 {
|
||||
containerID = splits[1]
|
||||
}
|
||||
}
|
||||
|
||||
return containerID
|
||||
}
|
||||
|
||||
// short function so we know when to return "" for a string
|
||||
func GetIpString(ip *net.IP) string {
|
||||
if len(*ip) == 0 {
|
||||
return ""
|
||||
} else {
|
||||
return ip.String()
|
||||
}
|
||||
}
|
||||
|
||||
func GenerateHnsEndpoint(epInfo *EndpointInfo, n *NetConf) (*hcsshim.HNSEndpoint, error) {
|
||||
// run the IPAM plugin and get back the config to apply
|
||||
hnsEndpoint, err := hcsshim.GetHNSEndpointByName(epInfo.EndpointName)
|
||||
if err != nil && !hcsshim.IsNotExist(err) {
|
||||
return nil, errors.Annotatef(err, "failed to get endpoint %q", epInfo.EndpointName)
|
||||
}
|
||||
|
||||
if hnsEndpoint != nil {
|
||||
if hnsEndpoint.VirtualNetwork != epInfo.NetworkId {
|
||||
_, err = hnsEndpoint.Delete()
|
||||
if err != nil {
|
||||
return nil, errors.Annotatef(err, "failed to delete endpoint %s", epInfo.EndpointName)
|
||||
}
|
||||
hnsEndpoint = nil
|
||||
}
|
||||
}
|
||||
|
||||
if n.LoopbackDSR {
|
||||
n.ApplyLoopbackDSR(&epInfo.IpAddress)
|
||||
}
|
||||
if hnsEndpoint == nil {
|
||||
hnsEndpoint = &hcsshim.HNSEndpoint{
|
||||
Name: epInfo.EndpointName,
|
||||
VirtualNetwork: epInfo.NetworkId,
|
||||
DNSServerList: strings.Join(epInfo.DNS.Nameservers, ","),
|
||||
DNSSuffix: strings.Join(epInfo.DNS.Search, ","),
|
||||
GatewayAddress: GetIpString(&epInfo.Gateway),
|
||||
IPAddress: epInfo.IpAddress,
|
||||
Policies: n.MarshalPolicies(),
|
||||
}
|
||||
}
|
||||
return hnsEndpoint, nil
|
||||
}
|
||||
|
||||
func GenerateHcnEndpoint(epInfo *EndpointInfo, n *NetConf) (*hcn.HostComputeEndpoint, error) {
|
||||
// run the IPAM plugin and get back the config to apply
|
||||
hcnEndpoint, err := hcn.GetEndpointByName(epInfo.EndpointName)
|
||||
if err != nil && !hcn.IsNotFoundError(err) {
|
||||
return nil, errors.Annotatef(err, "failed to get endpoint %q", epInfo.EndpointName)
|
||||
}
|
||||
|
||||
if hcnEndpoint != nil {
|
||||
// If the endpont already exists, then we should return error unless
|
||||
// the endpoint is based on a different network then delete
|
||||
// should that fail return error
|
||||
if !strings.EqualFold(hcnEndpoint.HostComputeNetwork, epInfo.NetworkId) {
|
||||
err = hcnEndpoint.Delete()
|
||||
if err != nil {
|
||||
return nil, errors.Annotatef(err, "failed to delete endpoint %s", epInfo.EndpointName)
|
||||
}
|
||||
} else {
|
||||
return nil, fmt.Errorf("endpoint %q already exits", epInfo.EndpointName)
|
||||
}
|
||||
}
|
||||
|
||||
if hcnEndpoint == nil {
|
||||
routes := []hcn.Route{
|
||||
{
|
||||
NextHop: GetIpString(&epInfo.Gateway),
|
||||
DestinationPrefix: GetDefaultDestinationPrefix(&epInfo.Gateway),
|
||||
},
|
||||
}
|
||||
|
||||
hcnDns := hcn.Dns{
|
||||
Search: epInfo.DNS.Search,
|
||||
ServerList: epInfo.DNS.Nameservers,
|
||||
}
|
||||
|
||||
hcnIpConfig := hcn.IpConfig{
|
||||
IpAddress: GetIpString(&epInfo.IpAddress),
|
||||
}
|
||||
ipConfigs := []hcn.IpConfig{hcnIpConfig}
|
||||
|
||||
if n.LoopbackDSR {
|
||||
n.ApplyLoopbackDSR(&epInfo.IpAddress)
|
||||
}
|
||||
hcnEndpoint = &hcn.HostComputeEndpoint{
|
||||
SchemaVersion: hcn.Version{Major: 2},
|
||||
Name: epInfo.EndpointName,
|
||||
HostComputeNetwork: epInfo.NetworkId,
|
||||
Dns: hcnDns,
|
||||
Routes: routes,
|
||||
IpConfigurations: ipConfigs,
|
||||
Policies: func() []hcn.EndpointPolicy {
|
||||
if n.HcnPolicyArgs == nil {
|
||||
n.HcnPolicyArgs = []hcn.EndpointPolicy{}
|
||||
}
|
||||
return n.HcnPolicyArgs
|
||||
}(),
|
||||
}
|
||||
}
|
||||
return hcnEndpoint, nil
|
||||
}
|
||||
|
||||
// ConstructEndpointName constructs enpointId which is used to identify an endpoint from HNS
|
||||
// There is a special consideration for netNs name here, which is required for Windows Server 1709
|
||||
// containerID is the Id of the container on which the endpoint is worked on
|
||||
func ConstructEndpointName(containerID string, netNs string, networkName string) string {
|
||||
return GetSandboxContainerID(containerID, netNs) + "_" + networkName
|
||||
}
|
||||
|
||||
// DeprovisionEndpoint removes an endpoint from the container by sending a Detach request to HNS
|
||||
// For shared endpoint, ContainerDetach is used
|
||||
// for removing the endpoint completely, HotDetachEndpoint is used
|
||||
func DeprovisionEndpoint(epName string, netns string, containerID string) error {
|
||||
if len(netns) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
hnsEndpoint, err := hcsshim.GetHNSEndpointByName(epName)
|
||||
|
||||
if hcsshim.IsNotExist(err) {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return errors.Annotatef(err, "failed to find HNSEndpoint %s", epName)
|
||||
}
|
||||
|
||||
if netns != pauseContainerNetNS {
|
||||
// Shared endpoint removal. Do not remove the endpoint.
|
||||
hnsEndpoint.ContainerDetach(containerID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Do not consider this as failure, else this would leak endpoints
|
||||
hcsshim.HotDetachEndpoint(containerID, hnsEndpoint.Id)
|
||||
|
||||
// Do not return error
|
||||
hnsEndpoint.Delete()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type EndpointMakerFunc func() (*hcsshim.HNSEndpoint, error)
|
||||
|
||||
// ProvisionEndpoint provisions an endpoint to a container specified by containerID.
|
||||
// If an endpoint already exists, the endpoint is reused.
|
||||
// This call is idempotent
|
||||
func ProvisionEndpoint(epName string, expectedNetworkId string, containerID string, netns string, makeEndpoint EndpointMakerFunc) (*hcsshim.HNSEndpoint, error) {
|
||||
// On the second add call we expect that the endpoint already exists. If it
|
||||
// does not then we should return an error.
|
||||
if netns != pauseContainerNetNS {
|
||||
_, err := hcsshim.GetHNSEndpointByName(epName)
|
||||
if err != nil {
|
||||
return nil, errors.Annotatef(err, "failed to find HNSEndpoint %s", epName)
|
||||
}
|
||||
}
|
||||
|
||||
// check if endpoint already exists
|
||||
createEndpoint := true
|
||||
hnsEndpoint, err := hcsshim.GetHNSEndpointByName(epName)
|
||||
if hnsEndpoint != nil && strings.EqualFold(hnsEndpoint.VirtualNetwork, expectedNetworkId) {
|
||||
createEndpoint = false
|
||||
}
|
||||
|
||||
if createEndpoint {
|
||||
if hnsEndpoint != nil {
|
||||
if _, err = hnsEndpoint.Delete(); err != nil {
|
||||
return nil, errors.Annotate(err, "failed to delete the stale HNSEndpoint")
|
||||
}
|
||||
}
|
||||
|
||||
if hnsEndpoint, err = makeEndpoint(); err != nil {
|
||||
return nil, errors.Annotate(err, "failed to make a new HNSEndpoint")
|
||||
}
|
||||
|
||||
if hnsEndpoint, err = hnsEndpoint.Create(); err != nil {
|
||||
return nil, errors.Annotate(err, "failed to create the new HNSEndpoint")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// hot attach
|
||||
if err := hcsshim.HotAttachEndpoint(containerID, hnsEndpoint.Id); err != nil {
|
||||
if createEndpoint {
|
||||
err := DeprovisionEndpoint(epName, netns, containerID)
|
||||
if err != nil {
|
||||
return nil, errors.Annotatef(err, "failed to Deprovsion after HotAttach failure")
|
||||
}
|
||||
}
|
||||
if hcsshim.ErrComputeSystemDoesNotExist == err {
|
||||
return hnsEndpoint, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return hnsEndpoint, nil
|
||||
}
|
||||
|
||||
type HcnEndpointMakerFunc func() (*hcn.HostComputeEndpoint, error)
|
||||
|
||||
func AddHcnEndpoint(epName string, expectedNetworkId string, namespace string,
|
||||
makeEndpoint HcnEndpointMakerFunc) (*hcn.HostComputeEndpoint, error) {
|
||||
|
||||
hcnEndpoint, err := makeEndpoint()
|
||||
if err != nil {
|
||||
return nil, errors.Annotate(err, "failed to make a new HNSEndpoint")
|
||||
}
|
||||
|
||||
if hcnEndpoint, err = hcnEndpoint.Create(); err != nil {
|
||||
return nil, errors.Annotate(err, "failed to create the new HNSEndpoint")
|
||||
}
|
||||
|
||||
err = hcn.AddNamespaceEndpoint(namespace, hcnEndpoint.Id)
|
||||
if err != nil {
|
||||
err := RemoveHcnEndpoint(epName)
|
||||
if err != nil {
|
||||
return nil, errors.Annotatef(err, "failed to Remove Endpoint after AddNamespaceEndpoint failure")
|
||||
}
|
||||
return nil, errors.Annotate(err, "failed to Add endpoint to namespace")
|
||||
}
|
||||
return hcnEndpoint, nil
|
||||
|
||||
}
|
||||
|
||||
// ConstructResult constructs the CNI result for the endpoint
|
||||
func ConstructResult(hnsNetwork *hcsshim.HNSNetwork, hnsEndpoint *hcsshim.HNSEndpoint) (*current.Result, error) {
|
||||
resultInterface := ¤t.Interface{
|
||||
Name: hnsEndpoint.Name,
|
||||
Mac: hnsEndpoint.MacAddress,
|
||||
}
|
||||
_, ipSubnet, err := net.ParseCIDR(hnsNetwork.Subnets[0].AddressPrefix)
|
||||
if err != nil {
|
||||
return nil, errors.Annotatef(err, "failed to parse CIDR from %s", hnsNetwork.Subnets[0].AddressPrefix)
|
||||
}
|
||||
|
||||
var ipVersion string
|
||||
if ipv4 := hnsEndpoint.IPAddress.To4(); ipv4 != nil {
|
||||
ipVersion = "4"
|
||||
} else if ipv6 := hnsEndpoint.IPAddress.To16(); ipv6 != nil {
|
||||
ipVersion = "6"
|
||||
} else {
|
||||
return nil, fmt.Errorf("IPAddress of HNSEndpoint %s isn't a valid ipv4 or ipv6 Address", hnsEndpoint.Name)
|
||||
}
|
||||
|
||||
resultIPConfig := ¤t.IPConfig{
|
||||
Version: ipVersion,
|
||||
Address: net.IPNet{
|
||||
IP: hnsEndpoint.IPAddress,
|
||||
Mask: ipSubnet.Mask},
|
||||
Gateway: net.ParseIP(hnsEndpoint.GatewayAddress),
|
||||
}
|
||||
result := ¤t.Result{}
|
||||
result.Interfaces = []*current.Interface{resultInterface}
|
||||
result.IPs = []*current.IPConfig{resultIPConfig}
|
||||
result.DNS = types.DNS{
|
||||
Search: strings.Split(hnsEndpoint.DNSSuffix, ","),
|
||||
Nameservers: strings.Split(hnsEndpoint.DNSServerList, ","),
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// This version follows the v2 workflow of removing the endpoint from the namespace and deleting it
|
||||
func RemoveHcnEndpoint(epName string) error {
|
||||
hcnEndpoint, err := hcn.GetEndpointByName(epName)
|
||||
if hcn.IsNotFoundError(err) {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
_ = fmt.Errorf("[win-cni] Failed to find endpoint %v, err:%v", epName, err)
|
||||
return err
|
||||
}
|
||||
if hcnEndpoint != nil {
|
||||
err = hcnEndpoint.Delete()
|
||||
if err != nil {
|
||||
return fmt.Errorf("[win-cni] Failed to delete endpoint %v, err:%v", epName, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ConstructHcnResult(hcnNetwork *hcn.HostComputeNetwork, hcnEndpoint *hcn.HostComputeEndpoint) (*current.Result, error) {
|
||||
resultInterface := ¤t.Interface{
|
||||
Name: hcnEndpoint.Name,
|
||||
Mac: hcnEndpoint.MacAddress,
|
||||
}
|
||||
_, ipSubnet, err := net.ParseCIDR(hcnNetwork.Ipams[0].Subnets[0].IpAddressPrefix)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var ipVersion string
|
||||
ipAddress := net.ParseIP(hcnEndpoint.IpConfigurations[0].IpAddress)
|
||||
if ipv4 := ipAddress.To4(); ipv4 != nil {
|
||||
ipVersion = "4"
|
||||
} else if ipv6 := ipAddress.To16(); ipv6 != nil {
|
||||
ipVersion = "6"
|
||||
} else {
|
||||
return nil, fmt.Errorf("[win-cni] The IPAddress of hnsEndpoint isn't a valid ipv4 or ipv6 Address.")
|
||||
}
|
||||
|
||||
resultIPConfig := ¤t.IPConfig{
|
||||
Version: ipVersion,
|
||||
Address: net.IPNet{
|
||||
IP: ipAddress,
|
||||
Mask: ipSubnet.Mask},
|
||||
Gateway: net.ParseIP(hcnEndpoint.Routes[0].NextHop),
|
||||
}
|
||||
result := ¤t.Result{}
|
||||
result.Interfaces = []*current.Interface{resultInterface}
|
||||
result.IPs = []*current.IPConfig{resultIPConfig}
|
||||
result.DNS = types.DNS{
|
||||
Search: hcnEndpoint.Dns.Search,
|
||||
Nameservers: hcnEndpoint.Dns.ServerList,
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
26
pkg/hns/netconf_suite_windows_test.go
Normal file
26
pkg/hns/netconf_suite_windows_test.go
Normal file
@ -0,0 +1,26 @@
|
||||
// Copyright 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 hns
|
||||
|
||||
import (
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHns(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "HNS NetConf Suite")
|
||||
}
|
236
pkg/hns/netconf_windows.go
Normal file
236
pkg/hns/netconf_windows.go
Normal file
@ -0,0 +1,236 @@
|
||||
// Copyright 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 hns
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"strings"
|
||||
|
||||
"github.com/Microsoft/hcsshim/hcn"
|
||||
"github.com/buger/jsonparser"
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
)
|
||||
|
||||
// NetConf is the CNI spec
|
||||
type NetConf struct {
|
||||
types.NetConf
|
||||
// ApiVersion is either 1 or 2, which specifies which hns APIs to call
|
||||
ApiVersion int `json:"ApiVersion"`
|
||||
// V2 Api Policies
|
||||
HcnPolicyArgs []hcn.EndpointPolicy `json:"HcnPolicyArgs,omitempty"`
|
||||
// V1 Api Policies
|
||||
Policies []policy `json:"policies,omitempty"`
|
||||
// Options to be passed in by the runtime
|
||||
RuntimeConfig RuntimeConfig `json:"runtimeConfig"`
|
||||
// If true, adds a policy to endpoints to support loopback direct server return
|
||||
LoopbackDSR bool `json:"loopbackDSR"`
|
||||
}
|
||||
|
||||
type RuntimeDNS struct {
|
||||
Nameservers []string `json:"servers,omitempty"`
|
||||
Search []string `json:"searches,omitempty"`
|
||||
}
|
||||
|
||||
type PortMapEntry struct {
|
||||
HostPort int `json:"hostPort"`
|
||||
ContainerPort int `json:"containerPort"`
|
||||
Protocol string `json:"protocol"`
|
||||
HostIP string `json:"hostIP,omitempty"`
|
||||
}
|
||||
|
||||
type RuntimeConfig struct {
|
||||
DNS RuntimeDNS `json:"dns"`
|
||||
PortMaps []PortMapEntry `json:"portMappings,omitempty"`
|
||||
}
|
||||
|
||||
type policy struct {
|
||||
Name string `json:"name"`
|
||||
Value json.RawMessage `json:"value"`
|
||||
}
|
||||
|
||||
func GetDefaultDestinationPrefix(ip *net.IP) string {
|
||||
destinationPrefix := "0.0.0.0/0"
|
||||
if ipv6 := ip.To4(); ipv6 == nil {
|
||||
destinationPrefix = "::/0"
|
||||
}
|
||||
return destinationPrefix
|
||||
}
|
||||
|
||||
func (n *NetConf) ApplyLoopbackDSR(ip *net.IP) {
|
||||
value := fmt.Sprintf(`"Destinations" : ["%s"]`, ip.String())
|
||||
if n.ApiVersion == 2 {
|
||||
hcnLoopbackRoute := hcn.EndpointPolicy{
|
||||
Type: "OutBoundNAT",
|
||||
Settings: []byte(fmt.Sprintf("{%s}", value)),
|
||||
}
|
||||
n.HcnPolicyArgs = append(n.HcnPolicyArgs, hcnLoopbackRoute)
|
||||
} else {
|
||||
hnsLoopbackRoute := policy{
|
||||
Name: "EndpointPolicy",
|
||||
Value: []byte(fmt.Sprintf(`{"Type": "OutBoundNAT", %s}`, value)),
|
||||
}
|
||||
n.Policies = append(n.Policies, hnsLoopbackRoute)
|
||||
}
|
||||
}
|
||||
|
||||
// If runtime dns values are there use that else use cni conf supplied dns
|
||||
func (n *NetConf) GetDNS() types.DNS {
|
||||
dnsResult := n.DNS
|
||||
if len(n.RuntimeConfig.DNS.Nameservers) > 0 {
|
||||
dnsResult.Nameservers = n.RuntimeConfig.DNS.Nameservers
|
||||
}
|
||||
if len(n.RuntimeConfig.DNS.Search) > 0 {
|
||||
dnsResult.Search = n.RuntimeConfig.DNS.Search
|
||||
}
|
||||
return dnsResult
|
||||
}
|
||||
|
||||
// MarshalPolicies converts the Endpoint policies in Policies
|
||||
// to HNS specific policies as Json raw bytes
|
||||
func (n *NetConf) MarshalPolicies() []json.RawMessage {
|
||||
if n.Policies == nil {
|
||||
n.Policies = make([]policy, 0)
|
||||
}
|
||||
|
||||
result := make([]json.RawMessage, 0, len(n.Policies))
|
||||
for _, p := range n.Policies {
|
||||
if !strings.EqualFold(p.Name, "EndpointPolicy") {
|
||||
continue
|
||||
}
|
||||
|
||||
result = append(result, p.Value)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// ApplyOutboundNatPolicy applies NAT Policy in VFP using HNS
|
||||
// Simultaneously an exception is added for the network that has to be Nat'd
|
||||
func (n *NetConf) ApplyOutboundNatPolicy(nwToNat string) {
|
||||
if n.Policies == nil {
|
||||
n.Policies = make([]policy, 0)
|
||||
}
|
||||
|
||||
nwToNatBytes := []byte(nwToNat)
|
||||
|
||||
for i, p := range n.Policies {
|
||||
if !strings.EqualFold(p.Name, "EndpointPolicy") {
|
||||
continue
|
||||
}
|
||||
|
||||
typeValue, err := jsonparser.GetUnsafeString(p.Value, "Type")
|
||||
if err != nil || len(typeValue) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if !strings.EqualFold(typeValue, "OutBoundNAT") {
|
||||
continue
|
||||
}
|
||||
|
||||
exceptionListValue, dt, _, _ := jsonparser.Get(p.Value, "ExceptionList")
|
||||
// OutBoundNAT must with ExceptionList, so don't need to judge jsonparser.NotExist
|
||||
if dt == jsonparser.Array {
|
||||
buf := bytes.Buffer{}
|
||||
buf.WriteString(`{"Type": "OutBoundNAT", "ExceptionList": [`)
|
||||
|
||||
jsonparser.ArrayEach(exceptionListValue, func(value []byte, dataType jsonparser.ValueType, offset int, err error) {
|
||||
if dataType == jsonparser.String && len(value) != 0 {
|
||||
if bytes.Compare(value, nwToNatBytes) != 0 {
|
||||
buf.WriteByte('"')
|
||||
buf.Write(value)
|
||||
buf.WriteByte('"')
|
||||
buf.WriteByte(',')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
buf.WriteString(`"` + nwToNat + `"]}`)
|
||||
|
||||
n.Policies[i] = policy{
|
||||
Name: "EndpointPolicy",
|
||||
Value: buf.Bytes(),
|
||||
}
|
||||
} else {
|
||||
n.Policies[i] = policy{
|
||||
Name: "EndpointPolicy",
|
||||
Value: []byte(`{"Type": "OutBoundNAT", "ExceptionList": ["` + nwToNat + `"]}`),
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// didn't find the policyArg, add it
|
||||
n.Policies = append(n.Policies, policy{
|
||||
Name: "EndpointPolicy",
|
||||
Value: []byte(`{"Type": "OutBoundNAT", "ExceptionList": ["` + nwToNat + `"]}`),
|
||||
})
|
||||
}
|
||||
|
||||
// ApplyDefaultPAPolicy is used to configure a endpoint PA policy in HNS
|
||||
func (n *NetConf) ApplyDefaultPAPolicy(paAddress string) {
|
||||
if n.Policies == nil {
|
||||
n.Policies = make([]policy, 0)
|
||||
}
|
||||
|
||||
// if its already present, leave untouched
|
||||
for i, p := range n.Policies {
|
||||
if !strings.EqualFold(p.Name, "EndpointPolicy") {
|
||||
continue
|
||||
}
|
||||
|
||||
paValue, dt, _, _ := jsonparser.Get(p.Value, "PA")
|
||||
if dt == jsonparser.NotExist {
|
||||
continue
|
||||
} else if dt == jsonparser.String && len(paValue) != 0 {
|
||||
// found it, don't override
|
||||
return
|
||||
}
|
||||
|
||||
n.Policies[i] = policy{
|
||||
Name: "EndpointPolicy",
|
||||
Value: []byte(`{"Type": "PA", "PA": "` + paAddress + `"}`),
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// didn't find the policyArg, add it
|
||||
n.Policies = append(n.Policies, policy{
|
||||
Name: "EndpointPolicy",
|
||||
Value: []byte(`{"Type": "PA", "PA": "` + paAddress + `"}`),
|
||||
})
|
||||
}
|
||||
|
||||
// ApplyPortMappingPolicy is used to configure HostPort<>ContainerPort mapping in HNS
|
||||
func (n *NetConf) ApplyPortMappingPolicy(portMappings []PortMapEntry) {
|
||||
if portMappings == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if n.Policies == nil {
|
||||
n.Policies = make([]policy, 0)
|
||||
}
|
||||
|
||||
for _, portMapping := range portMappings {
|
||||
n.Policies = append(n.Policies, policy{
|
||||
Name: "EndpointPolicy",
|
||||
Value: []byte(fmt.Sprintf(`{"Type": "NAT", "InternalPort": %d, "ExternalPort": %d, "Protocol": "%s"}`, portMapping.ContainerPort, portMapping.HostPort, portMapping.Protocol)),
|
||||
})
|
||||
}
|
||||
}
|
236
pkg/hns/netconf_windows_test.go
Normal file
236
pkg/hns/netconf_windows_test.go
Normal file
@ -0,0 +1,236 @@
|
||||
// Copyright 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 hns
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("HNS NetConf", func() {
|
||||
Describe("ApplyOutBoundNATPolicy", func() {
|
||||
Context("when not set by user", func() {
|
||||
It("sets it by adding a policy", func() {
|
||||
|
||||
// apply it
|
||||
n := NetConf{}
|
||||
n.ApplyOutboundNatPolicy("192.168.0.0/16")
|
||||
|
||||
addlArgs := n.Policies
|
||||
Expect(addlArgs).Should(HaveLen(1))
|
||||
|
||||
policy := addlArgs[0]
|
||||
Expect(policy.Name).Should(Equal("EndpointPolicy"))
|
||||
|
||||
value := make(map[string]interface{})
|
||||
json.Unmarshal(policy.Value, &value)
|
||||
|
||||
Expect(value).Should(HaveKey("Type"))
|
||||
Expect(value).Should(HaveKey("ExceptionList"))
|
||||
Expect(value["Type"]).Should(Equal("OutBoundNAT"))
|
||||
|
||||
exceptionList := value["ExceptionList"].([]interface{})
|
||||
Expect(exceptionList).Should(HaveLen(1))
|
||||
Expect(exceptionList[0].(string)).Should(Equal("192.168.0.0/16"))
|
||||
})
|
||||
})
|
||||
|
||||
Context("when set by user", func() {
|
||||
It("appends exceptions to the existing policy", func() {
|
||||
// first set it
|
||||
n := NetConf{}
|
||||
n.ApplyOutboundNatPolicy("192.168.0.0/16")
|
||||
|
||||
// then attempt to update it
|
||||
n.ApplyOutboundNatPolicy("10.244.0.0/16")
|
||||
|
||||
// it should be unchanged!
|
||||
addlArgs := n.Policies
|
||||
Expect(addlArgs).Should(HaveLen(1))
|
||||
|
||||
policy := addlArgs[0]
|
||||
Expect(policy.Name).Should(Equal("EndpointPolicy"))
|
||||
|
||||
var value map[string]interface{}
|
||||
json.Unmarshal(policy.Value, &value)
|
||||
|
||||
Expect(value).Should(HaveKey("Type"))
|
||||
Expect(value).Should(HaveKey("ExceptionList"))
|
||||
Expect(value["Type"]).Should(Equal("OutBoundNAT"))
|
||||
|
||||
exceptionList := value["ExceptionList"].([]interface{})
|
||||
Expect(exceptionList).Should(HaveLen(2))
|
||||
Expect(exceptionList[0].(string)).Should(Equal("192.168.0.0/16"))
|
||||
Expect(exceptionList[1].(string)).Should(Equal("10.244.0.0/16"))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Describe("ApplyDefaultPAPolicy", func() {
|
||||
Context("when not set by user", func() {
|
||||
It("sets it by adding a policy", func() {
|
||||
|
||||
n := NetConf{}
|
||||
n.ApplyDefaultPAPolicy("192.168.0.1")
|
||||
|
||||
addlArgs := n.Policies
|
||||
Expect(addlArgs).Should(HaveLen(1))
|
||||
|
||||
policy := addlArgs[0]
|
||||
Expect(policy.Name).Should(Equal("EndpointPolicy"))
|
||||
|
||||
value := make(map[string]interface{})
|
||||
json.Unmarshal(policy.Value, &value)
|
||||
|
||||
Expect(value).Should(HaveKey("Type"))
|
||||
Expect(value["Type"]).Should(Equal("PA"))
|
||||
|
||||
paAddress := value["PA"].(string)
|
||||
Expect(paAddress).Should(Equal("192.168.0.1"))
|
||||
})
|
||||
})
|
||||
|
||||
Context("when set by user", func() {
|
||||
It("does not override", func() {
|
||||
n := NetConf{}
|
||||
n.ApplyDefaultPAPolicy("192.168.0.1")
|
||||
n.ApplyDefaultPAPolicy("192.168.0.2")
|
||||
|
||||
addlArgs := n.Policies
|
||||
Expect(addlArgs).Should(HaveLen(1))
|
||||
|
||||
policy := addlArgs[0]
|
||||
Expect(policy.Name).Should(Equal("EndpointPolicy"))
|
||||
|
||||
value := make(map[string]interface{})
|
||||
json.Unmarshal(policy.Value, &value)
|
||||
|
||||
Expect(value).Should(HaveKey("Type"))
|
||||
Expect(value["Type"]).Should(Equal("PA"))
|
||||
|
||||
paAddress := value["PA"].(string)
|
||||
Expect(paAddress).Should(Equal("192.168.0.1"))
|
||||
Expect(paAddress).ShouldNot(Equal("192.168.0.2"))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Describe("ApplyPortMappingPolicy", func() {
|
||||
Context("when portMappings not activated", func() {
|
||||
It("does nothing", func() {
|
||||
n := NetConf{}
|
||||
n.ApplyPortMappingPolicy(nil)
|
||||
Expect(n.Policies).Should(BeNil())
|
||||
|
||||
n.ApplyPortMappingPolicy([]PortMapEntry{})
|
||||
Expect(n.Policies).Should(HaveLen(0))
|
||||
})
|
||||
})
|
||||
|
||||
Context("when portMappings is activated", func() {
|
||||
It("creates NAT policies", func() {
|
||||
n := NetConf{}
|
||||
n.ApplyPortMappingPolicy([]PortMapEntry{
|
||||
{
|
||||
ContainerPort: 80,
|
||||
HostPort: 8080,
|
||||
Protocol: "TCP",
|
||||
HostIP: "ignored",
|
||||
},
|
||||
})
|
||||
|
||||
Expect(n.Policies).Should(HaveLen(1))
|
||||
|
||||
policy := n.Policies[0]
|
||||
Expect(policy.Name).Should(Equal("EndpointPolicy"))
|
||||
|
||||
value := make(map[string]interface{})
|
||||
json.Unmarshal(policy.Value, &value)
|
||||
|
||||
Expect(value).Should(HaveKey("Type"))
|
||||
Expect(value["Type"]).Should(Equal("NAT"))
|
||||
|
||||
Expect(value).Should(HaveKey("InternalPort"))
|
||||
Expect(value["InternalPort"]).Should(Equal(float64(80)))
|
||||
|
||||
Expect(value).Should(HaveKey("ExternalPort"))
|
||||
Expect(value["ExternalPort"]).Should(Equal(float64(8080)))
|
||||
|
||||
Expect(value).Should(HaveKey("Protocol"))
|
||||
Expect(value["Protocol"]).Should(Equal("TCP"))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Describe("MarshalPolicies", func() {
|
||||
Context("when not set by user", func() {
|
||||
It("sets it by adding a policy", func() {
|
||||
|
||||
n := NetConf{
|
||||
Policies: []policy{
|
||||
{
|
||||
Name: "EndpointPolicy",
|
||||
Value: []byte(`{"someKey": "someValue"}`),
|
||||
},
|
||||
{
|
||||
Name: "someOtherType",
|
||||
Value: []byte(`{"someOtherKey": "someOtherValue"}`),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result := n.MarshalPolicies()
|
||||
Expect(len(result)).To(Equal(1))
|
||||
|
||||
policy := make(map[string]interface{})
|
||||
err := json.Unmarshal(result[0], &policy)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(policy).Should(HaveKey("someKey"))
|
||||
Expect(policy["someKey"]).To(Equal("someValue"))
|
||||
})
|
||||
})
|
||||
|
||||
Context("when set by user", func() {
|
||||
It("appends exceptions to the existing policy", func() {
|
||||
// first set it
|
||||
n := NetConf{}
|
||||
n.ApplyOutboundNatPolicy("192.168.0.0/16")
|
||||
|
||||
// then attempt to update it
|
||||
n.ApplyOutboundNatPolicy("10.244.0.0/16")
|
||||
|
||||
// it should be unchanged!
|
||||
addlArgs := n.Policies
|
||||
Expect(addlArgs).Should(HaveLen(1))
|
||||
|
||||
policy := addlArgs[0]
|
||||
Expect(policy.Name).Should(Equal("EndpointPolicy"))
|
||||
|
||||
var value map[string]interface{}
|
||||
json.Unmarshal(policy.Value, &value)
|
||||
|
||||
Expect(value).Should(HaveKey("Type"))
|
||||
Expect(value).Should(HaveKey("ExceptionList"))
|
||||
Expect(value["Type"]).Should(Equal("OutBoundNAT"))
|
||||
|
||||
exceptionList := value["ExceptionList"].([]interface{})
|
||||
Expect(exceptionList).Should(HaveLen(2))
|
||||
Expect(exceptionList[0].(string)).Should(Equal("192.168.0.0/16"))
|
||||
Expect(exceptionList[1].(string)).Should(Equal("10.244.0.0/16"))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
@ -23,5 +23,5 @@ import (
|
||||
|
||||
func TestIp(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Ip Suite")
|
||||
RunSpecs(t, "pkg/ip")
|
||||
}
|
||||
|
@ -15,6 +15,7 @@
|
||||
package ip
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/types/current"
|
||||
@ -51,5 +52,10 @@ func EnableForward(ips []*current.IPConfig) error {
|
||||
}
|
||||
|
||||
func echo1(f string) error {
|
||||
if content, err := ioutil.ReadFile(f); err == nil {
|
||||
if bytes.Equal(bytes.TrimSpace(content), []byte("1")) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return ioutil.WriteFile(f, []byte("1"), 0644)
|
||||
}
|
32
pkg/ip/ipforward_linux_test.go
Normal file
32
pkg/ip/ipforward_linux_test.go
Normal file
@ -0,0 +1,32 @@
|
||||
package ip
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("IpforwardLinux", func() {
|
||||
It("echo1 must not write the file if content is 1", func() {
|
||||
file, err := ioutil.TempFile(os.TempDir(), "containernetworking")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
defer os.Remove(file.Name())
|
||||
err = echo1(file.Name())
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
statBefore, err := file.Stat()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// take a duration here, otherwise next file modification operation time
|
||||
// will be same as previous one.
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
err = echo1(file.Name())
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
statAfter, err := file.Stat()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(statBefore.ModTime()).To(Equal(statAfter.ModTime()))
|
||||
})
|
||||
})
|
@ -22,7 +22,7 @@ import (
|
||||
)
|
||||
|
||||
// SetupIPMasq installs iptables rules to masquerade traffic
|
||||
// coming from ipn and going outside of it
|
||||
// coming from ip of ipn and going outside of ipn
|
||||
func SetupIPMasq(ipn *net.IPNet, chain string, comment string) error {
|
||||
isV6 := ipn.IP.To4() == nil
|
||||
|
||||
@ -70,23 +70,57 @@ func SetupIPMasq(ipn *net.IPNet, chain string, comment string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
return ipt.AppendUnique("nat", "POSTROUTING", "-s", ipn.String(), "-j", chain, "-m", "comment", "--comment", comment)
|
||||
// Packets from the specific IP of this network will hit the chain
|
||||
return ipt.AppendUnique("nat", "POSTROUTING", "-s", ipn.IP.String(), "-j", chain, "-m", "comment", "--comment", comment)
|
||||
}
|
||||
|
||||
// TeardownIPMasq undoes the effects of SetupIPMasq
|
||||
func TeardownIPMasq(ipn *net.IPNet, chain string, comment string) error {
|
||||
ipt, err := iptables.New()
|
||||
isV6 := ipn.IP.To4() == nil
|
||||
|
||||
var ipt *iptables.IPTables
|
||||
var err error
|
||||
|
||||
if isV6 {
|
||||
ipt, err = iptables.NewWithProtocol(iptables.ProtocolIPv6)
|
||||
} else {
|
||||
ipt, err = iptables.NewWithProtocol(iptables.ProtocolIPv4)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to locate iptables: %v", err)
|
||||
}
|
||||
|
||||
if err = ipt.Delete("nat", "POSTROUTING", "-s", ipn.String(), "-j", chain, "-m", "comment", "--comment", comment); err != nil {
|
||||
err = ipt.Delete("nat", "POSTROUTING", "-s", ipn.IP.String(), "-j", chain, "-m", "comment", "--comment", comment)
|
||||
if err != nil && !isNotExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = ipt.ClearChain("nat", chain); err != nil {
|
||||
// for downward compatibility
|
||||
err = ipt.Delete("nat", "POSTROUTING", "-s", ipn.String(), "-j", chain, "-m", "comment", "--comment", comment)
|
||||
if err != nil && !isNotExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
return ipt.DeleteChain("nat", chain)
|
||||
err = ipt.ClearChain("nat", chain)
|
||||
if err != nil && !isNotExist(err) {
|
||||
return err
|
||||
|
||||
}
|
||||
|
||||
err = ipt.DeleteChain("nat", chain)
|
||||
if err != nil && !isNotExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// isNotExist returnst true if the error is from iptables indicating
|
||||
// that the target does not exist.
|
||||
func isNotExist(err error) bool {
|
||||
e, ok := err.(*iptables.Error)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return e.IsNotExist()
|
||||
}
|
@ -21,9 +21,12 @@ import (
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"github.com/safchain/ethtool"
|
||||
"github.com/vishvananda/netlink"
|
||||
|
||||
"github.com/containernetworking/plugins/pkg/ns"
|
||||
"github.com/containernetworking/plugins/pkg/utils/hwaddr"
|
||||
"github.com/vishvananda/netlink"
|
||||
"github.com/containernetworking/plugins/pkg/utils/sysctl"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -42,8 +45,14 @@ func makeVethPair(name, peer string, mtu int) (netlink.Link, error) {
|
||||
if err := netlink.LinkAdd(veth); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Re-fetch the link to get its creation-time parameters, e.g. index and mac
|
||||
veth2, err := netlink.LinkByName(name)
|
||||
if err != nil {
|
||||
netlink.LinkDel(veth) // try and clean up the link if possible.
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return veth, nil
|
||||
return veth2, nil
|
||||
}
|
||||
|
||||
func peerExists(name string) bool {
|
||||
@ -53,11 +62,15 @@ func peerExists(name string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func makeVeth(name string, mtu int) (peerName string, veth netlink.Link, err error) {
|
||||
func makeVeth(name, vethPeerName string, mtu int) (peerName string, veth netlink.Link, err error) {
|
||||
for i := 0; i < 10; i++ {
|
||||
peerName, err = RandomVethName()
|
||||
if err != nil {
|
||||
return
|
||||
if vethPeerName != "" {
|
||||
peerName = vethPeerName
|
||||
} else {
|
||||
peerName, err = RandomVethName()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
veth, err = makeVethPair(name, peerName, mtu)
|
||||
@ -66,7 +79,7 @@ func makeVeth(name string, mtu int) (peerName string, veth netlink.Link, err err
|
||||
return
|
||||
|
||||
case os.IsExist(err):
|
||||
if peerExists(peerName) {
|
||||
if peerExists(peerName) && vethPeerName == "" {
|
||||
continue
|
||||
}
|
||||
err = fmt.Errorf("container veth name provided (%v) already exists", name)
|
||||
@ -114,12 +127,13 @@ func ifaceFromNetlinkLink(l netlink.Link) net.Interface {
|
||||
}
|
||||
}
|
||||
|
||||
// SetupVeth sets up a pair of virtual ethernet devices.
|
||||
// Call SetupVeth from inside the container netns. It will create both veth
|
||||
// SetupVethWithName sets up a pair of virtual ethernet devices.
|
||||
// Call SetupVethWithName from inside the container netns. It will create both veth
|
||||
// devices and move the host-side veth into the provided hostNS namespace.
|
||||
// On success, SetupVeth returns (hostVeth, containerVeth, nil)
|
||||
func SetupVeth(contVethName string, mtu int, hostNS ns.NetNS) (net.Interface, net.Interface, error) {
|
||||
hostVethName, contVeth, err := makeVeth(contVethName, mtu)
|
||||
// hostVethName: If hostVethName is not specified, the host-side veth name will use a random string.
|
||||
// On success, SetupVethWithName returns (hostVeth, containerVeth, nil)
|
||||
func SetupVethWithName(contVethName, hostVethName string, mtu int, hostNS ns.NetNS) (net.Interface, net.Interface, error) {
|
||||
hostVethName, contVeth, err := makeVeth(contVethName, hostVethName, mtu)
|
||||
if err != nil {
|
||||
return net.Interface{}, net.Interface{}, err
|
||||
}
|
||||
@ -146,6 +160,9 @@ func SetupVeth(contVethName string, mtu int, hostNS ns.NetNS) (net.Interface, ne
|
||||
if err = netlink.LinkSetUp(hostVeth); err != nil {
|
||||
return fmt.Errorf("failed to set %q up: %v", hostVethName, err)
|
||||
}
|
||||
|
||||
// we want to own the routes for this interface
|
||||
_, _ = sysctl.Sysctl(fmt.Sprintf("net/ipv6/conf/%s/accept_ra", hostVethName), "0")
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
@ -154,10 +171,21 @@ func SetupVeth(contVethName string, mtu int, hostNS ns.NetNS) (net.Interface, ne
|
||||
return ifaceFromNetlinkLink(hostVeth), ifaceFromNetlinkLink(contVeth), nil
|
||||
}
|
||||
|
||||
// SetupVeth sets up a pair of virtual ethernet devices.
|
||||
// Call SetupVeth from inside the container netns. It will create both veth
|
||||
// devices and move the host-side veth into the provided hostNS namespace.
|
||||
// On success, SetupVeth returns (hostVeth, containerVeth, nil)
|
||||
func SetupVeth(contVethName string, mtu int, hostNS ns.NetNS) (net.Interface, net.Interface, error) {
|
||||
return SetupVethWithName(contVethName, "", mtu, hostNS)
|
||||
}
|
||||
|
||||
// DelLinkByName removes an interface link.
|
||||
func DelLinkByName(ifName string) error {
|
||||
iface, err := netlink.LinkByName(ifName)
|
||||
if err != nil {
|
||||
if _, ok := err.(netlink.LinkNotFoundError); ok {
|
||||
return ErrLinkNotFound
|
||||
}
|
||||
return fmt.Errorf("failed to lookup %q: %v", ifName, err)
|
||||
}
|
||||
|
||||
@ -168,19 +196,18 @@ func DelLinkByName(ifName string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// DelLinkByNameAddr remove an interface returns its IP address
|
||||
// of the specified family
|
||||
func DelLinkByNameAddr(ifName string, family int) (*net.IPNet, error) {
|
||||
// DelLinkByNameAddr remove an interface and returns its addresses
|
||||
func DelLinkByNameAddr(ifName string) ([]*net.IPNet, error) {
|
||||
iface, err := netlink.LinkByName(ifName)
|
||||
if err != nil {
|
||||
if err != nil && err.Error() == "Link not found" {
|
||||
if _, ok := err.(netlink.LinkNotFoundError); ok {
|
||||
return nil, ErrLinkNotFound
|
||||
}
|
||||
return nil, fmt.Errorf("failed to lookup %q: %v", ifName, err)
|
||||
}
|
||||
|
||||
addrs, err := netlink.AddrList(iface, family)
|
||||
if err != nil || len(addrs) == 0 {
|
||||
addrs, err := netlink.AddrList(iface, netlink.FAMILY_ALL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get IP addresses for %q: %v", ifName, err)
|
||||
}
|
||||
|
||||
@ -188,7 +215,14 @@ func DelLinkByNameAddr(ifName string, family int) (*net.IPNet, error) {
|
||||
return nil, fmt.Errorf("failed to delete %q: %v", ifName, err)
|
||||
}
|
||||
|
||||
return addrs[0].IPNet, nil
|
||||
out := []*net.IPNet{}
|
||||
for _, addr := range addrs {
|
||||
if addr.IP.IsGlobalUnicast() {
|
||||
out = append(out, addr.IPNet)
|
||||
}
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func SetHWAddrByIP(ifName string, ip4 net.IP, ip6 net.IP) error {
|
||||
@ -217,3 +251,43 @@ func SetHWAddrByIP(ifName string, ip4 net.IP, ip6 net.IP) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetVethPeerIfindex returns the veth link object, the peer ifindex of the
|
||||
// veth, or an error. This peer ifindex will only be valid in the peer's
|
||||
// network namespace.
|
||||
func GetVethPeerIfindex(ifName string) (netlink.Link, int, error) {
|
||||
link, err := netlink.LinkByName(ifName)
|
||||
if err != nil {
|
||||
return nil, -1, fmt.Errorf("could not look up %q: %v", ifName, err)
|
||||
}
|
||||
if _, ok := link.(*netlink.Veth); !ok {
|
||||
return nil, -1, fmt.Errorf("interface %q was not a veth interface", ifName)
|
||||
}
|
||||
|
||||
// veth supports IFLA_LINK (what vishvananda/netlink calls ParentIndex)
|
||||
// on 4.1 and higher kernels
|
||||
peerIndex := link.Attrs().ParentIndex
|
||||
if peerIndex <= 0 {
|
||||
// Fall back to ethtool for 4.0 and earlier kernels
|
||||
e, err := ethtool.NewEthtool()
|
||||
if err != nil {
|
||||
return nil, -1, fmt.Errorf("failed to initialize ethtool: %v", err)
|
||||
}
|
||||
defer e.Close()
|
||||
|
||||
stats, err := e.Stats(link.Attrs().Name)
|
||||
if err != nil {
|
||||
return nil, -1, fmt.Errorf("failed to request ethtool stats: %v", err)
|
||||
}
|
||||
n, ok := stats["peer_ifindex"]
|
||||
if !ok {
|
||||
return nil, -1, fmt.Errorf("failed to find 'peer_ifindex' in ethtool stats")
|
||||
}
|
||||
if n > 32767 || n == 0 {
|
||||
return nil, -1, fmt.Errorf("invalid 'peer_ifindex' %d", n)
|
||||
}
|
||||
peerIndex = int(n)
|
||||
}
|
||||
|
||||
return link, peerIndex, nil
|
||||
}
|
@ -25,9 +25,9 @@ import (
|
||||
|
||||
"github.com/containernetworking/plugins/pkg/ip"
|
||||
"github.com/containernetworking/plugins/pkg/ns"
|
||||
"github.com/containernetworking/plugins/pkg/testutils"
|
||||
|
||||
"github.com/vishvananda/netlink"
|
||||
"github.com/vishvananda/netlink/nl"
|
||||
)
|
||||
|
||||
func getHwAddr(linkname string) string {
|
||||
@ -59,10 +59,10 @@ var _ = Describe("Link", func() {
|
||||
BeforeEach(func() {
|
||||
var err error
|
||||
|
||||
hostNetNS, err = ns.NewNS()
|
||||
hostNetNS, err = testutils.NewNS()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
containerNetNS, err = ns.NewNS()
|
||||
containerNetNS, err = testutils.NewNS()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
fakeBytes := make([]byte, 20)
|
||||
@ -92,6 +92,46 @@ var _ = Describe("Link", func() {
|
||||
rand.Reader = originalRandReader
|
||||
})
|
||||
|
||||
Describe("GetVethPeerIfindex", func() {
|
||||
It("returns the link and peer index of the named interface", func() {
|
||||
By("looking up the container veth index using the host veth name")
|
||||
_ = hostNetNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
gotHostLink, gotContainerIndex, err := ip.GetVethPeerIfindex(hostVethName)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("checking we got back the host link")
|
||||
attrs := gotHostLink.Attrs()
|
||||
Expect(attrs.Index).To(Equal(hostVeth.Index))
|
||||
Expect(attrs.Name).To(Equal(hostVeth.Name))
|
||||
|
||||
By("checking we got back the container veth index")
|
||||
Expect(gotContainerIndex).To(Equal(containerVeth.Index))
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
By("looking up the host veth index using the container veth name")
|
||||
_ = containerNetNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
gotContainerLink, gotHostIndex, err := ip.GetVethPeerIfindex(containerVethName)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("checking we got back the container link")
|
||||
attrs := gotContainerLink.Attrs()
|
||||
Expect(attrs.Index).To(Equal(containerVeth.Index))
|
||||
Expect(attrs.Name).To(Equal(containerVeth.Name))
|
||||
|
||||
By("checking we got back the host veth index")
|
||||
Expect(gotHostIndex).To(Equal(hostVeth.Index))
|
||||
|
||||
return nil
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
It("SetupVeth must put the veth endpoints into the separate namespaces", func() {
|
||||
_ = containerNetNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
@ -133,7 +173,7 @@ var _ = Describe("Link", func() {
|
||||
defer GinkgoRecover()
|
||||
|
||||
// This string should match the expected error codes in the cmdDel functions of some of the plugins
|
||||
_, err := ip.DelLinkByNameAddr("THIS_DONT_EXIST", netlink.FAMILY_V4)
|
||||
_, err := ip.DelLinkByNameAddr("THIS_DONT_EXIST")
|
||||
Expect(err).To(Equal(ip.ErrLinkNotFound))
|
||||
|
||||
return nil
|
||||
@ -149,9 +189,8 @@ var _ = Describe("Link", func() {
|
||||
It("returns useful error", func() {
|
||||
_ = containerNetNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
_, _, err := ip.SetupVeth(containerVethName, mtu, hostNetNS)
|
||||
Expect(err.Error()).To(Equal("failed to move veth to host netns: file exists"))
|
||||
Expect(err.Error()).To(HavePrefix("failed to move veth to host netns: "))
|
||||
|
||||
return nil
|
||||
})
|
||||
@ -220,16 +259,14 @@ var _ = Describe("Link", func() {
|
||||
})
|
||||
})
|
||||
|
||||
It("DelLinkByNameAddr must throw an error for configured interfaces", func() {
|
||||
It("DelLinkByNameAddr should return no IPs when no IPs are configured", func() {
|
||||
_ = containerNetNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
// this will delete the host endpoint too
|
||||
addr, err := ip.DelLinkByNameAddr(containerVethName, nl.FAMILY_V4)
|
||||
Expect(err).To(HaveOccurred())
|
||||
|
||||
var ipNetNil *net.IPNet
|
||||
Expect(addr).To(Equal(ipNetNil))
|
||||
addr, err := ip.DelLinkByNameAddr(containerVethName)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(addr).To(HaveLen(0))
|
||||
return nil
|
||||
})
|
||||
})
|
@ -39,3 +39,9 @@ func AddHostRoute(ipn *net.IPNet, gw net.IP, dev netlink.Link) error {
|
||||
Gw: gw,
|
||||
})
|
||||
}
|
||||
|
||||
// AddDefaultRoute sets the default route on the given gateway.
|
||||
func AddDefaultRoute(gw net.IP, dev netlink.Link) error {
|
||||
_, defNet, _ := net.ParseCIDR("0.0.0.0/0")
|
||||
return AddRoute(defNet, gw, dev)
|
||||
}
|
||||
|
120
pkg/ip/utils_linux.go
Normal file
120
pkg/ip/utils_linux.go
Normal file
@ -0,0 +1,120 @@
|
||||
// +build linux
|
||||
|
||||
// Copyright 2016 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 ip
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
"github.com/containernetworking/cni/pkg/types/current"
|
||||
"github.com/vishvananda/netlink"
|
||||
)
|
||||
|
||||
func ValidateExpectedInterfaceIPs(ifName string, resultIPs []*current.IPConfig) error {
|
||||
|
||||
// Ensure ips
|
||||
for _, ips := range resultIPs {
|
||||
ourAddr := netlink.Addr{IPNet: &ips.Address}
|
||||
match := false
|
||||
|
||||
link, err := netlink.LinkByName(ifName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Cannot find container link %v", ifName)
|
||||
}
|
||||
|
||||
addrList, err := netlink.AddrList(link, netlink.FAMILY_ALL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Cannot obtain List of IP Addresses")
|
||||
}
|
||||
|
||||
for _, addr := range addrList {
|
||||
if addr.Equal(ourAddr) {
|
||||
match = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if match == false {
|
||||
return fmt.Errorf("Failed to match addr %v on interface %v", ourAddr, ifName)
|
||||
}
|
||||
|
||||
// Convert the host/prefixlen to just prefix for route lookup.
|
||||
_, ourPrefix, err := net.ParseCIDR(ourAddr.String())
|
||||
|
||||
findGwy := &netlink.Route{Dst: ourPrefix}
|
||||
routeFilter := netlink.RT_FILTER_DST
|
||||
var family int
|
||||
|
||||
switch {
|
||||
case ips.Version == "4":
|
||||
family = netlink.FAMILY_V4
|
||||
case ips.Version == "6":
|
||||
family = netlink.FAMILY_V6
|
||||
default:
|
||||
return fmt.Errorf("Invalid IP Version %v for interface %v", ips.Version, ifName)
|
||||
}
|
||||
|
||||
gwy, err := netlink.RouteListFiltered(family, findGwy, routeFilter)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error %v trying to find Gateway %v for interface %v", err, ips.Gateway, ifName)
|
||||
}
|
||||
if gwy == nil {
|
||||
return fmt.Errorf("Failed to find Gateway %v for interface %v", ips.Gateway, ifName)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ValidateExpectedRoute(resultRoutes []*types.Route) error {
|
||||
|
||||
// Ensure that each static route in prevResults is found in the routing table
|
||||
for _, route := range resultRoutes {
|
||||
find := &netlink.Route{Dst: &route.Dst, Gw: route.GW}
|
||||
routeFilter := netlink.RT_FILTER_DST | netlink.RT_FILTER_GW
|
||||
var family int
|
||||
|
||||
switch {
|
||||
case route.Dst.IP.To4() != nil:
|
||||
family = netlink.FAMILY_V4
|
||||
// Default route needs Dst set to nil
|
||||
if route.Dst.String() == "0.0.0.0/0" {
|
||||
find = &netlink.Route{Dst: nil, Gw: route.GW}
|
||||
routeFilter = netlink.RT_FILTER_DST
|
||||
}
|
||||
case len(route.Dst.IP) == net.IPv6len:
|
||||
family = netlink.FAMILY_V6
|
||||
// Default route needs Dst set to nil
|
||||
if route.Dst.String() == "::/0" {
|
||||
find = &netlink.Route{Dst: nil, Gw: route.GW}
|
||||
routeFilter = netlink.RT_FILTER_DST
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("Invalid static route found %v", route)
|
||||
}
|
||||
|
||||
wasFound, err := netlink.RouteListFiltered(family, find, routeFilter)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Expected Route %v not route table lookup error %v", route, err)
|
||||
}
|
||||
if wasFound == nil {
|
||||
return fmt.Errorf("Expected Route %v not found in routing table", route)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -15,85 +15,19 @@
|
||||
package ipam
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"context"
|
||||
"github.com/containernetworking/cni/pkg/invoke"
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
"github.com/containernetworking/cni/pkg/types/current"
|
||||
"github.com/containernetworking/plugins/pkg/ip"
|
||||
|
||||
"github.com/vishvananda/netlink"
|
||||
)
|
||||
|
||||
func ExecAdd(plugin string, netconf []byte) (types.Result, error) {
|
||||
return invoke.DelegateAdd(plugin, netconf)
|
||||
return invoke.DelegateAdd(context.TODO(), plugin, netconf, nil)
|
||||
}
|
||||
|
||||
func ExecCheck(plugin string, netconf []byte) error {
|
||||
return invoke.DelegateCheck(context.TODO(), plugin, netconf, nil)
|
||||
}
|
||||
|
||||
func ExecDel(plugin string, netconf []byte) error {
|
||||
return invoke.DelegateDel(plugin, netconf)
|
||||
}
|
||||
|
||||
// ConfigureIface takes the result of IPAM plugin and
|
||||
// applies to the ifName interface
|
||||
func ConfigureIface(ifName string, res *current.Result) error {
|
||||
if len(res.Interfaces) == 0 {
|
||||
return fmt.Errorf("no interfaces to configure")
|
||||
}
|
||||
|
||||
link, err := netlink.LinkByName(ifName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to lookup %q: %v", ifName, err)
|
||||
}
|
||||
|
||||
if err := netlink.LinkSetUp(link); err != nil {
|
||||
return fmt.Errorf("failed to set %q UP: %v", ifName, err)
|
||||
}
|
||||
|
||||
var v4gw, v6gw net.IP
|
||||
for _, ipc := range res.IPs {
|
||||
if ipc.Interface == nil {
|
||||
continue
|
||||
}
|
||||
intIdx := *ipc.Interface
|
||||
if intIdx < 0 || intIdx >= len(res.Interfaces) || res.Interfaces[intIdx].Name != ifName {
|
||||
// IP address is for a different interface
|
||||
return fmt.Errorf("failed to add IP addr %v to %q: invalid interface index", ipc, ifName)
|
||||
}
|
||||
|
||||
addr := &netlink.Addr{IPNet: &ipc.Address, Label: ""}
|
||||
if err = netlink.AddrAdd(link, addr); err != nil {
|
||||
return fmt.Errorf("failed to add IP addr %v to %q: %v", ipc, ifName, err)
|
||||
}
|
||||
|
||||
gwIsV4 := ipc.Gateway.To4() != nil
|
||||
if gwIsV4 && v4gw == nil {
|
||||
v4gw = ipc.Gateway
|
||||
} else if !gwIsV4 && v6gw == nil {
|
||||
v6gw = ipc.Gateway
|
||||
}
|
||||
}
|
||||
|
||||
ip.SettleAddresses(ifName, 10)
|
||||
|
||||
for _, r := range res.Routes {
|
||||
routeIsV4 := r.Dst.IP.To4() != nil
|
||||
gw := r.GW
|
||||
if gw == nil {
|
||||
if routeIsV4 && v4gw != nil {
|
||||
gw = v4gw
|
||||
} else if !routeIsV4 && v6gw != nil {
|
||||
gw = v6gw
|
||||
}
|
||||
}
|
||||
if err = ip.AddRoute(&r.Dst, gw, link); err != nil {
|
||||
// we skip over duplicate routes as we assume the first one wins
|
||||
if !os.IsExist(err) {
|
||||
return fmt.Errorf("failed to add route '%v via %v dev %v': %v", r.Dst, gw, ifName, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return invoke.DelegateDel(context.TODO(), plugin, netconf, nil)
|
||||
}
|
||||
|
121
pkg/ipam/ipam_linux.go
Normal file
121
pkg/ipam/ipam_linux.go
Normal file
@ -0,0 +1,121 @@
|
||||
// Copyright 2015 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 ipam
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/types/current"
|
||||
"github.com/containernetworking/plugins/pkg/ip"
|
||||
"github.com/containernetworking/plugins/pkg/utils/sysctl"
|
||||
|
||||
"github.com/vishvananda/netlink"
|
||||
)
|
||||
|
||||
const (
|
||||
DisableIPv6SysctlTemplate = "net.ipv6.conf.%s.disable_ipv6"
|
||||
)
|
||||
|
||||
// ConfigureIface takes the result of IPAM plugin and
|
||||
// applies to the ifName interface
|
||||
func ConfigureIface(ifName string, res *current.Result) error {
|
||||
if len(res.Interfaces) == 0 {
|
||||
return fmt.Errorf("no interfaces to configure")
|
||||
}
|
||||
|
||||
link, err := netlink.LinkByName(ifName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to lookup %q: %v", ifName, err)
|
||||
}
|
||||
|
||||
if err := netlink.LinkSetUp(link); err != nil {
|
||||
return fmt.Errorf("failed to set %q UP: %v", ifName, err)
|
||||
}
|
||||
|
||||
var v4gw, v6gw net.IP
|
||||
var has_enabled_ipv6 bool = false
|
||||
for _, ipc := range res.IPs {
|
||||
if ipc.Interface == nil {
|
||||
continue
|
||||
}
|
||||
intIdx := *ipc.Interface
|
||||
if intIdx < 0 || intIdx >= len(res.Interfaces) || res.Interfaces[intIdx].Name != ifName {
|
||||
// IP address is for a different interface
|
||||
return fmt.Errorf("failed to add IP addr %v to %q: invalid interface index", ipc, ifName)
|
||||
}
|
||||
|
||||
// Make sure sysctl "disable_ipv6" is 0 if we are about to add
|
||||
// an IPv6 address to the interface
|
||||
if !has_enabled_ipv6 && ipc.Version == "6" {
|
||||
// Enabled IPv6 for loopback "lo" and the interface
|
||||
// being configured
|
||||
for _, iface := range [2]string{"lo", ifName} {
|
||||
ipv6SysctlValueName := fmt.Sprintf(DisableIPv6SysctlTemplate, iface)
|
||||
|
||||
// Read current sysctl value
|
||||
value, err := sysctl.Sysctl(ipv6SysctlValueName)
|
||||
if err != nil || value == "0" {
|
||||
// FIXME: log warning if unable to read sysctl value
|
||||
continue
|
||||
}
|
||||
|
||||
// Write sysctl to enable IPv6
|
||||
_, err = sysctl.Sysctl(ipv6SysctlValueName, "0")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to enable IPv6 for interface %q (%s=%s): %v", iface, ipv6SysctlValueName, value, err)
|
||||
}
|
||||
}
|
||||
has_enabled_ipv6 = true
|
||||
}
|
||||
|
||||
addr := &netlink.Addr{IPNet: &ipc.Address, Label: ""}
|
||||
if err = netlink.AddrAdd(link, addr); err != nil {
|
||||
return fmt.Errorf("failed to add IP addr %v to %q: %v", ipc, ifName, err)
|
||||
}
|
||||
|
||||
gwIsV4 := ipc.Gateway.To4() != nil
|
||||
if gwIsV4 && v4gw == nil {
|
||||
v4gw = ipc.Gateway
|
||||
} else if !gwIsV4 && v6gw == nil {
|
||||
v6gw = ipc.Gateway
|
||||
}
|
||||
}
|
||||
|
||||
if v6gw != nil {
|
||||
ip.SettleAddresses(ifName, 10)
|
||||
}
|
||||
|
||||
for _, r := range res.Routes {
|
||||
routeIsV4 := r.Dst.IP.To4() != nil
|
||||
gw := r.GW
|
||||
if gw == nil {
|
||||
if routeIsV4 && v4gw != nil {
|
||||
gw = v4gw
|
||||
} else if !routeIsV4 && v6gw != nil {
|
||||
gw = v6gw
|
||||
}
|
||||
}
|
||||
if err = ip.AddRoute(&r.Dst, gw, link); err != nil {
|
||||
// we skip over duplicate routes as we assume the first one wins
|
||||
if !os.IsExist(err) {
|
||||
return fmt.Errorf("failed to add route '%v via %v dev %v': %v", r.Dst, gw, ifName, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -21,6 +21,7 @@ import (
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
"github.com/containernetworking/cni/pkg/types/current"
|
||||
"github.com/containernetworking/plugins/pkg/ns"
|
||||
"github.com/containernetworking/plugins/pkg/testutils"
|
||||
|
||||
"github.com/vishvananda/netlink"
|
||||
|
||||
@ -39,7 +40,7 @@ func ipNetEqual(a, b *net.IPNet) bool {
|
||||
return a.IP.Equal(b.IP)
|
||||
}
|
||||
|
||||
var _ = Describe("IPAM Operations", func() {
|
||||
var _ = Describe("ConfigureIface", func() {
|
||||
var originalNS ns.NetNS
|
||||
var ipv4, ipv6, routev4, routev6 *net.IPNet
|
||||
var ipgw4, ipgw6, routegwv4, routegwv6 net.IP
|
||||
@ -48,7 +49,7 @@ var _ = Describe("IPAM Operations", func() {
|
||||
BeforeEach(func() {
|
||||
// Create a new NetNS so we don't modify the host
|
||||
var err error
|
||||
originalNS, err = ns.NewNS()
|
||||
originalNS, err = testutils.NewNS()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
err = originalNS.Do(func(ns.NetNS) error {
|
@ -23,5 +23,5 @@ import (
|
||||
|
||||
func TestIpam(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Ipam Suite")
|
||||
RunSpecs(t, "pkg/ipam")
|
||||
}
|
||||
|
@ -12,10 +12,6 @@ For example, you cannot rely on the `ns.Set()` namespace being the current names
|
||||
The `ns.Do()` method provides **partial** control over network namespaces for you by implementing these strategies. All code dependent on a particular network namespace (including the root namespace) should be wrapped in the `ns.Do()` method to ensure the correct namespace is selected for the duration of your code. For example:
|
||||
|
||||
```go
|
||||
targetNs, err := ns.NewNS()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = targetNs.Do(func(hostNs ns.NetNS) error {
|
||||
dummy := &netlink.Dummy{
|
||||
LinkAttrs: netlink.LinkAttrs{
|
||||
@ -26,11 +22,16 @@ err = targetNs.Do(func(hostNs ns.NetNS) error {
|
||||
})
|
||||
```
|
||||
|
||||
Note this requirement to wrap every network call is very onerous - any libraries you call might call out to network services such as DNS, and all such calls need to be protected after you call `ns.Do()`. The CNI plugins all exit very soon after calling `ns.Do()` which helps to minimize the problem.
|
||||
Note this requirement to wrap every network call is very onerous - any libraries you call might call out to network services such as DNS, and all such calls need to be protected after you call `ns.Do()`. All goroutines spawned from within the `ns.Do` will not inherit the new namespace. The CNI plugins all exit very soon after calling `ns.Do()` which helps to minimize the problem.
|
||||
|
||||
Also: If the runtime spawns a new OS thread, it will inherit the network namespace of the parent thread, which may have been temporarily switched, and thus the new OS thread will be permanently "stuck in the wrong namespace".
|
||||
When a new thread is spawned in Linux, it inherits the namespace of its parent. In versions of go **prior to 1.10**, if the runtime spawns a new OS thread, it picks the parent randomly. If the chosen parent thread has been moved to a new namespace (even temporarily), the new OS thread will be permanently "stuck in the wrong namespace", and goroutines will non-deterministically switch namespaces as they are rescheduled.
|
||||
|
||||
In short, **there was no safe way to change network namespaces, even temporarily, from within a long-lived, multithreaded Go process**. If you wish to do this, you must use go 1.10 or greater.
|
||||
|
||||
|
||||
### Creating network namespaces
|
||||
Earlier versions of this library managed namespace creation, but as CNI does not actually utilize this feature (and it was essentially unmaintained), it was removed. If you're writing a container runtime, you should implement namespace management yourself. However, there are some gotchas when doing so, especially around handling `/var/run/netns`. A reasonably correct reference implementation, borrowed from `rkt`, can be found in `pkg/testutils/netns_linux.go` if you're in need of a source of inspiration.
|
||||
|
||||
In short, **there is no safe way to change network namespaces from within a long-lived, multithreaded Go process**. If your daemon process needs to be namespace aware, consider spawning a separate process (like a CNI plugin) for each namespace.
|
||||
|
||||
### Further Reading
|
||||
- https://github.com/golang/go/wiki/LockOSThread
|
||||
|
178
pkg/ns/ns.go
178
pkg/ns/ns.go
@ -1,178 +0,0 @@
|
||||
// Copyright 2015 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 ns
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"sync"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
type NetNS interface {
|
||||
// Executes the passed closure in this object's network namespace,
|
||||
// attempting to restore the original namespace before returning.
|
||||
// However, since each OS thread can have a different network namespace,
|
||||
// and Go's thread scheduling is highly variable, callers cannot
|
||||
// guarantee any specific namespace is set unless operations that
|
||||
// require that namespace are wrapped with Do(). Also, no code called
|
||||
// from Do() should call runtime.UnlockOSThread(), or the risk
|
||||
// of executing code in an incorrect namespace will be greater. See
|
||||
// https://github.com/golang/go/wiki/LockOSThread for further details.
|
||||
Do(toRun func(NetNS) error) error
|
||||
|
||||
// Sets the current network namespace to this object's network namespace.
|
||||
// Note that since Go's thread scheduling is highly variable, callers
|
||||
// cannot guarantee the requested namespace will be the current namespace
|
||||
// after this function is called; to ensure this wrap operations that
|
||||
// require the namespace with Do() instead.
|
||||
Set() error
|
||||
|
||||
// Returns the filesystem path representing this object's network namespace
|
||||
Path() string
|
||||
|
||||
// Returns a file descriptor representing this object's network namespace
|
||||
Fd() uintptr
|
||||
|
||||
// Cleans up this instance of the network namespace; if this instance
|
||||
// is the last user the namespace will be destroyed
|
||||
Close() error
|
||||
}
|
||||
|
||||
type netNS struct {
|
||||
file *os.File
|
||||
mounted bool
|
||||
closed bool
|
||||
}
|
||||
|
||||
// netNS implements the NetNS interface
|
||||
var _ NetNS = &netNS{}
|
||||
|
||||
const (
|
||||
// https://github.com/torvalds/linux/blob/master/include/uapi/linux/magic.h
|
||||
NSFS_MAGIC = 0x6e736673
|
||||
PROCFS_MAGIC = 0x9fa0
|
||||
)
|
||||
|
||||
type NSPathNotExistErr struct{ msg string }
|
||||
|
||||
func (e NSPathNotExistErr) Error() string { return e.msg }
|
||||
|
||||
type NSPathNotNSErr struct{ msg string }
|
||||
|
||||
func (e NSPathNotNSErr) Error() string { return e.msg }
|
||||
|
||||
func IsNSorErr(nspath string) error {
|
||||
stat := syscall.Statfs_t{}
|
||||
if err := syscall.Statfs(nspath, &stat); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
err = NSPathNotExistErr{msg: fmt.Sprintf("failed to Statfs %q: %v", nspath, err)}
|
||||
} else {
|
||||
err = fmt.Errorf("failed to Statfs %q: %v", nspath, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
switch stat.Type {
|
||||
case PROCFS_MAGIC, NSFS_MAGIC:
|
||||
return nil
|
||||
default:
|
||||
return NSPathNotNSErr{msg: fmt.Sprintf("unknown FS magic on %q: %x", nspath, stat.Type)}
|
||||
}
|
||||
}
|
||||
|
||||
// Returns an object representing the namespace referred to by @path
|
||||
func GetNS(nspath string) (NetNS, error) {
|
||||
err := IsNSorErr(nspath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fd, err := os.Open(nspath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &netNS{file: fd}, nil
|
||||
}
|
||||
|
||||
func (ns *netNS) Path() string {
|
||||
return ns.file.Name()
|
||||
}
|
||||
|
||||
func (ns *netNS) Fd() uintptr {
|
||||
return ns.file.Fd()
|
||||
}
|
||||
|
||||
func (ns *netNS) errorIfClosed() error {
|
||||
if ns.closed {
|
||||
return fmt.Errorf("%q has already been closed", ns.file.Name())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ns *netNS) Do(toRun func(NetNS) error) error {
|
||||
if err := ns.errorIfClosed(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
containedCall := func(hostNS NetNS) error {
|
||||
threadNS, err := GetCurrentNS()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open current netns: %v", err)
|
||||
}
|
||||
defer threadNS.Close()
|
||||
|
||||
// switch to target namespace
|
||||
if err = ns.Set(); err != nil {
|
||||
return fmt.Errorf("error switching to ns %v: %v", ns.file.Name(), err)
|
||||
}
|
||||
defer threadNS.Set() // switch back
|
||||
|
||||
return toRun(hostNS)
|
||||
}
|
||||
|
||||
// save a handle to current network namespace
|
||||
hostNS, err := GetCurrentNS()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to open current namespace: %v", err)
|
||||
}
|
||||
defer hostNS.Close()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
|
||||
var innerError error
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
runtime.LockOSThread()
|
||||
innerError = containedCall(hostNS)
|
||||
}()
|
||||
wg.Wait()
|
||||
|
||||
return innerError
|
||||
}
|
||||
|
||||
// WithNetNSPath executes the passed closure under the given network
|
||||
// namespace, restoring the original namespace afterwards.
|
||||
func WithNetNSPath(nspath string, toRun func(NetNS) error) error {
|
||||
ns, err := GetNS(nspath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer ns.Close()
|
||||
return ns.Do(toRun)
|
||||
}
|
@ -15,18 +15,22 @@
|
||||
package ns
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"runtime"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// Returns an object representing the current OS thread's network namespace
|
||||
func GetCurrentNS() (NetNS, error) {
|
||||
// Lock the thread in case other goroutine executes in it and changes its
|
||||
// network namespace after getCurrentThreadNetNSPath(), otherwise it might
|
||||
// return an unexpected network namespace.
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
return GetNS(getCurrentThreadNetNSPath())
|
||||
}
|
||||
|
||||
@ -37,82 +41,6 @@ func getCurrentThreadNetNSPath() string {
|
||||
return fmt.Sprintf("/proc/%d/task/%d/ns/net", os.Getpid(), unix.Gettid())
|
||||
}
|
||||
|
||||
// Creates a new persistent network namespace and returns an object
|
||||
// representing that namespace, without switching to it
|
||||
func NewNS() (NetNS, error) {
|
||||
const nsRunDir = "/var/run/netns"
|
||||
|
||||
b := make([]byte, 16)
|
||||
_, err := rand.Reader.Read(b)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate random netns name: %v", err)
|
||||
}
|
||||
|
||||
err = os.MkdirAll(nsRunDir, 0755)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// create an empty file at the mount point
|
||||
nsName := fmt.Sprintf("cni-%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:])
|
||||
nsPath := path.Join(nsRunDir, nsName)
|
||||
mountPointFd, err := os.Create(nsPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mountPointFd.Close()
|
||||
|
||||
// Ensure the mount point is cleaned up on errors; if the namespace
|
||||
// was successfully mounted this will have no effect because the file
|
||||
// is in-use
|
||||
defer os.RemoveAll(nsPath)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
|
||||
// do namespace work in a dedicated goroutine, so that we can safely
|
||||
// Lock/Unlock OSThread without upsetting the lock/unlock state of
|
||||
// the caller of this function
|
||||
var fd *os.File
|
||||
go (func() {
|
||||
defer wg.Done()
|
||||
runtime.LockOSThread()
|
||||
|
||||
var origNS NetNS
|
||||
origNS, err = GetNS(getCurrentThreadNetNSPath())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer origNS.Close()
|
||||
|
||||
// create a new netns on the current thread
|
||||
err = unix.Unshare(unix.CLONE_NEWNET)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer origNS.Set()
|
||||
|
||||
// bind mount the new netns from the current thread onto the mount point
|
||||
err = unix.Mount(getCurrentThreadNetNSPath(), nsPath, "none", unix.MS_BIND, "")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
fd, err = os.Open(nsPath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
})()
|
||||
wg.Wait()
|
||||
|
||||
if err != nil {
|
||||
unix.Unmount(nsPath, unix.MNT_DETACH)
|
||||
return nil, fmt.Errorf("failed to create namespace: %v", err)
|
||||
}
|
||||
|
||||
return &netNS{file: fd, mounted: true}, nil
|
||||
}
|
||||
|
||||
func (ns *netNS) Close() error {
|
||||
if err := ns.errorIfClosed(); err != nil {
|
||||
return err
|
||||
@ -123,16 +51,6 @@ func (ns *netNS) Close() error {
|
||||
}
|
||||
ns.closed = true
|
||||
|
||||
if ns.mounted {
|
||||
if err := unix.Unmount(ns.file.Name(), unix.MNT_DETACH); err != nil {
|
||||
return fmt.Errorf("Failed to unmount namespace %s: %v", ns.file.Name(), err)
|
||||
}
|
||||
if err := os.RemoveAll(ns.file.Name()); err != nil {
|
||||
return fmt.Errorf("Failed to clean up namespace %s: %v", ns.file.Name(), err)
|
||||
}
|
||||
ns.mounted = false
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -147,3 +65,170 @@ func (ns *netNS) Set() error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type NetNS interface {
|
||||
// Executes the passed closure in this object's network namespace,
|
||||
// attempting to restore the original namespace before returning.
|
||||
// However, since each OS thread can have a different network namespace,
|
||||
// and Go's thread scheduling is highly variable, callers cannot
|
||||
// guarantee any specific namespace is set unless operations that
|
||||
// require that namespace are wrapped with Do(). Also, no code called
|
||||
// from Do() should call runtime.UnlockOSThread(), or the risk
|
||||
// of executing code in an incorrect namespace will be greater. See
|
||||
// https://github.com/golang/go/wiki/LockOSThread for further details.
|
||||
Do(toRun func(NetNS) error) error
|
||||
|
||||
// Sets the current network namespace to this object's network namespace.
|
||||
// Note that since Go's thread scheduling is highly variable, callers
|
||||
// cannot guarantee the requested namespace will be the current namespace
|
||||
// after this function is called; to ensure this wrap operations that
|
||||
// require the namespace with Do() instead.
|
||||
Set() error
|
||||
|
||||
// Returns the filesystem path representing this object's network namespace
|
||||
Path() string
|
||||
|
||||
// Returns a file descriptor representing this object's network namespace
|
||||
Fd() uintptr
|
||||
|
||||
// Cleans up this instance of the network namespace; if this instance
|
||||
// is the last user the namespace will be destroyed
|
||||
Close() error
|
||||
}
|
||||
|
||||
type netNS struct {
|
||||
file *os.File
|
||||
closed bool
|
||||
}
|
||||
|
||||
// netNS implements the NetNS interface
|
||||
var _ NetNS = &netNS{}
|
||||
|
||||
const (
|
||||
// https://github.com/torvalds/linux/blob/master/include/uapi/linux/magic.h
|
||||
NSFS_MAGIC = 0x6e736673
|
||||
PROCFS_MAGIC = 0x9fa0
|
||||
)
|
||||
|
||||
type NSPathNotExistErr struct{ msg string }
|
||||
|
||||
func (e NSPathNotExistErr) Error() string { return e.msg }
|
||||
|
||||
type NSPathNotNSErr struct{ msg string }
|
||||
|
||||
func (e NSPathNotNSErr) Error() string { return e.msg }
|
||||
|
||||
func IsNSorErr(nspath string) error {
|
||||
stat := syscall.Statfs_t{}
|
||||
if err := syscall.Statfs(nspath, &stat); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
err = NSPathNotExistErr{msg: fmt.Sprintf("failed to Statfs %q: %v", nspath, err)}
|
||||
} else {
|
||||
err = fmt.Errorf("failed to Statfs %q: %v", nspath, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
switch stat.Type {
|
||||
case PROCFS_MAGIC, NSFS_MAGIC:
|
||||
return nil
|
||||
default:
|
||||
return NSPathNotNSErr{msg: fmt.Sprintf("unknown FS magic on %q: %x", nspath, stat.Type)}
|
||||
}
|
||||
}
|
||||
|
||||
// Returns an object representing the namespace referred to by @path
|
||||
func GetNS(nspath string) (NetNS, error) {
|
||||
err := IsNSorErr(nspath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fd, err := os.Open(nspath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &netNS{file: fd}, nil
|
||||
}
|
||||
|
||||
func (ns *netNS) Path() string {
|
||||
return ns.file.Name()
|
||||
}
|
||||
|
||||
func (ns *netNS) Fd() uintptr {
|
||||
return ns.file.Fd()
|
||||
}
|
||||
|
||||
func (ns *netNS) errorIfClosed() error {
|
||||
if ns.closed {
|
||||
return fmt.Errorf("%q has already been closed", ns.file.Name())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ns *netNS) Do(toRun func(NetNS) error) error {
|
||||
if err := ns.errorIfClosed(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
containedCall := func(hostNS NetNS) error {
|
||||
threadNS, err := GetCurrentNS()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open current netns: %v", err)
|
||||
}
|
||||
defer threadNS.Close()
|
||||
|
||||
// switch to target namespace
|
||||
if err = ns.Set(); err != nil {
|
||||
return fmt.Errorf("error switching to ns %v: %v", ns.file.Name(), err)
|
||||
}
|
||||
defer func() {
|
||||
err := threadNS.Set() // switch back
|
||||
if err == nil {
|
||||
// Unlock the current thread only when we successfully switched back
|
||||
// to the original namespace; otherwise leave the thread locked which
|
||||
// will force the runtime to scrap the current thread, that is maybe
|
||||
// not as optimal but at least always safe to do.
|
||||
runtime.UnlockOSThread()
|
||||
}
|
||||
}()
|
||||
|
||||
return toRun(hostNS)
|
||||
}
|
||||
|
||||
// save a handle to current network namespace
|
||||
hostNS, err := GetCurrentNS()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to open current namespace: %v", err)
|
||||
}
|
||||
defer hostNS.Close()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
|
||||
// Start the callback in a new green thread so that if we later fail
|
||||
// to switch the namespace back to the original one, we can safely
|
||||
// leave the thread locked to die without a risk of the current thread
|
||||
// left lingering with incorrect namespace.
|
||||
var innerError error
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
runtime.LockOSThread()
|
||||
innerError = containedCall(hostNS)
|
||||
}()
|
||||
wg.Wait()
|
||||
|
||||
return innerError
|
||||
}
|
||||
|
||||
// WithNetNSPath executes the passed closure under the given network
|
||||
// namespace, restoring the original namespace afterwards.
|
||||
func WithNetNSPath(nspath string, toRun func(NetNS) error) error {
|
||||
ns, err := GetNS(nspath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer ns.Close()
|
||||
return ns.Do(toRun)
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
// Copyright 2016 CNI authors
|
||||
// Copyright 2016-2018 CNI authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@ -20,8 +20,10 @@ import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"github.com/containernetworking/plugins/pkg/ns"
|
||||
"github.com/containernetworking/plugins/pkg/testutils"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
"golang.org/x/sys/unix"
|
||||
@ -65,16 +67,19 @@ var _ = Describe("Linux namespace operations", func() {
|
||||
BeforeEach(func() {
|
||||
var err error
|
||||
|
||||
originalNetNS, err = ns.NewNS()
|
||||
originalNetNS, err = testutils.NewNS()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
targetNetNS, err = ns.NewNS()
|
||||
targetNetNS, err = testutils.NewNS()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
Expect(targetNetNS.Close()).To(Succeed())
|
||||
Expect(originalNetNS.Close()).To(Succeed())
|
||||
targetNetNS.Close()
|
||||
originalNetNS.Close()
|
||||
|
||||
Expect(testutils.UnmountNS(targetNetNS)).To(Succeed())
|
||||
Expect(testutils.UnmountNS(originalNetNS)).To(Succeed())
|
||||
})
|
||||
|
||||
It("executes the callback within the target network namespace", func() {
|
||||
@ -108,11 +113,39 @@ var _ = Describe("Linux namespace operations", func() {
|
||||
Expect(hostNSInode).To(Equal(origNSInode))
|
||||
return nil
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
return nil
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
|
||||
Context("when called concurrently", func() {
|
||||
It("provides the original namespace as the argument to the callback", func() {
|
||||
concurrency := 200
|
||||
origNS, err := ns.GetCurrentNS()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
origNSInode, err := getInodeNS(origNS)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(concurrency)
|
||||
for i := 0; i < concurrency; i++ {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
targetNetNS.Do(func(hostNS ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
hostNSInode, err := getInodeNS(hostNS)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(hostNSInode).To(Equal(origNSInode))
|
||||
return nil
|
||||
})
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
})
|
||||
})
|
||||
|
||||
Context("when the callback returns an error", func() {
|
||||
It("restores the calling thread to the original namespace before returning", func() {
|
||||
err := originalNetNS.Do(func(ns.NetNS) error {
|
||||
@ -155,18 +188,21 @@ var _ = Describe("Linux namespace operations", func() {
|
||||
|
||||
It("should not leak a closed netns onto any threads in the process", func() {
|
||||
By("creating a new netns")
|
||||
createdNetNS, err := ns.NewNS()
|
||||
createdNetNS, err := testutils.NewNS()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("discovering the inode of the created netns")
|
||||
createdNetNSInode, err := getInodeNS(createdNetNS)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
createdNetNS.Close()
|
||||
Expect(testutils.UnmountNS(createdNetNS)).NotTo(HaveOccurred())
|
||||
|
||||
By("comparing against the netns inode of every thread in the process")
|
||||
for _, netnsPath := range allNetNSInCurrentProcess() {
|
||||
netnsInode, err := getInode(netnsPath)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
if !os.IsNotExist(err) {
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
}
|
||||
Expect(netnsInode).NotTo(Equal(createdNetNSInode))
|
||||
}
|
||||
})
|
||||
@ -188,7 +224,8 @@ var _ = Describe("Linux namespace operations", func() {
|
||||
|
||||
Describe("closing a network namespace", func() {
|
||||
It("should prevent further operations", func() {
|
||||
createdNetNS, err := ns.NewNS()
|
||||
createdNetNS, err := testutils.NewNS()
|
||||
defer testutils.UnmountNS(createdNetNS)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
err = createdNetNS.Close()
|
||||
@ -202,8 +239,9 @@ var _ = Describe("Linux namespace operations", func() {
|
||||
})
|
||||
|
||||
It("should only work once", func() {
|
||||
createdNetNS, err := ns.NewNS()
|
||||
createdNetNS, err := testutils.NewNS()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
defer testutils.UnmountNS(createdNetNS)
|
||||
|
||||
err = createdNetNS.Close()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
@ -216,7 +254,9 @@ var _ = Describe("Linux namespace operations", func() {
|
||||
|
||||
Describe("IsNSorErr", func() {
|
||||
It("should detect a namespace", func() {
|
||||
createdNetNS, err := ns.NewNS()
|
||||
createdNetNS, err := testutils.NewNS()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
defer testutils.UnmountNS(createdNetNS)
|
||||
err = ns.IsNSorErr(createdNetNS.Path())
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
@ -30,5 +30,5 @@ func TestNs(t *testing.T) {
|
||||
runtime.LockOSThread()
|
||||
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "pkg/ns Suite")
|
||||
RunSpecs(t, "pkg/ns")
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/skel"
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
"github.com/containernetworking/cni/pkg/version"
|
||||
)
|
||||
@ -27,13 +28,15 @@ func envCleanup() {
|
||||
os.Unsetenv("CNI_PATH")
|
||||
os.Unsetenv("CNI_NETNS")
|
||||
os.Unsetenv("CNI_IFNAME")
|
||||
os.Unsetenv("CNI_CONTAINERID")
|
||||
}
|
||||
|
||||
func CmdAddWithResult(cniNetns, cniIfname string, conf []byte, f func() error) (types.Result, []byte, error) {
|
||||
func CmdAdd(cniNetns, cniContainerID, cniIfname string, conf []byte, f func() error) (types.Result, []byte, error) {
|
||||
os.Setenv("CNI_COMMAND", "ADD")
|
||||
os.Setenv("CNI_PATH", os.Getenv("PATH"))
|
||||
os.Setenv("CNI_NETNS", cniNetns)
|
||||
os.Setenv("CNI_IFNAME", cniIfname)
|
||||
os.Setenv("CNI_CONTAINERID", cniContainerID)
|
||||
defer envCleanup()
|
||||
|
||||
// Redirect stdout to capture plugin result
|
||||
@ -74,12 +77,36 @@ func CmdAddWithResult(cniNetns, cniIfname string, conf []byte, f func() error) (
|
||||
return result, out, nil
|
||||
}
|
||||
|
||||
func CmdDelWithResult(cniNetns, cniIfname string, f func() error) error {
|
||||
os.Setenv("CNI_COMMAND", "DEL")
|
||||
func CmdAddWithArgs(args *skel.CmdArgs, f func() error) (types.Result, []byte, error) {
|
||||
return CmdAdd(args.Netns, args.ContainerID, args.IfName, args.StdinData, f)
|
||||
}
|
||||
|
||||
func CmdCheck(cniNetns, cniContainerID, cniIfname string, conf []byte, f func() error) error {
|
||||
os.Setenv("CNI_COMMAND", "CHECK")
|
||||
os.Setenv("CNI_PATH", os.Getenv("PATH"))
|
||||
os.Setenv("CNI_NETNS", cniNetns)
|
||||
os.Setenv("CNI_IFNAME", cniIfname)
|
||||
os.Setenv("CNI_CONTAINERID", cniContainerID)
|
||||
defer envCleanup()
|
||||
|
||||
return f()
|
||||
}
|
||||
|
||||
func CmdCheckWithArgs(args *skel.CmdArgs, f func() error) error {
|
||||
return CmdCheck(args.Netns, args.ContainerID, args.IfName, args.StdinData, f)
|
||||
}
|
||||
|
||||
func CmdDel(cniNetns, cniContainerID, cniIfname string, f func() error) error {
|
||||
os.Setenv("CNI_COMMAND", "DEL")
|
||||
os.Setenv("CNI_PATH", os.Getenv("PATH"))
|
||||
os.Setenv("CNI_NETNS", cniNetns)
|
||||
os.Setenv("CNI_IFNAME", cniIfname)
|
||||
os.Setenv("CNI_CONTAINERID", cniContainerID)
|
||||
defer envCleanup()
|
||||
|
||||
return f()
|
||||
}
|
||||
|
||||
func CmdDelWithArgs(args *skel.CmdArgs, f func() error) error {
|
||||
return CmdDel(args.Netns, args.ContainerID, args.IfName, f)
|
||||
}
|
||||
|
60
pkg/testutils/dns.go
Normal file
60
pkg/testutils/dns.go
Normal file
@ -0,0 +1,60 @@
|
||||
// Copyright 2019 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 testutils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
)
|
||||
|
||||
// TmpResolvConf will create a temporary file and write the provided DNS settings to
|
||||
// it in the resolv.conf format. It returns the path of the created temporary file or
|
||||
// an error if any occurs while creating/writing the file. It is the caller's
|
||||
// responsibility to remove the file.
|
||||
func TmpResolvConf(dnsConf types.DNS) (string, error) {
|
||||
f, err := ioutil.TempFile("", "cni_test_resolv.conf")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get temp file for CNI test resolv.conf: %v", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
path := f.Name()
|
||||
defer func() {
|
||||
if err != nil {
|
||||
os.RemoveAll(path)
|
||||
}
|
||||
}()
|
||||
|
||||
// see "man 5 resolv.conf" for the format of resolv.conf
|
||||
var resolvConfLines []string
|
||||
for _, nameserver := range dnsConf.Nameservers {
|
||||
resolvConfLines = append(resolvConfLines, fmt.Sprintf("nameserver %s", nameserver))
|
||||
}
|
||||
resolvConfLines = append(resolvConfLines, fmt.Sprintf("domain %s", dnsConf.Domain))
|
||||
resolvConfLines = append(resolvConfLines, fmt.Sprintf("search %s", strings.Join(dnsConf.Search, " ")))
|
||||
resolvConfLines = append(resolvConfLines, fmt.Sprintf("options %s", strings.Join(dnsConf.Options, " ")))
|
||||
|
||||
resolvConf := strings.Join(resolvConfLines, "\n")
|
||||
_, err = f.Write([]byte(resolvConf))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to write temp resolv.conf for CNI test: %v", err)
|
||||
}
|
||||
|
||||
return path, err
|
||||
}
|
90
pkg/testutils/echo/client/client.go
Normal file
90
pkg/testutils/echo/client/client.go
Normal file
@ -0,0 +1,90 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
)
|
||||
|
||||
func main() {
|
||||
target := flag.String("target", "", "the server address")
|
||||
payload := flag.String("message", "", "the message to send to the server")
|
||||
protocol := flag.String("protocol", "tcp", "the protocol to use with the server [udp,tcp], default tcp")
|
||||
flag.Parse()
|
||||
|
||||
if *target == "" || *payload == "" {
|
||||
flag.Usage()
|
||||
panic("invalid arguments")
|
||||
}
|
||||
|
||||
switch *protocol {
|
||||
case "tcp":
|
||||
connectTCP(*target, *payload)
|
||||
case "udp":
|
||||
connectUDP(*target, *payload)
|
||||
default:
|
||||
panic("invalid protocol")
|
||||
}
|
||||
}
|
||||
|
||||
func connectTCP(target, payload string) {
|
||||
conn, err := net.Dial("tcp", target)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Failed to open connection to [%s] %v", target, err))
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
_, err = conn.Write([]byte(payload))
|
||||
if err != nil {
|
||||
panic("Failed to send payload")
|
||||
}
|
||||
_, err = conn.Write([]byte("\n"))
|
||||
if err != nil {
|
||||
panic("Failed to send payload")
|
||||
}
|
||||
buf := make([]byte, 1024)
|
||||
for {
|
||||
n, err := conn.Read(buf)
|
||||
fmt.Print(string(buf[:n]))
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
panic("Failed to read from socket")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// UDP uses a constant source port to trigger conntrack problems
|
||||
func connectUDP(target, payload string) {
|
||||
LocalAddr, err := net.ResolveUDPAddr("udp", ":54321")
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Failed to resolve UDP local address on port 54321 %v", err))
|
||||
}
|
||||
RemoteAddr, err := net.ResolveUDPAddr("udp", target)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Failed to resolve UDP remote address [%s] %v", target, err))
|
||||
}
|
||||
conn, err := net.DialUDP("udp", LocalAddr, RemoteAddr)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Failed to open connection to [%s] %v", target, err))
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
_, err = conn.Write([]byte(payload))
|
||||
if err != nil {
|
||||
panic("Failed to send payload")
|
||||
}
|
||||
_, err = conn.Write([]byte("\n"))
|
||||
if err != nil {
|
||||
panic("Failed to send payload")
|
||||
}
|
||||
|
||||
buf := make([]byte, 1024)
|
||||
n, err := conn.Read(buf)
|
||||
if err != nil {
|
||||
panic("Failed to read from socket")
|
||||
}
|
||||
fmt.Print(string(buf[:n]))
|
||||
}
|
98
pkg/testutils/echo/echo_test.go
Normal file
98
pkg/testutils/echo/echo_test.go
Normal file
@ -0,0 +1,98 @@
|
||||
package main_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/onsi/gomega/gbytes"
|
||||
"github.com/onsi/gomega/gexec"
|
||||
)
|
||||
|
||||
var serverBinaryPath, clientBinaryPath string
|
||||
|
||||
var _ = SynchronizedBeforeSuite(func() []byte {
|
||||
serverBinaryPath, err := gexec.Build("github.com/containernetworking/plugins/pkg/testutils/echo/server")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
clientBinaryPath, err := gexec.Build("github.com/containernetworking/plugins/pkg/testutils/echo/client")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
return []byte(strings.Join([]string{serverBinaryPath, clientBinaryPath}, ","))
|
||||
}, func(data []byte) {
|
||||
binaries := strings.Split(string(data), ",")
|
||||
serverBinaryPath = binaries[0]
|
||||
clientBinaryPath = binaries[1]
|
||||
})
|
||||
|
||||
var _ = SynchronizedAfterSuite(func() {}, func() {
|
||||
gexec.CleanupBuildArtifacts()
|
||||
})
|
||||
|
||||
var _ = Describe("Echosvr", func() {
|
||||
var session *gexec.Session
|
||||
BeforeEach(func() {
|
||||
var err error
|
||||
cmd := exec.Command(serverBinaryPath)
|
||||
session, err = gexec.Start(cmd, GinkgoWriter, GinkgoWriter)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
session.Kill().Wait()
|
||||
})
|
||||
|
||||
Context("Server test", func() {
|
||||
It("starts and doesn't terminate immediately", func() {
|
||||
Consistently(session).ShouldNot(gexec.Exit())
|
||||
})
|
||||
|
||||
tryConnect := func() (net.Conn, error) {
|
||||
programOutput := session.Out.Contents()
|
||||
addr := strings.TrimSpace(string(programOutput))
|
||||
|
||||
conn, err := net.Dial("tcp", addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return conn, err
|
||||
}
|
||||
|
||||
It("prints its listening address to stdout", func() {
|
||||
Eventually(session.Out).Should(gbytes.Say("\n"))
|
||||
conn, err := tryConnect()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
conn.Close()
|
||||
})
|
||||
|
||||
It("will echo data back to us", func() {
|
||||
Eventually(session.Out).Should(gbytes.Say("\n"))
|
||||
conn, err := tryConnect()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
defer conn.Close()
|
||||
|
||||
fmt.Fprintf(conn, "hello\n")
|
||||
Expect(ioutil.ReadAll(conn)).To(Equal([]byte("hello")))
|
||||
})
|
||||
})
|
||||
|
||||
Context("Client Server Test", func() {
|
||||
It("starts and doesn't terminate immediately", func() {
|
||||
Consistently(session).ShouldNot(gexec.Exit())
|
||||
})
|
||||
|
||||
It("connects successfully using echo client", func() {
|
||||
Eventually(session.Out).Should(gbytes.Say("\n"))
|
||||
serverAddress := strings.TrimSpace(string(session.Out.Contents()))
|
||||
fmt.Println("Server address", string(serverAddress))
|
||||
|
||||
cmd := exec.Command(clientBinaryPath, "-target", serverAddress, "-message", "hello")
|
||||
clientSession, err := gexec.Start(cmd, GinkgoWriter, GinkgoWriter)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Eventually(clientSession.Out).Should(gbytes.Say("hello"))
|
||||
Eventually(clientSession).Should(gexec.Exit())
|
||||
})
|
||||
})
|
||||
})
|
13
pkg/testutils/echo/init_test.go
Normal file
13
pkg/testutils/echo/init_test.go
Normal file
@ -0,0 +1,13 @@
|
||||
package main_test
|
||||
|
||||
import (
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEchosvr(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "pkg/testutils/echosvr")
|
||||
}
|
86
pkg/testutils/echo/server/main.go
Normal file
86
pkg/testutils/echo/server/main.go
Normal file
@ -0,0 +1,86 @@
|
||||
// Echosvr is a simple TCP echo server
|
||||
//
|
||||
// It prints its listen address on stdout
|
||||
// 127.0.0.1:xxxxx
|
||||
// A test should wait for this line, parse it
|
||||
// and may then attempt to connect.
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Start TCP server
|
||||
listener, err := net.Listen("tcp", ":")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer listener.Close()
|
||||
// use the same port for UDP
|
||||
_, port, err := net.SplitHostPort(listener.Addr().String())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Printf("127.0.0.1:%s\n", port)
|
||||
go func() {
|
||||
for {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
go handleConnection(conn)
|
||||
}
|
||||
}()
|
||||
|
||||
// Start UDP server
|
||||
addr, err := net.ResolveUDPAddr("udp", fmt.Sprintf(":%s", port))
|
||||
if err != nil {
|
||||
log.Fatalf("Error from net.ResolveUDPAddr(): %s", err)
|
||||
}
|
||||
sock, err := net.ListenUDP("udp", addr)
|
||||
if err != nil {
|
||||
log.Fatalf("Error from ListenUDP(): %s", err)
|
||||
}
|
||||
defer sock.Close()
|
||||
|
||||
buffer := make([]byte, 1024)
|
||||
for {
|
||||
n, addr, err := sock.ReadFrom(buffer)
|
||||
if err != nil {
|
||||
log.Fatalf("Error from ReadFrom(): %s", err)
|
||||
}
|
||||
sock.SetWriteDeadline(time.Now().Add(1 * time.Minute))
|
||||
n, err = sock.WriteTo(buffer[0:n], addr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleConnection(conn net.Conn) {
|
||||
conn.SetReadDeadline(time.Now().Add(1 * time.Minute))
|
||||
content, err := bufio.NewReader(conn).ReadString('\n')
|
||||
if err != nil && err != io.EOF {
|
||||
fmt.Fprint(os.Stderr, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
conn.SetWriteDeadline(time.Now().Add(1 * time.Minute))
|
||||
if _, err = conn.Write([]byte(strings.TrimSuffix(content, "\n"))); err != nil {
|
||||
fmt.Fprint(os.Stderr, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err = conn.Close(); err != nil {
|
||||
fmt.Fprint(os.Stderr, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
176
pkg/testutils/netns_linux.go
Normal file
176
pkg/testutils/netns_linux.go
Normal file
@ -0,0 +1,176 @@
|
||||
// Copyright 2018 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 testutils
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
"github.com/containernetworking/plugins/pkg/ns"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func getNsRunDir() string {
|
||||
xdgRuntimeDir := os.Getenv("XDG_RUNTIME_DIR")
|
||||
|
||||
/// If XDG_RUNTIME_DIR is set, check if the current user owns /var/run. If
|
||||
// the owner is different, we are most likely running in a user namespace.
|
||||
// In that case use $XDG_RUNTIME_DIR/netns as runtime dir.
|
||||
if xdgRuntimeDir != "" {
|
||||
if s, err := os.Stat("/var/run"); err == nil {
|
||||
st, ok := s.Sys().(*syscall.Stat_t)
|
||||
if ok && int(st.Uid) != os.Geteuid() {
|
||||
return path.Join(xdgRuntimeDir, "netns")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "/var/run/netns"
|
||||
}
|
||||
|
||||
// Creates a new persistent (bind-mounted) network namespace and returns an object
|
||||
// representing that namespace, without switching to it.
|
||||
func NewNS() (ns.NetNS, error) {
|
||||
|
||||
nsRunDir := getNsRunDir()
|
||||
|
||||
b := make([]byte, 16)
|
||||
_, err := rand.Reader.Read(b)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate random netns name: %v", err)
|
||||
}
|
||||
|
||||
// Create the directory for mounting network namespaces
|
||||
// This needs to be a shared mountpoint in case it is mounted in to
|
||||
// other namespaces (containers)
|
||||
err = os.MkdirAll(nsRunDir, 0755)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Remount the namespace directory shared. This will fail if it is not
|
||||
// already a mountpoint, so bind-mount it on to itself to "upgrade" it
|
||||
// to a mountpoint.
|
||||
err = unix.Mount("", nsRunDir, "none", unix.MS_SHARED|unix.MS_REC, "")
|
||||
if err != nil {
|
||||
if err != unix.EINVAL {
|
||||
return nil, fmt.Errorf("mount --make-rshared %s failed: %q", nsRunDir, err)
|
||||
}
|
||||
|
||||
// Recursively remount /var/run/netns on itself. The recursive flag is
|
||||
// so that any existing netns bindmounts are carried over.
|
||||
err = unix.Mount(nsRunDir, nsRunDir, "none", unix.MS_BIND|unix.MS_REC, "")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("mount --rbind %s %s failed: %q", nsRunDir, nsRunDir, err)
|
||||
}
|
||||
|
||||
// Now we can make it shared
|
||||
err = unix.Mount("", nsRunDir, "none", unix.MS_SHARED|unix.MS_REC, "")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("mount --make-rshared %s failed: %q", nsRunDir, err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
nsName := fmt.Sprintf("cnitest-%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:])
|
||||
|
||||
// create an empty file at the mount point
|
||||
nsPath := path.Join(nsRunDir, nsName)
|
||||
mountPointFd, err := os.Create(nsPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mountPointFd.Close()
|
||||
|
||||
// Ensure the mount point is cleaned up on errors; if the namespace
|
||||
// was successfully mounted this will have no effect because the file
|
||||
// is in-use
|
||||
defer os.RemoveAll(nsPath)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
|
||||
// do namespace work in a dedicated goroutine, so that we can safely
|
||||
// Lock/Unlock OSThread without upsetting the lock/unlock state of
|
||||
// the caller of this function
|
||||
go (func() {
|
||||
defer wg.Done()
|
||||
runtime.LockOSThread()
|
||||
// Don't unlock. By not unlocking, golang will kill the OS thread when the
|
||||
// goroutine is done (for go1.10+)
|
||||
|
||||
var origNS ns.NetNS
|
||||
origNS, err = ns.GetNS(getCurrentThreadNetNSPath())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer origNS.Close()
|
||||
|
||||
// create a new netns on the current thread
|
||||
err = unix.Unshare(unix.CLONE_NEWNET)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Put this thread back to the orig ns, since it might get reused (pre go1.10)
|
||||
defer origNS.Set()
|
||||
|
||||
// bind mount the netns from the current thread (from /proc) onto the
|
||||
// mount point. This causes the namespace to persist, even when there
|
||||
// are no threads in the ns.
|
||||
err = unix.Mount(getCurrentThreadNetNSPath(), nsPath, "none", unix.MS_BIND, "")
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to bind mount ns at %s: %v", nsPath, err)
|
||||
}
|
||||
})()
|
||||
wg.Wait()
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create namespace: %v", err)
|
||||
}
|
||||
|
||||
return ns.GetNS(nsPath)
|
||||
}
|
||||
|
||||
// UnmountNS unmounts the NS held by the netns object
|
||||
func UnmountNS(ns ns.NetNS) error {
|
||||
nsPath := ns.Path()
|
||||
// Only unmount if it's been bind-mounted (don't touch namespaces in /proc...)
|
||||
if strings.HasPrefix(nsPath, getNsRunDir()) {
|
||||
if err := unix.Unmount(nsPath, 0); err != nil {
|
||||
return fmt.Errorf("failed to unmount NS: at %s: %v", nsPath, err)
|
||||
}
|
||||
|
||||
if err := os.Remove(nsPath); err != nil {
|
||||
return fmt.Errorf("failed to remove ns path %s: %v", nsPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getCurrentThreadNetNSPath copied from pkg/ns
|
||||
func getCurrentThreadNetNSPath() string {
|
||||
// /proc/self/ns/net returns the namespace of the main thread, not
|
||||
// of whatever thread this goroutine is running on. Make sure we
|
||||
// use the thread's net namespace since the thread is switching around
|
||||
return fmt.Sprintf("/proc/%d/task/%d/ns/net", os.Getpid(), unix.Gettid())
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
// Copyright 2015-2017 CNI authors
|
||||
// Copyright 2019 CNI authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@ -12,25 +12,15 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// +build !linux
|
||||
// Buildversion is a destination for the linker trickery so we can auto
|
||||
// set the build-version
|
||||
package buildversion
|
||||
|
||||
package ns
|
||||
import "fmt"
|
||||
|
||||
import "github.com/containernetworking/cni/pkg/types"
|
||||
// This is overridden in the linker script
|
||||
var BuildVersion = "version unknown"
|
||||
|
||||
// Returns an object representing the current OS thread's network namespace
|
||||
func GetCurrentNS() (NetNS, error) {
|
||||
return nil, types.NotImplementedError
|
||||
}
|
||||
|
||||
func NewNS() (NetNS, error) {
|
||||
return nil, types.NotImplementedError
|
||||
}
|
||||
|
||||
func (ns *netNS) Close() error {
|
||||
return types.NotImplementedError
|
||||
}
|
||||
|
||||
func (ns *netNS) Set() error {
|
||||
return types.NotImplementedError
|
||||
func BuildString(pluginName string) string {
|
||||
return fmt.Sprintf("CNI %s plugin %s", pluginName, BuildVersion)
|
||||
}
|
73
pkg/utils/conntrack.go
Normal file
73
pkg/utils/conntrack.go
Normal file
@ -0,0 +1,73 @@
|
||||
// Copyright 2020 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 utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/vishvananda/netlink"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// Assigned Internet Protocol Numbers
|
||||
// https://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml
|
||||
const (
|
||||
PROTOCOL_TCP = 6
|
||||
PROTOCOL_UDP = 17
|
||||
PROTOCOL_SCTP = 132
|
||||
)
|
||||
|
||||
// getNetlinkFamily returns the Netlink IP family constant
|
||||
func getNetlinkFamily(isIPv6 bool) netlink.InetFamily {
|
||||
if isIPv6 {
|
||||
return unix.AF_INET6
|
||||
}
|
||||
return unix.AF_INET
|
||||
}
|
||||
|
||||
// DeleteConntrackEntriesForDstIP delete the conntrack entries for the connections
|
||||
// specified by the given destination IP and protocol
|
||||
func DeleteConntrackEntriesForDstIP(dstIP string, protocol uint8) error {
|
||||
ip := net.ParseIP(dstIP)
|
||||
if ip == nil {
|
||||
return fmt.Errorf("error deleting connection tracking state, bad IP %s", ip)
|
||||
}
|
||||
family := getNetlinkFamily(ip.To4() == nil)
|
||||
|
||||
filter := &netlink.ConntrackFilter{}
|
||||
filter.AddIP(netlink.ConntrackOrigDstIP, ip)
|
||||
filter.AddProtocol(protocol)
|
||||
|
||||
_, err := netlink.ConntrackDeleteFilter(netlink.ConntrackTable, family, filter)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error deleting connection tracking state for protocol: %d IP: %s, error: %v", protocol, ip, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteConntrackEntriesForDstPort delete the conntrack entries for the connections specified
|
||||
// by the given destination port, protocol and IP family
|
||||
func DeleteConntrackEntriesForDstPort(port uint16, protocol uint8, family netlink.InetFamily) error {
|
||||
filter := &netlink.ConntrackFilter{}
|
||||
filter.AddPort(netlink.ConntrackOrigDstPort, port)
|
||||
filter.AddProtocol(protocol)
|
||||
|
||||
_, err := netlink.ConntrackDeleteFilter(netlink.ConntrackTable, family, filter)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error deleting connection tracking state for protocol: %d Port: %d, error: %v", protocol, port, err)
|
||||
}
|
||||
return nil
|
||||
}
|
@ -23,5 +23,5 @@ import (
|
||||
|
||||
func TestHwaddr(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Hwaddr Suite")
|
||||
RunSpecs(t, "pkg/utils/hwaddr")
|
||||
}
|
||||
|
121
pkg/utils/iptables.go
Normal file
121
pkg/utils/iptables.go
Normal file
@ -0,0 +1,121 @@
|
||||
// Copyright 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 utils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/coreos/go-iptables/iptables"
|
||||
)
|
||||
|
||||
const statusChainExists = 1
|
||||
|
||||
// EnsureChain idempotently creates the iptables chain. It does not
|
||||
// return an error if the chain already exists.
|
||||
func EnsureChain(ipt *iptables.IPTables, table, chain string) error {
|
||||
if ipt == nil {
|
||||
return errors.New("failed to ensure iptable chain: IPTables was nil")
|
||||
}
|
||||
exists, err := ChainExists(ipt, table, chain)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list iptables chains: %v", err)
|
||||
}
|
||||
if !exists {
|
||||
err = ipt.NewChain(table, chain)
|
||||
if err != nil {
|
||||
eerr, eok := err.(*iptables.Error)
|
||||
if eok && eerr.ExitStatus() != statusChainExists {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ChainExists checks whether an iptables chain exists.
|
||||
func ChainExists(ipt *iptables.IPTables, table, chain string) (bool, error) {
|
||||
if ipt == nil {
|
||||
return false, errors.New("failed to check iptable chain: IPTables was nil")
|
||||
}
|
||||
chains, err := ipt.ListChains(table)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
for _, ch := range chains {
|
||||
if ch == chain {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// DeleteRule idempotently delete the iptables rule in the specified table/chain.
|
||||
// It does not return an error if the referring chain doesn't exist
|
||||
func DeleteRule(ipt *iptables.IPTables, table, chain string, rulespec ...string) error {
|
||||
if ipt == nil {
|
||||
return errors.New("failed to ensure iptable chain: IPTables was nil")
|
||||
}
|
||||
if err := ipt.Delete(table, chain, rulespec...); err != nil {
|
||||
eerr, eok := err.(*iptables.Error)
|
||||
switch {
|
||||
case eok && eerr.IsNotExist():
|
||||
// swallow here, the chain was already deleted
|
||||
return nil
|
||||
case eok && eerr.ExitStatus() == 2:
|
||||
// swallow here, invalid command line parameter because the referring rule is missing
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("Failed to delete referring rule %s %s: %v", table, chain, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteChain idempotently deletes the specified table/chain.
|
||||
// It does not return an errors if the chain does not exist
|
||||
func DeleteChain(ipt *iptables.IPTables, table, chain string) error {
|
||||
if ipt == nil {
|
||||
return errors.New("failed to ensure iptable chain: IPTables was nil")
|
||||
}
|
||||
|
||||
err := ipt.DeleteChain(table, chain)
|
||||
eerr, eok := err.(*iptables.Error)
|
||||
switch {
|
||||
case eok && eerr.IsNotExist():
|
||||
// swallow here, the chain was already deleted
|
||||
return nil
|
||||
default:
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// ClearChain idempotently clear the iptables rules in the specified table/chain.
|
||||
// If the chain does not exist, a new one will be created
|
||||
func ClearChain(ipt *iptables.IPTables, table, chain string) error {
|
||||
if ipt == nil {
|
||||
return errors.New("failed to ensure iptable chain: IPTables was nil")
|
||||
}
|
||||
err := ipt.ClearChain(table, chain)
|
||||
eerr, eok := err.(*iptables.Error)
|
||||
switch {
|
||||
case eok && eerr.IsNotExist():
|
||||
// swallow here, the chain was already deleted
|
||||
return EnsureChain(ipt, table, chain)
|
||||
default:
|
||||
return err
|
||||
}
|
||||
}
|
97
pkg/utils/iptables_test.go
Normal file
97
pkg/utils/iptables_test.go
Normal file
@ -0,0 +1,97 @@
|
||||
// Copyright 2017-2018 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 utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"runtime"
|
||||
|
||||
"github.com/containernetworking/plugins/pkg/ns"
|
||||
"github.com/containernetworking/plugins/pkg/testutils"
|
||||
"github.com/coreos/go-iptables/iptables"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
const TABLE = "filter" // We'll monkey around here
|
||||
|
||||
var _ = Describe("chain tests", func() {
|
||||
var testChain string
|
||||
var ipt *iptables.IPTables
|
||||
var cleanup func()
|
||||
|
||||
BeforeEach(func() {
|
||||
|
||||
// Save a reference to the original namespace,
|
||||
// Add a new NS
|
||||
currNs, err := ns.GetCurrentNS()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
testNs, err := testutils.NewNS()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
testChain = fmt.Sprintf("cni-test-%d", rand.Intn(10000000))
|
||||
|
||||
ipt, err = iptables.NewWithProtocol(iptables.ProtocolIPv4)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
runtime.LockOSThread()
|
||||
err = testNs.Set()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
cleanup = func() {
|
||||
if ipt == nil {
|
||||
return
|
||||
}
|
||||
ipt.ClearChain(TABLE, testChain)
|
||||
ipt.DeleteChain(TABLE, testChain)
|
||||
currNs.Set()
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
cleanup()
|
||||
})
|
||||
|
||||
Describe("EnsureChain", func() {
|
||||
It("creates chains idempotently", func() {
|
||||
err := EnsureChain(ipt, TABLE, testChain)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Create it again!
|
||||
err = EnsureChain(ipt, TABLE, testChain)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("DeleteChain", func() {
|
||||
It("delete chains idempotently", func() {
|
||||
// Create chain
|
||||
err := EnsureChain(ipt, TABLE, testChain)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Delete chain
|
||||
err = DeleteChain(ipt, TABLE, testChain)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Delete it again!
|
||||
err = DeleteChain(ipt, TABLE, testChain)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
})
|
||||
|
||||
})
|
@ -35,7 +35,7 @@ func Sysctl(name string, params ...string) (string, error) {
|
||||
}
|
||||
|
||||
func getSysctl(name string) (string, error) {
|
||||
fullName := filepath.Join("/proc/sys", strings.Replace(name, ".", "/", -1))
|
||||
fullName := filepath.Join("/proc/sys", toNormalName(name))
|
||||
fullName = filepath.Clean(fullName)
|
||||
data, err := ioutil.ReadFile(fullName)
|
||||
if err != nil {
|
||||
@ -46,7 +46,7 @@ func getSysctl(name string) (string, error) {
|
||||
}
|
||||
|
||||
func setSysctl(name, value string) (string, error) {
|
||||
fullName := filepath.Join("/proc/sys", strings.Replace(name, ".", "/", -1))
|
||||
fullName := filepath.Join("/proc/sys", toNormalName(name))
|
||||
fullName = filepath.Clean(fullName)
|
||||
if err := ioutil.WriteFile(fullName, []byte(value), 0644); err != nil {
|
||||
return "", err
|
||||
@ -54,3 +54,27 @@ func setSysctl(name, value string) (string, error) {
|
||||
|
||||
return getSysctl(name)
|
||||
}
|
||||
|
||||
// Normalize names by using slash as separator
|
||||
// Sysctl names can use dots or slashes as separator:
|
||||
// - if dots are used, dots and slashes are interchanged.
|
||||
// - if slashes are used, slashes and dots are left intact.
|
||||
// Separator in use is determined by first occurrence.
|
||||
func toNormalName(name string) string {
|
||||
interchange := false
|
||||
for _, c := range name {
|
||||
if c == '.' {
|
||||
interchange = true
|
||||
break
|
||||
}
|
||||
if c == '/' {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if interchange {
|
||||
r := strings.NewReplacer(".", "/", "/", ".")
|
||||
return r.Replace(name)
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
114
pkg/utils/sysctl/sysctl_linux_test.go
Normal file
114
pkg/utils/sysctl/sysctl_linux_test.go
Normal file
@ -0,0 +1,114 @@
|
||||
// Copyright 2017-2020 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 sysctl_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/containernetworking/plugins/pkg/ns"
|
||||
"github.com/containernetworking/plugins/pkg/testutils"
|
||||
"github.com/containernetworking/plugins/pkg/utils/sysctl"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/vishvananda/netlink"
|
||||
)
|
||||
|
||||
const (
|
||||
sysctlDotKeyTemplate = "net.ipv4.conf.%s.proxy_arp"
|
||||
sysctlSlashKeyTemplate = "net/ipv4/conf/%s/proxy_arp"
|
||||
)
|
||||
|
||||
var _ = Describe("Sysctl tests", func() {
|
||||
var testIfaceName string
|
||||
var cleanup func()
|
||||
|
||||
BeforeEach(func() {
|
||||
|
||||
// Save a reference to the original namespace,
|
||||
// Add a new NS
|
||||
currNs, err := ns.GetCurrentNS()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
testNs, err := testutils.NewNS()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
testIfaceName = fmt.Sprintf("cnitest.%d", rand.Intn(100000))
|
||||
testIface := &netlink.Dummy{
|
||||
LinkAttrs: netlink.LinkAttrs{
|
||||
Name: testIfaceName,
|
||||
Namespace: netlink.NsFd(int(testNs.Fd())),
|
||||
},
|
||||
}
|
||||
|
||||
err = netlink.LinkAdd(testIface)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
runtime.LockOSThread()
|
||||
err = testNs.Set()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
cleanup = func() {
|
||||
netlink.LinkDel(testIface)
|
||||
currNs.Set()
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
cleanup()
|
||||
})
|
||||
|
||||
Describe("Sysctl", func() {
|
||||
It("reads keys with dot separators", func() {
|
||||
sysctlIfaceName := strings.Replace(testIfaceName, ".", "/", -1)
|
||||
sysctlKey := fmt.Sprintf(sysctlDotKeyTemplate, sysctlIfaceName)
|
||||
|
||||
_, err := sysctl.Sysctl(sysctlKey)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Sysctl", func() {
|
||||
It("reads keys with slash separators", func() {
|
||||
sysctlKey := fmt.Sprintf(sysctlSlashKeyTemplate, testIfaceName)
|
||||
|
||||
_, err := sysctl.Sysctl(sysctlKey)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Sysctl", func() {
|
||||
It("writes keys with dot separators", func() {
|
||||
sysctlIfaceName := strings.Replace(testIfaceName, ".", "/", -1)
|
||||
sysctlKey := fmt.Sprintf(sysctlDotKeyTemplate, sysctlIfaceName)
|
||||
|
||||
_, err := sysctl.Sysctl(sysctlKey, "1")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Sysctl", func() {
|
||||
It("writes keys with slash separators", func() {
|
||||
sysctlKey := fmt.Sprintf(sysctlSlashKeyTemplate, testIfaceName)
|
||||
|
||||
_, err := sysctl.Sysctl(sysctlKey, "1")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
})
|
||||
|
||||
})
|
@ -1,4 +1,4 @@
|
||||
// Copyright 2015 CNI authors
|
||||
// Copyright 2017-2020 CNI authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@ -12,16 +12,16 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package ip
|
||||
package sysctl_test
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/vishvananda/netlink"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
// AddDefaultRoute sets the default route on the given gateway.
|
||||
func AddDefaultRoute(gw net.IP, dev netlink.Link) error {
|
||||
_, defNet, _ := net.ParseCIDR("0.0.0.0/0")
|
||||
return AddRoute(defNet, gw, dev)
|
||||
func TestSysctl(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Sysctl Suite")
|
||||
}
|
@ -22,16 +22,22 @@ import (
|
||||
const (
|
||||
maxChainLength = 28
|
||||
chainPrefix = "CNI-"
|
||||
prefixLength = len(chainPrefix)
|
||||
)
|
||||
|
||||
// Generates a chain name to be used with iptables.
|
||||
// Ensures that the generated chain name is exactly
|
||||
// maxChainLength chars in length
|
||||
// FormatChainName generates a chain name to be used
|
||||
// with iptables. Ensures that the generated chain
|
||||
// name is exactly maxChainLength chars in length.
|
||||
func FormatChainName(name string, id string) string {
|
||||
chainBytes := sha512.Sum512([]byte(name + id))
|
||||
chain := fmt.Sprintf("%s%x", chainPrefix, chainBytes)
|
||||
return chain[:maxChainLength]
|
||||
return MustFormatChainNameWithPrefix(name, id, "")
|
||||
}
|
||||
|
||||
// MustFormatChainNameWithPrefix generates a chain name similar
|
||||
// to FormatChainName, but adds a custom prefix between
|
||||
// chainPrefix and unique identifier. Ensures that the
|
||||
// generated chain name is exactly maxChainLength chars in length.
|
||||
// Panics if the given prefix is too long.
|
||||
func MustFormatChainNameWithPrefix(name string, id string, prefix string) string {
|
||||
return MustFormatHashWithPrefix(maxChainLength, chainPrefix+prefix, name+id)
|
||||
}
|
||||
|
||||
// FormatComment returns a comment used for easier
|
||||
@ -39,3 +45,16 @@ func FormatChainName(name string, id string) string {
|
||||
func FormatComment(name string, id string) string {
|
||||
return fmt.Sprintf("name: %q id: %q", name, id)
|
||||
}
|
||||
|
||||
const MaxHashLen = sha512.Size * 2
|
||||
|
||||
// MustFormatHashWithPrefix returns a string of given length that begins with the
|
||||
// given prefix. It is filled with entropy based on the given string toHash.
|
||||
func MustFormatHashWithPrefix(length int, prefix string, toHash string) string {
|
||||
if len(prefix) >= length || length > MaxHashLen {
|
||||
panic("invalid length")
|
||||
}
|
||||
|
||||
output := sha512.Sum512([]byte(toHash))
|
||||
return fmt.Sprintf("%s%x", prefix, output)[:length]
|
||||
}
|
||||
|
@ -23,5 +23,5 @@ import (
|
||||
|
||||
func TestUtils(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Utils Suite")
|
||||
RunSpecs(t, "pkg/utils")
|
||||
}
|
||||
|
@ -15,37 +15,151 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("Utils", func() {
|
||||
It("must format a short name", func() {
|
||||
chain := FormatChainName("test", "1234")
|
||||
Expect(len(chain)).To(Equal(maxChainLength))
|
||||
Expect(chain).To(Equal("CNI-2bbe0c48b91a7d1b8a6753a8"))
|
||||
Describe("FormatChainName", func() {
|
||||
It("must format a short name", func() {
|
||||
chain := FormatChainName("test", "1234")
|
||||
Expect(len(chain)).To(Equal(maxChainLength))
|
||||
Expect(chain).To(Equal("CNI-2bbe0c48b91a7d1b8a6753a8"))
|
||||
})
|
||||
|
||||
It("must truncate a long name", func() {
|
||||
chain := FormatChainName("testalongnamethatdoesnotmakesense", "1234")
|
||||
Expect(len(chain)).To(Equal(maxChainLength))
|
||||
Expect(chain).To(Equal("CNI-374f33fe84ab0ed84dcdebe3"))
|
||||
})
|
||||
|
||||
It("must be predictable", func() {
|
||||
chain1 := FormatChainName("testalongnamethatdoesnotmakesense", "1234")
|
||||
chain2 := FormatChainName("testalongnamethatdoesnotmakesense", "1234")
|
||||
Expect(len(chain1)).To(Equal(maxChainLength))
|
||||
Expect(len(chain2)).To(Equal(maxChainLength))
|
||||
Expect(chain1).To(Equal(chain2))
|
||||
})
|
||||
|
||||
It("must change when a character changes", func() {
|
||||
chain1 := FormatChainName("testalongnamethatdoesnotmakesense", "1234")
|
||||
chain2 := FormatChainName("testalongnamethatdoesnotmakesense", "1235")
|
||||
Expect(len(chain1)).To(Equal(maxChainLength))
|
||||
Expect(len(chain2)).To(Equal(maxChainLength))
|
||||
Expect(chain1).To(Equal("CNI-374f33fe84ab0ed84dcdebe3"))
|
||||
Expect(chain1).NotTo(Equal(chain2))
|
||||
})
|
||||
})
|
||||
|
||||
It("must truncate a long name", func() {
|
||||
chain := FormatChainName("testalongnamethatdoesnotmakesense", "1234")
|
||||
Expect(len(chain)).To(Equal(maxChainLength))
|
||||
Expect(chain).To(Equal("CNI-374f33fe84ab0ed84dcdebe3"))
|
||||
Describe("MustFormatChainNameWithPrefix", func() {
|
||||
It("generates a chain name with a prefix", func() {
|
||||
chain := MustFormatChainNameWithPrefix("test", "1234", "PREFIX-")
|
||||
Expect(len(chain)).To(Equal(maxChainLength))
|
||||
Expect(chain).To(Equal("CNI-PREFIX-2bbe0c48b91a7d1b8"))
|
||||
})
|
||||
|
||||
It("must format a short name", func() {
|
||||
chain := MustFormatChainNameWithPrefix("test", "1234", "PREFIX-")
|
||||
Expect(len(chain)).To(Equal(maxChainLength))
|
||||
Expect(chain).To(Equal("CNI-PREFIX-2bbe0c48b91a7d1b8"))
|
||||
})
|
||||
|
||||
It("must truncate a long name", func() {
|
||||
chain := MustFormatChainNameWithPrefix("testalongnamethatdoesnotmakesense", "1234", "PREFIX-")
|
||||
Expect(len(chain)).To(Equal(maxChainLength))
|
||||
Expect(chain).To(Equal("CNI-PREFIX-374f33fe84ab0ed84"))
|
||||
})
|
||||
|
||||
It("must be predictable", func() {
|
||||
chain1 := MustFormatChainNameWithPrefix("testalongnamethatdoesnotmakesense", "1234", "PREFIX-")
|
||||
chain2 := MustFormatChainNameWithPrefix("testalongnamethatdoesnotmakesense", "1234", "PREFIX-")
|
||||
Expect(len(chain1)).To(Equal(maxChainLength))
|
||||
Expect(len(chain2)).To(Equal(maxChainLength))
|
||||
Expect(chain1).To(Equal(chain2))
|
||||
})
|
||||
|
||||
It("must change when a character changes", func() {
|
||||
chain1 := MustFormatChainNameWithPrefix("testalongnamethatdoesnotmakesense", "1234", "PREFIX-")
|
||||
chain2 := MustFormatChainNameWithPrefix("testalongnamethatdoesnotmakesense", "1235", "PREFIX-")
|
||||
Expect(len(chain1)).To(Equal(maxChainLength))
|
||||
Expect(len(chain2)).To(Equal(maxChainLength))
|
||||
Expect(chain1).To(Equal("CNI-PREFIX-374f33fe84ab0ed84"))
|
||||
Expect(chain1).NotTo(Equal(chain2))
|
||||
})
|
||||
|
||||
It("panics when prefix is too large", func() {
|
||||
longPrefix := strings.Repeat("PREFIX-", 4)
|
||||
Expect(func() {
|
||||
MustFormatChainNameWithPrefix("test", "1234", longPrefix)
|
||||
}).To(Panic())
|
||||
})
|
||||
})
|
||||
|
||||
It("must be predictable", func() {
|
||||
chain1 := FormatChainName("testalongnamethatdoesnotmakesense", "1234")
|
||||
chain2 := FormatChainName("testalongnamethatdoesnotmakesense", "1234")
|
||||
Expect(len(chain1)).To(Equal(maxChainLength))
|
||||
Expect(len(chain2)).To(Equal(maxChainLength))
|
||||
Expect(chain1).To(Equal(chain2))
|
||||
Describe("MustFormatHashWithPrefix", func() {
|
||||
It("always returns a string with the given prefix", func() {
|
||||
Expect(MustFormatHashWithPrefix(10, "AAA", "some string")).To(HavePrefix("AAA"))
|
||||
Expect(MustFormatHashWithPrefix(10, "foo", "some string")).To(HavePrefix("foo"))
|
||||
Expect(MustFormatHashWithPrefix(10, "bar", "some string")).To(HavePrefix("bar"))
|
||||
})
|
||||
|
||||
It("always returns a string of the given length", func() {
|
||||
Expect(MustFormatHashWithPrefix(10, "AAA", "some string")).To(HaveLen(10))
|
||||
Expect(MustFormatHashWithPrefix(15, "AAA", "some string")).To(HaveLen(15))
|
||||
Expect(MustFormatHashWithPrefix(5, "AAA", "some string")).To(HaveLen(5))
|
||||
})
|
||||
|
||||
It("is deterministic", func() {
|
||||
val1 := MustFormatHashWithPrefix(10, "AAA", "some string")
|
||||
val2 := MustFormatHashWithPrefix(10, "AAA", "some string")
|
||||
val3 := MustFormatHashWithPrefix(10, "AAA", "some string")
|
||||
Expect(val1).To(Equal(val2))
|
||||
Expect(val1).To(Equal(val3))
|
||||
})
|
||||
|
||||
It("is (nearly) perfect (injective function)", func() {
|
||||
hashes := map[string]int{}
|
||||
|
||||
for i := 0; i < 1000; i++ {
|
||||
name := fmt.Sprintf("string %d", i)
|
||||
hashes[MustFormatHashWithPrefix(8, "", name)]++
|
||||
}
|
||||
|
||||
for key, count := range hashes {
|
||||
Expect(count).To(Equal(1), "for key "+key+" got non-unique correspondence")
|
||||
}
|
||||
})
|
||||
|
||||
assertPanicWith := func(f func(), expectedErrorMessage string) {
|
||||
defer func() {
|
||||
Expect(recover()).To(Equal(expectedErrorMessage))
|
||||
}()
|
||||
f()
|
||||
Fail("function should have panicked but did not")
|
||||
}
|
||||
|
||||
It("panics when prefix is longer than the length", func() {
|
||||
assertPanicWith(
|
||||
func() { MustFormatHashWithPrefix(3, "AAA", "some string") },
|
||||
"invalid length",
|
||||
)
|
||||
})
|
||||
|
||||
It("panics when length is not positive", func() {
|
||||
assertPanicWith(
|
||||
func() { MustFormatHashWithPrefix(0, "", "some string") },
|
||||
"invalid length",
|
||||
)
|
||||
})
|
||||
|
||||
It("panics when length is larger than MaxLen", func() {
|
||||
assertPanicWith(
|
||||
func() { MustFormatHashWithPrefix(MaxHashLen+1, "", "some string") },
|
||||
"invalid length",
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
It("must change when a character changes", func() {
|
||||
chain1 := FormatChainName("testalongnamethatdoesnotmakesense", "1234")
|
||||
chain2 := FormatChainName("testalongnamethatdoesnotmakesense", "1235")
|
||||
Expect(len(chain1)).To(Equal(maxChainLength))
|
||||
Expect(len(chain2)).To(Equal(maxChainLength))
|
||||
Expect(chain1).To(Equal("CNI-374f33fe84ab0ed84dcdebe3"))
|
||||
Expect(chain1).NotTo(Equal(chain2))
|
||||
})
|
||||
})
|
||||
|
@ -1,38 +1,4 @@
|
||||
# dhcp plugin
|
||||
|
||||
## Overview
|
||||
This document has moved to the [containernetworking/cni.dev](https://github.com/containernetworking/cni.dev) repo.
|
||||
|
||||
With dhcp plugin the containers can get an IP allocated by a DHCP server already running on your network.
|
||||
This can be especially useful with plugin types such as [macvlan](../../main/macvlan/README.md).
|
||||
Because a DHCP lease must be periodically renewed for the duration of container lifetime, a separate daemon is required to be running.
|
||||
The same plugin binary can also be run in the daemon mode.
|
||||
|
||||
## Operation
|
||||
To use the dhcp IPAM plugin, first launch the dhcp daemon:
|
||||
|
||||
```
|
||||
# Make sure the unix socket has been removed
|
||||
$ rm -f /run/cni/dhcp.sock
|
||||
$ ./dhcp daemon
|
||||
```
|
||||
|
||||
If given `-pidfile <path>` arguments after 'daemon', the dhcp plugin will write
|
||||
its PID to the given file.
|
||||
|
||||
Alternatively, you can use systemd socket activation protocol.
|
||||
Be sure that the .socket file uses /run/cni/dhcp.sock as the socket path.
|
||||
|
||||
With the daemon running, containers using the dhcp plugin can be launched.
|
||||
|
||||
## Example configuration
|
||||
|
||||
```
|
||||
{
|
||||
"ipam": {
|
||||
"type": "dhcp",
|
||||
}
|
||||
}
|
||||
|
||||
## Network configuration reference
|
||||
|
||||
* `type` (string, required): "dhcp"
|
||||
You can find it online here: https://cni.dev/plugins/ipam/dhcp/
|
||||
|
121
plugins/ipam/dhcp/client.go
Normal file
121
plugins/ipam/dhcp/client.go
Normal file
@ -0,0 +1,121 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/d2g/dhcp4"
|
||||
"github.com/d2g/dhcp4client"
|
||||
)
|
||||
|
||||
const (
|
||||
MaxDHCPLen = 576
|
||||
)
|
||||
|
||||
//Send the Discovery Packet to the Broadcast Channel
|
||||
func DhcpSendDiscoverPacket(c *dhcp4client.Client, options dhcp4.Options) (dhcp4.Packet, error) {
|
||||
discoveryPacket := c.DiscoverPacket()
|
||||
|
||||
for opt, data := range options {
|
||||
discoveryPacket.AddOption(opt, data)
|
||||
}
|
||||
|
||||
discoveryPacket.PadToMinSize()
|
||||
return discoveryPacket, c.SendPacket(discoveryPacket)
|
||||
}
|
||||
|
||||
//Send Request Based On the offer Received.
|
||||
func DhcpSendRequest(c *dhcp4client.Client, options dhcp4.Options, offerPacket *dhcp4.Packet) (dhcp4.Packet, error) {
|
||||
requestPacket := c.RequestPacket(offerPacket)
|
||||
|
||||
for opt, data := range options {
|
||||
requestPacket.AddOption(opt, data)
|
||||
}
|
||||
|
||||
requestPacket.PadToMinSize()
|
||||
|
||||
return requestPacket, c.SendPacket(requestPacket)
|
||||
}
|
||||
|
||||
//Send Decline to the received acknowledgement.
|
||||
func DhcpSendDecline(c *dhcp4client.Client, acknowledgementPacket *dhcp4.Packet, options dhcp4.Options) (dhcp4.Packet, error) {
|
||||
declinePacket := c.DeclinePacket(acknowledgementPacket)
|
||||
|
||||
for opt, data := range options {
|
||||
declinePacket.AddOption(opt, data)
|
||||
}
|
||||
|
||||
declinePacket.PadToMinSize()
|
||||
|
||||
return declinePacket, c.SendPacket(declinePacket)
|
||||
}
|
||||
|
||||
//Lets do a Full DHCP Request.
|
||||
func DhcpRequest(c *dhcp4client.Client, options dhcp4.Options) (bool, dhcp4.Packet, error) {
|
||||
discoveryPacket, err := DhcpSendDiscoverPacket(c, options)
|
||||
if err != nil {
|
||||
return false, discoveryPacket, err
|
||||
}
|
||||
|
||||
offerPacket, err := c.GetOffer(&discoveryPacket)
|
||||
if err != nil {
|
||||
return false, offerPacket, err
|
||||
}
|
||||
|
||||
requestPacket, err := DhcpSendRequest(c, options, &offerPacket)
|
||||
if err != nil {
|
||||
return false, requestPacket, err
|
||||
}
|
||||
|
||||
acknowledgement, err := c.GetAcknowledgement(&requestPacket)
|
||||
if err != nil {
|
||||
return false, acknowledgement, err
|
||||
}
|
||||
|
||||
acknowledgementOptions := acknowledgement.ParseOptions()
|
||||
if dhcp4.MessageType(acknowledgementOptions[dhcp4.OptionDHCPMessageType][0]) != dhcp4.ACK {
|
||||
return false, acknowledgement, nil
|
||||
}
|
||||
|
||||
return true, acknowledgement, nil
|
||||
}
|
||||
|
||||
//Renew a lease backed on the Acknowledgement Packet.
|
||||
//Returns Successful, The AcknoledgementPacket, Any Errors
|
||||
func DhcpRenew(c *dhcp4client.Client, acknowledgement dhcp4.Packet, options dhcp4.Options) (bool, dhcp4.Packet, error) {
|
||||
renewRequest := c.RenewalRequestPacket(&acknowledgement)
|
||||
|
||||
for opt, data := range options {
|
||||
renewRequest.AddOption(opt, data)
|
||||
}
|
||||
|
||||
renewRequest.PadToMinSize()
|
||||
|
||||
err := c.SendPacket(renewRequest)
|
||||
if err != nil {
|
||||
return false, renewRequest, err
|
||||
}
|
||||
|
||||
newAcknowledgement, err := c.GetAcknowledgement(&renewRequest)
|
||||
if err != nil {
|
||||
return false, newAcknowledgement, err
|
||||
}
|
||||
|
||||
newAcknowledgementOptions := newAcknowledgement.ParseOptions()
|
||||
if dhcp4.MessageType(newAcknowledgementOptions[dhcp4.OptionDHCPMessageType][0]) != dhcp4.ACK {
|
||||
return false, newAcknowledgement, nil
|
||||
}
|
||||
|
||||
return true, newAcknowledgement, nil
|
||||
}
|
||||
|
||||
//Release a lease backed on the Acknowledgement Packet.
|
||||
//Returns Any Errors
|
||||
func DhcpRelease(c *dhcp4client.Client, acknowledgement dhcp4.Packet, options dhcp4.Options) error {
|
||||
release := c.ReleasePacket(&acknowledgement)
|
||||
|
||||
for opt, data := range options {
|
||||
release.AddOption(opt, data)
|
||||
}
|
||||
|
||||
release.PadToMinSize()
|
||||
|
||||
return c.SendPacket(release)
|
||||
}
|
@ -34,13 +34,13 @@ import (
|
||||
)
|
||||
|
||||
const listenFdsStart = 3
|
||||
const resendCount = 3
|
||||
|
||||
var errNoMoreTries = errors.New("no more tries")
|
||||
|
||||
type DHCP struct {
|
||||
mux sync.Mutex
|
||||
leases map[string]*DHCPLease
|
||||
mux sync.Mutex
|
||||
leases map[string]*DHCPLease
|
||||
hostNetnsPrefix string
|
||||
}
|
||||
|
||||
func newDHCP() *DHCP {
|
||||
@ -49,6 +49,10 @@ func newDHCP() *DHCP {
|
||||
}
|
||||
}
|
||||
|
||||
func generateClientID(containerID string, netName string, ifName string) string {
|
||||
return containerID + "/" + netName + "/" + ifName
|
||||
}
|
||||
|
||||
// Allocate acquires an IP from a DHCP server for a specified container.
|
||||
// The acquired lease will be maintained until Release() is called.
|
||||
func (d *DHCP) Allocate(args *skel.CmdArgs, result *current.Result) error {
|
||||
@ -57,8 +61,9 @@ func (d *DHCP) Allocate(args *skel.CmdArgs, result *current.Result) error {
|
||||
return fmt.Errorf("error parsing netconf: %v", err)
|
||||
}
|
||||
|
||||
clientID := args.ContainerID + "/" + conf.Name
|
||||
l, err := AcquireLease(clientID, args.Netns, args.IfName)
|
||||
clientID := generateClientID(args.ContainerID, conf.Name, args.IfName)
|
||||
hostNetns := d.hostNetnsPrefix + args.Netns
|
||||
l, err := AcquireLease(clientID, hostNetns, args.IfName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -69,7 +74,7 @@ func (d *DHCP) Allocate(args *skel.CmdArgs, result *current.Result) error {
|
||||
return err
|
||||
}
|
||||
|
||||
d.setLease(args.ContainerID, conf.Name, l)
|
||||
d.setLease(clientID, l)
|
||||
|
||||
result.IPs = []*current.IPConfig{{
|
||||
Version: "4",
|
||||
@ -89,36 +94,46 @@ func (d *DHCP) Release(args *skel.CmdArgs, reply *struct{}) error {
|
||||
return fmt.Errorf("error parsing netconf: %v", err)
|
||||
}
|
||||
|
||||
if l := d.getLease(args.ContainerID, conf.Name); l != nil {
|
||||
clientID := generateClientID(args.ContainerID, conf.Name, args.IfName)
|
||||
if l := d.getLease(clientID); l != nil {
|
||||
l.Stop()
|
||||
return nil
|
||||
d.clearLease(clientID)
|
||||
}
|
||||
|
||||
return fmt.Errorf("lease not found: %v/%v", args.ContainerID, conf.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DHCP) getLease(contID, netName string) *DHCPLease {
|
||||
func (d *DHCP) getLease(clientID string) *DHCPLease {
|
||||
d.mux.Lock()
|
||||
defer d.mux.Unlock()
|
||||
|
||||
// TODO(eyakubovich): hash it to avoid collisions
|
||||
l, ok := d.leases[contID+netName]
|
||||
l, ok := d.leases[clientID]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return l
|
||||
}
|
||||
|
||||
func (d *DHCP) setLease(contID, netName string, l *DHCPLease) {
|
||||
func (d *DHCP) setLease(clientID string, l *DHCPLease) {
|
||||
d.mux.Lock()
|
||||
defer d.mux.Unlock()
|
||||
|
||||
// TODO(eyakubovich): hash it to avoid collisions
|
||||
d.leases[contID+netName] = l
|
||||
d.leases[clientID] = l
|
||||
}
|
||||
|
||||
func getListener() (net.Listener, error) {
|
||||
l, err := activation.Listeners(true)
|
||||
//func (d *DHCP) clearLease(contID, netName, ifName string) {
|
||||
func (d *DHCP) clearLease(clientID string) {
|
||||
d.mux.Lock()
|
||||
defer d.mux.Unlock()
|
||||
|
||||
// TODO(eyakubovich): hash it to avoid collisions
|
||||
delete(d.leases, clientID)
|
||||
}
|
||||
|
||||
func getListener(socketPath string) (net.Listener, error) {
|
||||
l, err := activation.Listeners()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -141,7 +156,7 @@ func getListener() (net.Listener, error) {
|
||||
}
|
||||
}
|
||||
|
||||
func runDaemon(pidfilePath string) error {
|
||||
func runDaemon(pidfilePath string, hostPrefix string, socketPath string) error {
|
||||
// since other goroutines (on separate threads) will change namespaces,
|
||||
// ensure the RPC server does not get scheduled onto those
|
||||
runtime.LockOSThread()
|
||||
@ -156,12 +171,13 @@ func runDaemon(pidfilePath string) error {
|
||||
}
|
||||
}
|
||||
|
||||
l, err := getListener()
|
||||
l, err := getListener(hostPrefix + socketPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error getting listener: %v", err)
|
||||
}
|
||||
|
||||
dhcp := newDHCP()
|
||||
dhcp.hostNetnsPrefix = hostPrefix
|
||||
rpc.Register(dhcp)
|
||||
rpc.HandleHTTP()
|
||||
http.Serve(l, nil)
|
||||
|
183
plugins/ipam/dhcp/dhcp2_test.go
Normal file
183
plugins/ipam/dhcp/dhcp2_test.go
Normal file
@ -0,0 +1,183 @@
|
||||
// Copyright 2015-2018 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 main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/skel"
|
||||
"github.com/containernetworking/cni/pkg/types/current"
|
||||
"github.com/containernetworking/plugins/pkg/ns"
|
||||
"github.com/containernetworking/plugins/pkg/testutils"
|
||||
|
||||
"github.com/vishvananda/netlink"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("DHCP Multiple Lease Operations", func() {
|
||||
var originalNS, targetNS ns.NetNS
|
||||
var dhcpServerStopCh chan bool
|
||||
var dhcpServerDone *sync.WaitGroup
|
||||
var clientCmd *exec.Cmd
|
||||
var socketPath string
|
||||
var tmpDir string
|
||||
var serverIP net.IPNet
|
||||
var err error
|
||||
|
||||
BeforeEach(func() {
|
||||
dhcpServerStopCh, serverIP, socketPath, originalNS, targetNS, err = dhcpSetupOriginalNS()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Move the container side to the container's NS
|
||||
err = targetNS.Do(func(_ ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
link, err := netlink.LinkByName(contVethName0)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
err = netlink.LinkSetUp(link)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
link1, err := netlink.LinkByName(contVethName1)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
err = netlink.LinkSetUp(link1)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
return nil
|
||||
})
|
||||
|
||||
// Start the DHCP server
|
||||
dhcpServerDone, err = dhcpServerStart(originalNS, net.IPv4(192, 168, 1, 5), serverIP.IP, 2, dhcpServerStopCh)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Start the DHCP client daemon
|
||||
dhcpPluginPath, err := exec.LookPath("dhcp")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
clientCmd = exec.Command(dhcpPluginPath, "daemon", "-socketpath", socketPath)
|
||||
err = clientCmd.Start()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(clientCmd.Process).NotTo(BeNil())
|
||||
|
||||
// Wait up to 15 seconds for the client socket
|
||||
Eventually(func() bool {
|
||||
_, err := os.Stat(socketPath)
|
||||
return err == nil
|
||||
}, time.Second*15, time.Second/4).Should(BeTrue())
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
dhcpServerStopCh <- true
|
||||
dhcpServerDone.Wait()
|
||||
clientCmd.Process.Kill()
|
||||
clientCmd.Wait()
|
||||
|
||||
Expect(originalNS.Close()).To(Succeed())
|
||||
Expect(targetNS.Close()).To(Succeed())
|
||||
defer os.RemoveAll(tmpDir)
|
||||
})
|
||||
|
||||
It("configures multiple links with multiple ADD/DEL", func() {
|
||||
conf := fmt.Sprintf(`{
|
||||
"cniVersion": "0.3.1",
|
||||
"name": "mynet",
|
||||
"type": "bridge",
|
||||
"bridge": "%s",
|
||||
"ipam": {
|
||||
"type": "dhcp",
|
||||
"daemonSocketPath": "%s"
|
||||
}
|
||||
}`, hostBridgeName, socketPath)
|
||||
|
||||
args := &skel.CmdArgs{
|
||||
ContainerID: "dummy",
|
||||
Netns: targetNS.Path(),
|
||||
IfName: contVethName0,
|
||||
StdinData: []byte(conf),
|
||||
}
|
||||
|
||||
var addResult *current.Result
|
||||
err := originalNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
r, _, err := testutils.CmdAddWithArgs(args, func() error {
|
||||
return cmdAdd(args)
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
addResult, err = current.GetResult(r)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(len(addResult.IPs)).To(Equal(1))
|
||||
Expect(addResult.IPs[0].Address.String()).To(Equal("192.168.1.5/24"))
|
||||
return nil
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
args = &skel.CmdArgs{
|
||||
ContainerID: "dummy",
|
||||
Netns: targetNS.Path(),
|
||||
IfName: contVethName1,
|
||||
StdinData: []byte(conf),
|
||||
}
|
||||
|
||||
err = originalNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
r, _, err := testutils.CmdAddWithArgs(args, func() error {
|
||||
return cmdAdd(args)
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
addResult, err = current.GetResult(r)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(len(addResult.IPs)).To(Equal(1))
|
||||
Expect(addResult.IPs[0].Address.String()).To(Equal("192.168.1.6/24"))
|
||||
return nil
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
args = &skel.CmdArgs{
|
||||
ContainerID: "dummy",
|
||||
Netns: targetNS.Path(),
|
||||
IfName: contVethName1,
|
||||
StdinData: []byte(conf),
|
||||
}
|
||||
|
||||
err = originalNS.Do(func(ns.NetNS) error {
|
||||
return testutils.CmdDelWithArgs(args, func() error {
|
||||
return cmdDel(args)
|
||||
})
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
args = &skel.CmdArgs{
|
||||
ContainerID: "dummy",
|
||||
Netns: targetNS.Path(),
|
||||
IfName: contVethName0,
|
||||
StdinData: []byte(conf),
|
||||
}
|
||||
|
||||
err = originalNS.Do(func(ns.NetNS) error {
|
||||
return testutils.CmdDelWithArgs(args, func() error {
|
||||
return cmdDel(args)
|
||||
})
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
})
|
@ -1,4 +1,4 @@
|
||||
// Copyright 2015-2017 CNI authors
|
||||
// Copyright 2016 CNI authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@ -12,23 +12,16 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// +build !linux
|
||||
|
||||
package ip
|
||||
package main
|
||||
|
||||
import (
|
||||
"net"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
"github.com/vishvananda/netlink"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// AddRoute adds a universally-scoped route to a device.
|
||||
func AddRoute(ipn *net.IPNet, gw net.IP, dev netlink.Link) error {
|
||||
return types.NotImplementedError
|
||||
}
|
||||
|
||||
// AddHostRoute adds a host-scoped route to a device.
|
||||
func AddHostRoute(ipn *net.IPNet, gw net.IP, dev netlink.Link) error {
|
||||
return types.NotImplementedError
|
||||
func TestDHCP(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "plugins/ipam/dhcp")
|
||||
}
|
618
plugins/ipam/dhcp/dhcp_test.go
Normal file
618
plugins/ipam/dhcp/dhcp_test.go
Normal file
@ -0,0 +1,618 @@
|
||||
// Copyright 2015-2018 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 main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/skel"
|
||||
"github.com/containernetworking/cni/pkg/types/current"
|
||||
"github.com/containernetworking/plugins/pkg/ns"
|
||||
"github.com/containernetworking/plugins/pkg/testutils"
|
||||
|
||||
"github.com/vishvananda/netlink"
|
||||
|
||||
"github.com/d2g/dhcp4"
|
||||
"github.com/d2g/dhcp4server"
|
||||
"github.com/d2g/dhcp4server/leasepool"
|
||||
"github.com/d2g/dhcp4server/leasepool/memorypool"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
func getTmpDir() (string, error) {
|
||||
tmpDir, err := ioutil.TempDir(cniDirPrefix, "dhcp")
|
||||
if err == nil {
|
||||
tmpDir = filepath.ToSlash(tmpDir)
|
||||
}
|
||||
|
||||
return tmpDir, err
|
||||
}
|
||||
|
||||
func dhcpServerStart(netns ns.NetNS, leaseIP, serverIP net.IP, numLeases int, stopCh <-chan bool) (*sync.WaitGroup, error) {
|
||||
// Add the expected IP to the pool
|
||||
lp := memorypool.MemoryPool{}
|
||||
|
||||
Expect(numLeases).To(BeNumerically(">", 0))
|
||||
// Currently tests only need at most 2
|
||||
Expect(numLeases).To(BeNumerically("<=", 2))
|
||||
|
||||
// tests expect first lease to be at address 192.168.1.5
|
||||
for i := 5; i < numLeases+5; i++ {
|
||||
err := lp.AddLease(leasepool.Lease{IP: dhcp4.IPAdd(net.IPv4(192, 168, 1, byte(i)), 0)})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error adding IP to DHCP pool: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
dhcpServer, err := dhcp4server.New(
|
||||
net.IPv4(192, 168, 1, 1),
|
||||
&lp,
|
||||
dhcp4server.SetLocalAddr(net.UDPAddr{IP: net.IPv4(0, 0, 0, 0), Port: 67}),
|
||||
dhcp4server.SetRemoteAddr(net.UDPAddr{IP: net.IPv4bcast, Port: 68}),
|
||||
dhcp4server.LeaseDuration(time.Minute*15),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create DHCP server: %v", err)
|
||||
}
|
||||
|
||||
stopWg := sync.WaitGroup{}
|
||||
stopWg.Add(2)
|
||||
startWg := sync.WaitGroup{}
|
||||
startWg.Add(2)
|
||||
|
||||
// Run DHCP server in a goroutine so it doesn't block the main thread
|
||||
go func() {
|
||||
defer GinkgoRecover()
|
||||
|
||||
err = netns.Do(func(ns.NetNS) error {
|
||||
startWg.Done()
|
||||
if err := dhcpServer.ListenAndServe(); err != nil {
|
||||
// Log, but don't trap errors; the server will
|
||||
// always report an error when stopped
|
||||
GinkgoT().Logf("DHCP server finished with error: %v", err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
stopWg.Done()
|
||||
// Trap any errors after the Done, to allow the main test thread
|
||||
// to continue and clean up. Otherwise the test hangs.
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
}()
|
||||
|
||||
// Stop DHCP server in another goroutine for the same reason
|
||||
go func() {
|
||||
startWg.Done()
|
||||
<-stopCh
|
||||
dhcpServer.Shutdown()
|
||||
stopWg.Done()
|
||||
}()
|
||||
startWg.Wait()
|
||||
|
||||
return &stopWg, nil
|
||||
}
|
||||
|
||||
const (
|
||||
hostVethName string = "dhcp0"
|
||||
contVethName string = "eth0"
|
||||
cniDirPrefix string = "/var/run/cni"
|
||||
)
|
||||
|
||||
var _ = BeforeSuite(func() {
|
||||
err := os.MkdirAll(cniDirPrefix, 0700)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
|
||||
var _ = Describe("DHCP Operations", func() {
|
||||
var originalNS, targetNS ns.NetNS
|
||||
var dhcpServerStopCh chan bool
|
||||
var dhcpServerDone *sync.WaitGroup
|
||||
var clientCmd *exec.Cmd
|
||||
var socketPath string
|
||||
var tmpDir string
|
||||
var err error
|
||||
|
||||
BeforeEach(func() {
|
||||
dhcpServerStopCh = make(chan bool)
|
||||
|
||||
tmpDir, err = getTmpDir()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
socketPath = filepath.Join(tmpDir, "dhcp.sock")
|
||||
|
||||
// Create a new NetNS so we don't modify the host
|
||||
var err error
|
||||
originalNS, err = testutils.NewNS()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
targetNS, err = testutils.NewNS()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
serverIP := net.IPNet{
|
||||
IP: net.IPv4(192, 168, 1, 1),
|
||||
Mask: net.IPv4Mask(255, 255, 255, 0),
|
||||
}
|
||||
|
||||
// Create a veth pair in the "host" (original) NS
|
||||
err = originalNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
err = netlink.LinkAdd(&netlink.Veth{
|
||||
LinkAttrs: netlink.LinkAttrs{
|
||||
Name: hostVethName,
|
||||
},
|
||||
PeerName: contVethName,
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
host, err := netlink.LinkByName(hostVethName)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
err = netlink.LinkSetUp(host)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
err = netlink.AddrAdd(host, &netlink.Addr{IPNet: &serverIP})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
err = netlink.RouteAdd(&netlink.Route{
|
||||
LinkIndex: host.Attrs().Index,
|
||||
Scope: netlink.SCOPE_UNIVERSE,
|
||||
Dst: &net.IPNet{
|
||||
IP: net.IPv4(0, 0, 0, 0),
|
||||
Mask: net.IPv4Mask(0, 0, 0, 0),
|
||||
},
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
cont, err := netlink.LinkByName(contVethName)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
err = netlink.LinkSetNsFd(cont, int(targetNS.Fd()))
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
return nil
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Move the container side to the container's NS
|
||||
err = targetNS.Do(func(_ ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
link, err := netlink.LinkByName(contVethName)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
err = netlink.LinkSetUp(link)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
return nil
|
||||
})
|
||||
|
||||
// Start the DHCP server
|
||||
dhcpServerDone, err = dhcpServerStart(originalNS, net.IPv4(192, 168, 1, 5), serverIP.IP, 1, dhcpServerStopCh)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Start the DHCP client daemon
|
||||
dhcpPluginPath, err := exec.LookPath("dhcp")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
clientCmd = exec.Command(dhcpPluginPath, "daemon", "-socketpath", socketPath)
|
||||
err = clientCmd.Start()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(clientCmd.Process).NotTo(BeNil())
|
||||
|
||||
// Wait up to 15 seconds for the client socket
|
||||
Eventually(func() bool {
|
||||
_, err := os.Stat(socketPath)
|
||||
return err == nil
|
||||
}, time.Second*15, time.Second/4).Should(BeTrue())
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
dhcpServerStopCh <- true
|
||||
dhcpServerDone.Wait()
|
||||
clientCmd.Process.Kill()
|
||||
clientCmd.Wait()
|
||||
|
||||
Expect(originalNS.Close()).To(Succeed())
|
||||
Expect(targetNS.Close()).To(Succeed())
|
||||
defer os.RemoveAll(tmpDir)
|
||||
})
|
||||
|
||||
It("configures and deconfigures a link with ADD/DEL", func() {
|
||||
conf := fmt.Sprintf(`{
|
||||
"cniVersion": "0.3.1",
|
||||
"name": "mynet",
|
||||
"type": "ipvlan",
|
||||
"ipam": {
|
||||
"type": "dhcp",
|
||||
"daemonSocketPath": "%s"
|
||||
}
|
||||
}`, socketPath)
|
||||
|
||||
args := &skel.CmdArgs{
|
||||
ContainerID: "dummy",
|
||||
Netns: targetNS.Path(),
|
||||
IfName: contVethName,
|
||||
StdinData: []byte(conf),
|
||||
}
|
||||
|
||||
var addResult *current.Result
|
||||
err := originalNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
r, _, err := testutils.CmdAddWithArgs(args, func() error {
|
||||
return cmdAdd(args)
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
addResult, err = current.GetResult(r)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(len(addResult.IPs)).To(Equal(1))
|
||||
Expect(addResult.IPs[0].Address.String()).To(Equal("192.168.1.5/24"))
|
||||
return nil
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
err = originalNS.Do(func(ns.NetNS) error {
|
||||
return testutils.CmdDelWithArgs(args, func() error {
|
||||
return cmdDel(args)
|
||||
})
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
|
||||
It("correctly handles multiple DELs for the same container", func() {
|
||||
conf := fmt.Sprintf(`{
|
||||
"cniVersion": "0.3.1",
|
||||
"name": "mynet",
|
||||
"type": "ipvlan",
|
||||
"ipam": {
|
||||
"type": "dhcp",
|
||||
"daemonSocketPath": "%s"
|
||||
}
|
||||
}`, socketPath)
|
||||
|
||||
args := &skel.CmdArgs{
|
||||
ContainerID: "dummy",
|
||||
Netns: targetNS.Path(),
|
||||
IfName: contVethName,
|
||||
StdinData: []byte(conf),
|
||||
}
|
||||
|
||||
var addResult *current.Result
|
||||
err := originalNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
r, _, err := testutils.CmdAddWithArgs(args, func() error {
|
||||
return cmdAdd(args)
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
addResult, err = current.GetResult(r)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(len(addResult.IPs)).To(Equal(1))
|
||||
Expect(addResult.IPs[0].Address.String()).To(Equal("192.168.1.5/24"))
|
||||
return nil
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(3)
|
||||
started := sync.WaitGroup{}
|
||||
started.Add(3)
|
||||
for i := 0; i < 3; i++ {
|
||||
go func() {
|
||||
defer GinkgoRecover()
|
||||
|
||||
// Wait until all goroutines are running
|
||||
started.Done()
|
||||
started.Wait()
|
||||
|
||||
err = originalNS.Do(func(ns.NetNS) error {
|
||||
return testutils.CmdDelWithArgs(args, func() error {
|
||||
return cmdDel(args)
|
||||
})
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
err = originalNS.Do(func(ns.NetNS) error {
|
||||
return testutils.CmdDelWithArgs(args, func() error {
|
||||
return cmdDel(args)
|
||||
})
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
})
|
||||
|
||||
const (
|
||||
hostBridgeName string = "dhcpbr0"
|
||||
hostVethName0 string = "br-eth0"
|
||||
contVethName0 string = "eth0"
|
||||
hostVethName1 string = "br-eth1"
|
||||
contVethName1 string = "eth1"
|
||||
)
|
||||
|
||||
func dhcpSetupOriginalNS() (chan bool, net.IPNet, string, ns.NetNS, ns.NetNS, error) {
|
||||
var originalNS, targetNS ns.NetNS
|
||||
var dhcpServerStopCh chan bool
|
||||
var socketPath string
|
||||
var br *netlink.Bridge
|
||||
var tmpDir string
|
||||
var err error
|
||||
|
||||
dhcpServerStopCh = make(chan bool)
|
||||
|
||||
tmpDir, err = getTmpDir()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
socketPath = filepath.Join(tmpDir, "dhcp.sock")
|
||||
|
||||
// Create a new NetNS so we don't modify the host
|
||||
originalNS, err = testutils.NewNS()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
targetNS, err = testutils.NewNS()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
serverIP := net.IPNet{
|
||||
IP: net.IPv4(192, 168, 1, 1),
|
||||
Mask: net.IPv4Mask(255, 255, 255, 0),
|
||||
}
|
||||
|
||||
// Use (original) NS
|
||||
err = originalNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
// Create bridge in the "host" (original) NS
|
||||
br = &netlink.Bridge{
|
||||
LinkAttrs: netlink.LinkAttrs{
|
||||
Name: hostBridgeName,
|
||||
},
|
||||
}
|
||||
|
||||
err = netlink.LinkAdd(br)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
address := &netlink.Addr{IPNet: &net.IPNet{
|
||||
IP: net.IPv4(192, 168, 1, 1),
|
||||
Mask: net.IPv4Mask(255, 255, 255, 0),
|
||||
}}
|
||||
err = netlink.AddrAdd(br, address)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
err = netlink.LinkSetUp(br)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
err = netlink.RouteAdd(&netlink.Route{
|
||||
LinkIndex: br.Attrs().Index,
|
||||
Scope: netlink.SCOPE_UNIVERSE,
|
||||
Dst: &net.IPNet{
|
||||
IP: net.IPv4(0, 0, 0, 0),
|
||||
Mask: net.IPv4Mask(0, 0, 0, 0),
|
||||
},
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Create veth pair eth0
|
||||
vethLinkAttrs := netlink.NewLinkAttrs()
|
||||
vethLinkAttrs.Name = hostVethName0
|
||||
|
||||
veth := &netlink.Veth{
|
||||
LinkAttrs: vethLinkAttrs,
|
||||
PeerName: contVethName0,
|
||||
}
|
||||
err = netlink.LinkAdd(veth)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
err = netlink.LinkSetUp(veth)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
bridgeLink, err := netlink.LinkByName(hostBridgeName)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
hostVethLink, err := netlink.LinkByName(hostVethName0)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
err = netlink.LinkSetMaster(hostVethLink, bridgeLink.(*netlink.Bridge))
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
cont, err := netlink.LinkByName(contVethName0)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
err = netlink.LinkSetNsFd(cont, int(targetNS.Fd()))
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Create veth path - eth1
|
||||
vethLinkAttrs1 := netlink.NewLinkAttrs()
|
||||
vethLinkAttrs1.Name = hostVethName1
|
||||
|
||||
veth1 := &netlink.Veth{
|
||||
LinkAttrs: vethLinkAttrs1,
|
||||
PeerName: contVethName1,
|
||||
}
|
||||
err = netlink.LinkAdd(veth1)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
err = netlink.LinkSetUp(veth1)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
bridgeLink, err = netlink.LinkByName(hostBridgeName)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
hostVethLink1, err := netlink.LinkByName(hostVethName1)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
err = netlink.LinkSetMaster(hostVethLink1, bridgeLink.(*netlink.Bridge))
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
cont1, err := netlink.LinkByName(contVethName1)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
err = netlink.LinkSetNsFd(cont1, int(targetNS.Fd()))
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return dhcpServerStopCh, serverIP, socketPath, originalNS, targetNS, err
|
||||
}
|
||||
|
||||
var _ = Describe("DHCP Lease Unavailable Operations", func() {
|
||||
var originalNS, targetNS ns.NetNS
|
||||
var dhcpServerStopCh chan bool
|
||||
var dhcpServerDone *sync.WaitGroup
|
||||
var clientCmd *exec.Cmd
|
||||
var socketPath string
|
||||
var tmpDir string
|
||||
var serverIP net.IPNet
|
||||
var err error
|
||||
|
||||
BeforeEach(func() {
|
||||
dhcpServerStopCh, serverIP, socketPath, originalNS, targetNS, err = dhcpSetupOriginalNS()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Move the container side to the container's NS
|
||||
err = targetNS.Do(func(_ ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
link, err := netlink.LinkByName(contVethName0)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
err = netlink.LinkSetUp(link)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
link1, err := netlink.LinkByName(contVethName1)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
err = netlink.LinkSetUp(link1)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
return nil
|
||||
})
|
||||
|
||||
// Start the DHCP server
|
||||
dhcpServerDone, err = dhcpServerStart(originalNS, net.IPv4(192, 168, 1, 5), serverIP.IP, 1, dhcpServerStopCh)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Start the DHCP client daemon
|
||||
dhcpPluginPath, err := exec.LookPath("dhcp")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
clientCmd = exec.Command(dhcpPluginPath, "daemon", "-socketpath", socketPath)
|
||||
err = clientCmd.Start()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(clientCmd.Process).NotTo(BeNil())
|
||||
|
||||
// Wait up to 15 seconds for the client socket
|
||||
Eventually(func() bool {
|
||||
_, err := os.Stat(socketPath)
|
||||
return err == nil
|
||||
}, time.Second*15, time.Second/4).Should(BeTrue())
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
dhcpServerStopCh <- true
|
||||
dhcpServerDone.Wait()
|
||||
clientCmd.Process.Kill()
|
||||
clientCmd.Wait()
|
||||
|
||||
Expect(originalNS.Close()).To(Succeed())
|
||||
Expect(targetNS.Close()).To(Succeed())
|
||||
defer os.RemoveAll(tmpDir)
|
||||
})
|
||||
|
||||
It("Configures multiple links with multiple ADD with second lease unavailable", func() {
|
||||
conf := fmt.Sprintf(`{
|
||||
"cniVersion": "0.3.1",
|
||||
"name": "mynet",
|
||||
"type": "bridge",
|
||||
"bridge": "%s",
|
||||
"ipam": {
|
||||
"type": "dhcp",
|
||||
"daemonSocketPath": "%s"
|
||||
}
|
||||
}`, hostBridgeName, socketPath)
|
||||
|
||||
args := &skel.CmdArgs{
|
||||
ContainerID: "dummy",
|
||||
Netns: targetNS.Path(),
|
||||
IfName: contVethName0,
|
||||
StdinData: []byte(conf),
|
||||
}
|
||||
|
||||
var addResult *current.Result
|
||||
err := originalNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
r, _, err := testutils.CmdAddWithArgs(args, func() error {
|
||||
return cmdAdd(args)
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
addResult, err = current.GetResult(r)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(len(addResult.IPs)).To(Equal(1))
|
||||
Expect(addResult.IPs[0].Address.String()).To(Equal("192.168.1.5/24"))
|
||||
return nil
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
args = &skel.CmdArgs{
|
||||
ContainerID: "dummy",
|
||||
Netns: targetNS.Path(),
|
||||
IfName: contVethName1,
|
||||
StdinData: []byte(conf),
|
||||
}
|
||||
|
||||
err = originalNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
_, _, err := testutils.CmdAddWithArgs(args, func() error {
|
||||
return cmdAdd(args)
|
||||
})
|
||||
Expect(err).To(HaveOccurred())
|
||||
println(err.Error())
|
||||
Expect(err.Error()).To(Equal("error calling DHCP.Allocate: no more tries"))
|
||||
return nil
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
args = &skel.CmdArgs{
|
||||
ContainerID: "dummy",
|
||||
Netns: targetNS.Path(),
|
||||
IfName: contVethName1,
|
||||
StdinData: []byte(conf),
|
||||
}
|
||||
|
||||
err = originalNS.Do(func(ns.NetNS) error {
|
||||
return testutils.CmdDelWithArgs(args, func() error {
|
||||
return cmdDel(args)
|
||||
})
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
args = &skel.CmdArgs{
|
||||
ContainerID: "dummy",
|
||||
Netns: targetNS.Path(),
|
||||
IfName: contVethName0,
|
||||
StdinData: []byte(conf),
|
||||
}
|
||||
|
||||
err = originalNS.Do(func(ns.NetNS) error {
|
||||
return testutils.CmdDelWithArgs(args, func() error {
|
||||
return cmdDel(args)
|
||||
})
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
})
|
@ -20,6 +20,7 @@ import (
|
||||
"math/rand"
|
||||
"net"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/d2g/dhcp4"
|
||||
@ -33,7 +34,7 @@ import (
|
||||
// RFC 2131 suggests using exponential backoff, starting with 4sec
|
||||
// and randomized to +/- 1sec
|
||||
const resendDelay0 = 4 * time.Second
|
||||
const resendDelayMax = 32 * time.Second
|
||||
const resendDelayMax = 62 * time.Second
|
||||
|
||||
const (
|
||||
leaseStateBound = iota
|
||||
@ -55,6 +56,7 @@ type DHCPLease struct {
|
||||
renewalTime time.Time
|
||||
rebindingTime time.Time
|
||||
expireTime time.Time
|
||||
stopping uint32
|
||||
stop chan struct{}
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
@ -106,12 +108,14 @@ func AcquireLease(clientID, netns, ifName string) (*DHCPLease, error) {
|
||||
// Stop terminates the background task that maintains the lease
|
||||
// and issues a DHCP Release
|
||||
func (l *DHCPLease) Stop() {
|
||||
close(l.stop)
|
||||
if atomic.CompareAndSwapUint32(&l.stopping, 0, 1) {
|
||||
close(l.stop)
|
||||
}
|
||||
l.wg.Wait()
|
||||
}
|
||||
|
||||
func (l *DHCPLease) acquire() error {
|
||||
c, err := newDHCPClient(l.link)
|
||||
c, err := newDHCPClient(l.link, l.clientID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -124,8 +128,12 @@ func (l *DHCPLease) acquire() error {
|
||||
}
|
||||
}
|
||||
|
||||
opts := make(dhcp4.Options)
|
||||
opts[dhcp4.OptionClientIdentifier] = []byte(l.clientID)
|
||||
opts[dhcp4.OptionParameterRequestList] = []byte{byte(dhcp4.OptionRouter), byte(dhcp4.OptionSubnetMask)}
|
||||
|
||||
pkt, err := backoffRetry(func() (*dhcp4.Packet, error) {
|
||||
ok, ack, err := c.Request()
|
||||
ok, ack, err := DhcpRequest(c, opts)
|
||||
switch {
|
||||
case err != nil:
|
||||
return nil, err
|
||||
@ -234,14 +242,17 @@ func (l *DHCPLease) downIface() {
|
||||
}
|
||||
|
||||
func (l *DHCPLease) renew() error {
|
||||
c, err := newDHCPClient(l.link)
|
||||
c, err := newDHCPClient(l.link, l.clientID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
opts := make(dhcp4.Options)
|
||||
opts[dhcp4.OptionClientIdentifier] = []byte(l.clientID)
|
||||
|
||||
pkt, err := backoffRetry(func() (*dhcp4.Packet, error) {
|
||||
ok, ack, err := c.Renew(*l.ack)
|
||||
ok, ack, err := DhcpRenew(c, *l.ack, opts)
|
||||
switch {
|
||||
case err != nil:
|
||||
return nil, err
|
||||
@ -262,13 +273,16 @@ func (l *DHCPLease) renew() error {
|
||||
func (l *DHCPLease) release() error {
|
||||
log.Printf("%v: releasing lease", l.clientID)
|
||||
|
||||
c, err := newDHCPClient(l.link)
|
||||
c, err := newDHCPClient(l.link, l.clientID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
if err = c.Release(*l.ack); err != nil {
|
||||
opts := make(dhcp4.Options)
|
||||
opts[dhcp4.OptionClientIdentifier] = []byte(l.clientID)
|
||||
|
||||
if err = DhcpRelease(c, *l.ack, opts); err != nil {
|
||||
return fmt.Errorf("failed to send DHCPRELEASE")
|
||||
}
|
||||
|
||||
@ -292,8 +306,26 @@ func (l *DHCPLease) Gateway() net.IP {
|
||||
}
|
||||
|
||||
func (l *DHCPLease) Routes() []*types.Route {
|
||||
routes := parseRoutes(l.opts)
|
||||
return append(routes, parseCIDRRoutes(l.opts)...)
|
||||
routes := []*types.Route{}
|
||||
|
||||
// RFC 3442 states that if Classless Static Routes (option 121)
|
||||
// exist, we ignore Static Routes (option 33) and the Router/Gateway.
|
||||
opt121_routes := parseCIDRRoutes(l.opts)
|
||||
if len(opt121_routes) > 0 {
|
||||
return append(routes, opt121_routes...)
|
||||
}
|
||||
|
||||
// Append Static Routes
|
||||
routes = append(routes, parseRoutes(l.opts)...)
|
||||
|
||||
// The CNI spec says even if there is a gateway specified, we must
|
||||
// add a default route in the routes section.
|
||||
if gw := l.Gateway(); gw != nil {
|
||||
_, defaultRoute, _ := net.ParseCIDR("0.0.0.0/0")
|
||||
routes = append(routes, &types.Route{Dst: *defaultRoute, GW: gw})
|
||||
}
|
||||
|
||||
return routes
|
||||
}
|
||||
|
||||
// jitter returns a random value within [-span, span) range
|
||||
@ -303,8 +335,9 @@ func jitter(span time.Duration) time.Duration {
|
||||
|
||||
func backoffRetry(f func() (*dhcp4.Packet, error)) (*dhcp4.Packet, error) {
|
||||
var baseDelay time.Duration = resendDelay0
|
||||
var sleepTime time.Duration
|
||||
|
||||
for i := 0; i < resendCount; i++ {
|
||||
for {
|
||||
pkt, err := f()
|
||||
if err == nil {
|
||||
return pkt, nil
|
||||
@ -312,17 +345,23 @@ func backoffRetry(f func() (*dhcp4.Packet, error)) (*dhcp4.Packet, error) {
|
||||
|
||||
log.Print(err)
|
||||
|
||||
time.Sleep(baseDelay + jitter(time.Second))
|
||||
sleepTime = baseDelay + jitter(time.Second)
|
||||
|
||||
log.Printf("retrying in %f seconds", sleepTime.Seconds())
|
||||
|
||||
time.Sleep(sleepTime)
|
||||
|
||||
if baseDelay < resendDelayMax {
|
||||
baseDelay *= 2
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errNoMoreTries
|
||||
}
|
||||
|
||||
func newDHCPClient(link netlink.Link) (*dhcp4client.Client, error) {
|
||||
func newDHCPClient(link netlink.Link, clientID string) (*dhcp4client.Client, error) {
|
||||
pktsock, err := dhcp4client.NewPacketSock(link.Attrs().Index)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -15,6 +15,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
@ -26,23 +27,32 @@ import (
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
"github.com/containernetworking/cni/pkg/types/current"
|
||||
"github.com/containernetworking/cni/pkg/version"
|
||||
bv "github.com/containernetworking/plugins/pkg/utils/buildversion"
|
||||
)
|
||||
|
||||
const socketPath = "/run/cni/dhcp.sock"
|
||||
const defaultSocketPath = "/run/cni/dhcp.sock"
|
||||
|
||||
func main() {
|
||||
if len(os.Args) > 1 && os.Args[1] == "daemon" {
|
||||
var pidfilePath string
|
||||
var hostPrefix string
|
||||
var socketPath string
|
||||
daemonFlags := flag.NewFlagSet("daemon", flag.ExitOnError)
|
||||
daemonFlags.StringVar(&pidfilePath, "pidfile", "", "optional path to write daemon PID to")
|
||||
daemonFlags.StringVar(&hostPrefix, "hostprefix", "", "optional prefix to host root")
|
||||
daemonFlags.StringVar(&socketPath, "socketpath", "", "optional dhcp server socketpath")
|
||||
daemonFlags.Parse(os.Args[2:])
|
||||
|
||||
if err := runDaemon(pidfilePath); err != nil {
|
||||
if socketPath == "" {
|
||||
socketPath = defaultSocketPath
|
||||
}
|
||||
|
||||
if err := runDaemon(pidfilePath, hostPrefix, socketPath); err != nil {
|
||||
log.Printf(err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
} else {
|
||||
skel.PluginMain(cmdAdd, cmdDel, version.All)
|
||||
skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.All, bv.BuildString("dhcp"))
|
||||
}
|
||||
}
|
||||
|
||||
@ -65,12 +75,55 @@ func cmdAdd(args *skel.CmdArgs) error {
|
||||
func cmdDel(args *skel.CmdArgs) error {
|
||||
result := struct{}{}
|
||||
if err := rpcCall("DHCP.Release", args, &result); err != nil {
|
||||
return fmt.Errorf("error dialing DHCP daemon: %v", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func cmdCheck(args *skel.CmdArgs) error {
|
||||
// TODO: implement
|
||||
//return fmt.Errorf("not implemented")
|
||||
// Plugin must return result in same version as specified in netconf
|
||||
versionDecoder := &version.ConfigDecoder{}
|
||||
//confVersion, err := versionDecoder.Decode(args.StdinData)
|
||||
_, err := versionDecoder.Decode(args.StdinData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result := ¤t.Result{}
|
||||
if err := rpcCall("DHCP.Allocate", args, result); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type SocketPathConf struct {
|
||||
DaemonSocketPath string `json:"daemonSocketPath,omitempty"`
|
||||
}
|
||||
|
||||
type TempNetConf struct {
|
||||
IPAM SocketPathConf `json:"ipam,omitempty"`
|
||||
}
|
||||
|
||||
func getSocketPath(stdinData []byte) (string, error) {
|
||||
conf := TempNetConf{}
|
||||
if err := json.Unmarshal(stdinData, &conf); err != nil {
|
||||
return "", fmt.Errorf("error parsing socket path conf: %v", err)
|
||||
}
|
||||
if conf.IPAM.DaemonSocketPath == "" {
|
||||
return defaultSocketPath, nil
|
||||
}
|
||||
return conf.IPAM.DaemonSocketPath, nil
|
||||
}
|
||||
|
||||
func rpcCall(method string, args *skel.CmdArgs, result interface{}) error {
|
||||
socketPath, err := getSocketPath(args.StdinData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error obtaining socketPath: %v", err)
|
||||
}
|
||||
|
||||
client, err := rpc.DialHTTP("unix", socketPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error dialing DHCP daemon: %v", err)
|
||||
|
@ -98,7 +98,7 @@ func parseCIDRRoutes(opts dhcp4.Options) []*types.Route {
|
||||
}
|
||||
routes = append(routes, rt)
|
||||
|
||||
opt = opt[octets+5 : len(opt)]
|
||||
opt = opt[octets+5:]
|
||||
}
|
||||
}
|
||||
return routes
|
||||
|
@ -24,14 +24,14 @@ import (
|
||||
|
||||
func validateRoutes(t *testing.T, routes []*types.Route) {
|
||||
expected := []*types.Route{
|
||||
&types.Route{
|
||||
{
|
||||
Dst: net.IPNet{
|
||||
IP: net.IPv4(10, 0, 0, 0),
|
||||
Mask: net.CIDRMask(8, 32),
|
||||
},
|
||||
GW: net.IPv4(10, 1, 2, 3),
|
||||
},
|
||||
&types.Route{
|
||||
{
|
||||
Dst: net.IPNet{
|
||||
IP: net.IPv4(192, 168, 1, 0),
|
||||
Mask: net.CIDRMask(24, 32),
|
||||
|
11
plugins/ipam/dhcp/systemd/cni-dhcp.service
Normal file
11
plugins/ipam/dhcp/systemd/cni-dhcp.service
Normal file
@ -0,0 +1,11 @@
|
||||
[Unit]
|
||||
Description=CNI DHCP service
|
||||
Documentation=https://github.com/containernetworking/plugins/tree/master/plugins/ipam/dhcp
|
||||
After=network.target cni-dhcp.socket
|
||||
Requires=cni-dhcp.socket
|
||||
|
||||
[Service]
|
||||
ExecStart=/opt/cni/bin/dhcp daemon
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
14
plugins/ipam/dhcp/systemd/cni-dhcp.socket
Normal file
14
plugins/ipam/dhcp/systemd/cni-dhcp.socket
Normal file
@ -0,0 +1,14 @@
|
||||
[Unit]
|
||||
Description=CNI DHCP service socket
|
||||
Documentation=https://github.com/containernetworking/plugins/tree/master/plugins/ipam/dhcp
|
||||
PartOf=cni-dhcp.service
|
||||
|
||||
[Socket]
|
||||
ListenStream=/run/cni/dhcp.sock
|
||||
SocketMode=0660
|
||||
SocketUser=root
|
||||
SocketGroup=root
|
||||
RemoveOnStop=true
|
||||
|
||||
[Install]
|
||||
WantedBy=sockets.target
|
@ -1,138 +1,4 @@
|
||||
# host-local IP address management plugin
|
||||
|
||||
host-local IPAM allocates IPv4 and IPv6 addresses out of a specified address range. Optionally,
|
||||
it can include a DNS configuration from a `resolv.conf` file on the host.
|
||||
This document has moved to the [containernetworking/cni.dev](https://github.com/containernetworking/cni.dev) repo.
|
||||
|
||||
## Overview
|
||||
|
||||
host-local IPAM plugin allocates ip addresses out of a set of address ranges.
|
||||
It stores the state locally on the host filesystem, therefore ensuring uniqueness of IP addresses on a single host.
|
||||
|
||||
The allocator can allocate multiple ranges, and supports sets of multiple (disjoint)
|
||||
subnets. The allocation strategy is loosely round-robin within each range set.
|
||||
|
||||
## Example configurations
|
||||
|
||||
Note that the key `ranges` is a list of range sets. That is to say, the length
|
||||
of the top-level array is the number of addresses returned. The second-level
|
||||
array is a set of subnets to use as a pool of possible addresses.
|
||||
|
||||
This example configuration returns 2 IP addresses.
|
||||
|
||||
```json
|
||||
{
|
||||
"ipam": {
|
||||
"type": "host-local",
|
||||
"ranges": [
|
||||
[
|
||||
{
|
||||
"subnet": "10.10.0.0/16",
|
||||
"rangeStart": "10.10.1.20",
|
||||
"rangeEnd": "10.10.3.50",
|
||||
"gateway": "10.10.0.254"
|
||||
},
|
||||
{
|
||||
"subnet": "172.16.5.0/24"
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"subnet": "3ffe:ffff:0:01ff::/64",
|
||||
"rangeStart": "3ffe:ffff:0:01ff::0010",
|
||||
"rangeEnd": "3ffe:ffff:0:01ff::0020"
|
||||
}
|
||||
]
|
||||
],
|
||||
"routes": [
|
||||
{ "dst": "0.0.0.0/0" },
|
||||
{ "dst": "192.168.0.0/16", "gw": "10.10.5.1" },
|
||||
{ "dst": "3ffe:ffff:0:01ff::1/64" }
|
||||
],
|
||||
"dataDir": "/run/my-orchestrator/container-ipam-state"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Previous versions of the `host-local` allocator did not support the `ranges`
|
||||
property, and instead expected a single range on the top level. This is
|
||||
deprecated but still supported.
|
||||
```json
|
||||
{
|
||||
"ipam": {
|
||||
"type": "host-local",
|
||||
"subnet": "3ffe:ffff:0:01ff::/64",
|
||||
"rangeStart": "3ffe:ffff:0:01ff::0010",
|
||||
"rangeEnd": "3ffe:ffff:0:01ff::0020",
|
||||
"routes": [
|
||||
{ "dst": "3ffe:ffff:0:01ff::1/64" }
|
||||
],
|
||||
"resolvConf": "/etc/resolv.conf"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
We can test it out on the command-line:
|
||||
|
||||
```bash
|
||||
$ echo '{ "cniVersion": "0.3.1", "name": "examplenet", "ipam": { "type": "host-local", "ranges": [ [{"subnet": "203.0.113.0/24"}], [{"subnet": "2001:db8:1::/64"}]], "dataDir": "/tmp/cni-example" } }' | CNI_COMMAND=ADD CNI_CONTAINERID=example CNI_NETNS=/dev/null CNI_IFNAME=dummy0 CNI_PATH=. ./host-local
|
||||
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"ips": [
|
||||
{
|
||||
"version": "4",
|
||||
"address": "203.0.113.2/24",
|
||||
"gateway": "203.0.113.1"
|
||||
},
|
||||
{
|
||||
"version": "6",
|
||||
"address": "2001:db8:1::2/64",
|
||||
"gateway": "2001:db8:1::1"
|
||||
}
|
||||
],
|
||||
"dns": {}
|
||||
}
|
||||
```
|
||||
|
||||
## Network configuration reference
|
||||
|
||||
* `type` (string, required): "host-local".
|
||||
* `routes` (string, optional): list of routes to add to the container namespace. Each route is a dictionary with "dst" and optional "gw" fields. If "gw" is omitted, value of "gateway" will be used.
|
||||
* `resolvConf` (string, optional): Path to a `resolv.conf` on the host to parse and return as the DNS configuration
|
||||
* `dataDir` (string, optional): Path to a directory to use for maintaining state, e.g. which IPs have been allocated to which containers
|
||||
* `ranges`, (array, required, nonempty) an array of arrays of range objects:
|
||||
* `subnet` (string, required): CIDR block to allocate out of.
|
||||
* `rangeStart` (string, optional): IP inside of "subnet" from which to start allocating addresses. Defaults to ".2" IP inside of the "subnet" block.
|
||||
* `rangeEnd` (string, optional): IP inside of "subnet" with which to end allocating addresses. Defaults to ".254" IP inside of the "subnet" block for ipv4, ".255" for IPv6
|
||||
* `gateway` (string, optional): IP inside of "subnet" to designate as the gateway. Defaults to ".1" IP inside of the "subnet" block.
|
||||
|
||||
Older versions of the `host-local` plugin did not support the `ranges` array. Instead,
|
||||
all the properties in the `range` object were top-level. This is still supported but deprecated.
|
||||
|
||||
## Supported arguments
|
||||
The following [CNI_ARGS](https://github.com/containernetworking/cni/blob/master/SPEC.md#parameters) are supported:
|
||||
|
||||
* `ip`: request a specific IP address from a subnet.
|
||||
|
||||
The following [args conventions](https://github.com/containernetworking/cni/blob/master/CONVENTIONS.md) are supported:
|
||||
|
||||
* `ips` (array of strings): A list of custom IPs to attempt to allocate
|
||||
|
||||
### Custom IP allocation
|
||||
For every requested custom IP, the `host-local` allocator will request that IP
|
||||
if it falls within one of the `range` objects. Thus it is possible to specify
|
||||
multiple custom IPs and multiple ranges.
|
||||
|
||||
If any requested IPs cannot be reserved, either because they are already in use
|
||||
or are not part of a specified range, the plugin will return an error.
|
||||
|
||||
|
||||
## Files
|
||||
|
||||
Allocated IP addresses are stored as files in `/var/lib/cni/networks/$NETWORK_NAME`.
|
||||
The path can be customized with the `dataDir` option listed above. Environments
|
||||
where IPs are released automatically on reboot (e.g. running containers are not
|
||||
restored) may wish to specify `/var/run/cni` or another tmpfs mounted directory
|
||||
instead.
|
||||
You can find it online here: https://cni.dev/plugins/ipam/host-local/
|
||||
|
@ -40,8 +40,8 @@ func NewIPAllocator(s *RangeSet, store backend.Store, id int) *IPAllocator {
|
||||
}
|
||||
}
|
||||
|
||||
// Get alocates an IP
|
||||
func (a *IPAllocator) Get(id string, requestedIP net.IP) (*current.IPConfig, error) {
|
||||
// Get allocates an IP
|
||||
func (a *IPAllocator) Get(id string, ifname string, requestedIP net.IP) (*current.IPConfig, error) {
|
||||
a.store.Lock()
|
||||
defer a.store.Unlock()
|
||||
|
||||
@ -62,7 +62,7 @@ func (a *IPAllocator) Get(id string, requestedIP net.IP) (*current.IPConfig, err
|
||||
return nil, fmt.Errorf("requested ip %s is subnet's gateway", requestedIP.String())
|
||||
}
|
||||
|
||||
reserved, err := a.store.Reserve(id, requestedIP, a.rangeID)
|
||||
reserved, err := a.store.Reserve(id, ifname, requestedIP, a.rangeID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -73,6 +73,17 @@ func (a *IPAllocator) Get(id string, requestedIP net.IP) (*current.IPConfig, err
|
||||
gw = r.Gateway
|
||||
|
||||
} else {
|
||||
// try to get allocated IPs for this given id, if exists, just return error
|
||||
// because duplicate allocation is not allowed in SPEC
|
||||
// https://github.com/containernetworking/cni/blob/master/SPEC.md
|
||||
allocatedIPs := a.store.GetByID(id, ifname)
|
||||
for _, allocatedIP := range allocatedIPs {
|
||||
// check whether the existing IP belong to this range set
|
||||
if _, err := a.rangeset.RangeFor(allocatedIP); err == nil {
|
||||
return nil, fmt.Errorf("%s has been allocated to %s, duplicate allocation is not allowed", allocatedIP.String(), id)
|
||||
}
|
||||
}
|
||||
|
||||
iter, err := a.GetIter()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -83,7 +94,7 @@ func (a *IPAllocator) Get(id string, requestedIP net.IP) (*current.IPConfig, err
|
||||
break
|
||||
}
|
||||
|
||||
reserved, err := a.store.Reserve(id, reservedIP.IP, a.rangeID)
|
||||
reserved, err := a.store.Reserve(id, ifname, reservedIP.IP, a.rangeID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -110,11 +121,11 @@ func (a *IPAllocator) Get(id string, requestedIP net.IP) (*current.IPConfig, err
|
||||
}
|
||||
|
||||
// Release clears all IPs allocated for the container with given ID
|
||||
func (a *IPAllocator) Release(id string) error {
|
||||
func (a *IPAllocator) Release(id string, ifname string) error {
|
||||
a.store.Lock()
|
||||
defer a.store.Unlock()
|
||||
|
||||
return a.store.ReleaseByID(id)
|
||||
return a.store.ReleaseByID(id, ifname)
|
||||
}
|
||||
|
||||
type RangeIter struct {
|
||||
|
@ -23,5 +23,5 @@ import (
|
||||
|
||||
func TestAllocator(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Allocator Suite")
|
||||
RunSpecs(t, "plugins/ipam/host-local/backend/allocator")
|
||||
}
|
||||
|
@ -70,7 +70,7 @@ func (t AllocatorTestCase) run(idx int) (*current.IPConfig, error) {
|
||||
rangeID: "rangeid",
|
||||
}
|
||||
|
||||
return alloc.Get("ID", nil)
|
||||
return alloc.Get("ID", "eth0", nil)
|
||||
}
|
||||
|
||||
var _ = Describe("host-local ip allocator", func() {
|
||||
@ -88,8 +88,8 @@ var _ = Describe("host-local ip allocator", func() {
|
||||
|
||||
It("should loop correctly from the end", func() {
|
||||
a := mkalloc()
|
||||
a.store.Reserve("ID", net.IP{192, 168, 1, 6}, a.rangeID)
|
||||
a.store.ReleaseByID("ID")
|
||||
a.store.Reserve("ID", "eth0", net.IP{192, 168, 1, 6}, a.rangeID)
|
||||
a.store.ReleaseByID("ID", "eth0")
|
||||
r, _ := a.GetIter()
|
||||
Expect(r.nextip()).To(Equal(net.IP{192, 168, 1, 2}))
|
||||
Expect(r.nextip()).To(Equal(net.IP{192, 168, 1, 3}))
|
||||
@ -100,8 +100,8 @@ var _ = Describe("host-local ip allocator", func() {
|
||||
})
|
||||
It("should loop correctly from the middle", func() {
|
||||
a := mkalloc()
|
||||
a.store.Reserve("ID", net.IP{192, 168, 1, 3}, a.rangeID)
|
||||
a.store.ReleaseByID("ID")
|
||||
a.store.Reserve("ID", "eth0", net.IP{192, 168, 1, 3}, a.rangeID)
|
||||
a.store.ReleaseByID("ID", "eth0")
|
||||
r, _ := a.GetIter()
|
||||
Expect(r.nextip()).To(Equal(net.IP{192, 168, 1, 4}))
|
||||
Expect(r.nextip()).To(Equal(net.IP{192, 168, 1, 5}))
|
||||
@ -221,28 +221,28 @@ var _ = Describe("host-local ip allocator", func() {
|
||||
It("should not allocate the broadcast address", func() {
|
||||
alloc := mkalloc()
|
||||
for i := 2; i < 7; i++ {
|
||||
res, err := alloc.Get("ID", nil)
|
||||
res, err := alloc.Get(fmt.Sprintf("ID%d", i), "eth0", nil)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
s := fmt.Sprintf("192.168.1.%d/29", i)
|
||||
Expect(s).To(Equal(res.Address.String()))
|
||||
fmt.Fprintln(GinkgoWriter, "got ip", res.Address.String())
|
||||
}
|
||||
|
||||
x, err := alloc.Get("ID", nil)
|
||||
x, err := alloc.Get("ID8", "eth0", nil)
|
||||
fmt.Fprintln(GinkgoWriter, "got ip", x)
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
|
||||
It("should allocate in a round-robin fashion", func() {
|
||||
alloc := mkalloc()
|
||||
res, err := alloc.Get("ID", nil)
|
||||
res, err := alloc.Get("ID", "eth0", nil)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(res.Address.String()).To(Equal("192.168.1.2/29"))
|
||||
|
||||
err = alloc.Release("ID")
|
||||
err = alloc.Release("ID", "eth0")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
res, err = alloc.Get("ID", nil)
|
||||
res, err = alloc.Get("ID", "eth0", nil)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(res.Address.String()).To(Equal("192.168.1.3/29"))
|
||||
|
||||
@ -252,7 +252,7 @@ var _ = Describe("host-local ip allocator", func() {
|
||||
It("must allocate the requested IP", func() {
|
||||
alloc := mkalloc()
|
||||
requestedIP := net.IP{192, 168, 1, 5}
|
||||
res, err := alloc.Get("ID", requestedIP)
|
||||
res, err := alloc.Get("ID", "eth0", requestedIP)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(res.Address.IP.String()).To(Equal(requestedIP.String()))
|
||||
})
|
||||
@ -260,11 +260,11 @@ var _ = Describe("host-local ip allocator", func() {
|
||||
It("must fail when the requested IP is allocated", func() {
|
||||
alloc := mkalloc()
|
||||
requestedIP := net.IP{192, 168, 1, 5}
|
||||
res, err := alloc.Get("ID", requestedIP)
|
||||
res, err := alloc.Get("ID", "eth0", requestedIP)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(res.Address.IP.String()).To(Equal(requestedIP.String()))
|
||||
|
||||
_, err = alloc.Get("ID", requestedIP)
|
||||
_, err = alloc.Get("ID", "eth0", requestedIP)
|
||||
Expect(err).To(MatchError(`requested IP address 192.168.1.5 is not available in range set 192.168.1.1-192.168.1.6`))
|
||||
})
|
||||
|
||||
@ -272,7 +272,7 @@ var _ = Describe("host-local ip allocator", func() {
|
||||
alloc := mkalloc()
|
||||
(*alloc.rangeset)[0].RangeEnd = net.IP{192, 168, 1, 4}
|
||||
requestedIP := net.IP{192, 168, 1, 5}
|
||||
_, err := alloc.Get("ID", requestedIP)
|
||||
_, err := alloc.Get("ID", "eth0", requestedIP)
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
|
||||
@ -280,7 +280,7 @@ var _ = Describe("host-local ip allocator", func() {
|
||||
alloc := mkalloc()
|
||||
(*alloc.rangeset)[0].RangeStart = net.IP{192, 168, 1, 3}
|
||||
requestedIP := net.IP{192, 168, 1, 2}
|
||||
_, err := alloc.Get("ID", requestedIP)
|
||||
_, err := alloc.Get("ID", "eth0", requestedIP)
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
})
|
||||
|
@ -20,15 +20,19 @@ import (
|
||||
"net"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
types020 "github.com/containernetworking/cni/pkg/types/020"
|
||||
"github.com/containernetworking/cni/pkg/types/020"
|
||||
)
|
||||
|
||||
// The top-level network config, just so we can get the IPAM block
|
||||
// The top-level network config - IPAM plugins are passed the full configuration
|
||||
// of the calling plugin, not just the IPAM section.
|
||||
type Net struct {
|
||||
Name string `json:"name"`
|
||||
CNIVersion string `json:"cniVersion"`
|
||||
IPAM *IPAMConfig `json:"ipam"`
|
||||
Args *struct {
|
||||
Name string `json:"name"`
|
||||
CNIVersion string `json:"cniVersion"`
|
||||
IPAM *IPAMConfig `json:"ipam"`
|
||||
RuntimeConfig struct { // The capability arg
|
||||
IPRanges []RangeSet `json:"ipRanges,omitempty"`
|
||||
} `json:"runtimeConfig,omitempty"`
|
||||
Args *struct {
|
||||
A *IPAMArgs `json:"cni"`
|
||||
} `json:"args"`
|
||||
}
|
||||
@ -93,7 +97,7 @@ func LoadIPAMConfig(bytes []byte, envArgs string) (*IPAMConfig, string, error) {
|
||||
n.IPAM.IPArgs = append(n.IPAM.IPArgs, n.Args.A.IPs...)
|
||||
}
|
||||
|
||||
for idx, _ := range n.IPAM.IPArgs {
|
||||
for idx := range n.IPAM.IPArgs {
|
||||
if err := canonicalizeIP(&n.IPAM.IPArgs[idx]); err != nil {
|
||||
return nil, "", fmt.Errorf("cannot understand ip: %v", err)
|
||||
}
|
||||
@ -106,6 +110,11 @@ func LoadIPAMConfig(bytes []byte, envArgs string) (*IPAMConfig, string, error) {
|
||||
}
|
||||
n.IPAM.Range = nil
|
||||
|
||||
// If a range is supplied as a runtime config, prepend it to the Ranges
|
||||
if len(n.RuntimeConfig.IPRanges) > 0 {
|
||||
n.IPAM.Ranges = append(n.RuntimeConfig.IPRanges, n.IPAM.Ranges...)
|
||||
}
|
||||
|
||||
if len(n.IPAM.Ranges) == 0 {
|
||||
return nil, "", fmt.Errorf("no IP ranges specified")
|
||||
}
|
||||
@ -113,7 +122,7 @@ func LoadIPAMConfig(bytes []byte, envArgs string) (*IPAMConfig, string, error) {
|
||||
// Validate all ranges
|
||||
numV4 := 0
|
||||
numV6 := 0
|
||||
for i, _ := range n.IPAM.Ranges {
|
||||
for i := range n.IPAM.Ranges {
|
||||
if err := n.IPAM.Ranges[i].Canonicalize(); err != nil {
|
||||
return nil, "", fmt.Errorf("invalid range set %d: %s", i, err)
|
||||
}
|
||||
|
@ -45,7 +45,7 @@ var _ = Describe("IPAM config", func() {
|
||||
Name: "mynet",
|
||||
Type: "host-local",
|
||||
Ranges: []RangeSet{
|
||||
RangeSet{
|
||||
{
|
||||
{
|
||||
RangeStart: net.IP{10, 1, 2, 9},
|
||||
RangeEnd: net.IP{10, 1, 2, 20},
|
||||
@ -132,12 +132,18 @@ var _ = Describe("IPAM config", func() {
|
||||
}))
|
||||
})
|
||||
|
||||
It("Should parse a mixed config", func() {
|
||||
It("Should parse a mixed config with runtime args", func() {
|
||||
input := `{
|
||||
"cniVersion": "0.3.1",
|
||||
"name": "mynet",
|
||||
"type": "ipvlan",
|
||||
"master": "foo0",
|
||||
"runtimeConfig": {
|
||||
"irrelevant": "a",
|
||||
"ipRanges": [
|
||||
[{ "subnet": "12.1.3.0/24" }]
|
||||
]
|
||||
},
|
||||
"ipam": {
|
||||
"type": "host-local",
|
||||
"subnet": "10.1.2.0/24",
|
||||
@ -162,6 +168,17 @@ var _ = Describe("IPAM config", func() {
|
||||
Name: "mynet",
|
||||
Type: "host-local",
|
||||
Ranges: []RangeSet{
|
||||
{ // The RuntimeConfig should always be first
|
||||
{
|
||||
RangeStart: net.IP{12, 1, 3, 1},
|
||||
RangeEnd: net.IP{12, 1, 3, 254},
|
||||
Gateway: net.IP{12, 1, 3, 1},
|
||||
Subnet: types.IPNet{
|
||||
IP: net.IP{12, 1, 3, 0},
|
||||
Mask: net.CIDRMask(24, 32),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
{
|
||||
RangeStart: net.IP{10, 1, 2, 9},
|
||||
@ -355,7 +372,7 @@ var _ = Describe("IPAM config", func() {
|
||||
"type": "host-local",
|
||||
"ranges": [
|
||||
[{"subnet": "10.1.2.0/24"}],
|
||||
[{"subnet": "2001:db8:1::/24"}]
|
||||
[{"subnet": "2001:db8:1::/48"}]
|
||||
]
|
||||
}
|
||||
}`
|
||||
|
@ -40,6 +40,12 @@ func (r *Range) Canonicalize() error {
|
||||
return fmt.Errorf("IPNet IP and Mask version mismatch")
|
||||
}
|
||||
|
||||
// Ensure Subnet IP is the network address, not some other address
|
||||
networkIP := r.Subnet.IP.Mask(r.Subnet.Mask)
|
||||
if !r.Subnet.IP.Equal(networkIP) {
|
||||
return fmt.Errorf("Network has host bits set. For a subnet mask of length %d the network address is %s", ones, networkIP.String())
|
||||
}
|
||||
|
||||
// If the gateway is nil, claim .1
|
||||
if r.Gateway == nil {
|
||||
r.Gateway = ip.NextIP(r.Subnet.IP)
|
||||
@ -47,10 +53,6 @@ func (r *Range) Canonicalize() error {
|
||||
if err := canonicalizeIP(&r.Gateway); err != nil {
|
||||
return err
|
||||
}
|
||||
subnet := (net.IPNet)(r.Subnet)
|
||||
if !subnet.Contains(r.Gateway) {
|
||||
return fmt.Errorf("gateway %s not in network %s", r.Gateway.String(), subnet.String())
|
||||
}
|
||||
}
|
||||
|
||||
// RangeStart: If specified, make sure it's sane (inside the subnet),
|
||||
|
@ -61,7 +61,7 @@ func (s *RangeSet) Canonicalize() error {
|
||||
}
|
||||
|
||||
fam := 0
|
||||
for i, _ := range *s {
|
||||
for i := range *s {
|
||||
if err := (*s)[i].Canonicalize(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -25,7 +25,7 @@ import (
|
||||
)
|
||||
|
||||
var _ = Describe("IP ranges", func() {
|
||||
It("should generate sane defaults for ipv4", func() {
|
||||
It("should generate sane defaults for ipv4 with a clean prefix", func() {
|
||||
snstr := "192.0.2.0/24"
|
||||
r := Range{Subnet: mustSubnet(snstr)}
|
||||
|
||||
@ -33,7 +33,7 @@ var _ = Describe("IP ranges", func() {
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
Expect(r).To(Equal(Range{
|
||||
Subnet: mustSubnet(snstr),
|
||||
Subnet: networkSubnet(snstr),
|
||||
RangeStart: net.IP{192, 0, 2, 1},
|
||||
RangeEnd: net.IP{192, 0, 2, 254},
|
||||
Gateway: net.IP{192, 0, 2, 1},
|
||||
@ -47,13 +47,41 @@ var _ = Describe("IP ranges", func() {
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
Expect(r).To(Equal(Range{
|
||||
Subnet: mustSubnet(snstr),
|
||||
Subnet: networkSubnet(snstr),
|
||||
RangeStart: net.IP{192, 0, 2, 1},
|
||||
RangeEnd: net.IP{192, 0, 2, 126},
|
||||
Gateway: net.IP{192, 0, 2, 1},
|
||||
}))
|
||||
})
|
||||
It("should generate sane defaults for ipv6", func() {
|
||||
It("should reject ipv4 subnet using a masked address", func() {
|
||||
snstr := "192.0.2.12/24"
|
||||
r := Range{Subnet: mustSubnet(snstr)}
|
||||
|
||||
err := r.Canonicalize()
|
||||
Expect(err).Should(MatchError("Network has host bits set. For a subnet mask of length 24 the network address is 192.0.2.0"))
|
||||
})
|
||||
It("should reject ipv6 subnet using a masked address", func() {
|
||||
snstr := "2001:DB8:1::24:19ff:fee1:c44a/64"
|
||||
r := Range{Subnet: mustSubnet(snstr)}
|
||||
|
||||
err := r.Canonicalize()
|
||||
Expect(err).Should(MatchError("Network has host bits set. For a subnet mask of length 64 the network address is 2001:db8:1::"))
|
||||
})
|
||||
It("should reject ipv6 prefix with host bit set", func() {
|
||||
snstr := "2001:DB8:24:19ff::/63"
|
||||
r := Range{Subnet: mustSubnet(snstr)}
|
||||
|
||||
err := r.Canonicalize()
|
||||
Expect(err).Should(MatchError("Network has host bits set. For a subnet mask of length 63 the network address is 2001:db8:24:19fe::"))
|
||||
})
|
||||
It("should reject ipv4 network with host bit set", func() {
|
||||
snstr := "192.168.127.0/23"
|
||||
r := Range{Subnet: mustSubnet(snstr)}
|
||||
|
||||
err := r.Canonicalize()
|
||||
Expect(err).Should(MatchError("Network has host bits set. For a subnet mask of length 23 the network address is 192.168.126.0"))
|
||||
})
|
||||
It("should generate sane defaults for ipv6 with a clean prefix", func() {
|
||||
snstr := "2001:DB8:1::/64"
|
||||
r := Range{Subnet: mustSubnet(snstr)}
|
||||
|
||||
@ -61,7 +89,7 @@ var _ = Describe("IP ranges", func() {
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
Expect(r).To(Equal(Range{
|
||||
Subnet: mustSubnet(snstr),
|
||||
Subnet: networkSubnet(snstr),
|
||||
RangeStart: net.ParseIP("2001:DB8:1::1"),
|
||||
RangeEnd: net.ParseIP("2001:DB8:1::ffff:ffff:ffff:ffff"),
|
||||
Gateway: net.ParseIP("2001:DB8:1::1"),
|
||||
@ -75,16 +103,17 @@ var _ = Describe("IP ranges", func() {
|
||||
})
|
||||
|
||||
It("should reject invalid RangeStart and RangeEnd specifications", func() {
|
||||
r := Range{Subnet: mustSubnet("192.0.2.0/24"), RangeStart: net.ParseIP("192.0.3.0")}
|
||||
snstr := "192.0.2.0/24"
|
||||
r := Range{Subnet: mustSubnet(snstr), RangeStart: net.ParseIP("192.0.3.0")}
|
||||
err := r.Canonicalize()
|
||||
Expect(err).Should(MatchError("RangeStart 192.0.3.0 not in network 192.0.2.0/24"))
|
||||
|
||||
r = Range{Subnet: mustSubnet("192.0.2.0/24"), RangeEnd: net.ParseIP("192.0.4.0")}
|
||||
r = Range{Subnet: mustSubnet(snstr), RangeEnd: net.ParseIP("192.0.4.0")}
|
||||
err = r.Canonicalize()
|
||||
Expect(err).Should(MatchError("RangeEnd 192.0.4.0 not in network 192.0.2.0/24"))
|
||||
|
||||
r = Range{
|
||||
Subnet: mustSubnet("192.0.2.0/24"),
|
||||
Subnet: networkSubnet(snstr),
|
||||
RangeStart: net.ParseIP("192.0.2.50"),
|
||||
RangeEnd: net.ParseIP("192.0.2.40"),
|
||||
}
|
||||
@ -92,15 +121,10 @@ var _ = Describe("IP ranges", func() {
|
||||
Expect(err).Should(MatchError("RangeStart 192.0.2.50 not in network 192.0.2.0/24"))
|
||||
})
|
||||
|
||||
It("should reject invalid gateways", func() {
|
||||
r := Range{Subnet: mustSubnet("192.0.2.0/24"), Gateway: net.ParseIP("192.0.3.0")}
|
||||
err := r.Canonicalize()
|
||||
Expect(err).Should(MatchError("gateway 192.0.3.0 not in network 192.0.2.0/24"))
|
||||
})
|
||||
|
||||
It("should parse all fields correctly", func() {
|
||||
snstr := "192.0.2.0/24"
|
||||
r := Range{
|
||||
Subnet: mustSubnet("192.0.2.0/24"),
|
||||
Subnet: mustSubnet(snstr),
|
||||
RangeStart: net.ParseIP("192.0.2.40"),
|
||||
RangeEnd: net.ParseIP("192.0.2.50"),
|
||||
Gateway: net.ParseIP("192.0.2.254"),
|
||||
@ -109,7 +133,7 @@ var _ = Describe("IP ranges", func() {
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
Expect(r).To(Equal(Range{
|
||||
Subnet: mustSubnet("192.0.2.0/24"),
|
||||
Subnet: networkSubnet(snstr),
|
||||
RangeStart: net.IP{192, 0, 2, 40},
|
||||
RangeEnd: net.IP{192, 0, 2, 50},
|
||||
Gateway: net.IP{192, 0, 2, 254},
|
||||
@ -207,3 +231,9 @@ func mustSubnet(s string) types.IPNet {
|
||||
canonicalizeIP(&n.IP)
|
||||
return types.IPNet(*n)
|
||||
}
|
||||
|
||||
func networkSubnet(s string) types.IPNet {
|
||||
net := mustSubnet(s)
|
||||
net.IP = net.IP.Mask(net.Mask)
|
||||
return net
|
||||
}
|
||||
|
@ -19,19 +19,21 @@ import (
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/containernetworking/plugins/plugins/ipam/host-local/backend"
|
||||
)
|
||||
|
||||
const lastIPFilePrefix = "last_reserved_ip."
|
||||
const LineBreak = "\r\n"
|
||||
|
||||
var defaultDataDir = "/var/lib/cni/networks"
|
||||
|
||||
// Store is a simple disk-backed store that creates one file per IP
|
||||
// address in a given directory. The contents of the file are the container ID.
|
||||
type Store struct {
|
||||
FileLock
|
||||
*FileLock
|
||||
dataDir string
|
||||
}
|
||||
|
||||
@ -51,11 +53,12 @@ func New(network, dataDir string) (*Store, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Store{*lk, dir}, nil
|
||||
return &Store{lk, dir}, nil
|
||||
}
|
||||
|
||||
func (s *Store) Reserve(id string, ip net.IP, rangeID string) (bool, error) {
|
||||
fname := filepath.Join(s.dataDir, ip.String())
|
||||
func (s *Store) Reserve(id string, ifname string, ip net.IP, rangeID string) (bool, error) {
|
||||
fname := GetEscapedPath(s.dataDir, ip.String())
|
||||
|
||||
f, err := os.OpenFile(fname, os.O_RDWR|os.O_EXCL|os.O_CREATE, 0644)
|
||||
if os.IsExist(err) {
|
||||
return false, nil
|
||||
@ -63,7 +66,7 @@ func (s *Store) Reserve(id string, ip net.IP, rangeID string) (bool, error) {
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if _, err := f.WriteString(strings.TrimSpace(id)); err != nil {
|
||||
if _, err := f.WriteString(strings.TrimSpace(id) + LineBreak + ifname); err != nil {
|
||||
f.Close()
|
||||
os.Remove(f.Name())
|
||||
return false, err
|
||||
@ -73,7 +76,7 @@ func (s *Store) Reserve(id string, ip net.IP, rangeID string) (bool, error) {
|
||||
return false, err
|
||||
}
|
||||
// store the reserved ip in lastIPFile
|
||||
ipfile := filepath.Join(s.dataDir, lastIPFilePrefix+rangeID)
|
||||
ipfile := GetEscapedPath(s.dataDir, lastIPFilePrefix+rangeID)
|
||||
err = ioutil.WriteFile(ipfile, []byte(ip.String()), 0644)
|
||||
if err != nil {
|
||||
return false, err
|
||||
@ -83,7 +86,7 @@ func (s *Store) Reserve(id string, ip net.IP, rangeID string) (bool, error) {
|
||||
|
||||
// LastReservedIP returns the last reserved IP if exists
|
||||
func (s *Store) LastReservedIP(rangeID string) (net.IP, error) {
|
||||
ipfile := filepath.Join(s.dataDir, lastIPFilePrefix+rangeID)
|
||||
ipfile := GetEscapedPath(s.dataDir, lastIPFilePrefix+rangeID)
|
||||
data, err := ioutil.ReadFile(ipfile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -92,12 +95,12 @@ func (s *Store) LastReservedIP(rangeID string) (net.IP, error) {
|
||||
}
|
||||
|
||||
func (s *Store) Release(ip net.IP) error {
|
||||
return os.Remove(filepath.Join(s.dataDir, ip.String()))
|
||||
return os.Remove(GetEscapedPath(s.dataDir, ip.String()))
|
||||
}
|
||||
|
||||
// N.B. This function eats errors to be tolerant and
|
||||
// release as much as possible
|
||||
func (s *Store) ReleaseByID(id string) error {
|
||||
func (s *Store) FindByKey(id string, ifname string, match string) (bool, error) {
|
||||
found := false
|
||||
|
||||
err := filepath.Walk(s.dataDir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil || info.IsDir() {
|
||||
return nil
|
||||
@ -106,12 +109,101 @@ func (s *Store) ReleaseByID(id string) error {
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if strings.TrimSpace(string(data)) == strings.TrimSpace(id) {
|
||||
if strings.TrimSpace(string(data)) == match {
|
||||
found = true
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return found, err
|
||||
|
||||
}
|
||||
|
||||
func (s *Store) FindByID(id string, ifname string) bool {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
|
||||
found := false
|
||||
match := strings.TrimSpace(id) + LineBreak + ifname
|
||||
found, err := s.FindByKey(id, ifname, match)
|
||||
|
||||
// Match anything created by this id
|
||||
if !found && err == nil {
|
||||
match := strings.TrimSpace(id)
|
||||
found, err = s.FindByKey(id, ifname, match)
|
||||
}
|
||||
|
||||
return found
|
||||
}
|
||||
|
||||
func (s *Store) ReleaseByKey(id string, ifname string, match string) (bool, error) {
|
||||
found := false
|
||||
err := filepath.Walk(s.dataDir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil || info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
data, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if strings.TrimSpace(string(data)) == match {
|
||||
if err := os.Remove(path); err != nil {
|
||||
return nil
|
||||
}
|
||||
found = true
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return found, err
|
||||
|
||||
}
|
||||
|
||||
// N.B. This function eats errors to be tolerant and
|
||||
// release as much as possible
|
||||
func (s *Store) ReleaseByID(id string, ifname string) error {
|
||||
found := false
|
||||
match := strings.TrimSpace(id) + LineBreak + ifname
|
||||
found, err := s.ReleaseByKey(id, ifname, match)
|
||||
|
||||
// For backwards compatibility, look for files written by a previous version
|
||||
if !found && err == nil {
|
||||
match := strings.TrimSpace(id)
|
||||
found, err = s.ReleaseByKey(id, ifname, match)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// GetByID returns the IPs which have been allocated to the specific ID
|
||||
func (s *Store) GetByID(id string, ifname string) []net.IP {
|
||||
var ips []net.IP
|
||||
|
||||
match := strings.TrimSpace(id) + LineBreak + ifname
|
||||
// matchOld for backwards compatibility
|
||||
matchOld := strings.TrimSpace(id)
|
||||
|
||||
// walk through all ips in this network to get the ones which belong to a specific ID
|
||||
_ = filepath.Walk(s.dataDir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil || info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
data, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if strings.TrimSpace(string(data)) == match || strings.TrimSpace(string(data)) == matchOld {
|
||||
_, ipString := filepath.Split(path)
|
||||
if ip := net.ParseIP(ipString); ip != nil {
|
||||
ips = append(ips, ip)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return err
|
||||
|
||||
return ips
|
||||
}
|
||||
|
||||
func GetEscapedPath(dataDir string, fname string) string {
|
||||
if runtime.GOOS == "windows" {
|
||||
fname = strings.Replace(fname, ":", "_", -1)
|
||||
}
|
||||
return filepath.Join(dataDir, fname)
|
||||
}
|
||||
|
27
plugins/ipam/host-local/backend/disk/disk_suite_test.go
Normal file
27
plugins/ipam/host-local/backend/disk/disk_suite_test.go
Normal file
@ -0,0 +1,27 @@
|
||||
// Copyright 2016 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 disk
|
||||
|
||||
import (
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLock(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "plugins/ipam/host-local/backend/disk")
|
||||
}
|
@ -15,18 +15,28 @@
|
||||
package disk
|
||||
|
||||
import (
|
||||
"github.com/alexflint/go-filemutex"
|
||||
"os"
|
||||
"syscall"
|
||||
"path"
|
||||
)
|
||||
|
||||
// FileLock wraps os.File to be used as a lock using flock
|
||||
type FileLock struct {
|
||||
f *os.File
|
||||
f *filemutex.FileMutex
|
||||
}
|
||||
|
||||
// NewFileLock opens file/dir at path and returns unlocked FileLock object
|
||||
func NewFileLock(path string) (*FileLock, error) {
|
||||
f, err := os.Open(path)
|
||||
func NewFileLock(lockPath string) (*FileLock, error) {
|
||||
fi, err := os.Stat(lockPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if fi.IsDir() {
|
||||
lockPath = path.Join(lockPath, "lock")
|
||||
}
|
||||
|
||||
f, err := filemutex.New(lockPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -34,17 +44,16 @@ func NewFileLock(path string) (*FileLock, error) {
|
||||
return &FileLock{f}, nil
|
||||
}
|
||||
|
||||
// Close closes underlying file
|
||||
func (l *FileLock) Close() error {
|
||||
return l.f.Close()
|
||||
}
|
||||
|
||||
// Lock acquires an exclusive lock
|
||||
func (l *FileLock) Lock() error {
|
||||
return syscall.Flock(int(l.f.Fd()), syscall.LOCK_EX)
|
||||
return l.f.Lock()
|
||||
}
|
||||
|
||||
// Unlock releases the lock
|
||||
func (l *FileLock) Unlock() error {
|
||||
return syscall.Flock(int(l.f.Fd()), syscall.LOCK_UN)
|
||||
return l.f.Unlock()
|
||||
}
|
||||
|
63
plugins/ipam/host-local/backend/disk/lock_test.go
Normal file
63
plugins/ipam/host-local/backend/disk/lock_test.go
Normal file
@ -0,0 +1,63 @@
|
||||
// Copyright 2016 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 disk
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("Lock Operations", func() {
|
||||
It("locks a file path", func() {
|
||||
dir, err := ioutil.TempDir("", "")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
// create a dummy file to lock
|
||||
path := filepath.Join(dir, "x")
|
||||
f, err := os.OpenFile(path, os.O_RDONLY|os.O_CREATE, 0666)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
err = f.Close()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
// now use it to lock
|
||||
m, err := NewFileLock(path)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
err = m.Lock()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
err = m.Unlock()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
|
||||
It("locks a folder path", func() {
|
||||
dir, err := ioutil.TempDir("", "")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
// use the folder to lock
|
||||
m, err := NewFileLock(dir)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
err = m.Lock()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
err = m.Unlock()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
})
|
@ -20,8 +20,9 @@ type Store interface {
|
||||
Lock() error
|
||||
Unlock() error
|
||||
Close() error
|
||||
Reserve(id string, ip net.IP, rangeID string) (bool, error)
|
||||
Reserve(id string, ifname string, ip net.IP, rangeID string) (bool, error)
|
||||
LastReservedIP(rangeID string) (net.IP, error)
|
||||
Release(ip net.IP) error
|
||||
ReleaseByID(id string) error
|
||||
ReleaseByID(id string, ifname string) error
|
||||
GetByID(id string, ifname string) []net.IP
|
||||
}
|
||||
|
@ -45,7 +45,7 @@ func (s *FakeStore) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *FakeStore) Reserve(id string, ip net.IP, rangeID string) (bool, error) {
|
||||
func (s *FakeStore) Reserve(id string, ifname string, ip net.IP, rangeID string) (bool, error) {
|
||||
key := ip.String()
|
||||
if _, ok := s.ipMap[key]; !ok {
|
||||
s.ipMap[key] = id
|
||||
@ -68,7 +68,7 @@ func (s *FakeStore) Release(ip net.IP) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *FakeStore) ReleaseByID(id string) error {
|
||||
func (s *FakeStore) ReleaseByID(id string, ifname string) error {
|
||||
toDelete := []string{}
|
||||
for k, v := range s.ipMap {
|
||||
if v == id {
|
||||
@ -81,6 +81,16 @@ func (s *FakeStore) ReleaseByID(id string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *FakeStore) GetByID(id string, ifname string) []net.IP {
|
||||
var ips []net.IP
|
||||
for k, v := range s.ipMap {
|
||||
if v == id {
|
||||
ips = append(ips, net.ParseIP(k))
|
||||
}
|
||||
}
|
||||
return ips
|
||||
}
|
||||
|
||||
func (s *FakeStore) SetIPMap(m map[string]string) {
|
||||
s.ipMap = m
|
||||
}
|
||||
|
@ -65,12 +65,11 @@ options four
|
||||
|
||||
func parse(contents string) (*types.DNS, error) {
|
||||
f, err := ioutil.TempFile("", "host_local_resolv")
|
||||
defer f.Close()
|
||||
defer os.Remove(f.Name())
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
defer os.Remove(f.Name())
|
||||
|
||||
if _, err := f.WriteString(contents); err != nil {
|
||||
return nil, err
|
||||
|
@ -23,5 +23,5 @@ import (
|
||||
|
||||
func TestHostLocal(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "HostLocal Suite")
|
||||
RunSpecs(t, "plugins/ipam/host-local")
|
||||
}
|
||||
|
@ -28,16 +28,19 @@ import (
|
||||
"github.com/containernetworking/cni/pkg/types/current"
|
||||
"github.com/containernetworking/plugins/pkg/testutils"
|
||||
|
||||
"github.com/containernetworking/plugins/plugins/ipam/host-local/backend/disk"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
const LineBreak = "\r\n"
|
||||
|
||||
var _ = Describe("host-local Operations", func() {
|
||||
It("allocates and releases addresses with ADD/DEL", func() {
|
||||
const ifname string = "eth0"
|
||||
const nspath string = "/some/where"
|
||||
|
||||
tmpDir, err := ioutil.TempDir("", "host_local_artifacts")
|
||||
tmpDir, err := getTmpDir()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
@ -45,26 +48,26 @@ var _ = Describe("host-local Operations", func() {
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
conf := fmt.Sprintf(`{
|
||||
"cniVersion": "0.3.1",
|
||||
"name": "mynet",
|
||||
"type": "ipvlan",
|
||||
"master": "foo0",
|
||||
"ipam": {
|
||||
"type": "host-local",
|
||||
"dataDir": "%s",
|
||||
"resolvConf": "%s/resolv.conf",
|
||||
"ranges": [
|
||||
[{ "subnet": "10.1.2.0/24" }, {"subnet": "10.2.2.0/24"}],
|
||||
[{ "subnet": "2001:db8:1::0/64" }]
|
||||
],
|
||||
"routes": [
|
||||
{"dst": "0.0.0.0/0"},
|
||||
{"dst": "::/0"},
|
||||
{"dst": "192.168.0.0/16", "gw": "1.1.1.1"},
|
||||
{"dst": "2001:db8:2::0/64", "gw": "2001:db8:3::1"}
|
||||
]
|
||||
}
|
||||
}`, tmpDir, tmpDir)
|
||||
"cniVersion": "0.3.1",
|
||||
"name": "mynet",
|
||||
"type": "ipvlan",
|
||||
"master": "foo0",
|
||||
"ipam": {
|
||||
"type": "host-local",
|
||||
"dataDir": "%s",
|
||||
"resolvConf": "%s/resolv.conf",
|
||||
"ranges": [
|
||||
[{ "subnet": "10.1.2.0/24" }, {"subnet": "10.2.2.0/24"}],
|
||||
[{ "subnet": "2001:db8:1::0/64" }]
|
||||
],
|
||||
"routes": [
|
||||
{"dst": "0.0.0.0/0"},
|
||||
{"dst": "::/0"},
|
||||
{"dst": "192.168.0.0/16", "gw": "1.1.1.1"},
|
||||
{"dst": "2001:db8:2::0/64", "gw": "2001:db8:3::1"}
|
||||
]
|
||||
}
|
||||
}`, tmpDir, tmpDir)
|
||||
|
||||
args := &skel.CmdArgs{
|
||||
ContainerID: "dummy",
|
||||
@ -74,7 +77,7 @@ var _ = Describe("host-local Operations", func() {
|
||||
}
|
||||
|
||||
// Allocate the IP
|
||||
r, raw, err := testutils.CmdAddWithResult(nspath, ifname, []byte(conf), func() error {
|
||||
r, raw, err := testutils.CmdAddWithArgs(args, func() error {
|
||||
return cmdAdd(args)
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
@ -110,12 +113,12 @@ var _ = Describe("host-local Operations", func() {
|
||||
ipFilePath1 := filepath.Join(tmpDir, "mynet", "10.1.2.2")
|
||||
contents, err := ioutil.ReadFile(ipFilePath1)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(string(contents)).To(Equal("dummy"))
|
||||
Expect(string(contents)).To(Equal(args.ContainerID + LineBreak + ifname))
|
||||
|
||||
ipFilePath2 := filepath.Join(tmpDir, "mynet", "2001:db8:1::2")
|
||||
ipFilePath2 := filepath.Join(tmpDir, disk.GetEscapedPath("mynet", "2001:db8:1::2"))
|
||||
contents, err = ioutil.ReadFile(ipFilePath2)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(string(contents)).To(Equal("dummy"))
|
||||
Expect(string(contents)).To(Equal(args.ContainerID + LineBreak + ifname))
|
||||
|
||||
lastFilePath1 := filepath.Join(tmpDir, "mynet", "last_reserved_ip.0")
|
||||
contents, err = ioutil.ReadFile(lastFilePath1)
|
||||
@ -127,7 +130,7 @@ var _ = Describe("host-local Operations", func() {
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(string(contents)).To(Equal("2001:db8:1::2"))
|
||||
// Release the IP
|
||||
err = testutils.CmdDelWithResult(nspath, ifname, func() error {
|
||||
err = testutils.CmdDelWithArgs(args, func() error {
|
||||
return cmdDel(args)
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
@ -138,25 +141,136 @@ var _ = Describe("host-local Operations", func() {
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
|
||||
It("doesn't error when passed an unknown ID on DEL", func() {
|
||||
It("allocates and releases addresses on specific interface with ADD/DEL", func() {
|
||||
const ifname0 string = "eth0"
|
||||
const ifname1 string = "eth1"
|
||||
const nspath string = "/some/where"
|
||||
|
||||
tmpDir, err := getTmpDir()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
err = ioutil.WriteFile(filepath.Join(tmpDir, "resolv.conf"), []byte("nameserver 192.0.2.3"), 0644)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
conf0 := fmt.Sprintf(`{
|
||||
"cniVersion": "0.3.1",
|
||||
"name": "mynet0",
|
||||
"type": "ipvlan",
|
||||
"master": "foo0",
|
||||
"ipam": {
|
||||
"type": "host-local",
|
||||
"dataDir": "%s",
|
||||
"resolvConf": "%s/resolv.conf",
|
||||
"ranges": [
|
||||
[{ "subnet": "10.1.2.0/24" }]
|
||||
]
|
||||
}
|
||||
}`, tmpDir, tmpDir)
|
||||
|
||||
conf1 := fmt.Sprintf(`{
|
||||
"cniVersion": "0.3.1",
|
||||
"name": "mynet1",
|
||||
"type": "ipvlan",
|
||||
"master": "foo1",
|
||||
"ipam": {
|
||||
"type": "host-local",
|
||||
"dataDir": "%s",
|
||||
"resolvConf": "%s/resolv.conf",
|
||||
"ranges": [
|
||||
[{ "subnet": "10.2.2.0/24" }]
|
||||
]
|
||||
}
|
||||
}`, tmpDir, tmpDir)
|
||||
|
||||
args0 := &skel.CmdArgs{
|
||||
ContainerID: "dummy",
|
||||
Netns: nspath,
|
||||
IfName: ifname0,
|
||||
StdinData: []byte(conf0),
|
||||
}
|
||||
|
||||
// Allocate the IP
|
||||
r0, raw, err := testutils.CmdAddWithArgs(args0, func() error {
|
||||
return cmdAdd(args0)
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(strings.Index(string(raw), "\"version\":")).Should(BeNumerically(">", 0))
|
||||
|
||||
_, err = current.GetResult(r0)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
args1 := &skel.CmdArgs{
|
||||
ContainerID: "dummy",
|
||||
Netns: nspath,
|
||||
IfName: ifname1,
|
||||
StdinData: []byte(conf1),
|
||||
}
|
||||
|
||||
// Allocate the IP
|
||||
r1, raw, err := testutils.CmdAddWithArgs(args1, func() error {
|
||||
return cmdAdd(args1)
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(strings.Index(string(raw), "\"version\":")).Should(BeNumerically(">", 0))
|
||||
|
||||
_, err = current.GetResult(r1)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
ipFilePath0 := filepath.Join(tmpDir, "mynet0", "10.1.2.2")
|
||||
contents, err := ioutil.ReadFile(ipFilePath0)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(string(contents)).To(Equal(args0.ContainerID + LineBreak + ifname0))
|
||||
|
||||
ipFilePath1 := filepath.Join(tmpDir, "mynet1", "10.2.2.2")
|
||||
contents, err = ioutil.ReadFile(ipFilePath1)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(string(contents)).To(Equal(args1.ContainerID + LineBreak + ifname1))
|
||||
|
||||
// Release the IP on ifname0
|
||||
err = testutils.CmdDelWithArgs(args0, func() error {
|
||||
return cmdDel(args0)
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
_, err = os.Stat(ipFilePath0)
|
||||
Expect(err).To(HaveOccurred())
|
||||
|
||||
// reread ipFilePath1, ensure that ifname1 didn't get deleted
|
||||
contents, err = ioutil.ReadFile(ipFilePath1)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(string(contents)).To(Equal(args1.ContainerID + LineBreak + ifname1))
|
||||
|
||||
// Release the IP on ifname1
|
||||
err = testutils.CmdDelWithArgs(args1, func() error {
|
||||
return cmdDel(args1)
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
_, err = os.Stat(ipFilePath1)
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
|
||||
It("repeat allocating addresses on specific interface for same container ID with ADD", func() {
|
||||
const ifname string = "eth0"
|
||||
const nspath string = "/some/where"
|
||||
|
||||
tmpDir, err := ioutil.TempDir("", "host_local_artifacts")
|
||||
tmpDir, err := getTmpDir()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
conf := fmt.Sprintf(`{
|
||||
"cniVersion": "0.3.0",
|
||||
"name": "mynet",
|
||||
"type": "ipvlan",
|
||||
"master": "foo0",
|
||||
"ipam": {
|
||||
"type": "host-local",
|
||||
"subnet": "10.1.2.0/24",
|
||||
"dataDir": "%s"
|
||||
}
|
||||
}`, tmpDir)
|
||||
"cniVersion": "0.3.1",
|
||||
"name": "mynet0",
|
||||
"type": "ipvlan",
|
||||
"master": "foo0",
|
||||
"ipam": {
|
||||
"type": "host-local",
|
||||
"dataDir": "%s",
|
||||
"ranges": [
|
||||
[{ "subnet": "10.1.2.0/24" }]
|
||||
]
|
||||
}
|
||||
}`, tmpDir)
|
||||
|
||||
args := &skel.CmdArgs{
|
||||
ContainerID: "dummy",
|
||||
@ -165,18 +279,70 @@ var _ = Describe("host-local Operations", func() {
|
||||
StdinData: []byte(conf),
|
||||
}
|
||||
|
||||
// Release the IP
|
||||
err = testutils.CmdDelWithResult(nspath, ifname, func() error {
|
||||
args1 := &skel.CmdArgs{
|
||||
ContainerID: "dummy1",
|
||||
Netns: nspath,
|
||||
IfName: ifname,
|
||||
StdinData: []byte(conf),
|
||||
}
|
||||
|
||||
// Allocate the IP
|
||||
r0, raw, err := testutils.CmdAddWithArgs(args, func() error {
|
||||
return cmdAdd(args)
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(strings.Index(string(raw), "\"version\":")).Should(BeNumerically(">", 0))
|
||||
|
||||
result0, err := current.GetResult(r0)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(len(result0.IPs)).Should(Equal(1))
|
||||
Expect(result0.IPs[0].Address.String()).Should(Equal("10.1.2.2/24"))
|
||||
|
||||
// Allocate the IP with the same container ID
|
||||
_, _, err = testutils.CmdAddWithArgs(args, func() error {
|
||||
return cmdAdd(args)
|
||||
})
|
||||
Expect(err).To(HaveOccurred())
|
||||
|
||||
// Allocate the IP with the another container ID
|
||||
r1, raw, err := testutils.CmdAddWithArgs(args1, func() error {
|
||||
return cmdAdd(args1)
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(strings.Index(string(raw), "\"version\":")).Should(BeNumerically(">", 0))
|
||||
|
||||
result1, err := current.GetResult(r1)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(len(result1.IPs)).Should(Equal(1))
|
||||
Expect(result1.IPs[0].Address.String()).Should(Equal("10.1.2.3/24"))
|
||||
|
||||
// Allocate the IP with the same container ID again
|
||||
_, _, err = testutils.CmdAddWithArgs(args, func() error {
|
||||
return cmdAdd(args)
|
||||
})
|
||||
Expect(err).To(HaveOccurred())
|
||||
|
||||
ipFilePath := filepath.Join(tmpDir, "mynet0", "10.1.2.2")
|
||||
|
||||
// Release the IPs
|
||||
err = testutils.CmdDelWithArgs(args, func() error {
|
||||
return cmdDel(args)
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
_, err = os.Stat(ipFilePath)
|
||||
Expect(err).To(HaveOccurred())
|
||||
|
||||
err = testutils.CmdDelWithArgs(args1, func() error {
|
||||
return cmdDel(args1)
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
|
||||
It("allocates and releases an address with ADD/DEL and 0.1.0 config", func() {
|
||||
const ifname string = "eth0"
|
||||
It("Verify DEL works on backwards compatible allocate", func() {
|
||||
const nspath string = "/some/where"
|
||||
const ifname string = "eth0"
|
||||
|
||||
tmpDir, err := ioutil.TempDir("", "host_local_artifacts")
|
||||
tmpDir, err := getTmpDir()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
@ -184,17 +350,19 @@ var _ = Describe("host-local Operations", func() {
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
conf := fmt.Sprintf(`{
|
||||
"cniVersion": "0.1.0",
|
||||
"name": "mynet",
|
||||
"type": "ipvlan",
|
||||
"master": "foo0",
|
||||
"ipam": {
|
||||
"type": "host-local",
|
||||
"subnet": "10.1.2.0/24",
|
||||
"dataDir": "%s",
|
||||
"resolvConf": "%s/resolv.conf"
|
||||
}
|
||||
}`, tmpDir, tmpDir)
|
||||
"cniVersion": "0.3.1",
|
||||
"name": "mynet",
|
||||
"type": "ipvlan",
|
||||
"master": "foo",
|
||||
"ipam": {
|
||||
"type": "host-local",
|
||||
"dataDir": "%s",
|
||||
"resolvConf": "%s/resolv.conf",
|
||||
"ranges": [
|
||||
[{ "subnet": "10.1.2.0/24" }]
|
||||
]
|
||||
}
|
||||
}`, tmpDir, tmpDir)
|
||||
|
||||
args := &skel.CmdArgs{
|
||||
ContainerID: "dummy",
|
||||
@ -204,7 +372,97 @@ var _ = Describe("host-local Operations", func() {
|
||||
}
|
||||
|
||||
// Allocate the IP
|
||||
r, raw, err := testutils.CmdAddWithResult(nspath, ifname, []byte(conf), func() error {
|
||||
r, raw, err := testutils.CmdAddWithArgs(args, func() error {
|
||||
return cmdAdd(args)
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(strings.Index(string(raw), "\"version\":")).Should(BeNumerically(">", 0))
|
||||
|
||||
_, err = current.GetResult(r)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
ipFilePath := filepath.Join(tmpDir, "mynet", "10.1.2.2")
|
||||
contents, err := ioutil.ReadFile(ipFilePath)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(string(contents)).To(Equal(args.ContainerID + LineBreak + ifname))
|
||||
err = ioutil.WriteFile(ipFilePath, []byte(strings.TrimSpace(args.ContainerID)), 0644)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
err = testutils.CmdDelWithArgs(args, func() error {
|
||||
return cmdDel(args)
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
_, err = os.Stat(ipFilePath)
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
|
||||
It("doesn't error when passed an unknown ID on DEL", func() {
|
||||
const ifname string = "eth0"
|
||||
const nspath string = "/some/where"
|
||||
|
||||
tmpDir, err := getTmpDir()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
conf := fmt.Sprintf(`{
|
||||
"cniVersion": "0.3.0",
|
||||
"name": "mynet",
|
||||
"type": "ipvlan",
|
||||
"master": "foo0",
|
||||
"ipam": {
|
||||
"type": "host-local",
|
||||
"subnet": "10.1.2.0/24",
|
||||
"dataDir": "%s"
|
||||
}
|
||||
}`, tmpDir)
|
||||
|
||||
args := &skel.CmdArgs{
|
||||
ContainerID: "dummy",
|
||||
Netns: nspath,
|
||||
IfName: ifname,
|
||||
StdinData: []byte(conf),
|
||||
}
|
||||
|
||||
// Release the IP
|
||||
err = testutils.CmdDelWithArgs(args, func() error {
|
||||
return cmdDel(args)
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
|
||||
It("allocates and releases an address with ADD/DEL and 0.1.0 config", func() {
|
||||
const ifname string = "eth0"
|
||||
const nspath string = "/some/where"
|
||||
|
||||
tmpDir, err := getTmpDir()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
err = ioutil.WriteFile(filepath.Join(tmpDir, "resolv.conf"), []byte("nameserver 192.0.2.3"), 0644)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
conf := fmt.Sprintf(`{
|
||||
"cniVersion": "0.1.0",
|
||||
"name": "mynet",
|
||||
"type": "ipvlan",
|
||||
"master": "foo0",
|
||||
"ipam": {
|
||||
"type": "host-local",
|
||||
"subnet": "10.1.2.0/24",
|
||||
"dataDir": "%s",
|
||||
"resolvConf": "%s/resolv.conf"
|
||||
}
|
||||
}`, tmpDir, tmpDir)
|
||||
|
||||
args := &skel.CmdArgs{
|
||||
ContainerID: "dummy",
|
||||
Netns: nspath,
|
||||
IfName: ifname,
|
||||
StdinData: []byte(conf),
|
||||
}
|
||||
|
||||
// Allocate the IP
|
||||
r, raw, err := testutils.CmdAddWithArgs(args, func() error {
|
||||
return cmdAdd(args)
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
@ -222,7 +480,7 @@ var _ = Describe("host-local Operations", func() {
|
||||
ipFilePath := filepath.Join(tmpDir, "mynet", "10.1.2.2")
|
||||
contents, err := ioutil.ReadFile(ipFilePath)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(string(contents)).To(Equal("dummy"))
|
||||
Expect(string(contents)).To(Equal(args.ContainerID + LineBreak + ifname))
|
||||
|
||||
lastFilePath := filepath.Join(tmpDir, "mynet", "last_reserved_ip.0")
|
||||
contents, err = ioutil.ReadFile(lastFilePath)
|
||||
@ -232,7 +490,7 @@ var _ = Describe("host-local Operations", func() {
|
||||
Expect(result.DNS).To(Equal(types.DNS{Nameservers: []string{"192.0.2.3"}}))
|
||||
|
||||
// Release the IP
|
||||
err = testutils.CmdDelWithResult(nspath, ifname, func() error {
|
||||
err = testutils.CmdDelWithArgs(args, func() error {
|
||||
return cmdDel(args)
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
@ -245,21 +503,21 @@ var _ = Describe("host-local Operations", func() {
|
||||
const ifname string = "eth0"
|
||||
const nspath string = "/some/where"
|
||||
|
||||
tmpDir, err := ioutil.TempDir("", "host_local_artifacts")
|
||||
tmpDir, err := getTmpDir()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
conf := fmt.Sprintf(`{
|
||||
"cniVersion": "0.3.1",
|
||||
"name": "mynet",
|
||||
"type": "ipvlan",
|
||||
"master": "foo0",
|
||||
"ipam": {
|
||||
"type": "host-local",
|
||||
"subnet": "10.1.2.0/24",
|
||||
"dataDir": "%s"
|
||||
}
|
||||
}`, tmpDir)
|
||||
"cniVersion": "0.3.1",
|
||||
"name": "mynet",
|
||||
"type": "ipvlan",
|
||||
"master": "foo0",
|
||||
"ipam": {
|
||||
"type": "host-local",
|
||||
"subnet": "10.1.2.0/24",
|
||||
"dataDir": "%s"
|
||||
}
|
||||
}`, tmpDir)
|
||||
|
||||
args := &skel.CmdArgs{
|
||||
ContainerID: " dummy\n ",
|
||||
@ -269,7 +527,7 @@ var _ = Describe("host-local Operations", func() {
|
||||
}
|
||||
|
||||
// Allocate the IP
|
||||
r, _, err := testutils.CmdAddWithResult(nspath, ifname, []byte(conf), func() error {
|
||||
r, _, err := testutils.CmdAddWithArgs(args, func() error {
|
||||
return cmdAdd(args)
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
@ -280,10 +538,10 @@ var _ = Describe("host-local Operations", func() {
|
||||
ipFilePath := filepath.Join(tmpDir, "mynet", result.IPs[0].Address.IP.String())
|
||||
contents, err := ioutil.ReadFile(ipFilePath)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(string(contents)).To(Equal("dummy"))
|
||||
Expect(string(contents)).To(Equal("dummy" + LineBreak + ifname))
|
||||
|
||||
// Release the IP
|
||||
err = testutils.CmdDelWithResult(nspath, ifname, func() error {
|
||||
err = testutils.CmdDelWithArgs(args, func() error {
|
||||
return cmdDel(args)
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
@ -296,21 +554,21 @@ var _ = Describe("host-local Operations", func() {
|
||||
const ifname string = "eth0"
|
||||
const nspath string = "/some/where"
|
||||
|
||||
tmpDir, err := ioutil.TempDir("", "host_local_artifacts")
|
||||
tmpDir, err := getTmpDir()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
conf := fmt.Sprintf(`{
|
||||
"cniVersion": "0.2.0",
|
||||
"name": "mynet",
|
||||
"type": "ipvlan",
|
||||
"master": "foo0",
|
||||
"ipam": {
|
||||
"type": "host-local",
|
||||
"subnet": "10.1.2.0/24",
|
||||
"dataDir": "%s"
|
||||
}
|
||||
}`, tmpDir)
|
||||
"cniVersion": "0.2.0",
|
||||
"name": "mynet",
|
||||
"type": "ipvlan",
|
||||
"master": "foo0",
|
||||
"ipam": {
|
||||
"type": "host-local",
|
||||
"subnet": "10.1.2.0/24",
|
||||
"dataDir": "%s"
|
||||
}
|
||||
}`, tmpDir)
|
||||
|
||||
args := &skel.CmdArgs{
|
||||
ContainerID: "testing",
|
||||
@ -320,7 +578,7 @@ var _ = Describe("host-local Operations", func() {
|
||||
}
|
||||
|
||||
// Allocate the IP
|
||||
_, out, err := testutils.CmdAddWithResult(nspath, ifname, []byte(conf), func() error {
|
||||
_, out, err := testutils.CmdAddWithArgs(args, func() error {
|
||||
return cmdAdd(args)
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
@ -331,28 +589,28 @@ var _ = Describe("host-local Operations", func() {
|
||||
const ifname string = "eth0"
|
||||
const nspath string = "/some/where"
|
||||
|
||||
tmpDir, err := ioutil.TempDir("", "host_local_artifacts")
|
||||
tmpDir, err := getTmpDir()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
conf := fmt.Sprintf(`{
|
||||
"cniVersion": "0.3.1",
|
||||
"name": "mynet",
|
||||
"type": "ipvlan",
|
||||
"master": "foo0",
|
||||
"ipam": {
|
||||
"type": "host-local",
|
||||
"dataDir": "%s",
|
||||
"ranges": [
|
||||
[{ "subnet": "10.1.2.0/24" }]
|
||||
]
|
||||
},
|
||||
"args": {
|
||||
"cni": {
|
||||
"ips": ["10.1.2.88"]
|
||||
}
|
||||
}
|
||||
}`, tmpDir)
|
||||
"cniVersion": "0.3.1",
|
||||
"name": "mynet",
|
||||
"type": "ipvlan",
|
||||
"master": "foo0",
|
||||
"ipam": {
|
||||
"type": "host-local",
|
||||
"dataDir": "%s",
|
||||
"ranges": [
|
||||
[{ "subnet": "10.1.2.0/24" }]
|
||||
]
|
||||
},
|
||||
"args": {
|
||||
"cni": {
|
||||
"ips": ["10.1.2.88"]
|
||||
}
|
||||
}
|
||||
}`, tmpDir)
|
||||
|
||||
args := &skel.CmdArgs{
|
||||
ContainerID: "dummy",
|
||||
@ -362,7 +620,7 @@ var _ = Describe("host-local Operations", func() {
|
||||
}
|
||||
|
||||
// Allocate the IP
|
||||
r, _, err := testutils.CmdAddWithResult(nspath, ifname, []byte(conf), func() error {
|
||||
r, _, err := testutils.CmdAddWithArgs(args, func() error {
|
||||
return cmdAdd(args)
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
@ -376,7 +634,7 @@ var _ = Describe("host-local Operations", func() {
|
||||
const ifname string = "eth0"
|
||||
const nspath string = "/some/where"
|
||||
|
||||
tmpDir, err := ioutil.TempDir("", "host_local_artifacts")
|
||||
tmpDir, err := getTmpDir()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
@ -384,24 +642,24 @@ var _ = Describe("host-local Operations", func() {
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
conf := fmt.Sprintf(`{
|
||||
"cniVersion": "0.3.1",
|
||||
"name": "mynet",
|
||||
"type": "ipvlan",
|
||||
"master": "foo0",
|
||||
"ipam": {
|
||||
"type": "host-local",
|
||||
"dataDir": "%s",
|
||||
"ranges": [
|
||||
[{ "subnet": "10.1.2.0/24" }],
|
||||
[{ "subnet": "10.1.3.0/24" }]
|
||||
]
|
||||
},
|
||||
"args": {
|
||||
"cni": {
|
||||
"ips": ["10.1.2.88", "10.1.3.77"]
|
||||
}
|
||||
}
|
||||
}`, tmpDir)
|
||||
"cniVersion": "0.3.1",
|
||||
"name": "mynet",
|
||||
"type": "ipvlan",
|
||||
"master": "foo0",
|
||||
"ipam": {
|
||||
"type": "host-local",
|
||||
"dataDir": "%s",
|
||||
"ranges": [
|
||||
[{ "subnet": "10.1.2.0/24" }],
|
||||
[{ "subnet": "10.1.3.0/24" }]
|
||||
]
|
||||
},
|
||||
"args": {
|
||||
"cni": {
|
||||
"ips": ["10.1.2.88", "10.1.3.77"]
|
||||
}
|
||||
}
|
||||
}`, tmpDir)
|
||||
|
||||
args := &skel.CmdArgs{
|
||||
ContainerID: "dummy",
|
||||
@ -411,7 +669,7 @@ var _ = Describe("host-local Operations", func() {
|
||||
}
|
||||
|
||||
// Allocate the IP
|
||||
r, _, err := testutils.CmdAddWithResult(nspath, ifname, []byte(conf), func() error {
|
||||
r, _, err := testutils.CmdAddWithArgs(args, func() error {
|
||||
return cmdAdd(args)
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
@ -426,7 +684,7 @@ var _ = Describe("host-local Operations", func() {
|
||||
const ifname string = "eth0"
|
||||
const nspath string = "/some/where"
|
||||
|
||||
tmpDir, err := ioutil.TempDir("", "host_local_artifacts")
|
||||
tmpDir, err := getTmpDir()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
@ -434,24 +692,24 @@ var _ = Describe("host-local Operations", func() {
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
conf := fmt.Sprintf(`{
|
||||
"cniVersion": "0.3.1",
|
||||
"name": "mynet",
|
||||
"type": "ipvlan",
|
||||
"master": "foo0",
|
||||
"ipam": {
|
||||
"type": "host-local",
|
||||
"dataDir": "%s",
|
||||
"ranges": [
|
||||
[{"subnet":"172.16.1.0/24"}, { "subnet": "10.1.2.0/24" }],
|
||||
[{ "subnet": "2001:db8:1::/24" }]
|
||||
]
|
||||
},
|
||||
"args": {
|
||||
"cni": {
|
||||
"ips": ["10.1.2.88", "2001:db8:1::999"]
|
||||
}
|
||||
}
|
||||
}`, tmpDir)
|
||||
"cniVersion": "0.3.1",
|
||||
"name": "mynet",
|
||||
"type": "ipvlan",
|
||||
"master": "foo0",
|
||||
"ipam": {
|
||||
"type": "host-local",
|
||||
"dataDir": "%s",
|
||||
"ranges": [
|
||||
[{"subnet":"172.16.1.0/24"}, { "subnet": "10.1.2.0/24" }],
|
||||
[{ "subnet": "2001:db8:1::/48" }]
|
||||
]
|
||||
},
|
||||
"args": {
|
||||
"cni": {
|
||||
"ips": ["10.1.2.88", "2001:db8:1::999"]
|
||||
}
|
||||
}
|
||||
}`, tmpDir)
|
||||
|
||||
args := &skel.CmdArgs{
|
||||
ContainerID: "dummy",
|
||||
@ -461,7 +719,7 @@ var _ = Describe("host-local Operations", func() {
|
||||
}
|
||||
|
||||
// Allocate the IP
|
||||
r, _, err := testutils.CmdAddWithResult(nspath, ifname, []byte(conf), func() error {
|
||||
r, _, err := testutils.CmdAddWithArgs(args, func() error {
|
||||
return cmdAdd(args)
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
@ -476,29 +734,29 @@ var _ = Describe("host-local Operations", func() {
|
||||
const ifname string = "eth0"
|
||||
const nspath string = "/some/where"
|
||||
|
||||
tmpDir, err := ioutil.TempDir("", "host_local_artifacts")
|
||||
tmpDir, err := getTmpDir()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
conf := fmt.Sprintf(`{
|
||||
"cniVersion": "0.3.1",
|
||||
"name": "mynet",
|
||||
"type": "ipvlan",
|
||||
"master": "foo0",
|
||||
"ipam": {
|
||||
"type": "host-local",
|
||||
"dataDir": "%s",
|
||||
"ranges": [
|
||||
[{ "subnet": "10.1.2.0/24" }],
|
||||
[{ "subnet": "10.1.3.0/24" }]
|
||||
]
|
||||
},
|
||||
"args": {
|
||||
"cni": {
|
||||
"ips": ["10.1.2.88", "10.1.2.77"]
|
||||
}
|
||||
}
|
||||
}`, tmpDir)
|
||||
"cniVersion": "0.3.1",
|
||||
"name": "mynet",
|
||||
"type": "ipvlan",
|
||||
"master": "foo0",
|
||||
"ipam": {
|
||||
"type": "host-local",
|
||||
"dataDir": "%s",
|
||||
"ranges": [
|
||||
[{ "subnet": "10.1.2.0/24" }],
|
||||
[{ "subnet": "10.1.3.0/24" }]
|
||||
]
|
||||
},
|
||||
"args": {
|
||||
"cni": {
|
||||
"ips": ["10.1.2.88", "10.1.2.77"]
|
||||
}
|
||||
}
|
||||
}`, tmpDir)
|
||||
|
||||
args := &skel.CmdArgs{
|
||||
ContainerID: "dummy",
|
||||
@ -508,7 +766,7 @@ var _ = Describe("host-local Operations", func() {
|
||||
}
|
||||
|
||||
// Allocate the IP
|
||||
_, _, err = testutils.CmdAddWithResult(nspath, ifname, []byte(conf), func() error {
|
||||
_, _, err = testutils.CmdAddWithArgs(args, func() error {
|
||||
return cmdAdd(args)
|
||||
})
|
||||
Expect(err).To(HaveOccurred())
|
||||
@ -517,6 +775,15 @@ var _ = Describe("host-local Operations", func() {
|
||||
})
|
||||
})
|
||||
|
||||
func getTmpDir() (string, error) {
|
||||
tmpDir, err := ioutil.TempDir("", "host_local_artifacts")
|
||||
if err == nil {
|
||||
tmpDir = filepath.ToSlash(tmpDir)
|
||||
}
|
||||
|
||||
return tmpDir, err
|
||||
}
|
||||
|
||||
func mustCIDR(s string) net.IPNet {
|
||||
ip, n, err := net.ParseCIDR(s)
|
||||
n.IP = ip
|
||||
|
@ -15,10 +15,12 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
bv "github.com/containernetworking/plugins/pkg/utils/buildversion"
|
||||
"github.com/containernetworking/plugins/plugins/ipam/host-local/backend/allocator"
|
||||
"github.com/containernetworking/plugins/plugins/ipam/host-local/backend/disk"
|
||||
|
||||
@ -29,7 +31,38 @@ import (
|
||||
)
|
||||
|
||||
func main() {
|
||||
skel.PluginMain(cmdAdd, cmdDel, version.All)
|
||||
skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.All, bv.BuildString("host-local"))
|
||||
}
|
||||
|
||||
func loadNetConf(bytes []byte) (*types.NetConf, string, error) {
|
||||
n := &types.NetConf{}
|
||||
if err := json.Unmarshal(bytes, n); err != nil {
|
||||
return nil, "", fmt.Errorf("failed to load netconf: %v", err)
|
||||
}
|
||||
return n, n.CNIVersion, nil
|
||||
}
|
||||
|
||||
func cmdCheck(args *skel.CmdArgs) error {
|
||||
|
||||
ipamConf, _, err := allocator.LoadIPAMConfig(args.StdinData, args.Args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Look to see if there is at least one IP address allocated to the container
|
||||
// in the data dir, irrespective of what that address actually is
|
||||
store, err := disk.New(ipamConf.Name, ipamConf.DataDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
containerIpFound := store.FindByID(args.ContainerID, args.IfName)
|
||||
if containerIpFound == false {
|
||||
return fmt.Errorf("host-local: Failed to find address added by container %v", args.ContainerID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func cmdAdd(args *skel.CmdArgs) error {
|
||||
@ -79,11 +112,11 @@ func cmdAdd(args *skel.CmdArgs) error {
|
||||
}
|
||||
}
|
||||
|
||||
ipConf, err := allocator.Get(args.ContainerID, requestedIP)
|
||||
ipConf, err := allocator.Get(args.ContainerID, args.IfName, requestedIP)
|
||||
if err != nil {
|
||||
// Deallocate all already allocated IPs
|
||||
for _, alloc := range allocs {
|
||||
_ = alloc.Release(args.ContainerID)
|
||||
_ = alloc.Release(args.ContainerID, args.IfName)
|
||||
}
|
||||
return fmt.Errorf("failed to allocate for range %d: %v", idx, err)
|
||||
}
|
||||
@ -96,7 +129,7 @@ func cmdAdd(args *skel.CmdArgs) error {
|
||||
// If an IP was requested that wasn't fulfilled, fail
|
||||
if len(requestedIPs) != 0 {
|
||||
for _, alloc := range allocs {
|
||||
_ = alloc.Release(args.ContainerID)
|
||||
_ = alloc.Release(args.ContainerID, args.IfName)
|
||||
}
|
||||
errstr := "failed to allocate all requested IPs:"
|
||||
for _, ip := range requestedIPs {
|
||||
@ -127,7 +160,7 @@ func cmdDel(args *skel.CmdArgs) error {
|
||||
for idx, rangeset := range ipamConf.Ranges {
|
||||
ipAllocator := allocator.NewIPAllocator(&rangeset, store, idx)
|
||||
|
||||
err := ipAllocator.Release(args.ContainerID)
|
||||
err := ipAllocator.Release(args.ContainerID, args.IfName)
|
||||
if err != nil {
|
||||
errors = append(errors, err.Error())
|
||||
}
|
||||
|
4
plugins/ipam/static/README.md
Normal file
4
plugins/ipam/static/README.md
Normal file
@ -0,0 +1,4 @@
|
||||
|
||||
This document has moved to the [containernetworking/cni.dev](https://github.com/containernetworking/cni.dev) repo.
|
||||
|
||||
You can find it online here: https://cni.dev/plugins/ipam/static/
|
273
plugins/ipam/static/main.go
Normal file
273
plugins/ipam/static/main.go
Normal file
@ -0,0 +1,273 @@
|
||||
// Copyright 2018 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 main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/skel"
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
types020 "github.com/containernetworking/cni/pkg/types/020"
|
||||
"github.com/containernetworking/cni/pkg/types/current"
|
||||
"github.com/containernetworking/cni/pkg/version"
|
||||
bv "github.com/containernetworking/plugins/pkg/utils/buildversion"
|
||||
)
|
||||
|
||||
// The top-level network config - IPAM plugins are passed the full configuration
|
||||
// of the calling plugin, not just the IPAM section.
|
||||
type Net struct {
|
||||
Name string `json:"name"`
|
||||
CNIVersion string `json:"cniVersion"`
|
||||
IPAM *IPAMConfig `json:"ipam"`
|
||||
|
||||
RuntimeConfig struct {
|
||||
IPs []string `json:"ips,omitempty"`
|
||||
} `json:"runtimeConfig,omitempty"`
|
||||
Args *struct {
|
||||
A *IPAMArgs `json:"cni"`
|
||||
} `json:"args"`
|
||||
}
|
||||
|
||||
type IPAMConfig struct {
|
||||
Name string
|
||||
Type string `json:"type"`
|
||||
Routes []*types.Route `json:"routes"`
|
||||
Addresses []Address `json:"addresses,omitempty"`
|
||||
DNS types.DNS `json:"dns"`
|
||||
}
|
||||
|
||||
type IPAMEnvArgs struct {
|
||||
types.CommonArgs
|
||||
IP types.UnmarshallableString `json:"ip,omitempty"`
|
||||
GATEWAY types.UnmarshallableString `json:"gateway,omitempty"`
|
||||
}
|
||||
|
||||
type IPAMArgs struct {
|
||||
IPs []string `json:"ips"`
|
||||
}
|
||||
|
||||
type Address struct {
|
||||
AddressStr string `json:"address"`
|
||||
Gateway net.IP `json:"gateway,omitempty"`
|
||||
Address net.IPNet
|
||||
Version string
|
||||
}
|
||||
|
||||
func main() {
|
||||
skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.All, bv.BuildString("static"))
|
||||
}
|
||||
|
||||
func loadNetConf(bytes []byte) (*types.NetConf, string, error) {
|
||||
n := &types.NetConf{}
|
||||
if err := json.Unmarshal(bytes, n); err != nil {
|
||||
return nil, "", fmt.Errorf("failed to load netconf: %v", err)
|
||||
}
|
||||
return n, n.CNIVersion, nil
|
||||
}
|
||||
|
||||
func cmdCheck(args *skel.CmdArgs) error {
|
||||
ipamConf, _, err := LoadIPAMConfig(args.StdinData, args.Args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get PrevResult from stdin... store in RawPrevResult
|
||||
n, _, err := loadNetConf(args.StdinData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Parse previous result.
|
||||
if n.RawPrevResult == nil {
|
||||
return fmt.Errorf("Required prevResult missing")
|
||||
}
|
||||
|
||||
if err := version.ParsePrevResult(n); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result, err := current.NewResultFromResult(n.PrevResult)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Each configured IP should be found in result.IPs
|
||||
for _, rangeset := range ipamConf.Addresses {
|
||||
for _, ips := range result.IPs {
|
||||
// Ensure values are what we expect
|
||||
if rangeset.Address.IP.Equal(ips.Address.IP) {
|
||||
if rangeset.Gateway == nil {
|
||||
break
|
||||
} else if rangeset.Gateway.Equal(ips.Gateway) {
|
||||
break
|
||||
}
|
||||
return fmt.Errorf("static: Failed to match addr %v on interface %v", ips.Address.IP, args.IfName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// canonicalizeIP makes sure a provided ip is in standard form
|
||||
func canonicalizeIP(ip *net.IP) error {
|
||||
if ip.To4() != nil {
|
||||
*ip = ip.To4()
|
||||
return nil
|
||||
} else if ip.To16() != nil {
|
||||
*ip = ip.To16()
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("IP %s not v4 nor v6", *ip)
|
||||
}
|
||||
|
||||
// LoadIPAMConfig creates IPAMConfig using json encoded configuration provided
|
||||
// as `bytes`. At the moment values provided in envArgs are ignored so there
|
||||
// is no possibility to overload the json configuration using envArgs
|
||||
func LoadIPAMConfig(bytes []byte, envArgs string) (*IPAMConfig, string, error) {
|
||||
n := Net{}
|
||||
if err := json.Unmarshal(bytes, &n); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
// load IP from CNI_ARGS
|
||||
if envArgs != "" {
|
||||
e := IPAMEnvArgs{}
|
||||
err := types.LoadArgs(envArgs, &e)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
if e.IP != "" {
|
||||
for _, item := range strings.Split(string(e.IP), ",") {
|
||||
ipstr := strings.TrimSpace(item)
|
||||
|
||||
ip, subnet, err := net.ParseCIDR(ipstr)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("invalid CIDR %s: %s", ipstr, err)
|
||||
}
|
||||
|
||||
addr := Address{
|
||||
Address: net.IPNet{IP: ip, Mask: subnet.Mask},
|
||||
AddressStr: ipstr,
|
||||
}
|
||||
n.IPAM.Addresses = append(n.IPAM.Addresses, addr)
|
||||
}
|
||||
}
|
||||
|
||||
if e.GATEWAY != "" {
|
||||
for _, item := range strings.Split(string(e.GATEWAY), ",") {
|
||||
gwip := net.ParseIP(strings.TrimSpace(item))
|
||||
if gwip == nil {
|
||||
return nil, "", fmt.Errorf("invalid gateway address: %s", item)
|
||||
}
|
||||
|
||||
for i := range n.IPAM.Addresses {
|
||||
if n.IPAM.Addresses[i].Address.Contains(gwip) {
|
||||
n.IPAM.Addresses[i].Gateway = gwip
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// import address from args
|
||||
if n.Args != nil && n.Args.A != nil && len(n.Args.A.IPs) != 0 {
|
||||
// args IP overwrites IP, so clear IPAM Config
|
||||
n.IPAM.Addresses = make([]Address, 0, len(n.Args.A.IPs))
|
||||
for _, addr := range n.Args.A.IPs {
|
||||
n.IPAM.Addresses = append(n.IPAM.Addresses, Address{AddressStr: addr})
|
||||
}
|
||||
}
|
||||
|
||||
// import address from runtimeConfig
|
||||
if len(n.RuntimeConfig.IPs) != 0 {
|
||||
// runtimeConfig IP overwrites IP, so clear IPAM Config
|
||||
n.IPAM.Addresses = make([]Address, 0, len(n.RuntimeConfig.IPs))
|
||||
for _, addr := range n.RuntimeConfig.IPs {
|
||||
n.IPAM.Addresses = append(n.IPAM.Addresses, Address{AddressStr: addr})
|
||||
}
|
||||
}
|
||||
|
||||
if n.IPAM == nil {
|
||||
return nil, "", fmt.Errorf("IPAM config missing 'ipam' key")
|
||||
}
|
||||
|
||||
// Validate all ranges
|
||||
numV4 := 0
|
||||
numV6 := 0
|
||||
|
||||
for i := range n.IPAM.Addresses {
|
||||
ip, addr, err := net.ParseCIDR(n.IPAM.Addresses[i].AddressStr)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("invalid CIDR %s: %s", n.IPAM.Addresses[i].AddressStr, err)
|
||||
}
|
||||
n.IPAM.Addresses[i].Address = *addr
|
||||
n.IPAM.Addresses[i].Address.IP = ip
|
||||
|
||||
if err := canonicalizeIP(&n.IPAM.Addresses[i].Address.IP); err != nil {
|
||||
return nil, "", fmt.Errorf("invalid address %d: %s", i, err)
|
||||
}
|
||||
|
||||
if n.IPAM.Addresses[i].Address.IP.To4() != nil {
|
||||
n.IPAM.Addresses[i].Version = "4"
|
||||
numV4++
|
||||
} else {
|
||||
n.IPAM.Addresses[i].Version = "6"
|
||||
numV6++
|
||||
}
|
||||
}
|
||||
|
||||
// CNI spec 0.2.0 and below supported only one v4 and v6 address
|
||||
if numV4 > 1 || numV6 > 1 {
|
||||
for _, v := range types020.SupportedVersions {
|
||||
if n.CNIVersion == v {
|
||||
return nil, "", fmt.Errorf("CNI version %v does not support more than 1 address per family", n.CNIVersion)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Copy net name into IPAM so not to drag Net struct around
|
||||
n.IPAM.Name = n.Name
|
||||
|
||||
return n.IPAM, n.CNIVersion, nil
|
||||
}
|
||||
|
||||
func cmdAdd(args *skel.CmdArgs) error {
|
||||
ipamConf, confVersion, err := LoadIPAMConfig(args.StdinData, args.Args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result := ¤t.Result{}
|
||||
result.DNS = ipamConf.DNS
|
||||
result.Routes = ipamConf.Routes
|
||||
for _, v := range ipamConf.Addresses {
|
||||
result.IPs = append(result.IPs, ¤t.IPConfig{
|
||||
Version: v.Version,
|
||||
Address: v.Address,
|
||||
Gateway: v.Gateway})
|
||||
}
|
||||
|
||||
return types.PrintResult(result, confVersion)
|
||||
}
|
||||
|
||||
func cmdDel(args *skel.CmdArgs) error {
|
||||
// Nothing required because of no resource allocation in static plugin.
|
||||
return nil
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user