Compare commits
232 Commits
v0.7.0-rc1
...
v0.8.0
Author | SHA1 | Date | |
---|---|---|---|
0950a3607b | |||
a6a8a818e4 | |||
02ccf1ef9b | |||
a03e6b505e | |||
6bceb68143 | |||
e2187efec1 | |||
5a78120e24 | |||
071907f867 | |||
2d6d46d308 | |||
af692de1b8 | |||
ae0b03787f | |||
b199dc3ffe | |||
dbc86e4db2 | |||
29801c45a7 | |||
12b426a921 | |||
7efec9ea46 | |||
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 |
@ -1,28 +0,0 @@
|
||||
clone_folder: c:\gopath\src\github.com\containernetworking\plugins
|
||||
|
||||
environment:
|
||||
GOPATH: c:\gopath
|
||||
|
||||
install:
|
||||
- echo %PATH%
|
||||
- echo %GOPATH%
|
||||
- set PATH=%GOPATH%\bin;c:\go\bin;%PATH%
|
||||
- go version
|
||||
- go env
|
||||
|
||||
build: off
|
||||
|
||||
test_script:
|
||||
- ps: |
|
||||
go list ./... | Select-String -Pattern (Get-Content "./plugins/linux_only.txt") -NotMatch > "to_test.txt"
|
||||
echo "Will test:"
|
||||
Get-Content "to_test.txt"
|
||||
foreach ($pkg in Get-Content "to_test.txt") {
|
||||
if ($pkg) {
|
||||
echo $pkg
|
||||
go test -v $pkg
|
||||
if ($LastExitCode -ne 0) {
|
||||
throw "test failed"
|
||||
}
|
||||
}
|
||||
}
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -26,3 +26,5 @@ _testmain.go
|
||||
bin/
|
||||
gopath/
|
||||
.vagrant
|
||||
.idea
|
||||
/release-*
|
||||
|
20
.travis.yml
20
.travis.yml
@ -3,12 +3,13 @@ sudo: required
|
||||
dist: trusty
|
||||
|
||||
go:
|
||||
- 1.8.x
|
||||
- 1.9.x
|
||||
- 1.10.x
|
||||
- 1.11.x
|
||||
|
||||
env:
|
||||
global:
|
||||
- PATH=$GOROOT/bin:$GOPATH/bin:$PATH
|
||||
- CGO_ENABLED=0
|
||||
matrix:
|
||||
- TARGET=amd64
|
||||
- TARGET=arm
|
||||
@ -18,16 +19,27 @@ env:
|
||||
|
||||
matrix:
|
||||
fast_finish: true
|
||||
include:
|
||||
- os: windows
|
||||
env: TARGET=amd64
|
||||
go: 1.10.x
|
||||
- os: windows
|
||||
env: TARGET=amd64
|
||||
go: 1.11.x
|
||||
|
||||
install:
|
||||
- go get github.com/onsi/ginkgo/ginkgo
|
||||
- go get github.com/containernetworking/cni/cnitool
|
||||
- go get golang.org/x/tools/cmd/cover
|
||||
- go get github.com/modocache/gover
|
||||
- go get github.com/mattn/goveralls
|
||||
|
||||
script:
|
||||
- |
|
||||
if [ "${TARGET}" == "amd64" ]; then
|
||||
GOARCH="${TARGET}" ./test.sh
|
||||
GOARCH="${TARGET}" ./test_${TRAVIS_OS_NAME}.sh
|
||||
else
|
||||
GOARCH="${TARGET}" ./build.sh
|
||||
GOARCH="${TARGET}" ./build_linux.sh
|
||||
fi
|
||||
|
||||
notifications:
|
||||
|
@ -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,10 +66,12 @@ 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
|
||||
cd /go/src/github.com/containernetworking/plugins
|
||||
|
||||
# to run the full test suite
|
||||
|
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.
|
171
Godeps/Godeps.json
generated
171
Godeps/Godeps.json
generated
@ -1,59 +1,128 @@
|
||||
{
|
||||
"ImportPath": "github.com/containernetworking/plugins",
|
||||
"GoVersion": "go1.7",
|
||||
"GodepVersion": "v79",
|
||||
"GodepVersion": "v80",
|
||||
"Packages": [
|
||||
"./..."
|
||||
],
|
||||
"Deps": [
|
||||
{
|
||||
"ImportPath": "github.com/Microsoft/go-winio",
|
||||
"Comment": "v0.4.11",
|
||||
"Rev": "97e4973ce50b2ff5f09635a57e2b88a037aae829"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/Microsoft/hcsshim",
|
||||
"Comment": "v0.7.6",
|
||||
"Rev": "e44e499d29527b244d6858772f1b9090eeaddc4e"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/Microsoft/hcsshim/internal/guid",
|
||||
"Comment": "v0.7.4",
|
||||
"Rev": "e44e499d29527b244d6858772f1b9090eeaddc4e"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/Microsoft/hcsshim/internal/hcs",
|
||||
"Comment": "v0.7.4",
|
||||
"Rev": "e44e499d29527b244d6858772f1b9090eeaddc4e"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/Microsoft/hcsshim/internal/hcserror",
|
||||
"Comment": "v0.7.4",
|
||||
"Rev": "e44e499d29527b244d6858772f1b9090eeaddc4e"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/Microsoft/hcsshim/internal/hns",
|
||||
"Comment": "v0.7.4",
|
||||
"Rev": "e44e499d29527b244d6858772f1b9090eeaddc4e"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/Microsoft/hcsshim/internal/interop",
|
||||
"Comment": "v0.7.4",
|
||||
"Rev": "e44e499d29527b244d6858772f1b9090eeaddc4e"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/Microsoft/hcsshim/internal/longpath",
|
||||
"Comment": "v0.7.4",
|
||||
"Rev": "e44e499d29527b244d6858772f1b9090eeaddc4e"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/Microsoft/hcsshim/internal/mergemaps",
|
||||
"Comment": "v0.7.4",
|
||||
"Rev": "e44e499d29527b244d6858772f1b9090eeaddc4e"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/Microsoft/hcsshim/internal/safefile",
|
||||
"Comment": "v0.7.4",
|
||||
"Rev": "e44e499d29527b244d6858772f1b9090eeaddc4e"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/Microsoft/hcsshim/internal/schema1",
|
||||
"Comment": "v0.7.4",
|
||||
"Rev": "e44e499d29527b244d6858772f1b9090eeaddc4e"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/Microsoft/hcsshim/internal/timeout",
|
||||
"Comment": "v0.7.4",
|
||||
"Rev": "e44e499d29527b244d6858772f1b9090eeaddc4e"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/Microsoft/hcsshim/internal/wclayer",
|
||||
"Comment": "v0.7.4",
|
||||
"Rev": "e44e499d29527b244d6858772f1b9090eeaddc4e"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/alexflint/go-filemutex",
|
||||
"Rev": "72bdc8eae2aef913234599b837f5dda445ca9bd9"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/buger/jsonparser",
|
||||
"Rev": "f4dd9f5a6b441265aefc1d44872a6f8c10f42b37"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/containernetworking/cni/libcni",
|
||||
"Comment": "v0.6.0-rc1",
|
||||
"Rev": "a2da8f8d7fd8e6dc25f336408a8ac86f050fbd88"
|
||||
"Comment": "v0.7.0",
|
||||
"Rev": "7d76556571b6cf1ab90d7026a73092ac8d5e0c23"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/containernetworking/cni/pkg/invoke",
|
||||
"Comment": "v0.6.0-rc1",
|
||||
"Rev": "a2da8f8d7fd8e6dc25f336408a8ac86f050fbd88"
|
||||
"Comment": "v0.7.0",
|
||||
"Rev": "7d76556571b6cf1ab90d7026a73092ac8d5e0c23"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/containernetworking/cni/pkg/skel",
|
||||
"Comment": "v0.6.0-rc1",
|
||||
"Rev": "a2da8f8d7fd8e6dc25f336408a8ac86f050fbd88"
|
||||
"Comment": "v0.7.0",
|
||||
"Rev": "7d76556571b6cf1ab90d7026a73092ac8d5e0c23"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/containernetworking/cni/pkg/types",
|
||||
"Comment": "v0.6.0-rc1",
|
||||
"Rev": "a2da8f8d7fd8e6dc25f336408a8ac86f050fbd88"
|
||||
"Comment": "v0.7.0",
|
||||
"Rev": "7d76556571b6cf1ab90d7026a73092ac8d5e0c23"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/containernetworking/cni/pkg/types/020",
|
||||
"Comment": "v0.6.0-rc1",
|
||||
"Rev": "a2da8f8d7fd8e6dc25f336408a8ac86f050fbd88"
|
||||
"Comment": "v0.7.0",
|
||||
"Rev": "7d76556571b6cf1ab90d7026a73092ac8d5e0c23"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/containernetworking/cni/pkg/types/current",
|
||||
"Comment": "v0.6.0-rc1",
|
||||
"Rev": "a2da8f8d7fd8e6dc25f336408a8ac86f050fbd88"
|
||||
"Comment": "v0.7.0",
|
||||
"Rev": "7d76556571b6cf1ab90d7026a73092ac8d5e0c23"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/containernetworking/cni/pkg/version",
|
||||
"Comment": "v0.6.0-rc1",
|
||||
"Rev": "a2da8f8d7fd8e6dc25f336408a8ac86f050fbd88"
|
||||
"Comment": "v0.7.0",
|
||||
"Rev": "7d76556571b6cf1ab90d7026a73092ac8d5e0c23"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/go-iptables/iptables",
|
||||
"Comment": "v0.2.0",
|
||||
"Rev": "259c8e6a4275d497442c721fa52204d7a58bde8b"
|
||||
"Comment": "v0.4.1",
|
||||
"Rev": "78b5fff24e6df8886ef8eca9411f683a884349a5"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/go-systemd/activation",
|
||||
"Comment": "v2-53-g2688e91",
|
||||
"Rev": "2688e91251d9d8e404e86dd8f096e23b2f086958"
|
||||
"Comment": "v17",
|
||||
"Rev": "39ca1b05acc7ad1220e09f133283b8859a8b71ab"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/d2g/dhcp4",
|
||||
@ -63,10 +132,31 @@
|
||||
"ImportPath": "github.com/d2g/dhcp4client",
|
||||
"Rev": "bed07e1bc5b85f69c6f0fd73393aa35ec68ed892"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/d2g/dhcp4server",
|
||||
"Rev": "477b11cea4dcc56af002849238d4f9c1e093c744"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/d2g/dhcp4server/leasepool",
|
||||
"Rev": "477b11cea4dcc56af002849238d4f9c1e093c744"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/d2g/dhcp4server/leasepool/memorypool",
|
||||
"Rev": "477b11cea4dcc56af002849238d4f9c1e093c744"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/godbus/dbus",
|
||||
"Comment": "v4.1.0-6-g885f9cc",
|
||||
"Rev": "885f9cc04c9c1a6a61a2008e211d36c5737be3f5"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/j-keck/arping",
|
||||
"Rev": "2cf9dc699c5640a7e2c81403a44127bf28033600"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/juju/errors",
|
||||
"Rev": "22422dad46e14561a0854ad42497a75af9b61909"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/mattn/go-shellwords",
|
||||
"Comment": "v1.0.3",
|
||||
@ -222,21 +312,56 @@
|
||||
"Comment": "v1.0-71-g2152b45",
|
||||
"Rev": "2152b45fa28a361beba9aab0885972323a444e28"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/safchain/ethtool",
|
||||
"Rev": "7ff1ba29eca231991280817541cb3903f6be15d1"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/sirupsen/logrus",
|
||||
"Comment": "v1.0.6",
|
||||
"Rev": "3e01752db0189b9157070a0e1668a620f9a85da2"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/vishvananda/netlink",
|
||||
"Rev": "6e453822d85ef5721799774b654d4d02fed62afb"
|
||||
"Comment": "v1.0.0-40-g023a6da",
|
||||
"Rev": "023a6dafdcdfa7068ac83b260ab7f03cd4131aca"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/vishvananda/netlink/nl",
|
||||
"Rev": "6e453822d85ef5721799774b654d4d02fed62afb"
|
||||
"Comment": "v1.0.0-40-g023a6da",
|
||||
"Rev": "023a6dafdcdfa7068ac83b260ab7f03cd4131aca"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/vishvananda/netns",
|
||||
"Rev": "54f0e4339ce73702a0607f49922aaa1e749b418d"
|
||||
"Rev": "13995c7128ccc8e51e9a6bd2b551020a27180abd"
|
||||
},
|
||||
{
|
||||
"ImportPath": "golang.org/x/crypto/ssh/terminal",
|
||||
"Rev": "7c1a557ab941a71c619514f229f0b27ccb0c27cf"
|
||||
},
|
||||
{
|
||||
"ImportPath": "golang.org/x/net/bpf",
|
||||
"Rev": "49bb7cea24b1df9410e1712aa6433dae904ff66a"
|
||||
},
|
||||
{
|
||||
"ImportPath": "golang.org/x/net/internal/iana",
|
||||
"Rev": "49bb7cea24b1df9410e1712aa6433dae904ff66a"
|
||||
},
|
||||
{
|
||||
"ImportPath": "golang.org/x/net/ipv4",
|
||||
"Rev": "49bb7cea24b1df9410e1712aa6433dae904ff66a"
|
||||
},
|
||||
{
|
||||
"ImportPath": "golang.org/x/sys/unix",
|
||||
"Rev": "076b546753157f758b316e59bcb51e6807c04057"
|
||||
"Rev": "66b7b1311ac80bbafcd2daeef9a5e6e2cd1e2399"
|
||||
},
|
||||
{
|
||||
"ImportPath": "golang.org/x/sys/windows",
|
||||
"Rev": "66b7b1311ac80bbafcd2daeef9a5e6e2cd1e2399"
|
||||
},
|
||||
{
|
||||
"ImportPath": "golang.org/x/net/internal/socket",
|
||||
"Rev": "49bb7cea24b1df9410e1712aa6433dae904ff66a"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
8
OWNERS.md
Normal file
8
OWNERS.md
Normal file
@ -0,0 +1,8 @@
|
||||
# Owners
|
||||
This is the official list of the CNI network plugins owners:
|
||||
- Bryan Boreham <bryan@weave.works> (@bboreham)
|
||||
- Casey Callendrello <casey.callendrello@coreos.com> (@squeed)
|
||||
- Dan Williams <dcbw@redhat.com> (@dcbw)
|
||||
- Gabe Rosenhouse <grosenhouse@pivotal.io> (@rosenhouse)
|
||||
- Matt Dupre <matt@tigera.io> (@matthewdupre)
|
||||
- Stefan Junker <stefan.junker@coreos.com> (@steveeJ)
|
24
README.md
24
README.md
@ -1,26 +1,34 @@
|
||||
[](https://travis-ci.org/containernetworking/plugins)
|
||||
[](https://ci.appveyor.com/project/cni-bot/plugins/branch/master)
|
||||
[](https://travis-ci.org/containernetworking/plugins)
|
||||
|
||||
# plugins
|
||||
Some CNI network plugins, maintained by the containernetworking team. For more information, see the individual READMEs.
|
||||
|
||||
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.
|
||||
|
9
Vagrantfile
vendored
9
Vagrantfile
vendored
@ -8,14 +8,11 @@ Vagrant.configure(2) do |config|
|
||||
|
||||
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.9.1.linux-amd64.tar.gz | tar -C /usr/local -xz
|
||||
|
||||
apt-get install -y git gcc-multilib gcc-mingw-w64
|
||||
wget -qO- https://storage.googleapis.com/golang/go1.11.1.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
|
||||
end
|
@ -1,8 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
cd $(dirname "$0")
|
||||
|
||||
if [ "$(uname)" == "Darwin" ]; then
|
||||
export GOOS=linux
|
||||
export GOOS="${GOOS:-linux}"
|
||||
fi
|
||||
|
||||
ORG_PATH="github.com/containernetworking"
|
||||
@ -13,23 +14,19 @@ if [ ! -h gopath/src/${REPO_PATH} ]; then
|
||||
ln -s ../../../.. gopath/src/${REPO_PATH} || exit 255
|
||||
fi
|
||||
|
||||
export GO15VENDOREXPERIMENT=1
|
||||
export GOPATH=${PWD}/gopath
|
||||
export GO="${GO:-go}"
|
||||
|
||||
mkdir -p "${PWD}/bin"
|
||||
|
||||
echo "Building plugins"
|
||||
PLUGINS="plugins/meta/* plugins/main/* plugins/ipam/* plugins/sample"
|
||||
echo "Building plugins ${GOOS}"
|
||||
PLUGINS="plugins/meta/* plugins/main/* plugins/ipam/*"
|
||||
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"
|
||||
if [ $plugin != "windows" ]; then
|
||||
echo " $plugin"
|
||||
$GO build -o "${PWD}/bin/$plugin" "$@" "$REPO_PATH"/$d
|
||||
fi
|
||||
fi
|
||||
done
|
23
build_windows.sh
Executable file
23
build_windows.sh
Executable file
@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
cd $(dirname "$0")
|
||||
|
||||
ORG_PATH="github.com/containernetworking"
|
||||
export REPO_PATH="${ORG_PATH}/plugins"
|
||||
|
||||
export GOPATH=$(mktemp -d)
|
||||
mkdir -p ${GOPATH}/src/${ORG_PATH}
|
||||
trap "{ rm -rf $GOPATH; }" EXIT
|
||||
ln -s ${PWD} ${GOPATH}/src/${REPO_PATH} || exit 255
|
||||
|
||||
export GO="${GO:-go}"
|
||||
export GOOS=windows
|
||||
|
||||
PLUGINS=$(cat plugins/windows_only.txt)
|
||||
for d in $PLUGINS; do
|
||||
if [ -d "$d" ]; then
|
||||
plugin="$(basename "$d").exe"
|
||||
echo " $plugin"
|
||||
$GO build -o "${PWD}/bin/$plugin" "$@" "$REPO_PATH"/$d
|
||||
fi
|
||||
done
|
270
integration/integration_linux_test.go
Normal file
270
integration/integration_linux_test.go
Normal file
@ -0,0 +1,270 @@
|
||||
// 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\\.11\\.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.11.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) {
|
||||
message := bytes.Repeat([]byte{'a'}, numBytes)
|
||||
|
||||
bin, err := exec.LookPath("nc")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
var cmd *exec.Cmd
|
||||
if netns != "" {
|
||||
netns = filepath.Base(netns)
|
||||
cmd = exec.Command("ip", "netns", "exec", netns, bin, "-v", address, strconv.Itoa(port))
|
||||
} else {
|
||||
cmd = exec.Command("nc", address, strconv.Itoa(port))
|
||||
}
|
||||
cmd.Stdin = bytes.NewBuffer([]byte(message))
|
||||
cmd.Stderr = GinkgoWriter
|
||||
out, err := cmd.Output()
|
||||
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(string(out)).To(Equal(string(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)
|
||||
}
|
44
integration/integration_suite_test.go
Normal file
44
integration/integration_suite_test.go
Normal file
@ -0,0 +1,44 @@
|
||||
// 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 (
|
||||
"math/rand"
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
"github.com/onsi/ginkgo/config"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/onsi/gomega/gexec"
|
||||
)
|
||||
|
||||
func TestIntegration(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "integration")
|
||||
}
|
||||
|
||||
var echoServerBinaryPath string
|
||||
|
||||
var _ = SynchronizedBeforeSuite(func() []byte {
|
||||
binaryPath, err := gexec.Build("github.com/containernetworking/plugins/pkg/testutils/echosvr")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
return []byte(binaryPath)
|
||||
}, func(data []byte) {
|
||||
echoServerBinaryPath = string(data)
|
||||
rand.Seed(config.GinkgoConfig.RandomSeed + int64(GinkgoParallelNode()))
|
||||
})
|
||||
|
||||
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-0",
|
||||
"isDefaultGateway": true,
|
||||
"ipam": {
|
||||
"type": "host-local",
|
||||
"subnet": "10.11.2.0/24",
|
||||
"dataDir": "/tmp/foo"
|
||||
}
|
||||
},
|
||||
{
|
||||
"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
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
371
pkg/hns/endpoint_windows.go
Normal file
371
pkg/hns/endpoint_windows.go
Normal file
@ -0,0 +1,371 @@
|
||||
// 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/juju/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, "Attempt to get endpoint \"%v\" failed", 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 %v", epInfo.EndpointName)
|
||||
}
|
||||
hnsEndpoint = nil
|
||||
}
|
||||
}
|
||||
|
||||
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, "Attempt to get endpoint \"%v\" failed", 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 %v", epInfo.EndpointName)
|
||||
hcnEndpoint = nil
|
||||
|
||||
}
|
||||
} else {
|
||||
return nil, fmt.Errorf("Endpoint \"%v\" already exits", epInfo.EndpointName)
|
||||
}
|
||||
}
|
||||
|
||||
if hcnEndpoint == nil {
|
||||
routes := []hcn.Route{
|
||||
{
|
||||
NextHop: GetIpString(&epInfo.Gateway),
|
||||
DestinationPrefix: func() string {
|
||||
destinationPrefix := "0.0.0.0/0"
|
||||
if ipv6 := epInfo.Gateway.To4(); ipv6 == nil {
|
||||
destinationPrefix = "::/0"
|
||||
}
|
||||
return destinationPrefix
|
||||
}(),
|
||||
},
|
||||
}
|
||||
|
||||
hcnDns := hcn.Dns{
|
||||
Search: epInfo.DNS.Search,
|
||||
ServerList: epInfo.DNS.Nameservers,
|
||||
}
|
||||
|
||||
hcnIpConfig := hcn.IpConfig{
|
||||
IpAddress: GetIpString(&epInfo.IpAddress),
|
||||
}
|
||||
ipConfigs := []hcn.IpConfig{hcnIpConfig}
|
||||
|
||||
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.Annotatef(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
|
||||
}
|
13
pkg/hns/hns_suite_windows_test.go
Normal file
13
pkg/hns/hns_suite_windows_test.go
Normal file
@ -0,0 +1,13 @@
|
||||
package hns_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
func TestHns(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Hns Suite")
|
||||
}
|
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")
|
||||
}
|
174
pkg/hns/netconf_windows.go
Normal file
174
pkg/hns/netconf_windows.go
Normal file
@ -0,0 +1,174 @@
|
||||
// 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"
|
||||
"github.com/Microsoft/hcsshim/hcn"
|
||||
"github.com/buger/jsonparser"
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// NetConf is the CNI spec
|
||||
type NetConf struct {
|
||||
types.NetConf
|
||||
HcnPolicyArgs []hcn.EndpointPolicy `json:"HcnPolicyArgs,omitempty"`
|
||||
Policies []policy `json:"policies,omitempty"`
|
||||
RuntimeConfig RuntimeConfig `json:"runtimeConfig"`
|
||||
}
|
||||
|
||||
type RuntimeDNS struct {
|
||||
Nameservers []string `json:"servers,omitempty"`
|
||||
Search []string `json:"searches,omitempty"`
|
||||
}
|
||||
|
||||
type RuntimeConfig struct {
|
||||
DNS RuntimeDNS `json:"dns"`
|
||||
}
|
||||
|
||||
type policy struct {
|
||||
Name string `json:"name"`
|
||||
Value json.RawMessage `json:"value"`
|
||||
}
|
||||
|
||||
// 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 + `"}`),
|
||||
})
|
||||
}
|
189
pkg/hns/netconf_windows_test.go
Normal file
189
pkg/hns/netconf_windows_test.go
Normal file
@ -0,0 +1,189 @@
|
||||
// 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("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")
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ import (
|
||||
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())
|
||||
|
@ -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,7 +70,8 @@ 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
|
||||
@ -89,13 +90,37 @@ func TeardownIPMasq(ipn *net.IPNet, chain string, comment string) error {
|
||||
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()
|
||||
}
|
||||
|
@ -23,6 +23,7 @@ import (
|
||||
|
||||
"github.com/containernetworking/plugins/pkg/ns"
|
||||
"github.com/containernetworking/plugins/pkg/utils/hwaddr"
|
||||
"github.com/safchain/ethtool"
|
||||
"github.com/vishvananda/netlink"
|
||||
)
|
||||
|
||||
@ -42,8 +43,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 {
|
||||
@ -226,3 +233,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,6 +25,7 @@ import (
|
||||
|
||||
"github.com/containernetworking/plugins/pkg/ip"
|
||||
"github.com/containernetworking/plugins/pkg/ns"
|
||||
"github.com/containernetworking/plugins/pkg/testutils"
|
||||
|
||||
"github.com/vishvananda/netlink"
|
||||
)
|
||||
@ -58,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)
|
||||
@ -91,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()
|
||||
|
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,14 +15,29 @@
|
||||
package ipam
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/containernetworking/cni/pkg/invoke"
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
"os"
|
||||
)
|
||||
|
||||
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)
|
||||
cmd := os.Getenv("CNI_COMMAND")
|
||||
if cmd == "" {
|
||||
return fmt.Errorf("environment variable CNI_COMMAND must be specified.")
|
||||
}
|
||||
// Set CNI_COMMAND to DEL explicity. We might be deleting due to an ADD gone wrong.
|
||||
// restore CNI_COMMAND to original value upon return.
|
||||
os.Setenv("CNI_COMMAND", "DEL")
|
||||
defer os.Setenv("CNI_COMMAND", cmd)
|
||||
return invoke.DelegateDel(context.TODO(), plugin, netconf, nil)
|
||||
}
|
||||
|
@ -21,10 +21,15 @@ import (
|
||||
|
||||
"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 {
|
||||
@ -42,6 +47,7 @@ func ConfigureIface(ifName string, res *current.Result) error {
|
||||
}
|
||||
|
||||
var v4gw, v6gw net.IP
|
||||
var has_enabled_ipv6 bool = false
|
||||
for _, ipc := range res.IPs {
|
||||
if ipc.Interface == nil {
|
||||
continue
|
||||
@ -52,6 +58,30 @@ func ConfigureIface(ifName string, res *current.Result) error {
|
||||
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)
|
||||
|
@ -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"
|
||||
|
||||
@ -48,7 +49,7 @@ var _ = Describe("ConfigureIface", 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
|
||||
|
@ -15,10 +15,8 @@
|
||||
package ns
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"runtime"
|
||||
"sync"
|
||||
"syscall"
|
||||
@ -38,82 +36,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
|
||||
@ -124,16 +46,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
|
||||
}
|
||||
|
||||
@ -180,9 +92,8 @@ type NetNS interface {
|
||||
}
|
||||
|
||||
type netNS struct {
|
||||
file *os.File
|
||||
mounted bool
|
||||
closed bool
|
||||
file *os.File
|
||||
closed bool
|
||||
}
|
||||
|
||||
// netNS implements the NetNS interface
|
||||
|
@ -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.
|
||||
@ -22,6 +22,7 @@ import (
|
||||
"path/filepath"
|
||||
|
||||
"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 +66,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,6 +112,7 @@ var _ = Describe("Linux namespace operations", func() {
|
||||
Expect(hostNSInode).To(Equal(origNSInode))
|
||||
return nil
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
return nil
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
@ -155,13 +160,14 @@ 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() {
|
||||
@ -188,7 +194,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 +209,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 +224,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)
|
||||
}
|
||||
|
@ -68,7 +68,7 @@ var _ = Describe("Echosvr", func() {
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
defer conn.Close()
|
||||
|
||||
fmt.Fprintf(conn, "hello")
|
||||
fmt.Fprintf(conn, "hello\n")
|
||||
Expect(ioutil.ReadAll(conn)).To(Equal([]byte("hello")))
|
||||
})
|
||||
})
|
||||
|
@ -9,5 +9,5 @@ import (
|
||||
|
||||
func TestEchosvr(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Testutils Echosvr Suite")
|
||||
RunSpecs(t, "pkg/testutils/echosvr")
|
||||
}
|
||||
|
@ -7,8 +7,13 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@ -31,8 +36,22 @@ func main() {
|
||||
}
|
||||
|
||||
func handleConnection(conn net.Conn) {
|
||||
buf := make([]byte, 512)
|
||||
nBytesRead, _ := conn.Read(buf)
|
||||
conn.Write(buf[0:nBytesRead])
|
||||
conn.Close()
|
||||
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
|
||||
}
|
||||
|
||||
}
|
||||
|
157
pkg/testutils/netns_linux.go
Normal file
157
pkg/testutils/netns_linux.go
Normal file
@ -0,0 +1,157 @@
|
||||
// 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"
|
||||
|
||||
"github.com/containernetworking/plugins/pkg/ns"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
const nsRunDir = "/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) {
|
||||
|
||||
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, nsRunDir) {
|
||||
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())
|
||||
}
|
26
pkg/utils/buildversion/buildversion.go
Normal file
26
pkg/utils/buildversion/buildversion.go
Normal file
@ -0,0 +1,26 @@
|
||||
// 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.
|
||||
|
||||
// Buildversion is a destination for the linker trickery so we can auto
|
||||
// set the build-version
|
||||
package buildversion
|
||||
|
||||
import "fmt"
|
||||
|
||||
// This is overridden in the linker script
|
||||
var BuildVersion = "version unknown"
|
||||
|
||||
func BuildString(pluginName string) string {
|
||||
return fmt.Sprintf("CNI %s plugin %s", pluginName, BuildVersion)
|
||||
}
|
@ -23,5 +23,5 @@ import (
|
||||
|
||||
func TestHwaddr(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Hwaddr Suite")
|
||||
RunSpecs(t, "pkg/utils/hwaddr")
|
||||
}
|
||||
|
@ -23,5 +23,5 @@ import (
|
||||
|
||||
func TestUtils(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Utils Suite")
|
||||
RunSpecs(t, "pkg/utils")
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ $ ./dhcp daemon
|
||||
|
||||
If given `-pidfile <path>` arguments after 'daemon', the dhcp plugin will write
|
||||
its PID to the given file.
|
||||
If given `-hostprefix <prefix>` arguments after 'daemon', the dhcp plugin will use this prefix for netns as `<prefix>/<original netns>`. It could be used in case of running dhcp daemon as container.
|
||||
If given `-hostprefix <prefix>` arguments after 'daemon', the dhcp plugin will use this prefix for DHCP socket as `<prefix>/run/cni/dhcp.sock`. You can use this prefix for references to the host filesystem, e.g. to access netns and the unix socket.
|
||||
|
||||
Alternatively, you can use systemd socket activation protocol.
|
||||
Be sure that the .socket file uses /run/cni/dhcp.sock as the socket path.
|
||||
|
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)
|
||||
}
|
@ -50,6 +50,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 {
|
||||
@ -58,7 +62,7 @@ func (d *DHCP) Allocate(args *skel.CmdArgs, result *current.Result) error {
|
||||
return fmt.Errorf("error parsing netconf: %v", err)
|
||||
}
|
||||
|
||||
clientID := args.ContainerID + "/" + conf.Name
|
||||
clientID := generateClientID(args.ContainerID, conf.Name, args.IfName)
|
||||
hostNetns := d.hostNetnsPrefix + args.Netns
|
||||
l, err := AcquireLease(clientID, hostNetns, args.IfName)
|
||||
if err != nil {
|
||||
@ -71,7 +75,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",
|
||||
@ -91,35 +95,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()
|
||||
d.clearLease(clientID)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@ -142,7 +157,7 @@ func getListener() (net.Listener, error) {
|
||||
}
|
||||
}
|
||||
|
||||
func runDaemon(pidfilePath string, hostPrefix 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()
|
||||
@ -157,7 +172,7 @@ func runDaemon(pidfilePath string, hostPrefix string) error {
|
||||
}
|
||||
}
|
||||
|
||||
l, err := getListener()
|
||||
l, err := getListener(hostPrefix + socketPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error getting listener: %v", err)
|
||||
}
|
||||
|
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())
|
||||
})
|
||||
})
|
27
plugins/ipam/dhcp/dhcp_suite_test.go
Normal file
27
plugins/ipam/dhcp/dhcp_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 main
|
||||
|
||||
import (
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"testing"
|
||||
)
|
||||
|
||||
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"
|
||||
@ -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")
|
||||
}
|
||||
|
||||
@ -340,7 +354,7 @@ func backoffRetry(f func() (*dhcp4.Packet, error)) (*dhcp4.Packet, error) {
|
||||
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,25 +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 netns")
|
||||
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, hostPrefix); 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"))
|
||||
}
|
||||
}
|
||||
|
||||
@ -67,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
|
@ -41,7 +41,7 @@ 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) {
|
||||
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
|
||||
}
|
||||
@ -83,7 +83,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 +110,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("ID", "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("ID", "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,7 +20,7 @@ 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 - IPAM plugins are passed the full configuration
|
||||
@ -97,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)
|
||||
}
|
||||
@ -122,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},
|
||||
@ -372,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
|
||||
}
|
||||
|
@ -26,6 +26,7 @@ import (
|
||||
)
|
||||
|
||||
const lastIPFilePrefix = "last_reserved_ip."
|
||||
const LineBreak = "\r\n"
|
||||
|
||||
var defaultDataDir = "/var/lib/cni/networks"
|
||||
|
||||
@ -55,7 +56,7 @@ func New(network, dataDir string) (*Store, error) {
|
||||
return &Store{lk, dir}, nil
|
||||
}
|
||||
|
||||
func (s *Store) Reserve(id string, ip net.IP, rangeID string) (bool, error) {
|
||||
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)
|
||||
@ -65,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
|
||||
@ -97,9 +98,9 @@ func (s *Store) Release(ip net.IP) error {
|
||||
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
|
||||
@ -108,13 +109,66 @@ func (s *Store) ReleaseByID(id string) error {
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if strings.TrimSpace(string(data)) == strings.TrimSpace(id) {
|
||||
if err := os.Remove(path); err != nil {
|
||||
return nil
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -23,5 +23,5 @@ import (
|
||||
|
||||
func TestLock(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Disk Suite")
|
||||
RunSpecs(t, "plugins/ipam/host-local/backend/disk")
|
||||
}
|
||||
|
@ -20,8 +20,8 @@ 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
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -23,5 +23,5 @@ import (
|
||||
|
||||
func TestHostLocal(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "HostLocal Suite")
|
||||
RunSpecs(t, "plugins/ipam/host-local")
|
||||
}
|
||||
|
@ -33,6 +33,8 @@ import (
|
||||
. "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"
|
||||
@ -75,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())
|
||||
@ -111,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, 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)
|
||||
@ -128,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())
|
||||
@ -139,6 +141,173 @@ var _ = Describe("host-local Operations", func() {
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
|
||||
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("Verify DEL works on backwards compatible allocate", func() {
|
||||
const nspath string = "/some/where"
|
||||
const ifname string = "eth0"
|
||||
|
||||
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.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",
|
||||
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())
|
||||
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"
|
||||
@ -167,7 +336,7 @@ var _ = Describe("host-local Operations", func() {
|
||||
}
|
||||
|
||||
// Release the IP
|
||||
err = testutils.CmdDelWithResult(nspath, ifname, func() error {
|
||||
err = testutils.CmdDelWithArgs(args, func() error {
|
||||
return cmdDel(args)
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
@ -205,7 +374,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())
|
||||
@ -223,7 +392,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)
|
||||
@ -233,7 +402,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())
|
||||
@ -270,7 +439,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())
|
||||
@ -281,10 +450,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())
|
||||
@ -321,7 +490,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())
|
||||
@ -363,7 +532,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())
|
||||
@ -412,7 +581,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())
|
||||
@ -444,7 +613,7 @@ var _ = Describe("host-local Operations", func() {
|
||||
"dataDir": "%s",
|
||||
"ranges": [
|
||||
[{"subnet":"172.16.1.0/24"}, { "subnet": "10.1.2.0/24" }],
|
||||
[{ "subnet": "2001:db8:1::/24" }]
|
||||
[{ "subnet": "2001:db8:1::/48" }]
|
||||
]
|
||||
},
|
||||
"args": {
|
||||
@ -462,7 +631,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())
|
||||
@ -509,7 +678,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())
|
||||
|
@ -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())
|
||||
}
|
||||
|
54
plugins/ipam/static/README.md
Normal file
54
plugins/ipam/static/README.md
Normal file
@ -0,0 +1,54 @@
|
||||
# static IP address management plugin
|
||||
|
||||
## Overview
|
||||
|
||||
static IPAM is very simple IPAM plugin that assigns IPv4 and IPv6 addresses statically to container. This will be useful in debugging purpose and in case of assign same IP address in different vlan/vxlan to containers.
|
||||
|
||||
|
||||
## Example configuration
|
||||
|
||||
```
|
||||
{
|
||||
"ipam": {
|
||||
"type": "static",
|
||||
"addresses": [
|
||||
{
|
||||
"address": "10.10.0.1/24",
|
||||
"gateway": "10.10.0.254"
|
||||
},
|
||||
{
|
||||
"address": "3ffe:ffff:0:01ff::1/64",
|
||||
"gateway": "3ffe:ffff:0::1"
|
||||
}
|
||||
],
|
||||
"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" }
|
||||
],
|
||||
"dns": {
|
||||
"nameservers" : ["8.8.8.8"],
|
||||
"domain": "example.com",
|
||||
"search": [ "example.com" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Network configuration reference
|
||||
|
||||
* `type` (string, required): "static"
|
||||
* `addresses` (array, optional): an array of ip address objects:
|
||||
* `address` (string, required): CIDR notation IP address.
|
||||
* `gateway` (string, optional): IP inside of "subnet" to designate as the gateway.
|
||||
* `routes` (string, optional): list of routes 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.
|
||||
* `dns` (string, optional): the dictionary with "nameservers", "domain" and "search".
|
||||
|
||||
## Supported arguments
|
||||
|
||||
The following [CNI_ARGS](https://github.com/containernetworking/cni/blob/master/SPEC.md#parameters) are supported:
|
||||
|
||||
* `IP`: request a specific CIDR notation IP addresses, comma separated
|
||||
* `GATEWAY`: request a specific gateway address
|
||||
|
||||
(example: CNI_ARGS="IP=10.10.0.1/24;GATEWAY=10.10.0.254")
|
247
plugins/ipam/static/main.go
Normal file
247
plugins/ipam/static/main.go
Normal file
@ -0,0 +1,247 @@
|
||||
// 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"`
|
||||
}
|
||||
|
||||
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 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
|
||||
}
|
||||
|
||||
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++
|
||||
}
|
||||
}
|
||||
|
||||
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}}
|
||||
if addr.Address.IP.To4() != nil {
|
||||
addr.Version = "4"
|
||||
numV4++
|
||||
} else {
|
||||
addr.Version = "6"
|
||||
numV6++
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
27
plugins/ipam/static/static_suite_test.go
Normal file
27
plugins/ipam/static/static_suite_test.go
Normal file
@ -0,0 +1,27 @@
|
||||
// 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_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
func TestStatic(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "plugins/ipam/static")
|
||||
}
|
278
plugins/ipam/static/static_test.go
Normal file
278
plugins/ipam/static/static_test.go
Normal file
@ -0,0 +1,278 @@
|
||||
// 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 (
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/skel"
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
"github.com/containernetworking/cni/pkg/types/current"
|
||||
"github.com/containernetworking/plugins/pkg/testutils"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("static Operations", func() {
|
||||
It("allocates and releases addresses with ADD/DEL", func() {
|
||||
const ifname string = "eth0"
|
||||
const nspath string = "/some/where"
|
||||
|
||||
conf := `{
|
||||
"cniVersion": "0.3.1",
|
||||
"name": "mynet",
|
||||
"type": "ipvlan",
|
||||
"master": "foo0",
|
||||
"ipam": {
|
||||
"type": "static",
|
||||
"addresses": [ {
|
||||
"address": "10.10.0.1/24",
|
||||
"gateway": "10.10.0.254"
|
||||
},
|
||||
{
|
||||
"address": "3ffe:ffff:0:01ff::1/64",
|
||||
"gateway": "3ffe:ffff:0::1"
|
||||
}],
|
||||
"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" }],
|
||||
"dns": {
|
||||
"nameservers" : ["8.8.8.8"],
|
||||
"domain": "example.com",
|
||||
"search": [ "example.com" ]
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
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())
|
||||
Expect(strings.Index(string(raw), "\"version\":")).Should(BeNumerically(">", 0))
|
||||
|
||||
result, err := current.GetResult(r)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Gomega is cranky about slices with different caps
|
||||
Expect(*result.IPs[0]).To(Equal(
|
||||
current.IPConfig{
|
||||
Version: "4",
|
||||
Address: mustCIDR("10.10.0.1/24"),
|
||||
Gateway: net.ParseIP("10.10.0.254"),
|
||||
}))
|
||||
|
||||
Expect(*result.IPs[1]).To(Equal(
|
||||
current.IPConfig{
|
||||
Version: "6",
|
||||
Address: mustCIDR("3ffe:ffff:0:01ff::1/64"),
|
||||
Gateway: net.ParseIP("3ffe:ffff:0::1"),
|
||||
},
|
||||
))
|
||||
Expect(len(result.IPs)).To(Equal(2))
|
||||
|
||||
Expect(result.Routes).To(Equal([]*types.Route{
|
||||
{Dst: mustCIDR("0.0.0.0/0")},
|
||||
{Dst: mustCIDR("192.168.0.0/16"), GW: net.ParseIP("10.10.5.1")},
|
||||
{Dst: mustCIDR("3ffe:ffff:0:01ff::1/64")},
|
||||
}))
|
||||
|
||||
// Release the IP
|
||||
err = testutils.CmdDelWithArgs(args, func() error {
|
||||
return cmdDel(args)
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
|
||||
It("doesn't error when passed an unknown ID on DEL", func() {
|
||||
const ifname string = "eth0"
|
||||
const nspath string = "/some/where"
|
||||
|
||||
conf := `{
|
||||
"cniVersion": "0.3.0",
|
||||
"name": "mynet",
|
||||
"type": "ipvlan",
|
||||
"master": "foo0",
|
||||
"ipam": {
|
||||
"type": "static",
|
||||
"addresses": [ {
|
||||
"address": "10.10.0.1/24",
|
||||
"gateway": "10.10.0.254"
|
||||
},
|
||||
{
|
||||
"address": "3ffe:ffff:0:01ff::1/64",
|
||||
"gateway": "3ffe:ffff:0::1"
|
||||
}],
|
||||
"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" }],
|
||||
"dns": {
|
||||
"nameservers" : ["8.8.8.8"],
|
||||
"domain": "example.com",
|
||||
"search": [ "example.com" ]
|
||||
}}}`
|
||||
|
||||
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 addresses with ADD/DEL, with ENV variables", func() {
|
||||
const ifname string = "eth0"
|
||||
const nspath string = "/some/where"
|
||||
|
||||
conf := `{
|
||||
"cniVersion": "0.3.1",
|
||||
"name": "mynet",
|
||||
"type": "ipvlan",
|
||||
"master": "foo0",
|
||||
"ipam": {
|
||||
"type": "static",
|
||||
"routes": [
|
||||
{ "dst": "0.0.0.0/0" },
|
||||
{ "dst": "192.168.0.0/16", "gw": "10.10.5.1" }],
|
||||
"dns": {
|
||||
"nameservers" : ["8.8.8.8"],
|
||||
"domain": "example.com",
|
||||
"search": [ "example.com" ]
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
args := &skel.CmdArgs{
|
||||
ContainerID: "dummy",
|
||||
Netns: nspath,
|
||||
IfName: ifname,
|
||||
StdinData: []byte(conf),
|
||||
Args: "IP=10.10.0.1/24;GATEWAY=10.10.0.254",
|
||||
}
|
||||
|
||||
// Allocate the IP
|
||||
r, raw, err := testutils.CmdAddWithArgs(args, func() error {
|
||||
return cmdAdd(args)
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(strings.Index(string(raw), "\"version\":")).Should(BeNumerically(">", 0))
|
||||
|
||||
result, err := current.GetResult(r)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Gomega is cranky about slices with different caps
|
||||
Expect(*result.IPs[0]).To(Equal(
|
||||
current.IPConfig{
|
||||
Version: "4",
|
||||
Address: mustCIDR("10.10.0.1/24"),
|
||||
Gateway: net.ParseIP("10.10.0.254"),
|
||||
}))
|
||||
|
||||
Expect(len(result.IPs)).To(Equal(1))
|
||||
|
||||
Expect(result.Routes).To(Equal([]*types.Route{
|
||||
{Dst: mustCIDR("0.0.0.0/0")},
|
||||
{Dst: mustCIDR("192.168.0.0/16"), GW: net.ParseIP("10.10.5.1")},
|
||||
}))
|
||||
|
||||
// Release the IP
|
||||
err = testutils.CmdDelWithArgs(args, func() error {
|
||||
return cmdDel(args)
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
|
||||
It("allocates and releases multiple addresses with ADD/DEL, with ENV variables", func() {
|
||||
const ifname string = "eth0"
|
||||
const nspath string = "/some/where"
|
||||
|
||||
conf := `{
|
||||
"cniVersion": "0.3.1",
|
||||
"name": "mynet",
|
||||
"type": "ipvlan",
|
||||
"master": "foo0",
|
||||
"ipam": {
|
||||
"type": "static"
|
||||
}
|
||||
}`
|
||||
|
||||
args := &skel.CmdArgs{
|
||||
ContainerID: "dummy",
|
||||
Netns: nspath,
|
||||
IfName: ifname,
|
||||
StdinData: []byte(conf),
|
||||
Args: "IP=10.10.0.1/24,11.11.0.1/24;GATEWAY=10.10.0.254",
|
||||
}
|
||||
|
||||
// Allocate the IP
|
||||
r, raw, err := testutils.CmdAddWithArgs(args, func() error {
|
||||
return cmdAdd(args)
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(strings.Index(string(raw), "\"version\":")).Should(BeNumerically(">", 0))
|
||||
|
||||
result, err := current.GetResult(r)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Gomega is cranky about slices with different caps
|
||||
Expect(*result.IPs[0]).To(Equal(
|
||||
current.IPConfig{
|
||||
Version: "4",
|
||||
Address: mustCIDR("10.10.0.1/24"),
|
||||
Gateway: net.ParseIP("10.10.0.254"),
|
||||
}))
|
||||
Expect(*result.IPs[1]).To(Equal(
|
||||
current.IPConfig{
|
||||
Version: "4",
|
||||
Address: mustCIDR("11.11.0.1/24"),
|
||||
Gateway: nil,
|
||||
}))
|
||||
|
||||
Expect(len(result.IPs)).To(Equal(2))
|
||||
|
||||
// Release the IP
|
||||
err = testutils.CmdDelWithArgs(args, func() error {
|
||||
return cmdDel(args)
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
})
|
||||
|
||||
func mustCIDR(s string) net.IPNet {
|
||||
ip, n, err := net.ParseCIDR(s)
|
||||
n.IP = ip
|
||||
if err != nil {
|
||||
Fail(err.Error())
|
||||
}
|
||||
|
||||
return *n
|
||||
}
|
@ -8,3 +8,5 @@ plugins/main/ptp
|
||||
plugins/main/vlan
|
||||
plugins/meta/portmap
|
||||
plugins/meta/tuning
|
||||
plugins/meta/bandwidth
|
||||
plugins/meta/firewall
|
||||
|
@ -28,6 +28,17 @@ If the bridge is missing, the plugin will create one on first use and, if gatewa
|
||||
}
|
||||
```
|
||||
|
||||
## Example L2-only configuration
|
||||
```
|
||||
{
|
||||
"cniVersion": "0.3.1",
|
||||
"name": "mynet",
|
||||
"type": "bridge",
|
||||
"bridge": "mynet0",
|
||||
"ipam": {}
|
||||
}
|
||||
```
|
||||
|
||||
## Network configuration reference
|
||||
|
||||
* `name` (string, required): the name of the network.
|
||||
@ -39,5 +50,10 @@ If the bridge is missing, the plugin will create one on first use and, if gatewa
|
||||
* `ipMasq` (boolean, optional): set up IP Masquerade on the host for traffic originating from this network and destined outside of it. Defaults to false.
|
||||
* `mtu` (integer, optional): explicitly set MTU to the specified value. Defaults to the value chosen by the kernel.
|
||||
* `hairpinMode` (boolean, optional): set hairpin mode for interfaces on the bridge. Defaults to false.
|
||||
* `ipam` (dictionary, required): IPAM configuration to be used for this network.
|
||||
* `ipam` (dictionary, required): IPAM configuration to be used for this network. For L2-only network, create empty dictionary.
|
||||
* `promiscMode` (boolean, optional): set promiscuous mode on the bridge. Defaults to false.
|
||||
* `vlan` (int, optional): assign VLAN tag. Defaults to none.
|
||||
|
||||
*Note:* The VLAN parameter configures the VLAN tag on the host end of the veth and also enables the vlan_filtering feature on the bridge interface.
|
||||
|
||||
*Note:* To configure uplink for L2 network you need to allow the vlan on the uplink interface by using the following command ``` bridge vlan add vid VLAN_ID dev DEV```.
|
@ -18,11 +18,13 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"runtime"
|
||||
"syscall"
|
||||
|
||||
"io/ioutil"
|
||||
"github.com/j-keck/arping"
|
||||
"github.com/vishvananda/netlink"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/skel"
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
@ -32,10 +34,12 @@ import (
|
||||
"github.com/containernetworking/plugins/pkg/ipam"
|
||||
"github.com/containernetworking/plugins/pkg/ns"
|
||||
"github.com/containernetworking/plugins/pkg/utils"
|
||||
"github.com/j-keck/arping"
|
||||
"github.com/vishvananda/netlink"
|
||||
bv "github.com/containernetworking/plugins/pkg/utils/buildversion"
|
||||
)
|
||||
|
||||
// For testcases to force an error after IPAM has been performed
|
||||
var debugPostIPAMError error
|
||||
|
||||
const defaultBrName = "cni0"
|
||||
|
||||
type NetConf struct {
|
||||
@ -48,6 +52,7 @@ type NetConf struct {
|
||||
MTU int `json:"mtu"`
|
||||
HairpinMode bool `json:"hairpinMode"`
|
||||
PromiscMode bool `json:"promiscMode"`
|
||||
Vlan int `json:"vlan"`
|
||||
}
|
||||
|
||||
type gwInfo struct {
|
||||
@ -140,7 +145,7 @@ func calcGateways(result *current.Result, n *NetConf) (*gwInfo, *gwInfo, error)
|
||||
return gwsV4, gwsV6, nil
|
||||
}
|
||||
|
||||
func ensureBridgeAddr(br *netlink.Bridge, family int, ipn *net.IPNet, forceAddress bool) error {
|
||||
func ensureAddr(br netlink.Link, family int, ipn *net.IPNet, forceAddress bool) error {
|
||||
addrs, err := netlink.AddrList(br, family)
|
||||
if err != nil && err != syscall.ENOENT {
|
||||
return fmt.Errorf("could not get list of IP addresses: %v", err)
|
||||
@ -160,34 +165,34 @@ func ensureBridgeAddr(br *netlink.Bridge, family int, ipn *net.IPNet, forceAddre
|
||||
// forceAddress is true, otherwise throw an error.
|
||||
if family == netlink.FAMILY_V4 || a.IPNet.Contains(ipn.IP) || ipn.Contains(a.IPNet.IP) {
|
||||
if forceAddress {
|
||||
if err = deleteBridgeAddr(br, a.IPNet); err != nil {
|
||||
if err = deleteAddr(br, a.IPNet); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("%q already has an IP address different from %v", br.Name, ipnStr)
|
||||
return fmt.Errorf("%q already has an IP address different from %v", br.Attrs().Name, ipnStr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addr := &netlink.Addr{IPNet: ipn, Label: ""}
|
||||
if err := netlink.AddrAdd(br, addr); err != nil {
|
||||
return fmt.Errorf("could not add IP address to %q: %v", br.Name, err)
|
||||
return fmt.Errorf("could not add IP address to %q: %v", br.Attrs().Name, err)
|
||||
}
|
||||
|
||||
// Set the bridge's MAC to itself. Otherwise, the bridge will take the
|
||||
// lowest-numbered mac on the bridge, and will change as ifs churn
|
||||
if err := netlink.LinkSetHardwareAddr(br, br.HardwareAddr); err != nil {
|
||||
if err := netlink.LinkSetHardwareAddr(br, br.Attrs().HardwareAddr); err != nil {
|
||||
return fmt.Errorf("could not set bridge's mac: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func deleteBridgeAddr(br *netlink.Bridge, ipn *net.IPNet) error {
|
||||
func deleteAddr(br netlink.Link, ipn *net.IPNet) error {
|
||||
addr := &netlink.Addr{IPNet: ipn, Label: ""}
|
||||
|
||||
if err := netlink.AddrDel(br, addr); err != nil {
|
||||
return fmt.Errorf("could not remove IP address from %q: %v", br.Name, err)
|
||||
return fmt.Errorf("could not remove IP address from %q: %v", br.Attrs().Name, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
@ -205,7 +210,7 @@ func bridgeByName(name string) (*netlink.Bridge, error) {
|
||||
return br, nil
|
||||
}
|
||||
|
||||
func ensureBridge(brName string, mtu int, promiscMode bool) (*netlink.Bridge, error) {
|
||||
func ensureBridge(brName string, mtu int, promiscMode, vlanFiltering bool) (*netlink.Bridge, error) {
|
||||
br := &netlink.Bridge{
|
||||
LinkAttrs: netlink.LinkAttrs{
|
||||
Name: brName,
|
||||
@ -216,6 +221,7 @@ func ensureBridge(brName string, mtu int, promiscMode bool) (*netlink.Bridge, er
|
||||
// default packet limit
|
||||
TxQLen: -1,
|
||||
},
|
||||
VlanFiltering: &vlanFiltering,
|
||||
}
|
||||
|
||||
err := netlink.LinkAdd(br)
|
||||
@ -243,7 +249,35 @@ func ensureBridge(brName string, mtu int, promiscMode bool) (*netlink.Bridge, er
|
||||
return br, nil
|
||||
}
|
||||
|
||||
func setupVeth(netns ns.NetNS, br *netlink.Bridge, ifName string, mtu int, hairpinMode bool) (*current.Interface, *current.Interface, error) {
|
||||
func ensureVlanInterface(br *netlink.Bridge, vlanId int) (netlink.Link, error) {
|
||||
name := fmt.Sprintf("%s.%d", br.Name, vlanId)
|
||||
|
||||
brGatewayVeth, err := netlink.LinkByName(name)
|
||||
if err != nil {
|
||||
if err.Error() != "Link not found" {
|
||||
return nil, fmt.Errorf("failed to find interface %q: %v", name, err)
|
||||
}
|
||||
|
||||
hostNS, err := ns.GetCurrentNS()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("faild to find host namespace: %v", err)
|
||||
}
|
||||
|
||||
_, brGatewayIface, err := setupVeth(hostNS, br, name, br.MTU, false, vlanId)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("faild to create vlan gateway %q: %v", name, err)
|
||||
}
|
||||
|
||||
brGatewayVeth, err = netlink.LinkByName(brGatewayIface.Name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to lookup %q: %v", brGatewayIface.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
return brGatewayVeth, nil
|
||||
}
|
||||
|
||||
func setupVeth(netns ns.NetNS, br *netlink.Bridge, ifName string, mtu int, hairpinMode bool, vlanID int) (*current.Interface, *current.Interface, error) {
|
||||
contIface := ¤t.Interface{}
|
||||
hostIface := ¤t.Interface{}
|
||||
|
||||
@ -280,6 +314,13 @@ func setupVeth(netns ns.NetNS, br *netlink.Bridge, ifName string, mtu int, hairp
|
||||
return nil, nil, fmt.Errorf("failed to setup hairpin mode for %v: %v", hostVeth.Attrs().Name, err)
|
||||
}
|
||||
|
||||
if vlanID != 0 {
|
||||
err = netlink.BridgeVlanAdd(hostVeth, uint16(vlanID), true, true, false, true)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to setup vlan tag on interface %q: %v", hostIface.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
return hostIface, contIface, nil
|
||||
}
|
||||
|
||||
@ -289,8 +330,12 @@ func calcGatewayIP(ipn *net.IPNet) net.IP {
|
||||
}
|
||||
|
||||
func setupBridge(n *NetConf) (*netlink.Bridge, *current.Interface, error) {
|
||||
vlanFiltering := false
|
||||
if n.Vlan != 0 {
|
||||
vlanFiltering = true
|
||||
}
|
||||
// create bridge if necessary
|
||||
br, err := ensureBridge(n.BrName, n.MTU, n.PromiscMode)
|
||||
br, err := ensureBridge(n.BrName, n.MTU, n.PromiscMode, vlanFiltering)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to create bridge %q: %v", n.BrName, err)
|
||||
}
|
||||
@ -323,11 +368,15 @@ func enableIPForward(family int) error {
|
||||
}
|
||||
|
||||
func cmdAdd(args *skel.CmdArgs) error {
|
||||
var success bool = false
|
||||
|
||||
n, cniVersion, err := loadNetConf(args.StdinData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
isLayer3 := n.IPAM.Type != ""
|
||||
|
||||
if n.IsDefaultGW {
|
||||
n.IsGW = true
|
||||
}
|
||||
@ -347,100 +396,131 @@ func cmdAdd(args *skel.CmdArgs) error {
|
||||
}
|
||||
defer netns.Close()
|
||||
|
||||
hostInterface, containerInterface, err := setupVeth(netns, br, args.IfName, n.MTU, n.HairpinMode)
|
||||
hostInterface, containerInterface, err := setupVeth(netns, br, args.IfName, n.MTU, n.HairpinMode, n.Vlan)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// run the IPAM plugin and get back the config to apply
|
||||
r, err := ipam.ExecAdd(n.IPAM.Type, args.StdinData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Assume L2 interface only
|
||||
result := ¤t.Result{CNIVersion: cniVersion, Interfaces: []*current.Interface{brInterface, hostInterface, containerInterface}}
|
||||
|
||||
// Convert whatever the IPAM result was into the current Result type
|
||||
result, err := current.NewResultFromResult(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(result.IPs) == 0 {
|
||||
return errors.New("IPAM plugin returned missing IP config")
|
||||
}
|
||||
|
||||
result.Interfaces = []*current.Interface{brInterface, hostInterface, containerInterface}
|
||||
|
||||
// Gather gateway information for each IP family
|
||||
gwsV4, gwsV6, err := calcGateways(result, n)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Configure the container hardware address and IP address(es)
|
||||
if err := netns.Do(func(_ ns.NetNS) error {
|
||||
contVeth, err := net.InterfaceByName(args.IfName)
|
||||
if isLayer3 {
|
||||
// run the IPAM plugin and get back the config to apply
|
||||
r, err := ipam.ExecAdd(n.IPAM.Type, args.StdinData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Disable IPv6 DAD just in case hairpin mode is enabled on the
|
||||
// bridge. Hairpin mode causes echos of neighbor solicitation
|
||||
// packets, which causes DAD failures.
|
||||
for _, ipc := range result.IPs {
|
||||
if ipc.Version == "6" && (n.HairpinMode || n.PromiscMode) {
|
||||
if err := disableIPV6DAD(args.IfName); err != nil {
|
||||
return err
|
||||
}
|
||||
break
|
||||
// release IP in case of failure
|
||||
defer func() {
|
||||
if !success {
|
||||
ipam.ExecDel(n.IPAM.Type, args.StdinData)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Add the IP to the interface
|
||||
if err := ipam.ConfigureIface(args.IfName, result); err != nil {
|
||||
// Convert whatever the IPAM result was into the current Result type
|
||||
ipamResult, err := current.NewResultFromResult(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Send a gratuitous arp
|
||||
for _, ipc := range result.IPs {
|
||||
if ipc.Version == "4" {
|
||||
_ = arping.GratuitousArpOverIface(ipc.Address.IP, *contVeth)
|
||||
}
|
||||
result.IPs = ipamResult.IPs
|
||||
result.Routes = ipamResult.Routes
|
||||
|
||||
if len(result.IPs) == 0 {
|
||||
return errors.New("IPAM plugin returned missing IP config")
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if n.IsGW {
|
||||
var firstV4Addr net.IP
|
||||
// Set the IP address(es) on the bridge and enable forwarding
|
||||
for _, gws := range []*gwInfo{gwsV4, gwsV6} {
|
||||
for _, gw := range gws.gws {
|
||||
if gw.IP.To4() != nil && firstV4Addr == nil {
|
||||
firstV4Addr = gw.IP
|
||||
}
|
||||
|
||||
err = ensureBridgeAddr(br, gws.family, &gw, n.ForceAddress)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set bridge addr: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if gws.gws != nil {
|
||||
if err = enableIPForward(gws.family); err != nil {
|
||||
return fmt.Errorf("failed to enable forwarding: %v", err)
|
||||
}
|
||||
}
|
||||
// Gather gateway information for each IP family
|
||||
gwsV4, gwsV6, err := calcGateways(result, n)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if n.IPMasq {
|
||||
chain := utils.FormatChainName(n.Name, args.ContainerID)
|
||||
comment := utils.FormatComment(n.Name, args.ContainerID)
|
||||
for _, ipc := range result.IPs {
|
||||
if err = ip.SetupIPMasq(ip.Network(&ipc.Address), chain, comment); err != nil {
|
||||
// Configure the container hardware address and IP address(es)
|
||||
if err := netns.Do(func(_ ns.NetNS) error {
|
||||
contVeth, err := net.InterfaceByName(args.IfName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Disable IPv6 DAD just in case hairpin mode is enabled on the
|
||||
// bridge. Hairpin mode causes echos of neighbor solicitation
|
||||
// packets, which causes DAD failures.
|
||||
for _, ipc := range result.IPs {
|
||||
if ipc.Version == "6" && (n.HairpinMode || n.PromiscMode) {
|
||||
if err := disableIPV6DAD(args.IfName); err != nil {
|
||||
return err
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Add the IP to the interface
|
||||
if err := ipam.ConfigureIface(args.IfName, result); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Send a gratuitous arp
|
||||
for _, ipc := range result.IPs {
|
||||
if ipc.Version == "4" {
|
||||
_ = arping.GratuitousArpOverIface(ipc.Address.IP, *contVeth)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if n.IsGW {
|
||||
var firstV4Addr net.IP
|
||||
var vlanInterface *current.Interface
|
||||
// Set the IP address(es) on the bridge and enable forwarding
|
||||
for _, gws := range []*gwInfo{gwsV4, gwsV6} {
|
||||
for _, gw := range gws.gws {
|
||||
if gw.IP.To4() != nil && firstV4Addr == nil {
|
||||
firstV4Addr = gw.IP
|
||||
}
|
||||
if n.Vlan != 0 {
|
||||
vlanIface, err := ensureVlanInterface(br, n.Vlan)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create vlan interface: %v", err)
|
||||
}
|
||||
|
||||
if vlanInterface == nil {
|
||||
vlanInterface = ¤t.Interface{Name: vlanIface.Attrs().Name,
|
||||
Mac: vlanIface.Attrs().HardwareAddr.String()}
|
||||
result.Interfaces = append(result.Interfaces, vlanInterface)
|
||||
}
|
||||
|
||||
err = ensureAddr(vlanIface, gws.family, &gw, n.ForceAddress)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set vlan interface for bridge with addr: %v", err)
|
||||
}
|
||||
} else {
|
||||
err = ensureAddr(br, gws.family, &gw, n.ForceAddress)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set bridge addr: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if gws.gws != nil {
|
||||
if err = enableIPForward(gws.family); err != nil {
|
||||
return fmt.Errorf("failed to enable forwarding: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if n.IPMasq {
|
||||
chain := utils.FormatChainName(n.Name, args.ContainerID)
|
||||
comment := utils.FormatComment(n.Name, args.ContainerID)
|
||||
for _, ipc := range result.IPs {
|
||||
if err = ip.SetupIPMasq(ip.Network(&ipc.Address), chain, comment); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -454,6 +534,13 @@ func cmdAdd(args *skel.CmdArgs) error {
|
||||
|
||||
result.DNS = n.DNS
|
||||
|
||||
// Return an error requested by testcases, if any
|
||||
if debugPostIPAMError != nil {
|
||||
return debugPostIPAMError
|
||||
}
|
||||
|
||||
success = true
|
||||
|
||||
return types.PrintResult(result, cniVersion)
|
||||
}
|
||||
|
||||
@ -463,8 +550,12 @@ func cmdDel(args *skel.CmdArgs) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := ipam.ExecDel(n.IPAM.Type, args.StdinData); err != nil {
|
||||
return err
|
||||
isLayer3 := n.IPAM.Type != ""
|
||||
|
||||
if isLayer3 {
|
||||
if err := ipam.ExecDel(n.IPAM.Type, args.StdinData); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if args.Netns == "" {
|
||||
@ -488,7 +579,7 @@ func cmdDel(args *skel.CmdArgs) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if n.IPMasq {
|
||||
if isLayer3 && n.IPMasq {
|
||||
chain := utils.FormatChainName(n.Name, args.ContainerID)
|
||||
comment := utils.FormatComment(n.Name, args.ContainerID)
|
||||
for _, ipn := range ipnets {
|
||||
@ -502,5 +593,269 @@ func cmdDel(args *skel.CmdArgs) error {
|
||||
}
|
||||
|
||||
func main() {
|
||||
skel.PluginMain(cmdAdd, cmdDel, version.All)
|
||||
skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.All, bv.BuildString("bridge"))
|
||||
}
|
||||
|
||||
type cniBridgeIf struct {
|
||||
Name string
|
||||
ifIndex int
|
||||
peerIndex int
|
||||
masterIndex int
|
||||
found bool
|
||||
}
|
||||
|
||||
func validateInterface(intf current.Interface, expectInSb bool) (cniBridgeIf, netlink.Link, error) {
|
||||
|
||||
ifFound := cniBridgeIf{found: false}
|
||||
if intf.Name == "" {
|
||||
return ifFound, nil, fmt.Errorf("Interface name missing ")
|
||||
}
|
||||
|
||||
link, err := netlink.LinkByName(intf.Name)
|
||||
if err != nil {
|
||||
return ifFound, nil, fmt.Errorf("Interface name %s not found", intf.Name)
|
||||
}
|
||||
|
||||
if expectInSb {
|
||||
if intf.Sandbox == "" {
|
||||
return ifFound, nil, fmt.Errorf("Interface %s is expected to be in a sandbox", intf.Name)
|
||||
}
|
||||
} else {
|
||||
if intf.Sandbox != "" {
|
||||
return ifFound, nil, fmt.Errorf("Interface %s should not be in sandbox", intf.Name)
|
||||
}
|
||||
}
|
||||
|
||||
return ifFound, link, err
|
||||
}
|
||||
|
||||
func validateCniBrInterface(intf current.Interface, n *NetConf) (cniBridgeIf, error) {
|
||||
|
||||
brFound, link, err := validateInterface(intf, false)
|
||||
if err != nil {
|
||||
return brFound, err
|
||||
}
|
||||
|
||||
_, isBridge := link.(*netlink.Bridge)
|
||||
if !isBridge {
|
||||
return brFound, fmt.Errorf("Interface %s does not have link type of bridge", intf.Name)
|
||||
}
|
||||
|
||||
if intf.Mac != "" {
|
||||
if intf.Mac != link.Attrs().HardwareAddr.String() {
|
||||
return brFound, fmt.Errorf("Bridge interface %s Mac doesn't match: %s", intf.Name, intf.Mac)
|
||||
}
|
||||
}
|
||||
|
||||
linkPromisc := link.Attrs().Promisc != 0
|
||||
if linkPromisc != n.PromiscMode {
|
||||
return brFound, fmt.Errorf("Bridge interface %s configured Promisc Mode %v doesn't match current state: %v ",
|
||||
intf.Name, n.PromiscMode, linkPromisc)
|
||||
}
|
||||
|
||||
brFound.found = true
|
||||
brFound.Name = link.Attrs().Name
|
||||
brFound.ifIndex = link.Attrs().Index
|
||||
brFound.masterIndex = link.Attrs().MasterIndex
|
||||
|
||||
return brFound, nil
|
||||
}
|
||||
|
||||
func validateCniVethInterface(intf *current.Interface, brIf cniBridgeIf, contIf cniBridgeIf) (cniBridgeIf, error) {
|
||||
|
||||
vethFound, link, err := validateInterface(*intf, false)
|
||||
if err != nil {
|
||||
return vethFound, err
|
||||
}
|
||||
|
||||
_, isVeth := link.(*netlink.Veth)
|
||||
if !isVeth {
|
||||
// just skip it, it's not what CNI created
|
||||
return vethFound, nil
|
||||
}
|
||||
|
||||
_, vethFound.peerIndex, err = ip.GetVethPeerIfindex(link.Attrs().Name)
|
||||
if err != nil {
|
||||
return vethFound, fmt.Errorf("Unable to obtain veth peer index for veth %s", link.Attrs().Name)
|
||||
}
|
||||
vethFound.ifIndex = link.Attrs().Index
|
||||
vethFound.masterIndex = link.Attrs().MasterIndex
|
||||
|
||||
if vethFound.ifIndex != contIf.peerIndex {
|
||||
return vethFound, nil
|
||||
}
|
||||
|
||||
if contIf.ifIndex != vethFound.peerIndex {
|
||||
return vethFound, nil
|
||||
}
|
||||
|
||||
if vethFound.masterIndex != brIf.ifIndex {
|
||||
return vethFound, nil
|
||||
}
|
||||
|
||||
if intf.Mac != "" {
|
||||
if intf.Mac != link.Attrs().HardwareAddr.String() {
|
||||
return vethFound, fmt.Errorf("Interface %s Mac doesn't match: %s not found", intf.Name, intf.Mac)
|
||||
}
|
||||
}
|
||||
|
||||
vethFound.found = true
|
||||
vethFound.Name = link.Attrs().Name
|
||||
|
||||
return vethFound, nil
|
||||
}
|
||||
|
||||
func validateCniContainerInterface(intf current.Interface) (cniBridgeIf, error) {
|
||||
|
||||
vethFound, link, err := validateInterface(intf, true)
|
||||
if err != nil {
|
||||
return vethFound, err
|
||||
}
|
||||
|
||||
_, isVeth := link.(*netlink.Veth)
|
||||
if !isVeth {
|
||||
return vethFound, fmt.Errorf("Error: Container interface %s not of type veth", link.Attrs().Name)
|
||||
}
|
||||
_, vethFound.peerIndex, err = ip.GetVethPeerIfindex(link.Attrs().Name)
|
||||
if err != nil {
|
||||
return vethFound, fmt.Errorf("Unable to obtain veth peer index for veth %s", link.Attrs().Name)
|
||||
}
|
||||
vethFound.ifIndex = link.Attrs().Index
|
||||
|
||||
if intf.Mac != "" {
|
||||
if intf.Mac != link.Attrs().HardwareAddr.String() {
|
||||
return vethFound, fmt.Errorf("Interface %s Mac %s doesn't match container Mac: %s", intf.Name, intf.Mac, link.Attrs().HardwareAddr)
|
||||
}
|
||||
}
|
||||
|
||||
vethFound.found = true
|
||||
vethFound.Name = link.Attrs().Name
|
||||
|
||||
return vethFound, nil
|
||||
}
|
||||
|
||||
func cmdCheck(args *skel.CmdArgs) error {
|
||||
|
||||
n, _, err := loadNetConf(args.StdinData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
netns, err := ns.GetNS(args.Netns)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open netns %q: %v", args.Netns, err)
|
||||
}
|
||||
defer netns.Close()
|
||||
|
||||
// run the IPAM plugin and get back the config to apply
|
||||
err = ipam.ExecCheck(n.IPAM.Type, args.StdinData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Parse previous result.
|
||||
if n.NetConf.RawPrevResult == nil {
|
||||
return fmt.Errorf("Required prevResult missing")
|
||||
}
|
||||
|
||||
if err := version.ParsePrevResult(&n.NetConf); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result, err := current.NewResultFromResult(n.PrevResult)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var errLink error
|
||||
var contCNI, vethCNI cniBridgeIf
|
||||
var brMap, contMap current.Interface
|
||||
|
||||
// Find interfaces for names whe know, CNI Bridge and container
|
||||
for _, intf := range result.Interfaces {
|
||||
if n.BrName == intf.Name {
|
||||
brMap = *intf
|
||||
continue
|
||||
} else if args.IfName == intf.Name {
|
||||
if args.Netns == intf.Sandbox {
|
||||
contMap = *intf
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
brCNI, err := validateCniBrInterface(brMap, n)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// The namespace must be the same as what was configured
|
||||
if args.Netns != contMap.Sandbox {
|
||||
return fmt.Errorf("Sandbox in prevResult %s doesn't match configured netns: %s",
|
||||
contMap.Sandbox, args.Netns)
|
||||
}
|
||||
|
||||
// Check interface against values found in the container
|
||||
if err := netns.Do(func(_ ns.NetNS) error {
|
||||
contCNI, errLink = validateCniContainerInterface(contMap)
|
||||
if errLink != nil {
|
||||
return errLink
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Now look for veth that is peer with container interface.
|
||||
// Anything else wasn't created by CNI, skip it
|
||||
for _, intf := range result.Interfaces {
|
||||
// Skip this result if name is the same as cni bridge
|
||||
// It's either the cni bridge we dealt with above, or something with the
|
||||
// same name in a different namespace. We just skip since it's not ours
|
||||
if brMap.Name == intf.Name {
|
||||
continue
|
||||
}
|
||||
|
||||
// same here for container name
|
||||
if contMap.Name == intf.Name {
|
||||
continue
|
||||
}
|
||||
|
||||
vethCNI, errLink = validateCniVethInterface(intf, brCNI, contCNI)
|
||||
if errLink != nil {
|
||||
return errLink
|
||||
}
|
||||
|
||||
if vethCNI.found {
|
||||
// veth with container interface as peer and bridge as master found
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !brCNI.found {
|
||||
return fmt.Errorf("CNI created bridge %s in host namespace was not found", n.BrName)
|
||||
}
|
||||
if !contCNI.found {
|
||||
return fmt.Errorf("CNI created interface in container %s not found", args.IfName)
|
||||
}
|
||||
if !vethCNI.found {
|
||||
return fmt.Errorf("CNI veth created for bridge %s was not found", n.BrName)
|
||||
}
|
||||
|
||||
// Check prevResults for ips, routes and dns against values found in the container
|
||||
if err := netns.Do(func(_ ns.NetNS) error {
|
||||
err = ip.ValidateExpectedInterfaceIPs(args.IfName, result.IPs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = ip.ValidateExpectedRoute(result.Routes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -23,5 +23,5 @@ import (
|
||||
|
||||
func TestBridge(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "bridge Suite")
|
||||
RunSpecs(t, "plugins/main/bridge")
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,5 @@
|
||||
# host-device
|
||||
Move an already-existing device in to a container.
|
||||
Move an already-existing device into a container.
|
||||
|
||||
This simple plugin will move the requested device from the host's network namespace
|
||||
to the container's. Nothing else will be done - no IPAM, no addresses.
|
||||
@ -16,6 +16,7 @@ A sample configuration might look like:
|
||||
```json
|
||||
{
|
||||
"cniVersion": "0.3.1",
|
||||
"type": "host-device",
|
||||
"device": "enp0s1"
|
||||
}
|
||||
```
|
||||
|
@ -17,6 +17,7 @@ package main
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
@ -24,14 +25,20 @@ import (
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/vishvananda/netlink"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/skel"
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
"github.com/containernetworking/cni/pkg/types/current"
|
||||
"github.com/containernetworking/cni/pkg/version"
|
||||
|
||||
"github.com/containernetworking/plugins/pkg/ip"
|
||||
"github.com/containernetworking/plugins/pkg/ipam"
|
||||
"github.com/containernetworking/plugins/pkg/ns"
|
||||
"github.com/vishvananda/netlink"
|
||||
bv "github.com/containernetworking/plugins/pkg/utils/buildversion"
|
||||
)
|
||||
|
||||
//NetConf for host-device config, look the README to learn how to use those parameters
|
||||
type NetConf struct {
|
||||
types.NetConf
|
||||
Device string `json:"device"` // Device-Name, something like eth0 or can0 etc.
|
||||
@ -77,14 +84,68 @@ func cmdAdd(args *skel.CmdArgs) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to move link %v", err)
|
||||
}
|
||||
|
||||
var result *current.Result
|
||||
// run the IPAM plugin and get back the config to apply
|
||||
if cfg.IPAM.Type != "" {
|
||||
r, err := ipam.ExecAdd(cfg.IPAM.Type, args.StdinData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Invoke ipam del if err to avoid ip leak
|
||||
defer func() {
|
||||
if err != nil {
|
||||
ipam.ExecDel(cfg.IPAM.Type, args.StdinData)
|
||||
}
|
||||
}()
|
||||
|
||||
// Convert whatever the IPAM result was into the current Result type
|
||||
result, err = current.NewResultFromResult(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(result.IPs) == 0 {
|
||||
return errors.New("IPAM plugin returned missing IP config")
|
||||
}
|
||||
|
||||
result.Interfaces = []*current.Interface{{
|
||||
Name: contDev.Attrs().Name,
|
||||
Mac: contDev.Attrs().HardwareAddr.String(),
|
||||
Sandbox: containerNs.Path(),
|
||||
}}
|
||||
for _, ipc := range result.IPs {
|
||||
// All addresses apply to the container interface (move from host)
|
||||
ipc.Interface = current.Int(0)
|
||||
}
|
||||
|
||||
err = containerNs.Do(func(_ ns.NetNS) error {
|
||||
if err := ipam.ConfigureIface(args.IfName, result); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result.DNS = cfg.DNS
|
||||
|
||||
return types.PrintResult(result, cfg.CNIVersion)
|
||||
}
|
||||
|
||||
return printLink(contDev, cfg.CNIVersion, containerNs)
|
||||
}
|
||||
|
||||
func cmdDel(args *skel.CmdArgs) error {
|
||||
_, err := loadConf(args.StdinData)
|
||||
cfg, err := loadConf(args.StdinData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if args.Netns == "" {
|
||||
return nil
|
||||
}
|
||||
containerNs, err := ns.GetNS(args.Netns)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open netns %q: %v", args.Netns, err)
|
||||
@ -95,6 +156,12 @@ func cmdDel(args *skel.CmdArgs) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if cfg.IPAM.Type != "" {
|
||||
if err := ipam.ExecDel(cfg.IPAM.Type, args.StdinData); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -143,6 +210,11 @@ func moveLinkOut(containerNs ns.NetNS, ifName string) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find %q: %v", ifName, err)
|
||||
}
|
||||
|
||||
// Devices can be renamed only when down
|
||||
if err := netlink.LinkSetDown(dev); err != nil {
|
||||
return fmt.Errorf("failed to set %q down: %v", ifName, err)
|
||||
}
|
||||
// Rename device to it's original name
|
||||
if err := netlink.LinkSetName(dev, dev.Attrs().Alias); err != nil {
|
||||
return fmt.Errorf("failed to restore %q to original name %q: %v", ifName, dev.Attrs().Alias, err)
|
||||
@ -212,5 +284,109 @@ func getLink(devname, hwaddr, kernelpath string) (netlink.Link, error) {
|
||||
}
|
||||
|
||||
func main() {
|
||||
skel.PluginMain(cmdAdd, cmdDel, version.All)
|
||||
skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.All, bv.BuildString("host-device"))
|
||||
}
|
||||
|
||||
func cmdCheck(args *skel.CmdArgs) error {
|
||||
|
||||
cfg, err := loadConf(args.StdinData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
netns, err := ns.GetNS(args.Netns)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open netns %q: %v", args.Netns, err)
|
||||
}
|
||||
defer netns.Close()
|
||||
|
||||
// run the IPAM plugin and get back the config to apply
|
||||
if cfg.IPAM.Type != "" {
|
||||
err = ipam.ExecCheck(cfg.IPAM.Type, args.StdinData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Parse previous result.
|
||||
if cfg.NetConf.RawPrevResult == nil {
|
||||
return fmt.Errorf("Required prevResult missing")
|
||||
}
|
||||
|
||||
if err := version.ParsePrevResult(&cfg.NetConf); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result, err := current.NewResultFromResult(cfg.PrevResult)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var contMap current.Interface
|
||||
// Find interfaces for name we know, that of host-device inside container
|
||||
for _, intf := range result.Interfaces {
|
||||
if args.IfName == intf.Name {
|
||||
if args.Netns == intf.Sandbox {
|
||||
contMap = *intf
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The namespace must be the same as what was configured
|
||||
if args.Netns != contMap.Sandbox {
|
||||
return fmt.Errorf("Sandbox in prevResult %s doesn't match configured netns: %s",
|
||||
contMap.Sandbox, args.Netns)
|
||||
}
|
||||
|
||||
//
|
||||
// Check prevResults for ips, routes and dns against values found in the container
|
||||
if err := netns.Do(func(_ ns.NetNS) error {
|
||||
|
||||
// Check interface against values found in the container
|
||||
err := validateCniContainerInterface(contMap)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = ip.ValidateExpectedInterfaceIPs(args.IfName, result.IPs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = ip.ValidateExpectedRoute(result.Routes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
//
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateCniContainerInterface(intf current.Interface) error {
|
||||
|
||||
var link netlink.Link
|
||||
var err error
|
||||
|
||||
if intf.Name == "" {
|
||||
return fmt.Errorf("Container interface name missing in prevResult: %v", intf.Name)
|
||||
}
|
||||
link, err = netlink.LinkByName(intf.Name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Container Interface name in prevResult: %s not found", intf.Name)
|
||||
}
|
||||
if intf.Sandbox == "" {
|
||||
return fmt.Errorf("Error: Container interface %s should not be in host namespace", link.Attrs().Name)
|
||||
}
|
||||
|
||||
if intf.Mac != "" {
|
||||
if intf.Mac != link.Attrs().HardwareAddr.String() {
|
||||
return fmt.Errorf("Interface %s Mac %s doesn't match container Mac: %s", intf.Name, intf.Mac, link.Attrs().HardwareAddr)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -23,5 +23,5 @@ import (
|
||||
|
||||
func TestVlan(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "host-device Suite")
|
||||
RunSpecs(t, "plugins/main/host-device")
|
||||
}
|
||||
|
@ -15,26 +15,217 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"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/plugins/pkg/ns"
|
||||
"github.com/containernetworking/plugins/pkg/testutils"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/vishvananda/netlink"
|
||||
)
|
||||
|
||||
type Net struct {
|
||||
Name string `json:"name"`
|
||||
CNIVersion string `json:"cniVersion"`
|
||||
Type string `json:"type,omitempty"`
|
||||
Device string `json:"device"` // Device-Name, something like eth0 or can0 etc.
|
||||
HWAddr string `json:"hwaddr"` // MAC Address of target network interface
|
||||
KernelPath string `json:"kernelpath"` // Kernelpath of the device
|
||||
IPAM *IPAMConfig `json:"ipam,omitempty"`
|
||||
DNS types.DNS `json:"dns"`
|
||||
RawPrevResult map[string]interface{} `json:"prevResult,omitempty"`
|
||||
PrevResult current.Result `json:"-"`
|
||||
}
|
||||
|
||||
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 Address struct {
|
||||
AddressStr string `json:"address"`
|
||||
Gateway net.IP `json:"gateway,omitempty"`
|
||||
Address net.IPNet
|
||||
Version string
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
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++
|
||||
}
|
||||
}
|
||||
|
||||
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}}
|
||||
if addr.Address.IP.To4() != nil {
|
||||
addr.Version = "4"
|
||||
numV4++
|
||||
} else {
|
||||
addr.Version = "6"
|
||||
numV6++
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 buildOneConfig(name, cniVersion string, orig *Net, prevResult types.Result) (*Net, error) {
|
||||
var err error
|
||||
|
||||
inject := map[string]interface{}{
|
||||
"name": name,
|
||||
"cniVersion": cniVersion,
|
||||
}
|
||||
// Add previous plugin result
|
||||
if prevResult != nil {
|
||||
inject["prevResult"] = prevResult
|
||||
}
|
||||
|
||||
// Ensure every config uses the same name and version
|
||||
config := make(map[string]interface{})
|
||||
|
||||
confBytes, err := json.Marshal(orig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(confBytes, &config)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unmarshal existing network bytes: %s", err)
|
||||
}
|
||||
|
||||
for key, value := range inject {
|
||||
config[key] = value
|
||||
}
|
||||
|
||||
newBytes, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
conf := &Net{}
|
||||
if err := json.Unmarshal(newBytes, &conf); err != nil {
|
||||
return nil, fmt.Errorf("error parsing configuration: %s", err)
|
||||
}
|
||||
|
||||
return conf, nil
|
||||
|
||||
}
|
||||
|
||||
var _ = Describe("base functionality", func() {
|
||||
var originalNS ns.NetNS
|
||||
var ifname string
|
||||
|
||||
BeforeEach(func() {
|
||||
var err error
|
||||
originalNS, err = ns.NewNS()
|
||||
originalNS, err = testutils.NewNS()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
ifname = fmt.Sprintf("dummy-%x", rand.Int31())
|
||||
@ -44,7 +235,7 @@ var _ = Describe("base functionality", func() {
|
||||
originalNS.Close()
|
||||
})
|
||||
|
||||
It("Works with a valid config", func() {
|
||||
It("Works with a valid config without IPAM", func() {
|
||||
var origLink netlink.Link
|
||||
|
||||
// prepare ifname in original namespace
|
||||
@ -65,10 +256,10 @@ var _ = Describe("base functionality", func() {
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// call CmdAdd
|
||||
targetNS, err := ns.NewNS()
|
||||
targetNS, err := testutils.NewNS()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
CNI_IFNAME := "eth0"
|
||||
cniName := "eth0"
|
||||
conf := fmt.Sprintf(`{
|
||||
"cniVersion": "0.3.0",
|
||||
"name": "cni-plugin-host-device-test",
|
||||
@ -78,14 +269,14 @@ var _ = Describe("base functionality", func() {
|
||||
args := &skel.CmdArgs{
|
||||
ContainerID: "dummy",
|
||||
Netns: targetNS.Path(),
|
||||
IfName: CNI_IFNAME,
|
||||
IfName: cniName,
|
||||
StdinData: []byte(conf),
|
||||
}
|
||||
var resI types.Result
|
||||
err = originalNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
var err error
|
||||
resI, _, err = testutils.CmdAddWithResult(targetNS.Path(), CNI_IFNAME, []byte(conf), func() error { return cmdAdd(args) })
|
||||
resI, _, err = testutils.CmdAddWithArgs(args, func() error { return cmdAdd(args) })
|
||||
return err
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
@ -95,7 +286,7 @@ var _ = Describe("base functionality", func() {
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(res.Interfaces).To(Equal([]*current.Interface{
|
||||
{
|
||||
Name: CNI_IFNAME,
|
||||
Name: cniName,
|
||||
Mac: origLink.Attrs().HardwareAddr.String(),
|
||||
Sandbox: targetNS.Path(),
|
||||
},
|
||||
@ -104,7 +295,7 @@ var _ = Describe("base functionality", func() {
|
||||
// assert that dummy0 is now in the target namespace
|
||||
err = targetNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
link, err := netlink.LinkByName(CNI_IFNAME)
|
||||
link, err := netlink.LinkByName(cniName)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(link.Attrs().HardwareAddr).To(Equal(origLink.Attrs().HardwareAddr))
|
||||
return nil
|
||||
@ -123,7 +314,117 @@ var _ = Describe("base functionality", func() {
|
||||
// Check that deleting the device moves it back and restores the name
|
||||
err = originalNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
err = testutils.CmdDelWithResult(targetNS.Path(), CNI_IFNAME, func() error { return cmdDel(args) })
|
||||
err = testutils.CmdDelWithArgs(args, func() error {
|
||||
return cmdDel(args)
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
_, err := netlink.LinkByName(ifname)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
return nil
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
})
|
||||
|
||||
It("Works with a valid config with IPAM", func() {
|
||||
var origLink netlink.Link
|
||||
|
||||
// prepare ifname in original namespace
|
||||
err := originalNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
err := netlink.LinkAdd(&netlink.Dummy{
|
||||
LinkAttrs: netlink.LinkAttrs{
|
||||
Name: ifname,
|
||||
},
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
origLink, err = netlink.LinkByName(ifname)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
err = netlink.LinkSetUp(origLink)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
return nil
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// call CmdAdd
|
||||
targetNS, err := testutils.NewNS()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
targetIP := "10.10.0.1/24"
|
||||
cniName := "eth0"
|
||||
conf := fmt.Sprintf(`{
|
||||
"cniVersion": "0.3.0",
|
||||
"name": "cni-plugin-host-device-test",
|
||||
"type": "host-device",
|
||||
"ipam": {
|
||||
"type": "static",
|
||||
"addresses": [
|
||||
{
|
||||
"address":"`+targetIP+`",
|
||||
"gateway": "10.10.0.254"
|
||||
}]
|
||||
},
|
||||
"device": %q
|
||||
}`, ifname)
|
||||
args := &skel.CmdArgs{
|
||||
ContainerID: "dummy",
|
||||
Netns: targetNS.Path(),
|
||||
IfName: cniName,
|
||||
StdinData: []byte(conf),
|
||||
}
|
||||
var resI types.Result
|
||||
err = originalNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
var err error
|
||||
resI, _, err = testutils.CmdAddWithArgs(args, func() error { return cmdAdd(args) })
|
||||
return err
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// check that the result was sane
|
||||
res, err := current.NewResultFromResult(resI)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(res.Interfaces).To(Equal([]*current.Interface{
|
||||
{
|
||||
Name: cniName,
|
||||
Mac: origLink.Attrs().HardwareAddr.String(),
|
||||
Sandbox: targetNS.Path(),
|
||||
},
|
||||
}))
|
||||
|
||||
// assert that dummy0 is now in the target namespace
|
||||
err = targetNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
link, err := netlink.LinkByName(cniName)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(link.Attrs().HardwareAddr).To(Equal(origLink.Attrs().HardwareAddr))
|
||||
|
||||
//get the IP address of the interface in the target namespace
|
||||
addrs, err := netlink.AddrList(link, netlink.FAMILY_V4)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
addr := addrs[0].IPNet.String()
|
||||
//assert that IP address is what we set
|
||||
Expect(addr).To(Equal(targetIP))
|
||||
|
||||
return nil
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// assert that dummy0 is now NOT in the original namespace anymore
|
||||
err = originalNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
_, err := netlink.LinkByName(ifname)
|
||||
Expect(err).To(HaveOccurred())
|
||||
return nil
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Check that deleting the device moves it back and restores the name
|
||||
err = originalNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
err = testutils.CmdDelWithArgs(args, func() error {
|
||||
return cmdDel(args)
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
_, err := netlink.LinkByName(ifname)
|
||||
@ -147,9 +448,260 @@ var _ = Describe("base functionality", func() {
|
||||
IfName: ifname,
|
||||
StdinData: []byte(conf),
|
||||
}
|
||||
_, _, err := testutils.CmdAddWithResult(originalNS.Path(), ifname, []byte(conf), func() error { return cmdAdd(args) })
|
||||
_, _, err := testutils.CmdAddWithArgs(args, func() error { return cmdAdd(args) })
|
||||
Expect(err).To(MatchError(`specify either "device", "hwaddr" or "kernelpath"`))
|
||||
|
||||
})
|
||||
|
||||
It("Works with a valid 0.4.0 config without IPAM", func() {
|
||||
var origLink netlink.Link
|
||||
|
||||
// prepare ifname in original namespace
|
||||
err := originalNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
err := netlink.LinkAdd(&netlink.Dummy{
|
||||
LinkAttrs: netlink.LinkAttrs{
|
||||
Name: ifname,
|
||||
},
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
origLink, err = netlink.LinkByName(ifname)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
err = netlink.LinkSetUp(origLink)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
return nil
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// call CmdAdd
|
||||
targetNS, err := testutils.NewNS()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
cniName := "eth0"
|
||||
conf := fmt.Sprintf(`{
|
||||
"cniVersion": "0.4.0",
|
||||
"name": "cni-plugin-host-device-test",
|
||||
"type": "host-device",
|
||||
"device": %q
|
||||
}`, ifname)
|
||||
args := &skel.CmdArgs{
|
||||
ContainerID: "dummy",
|
||||
Netns: targetNS.Path(),
|
||||
IfName: cniName,
|
||||
StdinData: []byte(conf),
|
||||
}
|
||||
var resI types.Result
|
||||
err = originalNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
var err error
|
||||
resI, _, err = testutils.CmdAddWithArgs(args, func() error { return cmdAdd(args) })
|
||||
return err
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// check that the result was sane
|
||||
res, err := current.NewResultFromResult(resI)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(res.Interfaces).To(Equal([]*current.Interface{
|
||||
{
|
||||
Name: cniName,
|
||||
Mac: origLink.Attrs().HardwareAddr.String(),
|
||||
Sandbox: targetNS.Path(),
|
||||
},
|
||||
}))
|
||||
|
||||
// assert that dummy0 is now in the target namespace
|
||||
err = targetNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
link, err := netlink.LinkByName(cniName)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(link.Attrs().HardwareAddr).To(Equal(origLink.Attrs().HardwareAddr))
|
||||
return nil
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// assert that dummy0 is now NOT in the original namespace anymore
|
||||
err = originalNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
_, err := netlink.LinkByName(ifname)
|
||||
Expect(err).To(HaveOccurred())
|
||||
return nil
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// call CmdCheck
|
||||
n := &Net{}
|
||||
err = json.Unmarshal([]byte(conf), &n)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
cniVersion := "0.4.0"
|
||||
newConf, err := buildOneConfig("testConfig", cniVersion, n, res)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
confString, err := json.Marshal(newConf)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
args.StdinData = confString
|
||||
|
||||
// CNI Check host-device in the target namespace
|
||||
|
||||
err = originalNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
var err error
|
||||
err = testutils.CmdCheckWithArgs(args, func() error { return cmdCheck(args) })
|
||||
return err
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Check that deleting the device moves it back and restores the name
|
||||
err = originalNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
err = testutils.CmdDelWithArgs(args, func() error {
|
||||
return cmdDel(args)
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
_, err := netlink.LinkByName(ifname)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
return nil
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
})
|
||||
|
||||
It("Works with a valid 0.4.0 config with IPAM", func() {
|
||||
var origLink netlink.Link
|
||||
|
||||
// prepare ifname in original namespace
|
||||
err := originalNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
err := netlink.LinkAdd(&netlink.Dummy{
|
||||
LinkAttrs: netlink.LinkAttrs{
|
||||
Name: ifname,
|
||||
},
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
origLink, err = netlink.LinkByName(ifname)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
err = netlink.LinkSetUp(origLink)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
return nil
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// call CmdAdd
|
||||
targetNS, err := testutils.NewNS()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
targetIP := "10.10.0.1/24"
|
||||
cniName := "eth0"
|
||||
conf := fmt.Sprintf(`{
|
||||
"cniVersion": "0.4.0",
|
||||
"name": "cni-plugin-host-device-test",
|
||||
"type": "host-device",
|
||||
"ipam": {
|
||||
"type": "static",
|
||||
"addresses": [
|
||||
{
|
||||
"address":"`+targetIP+`",
|
||||
"gateway": "10.10.0.254"
|
||||
}]
|
||||
},
|
||||
"device": %q
|
||||
}`, ifname)
|
||||
args := &skel.CmdArgs{
|
||||
ContainerID: "dummy",
|
||||
Netns: targetNS.Path(),
|
||||
IfName: cniName,
|
||||
StdinData: []byte(conf),
|
||||
}
|
||||
var resI types.Result
|
||||
err = originalNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
var err error
|
||||
resI, _, err = testutils.CmdAddWithArgs(args, func() error { return cmdAdd(args) })
|
||||
return err
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// check that the result was sane
|
||||
res, err := current.NewResultFromResult(resI)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(res.Interfaces).To(Equal([]*current.Interface{
|
||||
{
|
||||
Name: cniName,
|
||||
Mac: origLink.Attrs().HardwareAddr.String(),
|
||||
Sandbox: targetNS.Path(),
|
||||
},
|
||||
}))
|
||||
|
||||
// assert that dummy0 is now in the target namespace
|
||||
err = targetNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
link, err := netlink.LinkByName(cniName)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(link.Attrs().HardwareAddr).To(Equal(origLink.Attrs().HardwareAddr))
|
||||
|
||||
//get the IP address of the interface in the target namespace
|
||||
addrs, err := netlink.AddrList(link, netlink.FAMILY_V4)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
addr := addrs[0].IPNet.String()
|
||||
//assert that IP address is what we set
|
||||
Expect(addr).To(Equal(targetIP))
|
||||
|
||||
return nil
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// assert that dummy0 is now NOT in the original namespace anymore
|
||||
err = originalNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
_, err := netlink.LinkByName(ifname)
|
||||
Expect(err).To(HaveOccurred())
|
||||
return nil
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// call CmdCheck
|
||||
n := &Net{}
|
||||
err = json.Unmarshal([]byte(conf), &n)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
n.IPAM, _, err = LoadIPAMConfig([]byte(conf), "")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
cniVersion := "0.4.0"
|
||||
newConf, err := buildOneConfig("testConfig", cniVersion, n, res)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
confString, err := json.Marshal(newConf)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
args.StdinData = confString
|
||||
|
||||
// CNI Check host-device in the target namespace
|
||||
|
||||
err = originalNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
var err error
|
||||
err = testutils.CmdCheckWithArgs(args, func() error { return cmdCheck(args) })
|
||||
return err
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Check that deleting the device moves it back and restores the name
|
||||
err = originalNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
err = testutils.CmdDelWithArgs(args, func() error {
|
||||
return cmdDel(args)
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
_, err := netlink.LinkByName(ifname)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
return nil
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
})
|
||||
|
||||
})
|
||||
|
@ -20,24 +20,21 @@ import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
|
||||
"github.com/vishvananda/netlink"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/skel"
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
"github.com/containernetworking/cni/pkg/types/current"
|
||||
"github.com/containernetworking/cni/pkg/version"
|
||||
|
||||
"github.com/containernetworking/plugins/pkg/ip"
|
||||
"github.com/containernetworking/plugins/pkg/ipam"
|
||||
"github.com/containernetworking/plugins/pkg/ns"
|
||||
"github.com/vishvananda/netlink"
|
||||
bv "github.com/containernetworking/plugins/pkg/utils/buildversion"
|
||||
)
|
||||
|
||||
type NetConf struct {
|
||||
types.NetConf
|
||||
|
||||
// support chaining for master interface and IP decisions
|
||||
// occurring prior to running ipvlan plugin
|
||||
RawPrevResult *map[string]interface{} `json:"prevResult"`
|
||||
PrevResult *current.Result `json:"-"`
|
||||
|
||||
Master string `json:"master"`
|
||||
Mode string `json:"mode"`
|
||||
MTU int `json:"mtu"`
|
||||
@ -50,33 +47,35 @@ func init() {
|
||||
runtime.LockOSThread()
|
||||
}
|
||||
|
||||
func loadConf(bytes []byte) (*NetConf, string, error) {
|
||||
func loadConf(bytes []byte, cmdCheck bool) (*NetConf, string, error) {
|
||||
n := &NetConf{}
|
||||
if err := json.Unmarshal(bytes, n); err != nil {
|
||||
return nil, "", fmt.Errorf("failed to load netconf: %v", err)
|
||||
}
|
||||
|
||||
if cmdCheck {
|
||||
return n, n.CNIVersion, nil
|
||||
}
|
||||
|
||||
var result *current.Result
|
||||
var err error
|
||||
// Parse previous result
|
||||
if n.RawPrevResult != nil {
|
||||
resultBytes, err := json.Marshal(n.RawPrevResult)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("could not serialize prevResult: %v", err)
|
||||
}
|
||||
res, err := version.NewResult(n.CNIVersion, resultBytes)
|
||||
if err != nil {
|
||||
if n.NetConf.RawPrevResult != nil {
|
||||
if err = version.ParsePrevResult(&n.NetConf); err != nil {
|
||||
return nil, "", fmt.Errorf("could not parse prevResult: %v", err)
|
||||
}
|
||||
n.RawPrevResult = nil
|
||||
n.PrevResult, err = current.NewResultFromResult(res)
|
||||
|
||||
result, err = current.NewResultFromResult(n.PrevResult)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("could not convert result to current version: %v", err)
|
||||
}
|
||||
}
|
||||
if n.Master == "" {
|
||||
if n.PrevResult == nil {
|
||||
if result == nil {
|
||||
return nil, "", fmt.Errorf(`"master" field is required. It specifies the host interface name to virtualize`)
|
||||
}
|
||||
if len(n.PrevResult.Interfaces) == 1 && n.PrevResult.Interfaces[0].Name != "" {
|
||||
n.Master = n.PrevResult.Interfaces[0].Name
|
||||
if len(result.Interfaces) == 1 && result.Interfaces[0].Name != "" {
|
||||
n.Master = result.Interfaces[0].Name
|
||||
} else {
|
||||
return nil, "", fmt.Errorf("chained master failure. PrevResult lacks a single named interface")
|
||||
}
|
||||
@ -97,6 +96,19 @@ func modeFromString(s string) (netlink.IPVlanMode, error) {
|
||||
}
|
||||
}
|
||||
|
||||
func modeToString(mode netlink.IPVlanMode) (string, error) {
|
||||
switch mode {
|
||||
case netlink.IPVLAN_MODE_L2:
|
||||
return "l2", nil
|
||||
case netlink.IPVLAN_MODE_L3:
|
||||
return "l3", nil
|
||||
case netlink.IPVLAN_MODE_L3S:
|
||||
return "l3s", nil
|
||||
default:
|
||||
return "", fmt.Errorf("unknown ipvlan mode: %q", mode)
|
||||
}
|
||||
}
|
||||
|
||||
func createIpvlan(conf *NetConf, ifName string, netns ns.NetNS) (*current.Interface, error) {
|
||||
ipvlan := ¤t.Interface{}
|
||||
|
||||
@ -156,7 +168,7 @@ func createIpvlan(conf *NetConf, ifName string, netns ns.NetNS) (*current.Interf
|
||||
}
|
||||
|
||||
func cmdAdd(args *skel.CmdArgs) error {
|
||||
n, cniVersion, err := loadConf(args.StdinData)
|
||||
n, cniVersion, err := loadConf(args.StdinData, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -175,14 +187,30 @@ func cmdAdd(args *skel.CmdArgs) error {
|
||||
var result *current.Result
|
||||
// Configure iface from PrevResult if we have IPs and an IPAM
|
||||
// block has not been configured
|
||||
if n.IPAM.Type == "" && n.PrevResult != nil && len(n.PrevResult.IPs) > 0 {
|
||||
result = n.PrevResult
|
||||
} else {
|
||||
haveResult := false
|
||||
if n.IPAM.Type == "" && n.PrevResult != nil {
|
||||
result, err = current.NewResultFromResult(n.PrevResult)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(result.IPs) > 0 {
|
||||
haveResult = true
|
||||
}
|
||||
}
|
||||
if !haveResult {
|
||||
// run the IPAM plugin and get back the config to apply
|
||||
r, err := ipam.ExecAdd(n.IPAM.Type, args.StdinData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Invoke ipam del if err to avoid ip leak
|
||||
defer func() {
|
||||
if err != nil {
|
||||
ipam.ExecDel(n.IPAM.Type, args.StdinData)
|
||||
}
|
||||
}()
|
||||
|
||||
// Convert whatever the IPAM result was into the current Result type
|
||||
result, err = current.NewResultFromResult(r)
|
||||
if err != nil {
|
||||
@ -213,7 +241,7 @@ func cmdAdd(args *skel.CmdArgs) error {
|
||||
}
|
||||
|
||||
func cmdDel(args *skel.CmdArgs) error {
|
||||
n, _, err := loadConf(args.StdinData)
|
||||
n, _, err := loadConf(args.StdinData, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -245,5 +273,130 @@ func cmdDel(args *skel.CmdArgs) error {
|
||||
}
|
||||
|
||||
func main() {
|
||||
skel.PluginMain(cmdAdd, cmdDel, version.All)
|
||||
skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.All, bv.BuildString("ipvlan"))
|
||||
}
|
||||
|
||||
func cmdCheck(args *skel.CmdArgs) error {
|
||||
|
||||
n, _, err := loadConf(args.StdinData, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
netns, err := ns.GetNS(args.Netns)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open netns %q: %v", args.Netns, err)
|
||||
}
|
||||
defer netns.Close()
|
||||
|
||||
if n.IPAM.Type != "" {
|
||||
// run the IPAM plugin and get back the config to apply
|
||||
err = ipam.ExecCheck(n.IPAM.Type, args.StdinData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Parse previous result.
|
||||
if n.NetConf.RawPrevResult == nil {
|
||||
return fmt.Errorf("Required prevResult missing")
|
||||
}
|
||||
|
||||
if err := version.ParsePrevResult(&n.NetConf); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result, err := current.NewResultFromResult(n.PrevResult)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var contMap current.Interface
|
||||
// Find interfaces for names whe know, ipvlan inside container
|
||||
for _, intf := range result.Interfaces {
|
||||
if args.IfName == intf.Name {
|
||||
if args.Netns == intf.Sandbox {
|
||||
contMap = *intf
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The namespace must be the same as what was configured
|
||||
if args.Netns != contMap.Sandbox {
|
||||
return fmt.Errorf("Sandbox in prevResult %s doesn't match configured netns: %s",
|
||||
contMap.Sandbox, args.Netns)
|
||||
}
|
||||
|
||||
m, err := netlink.LinkByName(n.Master)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to lookup master %q: %v", n.Master, err)
|
||||
}
|
||||
|
||||
// Check prevResults for ips, routes and dns against values found in the container
|
||||
if err := netns.Do(func(_ ns.NetNS) error {
|
||||
|
||||
// Check interface against values found in the container
|
||||
err := validateCniContainerInterface(contMap, m.Attrs().Index, n.Mode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = ip.ValidateExpectedInterfaceIPs(args.IfName, result.IPs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = ip.ValidateExpectedRoute(result.Routes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateCniContainerInterface(intf current.Interface, masterIndex int, modeExpected string) error {
|
||||
|
||||
var link netlink.Link
|
||||
var err error
|
||||
|
||||
if intf.Name == "" {
|
||||
return fmt.Errorf("Container interface name missing in prevResult: %v", intf.Name)
|
||||
}
|
||||
link, err = netlink.LinkByName(intf.Name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Container Interface name in prevResult: %s not found", intf.Name)
|
||||
}
|
||||
if intf.Sandbox == "" {
|
||||
return fmt.Errorf("Error: Container interface %s should not be in host namespace", link.Attrs().Name)
|
||||
}
|
||||
|
||||
ipv, isIPVlan := link.(*netlink.IPVlan)
|
||||
if !isIPVlan {
|
||||
return fmt.Errorf("Error: Container interface %s not of type ipvlan", link.Attrs().Name)
|
||||
}
|
||||
|
||||
mode, err := modeFromString(modeExpected)
|
||||
if ipv.Mode != mode {
|
||||
currString, err := modeToString(ipv.Mode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
confString, err := modeToString(mode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return fmt.Errorf("Container IPVlan mode %s does not match expected value: %s", currString, confString)
|
||||
}
|
||||
|
||||
if intf.Mac != "" {
|
||||
if intf.Mac != link.Attrs().HardwareAddr.String() {
|
||||
return fmt.Errorf("Interface %s Mac %s doesn't match container Mac: %s", intf.Name, intf.Mac, link.Attrs().HardwareAddr)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -23,5 +23,5 @@ import (
|
||||
|
||||
func TestIpvlan(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "ipvlan Suite")
|
||||
RunSpecs(t, "plugins/main/ipvlan")
|
||||
}
|
||||
|
@ -15,6 +15,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"syscall"
|
||||
@ -27,14 +28,73 @@ import (
|
||||
|
||||
"github.com/vishvananda/netlink"
|
||||
|
||||
"github.com/containernetworking/plugins/plugins/ipam/host-local/backend/allocator"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
const MASTER_NAME = "eth0"
|
||||
|
||||
type Net struct {
|
||||
Name string `json:"name"`
|
||||
CNIVersion string `json:"cniVersion"`
|
||||
Type string `json:"type,omitempty"`
|
||||
Master string `json:"master"`
|
||||
Mode string `json:"mode"`
|
||||
IPAM *allocator.IPAMConfig `json:"ipam"`
|
||||
DNS types.DNS `json:"dns"`
|
||||
RawPrevResult map[string]interface{} `json:"prevResult,omitempty"`
|
||||
PrevResult current.Result `json:"-"`
|
||||
}
|
||||
|
||||
func buildOneConfig(netName string, cniVersion string, master string, orig *Net, prevResult types.Result) (*Net, error) {
|
||||
var err error
|
||||
|
||||
inject := map[string]interface{}{
|
||||
"name": netName,
|
||||
"cniVersion": cniVersion,
|
||||
}
|
||||
// Add previous plugin result
|
||||
if prevResult != nil {
|
||||
inject["prevResult"] = prevResult
|
||||
}
|
||||
if orig.IPAM == nil {
|
||||
inject["master"] = master
|
||||
}
|
||||
|
||||
// Ensure every config uses the same name and version
|
||||
config := make(map[string]interface{})
|
||||
|
||||
confBytes, err := json.Marshal(orig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(confBytes, &config)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unmarshal existing network bytes: %s", err)
|
||||
}
|
||||
|
||||
for key, value := range inject {
|
||||
config[key] = value
|
||||
}
|
||||
|
||||
newBytes, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
conf := &Net{}
|
||||
if err := json.Unmarshal(newBytes, &conf); err != nil {
|
||||
return nil, fmt.Errorf("error parsing configuration: %s", err)
|
||||
}
|
||||
|
||||
return conf, nil
|
||||
|
||||
}
|
||||
|
||||
func ipvlanAddDelTest(conf, IFNAME string, originalNS ns.NetNS) {
|
||||
targetNs, err := ns.NewNS()
|
||||
targetNs, err := testutils.NewNS()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
defer targetNs.Close()
|
||||
|
||||
@ -49,7 +109,7 @@ func ipvlanAddDelTest(conf, IFNAME string, originalNS ns.NetNS) {
|
||||
err = originalNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
r, _, err := testutils.CmdAddWithResult(targetNs.Path(), IFNAME, []byte(conf), func() error {
|
||||
r, _, err := testutils.CmdAddWithArgs(args, func() error {
|
||||
return cmdAdd(args)
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
@ -86,7 +146,110 @@ func ipvlanAddDelTest(conf, IFNAME string, originalNS ns.NetNS) {
|
||||
err = originalNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
err = testutils.CmdDelWithResult(targetNs.Path(), IFNAME, func() error {
|
||||
err = testutils.CmdDelWithArgs(args, func() error {
|
||||
return cmdDel(args)
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
return nil
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Make sure ipvlan link has been deleted
|
||||
err = targetNs.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
link, err := netlink.LinkByName(IFNAME)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(link).To(BeNil())
|
||||
return nil
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
}
|
||||
|
||||
func ipvlanAddCheckDelTest(conf string, netName string, IFNAME string, originalNS ns.NetNS) {
|
||||
targetNs, err := testutils.NewNS()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
defer targetNs.Close()
|
||||
|
||||
args := &skel.CmdArgs{
|
||||
ContainerID: "dummy",
|
||||
Netns: targetNs.Path(),
|
||||
IfName: IFNAME,
|
||||
StdinData: []byte(conf),
|
||||
}
|
||||
|
||||
var result *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())
|
||||
|
||||
result, err = current.GetResult(r)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
Expect(len(result.Interfaces)).To(Equal(1))
|
||||
Expect(result.Interfaces[0].Name).To(Equal(IFNAME))
|
||||
Expect(len(result.IPs)).To(Equal(1))
|
||||
return nil
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Make sure ipvlan link exists in the target namespace
|
||||
err = targetNs.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
link, err := netlink.LinkByName(IFNAME)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(link.Attrs().Name).To(Equal(IFNAME))
|
||||
|
||||
hwaddr, err := net.ParseMAC(result.Interfaces[0].Mac)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(link.Attrs().HardwareAddr).To(Equal(hwaddr))
|
||||
|
||||
addrs, err := netlink.AddrList(link, syscall.AF_INET)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(len(addrs)).To(Equal(1))
|
||||
return nil
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
n := &Net{}
|
||||
err = json.Unmarshal([]byte(conf), &n)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
if n.IPAM != nil {
|
||||
n.IPAM, _, err = allocator.LoadIPAMConfig([]byte(conf), "")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
}
|
||||
|
||||
cniVersion := "0.4.0"
|
||||
newConf, err := buildOneConfig(netName, cniVersion, MASTER_NAME, n, result)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
confString, err := json.Marshal(newConf)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
args.StdinData = confString
|
||||
|
||||
// CNI Check on macvlan in the target namespace
|
||||
err = originalNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
err := testutils.CmdCheckWithArgs(args, func() error {
|
||||
return cmdCheck(args)
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
return nil
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
err = originalNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
err = testutils.CmdDelWithArgs(args, func() error {
|
||||
return cmdDel(args)
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
@ -112,7 +275,7 @@ var _ = Describe("ipvlan 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 {
|
||||
@ -149,7 +312,7 @@ var _ = Describe("ipvlan Operations", func() {
|
||||
}
|
||||
|
||||
// Create ipvlan in other namespace
|
||||
targetNs, err := ns.NewNS()
|
||||
targetNs, err := testutils.NewNS()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
defer targetNs.Close()
|
||||
|
||||
@ -234,7 +397,7 @@ var _ = Describe("ipvlan Operations", func() {
|
||||
}
|
||||
}`, MASTER_NAME)
|
||||
|
||||
targetNs, err := ns.NewNS()
|
||||
targetNs, err := testutils.NewNS()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
defer targetNs.Close()
|
||||
|
||||
@ -248,7 +411,7 @@ var _ = Describe("ipvlan Operations", func() {
|
||||
err = originalNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
err = testutils.CmdDelWithResult(targetNs.Path(), IFNAME, func() error {
|
||||
err = testutils.CmdDelWithArgs(args, func() error {
|
||||
return cmdDel(args)
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
@ -256,4 +419,49 @@ var _ = Describe("ipvlan Operations", func() {
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
|
||||
It("configures and deconfigures a cniVersion 0.4.0 iplvan link with ADD/CHECK/DEL", func() {
|
||||
const IFNAME = "ipvl0"
|
||||
|
||||
conf := fmt.Sprintf(`{
|
||||
"cniVersion": "0.4.0",
|
||||
"name": "ipvlanTest1",
|
||||
"type": "ipvlan",
|
||||
"master": "%s",
|
||||
"ipam": {
|
||||
"type": "host-local",
|
||||
"subnet": "10.1.2.0/24"
|
||||
}
|
||||
}`, MASTER_NAME)
|
||||
|
||||
ipvlanAddCheckDelTest(conf, "ipvlanTest1", IFNAME, originalNS)
|
||||
})
|
||||
|
||||
It("configures and deconfigures a cniVersion 0.4.0 iplvan link with ADD/CHECK/DEL when chained", func() {
|
||||
const IFNAME = "ipvl0"
|
||||
|
||||
conf := fmt.Sprintf(`{
|
||||
"cniVersion": "0.4.0",
|
||||
"name": "ipvlanTest2",
|
||||
"type": "ipvlan",
|
||||
"prevResult": {
|
||||
"interfaces": [
|
||||
{
|
||||
"name": "%s"
|
||||
}
|
||||
],
|
||||
"ips": [
|
||||
{
|
||||
"version": "4",
|
||||
"address": "10.1.2.2/24",
|
||||
"gateway": "10.1.2.1",
|
||||
"interface": 0
|
||||
}
|
||||
],
|
||||
"routes": []
|
||||
}
|
||||
}`, MASTER_NAME)
|
||||
|
||||
ipvlanAddCheckDelTest(conf, "ipvlanTest2", IFNAME, originalNS)
|
||||
})
|
||||
})
|
||||
|
@ -15,11 +15,14 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/vishvananda/netlink"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/skel"
|
||||
"github.com/containernetworking/cni/pkg/types/current"
|
||||
"github.com/containernetworking/cni/pkg/version"
|
||||
|
||||
"github.com/containernetworking/plugins/pkg/ns"
|
||||
"github.com/vishvananda/netlink"
|
||||
bv "github.com/containernetworking/plugins/pkg/utils/buildversion"
|
||||
)
|
||||
|
||||
func cmdAdd(args *skel.CmdArgs) error {
|
||||
@ -46,6 +49,9 @@ func cmdAdd(args *skel.CmdArgs) error {
|
||||
}
|
||||
|
||||
func cmdDel(args *skel.CmdArgs) error {
|
||||
if args.Netns == "" {
|
||||
return nil
|
||||
}
|
||||
args.IfName = "lo" // ignore config, this only works for loopback
|
||||
err := ns.WithNetNSPath(args.Netns, func(ns.NetNS) error {
|
||||
link, err := netlink.LinkByName(args.IfName)
|
||||
@ -68,5 +74,10 @@ func cmdDel(args *skel.CmdArgs) error {
|
||||
}
|
||||
|
||||
func main() {
|
||||
skel.PluginMain(cmdAdd, cmdDel, version.All)
|
||||
skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.All, bv.BuildString("loopback"))
|
||||
}
|
||||
|
||||
func cmdCheck(args *skel.CmdArgs) error {
|
||||
// TODO: implement
|
||||
return nil
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ var pathToLoPlugin string
|
||||
|
||||
func TestLoopback(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Loopback Suite")
|
||||
RunSpecs(t, "plugins/main/loopback")
|
||||
}
|
||||
|
||||
var _ = BeforeSuite(func() {
|
||||
|
@ -21,6 +21,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/containernetworking/plugins/pkg/ns"
|
||||
"github.com/containernetworking/plugins/pkg/testutils"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/onsi/gomega/gbytes"
|
||||
@ -29,21 +30,20 @@ import (
|
||||
|
||||
var _ = Describe("Loopback", func() {
|
||||
var (
|
||||
networkNS ns.NetNS
|
||||
containerID string
|
||||
command *exec.Cmd
|
||||
environ []string
|
||||
networkNS ns.NetNS
|
||||
command *exec.Cmd
|
||||
environ []string
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
command = exec.Command(pathToLoPlugin)
|
||||
|
||||
var err error
|
||||
networkNS, err = ns.NewNS()
|
||||
networkNS, err = testutils.NewNS()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
environ = []string{
|
||||
fmt.Sprintf("CNI_CONTAINERID=%s", containerID),
|
||||
fmt.Sprintf("CNI_CONTAINERID=%s", "dummy"),
|
||||
fmt.Sprintf("CNI_NETNS=%s", networkNS.Path()),
|
||||
fmt.Sprintf("CNI_IFNAME=%s", "this is ignored"),
|
||||
fmt.Sprintf("CNI_ARGS=%s", "none"),
|
||||
@ -58,6 +58,8 @@ var _ = Describe("Loopback", func() {
|
||||
|
||||
Context("when given a network namespace", func() {
|
||||
It("sets the lo device to UP", func() {
|
||||
|
||||
Skip("TODO: add network name")
|
||||
command.Env = append(environ, fmt.Sprintf("CNI_COMMAND=%s", "ADD"))
|
||||
|
||||
session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter)
|
||||
@ -78,6 +80,8 @@ var _ = Describe("Loopback", func() {
|
||||
})
|
||||
|
||||
It("sets the lo device to DOWN", func() {
|
||||
|
||||
Skip("TODO: add network name")
|
||||
command.Env = append(environ, fmt.Sprintf("CNI_COMMAND=%s", "DEL"))
|
||||
|
||||
session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter)
|
||||
|
@ -23,10 +23,10 @@ Since each macvlan interface has its own MAC address, it makes it easy to use wi
|
||||
|
||||
* `name` (string, required): the name of the network
|
||||
* `type` (string, required): "macvlan"
|
||||
* `master` (string, required): name of the host interface to enslave
|
||||
* `mode` (string, optional): one of "bridge", "private", "vepa", "passthrough". Defaults to "bridge".
|
||||
* `master` (string, optional): name of the host interface to enslave. Defaults to default route interace.
|
||||
* `mode` (string, optional): one of "bridge", "private", "vepa", "passthru". Defaults to "bridge".
|
||||
* `mtu` (integer, optional): explicitly set MTU to the specified value. Defaults to the value chosen by the kernel.
|
||||
* `ipam` (dictionary, required): IPAM configuration to be used for this network.
|
||||
* `ipam` (dictionary, required): IPAM configuration to be used for this network. For interface only without ip address, create empty dictionary.
|
||||
|
||||
## Notes
|
||||
|
||||
|
@ -21,16 +21,19 @@ import (
|
||||
"net"
|
||||
"runtime"
|
||||
|
||||
"github.com/j-keck/arping"
|
||||
"github.com/vishvananda/netlink"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/skel"
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
"github.com/containernetworking/cni/pkg/types/current"
|
||||
"github.com/containernetworking/cni/pkg/version"
|
||||
|
||||
"github.com/containernetworking/plugins/pkg/ip"
|
||||
"github.com/containernetworking/plugins/pkg/ipam"
|
||||
"github.com/containernetworking/plugins/pkg/ns"
|
||||
bv "github.com/containernetworking/plugins/pkg/utils/buildversion"
|
||||
"github.com/containernetworking/plugins/pkg/utils/sysctl"
|
||||
"github.com/j-keck/arping"
|
||||
"github.com/vishvananda/netlink"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -51,13 +54,36 @@ func init() {
|
||||
runtime.LockOSThread()
|
||||
}
|
||||
|
||||
func getDefaultRouteInterfaceName() (string, error) {
|
||||
routeToDstIP, err := netlink.RouteList(nil, netlink.FAMILY_ALL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for _, v := range routeToDstIP {
|
||||
if v.Dst == nil {
|
||||
l, err := netlink.LinkByIndex(v.LinkIndex)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return l.Attrs().Name, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("no default route interface found")
|
||||
}
|
||||
|
||||
func loadConf(bytes []byte) (*NetConf, string, error) {
|
||||
n := &NetConf{}
|
||||
if err := json.Unmarshal(bytes, n); err != nil {
|
||||
return nil, "", fmt.Errorf("failed to load netconf: %v", err)
|
||||
}
|
||||
if n.Master == "" {
|
||||
return nil, "", fmt.Errorf(`"master" field is required. It specifies the host interface name to virtualize`)
|
||||
defaultRouteInterface, err := getDefaultRouteInterfaceName()
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
n.Master = defaultRouteInterface
|
||||
}
|
||||
return n, n.CNIVersion, nil
|
||||
}
|
||||
@ -77,6 +103,21 @@ func modeFromString(s string) (netlink.MacvlanMode, error) {
|
||||
}
|
||||
}
|
||||
|
||||
func modeToString(mode netlink.MacvlanMode) (string, error) {
|
||||
switch mode {
|
||||
case netlink.MACVLAN_MODE_BRIDGE:
|
||||
return "bridge", nil
|
||||
case netlink.MACVLAN_MODE_PRIVATE:
|
||||
return "private", nil
|
||||
case netlink.MACVLAN_MODE_VEPA:
|
||||
return "vepa", nil
|
||||
case netlink.MACVLAN_MODE_PASSTHRU:
|
||||
return "passthru", nil
|
||||
default:
|
||||
return "", fmt.Errorf("unknown macvlan mode: %q", mode)
|
||||
}
|
||||
}
|
||||
|
||||
func createMacvlan(conf *NetConf, ifName string, netns ns.NetNS) (*current.Interface, error) {
|
||||
macvlan := ¤t.Interface{}
|
||||
|
||||
@ -150,6 +191,8 @@ func cmdAdd(args *skel.CmdArgs) error {
|
||||
return err
|
||||
}
|
||||
|
||||
isLayer3 := n.IPAM.Type != ""
|
||||
|
||||
netns, err := ns.GetNS(args.Netns)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open netns %q: %v", netns, err)
|
||||
@ -170,54 +213,78 @@ func cmdAdd(args *skel.CmdArgs) error {
|
||||
}
|
||||
}()
|
||||
|
||||
// run the IPAM plugin and get back the config to apply
|
||||
r, err := ipam.ExecAdd(n.IPAM.Type, args.StdinData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Assume L2 interface only
|
||||
result := ¤t.Result{CNIVersion: cniVersion, Interfaces: []*current.Interface{macvlanInterface}}
|
||||
|
||||
// Invoke ipam del if err to avoid ip leak
|
||||
defer func() {
|
||||
if isLayer3 {
|
||||
// run the IPAM plugin and get back the config to apply
|
||||
r, err := ipam.ExecAdd(n.IPAM.Type, args.StdinData)
|
||||
if err != nil {
|
||||
ipam.ExecDel(n.IPAM.Type, args.StdinData)
|
||||
}
|
||||
}()
|
||||
|
||||
// Convert whatever the IPAM result was into the current Result type
|
||||
result, err := current.NewResultFromResult(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(result.IPs) == 0 {
|
||||
return errors.New("IPAM plugin returned missing IP config")
|
||||
}
|
||||
result.Interfaces = []*current.Interface{macvlanInterface}
|
||||
|
||||
for _, ipc := range result.IPs {
|
||||
// All addresses apply to the container macvlan interface
|
||||
ipc.Interface = current.Int(0)
|
||||
}
|
||||
|
||||
err = netns.Do(func(_ ns.NetNS) error {
|
||||
if err := ipam.ConfigureIface(args.IfName, result); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
contVeth, err := net.InterfaceByName(args.IfName)
|
||||
// Invoke ipam del if err to avoid ip leak
|
||||
defer func() {
|
||||
if err != nil {
|
||||
ipam.ExecDel(n.IPAM.Type, args.StdinData)
|
||||
}
|
||||
}()
|
||||
|
||||
// Convert whatever the IPAM result was into the current Result type
|
||||
ipamResult, err := current.NewResultFromResult(r)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to look up %q: %v", args.IfName, err)
|
||||
return err
|
||||
}
|
||||
|
||||
for _, ipc := range result.IPs {
|
||||
if ipc.Version == "4" {
|
||||
_ = arping.GratuitousArpOverIface(ipc.Address.IP, *contVeth)
|
||||
}
|
||||
if len(ipamResult.IPs) == 0 {
|
||||
return errors.New("IPAM plugin returned missing IP config")
|
||||
}
|
||||
|
||||
result.IPs = ipamResult.IPs
|
||||
result.Routes = ipamResult.Routes
|
||||
|
||||
for _, ipc := range result.IPs {
|
||||
// All addresses apply to the container macvlan interface
|
||||
ipc.Interface = current.Int(0)
|
||||
}
|
||||
|
||||
err = netns.Do(func(_ ns.NetNS) error {
|
||||
if err := ipam.ConfigureIface(args.IfName, result); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
contVeth, err := net.InterfaceByName(args.IfName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to look up %q: %v", args.IfName, err)
|
||||
}
|
||||
|
||||
for _, ipc := range result.IPs {
|
||||
if ipc.Version == "4" {
|
||||
_ = arping.GratuitousArpOverIface(ipc.Address.IP, *contVeth)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// For L2 just change interface status to up
|
||||
err = netns.Do(func(_ ns.NetNS) error {
|
||||
macvlanInterfaceLink, err := netlink.LinkByName(args.IfName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find interface name %q: %v", macvlanInterface.Name, err)
|
||||
}
|
||||
|
||||
if err := netlink.LinkSetUp(macvlanInterfaceLink); err != nil {
|
||||
return fmt.Errorf("failed to set %q UP: %v", args.IfName, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result.DNS = n.DNS
|
||||
@ -231,9 +298,13 @@ func cmdDel(args *skel.CmdArgs) error {
|
||||
return err
|
||||
}
|
||||
|
||||
err = ipam.ExecDel(n.IPAM.Type, args.StdinData)
|
||||
if err != nil {
|
||||
return err
|
||||
isLayer3 := n.IPAM.Type != ""
|
||||
|
||||
if isLayer3 {
|
||||
err = ipam.ExecDel(n.IPAM.Type, args.StdinData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if args.Netns == "" {
|
||||
@ -255,5 +326,132 @@ func cmdDel(args *skel.CmdArgs) error {
|
||||
}
|
||||
|
||||
func main() {
|
||||
skel.PluginMain(cmdAdd, cmdDel, version.All)
|
||||
skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.All, bv.BuildString("macvlan"))
|
||||
}
|
||||
|
||||
func cmdCheck(args *skel.CmdArgs) error {
|
||||
|
||||
n, _, err := loadConf(args.StdinData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
isLayer3 := n.IPAM.Type != ""
|
||||
|
||||
netns, err := ns.GetNS(args.Netns)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open netns %q: %v", args.Netns, err)
|
||||
}
|
||||
defer netns.Close()
|
||||
|
||||
if isLayer3 {
|
||||
// run the IPAM plugin and get back the config to apply
|
||||
err = ipam.ExecCheck(n.IPAM.Type, args.StdinData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Parse previous result.
|
||||
if n.NetConf.RawPrevResult == nil {
|
||||
return fmt.Errorf("Required prevResult missing")
|
||||
}
|
||||
|
||||
if err := version.ParsePrevResult(&n.NetConf); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result, err := current.NewResultFromResult(n.PrevResult)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var contMap current.Interface
|
||||
// Find interfaces for names whe know, macvlan device name inside container
|
||||
for _, intf := range result.Interfaces {
|
||||
if args.IfName == intf.Name {
|
||||
if args.Netns == intf.Sandbox {
|
||||
contMap = *intf
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The namespace must be the same as what was configured
|
||||
if args.Netns != contMap.Sandbox {
|
||||
return fmt.Errorf("Sandbox in prevResult %s doesn't match configured netns: %s",
|
||||
contMap.Sandbox, args.Netns)
|
||||
}
|
||||
|
||||
m, err := netlink.LinkByName(n.Master)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to lookup master %q: %v", n.Master, err)
|
||||
}
|
||||
|
||||
// Check prevResults for ips, routes and dns against values found in the container
|
||||
if err := netns.Do(func(_ ns.NetNS) error {
|
||||
|
||||
// Check interface against values found in the container
|
||||
err := validateCniContainerInterface(contMap, m.Attrs().Index, n.Mode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = ip.ValidateExpectedInterfaceIPs(args.IfName, result.IPs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = ip.ValidateExpectedRoute(result.Routes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateCniContainerInterface(intf current.Interface, parentIndex int, modeExpected string) error {
|
||||
|
||||
var link netlink.Link
|
||||
var err error
|
||||
|
||||
if intf.Name == "" {
|
||||
return fmt.Errorf("Container interface name missing in prevResult: %v", intf.Name)
|
||||
}
|
||||
link, err = netlink.LinkByName(intf.Name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Container Interface name in prevResult: %s not found", intf.Name)
|
||||
}
|
||||
if intf.Sandbox == "" {
|
||||
return fmt.Errorf("Error: Container interface %s should not be in host namespace", link.Attrs().Name)
|
||||
}
|
||||
|
||||
macv, isMacvlan := link.(*netlink.Macvlan)
|
||||
if !isMacvlan {
|
||||
return fmt.Errorf("Error: Container interface %s not of type macvlan", link.Attrs().Name)
|
||||
}
|
||||
|
||||
mode, err := modeFromString(modeExpected)
|
||||
if macv.Mode != mode {
|
||||
currString, err := modeToString(macv.Mode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
confString, err := modeToString(mode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return fmt.Errorf("Container macvlan mode %s does not match expected value: %s", currString, confString)
|
||||
}
|
||||
|
||||
if intf.Mac != "" {
|
||||
if intf.Mac != link.Attrs().HardwareAddr.String() {
|
||||
return fmt.Errorf("Interface %s Mac %s doesn't match container Mac: %s", intf.Name, intf.Mac, link.Attrs().HardwareAddr)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -23,5 +23,5 @@ import (
|
||||
|
||||
func TestMacvlan(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "macvlan Suite")
|
||||
RunSpecs(t, "plugins/main/macvlan")
|
||||
}
|
||||
|
@ -15,6 +15,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"syscall"
|
||||
@ -27,19 +28,80 @@ import (
|
||||
|
||||
"github.com/vishvananda/netlink"
|
||||
|
||||
"github.com/containernetworking/plugins/plugins/ipam/host-local/backend/allocator"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
const MASTER_NAME = "eth0"
|
||||
|
||||
type Net struct {
|
||||
Name string `json:"name"`
|
||||
CNIVersion string `json:"cniVersion"`
|
||||
Type string `json:"type,omitempty"`
|
||||
Master string `json:"master"`
|
||||
Mode string `json:"mode"`
|
||||
IPAM *allocator.IPAMConfig `json:"ipam"`
|
||||
//RuntimeConfig struct { // The capability arg
|
||||
// IPRanges []RangeSet `json:"ipRanges,omitempty"`
|
||||
//} `json:"runtimeConfig,omitempty"`
|
||||
//Args *struct {
|
||||
// A *IPAMArgs `json:"cni"`
|
||||
DNS types.DNS `json:"dns"`
|
||||
RawPrevResult map[string]interface{} `json:"prevResult,omitempty"`
|
||||
PrevResult current.Result `json:"-"`
|
||||
}
|
||||
|
||||
func buildOneConfig(netName string, cniVersion string, orig *Net, prevResult types.Result) (*Net, error) {
|
||||
var err error
|
||||
|
||||
inject := map[string]interface{}{
|
||||
"name": netName,
|
||||
"cniVersion": cniVersion,
|
||||
}
|
||||
// Add previous plugin result
|
||||
if prevResult != nil {
|
||||
inject["prevResult"] = prevResult
|
||||
}
|
||||
|
||||
// Ensure every config uses the same name and version
|
||||
config := make(map[string]interface{})
|
||||
|
||||
confBytes, err := json.Marshal(orig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(confBytes, &config)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unmarshal existing network bytes: %s", err)
|
||||
}
|
||||
|
||||
for key, value := range inject {
|
||||
config[key] = value
|
||||
}
|
||||
|
||||
newBytes, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
conf := &Net{}
|
||||
if err := json.Unmarshal(newBytes, &conf); err != nil {
|
||||
return nil, fmt.Errorf("error parsing configuration: %s", err)
|
||||
}
|
||||
|
||||
return conf, nil
|
||||
|
||||
}
|
||||
|
||||
var _ = Describe("macvlan Operations", func() {
|
||||
var originalNS ns.NetNS
|
||||
|
||||
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 {
|
||||
@ -75,7 +137,7 @@ var _ = Describe("macvlan Operations", func() {
|
||||
MTU: 1500,
|
||||
}
|
||||
|
||||
targetNs, err := ns.NewNS()
|
||||
targetNs, err := testutils.NewNS()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
defer targetNs.Close()
|
||||
|
||||
@ -114,7 +176,7 @@ var _ = Describe("macvlan Operations", func() {
|
||||
}
|
||||
}`, MASTER_NAME)
|
||||
|
||||
targetNs, err := ns.NewNS()
|
||||
targetNs, err := testutils.NewNS()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
defer targetNs.Close()
|
||||
|
||||
@ -129,7 +191,7 @@ var _ = Describe("macvlan Operations", func() {
|
||||
err = originalNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
r, _, err := testutils.CmdAddWithResult(targetNs.Path(), IFNAME, []byte(conf), func() error {
|
||||
r, _, err := testutils.CmdAddWithArgs(args, func() error {
|
||||
return cmdAdd(args)
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
@ -166,7 +228,7 @@ var _ = Describe("macvlan Operations", func() {
|
||||
err = originalNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
err := testutils.CmdDelWithResult(targetNs.Path(), IFNAME, func() error {
|
||||
err := testutils.CmdDelWithArgs(args, func() error {
|
||||
return cmdDel(args)
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
@ -200,7 +262,7 @@ var _ = Describe("macvlan Operations", func() {
|
||||
}
|
||||
}`, MASTER_NAME)
|
||||
|
||||
targetNs, err := ns.NewNS()
|
||||
targetNs, err := testutils.NewNS()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
defer targetNs.Close()
|
||||
|
||||
@ -214,7 +276,7 @@ var _ = Describe("macvlan Operations", func() {
|
||||
err = originalNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
err := testutils.CmdDelWithResult(targetNs.Path(), IFNAME, func() error {
|
||||
err := testutils.CmdDelWithArgs(args, func() error {
|
||||
return cmdDel(args)
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
@ -223,4 +285,312 @@ var _ = Describe("macvlan Operations", func() {
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
})
|
||||
|
||||
It("configures and deconfigures a l2 macvlan link with ADD/DEL", func() {
|
||||
const IFNAME = "macvl0"
|
||||
|
||||
conf := fmt.Sprintf(`{
|
||||
"cniVersion": "0.3.1",
|
||||
"name": "mynet",
|
||||
"type": "macvlan",
|
||||
"master": "%s",
|
||||
"ipam": {}
|
||||
}`, MASTER_NAME)
|
||||
|
||||
targetNs, err := testutils.NewNS()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
defer targetNs.Close()
|
||||
|
||||
args := &skel.CmdArgs{
|
||||
ContainerID: "dummy",
|
||||
Netns: targetNs.Path(),
|
||||
IfName: IFNAME,
|
||||
StdinData: []byte(conf),
|
||||
}
|
||||
|
||||
var result *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())
|
||||
|
||||
result, err = current.GetResult(r)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
Expect(len(result.Interfaces)).To(Equal(1))
|
||||
Expect(result.Interfaces[0].Name).To(Equal(IFNAME))
|
||||
Expect(len(result.IPs)).To(Equal(0))
|
||||
return nil
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Make sure macvlan link exists in the target namespace
|
||||
err = targetNs.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
link, err := netlink.LinkByName(IFNAME)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(link.Attrs().Name).To(Equal(IFNAME))
|
||||
|
||||
hwaddr, err := net.ParseMAC(result.Interfaces[0].Mac)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(link.Attrs().HardwareAddr).To(Equal(hwaddr))
|
||||
|
||||
addrs, err := netlink.AddrList(link, syscall.AF_INET)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(len(addrs)).To(Equal(0))
|
||||
return nil
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
err = originalNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
err := testutils.CmdDelWithArgs(args, func() error {
|
||||
return cmdDel(args)
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
return nil
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Make sure macvlan link has been deleted
|
||||
err = targetNs.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
link, err := netlink.LinkByName(IFNAME)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(link).To(BeNil())
|
||||
return nil
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
|
||||
It("configures and deconfigures a cniVersion 0.4.0 macvlan link with ADD/DEL", func() {
|
||||
const IFNAME = "macvl0"
|
||||
|
||||
conf := fmt.Sprintf(`{
|
||||
"cniVersion": "0.4.0",
|
||||
"name": "macvlanTestv4",
|
||||
"type": "macvlan",
|
||||
"master": "%s",
|
||||
"ipam": {
|
||||
"type": "host-local",
|
||||
"ranges": [[ {"subnet": "10.1.2.0/24", "gateway": "10.1.2.1"} ]]
|
||||
}
|
||||
}`, MASTER_NAME)
|
||||
|
||||
targetNs, err := testutils.NewNS()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
defer targetNs.Close()
|
||||
|
||||
args := &skel.CmdArgs{
|
||||
ContainerID: "dummy",
|
||||
Netns: targetNs.Path(),
|
||||
IfName: IFNAME,
|
||||
StdinData: []byte(conf),
|
||||
}
|
||||
|
||||
var result *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())
|
||||
|
||||
result, err = current.GetResult(r)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
Expect(len(result.Interfaces)).To(Equal(1))
|
||||
Expect(result.Interfaces[0].Name).To(Equal(IFNAME))
|
||||
Expect(len(result.IPs)).To(Equal(1))
|
||||
return nil
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Make sure macvlan link exists in the target namespace
|
||||
err = targetNs.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
link, err := netlink.LinkByName(IFNAME)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(link.Attrs().Name).To(Equal(IFNAME))
|
||||
|
||||
hwaddr, err := net.ParseMAC(result.Interfaces[0].Mac)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(link.Attrs().HardwareAddr).To(Equal(hwaddr))
|
||||
|
||||
addrs, err := netlink.AddrList(link, syscall.AF_INET)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(len(addrs)).To(Equal(1))
|
||||
return nil
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
n := &Net{}
|
||||
err = json.Unmarshal([]byte(conf), &n)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
n.IPAM, _, err = allocator.LoadIPAMConfig([]byte(conf), "")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
cniVersion := "0.4.0"
|
||||
newConf, err := buildOneConfig("macvlanTestv4", cniVersion, n, result)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
confString, err := json.Marshal(newConf)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
args.StdinData = confString
|
||||
|
||||
// CNI Check on macvlan in the target namespace
|
||||
err = originalNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
err := testutils.CmdCheckWithArgs(args, func() error {
|
||||
return cmdCheck(args)
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
return nil
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
err = originalNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
err := testutils.CmdDelWithArgs(args, func() error {
|
||||
return cmdDel(args)
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
return nil
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Make sure macvlan link has been deleted
|
||||
err = targetNs.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
link, err := netlink.LinkByName(IFNAME)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(link).To(BeNil())
|
||||
return nil
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
|
||||
It("configures and deconfigures a macvlan link with ADD/DEL, without master config", func() {
|
||||
const IFNAME = "macvl0"
|
||||
|
||||
conf := `{
|
||||
"cniVersion": "0.3.1",
|
||||
"name": "mynet",
|
||||
"type": "macvlan",
|
||||
"ipam": {
|
||||
"type": "host-local",
|
||||
"subnet": "10.1.2.0/24"
|
||||
}
|
||||
}`
|
||||
|
||||
targetNs, err := testutils.NewNS()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
defer targetNs.Close()
|
||||
|
||||
args := &skel.CmdArgs{
|
||||
ContainerID: "dummy",
|
||||
Netns: targetNs.Path(),
|
||||
IfName: IFNAME,
|
||||
StdinData: []byte(conf),
|
||||
}
|
||||
|
||||
// Make MASTER_NAME as default route interface
|
||||
err = originalNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
link, err := netlink.LinkByName(MASTER_NAME)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
err = netlink.LinkSetUp(link)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
var address = &net.IPNet{IP: net.IPv4(192, 0, 0, 1), Mask: net.CIDRMask(24, 32)}
|
||||
var addr = &netlink.Addr{IPNet: address}
|
||||
err = netlink.AddrAdd(link, addr)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// add default gateway into MASTER
|
||||
dst := &net.IPNet{
|
||||
IP: net.IPv4(0, 0, 0, 0),
|
||||
Mask: net.CIDRMask(0, 0),
|
||||
}
|
||||
ip := net.IPv4(192, 0, 0, 254)
|
||||
route := netlink.Route{LinkIndex: link.Attrs().Index, Dst: dst, Gw: ip}
|
||||
err = netlink.RouteAdd(&route)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
return nil
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
var result *current.Result
|
||||
err = originalNS.Do(func(ns.NetNS) error {
|
||||
r, _, err := testutils.CmdAddWithArgs(args, func() error {
|
||||
return cmdAdd(args)
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
result, err = current.GetResult(r)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
Expect(len(result.Interfaces)).To(Equal(1))
|
||||
Expect(result.Interfaces[0].Name).To(Equal(IFNAME))
|
||||
Expect(len(result.IPs)).To(Equal(1))
|
||||
return nil
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Make sure macvlan link exists in the target namespace
|
||||
err = targetNs.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
link, err := netlink.LinkByName(IFNAME)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(link.Attrs().Name).To(Equal(IFNAME))
|
||||
|
||||
hwaddr, err := net.ParseMAC(result.Interfaces[0].Mac)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(link.Attrs().HardwareAddr).To(Equal(hwaddr))
|
||||
|
||||
addrs, err := netlink.AddrList(link, syscall.AF_INET)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(len(addrs)).To(Equal(1))
|
||||
return nil
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
err = originalNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
err := testutils.CmdDelWithArgs(args, func() error {
|
||||
return cmdDel(args)
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
return nil
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Make sure macvlan link has been deleted
|
||||
err = targetNs.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
link, err := netlink.LinkByName(IFNAME)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(link).To(BeNil())
|
||||
return nil
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
|
||||
})
|
||||
|
@ -26,7 +26,7 @@ The traffic of the container interface will be routed through the interface of t
|
||||
|
||||
* `name` (string, required): the name of the network
|
||||
* `type` (string, required): "ptp"
|
||||
* `ipMasq` (boolean, optional): set up IP Masquerade on the host for traffic originating from this network and destined outside of it. Defaults to false.
|
||||
* `ipMasq` (boolean, optional): set up IP Masquerade on the host for traffic originating from ip of this network and destined outside of this network. Defaults to false.
|
||||
* `mtu` (integer, optional): explicitly set MTU to the specified value. Defaults to value chosen by the kernel.
|
||||
* `ipam` (dictionary, required): IPAM configuration to be used for this network.
|
||||
* `dns` (dictionary, optional): DNS information to return as described in the [Result](https://github.com/containernetworking/cni/blob/master/SPEC.md#result).
|
||||
|
@ -22,16 +22,19 @@ import (
|
||||
"os"
|
||||
"runtime"
|
||||
|
||||
"github.com/j-keck/arping"
|
||||
"github.com/vishvananda/netlink"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/skel"
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
"github.com/containernetworking/cni/pkg/types/current"
|
||||
"github.com/containernetworking/cni/pkg/version"
|
||||
|
||||
"github.com/containernetworking/plugins/pkg/ip"
|
||||
"github.com/containernetworking/plugins/pkg/ipam"
|
||||
"github.com/containernetworking/plugins/pkg/ns"
|
||||
"github.com/containernetworking/plugins/pkg/utils"
|
||||
"github.com/j-keck/arping"
|
||||
"github.com/vishvananda/netlink"
|
||||
bv "github.com/containernetworking/plugins/pkg/utils/buildversion"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@ -49,7 +52,7 @@ type NetConf struct {
|
||||
|
||||
func setupContainerVeth(netns ns.NetNS, ifName string, mtu int, pr *current.Result) (*current.Interface, *current.Interface, error) {
|
||||
// The IPAM result will be something like IP=192.168.3.5/24, GW=192.168.3.1.
|
||||
// What we want is really a point-to-point link but veth does not support IFF_POINTOPONT.
|
||||
// What we want is really a point-to-point link but veth does not support IFF_POINTTOPOINT.
|
||||
// Next best thing would be to let it ARP but set interface to 192.168.3.5/32 and
|
||||
// add a route like "192.168.3.0/24 via 192.168.3.1 dev $ifName".
|
||||
// Unfortunately that won't work as the GW will be outside the interface's subnet.
|
||||
@ -110,7 +113,7 @@ func setupContainerVeth(netns ns.NetNS, ifName string, mtu int, pr *current.Resu
|
||||
}
|
||||
|
||||
for _, r := range []netlink.Route{
|
||||
netlink.Route{
|
||||
{
|
||||
LinkIndex: contVeth.Index,
|
||||
Dst: &net.IPNet{
|
||||
IP: ipc.Gateway,
|
||||
@ -119,7 +122,7 @@ func setupContainerVeth(netns ns.NetNS, ifName string, mtu int, pr *current.Resu
|
||||
Scope: netlink.SCOPE_LINK,
|
||||
Src: ipc.Address.IP,
|
||||
},
|
||||
netlink.Route{
|
||||
{
|
||||
LinkIndex: contVeth.Index,
|
||||
Dst: &net.IPNet{
|
||||
IP: ipc.Address.IP.Mask(ipc.Address.Mask),
|
||||
@ -197,6 +200,14 @@ func cmdAdd(args *skel.CmdArgs) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Invoke ipam del if err to avoid ip leak
|
||||
defer func() {
|
||||
if err != nil {
|
||||
ipam.ExecDel(conf.IPAM.Type, args.StdinData)
|
||||
}
|
||||
}()
|
||||
|
||||
// Convert whatever the IPAM result was into the current Result type
|
||||
result, err := current.NewResultFromResult(r)
|
||||
if err != nil {
|
||||
@ -285,5 +296,108 @@ func cmdDel(args *skel.CmdArgs) error {
|
||||
}
|
||||
|
||||
func main() {
|
||||
skel.PluginMain(cmdAdd, cmdDel, version.All)
|
||||
skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.All, bv.BuildString("ptp"))
|
||||
}
|
||||
|
||||
func cmdCheck(args *skel.CmdArgs) error {
|
||||
conf := NetConf{}
|
||||
if err := json.Unmarshal(args.StdinData, &conf); err != nil {
|
||||
return fmt.Errorf("failed to load netconf: %v", err)
|
||||
}
|
||||
|
||||
netns, err := ns.GetNS(args.Netns)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open netns %q: %v", args.Netns, err)
|
||||
}
|
||||
defer netns.Close()
|
||||
|
||||
// run the IPAM plugin and get back the config to apply
|
||||
err = ipam.ExecCheck(conf.IPAM.Type, args.StdinData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if conf.NetConf.RawPrevResult == nil {
|
||||
return fmt.Errorf("ptp: Required prevResult missing")
|
||||
}
|
||||
if err := version.ParsePrevResult(&conf.NetConf); err != nil {
|
||||
return err
|
||||
}
|
||||
// Convert whatever the IPAM result was into the current Result type
|
||||
result, err := current.NewResultFromResult(conf.PrevResult)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var contMap current.Interface
|
||||
// Find interfaces for name whe know, that of host-device inside container
|
||||
for _, intf := range result.Interfaces {
|
||||
if args.IfName == intf.Name {
|
||||
if args.Netns == intf.Sandbox {
|
||||
contMap = *intf
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The namespace must be the same as what was configured
|
||||
if args.Netns != contMap.Sandbox {
|
||||
return fmt.Errorf("Sandbox in prevResult %s doesn't match configured netns: %s",
|
||||
contMap.Sandbox, args.Netns)
|
||||
}
|
||||
|
||||
//
|
||||
// Check prevResults for ips, routes and dns against values found in the container
|
||||
if err := netns.Do(func(_ ns.NetNS) error {
|
||||
|
||||
// Check interface against values found in the container
|
||||
err := validateCniContainerInterface(contMap)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = ip.ValidateExpectedInterfaceIPs(args.IfName, result.IPs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = ip.ValidateExpectedRoute(result.Routes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateCniContainerInterface(intf current.Interface) error {
|
||||
|
||||
var link netlink.Link
|
||||
var err error
|
||||
|
||||
if intf.Name == "" {
|
||||
return fmt.Errorf("Container interface name missing in prevResult: %v", intf.Name)
|
||||
}
|
||||
link, err = netlink.LinkByName(intf.Name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ptp: Container Interface name in prevResult: %s not found", intf.Name)
|
||||
}
|
||||
if intf.Sandbox == "" {
|
||||
return fmt.Errorf("ptp: Error: Container interface %s should not be in host namespace", link.Attrs().Name)
|
||||
}
|
||||
|
||||
_, isVeth := link.(*netlink.Veth)
|
||||
if !isVeth {
|
||||
return fmt.Errorf("Error: Container interface %s not of type veth/p2p", link.Attrs().Name)
|
||||
}
|
||||
|
||||
if intf.Mac != "" {
|
||||
if intf.Mac != link.Attrs().HardwareAddr.String() {
|
||||
return fmt.Errorf("ptp: Interface %s Mac %s doesn't match container Mac: %s", intf.Name, intf.Mac, link.Attrs().HardwareAddr)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -23,5 +23,5 @@ import (
|
||||
|
||||
func TestPtp(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "ptp Suite")
|
||||
RunSpecs(t, "plugins/main/ptp")
|
||||
}
|
||||
|
@ -15,6 +15,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/skel"
|
||||
@ -25,17 +26,73 @@ import (
|
||||
|
||||
"github.com/vishvananda/netlink"
|
||||
|
||||
"github.com/containernetworking/plugins/plugins/ipam/host-local/backend/allocator"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
type Net struct {
|
||||
Name string `json:"name"`
|
||||
CNIVersion string `json:"cniVersion"`
|
||||
Type string `json:"type,omitempty"`
|
||||
IPMasq bool `json:"ipMasq"`
|
||||
MTU int `json:"mtu"`
|
||||
IPAM *allocator.IPAMConfig `json:"ipam"`
|
||||
DNS types.DNS `json:"dns"`
|
||||
RawPrevResult map[string]interface{} `json:"prevResult,omitempty"`
|
||||
PrevResult current.Result `json:"-"`
|
||||
}
|
||||
|
||||
func buildOneConfig(netName string, cniVersion string, orig *Net, prevResult types.Result) (*Net, error) {
|
||||
var err error
|
||||
|
||||
inject := map[string]interface{}{
|
||||
"name": netName,
|
||||
"cniVersion": cniVersion,
|
||||
}
|
||||
// Add previous plugin result
|
||||
if prevResult != nil {
|
||||
inject["prevResult"] = prevResult
|
||||
}
|
||||
|
||||
// Ensure every config uses the same name and version
|
||||
config := make(map[string]interface{})
|
||||
|
||||
confBytes, err := json.Marshal(orig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(confBytes, &config)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unmarshal existing network bytes: %s", err)
|
||||
}
|
||||
|
||||
for key, value := range inject {
|
||||
config[key] = value
|
||||
}
|
||||
|
||||
newBytes, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
conf := &Net{}
|
||||
if err := json.Unmarshal(newBytes, &conf); err != nil {
|
||||
return nil, fmt.Errorf("error parsing configuration: %s", err)
|
||||
}
|
||||
|
||||
return conf, nil
|
||||
|
||||
}
|
||||
|
||||
var _ = Describe("ptp Operations", func() {
|
||||
var originalNS ns.NetNS
|
||||
|
||||
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())
|
||||
})
|
||||
|
||||
@ -46,7 +103,7 @@ var _ = Describe("ptp Operations", func() {
|
||||
doTest := func(conf string, numIPs int) {
|
||||
const IFNAME = "ptp0"
|
||||
|
||||
targetNs, err := ns.NewNS()
|
||||
targetNs, err := testutils.NewNS()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
defer targetNs.Close()
|
||||
|
||||
@ -64,7 +121,7 @@ var _ = Describe("ptp Operations", func() {
|
||||
err = originalNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
resI, _, err = testutils.CmdAddWithResult(targetNs.Path(), IFNAME, []byte(conf), func() error {
|
||||
resI, _, err = testutils.CmdAddWithArgs(args, func() error {
|
||||
return cmdAdd(args)
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
@ -78,12 +135,14 @@ var _ = Describe("ptp Operations", func() {
|
||||
// Make sure ptp link exists in the target namespace
|
||||
// Then, ping the gateway
|
||||
seenIPs := 0
|
||||
|
||||
wantMac := ""
|
||||
err = targetNs.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
link, err := netlink.LinkByName(IFNAME)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(link.Attrs().Name).To(Equal(IFNAME))
|
||||
wantMac = link.Attrs().HardwareAddr.String()
|
||||
|
||||
for _, ipc := range res.IPs {
|
||||
if *ipc.Interface != 1 {
|
||||
@ -105,11 +164,149 @@ var _ = Describe("ptp Operations", func() {
|
||||
|
||||
Expect(seenIPs).To(Equal(numIPs))
|
||||
|
||||
// make sure the interfaces are correct
|
||||
Expect(res.Interfaces).To(HaveLen(2))
|
||||
|
||||
Expect(res.Interfaces[0].Name).To(HavePrefix("veth"))
|
||||
Expect(res.Interfaces[0].Mac).To(HaveLen(17))
|
||||
Expect(res.Interfaces[0].Sandbox).To(BeEmpty())
|
||||
|
||||
Expect(res.Interfaces[1].Name).To(Equal(IFNAME))
|
||||
Expect(res.Interfaces[1].Mac).To(Equal(wantMac))
|
||||
Expect(res.Interfaces[1].Sandbox).To(Equal(targetNs.Path()))
|
||||
|
||||
// Call the plugins with the DEL command, deleting the veth endpoints
|
||||
err = originalNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
err := testutils.CmdDelWithResult(targetNs.Path(), IFNAME, func() error {
|
||||
err := testutils.CmdDelWithArgs(args, func() error {
|
||||
return cmdDel(args)
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
return nil
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Make sure ptp link has been deleted
|
||||
err = targetNs.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
link, err := netlink.LinkByName(IFNAME)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(link).To(BeNil())
|
||||
return nil
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
}
|
||||
|
||||
doTestv4 := func(conf string, netName string, numIPs int) {
|
||||
const IFNAME = "ptp0"
|
||||
|
||||
targetNs, err := testutils.NewNS()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
defer targetNs.Close()
|
||||
|
||||
args := &skel.CmdArgs{
|
||||
ContainerID: "dummy",
|
||||
Netns: targetNs.Path(),
|
||||
IfName: IFNAME,
|
||||
StdinData: []byte(conf),
|
||||
}
|
||||
|
||||
var resI types.Result
|
||||
var res *current.Result
|
||||
|
||||
// Execute the plugin with the ADD command, creating the veth endpoints
|
||||
err = originalNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
resI, _, err = testutils.CmdAddWithArgs(args, func() error {
|
||||
return cmdAdd(args)
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
return nil
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
res, err = current.NewResultFromResult(resI)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Make sure ptp link exists in the target namespace
|
||||
// Then, ping the gateway
|
||||
seenIPs := 0
|
||||
|
||||
wantMac := ""
|
||||
err = targetNs.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
link, err := netlink.LinkByName(IFNAME)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
wantMac = link.Attrs().HardwareAddr.String()
|
||||
|
||||
for _, ipc := range res.IPs {
|
||||
if *ipc.Interface != 1 {
|
||||
continue
|
||||
}
|
||||
seenIPs += 1
|
||||
saddr := ipc.Address.IP.String()
|
||||
daddr := ipc.Gateway.String()
|
||||
fmt.Fprintln(GinkgoWriter, "ping", saddr, "->", daddr)
|
||||
|
||||
if err := testutils.Ping(saddr, daddr, (ipc.Version == "6"), 30); err != nil {
|
||||
return fmt.Errorf("ping %s -> %s failed: %s", saddr, daddr, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
Expect(seenIPs).To(Equal(numIPs))
|
||||
|
||||
// make sure the interfaces are correct
|
||||
Expect(res.Interfaces).To(HaveLen(2))
|
||||
|
||||
Expect(res.Interfaces[0].Name).To(HavePrefix("veth"))
|
||||
Expect(res.Interfaces[0].Mac).To(HaveLen(17))
|
||||
Expect(res.Interfaces[0].Sandbox).To(BeEmpty())
|
||||
|
||||
Expect(res.Interfaces[1].Name).To(Equal(IFNAME))
|
||||
Expect(res.Interfaces[1].Mac).To(Equal(wantMac))
|
||||
Expect(res.Interfaces[1].Sandbox).To(Equal(targetNs.Path()))
|
||||
|
||||
// call CmdCheck
|
||||
n := &Net{}
|
||||
err = json.Unmarshal([]byte(conf), &n)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
n.IPAM, _, err = allocator.LoadIPAMConfig([]byte(conf), "")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
cniVersion := "0.4.0"
|
||||
newConf, err := buildOneConfig(netName, cniVersion, n, res)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
confString, err := json.Marshal(newConf)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
args.StdinData = confString
|
||||
|
||||
// CNI Check host-device in the target namespace
|
||||
err = originalNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
var err error
|
||||
err = testutils.CmdCheckWithArgs(args, func() error { return cmdCheck(args) })
|
||||
return err
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
args.StdinData = []byte(conf)
|
||||
|
||||
// Call the plugins with the DEL command, deleting the veth endpoints
|
||||
err = originalNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
err := testutils.CmdDelWithArgs(args, func() error {
|
||||
return cmdDel(args)
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
@ -179,7 +376,7 @@ var _ = Describe("ptp Operations", func() {
|
||||
}
|
||||
}`
|
||||
|
||||
targetNs, err := ns.NewNS()
|
||||
targetNs, err := testutils.NewNS()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
defer targetNs.Close()
|
||||
|
||||
@ -194,7 +391,7 @@ var _ = Describe("ptp Operations", func() {
|
||||
err = originalNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
err := testutils.CmdDelWithResult(targetNs.Path(), IFNAME, func() error {
|
||||
err := testutils.CmdDelWithArgs(args, func() error {
|
||||
return cmdDel(args)
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
@ -202,4 +399,39 @@ var _ = Describe("ptp Operations", func() {
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
|
||||
It("configures and deconfigures a CNI V4 ptp link with ADD/DEL", func() {
|
||||
conf := `{
|
||||
"cniVersion": "0.4.0",
|
||||
"name": "ptpNetv4",
|
||||
"type": "ptp",
|
||||
"ipMasq": true,
|
||||
"mtu": 5000,
|
||||
"ipam": {
|
||||
"type": "host-local",
|
||||
"subnet": "10.1.2.0/24"
|
||||
}
|
||||
}`
|
||||
|
||||
doTestv4(conf, "ptpNetv4", 1)
|
||||
})
|
||||
|
||||
It("configures and deconfigures a CNI V4 dual-stack ptp link with ADD/DEL", func() {
|
||||
conf := `{
|
||||
"cniVersion": "0.4.0",
|
||||
"name": "ptpNetv4ds",
|
||||
"type": "ptp",
|
||||
"ipMasq": true,
|
||||
"mtu": 5000,
|
||||
"ipam": {
|
||||
"type": "host-local",
|
||||
"ranges": [
|
||||
[{ "subnet": "10.1.2.0/24"}],
|
||||
[{ "subnet": "2001:db8:1::0/66"}]
|
||||
]
|
||||
}
|
||||
}`
|
||||
|
||||
doTestv4(conf, "ptpNetv4ds", 2)
|
||||
})
|
||||
})
|
||||
|
@ -20,14 +20,17 @@ import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
|
||||
"github.com/vishvananda/netlink"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/skel"
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
"github.com/containernetworking/cni/pkg/types/current"
|
||||
"github.com/containernetworking/cni/pkg/version"
|
||||
|
||||
"github.com/containernetworking/plugins/pkg/ip"
|
||||
"github.com/containernetworking/plugins/pkg/ipam"
|
||||
"github.com/containernetworking/plugins/pkg/ns"
|
||||
"github.com/vishvananda/netlink"
|
||||
bv "github.com/containernetworking/plugins/pkg/utils/buildversion"
|
||||
)
|
||||
|
||||
type NetConf struct {
|
||||
@ -137,6 +140,14 @@ func cmdAdd(args *skel.CmdArgs) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Invoke ipam del if err to avoid ip leak
|
||||
defer func() {
|
||||
if err != nil {
|
||||
ipam.ExecDel(n.IPAM.Type, args.StdinData)
|
||||
}
|
||||
}()
|
||||
|
||||
// Convert whatever the IPAM result was into the current Result type
|
||||
result, err := current.NewResultFromResult(r)
|
||||
if err != nil {
|
||||
@ -192,5 +203,130 @@ func cmdDel(args *skel.CmdArgs) error {
|
||||
}
|
||||
|
||||
func main() {
|
||||
skel.PluginMain(cmdAdd, cmdDel, version.All)
|
||||
skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.All, bv.BuildString("vlan"))
|
||||
}
|
||||
|
||||
func cmdCheck(args *skel.CmdArgs) error {
|
||||
conf := NetConf{}
|
||||
if err := json.Unmarshal(args.StdinData, &conf); err != nil {
|
||||
return fmt.Errorf("failed to load netconf: %v", err)
|
||||
}
|
||||
|
||||
netns, err := ns.GetNS(args.Netns)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open netns %q: %v", args.Netns, err)
|
||||
}
|
||||
defer netns.Close()
|
||||
|
||||
// run the IPAM plugin and get back the config to apply
|
||||
err = ipam.ExecCheck(conf.IPAM.Type, args.StdinData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if conf.NetConf.RawPrevResult == nil {
|
||||
return fmt.Errorf("ptp: Required prevResult missing")
|
||||
}
|
||||
if err := version.ParsePrevResult(&conf.NetConf); err != nil {
|
||||
return err
|
||||
}
|
||||
// Convert whatever the IPAM result was into the current Result type
|
||||
result, err := current.NewResultFromResult(conf.PrevResult)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var contMap current.Interface
|
||||
// Find interfaces for name whe know, that of host-device inside container
|
||||
for _, intf := range result.Interfaces {
|
||||
if args.IfName == intf.Name {
|
||||
if args.Netns == intf.Sandbox {
|
||||
contMap = *intf
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The namespace must be the same as what was configured
|
||||
if args.Netns != contMap.Sandbox {
|
||||
return fmt.Errorf("Sandbox in prevResult %s doesn't match configured netns: %s",
|
||||
contMap.Sandbox, args.Netns)
|
||||
}
|
||||
|
||||
m, err := netlink.LinkByName(conf.Master)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to lookup master %q: %v", conf.Master, err)
|
||||
}
|
||||
|
||||
//
|
||||
// Check prevResults for ips, routes and dns against values found in the container
|
||||
if err := netns.Do(func(_ ns.NetNS) error {
|
||||
|
||||
// Check interface against values found in the container
|
||||
err := validateCniContainerInterface(contMap, m.Attrs().Index, conf.VlanId, conf.MTU)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = ip.ValidateExpectedInterfaceIPs(args.IfName, result.IPs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = ip.ValidateExpectedRoute(result.Routes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateCniContainerInterface(intf current.Interface, masterIndex int, vlanId int, mtu int) error {
|
||||
|
||||
var link netlink.Link
|
||||
var err error
|
||||
|
||||
if intf.Name == "" {
|
||||
return fmt.Errorf("Container interface name missing in prevResult: %v", intf.Name)
|
||||
}
|
||||
link, err = netlink.LinkByName(intf.Name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ptp: Container Interface name in prevResult: %s not found", intf.Name)
|
||||
}
|
||||
if intf.Sandbox == "" {
|
||||
return fmt.Errorf("ptp: Error: Container interface %s should not be in host namespace", link.Attrs().Name)
|
||||
}
|
||||
|
||||
vlan, isVlan := link.(*netlink.Vlan)
|
||||
if !isVlan {
|
||||
return fmt.Errorf("Error: Container interface %s not of type vlan", link.Attrs().Name)
|
||||
}
|
||||
|
||||
// TODO This works when unit testing via cnitool; fails with ./test.sh
|
||||
//if masterIndex != vlan.Attrs().ParentIndex {
|
||||
// return fmt.Errorf("Container vlan Master %d does not match expected value: %d", vlan.Attrs().ParentIndex, masterIndex)
|
||||
//}
|
||||
|
||||
if intf.Mac != "" {
|
||||
if intf.Mac != link.Attrs().HardwareAddr.String() {
|
||||
return fmt.Errorf("vlan: Interface %s Mac %s doesn't match container Mac: %s", intf.Name, intf.Mac, link.Attrs().HardwareAddr)
|
||||
}
|
||||
}
|
||||
|
||||
if vlanId != vlan.VlanId {
|
||||
return fmt.Errorf("Error: Tuning link %s configured promisc is %v, current value is %d",
|
||||
intf.Name, vlanId, vlan.VlanId)
|
||||
}
|
||||
|
||||
if mtu != 0 {
|
||||
if mtu != link.Attrs().MTU {
|
||||
return fmt.Errorf("Error: Tuning configured MTU of %s is %d, current value is %d",
|
||||
intf.Name, mtu, link.Attrs().MTU)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -23,5 +23,5 @@ import (
|
||||
|
||||
func TestVlan(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "vlan Suite")
|
||||
RunSpecs(t, "plugins/main/vlan")
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user