Compare commits
618 Commits
v0.3.0-rc1
...
v0.7.1
Author | SHA1 | Date | |
---|---|---|---|
72b62babee | |||
26dafaa097 | |||
13e6a4b2ba | |||
00b072dd0b | |||
dd8ff8a5cf | |||
08ec2995ba | |||
136399f078 | |||
596480eadc | |||
61c3c42107 | |||
b522ed6aae | |||
26ef6e312d | |||
461d433911 | |||
84a01001be | |||
e2f063b534 | |||
9e5836047c | |||
808d4e20ae | |||
3468364f7e | |||
412b6d3128 | |||
9604565b22 | |||
2a0736c748 | |||
2eba56ad52 | |||
d228f980e1 | |||
6aa21c431e | |||
c42470bc79 | |||
ffc591e242 | |||
59f9976017 | |||
c26961a990 | |||
8ebea58550 | |||
97664d8a6a | |||
5c7e7c0913 | |||
b03d23a4fa | |||
03e316b07b | |||
ecdd827d3a | |||
1f02326d56 | |||
92c634042c | |||
2c05055101 | |||
5e830efb20 | |||
73fdc87395 | |||
d07d2aaf71 | |||
6c2ef734c2 | |||
47668f6d64 | |||
fbced0cccb | |||
99f6be0319 | |||
b09e0d28a7 | |||
5576f3120e | |||
449700f7ea | |||
4779f1d2bf | |||
7f98c94613 | |||
596b44301b | |||
0063a1b9d0 | |||
cc71426592 | |||
e256564546 | |||
c238c93b5e | |||
25ca6ccb52 | |||
5e46a66c89 | |||
6be2e8a0e2 | |||
b24225fc17 | |||
d8f2fd7a3c | |||
1396ab0bab | |||
92babd4a3d | |||
7a62515407 | |||
008024125a | |||
556e509097 | |||
dda9c2b1b0 | |||
0e3df2961c | |||
e1ea7f5ecb | |||
8fe8460c72 | |||
92e62b9f4d | |||
2f957864ea | |||
b49379d284 | |||
9769434a13 | |||
a124fb36e6 | |||
9fb22524a1 | |||
2d7d680874 | |||
7480240de9 | |||
27d027a6d3 | |||
5544d9ced0 | |||
998a0f6c6a | |||
20bc33abc5 | |||
bde6140771 | |||
a714098daf | |||
409be3d304 | |||
3b41be3784 | |||
b7b0e6c1d4 | |||
3745ee2d3f | |||
56d8984bbc | |||
e7328869fa | |||
3c9f1b16d6 | |||
0b9dbef185 | |||
55cdf255db | |||
ba6a709062 | |||
9d08b04a3a | |||
a690f775ae | |||
15f6798bf9 | |||
74054da7c5 | |||
7716c027cf | |||
3daee3214d | |||
f8134be6ef | |||
0da6780449 | |||
8185e7c59c | |||
ca3f28fa9e | |||
121362601b | |||
5a7a8b3069 | |||
f01230d75f | |||
ffdc748861 | |||
8c5e754617 | |||
5bbff37294 | |||
485f151536 | |||
494053dd98 | |||
83ec9bb73f | |||
e2558a03bb | |||
a38dd5f7a0 | |||
45650feb32 | |||
2e9e87732f | |||
7cda9af13f | |||
9c7aeff02c | |||
98cafb157c | |||
e76165b44f | |||
94e6489466 | |||
9a96aa2981 | |||
a860d4d244 | |||
6f45630cc1 | |||
e8bea554c5 | |||
43a24515ee | |||
0997c532a1 | |||
a7aaf0e377 | |||
22cda76afb | |||
b47d5b7c0a | |||
539174f8c8 | |||
8816992fcb | |||
4e2f467e21 | |||
3c436520be | |||
8b43e90949 | |||
dcf7368eea | |||
0c1c1b7d8c | |||
f5e4c117d1 | |||
c9e3fc4890 | |||
9f52fc0374 | |||
3ac78df0b6 | |||
db8f94a509 | |||
1430c43243 | |||
f130db5b61 | |||
d2792f264e | |||
4e0349a7e7 | |||
4af68521cd | |||
7cab2fd317 | |||
be5f1f723e | |||
8c27b64f13 | |||
af9127b7ea | |||
1a9288c3c0 | |||
e03c76b852 | |||
efe4f37afa | |||
c44a76ddd3 | |||
b686cad062 | |||
5e3be5a44f | |||
1e3715157c | |||
a11be4d759 | |||
f197c01b62 | |||
3cd4e26aeb | |||
27a5b994c2 | |||
f2faf549b4 | |||
231f6c4c53 | |||
a6c96acda4 | |||
6099d8c84c | |||
c11286b835 | |||
010c8a9503 | |||
73a2d131ee | |||
21d96393ea | |||
8eddd61fed | |||
0de29de33e | |||
87fbabeeef | |||
6ee2048ec2 | |||
279cae5ec5 | |||
43c11dda15 | |||
076080eedf | |||
6b3bf7ea7b | |||
7309649a66 | |||
9ef6f5f723 | |||
4a2f3ddd5d | |||
392e1bbd10 | |||
5bb59a12b8 | |||
137b4975ec | |||
7d3fe6923b | |||
382686ed63 | |||
d427963784 | |||
5b0d6a0c8f | |||
361ec957fb | |||
29ba54c045 | |||
8447ebc83a | |||
9445711fa5 | |||
d4bbce1865 | |||
ff92701693 | |||
a39d6845fa | |||
d66edc3180 | |||
aaba1b0224 | |||
470e86eb8b | |||
73c6616946 | |||
61f3b4b315 | |||
0799f5732f | |||
58b78e72fd | |||
13824487c6 | |||
699380d687 | |||
74d4cbed76 | |||
b87126377a | |||
c235b448d4 | |||
c1c7826922 | |||
1b9caefba5 | |||
bb322572eb | |||
f51dd618c5 | |||
559ffa1111 | |||
168a5a0210 | |||
d62b9a0584 | |||
71d96ea91c | |||
e4a0583d7a | |||
fb4a7f477b | |||
1b65890795 | |||
b07d768f12 | |||
58c834c4f3 | |||
4f36e5994e | |||
5d5b16f0b3 | |||
4ce9b019aa | |||
ac50624393 | |||
2678be24d0 | |||
474ba16901 | |||
1f6efc28b5 | |||
75a5cf1bf0 | |||
1003822e53 | |||
9b8a0e0c64 | |||
71c4a60741 | |||
6008de1ffd | |||
c806f3d62b | |||
09e5feae1f | |||
0b1b85413e | |||
7d8c23dd59 | |||
d6d012b88f | |||
1f4a8c2802 | |||
fc174a6635 | |||
2f1bfcee63 | |||
35153e7de8 | |||
2a6d12b206 | |||
5a984f0dee | |||
94e02e0768 | |||
f15d521b2a | |||
bbc23ea761 | |||
5124ac4759 | |||
b6e9bf8372 | |||
2aa863dab7 | |||
379d47760c | |||
1061ef1eb9 | |||
119d6f20f5 | |||
bdd70561ad | |||
70c153eb41 | |||
5b6d955695 | |||
fa53446854 | |||
f658c0b4b3 | |||
328ea56e75 | |||
6e6ad53ea8 | |||
1bc8141105 | |||
460e1c5be3 | |||
7d5619d277 | |||
7eeb21a999 | |||
46deb7b708 | |||
0dc0845c51 | |||
aafc728ec0 | |||
32246b315c | |||
2c33eb4f22 | |||
9f03c4bc2e | |||
7fa16f18d9 | |||
c783679114 | |||
dff808c98e | |||
c3cde44480 | |||
3ba89873ac | |||
98eb0d93a1 | |||
eb2ff7fe57 | |||
31dafa2f7a | |||
6f3b0abcc7 | |||
e3da6e1cfb | |||
95ca9d393a | |||
3fbf94a90a | |||
970de4b9e3 | |||
f85ac247e6 | |||
a35f8ae914 | |||
271be6c8df | |||
fc7de21fe2 | |||
2e90a8cc33 | |||
4c0b8f7054 | |||
162b73e44f | |||
246300ccfe | |||
e663e98444 | |||
46e17b26ff | |||
0d9a1d70f8 | |||
c4271dba67 | |||
06b397912b | |||
d5acb127b8 | |||
b89b56dc97 | |||
befad17174 | |||
e6adfe939a | |||
b0b896f79a | |||
0d19b79260 | |||
ad2a5ccb61 | |||
dae1177b53 | |||
befb95977c | |||
cb4cd0e12c | |||
dcadf4f9b5 | |||
2515b8c712 | |||
af5c2f303a | |||
1acc927ab0 | |||
04f357344d | |||
e476ab7189 | |||
13534e505c | |||
0c2a034f01 | |||
7e16c86a52 | |||
947b79e334 | |||
0a1b48f520 | |||
d716d73444 | |||
530dd71e19 | |||
fa58dd8889 | |||
20fde00ea6 | |||
cd16252299 | |||
5cde14cd7b | |||
4406607649 | |||
f60111b093 | |||
98ff61aac3 | |||
6dc01a772b | |||
e3bcc3e41b | |||
bc1d61e3db | |||
652bae1deb | |||
47f6604b5a | |||
1c1cf2faf7 | |||
ef00f717a6 | |||
06fd1369aa | |||
5852c60bc4 | |||
20ae9e156d | |||
76028d7f8c | |||
2ba6d20007 | |||
accac60053 | |||
b0808a006a | |||
a85593ce0d | |||
de002ff25a | |||
475581873d | |||
6eac0ee904 | |||
46e3a6b7be | |||
44ef109483 | |||
c20f895fa8 | |||
ba6c5af5dc | |||
15de81eac6 | |||
8b827d74b3 | |||
48d3b46eb2 | |||
7b2006eed8 | |||
e085226f82 | |||
ffbf708406 | |||
e6113c6517 | |||
79643855f7 | |||
f0daefa63d | |||
9201f3f1d9 | |||
2ad1d04561 | |||
fa264e6e36 | |||
b463642ac0 | |||
61d3f4515a | |||
8c6f6e0a60 | |||
0e09ad29df | |||
bb8bcf08a8 | |||
194f8ce6ea | |||
17b2a6d5e2 | |||
6e77c7f4f7 | |||
bff05e7572 | |||
cfb2ff3dcd | |||
8be31e1c6f | |||
3450f08611 | |||
c24fce2ae8 | |||
d872391998 | |||
0ee270c769 | |||
85ab5c0f21 | |||
af88b7c6cf | |||
165ca6f719 | |||
0087f302ea | |||
b2ef4aa1a7 | |||
b07b226c97 | |||
51b51a0182 | |||
a29fc24f11 | |||
b5e0a4ef2e | |||
52e4358cbd | |||
e70e6034d2 | |||
f721eaf6ed | |||
5b696f3307 | |||
0135e2751e | |||
04ba7c7761 | |||
fba37620e0 | |||
1e4e8fdb5d | |||
64bbcd8cf7 | |||
75d983ec25 | |||
921292ff77 | |||
fd150a4c97 | |||
56032390fe | |||
3f4011b3ec | |||
d32ccb1899 | |||
de91f31ae7 | |||
7958b9f0cc | |||
bf6948da19 | |||
d5e2e375d4 | |||
07a8a28637 | |||
95a9ea0bd2 | |||
959af1e6ab | |||
dfc4f7cd2a | |||
2f97407396 | |||
bf31ed1591 | |||
f467134838 | |||
97192fc979 | |||
2142c076ab | |||
deb4466041 | |||
bd3ade0c54 | |||
dea1c6e44d | |||
383c84031e | |||
adf28a84c6 | |||
b1e254d901 | |||
536cb5b99b | |||
c5e39a87f7 | |||
63ace9e496 | |||
e0ea82b229 | |||
26241a517a | |||
7281d5792a | |||
1ce5e54e99 | |||
4d77bc177c | |||
31ef82276a | |||
726c6b4578 | |||
d3ecadb860 | |||
0e675fa14f | |||
9d5e6e60e7 | |||
39f8436a8b | |||
21d25959ee | |||
fc58b79b45 | |||
488db88fd1 | |||
787d59ab5d | |||
415d707faf | |||
349d66d51c | |||
5a67f5d6fb | |||
60bb795c2e | |||
113dfd61bd | |||
3c1d4581c4 | |||
ad8a052dd3 | |||
8cb3a9323a | |||
7c579af789 | |||
08529639f4 | |||
ee64ac74bc | |||
d09b18dac4 | |||
054fa9e42d | |||
f5ead7969a | |||
22a0ddc906 | |||
c0ac3913a1 | |||
35ce5d68e0 | |||
c1ff202179 | |||
5a52316ab5 | |||
00f27577d5 | |||
f68cea27bc | |||
55fd81f775 | |||
c317888ac5 | |||
7d19c01818 | |||
1a04d9abba | |||
8e1c215116 | |||
b6137de993 | |||
924b30b57d | |||
c3e8563e77 | |||
3a1354cff6 | |||
7ea07a6405 | |||
96867eae05 | |||
75f1fa2260 | |||
1c1424c472 | |||
c19b82efc2 | |||
b2e53181d1 | |||
39a8fe4766 | |||
c657c61767 | |||
424a8b38cf | |||
04fbde4ce8 | |||
dfd2c142ae | |||
7bc96f05bb | |||
0506bae8fc | |||
bacdc3668d | |||
6496749565 | |||
5835c2bbb1 | |||
7f098f7c4a | |||
728071e7d1 | |||
a2aff8c6a8 | |||
f484990a00 | |||
30c99d17cc | |||
63755d7c57 | |||
eda79f7645 | |||
f436418525 | |||
f2b6ec0375 | |||
c17e700759 | |||
3c43314926 | |||
791d259e55 | |||
cd14fc0e06 | |||
8a93673209 | |||
0c2b5f5139 | |||
b8e92ed030 | |||
7b381a1af2 | |||
4666a24b6d | |||
bd341ab5d1 | |||
0136a73037 | |||
3bee295ba5 | |||
d48cb53715 | |||
3476000725 | |||
17dcf87589 | |||
c3c0803817 | |||
0ffad38994 | |||
efc0f2987f | |||
031567c216 | |||
c6864e1060 | |||
2445a960a9 | |||
5c3c171642 | |||
4a292cd451 | |||
f82af22b10 | |||
4f9faf60bb | |||
f44f4cf953 | |||
ddc0ca4791 | |||
e90d8c12b1 | |||
ffff8ac2fd | |||
3a4124d257 | |||
35f3a090b2 | |||
131ecc4055 | |||
cfedcfd8f2 | |||
d582c9ce8f | |||
5fe036b88a | |||
72337159c1 | |||
d7de8d4f98 | |||
7f90f9d559 | |||
d6674e0f49 | |||
3b5d7a3bb0 | |||
572fcc7076 | |||
6f63d9d707 | |||
2d47b0396d | |||
3bab8a2805 | |||
ea073fc4c7 | |||
f64cfb84f8 | |||
39eace3c38 | |||
a8d1029294 | |||
40440ec887 | |||
8814631f80 | |||
fce81fbf78 | |||
f2922c5c1f | |||
c019ed25d2 | |||
3738692372 | |||
0bd4bafcda | |||
734bf1ba5b | |||
5de15979c6 | |||
5978cf8b88 | |||
b34210aa1c | |||
fc229c5090 | |||
5cbd217cbe | |||
19850efc3a | |||
5158edacef | |||
5260862f80 | |||
c77f7431fb | |||
bd7b83dd98 | |||
3358f16dd6 | |||
9079565e0d | |||
680d1f6ec2 | |||
c3a90e7682 | |||
f73261c431 | |||
c272c49555 | |||
d701ca6c82 | |||
c085ec98fd | |||
82851a860e | |||
5f757b6af7 | |||
4a79ac4cda | |||
e1bc41a42a | |||
11c0bfada8 | |||
2c1633f8e9 | |||
6c9b5a361a | |||
42b74b9eb5 | |||
9612d36615 | |||
d4e088aad4 | |||
f429750105 | |||
f96a716154 | |||
b4a2a1fa51 | |||
b87cf1ba9c | |||
dc44feb5b5 | |||
fc83c24eb5 | |||
8cc424c5aa | |||
ca978caee7 | |||
b99854d124 | |||
05683bf11d | |||
e2673df403 | |||
d2af995349 | |||
0b6ad716e8 | |||
3382b459a7 | |||
a08b015fa5 | |||
c7db40fb4c | |||
3bbc43f24d | |||
cf3c027960 | |||
6b58eda23a | |||
dd719a43ce | |||
4bee884fae | |||
4da892a669 | |||
8525fef466 | |||
8d3eb91a37 | |||
4c57a90da6 | |||
ae3ceedd69 | |||
09a8148e36 | |||
104e8b483e | |||
b5db5d3075 | |||
ac7cd562fd | |||
169f8e0a52 | |||
a75401a13b | |||
d1a0905345 | |||
a41e1a699e | |||
77a2e9727c | |||
f741bc324b | |||
507805f8c4 | |||
4f74b78041 | |||
6caffd48b3 | |||
58f6bc9c8b | |||
31c0dbf110 | |||
a6a822268c | |||
e59e2fd890 | |||
c7a1442830 | |||
93be8e63f1 | |||
c24708ff62 |
28
.appveyor.yml
Normal file
28
.appveyor.yml
Normal file
@ -0,0 +1,28 @@
|
||||
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"
|
||||
}
|
||||
}
|
||||
}
|
27
.gitignore
vendored
27
.gitignore
vendored
@ -1,3 +1,28 @@
|
||||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||
*.o
|
||||
*.a
|
||||
*.so
|
||||
|
||||
# Folders
|
||||
_obj
|
||||
_test
|
||||
|
||||
# Architecture specific extensions/prefixes
|
||||
*.[568vq]
|
||||
[568vq].out
|
||||
|
||||
*.cgo1.go
|
||||
*.cgo2.c
|
||||
_cgo_defun.c
|
||||
_cgo_gotypes.go
|
||||
_cgo_export.*
|
||||
|
||||
_testmain.go
|
||||
|
||||
*.exe
|
||||
*.test
|
||||
*.prof
|
||||
|
||||
bin/
|
||||
gopath/
|
||||
*.sw[ponm]
|
||||
.vagrant
|
||||
|
36
.travis.yml
36
.travis.yml
@ -3,27 +3,35 @@ sudo: required
|
||||
dist: trusty
|
||||
|
||||
go:
|
||||
- 1.5.3
|
||||
- 1.6
|
||||
- tip
|
||||
|
||||
matrix:
|
||||
allow_failures:
|
||||
- go: tip
|
||||
- 1.8.x
|
||||
- 1.9.x
|
||||
|
||||
env:
|
||||
global:
|
||||
- TOOLS_CMD=golang.org/x/tools/cmd
|
||||
- PATH=$GOROOT/bin:$PATH
|
||||
- GO15VENDOREXPERIMENT=1
|
||||
- PATH=$GOROOT/bin:$GOPATH/bin:$PATH
|
||||
matrix:
|
||||
- TARGET=amd64
|
||||
- TARGET=arm
|
||||
- TARGET=arm64
|
||||
- TARGET=ppc64le
|
||||
- TARGET=s390x
|
||||
|
||||
matrix:
|
||||
fast_finish: true
|
||||
|
||||
install:
|
||||
- go get ${TOOLS_CMD}/cover
|
||||
- go get github.com/modocache/gover
|
||||
- go get github.com/mattn/goveralls
|
||||
- go get github.com/onsi/ginkgo/ginkgo
|
||||
|
||||
script:
|
||||
- ./test
|
||||
- |
|
||||
if [ "${TARGET}" == "amd64" ]; then
|
||||
GOARCH="${TARGET}" ./test.sh
|
||||
else
|
||||
GOARCH="${TARGET}" ./build.sh
|
||||
fi
|
||||
|
||||
notifications:
|
||||
email: false
|
||||
|
||||
git:
|
||||
depth: 9999999
|
||||
|
@ -1,11 +1,11 @@
|
||||
# How to Contribute
|
||||
|
||||
cni is [Apache 2.0 licensed](LICENSE) and accepts contributions via GitHub
|
||||
CNI is [Apache 2.0 licensed](LICENSE) and accepts contributions via GitHub
|
||||
pull requests. This document outlines some of the conventions on development
|
||||
workflow, commit message formatting, contact points and other resources to make
|
||||
it easier to get your contribution accepted.
|
||||
|
||||
We welcome improvements to documentation as well as to code.
|
||||
We gratefully welcome improvements to documentation as well as to code.
|
||||
|
||||
# Certificate of Origin
|
||||
|
||||
@ -37,11 +37,57 @@ This is a rough outline of how to prepare a contribution:
|
||||
- Make commits of logical units.
|
||||
- Make sure your commit messages are in the proper format (see below).
|
||||
- Push your changes to a topic branch in your fork of the repository.
|
||||
- If you changed code, make sure the tests pass, and add any new tests as appropriate.
|
||||
- Make sure any new code files have a license header.
|
||||
- If you changed code:
|
||||
- add automated tests to cover your changes, using the [Ginkgo](http://onsi.github.io/ginkgo/) & [Gomega](http://onsi.github.io/gomega/) style
|
||||
- if the package did not previously have any test coverage, add it to the list
|
||||
of `TESTABLE` packages in the `test.sh` script.
|
||||
- run the full test script and ensure it passes
|
||||
- Make sure any new code files have a license header (this is now enforced by automated tests)
|
||||
- Submit a pull request to the original repository.
|
||||
|
||||
Thanks for your contributions!
|
||||
## How to run the test suite
|
||||
We generally require test coverage of any new features or bug fixes.
|
||||
|
||||
Here's how you can run the test suite on any system (even Mac or Windows) using
|
||||
[Vagrant](https://www.vagrantup.com/) and a hypervisor of your choice:
|
||||
|
||||
First, ensure that you have the [CNI repo](https://github.com/containernetworking/cni) and this repo (plugins) cloned side-by-side:
|
||||
```bash
|
||||
cd ~/workspace
|
||||
git clone https://github.com/containernetworking/cni
|
||||
git clone https://github.com/containernetworking/plugins
|
||||
```
|
||||
|
||||
Next, boot the virtual machine and SSH in to run the tests:
|
||||
|
||||
```bash
|
||||
vagrant up
|
||||
vagrant ssh
|
||||
# you're now in a shell in a virtual machine
|
||||
sudo su
|
||||
cd /go/src/github.com/containernetworking/plugins
|
||||
|
||||
# to run the full test suite
|
||||
./test.sh
|
||||
|
||||
# to focus on a particular test suite
|
||||
cd plugins/main/loopback
|
||||
go test
|
||||
```
|
||||
|
||||
# Acceptance policy
|
||||
|
||||
These things will make a PR more likely to be accepted:
|
||||
|
||||
* a well-described requirement
|
||||
* tests for new code
|
||||
* tests for old code!
|
||||
* new code and tests follow the conventions in old code and tests
|
||||
* a good commit message (see below)
|
||||
|
||||
In general, we will merge a PR once two maintainers have endorsed it.
|
||||
Trivial changes (e.g., corrections to spelling) may get waved through.
|
||||
For substantial changes, more people may become involved, and you might get asked to resubmit the PR or divide the changes into more than one PR.
|
||||
|
||||
### Format of the Commit Message
|
||||
|
||||
@ -72,3 +118,17 @@ The first line is the subject and should be no longer than 70 characters, the
|
||||
second line is always blank, and other lines should be wrapped at 80 characters.
|
||||
This allows the message to be easier to read on GitHub as well as in various
|
||||
git tools.
|
||||
|
||||
## 3rd party plugins
|
||||
So you've built a CNI plugin. Where should it live?
|
||||
|
||||
Short answer: We'd be happy to link to it from our [list of 3rd party plugins](README.md#3rd-party-plugins).
|
||||
But we'd rather you kept the code in your own repo.
|
||||
|
||||
Long answer: An advantage of the CNI model is that independent plugins can be
|
||||
built, distributed and used without any code changes to this repository. While
|
||||
some widely used plugins (and a few less-popular legacy ones) live in this repo,
|
||||
we're reluctant to add more.
|
||||
|
||||
If you have a good reason why the CNI maintainers should take custody of your
|
||||
plugin, please open an issue or PR.
|
||||
|
36
DCO
36
DCO
@ -1,36 +0,0 @@
|
||||
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.
|
@ -1,41 +0,0 @@
|
||||
# host-local plugin
|
||||
|
||||
## Overview
|
||||
|
||||
host-local IPAM plugin allocates IPv4 addresses out of a specified address range.
|
||||
It stores the state locally on the host filesystem, therefore ensuring uniqueness of IP addresses on a single host.
|
||||
|
||||
## Example configuration
|
||||
```
|
||||
{
|
||||
"ipam": {
|
||||
"type": "host-local",
|
||||
"subnet": "10.10.0.0/16",
|
||||
"rangeStart": "10.10.1.20",
|
||||
"rangeEnd": "10.10.3.50",
|
||||
"gateway": "10.10.0.254",
|
||||
"routes": [
|
||||
{ "dst": "0.0.0.0/0" },
|
||||
{ "dst": "192.168.0.0/16", "gw": "10.10.5.1" }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Network configuration reference
|
||||
|
||||
* `type` (string, required): "host-local".
|
||||
* `subnet` (string, required): CIDR block to allocate out of.
|
||||
* `rangeStart` (string, optional): IP inside of "subnet" from which to start allocating addresses. Defaults to ".2" IP inside of the "subnet" block.
|
||||
* `rangeEnd` (string, optional): IP inside of "subnet" with which to end allocating addresses. Defaults to ".254" IP inside of the "subnet" block.
|
||||
* `gateway` (string, optional): IP inside of "subnet" to designate as the gateway. Defaults to ".1" IP inside of the "subnet" block.
|
||||
* `routes` (string, optional): list of routes to add to the container namespace. Each route is a dictionary with "dst" and optional "gw" fields. If "gw" is omitted, value of "gateway" will be used.
|
||||
|
||||
## Supported arguments
|
||||
The following [CNI_ARGS](https://github.com/containernetworking/cni/blob/master/SPEC.md#parameters) are supported:
|
||||
|
||||
* `ip`: request a specific IP address from the subnet. If it's not available, the plugin will exit with an error
|
||||
|
||||
## Files
|
||||
|
||||
Allocated IP addresses are stored as files in /var/lib/cni/networks/$NETWORK_NAME.
|
88
Godeps/Godeps.json
generated
88
Godeps/Godeps.json
generated
@ -1,14 +1,54 @@
|
||||
{
|
||||
"ImportPath": "github.com/containernetworking/cni",
|
||||
"GoVersion": "go1.6",
|
||||
"ImportPath": "github.com/containernetworking/plugins",
|
||||
"GoVersion": "go1.7",
|
||||
"GodepVersion": "v79",
|
||||
"Packages": [
|
||||
"./..."
|
||||
],
|
||||
"Deps": [
|
||||
{
|
||||
"ImportPath": "github.com/alexflint/go-filemutex",
|
||||
"Rev": "72bdc8eae2aef913234599b837f5dda445ca9bd9"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/containernetworking/cni/libcni",
|
||||
"Comment": "v0.6.0-rc1",
|
||||
"Rev": "a2da8f8d7fd8e6dc25f336408a8ac86f050fbd88"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/containernetworking/cni/pkg/invoke",
|
||||
"Comment": "v0.6.0-rc1",
|
||||
"Rev": "a2da8f8d7fd8e6dc25f336408a8ac86f050fbd88"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/containernetworking/cni/pkg/skel",
|
||||
"Comment": "v0.6.0-rc1",
|
||||
"Rev": "a2da8f8d7fd8e6dc25f336408a8ac86f050fbd88"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/containernetworking/cni/pkg/types",
|
||||
"Comment": "v0.6.0-rc1",
|
||||
"Rev": "a2da8f8d7fd8e6dc25f336408a8ac86f050fbd88"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/containernetworking/cni/pkg/types/020",
|
||||
"Comment": "v0.6.0-rc1",
|
||||
"Rev": "a2da8f8d7fd8e6dc25f336408a8ac86f050fbd88"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/containernetworking/cni/pkg/types/current",
|
||||
"Comment": "v0.6.0-rc1",
|
||||
"Rev": "a2da8f8d7fd8e6dc25f336408a8ac86f050fbd88"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/containernetworking/cni/pkg/version",
|
||||
"Comment": "v0.6.0-rc1",
|
||||
"Rev": "a2da8f8d7fd8e6dc25f336408a8ac86f050fbd88"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/go-iptables/iptables",
|
||||
"Comment": "v0.1.0",
|
||||
"Rev": "fbb73372b87f6e89951c2b6b31470c2c9d5cfae3"
|
||||
"Comment": "v0.2.0",
|
||||
"Rev": "259c8e6a4275d497442c721fa52204d7a58bde8b"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/go-systemd/activation",
|
||||
@ -23,6 +63,19 @@
|
||||
"ImportPath": "github.com/d2g/dhcp4client",
|
||||
"Rev": "bed07e1bc5b85f69c6f0fd73393aa35ec68ed892"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/d2g/dhcp4server",
|
||||
"Rev": "1b74244053681c90de5cf1af3d6b5c93b74e3abb"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/j-keck/arping",
|
||||
"Rev": "2cf9dc699c5640a7e2c81403a44127bf28033600"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/mattn/go-shellwords",
|
||||
"Comment": "v1.0.3",
|
||||
"Rev": "02e3cf038dcea8290e44424da473dd12be796a8a"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/onsi/ginkgo",
|
||||
"Comment": "v1.2.0-29-g7f8ab55",
|
||||
@ -33,6 +86,11 @@
|
||||
"Comment": "v1.2.0-29-g7f8ab55",
|
||||
"Rev": "7f8ab55aaf3b86885aa55b762e803744d1674700"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/onsi/ginkgo/extensions/table",
|
||||
"Comment": "v1.2.0-29-g7f8ab55",
|
||||
"Rev": "7f8ab55aaf3b86885aa55b762e803744d1674700"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/onsi/ginkgo/internal/codelocation",
|
||||
"Comment": "v1.2.0-29-g7f8ab55",
|
||||
@ -170,15 +228,31 @@
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/vishvananda/netlink",
|
||||
"Rev": "ecf47fd5739b3d2c3daf7c89c4b9715a2605c21b"
|
||||
"Rev": "6e453822d85ef5721799774b654d4d02fed62afb"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/vishvananda/netlink/nl",
|
||||
"Rev": "ecf47fd5739b3d2c3daf7c89c4b9715a2605c21b"
|
||||
"Rev": "6e453822d85ef5721799774b654d4d02fed62afb"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/vishvananda/netns",
|
||||
"Rev": "54f0e4339ce73702a0607f49922aaa1e749b418d"
|
||||
},
|
||||
{
|
||||
"ImportPath": "golang.org/x/net/bpf",
|
||||
"Rev": "e90d6d0afc4c315a0d87a568ae68577cc15149a0"
|
||||
},
|
||||
{
|
||||
"ImportPath": "golang.org/x/net/internal/iana",
|
||||
"Rev": "e90d6d0afc4c315a0d87a568ae68577cc15149a0"
|
||||
},
|
||||
{
|
||||
"ImportPath": "golang.org/x/net/ipv4",
|
||||
"Rev": "e90d6d0afc4c315a0d87a568ae68577cc15149a0"
|
||||
},
|
||||
{
|
||||
"ImportPath": "golang.org/x/sys/unix",
|
||||
"Rev": "e11762ca30adc5b39fdbfd8c4250dabeb8e456d3"
|
||||
"Rev": "076b546753157f758b316e59bcb51e6807c04057"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
1
LICENSE
1
LICENSE
@ -199,4 +199,3 @@
|
||||
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.
|
||||
|
||||
|
@ -1,5 +0,0 @@
|
||||
Gabe Rosenhouse <grosenhouse@pivotal.io> (@rosenhouse)
|
||||
Michael Bridgen <michael@weave.works> (@squaremo)
|
||||
Stefan Junker <stefan.junker@coreos.com> (@steveeJ)
|
||||
Tom Denham <tom.denham@metaswitch.com> (@tomdee)
|
||||
Zach Gershman <zachgersh@gmail.com> (@zachgersh)
|
179
README.md
179
README.md
@ -1,163 +1,26 @@
|
||||
[](https://travis-ci.org/containernetworking/cni)
|
||||
[](https://coveralls.io/github/containernetworking/cni?branch=master)
|
||||
[](https://travis-ci.org/containernetworking/plugins)
|
||||
[](https://ci.appveyor.com/project/cni-bot/plugins/branch/master)
|
||||
|
||||
# CNI - the Container Network Interface
|
||||
# plugins
|
||||
Some CNI network plugins, maintained by the containernetworking team. For more information, see the individual READMEs.
|
||||
|
||||
## What is CNI?
|
||||
## 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
|
||||
* `ptp`: Creates a veth pair.
|
||||
* `vlan`: Allocates a vlan device.
|
||||
|
||||
The CNI (_Container Network Interface_) project consists of a specification and libraries for writing plugins to configure network interfaces in Linux containers, along with a number of supported plugins.
|
||||
CNI concerns itself only with network connectivity of containers and removing allocated resources when the container is deleted.
|
||||
Because of this focus CNI has a wide range of support and the specification is simple to implement.
|
||||
### 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
|
||||
|
||||
As well as the [specification](SPEC.md), this repository contains the Go source code of a library for integrating CNI into applications, an example command-line tool, a template for making new plugins, and the supported plugins.
|
||||
### Meta: other plugins
|
||||
* `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.
|
||||
|
||||
The template code makes it straight-forward to create a CNI plugin for an existing container networking project.
|
||||
CNI also makes a good framework for creating a new container networking project from scratch.
|
||||
|
||||
## Why develop CNI?
|
||||
|
||||
Application containers on Linux are a rapidly evolving area, and within this area networking is not well addressed as it is highly environment-specific.
|
||||
We believe that many container runtimes and orchestrators will seek to solve the same problem of making the network layer pluggable.
|
||||
|
||||
To avoid duplication, we think it is prudent to define a common interface between the network plugins and container execution: hence we put forward this specification, along with libraries for Go and a set of plugins.
|
||||
|
||||
## Who is using CNI?
|
||||
|
||||
- [rkt - container engine](https://coreos.com/blog/rkt-cni-networking.html)
|
||||
- [Kurma - container runtime](http://kurma.io/)
|
||||
- [Kubernetes - a system to simplify container operations](http://kubernetes.io/docs/admin/network-plugins/)
|
||||
- [Cloud Foundry - a platform for cloud applications](https://github.com/cloudfoundry-incubator/guardian-cni-adapter)
|
||||
- [Weave - a multi-host Docker network](https://github.com/weaveworks/weave)
|
||||
- [Project Calico - a layer 3 virtual network](https://github.com/projectcalico/calico-cni)
|
||||
- [Contiv Networking - policy networking for various use cases](https://github.com/contiv/netplugin)
|
||||
|
||||
## Contributing to CNI
|
||||
|
||||
We welcome contributions, including [bug reports](https://github.com/containernetworking/cni/issues), and code and documentation improvements.
|
||||
If you intend to contribute to code or documentation, please read [CONTRIBUTING.md](CONTRIBUTING.md). Also see the [contact section](#contact) in this README.
|
||||
|
||||
## How do I use CNI?
|
||||
|
||||
### Requirements
|
||||
|
||||
CNI requires Go 1.5+ to build.
|
||||
|
||||
Go 1.5 users will need to set GO15VENDOREXPERIMENT=1 to get vendored
|
||||
dependencies. This flag is set by default in 1.6.
|
||||
|
||||
### Included Plugins
|
||||
|
||||
This repository includes a number of common plugins in the `plugins/` directory.
|
||||
Please see the [Documentation/](Documentation/) directory for documentation about particular plugins.
|
||||
|
||||
### Running the plugins
|
||||
|
||||
The scripts/ directory contains two scripts, `priv-net-run.sh` and `docker-run.sh`, that can be used to exercise the plugins.
|
||||
|
||||
**note - priv-net-run.sh depends on `jq`**
|
||||
|
||||
Start out by creating a netconf file to describe a network:
|
||||
|
||||
```bash
|
||||
$ mkdir -p /etc/cni/net.d
|
||||
$ cat >/etc/cni/net.d/10-mynet.conf <<EOF
|
||||
{
|
||||
"name": "mynet",
|
||||
"type": "bridge",
|
||||
"bridge": "cni0",
|
||||
"isGateway": true,
|
||||
"ipMasq": true,
|
||||
"ipam": {
|
||||
"type": "host-local",
|
||||
"subnet": "10.22.0.0/16",
|
||||
"routes": [
|
||||
{ "dst": "0.0.0.0/0" }
|
||||
]
|
||||
}
|
||||
}
|
||||
EOF
|
||||
$ cat >/etc/cni/net.d/99-loopback.conf <<EOF
|
||||
{
|
||||
"type": "loopback"
|
||||
}
|
||||
EOF
|
||||
```
|
||||
|
||||
The directory `/etc/cni/net.d` is the default location in which the scripts will look for net configurations.
|
||||
|
||||
Next, build the plugins:
|
||||
|
||||
```bash
|
||||
$ ./build
|
||||
```
|
||||
|
||||
Finally, execute a command (`ifconfig` in this example) in a private network namespace that has joined the `mynet` network:
|
||||
|
||||
```bash
|
||||
$ CNI_PATH=`pwd`/bin
|
||||
$ cd scripts
|
||||
$ sudo CNI_PATH=$CNI_PATH ./priv-net-run.sh ifconfig
|
||||
eth0 Link encap:Ethernet HWaddr f2:c2:6f:54:b8:2b
|
||||
inet addr:10.22.0.2 Bcast:0.0.0.0 Mask:255.255.0.0
|
||||
inet6 addr: fe80::f0c2:6fff:fe54:b82b/64 Scope:Link
|
||||
UP BROADCAST MULTICAST MTU:1500 Metric:1
|
||||
RX packets:1 errors:0 dropped:0 overruns:0 frame:0
|
||||
TX packets:0 errors:0 dropped:1 overruns:0 carrier:0
|
||||
collisions:0 txqueuelen:0
|
||||
RX bytes:90 (90.0 B) TX bytes:0 (0.0 B)
|
||||
|
||||
lo Link encap:Local Loopback
|
||||
inet addr:127.0.0.1 Mask:255.0.0.0
|
||||
inet6 addr: ::1/128 Scope:Host
|
||||
UP LOOPBACK RUNNING MTU:65536 Metric:1
|
||||
RX packets:0 errors:0 dropped:0 overruns:0 frame:0
|
||||
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
|
||||
collisions:0 txqueuelen:0
|
||||
RX bytes:0 (0.0 B) TX bytes:0 (0.0 B)
|
||||
```
|
||||
|
||||
The environment variable `CNI_PATH` tells the scripts and library where to look for plugin executables.
|
||||
|
||||
## Running a Docker container with network namespace set up by CNI plugins
|
||||
|
||||
Use the instructions in the previous section to define a netconf and build the plugins.
|
||||
Next, docker-run.sh script wraps `docker run`, to execute the plugins prior to entering the container:
|
||||
|
||||
```bash
|
||||
$ CNI_PATH=`pwd`/bin
|
||||
$ cd scripts
|
||||
$ sudo CNI_PATH=$CNI_PATH ./docker-run.sh --rm busybox:latest ifconfig
|
||||
eth0 Link encap:Ethernet HWaddr fa:60:70:aa:07:d1
|
||||
inet addr:10.22.0.2 Bcast:0.0.0.0 Mask:255.255.0.0
|
||||
inet6 addr: fe80::f860:70ff:feaa:7d1/64 Scope:Link
|
||||
UP BROADCAST MULTICAST MTU:1500 Metric:1
|
||||
RX packets:1 errors:0 dropped:0 overruns:0 frame:0
|
||||
TX packets:0 errors:0 dropped:1 overruns:0 carrier:0
|
||||
collisions:0 txqueuelen:0
|
||||
RX bytes:90 (90.0 B) TX bytes:0 (0.0 B)
|
||||
|
||||
lo Link encap:Local Loopback
|
||||
inet addr:127.0.0.1 Mask:255.0.0.0
|
||||
inet6 addr: ::1/128 Scope:Host
|
||||
UP LOOPBACK RUNNING MTU:65536 Metric:1
|
||||
RX packets:0 errors:0 dropped:0 overruns:0 frame:0
|
||||
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
|
||||
collisions:0 txqueuelen:0
|
||||
RX bytes:0 (0.0 B) TX bytes:0 (0.0 B)
|
||||
```
|
||||
|
||||
## What might CNI do in the future?
|
||||
|
||||
CNI currently covers a wide range of needs for network configuration due to it simple model and API.
|
||||
However, in the future CNI might want to branch out into other directions:
|
||||
|
||||
- Dynamic updates to existing network configuration
|
||||
- Dynamic policies for network bandwidth and firewall rules
|
||||
|
||||
If these topics of are interest please contact the team via the mailing list or IRC and find some like minded people in the community to put a proposal together.
|
||||
|
||||
## Contact
|
||||
|
||||
For any questions about CNI, please reach out on the mailing list:
|
||||
- Email: [cni-dev](https://groups.google.com/forum/#!forum/cni-dev)
|
||||
- IRC: #[containernetworking](irc://irc.freenode.org:6667/#containernetworking) channel on freenode.org
|
||||
### Sample
|
||||
The sample plugin provides an example for building your own plugin.
|
||||
|
35
RELEASING.md
Normal file
35
RELEASING.md
Normal file
@ -0,0 +1,35 @@
|
||||
# Release process
|
||||
|
||||
## Resulting artifacts
|
||||
Creating a new release produces the following artifacts:
|
||||
|
||||
- Binaries (stored in the `release-<TAG>` directory) :
|
||||
- `cni-plugins-<PLATFORM>-<VERSION>.tgz` binaries
|
||||
- `cni-plugins-<VERSION>.tgz` binary (copy of amd64 platform binary)
|
||||
- `sha1`, `sha256` and `sha512` files for the above files.
|
||||
|
||||
## Preparing for a release
|
||||
1. Releases are performed by maintainers and should usually be discussed and planned at a maintainer meeting.
|
||||
- Choose the version number. It should be prefixed with `v`, e.g. `v1.2.3`
|
||||
- Take a quick scan through the PRs and issues to make sure there isn't anything crucial that _must_ be in the next release.
|
||||
- Create a draft of the release note
|
||||
- Discuss the level of testing that's needed and create a test plan if sensible
|
||||
- Check what version of `go` is used in the build container, updating it if there's a new stable release.
|
||||
- Update the vendor directory and Godeps to pin to the corresponding containernetworking/cni release. Create a PR, makes sure it passes CI and get it merged.
|
||||
|
||||
## Creating the release artifacts
|
||||
1. Make sure you are on the master branch and don't have any local uncommitted changes.
|
||||
1. Create a signed tag for the release `git tag -s $VERSION` (Ensure that GPG keys are created and added to GitHub)
|
||||
1. Run the release script from the root of the repository
|
||||
- `scripts/release.sh`
|
||||
- The script requires Docker and ensures that a consistent environment is used.
|
||||
- The artifacts will now be present in the `release-<TAG>` directory.
|
||||
1. Test these binaries according to the test plan.
|
||||
|
||||
## Publishing the release
|
||||
1. Push the tag to git `git push origin <TAG>`
|
||||
1. Create a release on Github, using the tag which was just pushed.
|
||||
1. Attach all the artifacts from the release directory.
|
||||
1. Add the release note to the release.
|
||||
1. Announce the release on at least the CNI mailing, IRC and Slack.
|
||||
|
257
SPEC.md
257
SPEC.md
@ -1,257 +0,0 @@
|
||||
# Container Networking Interface Proposal
|
||||
|
||||
## Overview
|
||||
|
||||
This document proposes a generic plugin-based networking solution for application containers on Linux, the _Container Networking Interface_, or _CNI_.
|
||||
It is derived from the [rkt Networking Proposal][rkt-networking-proposal], which aimed to satisfy many of the [design considerations][rkt-networking-design] for networking in [rkt][rkt-github].
|
||||
|
||||
For the purposes of this proposal, we define two terms very specifically:
|
||||
- _container_ can be considered synonymous with a [Linux _network namespace_][namespaces]. What unit this corresponds to depends on a particular container runtime implementation: for example, in implementations of the [App Container Spec][appc-github] like rkt, each _pod_ runs in a unique network namespace. In [Docker][docker], on the other hand, network namespaces generally exist for each separate Docker container.
|
||||
- _network_ refers to a group of entities that are uniquely addressable that can communicate amongst each other. This could be either an individual container (as specified above), a machine, or some other network device (e.g. a router). Containers can be conceptually _added to_ or _removed from_ one or more networks.
|
||||
|
||||
[rkt-networking-proposal]: https://docs.google.com/a/coreos.com/document/d/1PUeV68q9muEmkHmRuW10HQ6cHgd4819_67pIxDRVNlM/edit#heading=h.ievko3xsjwxd
|
||||
[rkt-networking-design]:
|
||||
https://docs.google.com/a/coreos.com/document/d/1CTAL4gwqRofjxyp4tTkbgHtAwb2YCcP14UEbHNizd8g
|
||||
[rkt-github]: https://github.com/coreos/rkt
|
||||
[namespaces]: http://man7.org/linux/man-pages/man7/namespaces.7.html
|
||||
[appc-github]: https://github.com/appc/spec
|
||||
[docker]: https://docker.com
|
||||
|
||||
## General considerations
|
||||
|
||||
The intention is for the container runtime to first create a new network namespace for the container.
|
||||
It then determines which networks this container should belong to and for each network, which plugin must be executed.
|
||||
The network configuration is in JSON format and can easily be stored in a file.
|
||||
The network configuration includes mandatory fields such as "name" and "type" as well as plugin (type) specific ones.
|
||||
The container runtime sequentially sets up the networks by executing the corresponding plugin for each network.
|
||||
Upon completion of the container lifecycle, the runtime executes the plugins in reverse order (relative to the order in which they were added) to disconnect them from the networks.
|
||||
|
||||
## CNI Plugin
|
||||
|
||||
### Overview
|
||||
|
||||
Each CNI plugin is implemented as an executable that is invoked by the container management system (e.g. rkt or Docker).
|
||||
|
||||
A CNI plugin is responsible for inserting a network interface into the container network namespace (e.g. one end of a veth pair) and making any necessary changes on the host (e.g. attaching other end of veth into a bridge).
|
||||
It should then assign the IP to the interface and setup the routes consistent with IP Address Management section by invoking appropriate IPAM plugin.
|
||||
|
||||
### Parameters
|
||||
|
||||
The operations that the CNI plugin needs to support are:
|
||||
|
||||
|
||||
- Add container to network
|
||||
- Parameters:
|
||||
- **Version**. The version of CNI spec that the caller is using (container management system or the invoking plugin).
|
||||
- **Container ID**. This is optional but recommended, and should be unique across an administrative domain while the container is live (it may be reused in the future). For example, an environment with an IPAM system may require that each container is allocated a unique ID and that each IP allocation can thus be correlated back to a particular container. As another example, in appc implementations this would be the _pod ID_.
|
||||
- **Network namespace path**. This represents the path to the network namespace to be added, i.e. /proc/[pid]/ns/net or a bind-mount/link to it.
|
||||
- **Network configuration**. This is a JSON document describing a network to which a container can be joined. The schema is described below.
|
||||
- **Extra arguments**. This allows granular configuration of CNI plugins on a per-container basis.
|
||||
- **Name of the interface inside the container**. This is the name that should be assigned to the interface created inside the container (network namespace); consequently it must comply with the standard Linux restrictions on interface names.
|
||||
- Result:
|
||||
- **IPs assigned to the interface**. This is either an IPv4 address, an IPv6 address, or both.
|
||||
- **DNS information**. Dictionary that includes DNS information for nameservers, domain, search domains and options.
|
||||
|
||||
- Delete container from network
|
||||
- Parameters:
|
||||
- **Version**. The version of CNI spec that the caller is using (container management system or the invoking plugin).
|
||||
- **Container ID**, as defined above.
|
||||
- **Network namespace path**, as defined above.
|
||||
- **Network configuration**, as defined above.
|
||||
- **Extra arguments**, as defined above.
|
||||
- **Name of the interface inside the container**, as defined above.
|
||||
|
||||
The executable command-line API uses the type of network (see [Network Configuration](#network-configuration) below) as the name of the executable to invoke.
|
||||
It will then look for this executable in a list of predefined directories. Once found, it will invoke the executable using the following environment variables for argument passing:
|
||||
- `CNI_VERSION`: [Semantic Version 2.0](http://semver.org) of CNI specification. This effectively versions the CNI_XXX environment variables.
|
||||
- `CNI_COMMAND`: indicates the desired operation; either `ADD` or `DEL`
|
||||
- `CNI_CONTAINERID`: Container ID
|
||||
- `CNI_NETNS`: Path to network namespace file
|
||||
- `CNI_IFNAME`: Interface name to set up
|
||||
- `CNI_ARGS`: Extra arguments passed in by the user at invocation time. Alphanumeric key-value pairs separated by semicolons; for example, "FOO=BAR;ABC=123"
|
||||
- `CNI_PATH`: Colon-separated list of paths to search for CNI plugin executables
|
||||
|
||||
Network configuration in JSON format is streamed through stdin.
|
||||
|
||||
|
||||
### Result
|
||||
|
||||
Success is indicated by a return code of zero and the following JSON printed to stdout in the case of the ADD command. This should be the same output as was returned by the IPAM plugin (see [IP Allocation](#ip-allocation) for details).
|
||||
|
||||
```
|
||||
{
|
||||
"cniVersion": "0.1.0",
|
||||
"ip4": {
|
||||
"ip": <ipv4-and-subnet-in-CIDR>,
|
||||
"gateway": <ipv4-of-the-gateway>, (optional)
|
||||
"routes": <list-of-ipv4-routes> (optional)
|
||||
},
|
||||
"ip6": {
|
||||
"ip": <ipv6-and-subnet-in-CIDR>,
|
||||
"gateway": <ipv6-of-the-gateway>, (optional)
|
||||
"routes": <list-of-ipv6-routes> (optional)
|
||||
},
|
||||
"dns": {
|
||||
"nameservers": <list-of-nameservers> (optional)
|
||||
"domain": <name-of-local-domain> (optional)
|
||||
"search": <list-of-additional-search-domains> (optional)
|
||||
"options": <list-of-options> (optional)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`cniVersion` specifies a [Semantic Version 2.0](http://semver.org) of CNI specification used by the plugin.
|
||||
`dns` field contains a dictionary consisting of common DNS information that this network is aware of.
|
||||
The result is returned in the same format as specified in the [configuration](#network-configuration).
|
||||
The specification does not declare how this information must be processed by CNI consumers.
|
||||
Examples include generating an `/etc/resolv.conf` file to be injected into the container filesystem or running a DNS forwarder on the host.
|
||||
|
||||
Errors are indicated by a non-zero return code and the following JSON being printed to stdout:
|
||||
```
|
||||
{
|
||||
"cniVersion": "0.1.0",
|
||||
"code": <numeric-error-code>,
|
||||
"msg": <short-error-message>,
|
||||
"details": <long-error-message> (optional)
|
||||
}
|
||||
```
|
||||
|
||||
`cniVersion` specifies a [Semantic Version 2.0](http://semver.org) of CNI specification used by the plugin.
|
||||
Error codes 0-99 are reserved for well-known errors (see [Well-known Error Codes](#well-known-error-codes) section).
|
||||
Values of 100+ can be freely used for plugin specific errors.
|
||||
|
||||
In addition, stderr can be used for unstructured output such as logs.
|
||||
|
||||
### Network Configuration
|
||||
|
||||
The network configuration is described in JSON form. The configuration can be stored on disk or generated from other sources by the container runtime. The following fields are well-known and have the following meaning:
|
||||
- `cniVersion` (string): [Semantic Version 2.0](http://semver.org) of CNI specification to which this configuration conforms.
|
||||
- `name` (string): Network name. This should be unique across all containers on the host (or other administrative domain).
|
||||
- `type` (string): Refers to the filename of the CNI plugin executable.
|
||||
- `ipMasq` (boolean): Optional (if supported by the plugin). Set up an IP masquerade on the host for this network. This is necessary if the host will act as a gateway to subnets that are not able to route to the IP assigned to the container.
|
||||
- `ipam`: Dictionary with IPAM specific values:
|
||||
- `type` (string): Refers to the filename of the IPAM plugin executable.
|
||||
- `routes` (list): List of subnets (in CIDR notation) that the CNI plugin should ensure are reachable by routing them through the network. Each entry is a dictionary containing:
|
||||
- `dst` (string): subnet in CIDR notation
|
||||
- `gw` (string): IP address of the gateway to use. If not specified, the default gateway for the subnet is assumed (as determined by the IPAM plugin).
|
||||
- `dns`: Dictionary with DNS specific values:
|
||||
- `nameservers` (list of strings): list of a priority-ordered list of DNS nameservers that this network is aware of. Each entry in the list is a string containing either an IPv4 or an IPv6 address.
|
||||
- `domain` (string): the local domain used for short hostname lookups.
|
||||
- `search` (list of strings): list of priority ordered search domains for short hostname lookups. Will be preferred over `domain` by most resolvers.
|
||||
- `options` (list of strings): list of options that can be passed to the resolver
|
||||
|
||||
### Example configurations
|
||||
|
||||
```json
|
||||
{
|
||||
"cniVersion": "0.1.0",
|
||||
"name": "dbnet",
|
||||
"type": "bridge",
|
||||
// type (plugin) specific
|
||||
"bridge": "cni0",
|
||||
"ipam": {
|
||||
"type": "host-local",
|
||||
// ipam specific
|
||||
"subnet": "10.1.0.0/16",
|
||||
"gateway": "10.1.0.1"
|
||||
},
|
||||
"dns": {
|
||||
"nameservers": [ "10.1.0.1" ]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"cniVersion": "0.1.0",
|
||||
"name": "pci",
|
||||
"type": "ovs",
|
||||
// type (plugin) specific
|
||||
"bridge": "ovs0",
|
||||
"vxlanID": 42,
|
||||
"ipam": {
|
||||
"type": "dhcp",
|
||||
"routes": [ { "dst": "10.3.0.0/16" }, { "dst": "10.4.0.0/16" } ]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"cniVersion": "0.1",
|
||||
"name": "wan",
|
||||
"type": "macvlan",
|
||||
// ipam specific
|
||||
"ipam": {
|
||||
"type": "dhcp",
|
||||
"routes": [ { "dst": "10.0.0.0/8", "gw": "10.0.0.1" } ]
|
||||
},
|
||||
"dns": {
|
||||
"nameservers": [ "10.0.0.1" ]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### IP Allocation
|
||||
|
||||
As part of its operation, a CNI plugin is expected to assign (and maintain) an IP address to the interface and install any necessary routes relevant for that interface. This gives the CNI plugin great flexibility but also places a large burden on it. Many CNI plugins would need to have the same code to support several IP management schemes that users may desire (e.g. dhcp, host-local).
|
||||
|
||||
To lessen the burden and make IP management strategy be orthogonal to the type of CNI plugin, we define a second type of plugin -- IP Address Management Plugin (IPAM plugin). It is however the responsibility of the CNI plugin to invoke the IPAM plugin at the proper moment in its execution. The IPAM plugin is expected to determine the interface IP/subnet, Gateway and Routes and return this information to the "main" plugin to apply. The IPAM plugin may obtain the information via a protocol (e.g. dhcp), data stored on a local filesystem, the "ipam" section of the Network Configuration file or a combination of the above.
|
||||
|
||||
#### IP Address Management (IPAM) Interface
|
||||
|
||||
Like CNI plugins, the IPAM plugins are invoked by running an executable. The executable is searched for in a predefined list of paths, indicated to the CNI plugin via `CNI_PATH`. The IPAM Plugin receives all the same environment variables that were passed in to the CNI plugin. Just like the CNI plugin, IPAM receives the network configuration file via stdin.
|
||||
|
||||
Success is indicated by a zero return code and the following JSON being printed to stdout (in the case of the ADD command):
|
||||
|
||||
```
|
||||
{
|
||||
"cniVersion": "0.1.0",
|
||||
"ip4": {
|
||||
"ip": <ipv4-and-subnet-in-CIDR>,
|
||||
"gateway": <ipv4-of-the-gateway>, (optional)
|
||||
"routes": <list-of-ipv4-routes> (optional)
|
||||
},
|
||||
"ip6": {
|
||||
"ip": <ipv6-and-subnet-in-CIDR>,
|
||||
"gateway": <ipv6-of-the-gateway>, (optional)
|
||||
"routes": <list-of-ipv6-routes> (optional)
|
||||
},
|
||||
"dns": {
|
||||
"nameservers": <list-of-nameservers> (optional)
|
||||
"domain": <name-of-local-domain> (optional)
|
||||
"search": <list-of-search-domains> (optional)
|
||||
"options": <list-of-options> (optional)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`cniVersion` specifies a [Semantic Version 2.0](http://semver.org) of CNI specification used by the plugin.
|
||||
`gateway` is the default gateway for this subnet, if one exists.
|
||||
It does not instruct the CNI plugin to add any routes with this gateway: routes to add are specified separately via the `routes` field.
|
||||
An example use of this value is for the CNI plugin to add this IP address to the linux-bridge to make it a gateway.
|
||||
|
||||
Each route entry is a dictionary with the following fields:
|
||||
- `dst` (string): Destination subnet specified in CIDR notation.
|
||||
- `gw` (string): IP of the gateway. If omitted, a default gateway is assumed (as determined by the CNI plugin).
|
||||
|
||||
The "dns" field contains a dictionary consisting of common DNS information.
|
||||
- `nameservers` (list of strings): list of a priority-ordered list of DNS nameservers that this network is aware of. Each entry in the list is a string containing either an IPv4 or an IPv6 address.
|
||||
- `domain` (string): the local domain used for short hostname lookups.
|
||||
- `search` (list of strings): list of priority ordered search domains for short hostname lookups. Will be preferred over `domain` by most resolvers.
|
||||
- `options` (list of strings): list of options that can be passed to the resolver
|
||||
See [CNI Plugin Result](#result) section for more information.
|
||||
|
||||
Errors and logs are communicated in the same way as the CNI plugin. See [CNI Plugin Result](#result) section for details.
|
||||
|
||||
IPAM plugin examples:
|
||||
- **host-local**: Select an unused (by other containers on the same host) IP within the specified range.
|
||||
- **dhcp**: Use DHCP protocol to acquire and maintain a lease. The DHCP requests will be sent via the created container interface; therefore, the associated network must support broadcast.
|
||||
|
||||
#### Notes
|
||||
- Routes are expected to be added with a 0 metric.
|
||||
- A default route may be specified via "0.0.0.0/0". Since another network might have already configured the default route, the CNI plugin should be prepared to skip over its default route definition.
|
||||
|
||||
## Well-known Error Codes
|
||||
- `1` - Incompatible CNI version
|
21
Vagrantfile
vendored
Normal file
21
Vagrantfile
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
# -*- mode: ruby -*-
|
||||
# vi: set ft=ruby :
|
||||
|
||||
Vagrant.configure(2) do |config|
|
||||
config.vm.box = "bento/ubuntu-16.04"
|
||||
|
||||
config.vm.synced_folder "..", "/go/src/github.com/containernetworking"
|
||||
|
||||
config.vm.provision "shell", inline: <<-SHELL
|
||||
set -e -x -u
|
||||
|
||||
apt-get update -y || (sleep 40 && apt-get update -y)
|
||||
apt-get install -y git
|
||||
|
||||
wget -qO- https://storage.googleapis.com/golang/go1.9.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
|
30
build
30
build
@ -1,30 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
ORG_PATH="github.com/containernetworking"
|
||||
REPO_PATH="${ORG_PATH}/cni"
|
||||
|
||||
if [ ! -h gopath/src/${REPO_PATH} ]; then
|
||||
mkdir -p gopath/src/${ORG_PATH}
|
||||
ln -s ../../../.. gopath/src/${REPO_PATH} || exit 255
|
||||
fi
|
||||
|
||||
export GO15VENDOREXPERIMENT=1
|
||||
export GOBIN=${PWD}/bin
|
||||
export GOPATH=${PWD}/gopath
|
||||
|
||||
echo "Building API"
|
||||
go build "$@" ${REPO_PATH}/libcni
|
||||
|
||||
echo "Building reference CLI"
|
||||
go install "$@" ${REPO_PATH}/cnitool
|
||||
|
||||
echo "Building plugins"
|
||||
PLUGINS="plugins/meta/* plugins/main/* plugins/ipam/*"
|
||||
for d in $PLUGINS; do
|
||||
if [ -d $d ]; then
|
||||
plugin=$(basename $d)
|
||||
echo " " $plugin
|
||||
go install "$@" ${REPO_PATH}/$d
|
||||
fi
|
||||
done
|
35
build.sh
Executable file
35
build.sh
Executable file
@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
if [ "$(uname)" == "Darwin" ]; then
|
||||
export GOOS=linux
|
||||
fi
|
||||
|
||||
ORG_PATH="github.com/containernetworking"
|
||||
export REPO_PATH="${ORG_PATH}/plugins"
|
||||
|
||||
if [ ! -h gopath/src/${REPO_PATH} ]; then
|
||||
mkdir -p gopath/src/${ORG_PATH}
|
||||
ln -s ../../../.. gopath/src/${REPO_PATH} || exit 255
|
||||
fi
|
||||
|
||||
export GO15VENDOREXPERIMENT=1
|
||||
export GOPATH=${PWD}/gopath
|
||||
|
||||
mkdir -p "${PWD}/bin"
|
||||
|
||||
echo "Building plugins"
|
||||
PLUGINS="plugins/meta/* plugins/main/* plugins/ipam/* plugins/sample"
|
||||
for d in $PLUGINS; do
|
||||
if [ -d "$d" ]; then
|
||||
plugin="$(basename "$d")"
|
||||
echo " $plugin"
|
||||
# use go install so we don't duplicate work
|
||||
if [ -n "$FASTBUILD" ]
|
||||
then
|
||||
GOBIN=${PWD}/bin go install -pkgdir $GOPATH/pkg "$@" $REPO_PATH/$d
|
||||
else
|
||||
go build -o "${PWD}/bin/$plugin" -pkgdir "$GOPATH/pkg" "$@" "$REPO_PATH/$d"
|
||||
fi
|
||||
fi
|
||||
done
|
@ -1,87 +0,0 @@
|
||||
// Copyright 2015 CoreOS, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/containernetworking/cni/libcni"
|
||||
)
|
||||
|
||||
const (
|
||||
EnvCNIPath = "CNI_PATH"
|
||||
EnvNetDir = "NETCONFPATH"
|
||||
|
||||
DefaultNetDir = "/etc/cni/net.d"
|
||||
|
||||
CmdAdd = "add"
|
||||
CmdDel = "del"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 3 {
|
||||
usage()
|
||||
return
|
||||
}
|
||||
|
||||
netdir := os.Getenv(EnvNetDir)
|
||||
if netdir == "" {
|
||||
netdir = DefaultNetDir
|
||||
}
|
||||
netconf, err := libcni.LoadConf(netdir, os.Args[2])
|
||||
if err != nil {
|
||||
exit(err)
|
||||
}
|
||||
|
||||
netns := os.Args[3]
|
||||
|
||||
cninet := &libcni.CNIConfig{
|
||||
Path: strings.Split(os.Getenv(EnvCNIPath), ":"),
|
||||
}
|
||||
|
||||
rt := &libcni.RuntimeConf{
|
||||
ContainerID: "cni",
|
||||
NetNS: netns,
|
||||
IfName: "eth0",
|
||||
}
|
||||
|
||||
switch os.Args[1] {
|
||||
case CmdAdd:
|
||||
_, err := cninet.AddNetwork(netconf, rt)
|
||||
exit(err)
|
||||
case CmdDel:
|
||||
exit(cninet.DelNetwork(netconf, rt))
|
||||
}
|
||||
}
|
||||
|
||||
func usage() {
|
||||
exe := filepath.Base(os.Args[0])
|
||||
|
||||
fmt.Fprintf(os.Stderr, "%s: Add or remove network interfaces from a network namespace\n", exe)
|
||||
fmt.Fprintf(os.Stderr, " %s %s <net> <netns>\n", exe, CmdAdd)
|
||||
fmt.Fprintf(os.Stderr, " %s %s <net> <netns>\n", exe, CmdDel)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func exit(err error) {
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
@ -1,73 +0,0 @@
|
||||
// Copyright 2015 CNI authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package libcni
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/invoke"
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
)
|
||||
|
||||
type RuntimeConf struct {
|
||||
ContainerID string
|
||||
NetNS string
|
||||
IfName string
|
||||
Args [][2]string
|
||||
}
|
||||
|
||||
type NetworkConfig struct {
|
||||
Network *types.NetConf
|
||||
Bytes []byte
|
||||
}
|
||||
|
||||
type CNI interface {
|
||||
AddNetwork(net *NetworkConfig, rt *RuntimeConf) (*types.Result, error)
|
||||
DelNetwork(net *NetworkConfig, rt *RuntimeConf) error
|
||||
}
|
||||
|
||||
type CNIConfig struct {
|
||||
Path []string
|
||||
}
|
||||
|
||||
func (c *CNIConfig) AddNetwork(net *NetworkConfig, rt *RuntimeConf) (*types.Result, error) {
|
||||
pluginPath, err := invoke.FindInPath(net.Network.Type, c.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return invoke.ExecPluginWithResult(pluginPath, net.Bytes, c.args("ADD", rt))
|
||||
}
|
||||
|
||||
func (c *CNIConfig) DelNetwork(net *NetworkConfig, rt *RuntimeConf) error {
|
||||
pluginPath, err := invoke.FindInPath(net.Network.Type, c.Path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return invoke.ExecPluginWithoutResult(pluginPath, net.Bytes, c.args("DEL", rt))
|
||||
}
|
||||
|
||||
// =====
|
||||
func (c *CNIConfig) args(action string, rt *RuntimeConf) *invoke.Args {
|
||||
return &invoke.Args{
|
||||
Command: action,
|
||||
ContainerID: rt.ContainerID,
|
||||
NetNS: rt.NetNS,
|
||||
PluginArgs: rt.Args,
|
||||
IfName: rt.IfName,
|
||||
Path: strings.Join(c.Path, ":"),
|
||||
}
|
||||
}
|
@ -1,85 +0,0 @@
|
||||
// Copyright 2015 CNI authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package libcni
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
)
|
||||
|
||||
func ConfFromBytes(bytes []byte) (*NetworkConfig, error) {
|
||||
conf := &NetworkConfig{Bytes: bytes}
|
||||
if err := json.Unmarshal(bytes, &conf.Network); err != nil {
|
||||
return nil, fmt.Errorf("error parsing configuration: %s", err)
|
||||
}
|
||||
return conf, nil
|
||||
}
|
||||
|
||||
func ConfFromFile(filename string) (*NetworkConfig, error) {
|
||||
bytes, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading %s: %s", filename, err)
|
||||
}
|
||||
return ConfFromBytes(bytes)
|
||||
}
|
||||
|
||||
func ConfFiles(dir string) ([]string, error) {
|
||||
// In part, adapted from rkt/networking/podenv.go#listFiles
|
||||
files, err := ioutil.ReadDir(dir)
|
||||
switch {
|
||||
case err == nil: // break
|
||||
case os.IsNotExist(err):
|
||||
return nil, nil
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
|
||||
confFiles := []string{}
|
||||
for _, f := range files {
|
||||
if f.IsDir() {
|
||||
continue
|
||||
}
|
||||
if filepath.Ext(f.Name()) == ".conf" {
|
||||
confFiles = append(confFiles, filepath.Join(dir, f.Name()))
|
||||
}
|
||||
}
|
||||
return confFiles, nil
|
||||
}
|
||||
|
||||
func LoadConf(dir, name string) (*NetworkConfig, error) {
|
||||
files, err := ConfFiles(dir)
|
||||
switch {
|
||||
case err != nil:
|
||||
return nil, err
|
||||
case len(files) == 0:
|
||||
return nil, fmt.Errorf("no net configurations found")
|
||||
}
|
||||
sort.Strings(files)
|
||||
|
||||
for _, confFile := range files {
|
||||
conf, err := ConfFromFile(confFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if conf.Network.Name == name {
|
||||
return conf, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf(`no net configuration with name "%s" in %s`, name, dir)
|
||||
}
|
@ -1,78 +0,0 @@
|
||||
// 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 invoke_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/invoke"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("FindInPath", func() {
|
||||
var (
|
||||
multiplePaths []string
|
||||
pluginName string
|
||||
pluginDir string
|
||||
anotherTempDir string
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
tempDir, err := ioutil.TempDir("", "cni-find")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
plugin, err := ioutil.TempFile(tempDir, "a-cni-plugin")
|
||||
|
||||
anotherTempDir, err = ioutil.TempDir("", "nothing-here")
|
||||
|
||||
multiplePaths = []string{anotherTempDir, tempDir}
|
||||
pluginDir, pluginName = filepath.Split(plugin.Name())
|
||||
})
|
||||
|
||||
Context("when multiple paths are provided", func() {
|
||||
It("returns only the path to the plugin", func() {
|
||||
pluginPath, err := invoke.FindInPath(pluginName, multiplePaths)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(pluginPath).To(Equal(filepath.Join(pluginDir, pluginName)))
|
||||
})
|
||||
})
|
||||
|
||||
Context("when an error occurs", func() {
|
||||
Context("when no paths are provided", func() {
|
||||
It("returns an error noting no paths were provided", func() {
|
||||
_, err := invoke.FindInPath(pluginName, []string{})
|
||||
Expect(err).To(MatchError("no paths provided"))
|
||||
})
|
||||
})
|
||||
|
||||
Context("when no plugin is provided", func() {
|
||||
It("returns an error noting the plugin name wasn't found", func() {
|
||||
_, err := invoke.FindInPath("", multiplePaths)
|
||||
Expect(err).To(MatchError("no plugin name provided"))
|
||||
})
|
||||
})
|
||||
|
||||
Context("when the plugin cannot be found", func() {
|
||||
It("returns an error noting the path", func() {
|
||||
pathsWithNothing := []string{anotherTempDir}
|
||||
_, err := invoke.FindInPath(pluginName, pathsWithNothing)
|
||||
Expect(err).To(MatchError(fmt.Sprintf("failed to find plugin %q in path %s", pluginName, pathsWithNothing)))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
68
pkg/ip/addr_linux.go
Normal file
68
pkg/ip/addr_linux.go
Normal file
@ -0,0 +1,68 @@
|
||||
// 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 ip
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/vishvananda/netlink"
|
||||
)
|
||||
|
||||
const SETTLE_INTERVAL = 50 * time.Millisecond
|
||||
|
||||
// SettleAddresses waits for all addresses on a link to leave tentative state.
|
||||
// This is particularly useful for ipv6, where all addresses need to do DAD.
|
||||
// There is no easy way to wait for this as an event, so just loop until the
|
||||
// addresses are no longer tentative.
|
||||
// If any addresses are still tentative after timeout seconds, then error.
|
||||
func SettleAddresses(ifName string, timeout int) error {
|
||||
link, err := netlink.LinkByName(ifName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to retrieve link: %v", err)
|
||||
}
|
||||
|
||||
deadline := time.Now().Add(time.Duration(timeout) * time.Second)
|
||||
for {
|
||||
addrs, err := netlink.AddrList(link, netlink.FAMILY_ALL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not list addresses: %v", err)
|
||||
}
|
||||
|
||||
if len(addrs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
ok := true
|
||||
for _, addr := range addrs {
|
||||
if addr.Flags&(syscall.IFA_F_TENTATIVE|syscall.IFA_F_DADFAILED) > 0 {
|
||||
ok = false
|
||||
break // Break out of the `range addrs`, not the `for`
|
||||
}
|
||||
}
|
||||
|
||||
if ok {
|
||||
return nil
|
||||
}
|
||||
if time.Now().After(deadline) {
|
||||
return fmt.Errorf("link %s still has tentative addresses after %d seconds",
|
||||
ifName,
|
||||
timeout)
|
||||
}
|
||||
|
||||
time.Sleep(SETTLE_INTERVAL)
|
||||
}
|
||||
}
|
@ -31,6 +31,16 @@ func PrevIP(ip net.IP) net.IP {
|
||||
return intToIP(i.Sub(i, big.NewInt(1)))
|
||||
}
|
||||
|
||||
// Cmp compares two IPs, returning the usual ordering:
|
||||
// a < b : -1
|
||||
// a == b : 0
|
||||
// a > b : 1
|
||||
func Cmp(a, b net.IP) int {
|
||||
aa := ipToInt(a)
|
||||
bb := ipToInt(b)
|
||||
return aa.Cmp(bb)
|
||||
}
|
||||
|
||||
func ipToInt(ip net.IP) *big.Int {
|
||||
if v := ip.To4(); v != nil {
|
||||
return big.NewInt(0).SetBytes(v)
|
||||
|
@ -12,7 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package skel
|
||||
package ip_test
|
||||
|
||||
import (
|
||||
. "github.com/onsi/ginkgo"
|
||||
@ -21,7 +21,7 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSkel(t *testing.T) {
|
||||
func TestIp(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Skel Suite")
|
||||
RunSpecs(t, "Ip Suite")
|
||||
}
|
61
pkg/ip/ipforward_linux.go
Normal file
61
pkg/ip/ipforward_linux.go
Normal file
@ -0,0 +1,61 @@
|
||||
// Copyright 2015 CNI authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package ip
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/types/current"
|
||||
)
|
||||
|
||||
func EnableIP4Forward() error {
|
||||
return echo1("/proc/sys/net/ipv4/ip_forward")
|
||||
}
|
||||
|
||||
func EnableIP6Forward() error {
|
||||
return echo1("/proc/sys/net/ipv6/conf/all/forwarding")
|
||||
}
|
||||
|
||||
// EnableForward will enable forwarding for all configured
|
||||
// address families
|
||||
func EnableForward(ips []*current.IPConfig) error {
|
||||
v4 := false
|
||||
v6 := false
|
||||
|
||||
for _, ip := range ips {
|
||||
if ip.Version == "4" && !v4 {
|
||||
if err := EnableIP4Forward(); err != nil {
|
||||
return err
|
||||
}
|
||||
v4 = true
|
||||
} else if ip.Version == "6" && !v6 {
|
||||
if err := EnableIP6Forward(); err != nil {
|
||||
return err
|
||||
}
|
||||
v6 = true
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func echo1(f string) error {
|
||||
if content, err := ioutil.ReadFile(f); err == nil {
|
||||
if bytes.Equal(bytes.TrimSpace(content), []byte("1")) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return ioutil.WriteFile(f, []byte("1"), 0644)
|
||||
}
|
31
pkg/ip/ipforward_linux_test.go
Normal file
31
pkg/ip/ipforward_linux_test.go
Normal file
@ -0,0 +1,31 @@
|
||||
package ip
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("IpforwardLinux", func() {
|
||||
It("echo1 must not write the file if content is 1", func() {
|
||||
file, err := ioutil.TempFile(os.TempDir(), "containernetworking")
|
||||
defer os.Remove(file.Name())
|
||||
err = echo1(file.Name())
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
statBefore, err := file.Stat()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// take a duration here, otherwise next file modification operation time
|
||||
// will be same as previous one.
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
err = echo1(file.Name())
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
statAfter, err := file.Stat()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(statBefore.ModTime()).To(Equal(statAfter.ModTime()))
|
||||
})
|
||||
})
|
@ -24,23 +24,49 @@ import (
|
||||
// SetupIPMasq installs iptables rules to masquerade traffic
|
||||
// coming from ipn and going outside of it
|
||||
func SetupIPMasq(ipn *net.IPNet, chain string, comment string) error {
|
||||
ipt, err := iptables.New()
|
||||
isV6 := ipn.IP.To4() == nil
|
||||
|
||||
var ipt *iptables.IPTables
|
||||
var err error
|
||||
var multicastNet string
|
||||
|
||||
if isV6 {
|
||||
ipt, err = iptables.NewWithProtocol(iptables.ProtocolIPv6)
|
||||
multicastNet = "ff00::/8"
|
||||
} else {
|
||||
ipt, err = iptables.NewWithProtocol(iptables.ProtocolIPv4)
|
||||
multicastNet = "224.0.0.0/4"
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to locate iptables: %v", err)
|
||||
}
|
||||
|
||||
if err = ipt.NewChain("nat", chain); err != nil {
|
||||
if err.(*iptables.Error).ExitStatus() != 1 {
|
||||
// TODO(eyakubovich): assumes exit status 1 implies chain exists
|
||||
// Create chain if doesn't exist
|
||||
exists := false
|
||||
chains, err := ipt.ListChains("nat")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list chains: %v", err)
|
||||
}
|
||||
for _, ch := range chains {
|
||||
if ch == chain {
|
||||
exists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !exists {
|
||||
if err = ipt.NewChain("nat", chain); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err = ipt.AppendUnique("nat", chain, "-d", ipn.String(), "-j", "ACCEPT", "-m", "comment", "--comment", comment); err != nil {
|
||||
// Packets to this network should not be touched
|
||||
if err := ipt.AppendUnique("nat", chain, "-d", ipn.String(), "-j", "ACCEPT", "-m", "comment", "--comment", comment); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = ipt.AppendUnique("nat", chain, "!", "-d", "224.0.0.0/4", "-j", "MASQUERADE", "-m", "comment", "--comment", comment); err != nil {
|
||||
// Don't masquerade multicast - pods should be able to talk to other pods
|
||||
// on the local network via multicast.
|
||||
if err := ipt.AppendUnique("nat", chain, "!", "-d", multicastNet, "-j", "MASQUERADE", "-m", "comment", "--comment", comment); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -49,7 +75,16 @@ func SetupIPMasq(ipn *net.IPNet, chain string, comment string) error {
|
||||
|
||||
// TeardownIPMasq undoes the effects of SetupIPMasq
|
||||
func TeardownIPMasq(ipn *net.IPNet, chain string, comment string) error {
|
||||
ipt, err := iptables.New()
|
||||
isV6 := ipn.IP.To4() == nil
|
||||
|
||||
var ipt *iptables.IPTables
|
||||
var err error
|
||||
|
||||
if isV6 {
|
||||
ipt, err = iptables.NewWithProtocol(iptables.ProtocolIPv6)
|
||||
} else {
|
||||
ipt, err = iptables.NewWithProtocol(iptables.ProtocolIPv4)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to locate iptables: %v", err)
|
||||
}
|
153
pkg/ip/link.go
153
pkg/ip/link.go
@ -1,153 +0,0 @@
|
||||
// Copyright 2015 CNI authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package ip
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/ns"
|
||||
"github.com/vishvananda/netlink"
|
||||
)
|
||||
|
||||
func makeVethPair(name, peer string, mtu int) (netlink.Link, error) {
|
||||
veth := &netlink.Veth{
|
||||
LinkAttrs: netlink.LinkAttrs{
|
||||
Name: name,
|
||||
Flags: net.FlagUp,
|
||||
MTU: mtu,
|
||||
},
|
||||
PeerName: peer,
|
||||
}
|
||||
if err := netlink.LinkAdd(veth); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return veth, nil
|
||||
}
|
||||
|
||||
func makeVeth(name string, mtu int) (peerName string, veth netlink.Link, err error) {
|
||||
for i := 0; i < 10; i++ {
|
||||
peerName, err = RandomVethName()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
veth, err = makeVethPair(name, peerName, mtu)
|
||||
switch {
|
||||
case err == nil:
|
||||
return
|
||||
|
||||
case os.IsExist(err):
|
||||
continue
|
||||
|
||||
default:
|
||||
err = fmt.Errorf("failed to make veth pair: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// should really never be hit
|
||||
err = fmt.Errorf("failed to find a unique veth name")
|
||||
return
|
||||
}
|
||||
|
||||
// RandomVethName returns string "veth" with random prefix (hashed from entropy)
|
||||
func RandomVethName() (string, error) {
|
||||
entropy := make([]byte, 4)
|
||||
_, err := rand.Reader.Read(entropy)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to generate random veth name: %v", err)
|
||||
}
|
||||
|
||||
// NetworkManager (recent versions) will ignore veth devices that start with "veth"
|
||||
return fmt.Sprintf("veth%x", entropy), nil
|
||||
}
|
||||
|
||||
// SetupVeth sets up a virtual ethernet link.
|
||||
// Should be in container netns, and will switch back to hostNS to set the host
|
||||
// veth end up.
|
||||
func SetupVeth(contVethName string, mtu int, hostNS ns.NetNS) (hostVeth, contVeth netlink.Link, err error) {
|
||||
var hostVethName string
|
||||
hostVethName, contVeth, err = makeVeth(contVethName, mtu)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = netlink.LinkSetUp(contVeth); err != nil {
|
||||
err = fmt.Errorf("failed to set %q up: %v", contVethName, err)
|
||||
return
|
||||
}
|
||||
|
||||
hostVeth, err = netlink.LinkByName(hostVethName)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to lookup %q: %v", hostVethName, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = netlink.LinkSetNsFd(hostVeth, int(hostNS.Fd())); err != nil {
|
||||
err = fmt.Errorf("failed to move veth to host netns: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
err = hostNS.Do(func(_ ns.NetNS) error {
|
||||
hostVeth, err := netlink.LinkByName(hostVethName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to lookup %q in %q: %v", hostVethName, hostNS.Path(), err)
|
||||
}
|
||||
|
||||
if err = netlink.LinkSetUp(hostVeth); err != nil {
|
||||
return fmt.Errorf("failed to set %q up: %v", hostVethName, err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// DelLinkByName removes an interface link.
|
||||
func DelLinkByName(ifName string) error {
|
||||
iface, err := netlink.LinkByName(ifName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to lookup %q: %v", ifName, err)
|
||||
}
|
||||
|
||||
if err = netlink.LinkDel(iface); err != nil {
|
||||
return fmt.Errorf("failed to delete %q: %v", ifName, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DelLinkByNameAddr remove an interface returns its IP address
|
||||
// of the specified family
|
||||
func DelLinkByNameAddr(ifName string, family int) (*net.IPNet, error) {
|
||||
iface, err := netlink.LinkByName(ifName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to lookup %q: %v", ifName, err)
|
||||
}
|
||||
|
||||
addrs, err := netlink.AddrList(iface, family)
|
||||
if err != nil || len(addrs) == 0 {
|
||||
return nil, fmt.Errorf("failed to get IP addresses for %q: %v", ifName, err)
|
||||
}
|
||||
|
||||
if err = netlink.LinkDel(iface); err != nil {
|
||||
return nil, fmt.Errorf("failed to delete %q: %v", ifName, err)
|
||||
}
|
||||
|
||||
return addrs[0].IPNet, nil
|
||||
}
|
234
pkg/ip/link_linux.go
Normal file
234
pkg/ip/link_linux.go
Normal file
@ -0,0 +1,234 @@
|
||||
// Copyright 2015 CNI authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package ip
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"github.com/containernetworking/plugins/pkg/ns"
|
||||
"github.com/containernetworking/plugins/pkg/utils/hwaddr"
|
||||
"github.com/vishvananda/netlink"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrLinkNotFound = errors.New("link not found")
|
||||
)
|
||||
|
||||
func makeVethPair(name, peer string, mtu int) (netlink.Link, error) {
|
||||
veth := &netlink.Veth{
|
||||
LinkAttrs: netlink.LinkAttrs{
|
||||
Name: name,
|
||||
Flags: net.FlagUp,
|
||||
MTU: mtu,
|
||||
},
|
||||
PeerName: peer,
|
||||
}
|
||||
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 veth2, nil
|
||||
}
|
||||
|
||||
func peerExists(name string) bool {
|
||||
if _, err := netlink.LinkByName(name); err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func makeVeth(name string, mtu int) (peerName string, veth netlink.Link, err error) {
|
||||
for i := 0; i < 10; i++ {
|
||||
peerName, err = RandomVethName()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
veth, err = makeVethPair(name, peerName, mtu)
|
||||
switch {
|
||||
case err == nil:
|
||||
return
|
||||
|
||||
case os.IsExist(err):
|
||||
if peerExists(peerName) {
|
||||
continue
|
||||
}
|
||||
err = fmt.Errorf("container veth name provided (%v) already exists", name)
|
||||
return
|
||||
|
||||
default:
|
||||
err = fmt.Errorf("failed to make veth pair: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// should really never be hit
|
||||
err = fmt.Errorf("failed to find a unique veth name")
|
||||
return
|
||||
}
|
||||
|
||||
// RandomVethName returns string "veth" with random prefix (hashed from entropy)
|
||||
func RandomVethName() (string, error) {
|
||||
entropy := make([]byte, 4)
|
||||
_, err := rand.Reader.Read(entropy)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to generate random veth name: %v", err)
|
||||
}
|
||||
|
||||
// NetworkManager (recent versions) will ignore veth devices that start with "veth"
|
||||
return fmt.Sprintf("veth%x", entropy), nil
|
||||
}
|
||||
|
||||
func RenameLink(curName, newName string) error {
|
||||
link, err := netlink.LinkByName(curName)
|
||||
if err == nil {
|
||||
err = netlink.LinkSetName(link, newName)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func ifaceFromNetlinkLink(l netlink.Link) net.Interface {
|
||||
a := l.Attrs()
|
||||
return net.Interface{
|
||||
Index: a.Index,
|
||||
MTU: a.MTU,
|
||||
Name: a.Name,
|
||||
HardwareAddr: a.HardwareAddr,
|
||||
Flags: a.Flags,
|
||||
}
|
||||
}
|
||||
|
||||
// SetupVeth sets up a pair of virtual ethernet devices.
|
||||
// Call SetupVeth from inside the container netns. It will create both veth
|
||||
// devices and move the host-side veth into the provided hostNS namespace.
|
||||
// On success, SetupVeth returns (hostVeth, containerVeth, nil)
|
||||
func SetupVeth(contVethName string, mtu int, hostNS ns.NetNS) (net.Interface, net.Interface, error) {
|
||||
hostVethName, contVeth, err := makeVeth(contVethName, mtu)
|
||||
if err != nil {
|
||||
return net.Interface{}, net.Interface{}, err
|
||||
}
|
||||
|
||||
if err = netlink.LinkSetUp(contVeth); err != nil {
|
||||
return net.Interface{}, net.Interface{}, fmt.Errorf("failed to set %q up: %v", contVethName, err)
|
||||
}
|
||||
|
||||
hostVeth, err := netlink.LinkByName(hostVethName)
|
||||
if err != nil {
|
||||
return net.Interface{}, net.Interface{}, fmt.Errorf("failed to lookup %q: %v", hostVethName, err)
|
||||
}
|
||||
|
||||
if err = netlink.LinkSetNsFd(hostVeth, int(hostNS.Fd())); err != nil {
|
||||
return net.Interface{}, net.Interface{}, fmt.Errorf("failed to move veth to host netns: %v", err)
|
||||
}
|
||||
|
||||
err = hostNS.Do(func(_ ns.NetNS) error {
|
||||
hostVeth, err = netlink.LinkByName(hostVethName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to lookup %q in %q: %v", hostVethName, hostNS.Path(), err)
|
||||
}
|
||||
|
||||
if err = netlink.LinkSetUp(hostVeth); err != nil {
|
||||
return fmt.Errorf("failed to set %q up: %v", hostVethName, err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return net.Interface{}, net.Interface{}, err
|
||||
}
|
||||
return ifaceFromNetlinkLink(hostVeth), ifaceFromNetlinkLink(contVeth), nil
|
||||
}
|
||||
|
||||
// DelLinkByName removes an interface link.
|
||||
func DelLinkByName(ifName string) error {
|
||||
iface, err := netlink.LinkByName(ifName)
|
||||
if err != nil {
|
||||
if err.Error() == "Link not found" {
|
||||
return ErrLinkNotFound
|
||||
}
|
||||
return fmt.Errorf("failed to lookup %q: %v", ifName, err)
|
||||
}
|
||||
|
||||
if err = netlink.LinkDel(iface); err != nil {
|
||||
return fmt.Errorf("failed to delete %q: %v", ifName, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DelLinkByNameAddr remove an interface and returns its addresses
|
||||
func DelLinkByNameAddr(ifName string) ([]*net.IPNet, error) {
|
||||
iface, err := netlink.LinkByName(ifName)
|
||||
if err != nil {
|
||||
if err != nil && err.Error() == "Link not found" {
|
||||
return nil, ErrLinkNotFound
|
||||
}
|
||||
return nil, fmt.Errorf("failed to lookup %q: %v", ifName, err)
|
||||
}
|
||||
|
||||
addrs, err := netlink.AddrList(iface, netlink.FAMILY_ALL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get IP addresses for %q: %v", ifName, err)
|
||||
}
|
||||
|
||||
if err = netlink.LinkDel(iface); err != nil {
|
||||
return nil, fmt.Errorf("failed to delete %q: %v", ifName, err)
|
||||
}
|
||||
|
||||
out := []*net.IPNet{}
|
||||
for _, addr := range addrs {
|
||||
if addr.IP.IsGlobalUnicast() {
|
||||
out = append(out, addr.IPNet)
|
||||
}
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func SetHWAddrByIP(ifName string, ip4 net.IP, ip6 net.IP) error {
|
||||
iface, err := netlink.LinkByName(ifName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to lookup %q: %v", ifName, err)
|
||||
}
|
||||
|
||||
switch {
|
||||
case ip4 == nil && ip6 == nil:
|
||||
return fmt.Errorf("neither ip4 or ip6 specified")
|
||||
|
||||
case ip4 != nil:
|
||||
{
|
||||
hwAddr, err := hwaddr.GenerateHardwareAddr4(ip4, hwaddr.PrivateMACPrefix)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate hardware addr: %v", err)
|
||||
}
|
||||
if err = netlink.LinkSetHardwareAddr(iface, hwAddr); err != nil {
|
||||
return fmt.Errorf("failed to add hardware addr to %q: %v", ifName, err)
|
||||
}
|
||||
}
|
||||
case ip6 != nil:
|
||||
// TODO: IPv6
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
270
pkg/ip/link_linux_test.go
Normal file
270
pkg/ip/link_linux_test.go
Normal file
@ -0,0 +1,270 @@
|
||||
// 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_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"github.com/containernetworking/plugins/pkg/ip"
|
||||
"github.com/containernetworking/plugins/pkg/ns"
|
||||
|
||||
"github.com/vishvananda/netlink"
|
||||
)
|
||||
|
||||
func getHwAddr(linkname string) string {
|
||||
veth, err := netlink.LinkByName(linkname)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
return fmt.Sprintf("%s", veth.Attrs().HardwareAddr)
|
||||
}
|
||||
|
||||
var _ = Describe("Link", func() {
|
||||
const (
|
||||
ifaceFormatString string = "i%d"
|
||||
mtu int = 1400
|
||||
ip4onehwaddr = "0a:58:01:01:01:01"
|
||||
)
|
||||
var (
|
||||
hostNetNS ns.NetNS
|
||||
containerNetNS ns.NetNS
|
||||
ifaceCounter int = 0
|
||||
hostVeth net.Interface
|
||||
containerVeth net.Interface
|
||||
hostVethName string
|
||||
containerVethName string
|
||||
|
||||
ip4one = net.ParseIP("1.1.1.1")
|
||||
ip4two = net.ParseIP("1.1.1.2")
|
||||
originalRandReader = rand.Reader
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
var err error
|
||||
|
||||
hostNetNS, err = ns.NewNS()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
containerNetNS, err = ns.NewNS()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
fakeBytes := make([]byte, 20)
|
||||
//to be reset in AfterEach block
|
||||
rand.Reader = bytes.NewReader(fakeBytes)
|
||||
|
||||
_ = containerNetNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
hostVeth, containerVeth, err = ip.SetupVeth(fmt.Sprintf(ifaceFormatString, ifaceCounter), mtu, hostNetNS)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
hostVethName = hostVeth.Name
|
||||
containerVethName = containerVeth.Name
|
||||
|
||||
return nil
|
||||
})
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
Expect(containerNetNS.Close()).To(Succeed())
|
||||
Expect(hostNetNS.Close()).To(Succeed())
|
||||
ifaceCounter++
|
||||
rand.Reader = originalRandReader
|
||||
})
|
||||
|
||||
It("SetupVeth must put the veth endpoints into the separate namespaces", func() {
|
||||
_ = containerNetNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
containerVethFromName, err := netlink.LinkByName(containerVethName)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(containerVethFromName.Attrs().Index).To(Equal(containerVeth.Index))
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
_ = hostNetNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
hostVethFromName, err := netlink.LinkByName(hostVethName)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(hostVethFromName.Attrs().Index).To(Equal(hostVeth.Index))
|
||||
|
||||
return nil
|
||||
})
|
||||
})
|
||||
|
||||
Context("when container already has an interface with the same name", func() {
|
||||
It("returns useful error", func() {
|
||||
_ = containerNetNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
_, _, err := ip.SetupVeth(containerVethName, mtu, hostNetNS)
|
||||
Expect(err.Error()).To(Equal(fmt.Sprintf("container veth name provided (%s) already exists", containerVethName)))
|
||||
|
||||
return nil
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Context("deleting an non-existent device", func() {
|
||||
It("returns known error", func() {
|
||||
_ = containerNetNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
// This string should match the expected error codes in the cmdDel functions of some of the plugins
|
||||
_, err := ip.DelLinkByNameAddr("THIS_DONT_EXIST")
|
||||
Expect(err).To(Equal(ip.ErrLinkNotFound))
|
||||
|
||||
return nil
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Context("when there is no name available for the host-side", func() {
|
||||
BeforeEach(func() {
|
||||
//adding different interface to container ns
|
||||
containerVethName += "0"
|
||||
})
|
||||
It("returns useful error", func() {
|
||||
_ = containerNetNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
_, _, err := ip.SetupVeth(containerVethName, mtu, hostNetNS)
|
||||
Expect(err.Error()).To(Equal("failed to move veth to host netns: file exists"))
|
||||
|
||||
return nil
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Context("when there is no name conflict for the host or container interfaces", func() {
|
||||
BeforeEach(func() {
|
||||
//adding different interface to container and host ns
|
||||
containerVethName += "0"
|
||||
rand.Reader = originalRandReader
|
||||
})
|
||||
It("successfully creates the second veth pair", func() {
|
||||
_ = containerNetNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
hostVeth, _, err := ip.SetupVeth(containerVethName, mtu, hostNetNS)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
hostVethName = hostVeth.Name
|
||||
return nil
|
||||
})
|
||||
|
||||
//verify veths are in different namespaces
|
||||
_ = containerNetNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
_, err := netlink.LinkByName(containerVethName)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
_ = hostNetNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
_, err := netlink.LinkByName(hostVethName)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
return nil
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
It("DelLinkByName must delete the veth endpoints", func() {
|
||||
_ = containerNetNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
// this will delete the host endpoint too
|
||||
err := ip.DelLinkByName(containerVethName)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
_, err = netlink.LinkByName(containerVethName)
|
||||
Expect(err).To(HaveOccurred())
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
_ = hostNetNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
_, err := netlink.LinkByName(hostVethName)
|
||||
Expect(err).To(HaveOccurred())
|
||||
|
||||
return nil
|
||||
})
|
||||
})
|
||||
|
||||
It("DelLinkByNameAddr should return no IPs when no IPs are configured", func() {
|
||||
_ = containerNetNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
// this will delete the host endpoint too
|
||||
addr, err := ip.DelLinkByNameAddr(containerVethName)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(addr).To(HaveLen(0))
|
||||
return nil
|
||||
})
|
||||
})
|
||||
|
||||
It("SetHWAddrByIP must change the interface hwaddr and be predictable", func() {
|
||||
|
||||
_ = containerNetNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
var err error
|
||||
hwaddrBefore := getHwAddr(containerVethName)
|
||||
|
||||
err = ip.SetHWAddrByIP(containerVethName, ip4one, nil)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
hwaddrAfter1 := getHwAddr(containerVethName)
|
||||
|
||||
Expect(hwaddrBefore).NotTo(Equal(hwaddrAfter1))
|
||||
Expect(hwaddrAfter1).To(Equal(ip4onehwaddr))
|
||||
|
||||
return nil
|
||||
})
|
||||
})
|
||||
|
||||
It("SetHWAddrByIP must be injective", func() {
|
||||
|
||||
_ = containerNetNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
err := ip.SetHWAddrByIP(containerVethName, ip4one, nil)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
hwaddrAfter1 := getHwAddr(containerVethName)
|
||||
|
||||
err = ip.SetHWAddrByIP(containerVethName, ip4two, nil)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
hwaddrAfter2 := getHwAddr(containerVethName)
|
||||
|
||||
Expect(hwaddrAfter1).NotTo(Equal(hwaddrAfter2))
|
||||
return nil
|
||||
})
|
||||
})
|
||||
})
|
@ -1,4 +1,4 @@
|
||||
// Copyright 2015 CNI authors
|
||||
// Copyright 2015-2017 CNI authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@ -20,12 +20,6 @@ import (
|
||||
"github.com/vishvananda/netlink"
|
||||
)
|
||||
|
||||
// AddDefaultRoute sets the default route on the given gateway.
|
||||
func AddDefaultRoute(gw net.IP, dev netlink.Link) error {
|
||||
_, defNet, _ := net.ParseCIDR("0.0.0.0/0")
|
||||
return AddRoute(defNet, gw, dev)
|
||||
}
|
||||
|
||||
// AddRoute adds a universally-scoped route to a device.
|
||||
func AddRoute(ipn *net.IPNet, gw net.IP, dev netlink.Link) error {
|
||||
return netlink.RouteAdd(&netlink.Route{
|
||||
@ -45,3 +39,9 @@ func AddHostRoute(ipn *net.IPNet, gw net.IP, dev netlink.Link) error {
|
||||
Gw: gw,
|
||||
})
|
||||
}
|
||||
|
||||
// AddDefaultRoute sets the default route on the given gateway.
|
||||
func AddDefaultRoute(gw net.IP, dev netlink.Link) error {
|
||||
_, defNet, _ := net.ParseCIDR("0.0.0.0/0")
|
||||
return AddRoute(defNet, gw, dev)
|
||||
}
|
@ -15,54 +15,14 @@
|
||||
package ipam
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/invoke"
|
||||
"github.com/containernetworking/cni/pkg/ip"
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
|
||||
"github.com/vishvananda/netlink"
|
||||
)
|
||||
|
||||
func ExecAdd(plugin string, netconf []byte) (*types.Result, error) {
|
||||
func ExecAdd(plugin string, netconf []byte) (types.Result, error) {
|
||||
return invoke.DelegateAdd(plugin, netconf)
|
||||
}
|
||||
|
||||
func ExecDel(plugin string, netconf []byte) error {
|
||||
return invoke.DelegateDel(plugin, netconf)
|
||||
}
|
||||
|
||||
// ConfigureIface takes the result of IPAM plugin and
|
||||
// applies to the ifName interface
|
||||
func ConfigureIface(ifName string, res *types.Result) error {
|
||||
link, err := netlink.LinkByName(ifName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to lookup %q: %v", ifName, err)
|
||||
}
|
||||
|
||||
if err := netlink.LinkSetUp(link); err != nil {
|
||||
return fmt.Errorf("failed to set %q UP: %v", ifName, err)
|
||||
}
|
||||
|
||||
// TODO(eyakubovich): IPv6
|
||||
addr := &netlink.Addr{IPNet: &res.IP4.IP, Label: ""}
|
||||
if err = netlink.AddrAdd(link, addr); err != nil {
|
||||
return fmt.Errorf("failed to add IP addr to %q: %v", ifName, err)
|
||||
}
|
||||
|
||||
for _, r := range res.IP4.Routes {
|
||||
gw := r.GW
|
||||
if gw == nil {
|
||||
gw = res.IP4.Gateway
|
||||
}
|
||||
if err = ip.AddRoute(&r.Dst, gw, link); err != nil {
|
||||
// we skip over duplicate routes as we assume the first one wins
|
||||
if !os.IsExist(err) {
|
||||
return fmt.Errorf("failed to add route '%v via %v dev %v': %v", r.Dst, gw, ifName, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
121
pkg/ipam/ipam_linux.go
Normal file
121
pkg/ipam/ipam_linux.go
Normal file
@ -0,0 +1,121 @@
|
||||
// Copyright 2015 CNI authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package ipam
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/types/current"
|
||||
"github.com/containernetworking/plugins/pkg/ip"
|
||||
"github.com/containernetworking/plugins/pkg/utils/sysctl"
|
||||
|
||||
"github.com/vishvananda/netlink"
|
||||
)
|
||||
|
||||
const (
|
||||
DisableIPv6SysctlTemplate = "net.ipv6.conf.%s.disable_ipv6"
|
||||
)
|
||||
|
||||
// ConfigureIface takes the result of IPAM plugin and
|
||||
// applies to the ifName interface
|
||||
func ConfigureIface(ifName string, res *current.Result) error {
|
||||
if len(res.Interfaces) == 0 {
|
||||
return fmt.Errorf("no interfaces to configure")
|
||||
}
|
||||
|
||||
link, err := netlink.LinkByName(ifName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to lookup %q: %v", ifName, err)
|
||||
}
|
||||
|
||||
if err := netlink.LinkSetUp(link); err != nil {
|
||||
return fmt.Errorf("failed to set %q UP: %v", ifName, err)
|
||||
}
|
||||
|
||||
var v4gw, v6gw net.IP
|
||||
var has_enabled_ipv6 bool = false
|
||||
for _, ipc := range res.IPs {
|
||||
if ipc.Interface == nil {
|
||||
continue
|
||||
}
|
||||
intIdx := *ipc.Interface
|
||||
if intIdx < 0 || intIdx >= len(res.Interfaces) || res.Interfaces[intIdx].Name != ifName {
|
||||
// IP address is for a different interface
|
||||
return fmt.Errorf("failed to add IP addr %v to %q: invalid interface index", ipc, ifName)
|
||||
}
|
||||
|
||||
// Make sure sysctl "disable_ipv6" is 0 if we are about to add
|
||||
// an IPv6 address to the interface
|
||||
if !has_enabled_ipv6 && ipc.Version == "6" {
|
||||
// Enabled IPv6 for loopback "lo" and the interface
|
||||
// being configured
|
||||
for _, iface := range [2]string{"lo", ifName} {
|
||||
ipv6SysctlValueName := fmt.Sprintf(DisableIPv6SysctlTemplate, iface)
|
||||
|
||||
// Read current sysctl value
|
||||
value, err := sysctl.Sysctl(ipv6SysctlValueName)
|
||||
if err != nil || value == "0" {
|
||||
// FIXME: log warning if unable to read sysctl value
|
||||
continue
|
||||
}
|
||||
|
||||
// Write sysctl to enable IPv6
|
||||
_, err = sysctl.Sysctl(ipv6SysctlValueName, "0")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to enable IPv6 for interface %q (%s=%s): %v", iface, ipv6SysctlValueName, value, err)
|
||||
}
|
||||
}
|
||||
has_enabled_ipv6 = true
|
||||
}
|
||||
|
||||
addr := &netlink.Addr{IPNet: &ipc.Address, Label: ""}
|
||||
if err = netlink.AddrAdd(link, addr); err != nil {
|
||||
return fmt.Errorf("failed to add IP addr %v to %q: %v", ipc, ifName, err)
|
||||
}
|
||||
|
||||
gwIsV4 := ipc.Gateway.To4() != nil
|
||||
if gwIsV4 && v4gw == nil {
|
||||
v4gw = ipc.Gateway
|
||||
} else if !gwIsV4 && v6gw == nil {
|
||||
v6gw = ipc.Gateway
|
||||
}
|
||||
}
|
||||
|
||||
if v6gw != nil {
|
||||
ip.SettleAddresses(ifName, 10)
|
||||
}
|
||||
|
||||
for _, r := range res.Routes {
|
||||
routeIsV4 := r.Dst.IP.To4() != nil
|
||||
gw := r.GW
|
||||
if gw == nil {
|
||||
if routeIsV4 && v4gw != nil {
|
||||
gw = v4gw
|
||||
} else if !routeIsV4 && v6gw != nil {
|
||||
gw = v6gw
|
||||
}
|
||||
}
|
||||
if err = ip.AddRoute(&r.Dst, gw, link); err != nil {
|
||||
// we skip over duplicate routes as we assume the first one wins
|
||||
if !os.IsExist(err) {
|
||||
return fmt.Errorf("failed to add route '%v via %v dev %v': %v", r.Dst, gw, ifName, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
299
pkg/ipam/ipam_linux_test.go
Normal file
299
pkg/ipam/ipam_linux_test.go
Normal file
@ -0,0 +1,299 @@
|
||||
// Copyright 2015 CNI authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package ipam
|
||||
|
||||
import (
|
||||
"net"
|
||||
"syscall"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
"github.com/containernetworking/cni/pkg/types/current"
|
||||
"github.com/containernetworking/plugins/pkg/ns"
|
||||
|
||||
"github.com/vishvananda/netlink"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
const LINK_NAME = "eth0"
|
||||
|
||||
func ipNetEqual(a, b *net.IPNet) bool {
|
||||
aPrefix, aBits := a.Mask.Size()
|
||||
bPrefix, bBits := b.Mask.Size()
|
||||
if aPrefix != bPrefix || aBits != bBits {
|
||||
return false
|
||||
}
|
||||
return a.IP.Equal(b.IP)
|
||||
}
|
||||
|
||||
var _ = Describe("ConfigureIface", func() {
|
||||
var originalNS ns.NetNS
|
||||
var ipv4, ipv6, routev4, routev6 *net.IPNet
|
||||
var ipgw4, ipgw6, routegwv4, routegwv6 net.IP
|
||||
var result *current.Result
|
||||
|
||||
BeforeEach(func() {
|
||||
// Create a new NetNS so we don't modify the host
|
||||
var err error
|
||||
originalNS, err = ns.NewNS()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
err = originalNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
// Add master
|
||||
err = netlink.LinkAdd(&netlink.Dummy{
|
||||
LinkAttrs: netlink.LinkAttrs{
|
||||
Name: LINK_NAME,
|
||||
},
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
_, err = netlink.LinkByName(LINK_NAME)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
return nil
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
ipv4, err = types.ParseCIDR("1.2.3.30/24")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(ipv4).NotTo(BeNil())
|
||||
|
||||
_, routev4, err = net.ParseCIDR("15.5.6.8/24")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(routev4).NotTo(BeNil())
|
||||
routegwv4 = net.ParseIP("1.2.3.5")
|
||||
Expect(routegwv4).NotTo(BeNil())
|
||||
|
||||
ipgw4 = net.ParseIP("1.2.3.1")
|
||||
Expect(ipgw4).NotTo(BeNil())
|
||||
|
||||
ipv6, err = types.ParseCIDR("abcd:1234:ffff::cdde/64")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(ipv6).NotTo(BeNil())
|
||||
|
||||
_, routev6, err = net.ParseCIDR("1111:dddd::aaaa/80")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(routev6).NotTo(BeNil())
|
||||
routegwv6 = net.ParseIP("abcd:1234:ffff::10")
|
||||
Expect(routegwv6).NotTo(BeNil())
|
||||
|
||||
ipgw6 = net.ParseIP("abcd:1234:ffff::1")
|
||||
Expect(ipgw6).NotTo(BeNil())
|
||||
|
||||
result = ¤t.Result{
|
||||
Interfaces: []*current.Interface{
|
||||
{
|
||||
Name: "eth0",
|
||||
Mac: "00:11:22:33:44:55",
|
||||
Sandbox: "/proc/3553/ns/net",
|
||||
},
|
||||
{
|
||||
Name: "fake0",
|
||||
Mac: "00:33:44:55:66:77",
|
||||
Sandbox: "/proc/1234/ns/net",
|
||||
},
|
||||
},
|
||||
IPs: []*current.IPConfig{
|
||||
{
|
||||
Version: "4",
|
||||
Interface: current.Int(0),
|
||||
Address: *ipv4,
|
||||
Gateway: ipgw4,
|
||||
},
|
||||
{
|
||||
Version: "6",
|
||||
Interface: current.Int(0),
|
||||
Address: *ipv6,
|
||||
Gateway: ipgw6,
|
||||
},
|
||||
},
|
||||
Routes: []*types.Route{
|
||||
{Dst: *routev4, GW: routegwv4},
|
||||
{Dst: *routev6, GW: routegwv6},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
Expect(originalNS.Close()).To(Succeed())
|
||||
})
|
||||
|
||||
It("configures a link with addresses and routes", func() {
|
||||
err := originalNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
err := ConfigureIface(LINK_NAME, result)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
link, err := netlink.LinkByName(LINK_NAME)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(link.Attrs().Name).To(Equal(LINK_NAME))
|
||||
|
||||
v4addrs, err := netlink.AddrList(link, syscall.AF_INET)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(len(v4addrs)).To(Equal(1))
|
||||
Expect(ipNetEqual(v4addrs[0].IPNet, ipv4)).To(Equal(true))
|
||||
|
||||
v6addrs, err := netlink.AddrList(link, syscall.AF_INET6)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(len(v6addrs)).To(Equal(2))
|
||||
|
||||
var found bool
|
||||
for _, a := range v6addrs {
|
||||
if ipNetEqual(a.IPNet, ipv6) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
Expect(found).To(Equal(true))
|
||||
|
||||
// Ensure the v4 route, v6 route, and subnet route
|
||||
routes, err := netlink.RouteList(link, 0)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
var v4found, v6found bool
|
||||
for _, route := range routes {
|
||||
isv4 := route.Dst.IP.To4() != nil
|
||||
if isv4 && ipNetEqual(route.Dst, routev4) && route.Gw.Equal(routegwv4) {
|
||||
v4found = true
|
||||
}
|
||||
if !isv4 && ipNetEqual(route.Dst, routev6) && route.Gw.Equal(routegwv6) {
|
||||
v6found = true
|
||||
}
|
||||
|
||||
if v4found && v6found {
|
||||
break
|
||||
}
|
||||
}
|
||||
Expect(v4found).To(Equal(true))
|
||||
Expect(v6found).To(Equal(true))
|
||||
|
||||
return nil
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
|
||||
It("configures a link with routes using address gateways", func() {
|
||||
result.Routes[0].GW = nil
|
||||
result.Routes[1].GW = nil
|
||||
err := originalNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
err := ConfigureIface(LINK_NAME, result)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
link, err := netlink.LinkByName(LINK_NAME)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(link.Attrs().Name).To(Equal(LINK_NAME))
|
||||
|
||||
// Ensure the v4 route, v6 route, and subnet route
|
||||
routes, err := netlink.RouteList(link, 0)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
var v4found, v6found bool
|
||||
for _, route := range routes {
|
||||
isv4 := route.Dst.IP.To4() != nil
|
||||
if isv4 && ipNetEqual(route.Dst, routev4) && route.Gw.Equal(ipgw4) {
|
||||
v4found = true
|
||||
}
|
||||
if !isv4 && ipNetEqual(route.Dst, routev6) && route.Gw.Equal(ipgw6) {
|
||||
v6found = true
|
||||
}
|
||||
|
||||
if v4found && v6found {
|
||||
break
|
||||
}
|
||||
}
|
||||
Expect(v4found).To(Equal(true))
|
||||
Expect(v6found).To(Equal(true))
|
||||
|
||||
return nil
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
|
||||
It("returns an error when the interface index doesn't match the link name", func() {
|
||||
result.IPs[0].Interface = current.Int(1)
|
||||
err := originalNS.Do(func(ns.NetNS) error {
|
||||
return ConfigureIface(LINK_NAME, result)
|
||||
})
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
|
||||
It("returns an error when the interface index is too big", func() {
|
||||
result.IPs[0].Interface = current.Int(2)
|
||||
err := originalNS.Do(func(ns.NetNS) error {
|
||||
return ConfigureIface(LINK_NAME, result)
|
||||
})
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
|
||||
It("returns an error when the interface index is too small", func() {
|
||||
result.IPs[0].Interface = current.Int(-1)
|
||||
err := originalNS.Do(func(ns.NetNS) error {
|
||||
return ConfigureIface(LINK_NAME, result)
|
||||
})
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
|
||||
It("returns an error when there are no interfaces to configure", func() {
|
||||
result.Interfaces = []*current.Interface{}
|
||||
err := originalNS.Do(func(ns.NetNS) error {
|
||||
return ConfigureIface(LINK_NAME, result)
|
||||
})
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
|
||||
It("returns an error when configuring the wrong interface", func() {
|
||||
err := originalNS.Do(func(ns.NetNS) error {
|
||||
return ConfigureIface("asdfasdf", result)
|
||||
})
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
|
||||
It("does not panic when interface is not specified", func() {
|
||||
result = ¤t.Result{
|
||||
Interfaces: []*current.Interface{
|
||||
{
|
||||
Name: "eth0",
|
||||
Mac: "00:11:22:33:44:55",
|
||||
Sandbox: "/proc/3553/ns/net",
|
||||
},
|
||||
{
|
||||
Name: "fake0",
|
||||
Mac: "00:33:44:55:66:77",
|
||||
Sandbox: "/proc/1234/ns/net",
|
||||
},
|
||||
},
|
||||
IPs: []*current.IPConfig{
|
||||
{
|
||||
Version: "4",
|
||||
Address: *ipv4,
|
||||
Gateway: ipgw4,
|
||||
},
|
||||
{
|
||||
Version: "6",
|
||||
Address: *ipv6,
|
||||
Gateway: ipgw6,
|
||||
},
|
||||
},
|
||||
}
|
||||
err := originalNS.Do(func(ns.NetNS) error {
|
||||
return ConfigureIface(LINK_NAME, result)
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
})
|
@ -12,7 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package types_test
|
||||
package ipam_test
|
||||
|
||||
import (
|
||||
. "github.com/onsi/ginkgo"
|
||||
@ -21,7 +21,7 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTypes(t *testing.T) {
|
||||
func TestIpam(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Types Suite")
|
||||
RunSpecs(t, "Ipam Suite")
|
||||
}
|
@ -9,7 +9,7 @@ Go provides the `runtime.LockOSThread()` function to ensure a specific goroutine
|
||||
For example, you cannot rely on the `ns.Set()` namespace being the current namespace after the `Set()` call unless you do two things. First, the goroutine calling `Set()` must have previously called `LockOSThread()`. Second, you must ensure `runtime.UnlockOSThread()` is not called somewhere in-between. You also cannot rely on the initial network namespace remaining the current network namespace if any other code in your program switches namespaces, unless you have already called `LockOSThread()` in that goroutine. Note that `LockOSThread()` prevents the Go scheduler from optimally scheduling goroutines for best performance, so `LockOSThread()` should only be used in small, isolated goroutines that release the lock quickly.
|
||||
|
||||
### Do() The Recommended Thing
|
||||
The `ns.Do()` method provides control over network namespaces for you by implementing these strategies. All code dependent on a particular network namespace should be wrapped in the `ns.Do()` method to ensure the correct namespace is selected for the duration of your code. For example:
|
||||
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()
|
||||
@ -26,6 +26,15 @@ 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.
|
||||
|
||||
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".
|
||||
|
||||
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
|
||||
- http://morsmachine.dk/go-scheduler
|
||||
- https://github.com/containernetworking/cni/issues/262
|
||||
- https://golang.org/pkg/runtime/
|
||||
- https://www.weave.works/blog/linux-namespaces-and-go-don-t-mix
|
||||
|
@ -1,4 +1,4 @@
|
||||
// Copyright 2015 CNI authors
|
||||
// Copyright 2015-2017 CNI authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@ -26,40 +26,9 @@ import (
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
type NetNS interface {
|
||||
// Executes the passed closure in this object's network namespace,
|
||||
// attemtping to restore the original namespace before returning.
|
||||
// However, since each OS thread can have a different network namespace,
|
||||
// and Go's thread scheduling is highly variable, callers cannot
|
||||
// guarantee any specific namespace is set unless operations that
|
||||
// require that namespace are wrapped with Do(). Also, no code called
|
||||
// from Do() should call runtime.UnlockOSThread(), or the risk
|
||||
// of executing code in an incorrect namespace will be greater. See
|
||||
// https://github.com/golang/go/wiki/LockOSThread for further details.
|
||||
Do(toRun func(NetNS) error) error
|
||||
|
||||
// Sets the current network namespace to this object's network namespace.
|
||||
// Note that since Go's thread scheduling is highly variable, callers
|
||||
// cannot guarantee the requested namespace will be the current namespace
|
||||
// after this function is called; to ensure this wrap operations that
|
||||
// require the namespace with Do() instead.
|
||||
Set() error
|
||||
|
||||
// Returns the filesystem path representing this object's network namespace
|
||||
Path() string
|
||||
|
||||
// Returns a file descriptor representing this object's network namespace
|
||||
Fd() uintptr
|
||||
|
||||
// Cleans up this instance of the network namespace; if this instance
|
||||
// is the last user the namespace will be destroyed
|
||||
Close() error
|
||||
}
|
||||
|
||||
type netNS struct {
|
||||
file *os.File
|
||||
mounted bool
|
||||
closed bool
|
||||
// Returns an object representing the current OS thread's network namespace
|
||||
func GetCurrentNS() (NetNS, error) {
|
||||
return GetNS(getCurrentThreadNetNSPath())
|
||||
}
|
||||
|
||||
func getCurrentThreadNetNSPath() string {
|
||||
@ -69,43 +38,6 @@ func getCurrentThreadNetNSPath() string {
|
||||
return fmt.Sprintf("/proc/%d/task/%d/ns/net", os.Getpid(), unix.Gettid())
|
||||
}
|
||||
|
||||
// Returns an object representing the current OS thread's network namespace
|
||||
func GetCurrentNS() (NetNS, error) {
|
||||
return GetNS(getCurrentThreadNetNSPath())
|
||||
}
|
||||
|
||||
// Returns an object representing the namespace referred to by @path
|
||||
func GetNS(nspath string) (NetNS, error) {
|
||||
fd, err := os.Open(nspath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to open %v: %v", nspath, err)
|
||||
}
|
||||
|
||||
isNSFS, err := IsNSFS(nspath)
|
||||
if err != nil {
|
||||
fd.Close()
|
||||
return nil, err
|
||||
}
|
||||
if !isNSFS {
|
||||
fd.Close()
|
||||
return nil, fmt.Errorf("%v is not of type NSFS", nspath)
|
||||
}
|
||||
|
||||
return &netNS{file: fd}, nil
|
||||
}
|
||||
|
||||
// Returns whether or not the nspath argument points to a network namespace
|
||||
func IsNSFS(nspath string) (bool, error) {
|
||||
const NSFS_MAGIC = 0x6e736673
|
||||
|
||||
stat := syscall.Statfs_t{}
|
||||
if err := syscall.Statfs(nspath, &stat); err != nil {
|
||||
return false, fmt.Errorf("failed to Statfs %q: %v", nspath, err)
|
||||
}
|
||||
|
||||
return stat.Type == NSFS_MAGIC, nil
|
||||
}
|
||||
|
||||
// Creates a new persistent network namespace and returns an object
|
||||
// representing that namespace, without switching to it
|
||||
func NewNS() (NetNS, error) {
|
||||
@ -182,21 +114,6 @@ func NewNS() (NetNS, error) {
|
||||
return &netNS{file: fd, mounted: true}, nil
|
||||
}
|
||||
|
||||
func (ns *netNS) Path() string {
|
||||
return ns.file.Name()
|
||||
}
|
||||
|
||||
func (ns *netNS) Fd() uintptr {
|
||||
return ns.file.Fd()
|
||||
}
|
||||
|
||||
func (ns *netNS) errorIfClosed() error {
|
||||
if ns.closed {
|
||||
return fmt.Errorf("%q has already been closed", ns.file.Name())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ns *netNS) Close() error {
|
||||
if err := ns.errorIfClosed(); err != nil {
|
||||
return err
|
||||
@ -220,13 +137,127 @@ func (ns *netNS) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ns *netNS) Set() error {
|
||||
if err := ns.errorIfClosed(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := unix.Setns(int(ns.Fd()), unix.CLONE_NEWNET); err != nil {
|
||||
return fmt.Errorf("Error switching to ns %v: %v", ns.file.Name(), err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type NetNS interface {
|
||||
// Executes the passed closure in this object's network namespace,
|
||||
// attempting to restore the original namespace before returning.
|
||||
// However, since each OS thread can have a different network namespace,
|
||||
// and Go's thread scheduling is highly variable, callers cannot
|
||||
// guarantee any specific namespace is set unless operations that
|
||||
// require that namespace are wrapped with Do(). Also, no code called
|
||||
// from Do() should call runtime.UnlockOSThread(), or the risk
|
||||
// of executing code in an incorrect namespace will be greater. See
|
||||
// https://github.com/golang/go/wiki/LockOSThread for further details.
|
||||
Do(toRun func(NetNS) error) error
|
||||
|
||||
// Sets the current network namespace to this object's network namespace.
|
||||
// Note that since Go's thread scheduling is highly variable, callers
|
||||
// cannot guarantee the requested namespace will be the current namespace
|
||||
// after this function is called; to ensure this wrap operations that
|
||||
// require the namespace with Do() instead.
|
||||
Set() error
|
||||
|
||||
// Returns the filesystem path representing this object's network namespace
|
||||
Path() string
|
||||
|
||||
// Returns a file descriptor representing this object's network namespace
|
||||
Fd() uintptr
|
||||
|
||||
// Cleans up this instance of the network namespace; if this instance
|
||||
// is the last user the namespace will be destroyed
|
||||
Close() error
|
||||
}
|
||||
|
||||
type netNS struct {
|
||||
file *os.File
|
||||
mounted bool
|
||||
closed bool
|
||||
}
|
||||
|
||||
// netNS implements the NetNS interface
|
||||
var _ NetNS = &netNS{}
|
||||
|
||||
const (
|
||||
// https://github.com/torvalds/linux/blob/master/include/uapi/linux/magic.h
|
||||
NSFS_MAGIC = 0x6e736673
|
||||
PROCFS_MAGIC = 0x9fa0
|
||||
)
|
||||
|
||||
type NSPathNotExistErr struct{ msg string }
|
||||
|
||||
func (e NSPathNotExistErr) Error() string { return e.msg }
|
||||
|
||||
type NSPathNotNSErr struct{ msg string }
|
||||
|
||||
func (e NSPathNotNSErr) Error() string { return e.msg }
|
||||
|
||||
func IsNSorErr(nspath string) error {
|
||||
stat := syscall.Statfs_t{}
|
||||
if err := syscall.Statfs(nspath, &stat); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
err = NSPathNotExistErr{msg: fmt.Sprintf("failed to Statfs %q: %v", nspath, err)}
|
||||
} else {
|
||||
err = fmt.Errorf("failed to Statfs %q: %v", nspath, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
switch stat.Type {
|
||||
case PROCFS_MAGIC, NSFS_MAGIC:
|
||||
return nil
|
||||
default:
|
||||
return NSPathNotNSErr{msg: fmt.Sprintf("unknown FS magic on %q: %x", nspath, stat.Type)}
|
||||
}
|
||||
}
|
||||
|
||||
// Returns an object representing the namespace referred to by @path
|
||||
func GetNS(nspath string) (NetNS, error) {
|
||||
err := IsNSorErr(nspath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fd, err := os.Open(nspath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &netNS{file: fd}, nil
|
||||
}
|
||||
|
||||
func (ns *netNS) Path() string {
|
||||
return ns.file.Name()
|
||||
}
|
||||
|
||||
func (ns *netNS) Fd() uintptr {
|
||||
return ns.file.Fd()
|
||||
}
|
||||
|
||||
func (ns *netNS) errorIfClosed() error {
|
||||
if ns.closed {
|
||||
return fmt.Errorf("%q has already been closed", ns.file.Name())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ns *netNS) Do(toRun func(NetNS) error) error {
|
||||
if err := ns.errorIfClosed(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
containedCall := func(hostNS NetNS) error {
|
||||
threadNS, err := GetNS(getCurrentThreadNetNSPath())
|
||||
threadNS, err := GetCurrentNS()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open current netns: %v", err)
|
||||
}
|
||||
@ -242,7 +273,7 @@ func (ns *netNS) Do(toRun func(NetNS) error) error {
|
||||
}
|
||||
|
||||
// save a handle to current network namespace
|
||||
hostNS, err := GetNS(getCurrentThreadNetNSPath())
|
||||
hostNS, err := GetCurrentNS()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to open current namespace: %v", err)
|
||||
}
|
||||
@ -262,18 +293,6 @@ func (ns *netNS) Do(toRun func(NetNS) error) error {
|
||||
return innerError
|
||||
}
|
||||
|
||||
func (ns *netNS) Set() error {
|
||||
if err := ns.errorIfClosed(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, _, err := unix.Syscall(unix.SYS_SETNS, ns.Fd(), uintptr(unix.CLONE_NEWNET), 0); err != 0 {
|
||||
return fmt.Errorf("Error switching to ns %v: %v", ns.file.Name(), err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// WithNetNSPath executes the passed closure under the given network
|
||||
// namespace, restoring the original namespace afterwards.
|
||||
func WithNetNSPath(nspath string, toRun func(NetNS) error) error {
|
@ -21,7 +21,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/ns"
|
||||
"github.com/containernetworking/plugins/pkg/ns"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
"golang.org/x/sys/unix"
|
||||
@ -180,7 +180,9 @@ var _ = Describe("Linux namespace operations", func() {
|
||||
defer os.Remove(nspath)
|
||||
|
||||
_, err = ns.GetNS(nspath)
|
||||
Expect(err).To(MatchError(fmt.Sprintf("%v is not of type NSFS", nspath)))
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err).To(BeAssignableToTypeOf(ns.NSPathNotNSErr{}))
|
||||
Expect(err).NotTo(BeAssignableToTypeOf(ns.NSPathNotExistErr{}))
|
||||
})
|
||||
})
|
||||
|
||||
@ -212,12 +214,11 @@ var _ = Describe("Linux namespace operations", func() {
|
||||
})
|
||||
})
|
||||
|
||||
Describe("IsNSFS", func() {
|
||||
Describe("IsNSorErr", func() {
|
||||
It("should detect a namespace", func() {
|
||||
createdNetNS, err := ns.NewNS()
|
||||
isNSFS, err := ns.IsNSFS(createdNetNS.Path())
|
||||
err = ns.IsNSorErr(createdNetNS.Path())
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(isNSFS).To(Equal(true))
|
||||
})
|
||||
|
||||
It("should refuse other paths", func() {
|
||||
@ -228,9 +229,17 @@ var _ = Describe("Linux namespace operations", func() {
|
||||
nspath := tempFile.Name()
|
||||
defer os.Remove(nspath)
|
||||
|
||||
isNSFS, err := ns.IsNSFS(nspath)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(isNSFS).To(Equal(false))
|
||||
err = ns.IsNSorErr(nspath)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err).To(BeAssignableToTypeOf(ns.NSPathNotNSErr{}))
|
||||
Expect(err).NotTo(BeAssignableToTypeOf(ns.NSPathNotExistErr{}))
|
||||
})
|
||||
|
||||
It("should error on non-existing paths", func() {
|
||||
err := ns.IsNSorErr("/tmp/IDoNotExist")
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err).To(BeAssignableToTypeOf(ns.NSPathNotExistErr{}))
|
||||
Expect(err).NotTo(BeAssignableToTypeOf(ns.NSPathNotNSErr{}))
|
||||
})
|
||||
})
|
||||
})
|
117
pkg/skel/skel.go
117
pkg/skel/skel.go
@ -1,117 +0,0 @@
|
||||
// Copyright 2014 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 skel provides skeleton code for a CNI plugin.
|
||||
// In particular, it implements argument parsing and validation.
|
||||
package skel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
)
|
||||
|
||||
// CmdArgs captures all the arguments passed in to the plugin
|
||||
// via both env vars and stdin
|
||||
type CmdArgs struct {
|
||||
ContainerID string
|
||||
Netns string
|
||||
IfName string
|
||||
Args string
|
||||
Path string
|
||||
StdinData []byte
|
||||
}
|
||||
|
||||
// PluginMain is the "main" for a plugin. It accepts
|
||||
// two callback functions for add and del commands.
|
||||
func PluginMain(cmdAdd, cmdDel func(_ *CmdArgs) error) {
|
||||
var cmd, contID, netns, ifName, args, path string
|
||||
|
||||
vars := []struct {
|
||||
name string
|
||||
val *string
|
||||
req bool
|
||||
}{
|
||||
{"CNI_COMMAND", &cmd, true},
|
||||
{"CNI_CONTAINERID", &contID, false},
|
||||
{"CNI_NETNS", &netns, true},
|
||||
{"CNI_IFNAME", &ifName, true},
|
||||
{"CNI_ARGS", &args, false},
|
||||
{"CNI_PATH", &path, true},
|
||||
}
|
||||
|
||||
argsMissing := false
|
||||
for _, v := range vars {
|
||||
*v.val = os.Getenv(v.name)
|
||||
if v.req && *v.val == "" {
|
||||
log.Printf("%v env variable missing", v.name)
|
||||
argsMissing = true
|
||||
}
|
||||
}
|
||||
|
||||
if argsMissing {
|
||||
dieMsg("required env variables missing")
|
||||
}
|
||||
|
||||
stdinData, err := ioutil.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
dieMsg("error reading from stdin: %v", err)
|
||||
}
|
||||
|
||||
cmdArgs := &CmdArgs{
|
||||
ContainerID: contID,
|
||||
Netns: netns,
|
||||
IfName: ifName,
|
||||
Args: args,
|
||||
Path: path,
|
||||
StdinData: stdinData,
|
||||
}
|
||||
|
||||
switch cmd {
|
||||
case "ADD":
|
||||
err = cmdAdd(cmdArgs)
|
||||
|
||||
case "DEL":
|
||||
err = cmdDel(cmdArgs)
|
||||
|
||||
default:
|
||||
dieMsg("unknown CNI_COMMAND: %v", cmd)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if e, ok := err.(*types.Error); ok {
|
||||
// don't wrap Error in Error
|
||||
dieErr(e)
|
||||
}
|
||||
dieMsg(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func dieMsg(f string, args ...interface{}) {
|
||||
e := &types.Error{
|
||||
Code: 100,
|
||||
Msg: fmt.Sprintf(f, args...),
|
||||
}
|
||||
dieErr(e)
|
||||
}
|
||||
|
||||
func dieErr(e *types.Error) {
|
||||
if err := e.Print(); err != nil {
|
||||
log.Print("Error writing error JSON to stdout: ", err)
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
@ -1,75 +0,0 @@
|
||||
// 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 skel
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("Skel", func() {
|
||||
var (
|
||||
fNoop = func(_ *CmdArgs) error { return nil }
|
||||
// fErr = func(_ *CmdArgs) error { return errors.New("dummy") }
|
||||
envVars = []struct {
|
||||
name string
|
||||
val string
|
||||
}{
|
||||
{"CNI_CONTAINERID", "dummy"},
|
||||
{"CNI_NETNS", "dummy"},
|
||||
{"CNI_IFNAME", "dummy"},
|
||||
{"CNI_ARGS", "dummy"},
|
||||
{"CNI_PATH", "dummy"},
|
||||
}
|
||||
)
|
||||
|
||||
It("Must be possible to set the env vars", func() {
|
||||
for _, v := range envVars {
|
||||
err := os.Setenv(v.name, v.val)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
}
|
||||
})
|
||||
|
||||
Context("When dummy environment variables are passed", func() {
|
||||
|
||||
It("should not fail with ADD and noop callback", func() {
|
||||
err := os.Setenv("CNI_COMMAND", "ADD")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
PluginMain(fNoop, nil)
|
||||
})
|
||||
|
||||
// TODO: figure out howto mock printing and os.Exit()
|
||||
// It("should fail with ADD and error callback", func() {
|
||||
// err := os.Setenv("CNI_COMMAND", "ADD")
|
||||
// Expect(err).NotTo(HaveOccurred())
|
||||
// PluginMain(fErr, nil)
|
||||
// })
|
||||
|
||||
It("should not fail with DEL and noop callback", func() {
|
||||
err := os.Setenv("CNI_COMMAND", "DEL")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
PluginMain(nil, fNoop)
|
||||
})
|
||||
|
||||
// TODO: figure out howto mock printing and os.Exit()
|
||||
// It("should fail with DEL and error callback", func() {
|
||||
// err := os.Setenv("CNI_COMMAND", "DEL")
|
||||
// Expect(err).NotTo(HaveOccurred())
|
||||
// PluginMain(fErr, nil)
|
||||
// })
|
||||
})
|
||||
})
|
33
pkg/testutils/bad_reader.go
Normal file
33
pkg/testutils/bad_reader.go
Normal file
@ -0,0 +1,33 @@
|
||||
// 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 testutils
|
||||
|
||||
import "errors"
|
||||
|
||||
// BadReader is an io.Reader which always errors
|
||||
type BadReader struct {
|
||||
Error error
|
||||
}
|
||||
|
||||
func (r *BadReader) Read(buffer []byte) (int, error) {
|
||||
if r.Error != nil {
|
||||
return 0, r.Error
|
||||
}
|
||||
return 0, errors.New("banana")
|
||||
}
|
||||
|
||||
func (r *BadReader) Close() error {
|
||||
return nil
|
||||
}
|
@ -15,11 +15,11 @@
|
||||
package testutils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
"github.com/containernetworking/cni/pkg/version"
|
||||
)
|
||||
|
||||
func envCleanup() {
|
||||
@ -29,7 +29,7 @@ func envCleanup() {
|
||||
os.Unsetenv("CNI_IFNAME")
|
||||
}
|
||||
|
||||
func CmdAddWithResult(cniNetns, cniIfname string, f func() error) (*types.Result, error) {
|
||||
func CmdAddWithResult(cniNetns, 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)
|
||||
@ -40,30 +40,38 @@ func CmdAddWithResult(cniNetns, cniIfname string, f func() error) (*types.Result
|
||||
oldStdout := os.Stdout
|
||||
r, w, err := os.Pipe()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
os.Stdout = w
|
||||
err = f()
|
||||
w.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// parse the result
|
||||
out, err := ioutil.ReadAll(r)
|
||||
var out []byte
|
||||
if err == nil {
|
||||
out, err = ioutil.ReadAll(r)
|
||||
}
|
||||
os.Stdout = oldStdout
|
||||
|
||||
// Return errors after restoring stdout so Ginkgo will correctly
|
||||
// emit verbose error information on stdout
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
result := types.Result{}
|
||||
err = json.Unmarshal(out, &result)
|
||||
// Plugin must return result in same version as specified in netconf
|
||||
versionDecoder := &version.ConfigDecoder{}
|
||||
confVersion, err := versionDecoder.Decode(conf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
result, err := version.NewResult(confVersion, out)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return result, out, nil
|
||||
}
|
||||
|
||||
func CmdDelWithResult(cniNetns, cniIfname string, f func() error) error {
|
||||
|
74
pkg/testutils/echosvr/echosvr_test.go
Normal file
74
pkg/testutils/echosvr/echosvr_test.go
Normal file
@ -0,0 +1,74 @@
|
||||
package main_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/onsi/gomega/gbytes"
|
||||
"github.com/onsi/gomega/gexec"
|
||||
)
|
||||
|
||||
var binaryPath 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) {
|
||||
binaryPath = string(data)
|
||||
})
|
||||
|
||||
var _ = SynchronizedAfterSuite(func() {}, func() {
|
||||
gexec.CleanupBuildArtifacts()
|
||||
})
|
||||
|
||||
var _ = Describe("Echosvr", func() {
|
||||
var session *gexec.Session
|
||||
BeforeEach(func() {
|
||||
var err error
|
||||
cmd := exec.Command(binaryPath)
|
||||
session, err = gexec.Start(cmd, GinkgoWriter, GinkgoWriter)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
session.Kill().Wait()
|
||||
})
|
||||
|
||||
It("starts and doesn't terminate immediately", func() {
|
||||
Consistently(session).ShouldNot(gexec.Exit())
|
||||
})
|
||||
|
||||
tryConnect := func() (net.Conn, error) {
|
||||
programOutput := session.Out.Contents()
|
||||
addr := strings.TrimSpace(string(programOutput))
|
||||
|
||||
conn, err := net.Dial("tcp", addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return conn, err
|
||||
}
|
||||
|
||||
It("prints its listening address to stdout", func() {
|
||||
Eventually(session.Out).Should(gbytes.Say("\n"))
|
||||
conn, err := tryConnect()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
conn.Close()
|
||||
})
|
||||
|
||||
It("will echo data back to us", func() {
|
||||
Eventually(session.Out).Should(gbytes.Say("\n"))
|
||||
conn, err := tryConnect()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
defer conn.Close()
|
||||
|
||||
fmt.Fprintf(conn, "hello")
|
||||
Expect(ioutil.ReadAll(conn)).To(Equal([]byte("hello")))
|
||||
})
|
||||
})
|
13
pkg/testutils/echosvr/init_test.go
Normal file
13
pkg/testutils/echosvr/init_test.go
Normal file
@ -0,0 +1,13 @@
|
||||
package main_test
|
||||
|
||||
import (
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEchosvr(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Testutils Echosvr Suite")
|
||||
}
|
38
pkg/testutils/echosvr/main.go
Normal file
38
pkg/testutils/echosvr/main.go
Normal file
@ -0,0 +1,38 @@
|
||||
// Echosvr is a simple TCP echo server
|
||||
//
|
||||
// It prints its listen address on stdout
|
||||
// 127.0.0.1:xxxxx
|
||||
// A test should wait for this line, parse it
|
||||
// and may then attempt to connect.
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
)
|
||||
|
||||
func main() {
|
||||
listener, err := net.Listen("tcp", ":")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
_, port, err := net.SplitHostPort(listener.Addr().String())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Printf("127.0.0.1:%s\n", port)
|
||||
for {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
go handleConnection(conn)
|
||||
}
|
||||
}
|
||||
|
||||
func handleConnection(conn net.Conn) {
|
||||
buf := make([]byte, 512)
|
||||
nBytesRead, _ := conn.Read(buf)
|
||||
conn.Write(buf[0:nBytesRead])
|
||||
conn.Close()
|
||||
}
|
55
pkg/testutils/ping.go
Normal file
55
pkg/testutils/ping.go
Normal file
@ -0,0 +1,55 @@
|
||||
// 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 testutils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// Ping shells out to the `ping` command. Returns nil if successful.
|
||||
func Ping(saddr, daddr string, isV6 bool, timeoutSec int) error {
|
||||
args := []string{
|
||||
"-c", "1",
|
||||
"-W", strconv.Itoa(timeoutSec),
|
||||
"-I", saddr,
|
||||
daddr,
|
||||
}
|
||||
|
||||
bin := "ping"
|
||||
if isV6 {
|
||||
bin = "ping6"
|
||||
}
|
||||
|
||||
cmd := exec.Command(bin, args...)
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
switch e := err.(type) {
|
||||
case *exec.ExitError:
|
||||
return fmt.Errorf("%v exit status %d: %s",
|
||||
args, e.Sys().(syscall.WaitStatus).ExitStatus(),
|
||||
stderr.String())
|
||||
default:
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -1,106 +0,0 @@
|
||||
// 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 types_test
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
. "github.com/containernetworking/cni/pkg/types"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/ginkgo/extensions/table"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("UnmarshallableBool UnmarshalText", func() {
|
||||
DescribeTable("string to bool detection should succeed in all cases",
|
||||
func(inputs []string, expected bool) {
|
||||
for _, s := range inputs {
|
||||
var ub UnmarshallableBool
|
||||
err := ub.UnmarshalText([]byte(s))
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(ub).To(Equal(UnmarshallableBool(expected)))
|
||||
}
|
||||
},
|
||||
Entry("parse to true", []string{"True", "true", "1"}, true),
|
||||
Entry("parse to false", []string{"False", "false", "0"}, false),
|
||||
)
|
||||
|
||||
Context("When passed an invalid value", func() {
|
||||
It("should result in an error", func() {
|
||||
var ub UnmarshallableBool
|
||||
err := ub.UnmarshalText([]byte("invalid"))
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
var _ = Describe("GetKeyField", func() {
|
||||
type testcontainer struct {
|
||||
Valid string `json:"valid,omitempty"`
|
||||
}
|
||||
var (
|
||||
container = testcontainer{Valid: "valid"}
|
||||
containerInterface = func(i interface{}) interface{} { return i }(&container)
|
||||
containerValue = reflect.ValueOf(containerInterface)
|
||||
)
|
||||
Context("When a valid field is provided", func() {
|
||||
It("should return the correct field", func() {
|
||||
field := GetKeyField("Valid", containerValue)
|
||||
Expect(field.String()).To(Equal("valid"))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
var _ = Describe("LoadArgs", func() {
|
||||
Context("When no arguments are passed", func() {
|
||||
It("LoadArgs should succeed", func() {
|
||||
err := LoadArgs("", struct{}{})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
})
|
||||
|
||||
Context("When unknown arguments are passed and ignored", func() {
|
||||
It("LoadArgs should succeed", func() {
|
||||
ca := CommonArgs{}
|
||||
err := LoadArgs("IgnoreUnknown=True;Unk=nown", &ca)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
})
|
||||
|
||||
Context("When unknown arguments are passed and not ignored", func() {
|
||||
It("LoadArgs should fail", func() {
|
||||
ca := CommonArgs{}
|
||||
err := LoadArgs("Unk=nown", &ca)
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
})
|
||||
|
||||
Context("When unknown arguments are passed and explicitly not ignored", func() {
|
||||
It("LoadArgs should fail", func() {
|
||||
ca := CommonArgs{}
|
||||
err := LoadArgs("IgnoreUnknown=0, Unk=nown", &ca)
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
})
|
||||
|
||||
Context("When known arguments are passed", func() {
|
||||
It("LoadArgs should succeed", func() {
|
||||
ca := CommonArgs{}
|
||||
err := LoadArgs("IgnoreUnknown=1", &ca)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
})
|
||||
})
|
63
pkg/utils/hwaddr/hwaddr.go
Normal file
63
pkg/utils/hwaddr/hwaddr.go
Normal file
@ -0,0 +1,63 @@
|
||||
// Copyright 2016 CNI authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package hwaddr
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
)
|
||||
|
||||
const (
|
||||
ipRelevantByteLen = 4
|
||||
PrivateMACPrefixString = "0a:58"
|
||||
)
|
||||
|
||||
var (
|
||||
// private mac prefix safe to use
|
||||
PrivateMACPrefix = []byte{0x0a, 0x58}
|
||||
)
|
||||
|
||||
type SupportIp4OnlyErr struct{ msg string }
|
||||
|
||||
func (e SupportIp4OnlyErr) Error() string { return e.msg }
|
||||
|
||||
type MacParseErr struct{ msg string }
|
||||
|
||||
func (e MacParseErr) Error() string { return e.msg }
|
||||
|
||||
type InvalidPrefixLengthErr struct{ msg string }
|
||||
|
||||
func (e InvalidPrefixLengthErr) Error() string { return e.msg }
|
||||
|
||||
// GenerateHardwareAddr4 generates 48 bit virtual mac addresses based on the IP4 input.
|
||||
func GenerateHardwareAddr4(ip net.IP, prefix []byte) (net.HardwareAddr, error) {
|
||||
switch {
|
||||
|
||||
case ip.To4() == nil:
|
||||
return nil, SupportIp4OnlyErr{msg: "GenerateHardwareAddr4 only supports valid IPv4 address as input"}
|
||||
|
||||
case len(prefix) != len(PrivateMACPrefix):
|
||||
return nil, InvalidPrefixLengthErr{msg: fmt.Sprintf(
|
||||
"Prefix has length %d instead of %d", len(prefix), len(PrivateMACPrefix)),
|
||||
}
|
||||
}
|
||||
|
||||
ipByteLen := len(ip)
|
||||
return (net.HardwareAddr)(
|
||||
append(
|
||||
prefix,
|
||||
ip[ipByteLen-ipRelevantByteLen:ipByteLen]...),
|
||||
), nil
|
||||
}
|
@ -12,7 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package invoke_test
|
||||
package hwaddr_test
|
||||
|
||||
import (
|
||||
. "github.com/onsi/ginkgo"
|
||||
@ -21,7 +21,7 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestInvoke(t *testing.T) {
|
||||
func TestHwaddr(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Invoke Suite")
|
||||
RunSpecs(t, "Hwaddr Suite")
|
||||
}
|
74
pkg/utils/hwaddr/hwaddr_test.go
Normal file
74
pkg/utils/hwaddr/hwaddr_test.go
Normal file
@ -0,0 +1,74 @@
|
||||
// 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 hwaddr_test
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/containernetworking/plugins/pkg/utils/hwaddr"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("Hwaddr", func() {
|
||||
Context("Generate Hardware Address", func() {
|
||||
It("generate hardware address based on ipv4 address", func() {
|
||||
testCases := []struct {
|
||||
ip net.IP
|
||||
expectedMAC net.HardwareAddr
|
||||
}{
|
||||
{
|
||||
ip: net.ParseIP("10.0.0.2"),
|
||||
expectedMAC: (net.HardwareAddr)(append(hwaddr.PrivateMACPrefix, 0x0a, 0x00, 0x00, 0x02)),
|
||||
},
|
||||
{
|
||||
ip: net.ParseIP("10.250.0.244"),
|
||||
expectedMAC: (net.HardwareAddr)(append(hwaddr.PrivateMACPrefix, 0x0a, 0xfa, 0x00, 0xf4)),
|
||||
},
|
||||
{
|
||||
ip: net.ParseIP("172.17.0.2"),
|
||||
expectedMAC: (net.HardwareAddr)(append(hwaddr.PrivateMACPrefix, 0xac, 0x11, 0x00, 0x02)),
|
||||
},
|
||||
{
|
||||
ip: net.IPv4(byte(172), byte(17), byte(0), byte(2)),
|
||||
expectedMAC: (net.HardwareAddr)(append(hwaddr.PrivateMACPrefix, 0xac, 0x11, 0x00, 0x02)),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
mac, err := hwaddr.GenerateHardwareAddr4(tc.ip, hwaddr.PrivateMACPrefix)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(mac).To(Equal(tc.expectedMAC))
|
||||
}
|
||||
})
|
||||
|
||||
It("return error if input is not ipv4 address", func() {
|
||||
testCases := []net.IP{
|
||||
net.ParseIP(""),
|
||||
net.ParseIP("2001:db8:0:1:1:1:1:1"),
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
_, err := hwaddr.GenerateHardwareAddr4(tc, hwaddr.PrivateMACPrefix)
|
||||
Expect(err).To(BeAssignableToTypeOf(hwaddr.SupportIp4OnlyErr{}))
|
||||
}
|
||||
})
|
||||
|
||||
It("return error if prefix is invalid", func() {
|
||||
_, err := hwaddr.GenerateHardwareAddr4(net.ParseIP("10.0.0.2"), []byte{0x58})
|
||||
Expect(err).To(BeAssignableToTypeOf(hwaddr.InvalidPrefixLengthErr{}))
|
||||
})
|
||||
})
|
||||
})
|
@ -12,8 +12,6 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// build +linux
|
||||
|
||||
package sysctl
|
||||
|
||||
import (
|
||||
@ -26,7 +24,7 @@ import (
|
||||
// Sysctl provides a method to set/get values from /proc/sys - in linux systems
|
||||
// new interface to set/get values of variables formerly handled by sysctl syscall
|
||||
// If optional `params` have only one string value - this function will
|
||||
// set this value into coresponding sysctl variable
|
||||
// set this value into corresponding sysctl variable
|
||||
func Sysctl(name string, params ...string) (string, error) {
|
||||
if len(params) > 1 {
|
||||
return "", fmt.Errorf("unexcepted additional parameters")
|
||||
|
@ -3,7 +3,7 @@
|
||||
## Overview
|
||||
|
||||
With dhcp plugin the containers can get an IP allocated by a DHCP server already running on your network.
|
||||
This can be especially useful with plugin types such as [macvlan](https://github.com/containernetworking/cni/blob/master/Documentation/macvlan.md).
|
||||
This can be especially useful with plugin types such as [macvlan](../../main/macvlan/README.md).
|
||||
Because a DHCP lease must be periodically renewed for the duration of container lifetime, a separate daemon is required to be running.
|
||||
The same plugin binary can also be run in the daemon mode.
|
||||
|
||||
@ -16,6 +16,10 @@ $ rm -f /run/cni/dhcp.sock
|
||||
$ ./dhcp daemon
|
||||
```
|
||||
|
||||
If given `-pidfile <path>` arguments after 'daemon', the dhcp plugin will write
|
||||
its PID to the given file.
|
||||
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.
|
||||
|
||||
Alternatively, you can use systemd socket activation protocol.
|
||||
Be sure that the .socket file uses /run/cni/dhcp.sock as the socket path.
|
||||
|
@ -18,7 +18,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/rpc"
|
||||
@ -29,6 +29,7 @@ import (
|
||||
|
||||
"github.com/containernetworking/cni/pkg/skel"
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
"github.com/containernetworking/cni/pkg/types/current"
|
||||
"github.com/coreos/go-systemd/activation"
|
||||
)
|
||||
|
||||
@ -38,8 +39,9 @@ const resendCount = 3
|
||||
var errNoMoreTries = errors.New("no more tries")
|
||||
|
||||
type DHCP struct {
|
||||
mux sync.Mutex
|
||||
leases map[string]*DHCPLease
|
||||
mux sync.Mutex
|
||||
leases map[string]*DHCPLease
|
||||
hostNetnsPrefix string
|
||||
}
|
||||
|
||||
func newDHCP() *DHCP {
|
||||
@ -50,14 +52,15 @@ func newDHCP() *DHCP {
|
||||
|
||||
// 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 *types.Result) error {
|
||||
func (d *DHCP) Allocate(args *skel.CmdArgs, result *current.Result) error {
|
||||
conf := types.NetConf{}
|
||||
if err := json.Unmarshal(args.StdinData, &conf); err != nil {
|
||||
return fmt.Errorf("error parsing netconf: %v", err)
|
||||
}
|
||||
|
||||
clientID := args.ContainerID + "/" + conf.Name
|
||||
l, err := AcquireLease(clientID, args.Netns, args.IfName)
|
||||
hostNetns := d.hostNetnsPrefix + args.Netns
|
||||
l, err := AcquireLease(clientID, hostNetns, args.IfName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -70,11 +73,12 @@ func (d *DHCP) Allocate(args *skel.CmdArgs, result *types.Result) error {
|
||||
|
||||
d.setLease(args.ContainerID, conf.Name, l)
|
||||
|
||||
result.IP4 = &types.IPConfig{
|
||||
IP: *ipn,
|
||||
result.IPs = []*current.IPConfig{{
|
||||
Version: "4",
|
||||
Address: *ipn,
|
||||
Gateway: l.Gateway(),
|
||||
Routes: l.Routes(),
|
||||
}
|
||||
}}
|
||||
result.Routes = l.Routes()
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -89,10 +93,10 @@ func (d *DHCP) Release(args *skel.CmdArgs, reply *struct{}) error {
|
||||
|
||||
if l := d.getLease(args.ContainerID, conf.Name); l != nil {
|
||||
l.Stop()
|
||||
return nil
|
||||
d.clearLease(args.ContainerID, conf.Name)
|
||||
}
|
||||
|
||||
return fmt.Errorf("lease not found: %v/%v", args.ContainerID, conf.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DHCP) getLease(contID, netName string) *DHCPLease {
|
||||
@ -115,6 +119,14 @@ func (d *DHCP) setLease(contID, netName string, l *DHCPLease) {
|
||||
d.leases[contID+netName] = l
|
||||
}
|
||||
|
||||
func (d *DHCP) clearLease(contID, netName string) {
|
||||
d.mux.Lock()
|
||||
defer d.mux.Unlock()
|
||||
|
||||
// TODO(eyakubovich): hash it to avoid collisions
|
||||
delete(d.leases, contID+netName)
|
||||
}
|
||||
|
||||
func getListener() (net.Listener, error) {
|
||||
l, err := activation.Listeners(true)
|
||||
if err != nil {
|
||||
@ -139,19 +151,30 @@ func getListener() (net.Listener, error) {
|
||||
}
|
||||
}
|
||||
|
||||
func runDaemon() {
|
||||
func runDaemon(pidfilePath string, hostPrefix string) error {
|
||||
// since other goroutines (on separate threads) will change namespaces,
|
||||
// ensure the RPC server does not get scheduled onto those
|
||||
runtime.LockOSThread()
|
||||
|
||||
// Write the pidfile
|
||||
if pidfilePath != "" {
|
||||
if !filepath.IsAbs(pidfilePath) {
|
||||
return fmt.Errorf("Error writing pidfile %q: path not absolute", pidfilePath)
|
||||
}
|
||||
if err := ioutil.WriteFile(pidfilePath, []byte(fmt.Sprintf("%d", os.Getpid())), 0644); err != nil {
|
||||
return fmt.Errorf("Error writing pidfile %q: %v", pidfilePath, err)
|
||||
}
|
||||
}
|
||||
|
||||
l, err := getListener()
|
||||
if err != nil {
|
||||
log.Printf("Error getting listener: %v", err)
|
||||
return
|
||||
return fmt.Errorf("Error getting listener: %v", err)
|
||||
}
|
||||
|
||||
dhcp := newDHCP()
|
||||
dhcp.hostNetnsPrefix = hostPrefix
|
||||
rpc.Register(dhcp)
|
||||
rpc.HandleHTTP()
|
||||
http.Serve(l, nil)
|
||||
return nil
|
||||
}
|
||||
|
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, "DHCP Suite")
|
||||
}
|
319
plugins/ipam/dhcp/dhcp_test.go
Normal file
319
plugins/ipam/dhcp/dhcp_test.go
Normal file
@ -0,0 +1,319 @@
|
||||
// Copyright 2015 CNI authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package 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/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 dhcpServerStart(netns ns.NetNS, leaseIP, serverIP net.IP, stopCh <-chan bool) (*sync.WaitGroup, error) {
|
||||
// Add the expected IP to the pool
|
||||
lp := memorypool.MemoryPool{}
|
||||
err := lp.AddLease(leasepool.Lease{IP: dhcp4.IPAdd(net.IPv4(192, 168, 1, 5), 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"
|
||||
pidfilePath string = "/var/run/cni/dhcp-client.pid"
|
||||
)
|
||||
|
||||
var _ = BeforeSuite(func() {
|
||||
os.Remove(socketPath)
|
||||
os.Remove(pidfilePath)
|
||||
})
|
||||
|
||||
var _ = AfterSuite(func() {
|
||||
os.Remove(socketPath)
|
||||
os.Remove(pidfilePath)
|
||||
})
|
||||
|
||||
var _ = Describe("DHCP Operations", func() {
|
||||
var originalNS, targetNS ns.NetNS
|
||||
var dhcpServerStopCh chan bool
|
||||
var dhcpServerDone *sync.WaitGroup
|
||||
var clientCmd *exec.Cmd
|
||||
|
||||
BeforeEach(func() {
|
||||
dhcpServerStopCh = make(chan bool)
|
||||
|
||||
// Create a new NetNS so we don't modify the host
|
||||
var err error
|
||||
originalNS, err = ns.NewNS()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
targetNS, err = ns.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),
|
||||
},
|
||||
})
|
||||
|
||||
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, dhcpServerStopCh)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Start the DHCP client daemon
|
||||
os.MkdirAll(pidfilePath, 0755)
|
||||
dhcpPluginPath, err := exec.LookPath("dhcp")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
clientCmd = exec.Command(dhcpPluginPath, "daemon")
|
||||
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())
|
||||
os.Remove(socketPath)
|
||||
os.Remove(pidfilePath)
|
||||
})
|
||||
|
||||
It("configures and deconfigures a link with ADD/DEL", func() {
|
||||
conf := `{
|
||||
"cniVersion": "0.3.1",
|
||||
"name": "mynet",
|
||||
"type": "ipvlan",
|
||||
"ipam": {
|
||||
"type": "dhcp"
|
||||
}
|
||||
}`
|
||||
|
||||
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.CmdAddWithResult(targetNS.Path(), contVethName, []byte(conf), 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.CmdDelWithResult(targetNS.Path(), contVethName, func() error {
|
||||
return cmdDel(args)
|
||||
})
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
|
||||
It("correctly handles multiple DELs for the same container", func() {
|
||||
conf := `{
|
||||
"cniVersion": "0.3.1",
|
||||
"name": "mynet",
|
||||
"type": "ipvlan",
|
||||
"ipam": {
|
||||
"type": "dhcp"
|
||||
}
|
||||
}`
|
||||
|
||||
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.CmdAddWithResult(targetNS.Path(), contVethName, []byte(conf), 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.CmdDelWithResult(targetNS.Path(), contVethName, func() error {
|
||||
return cmdDel(args)
|
||||
})
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
err = originalNS.Do(func(ns.NetNS) error {
|
||||
return testutils.CmdDelWithResult(targetNS.Path(), contVethName, func() error {
|
||||
return cmdDel(args)
|
||||
})
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
})
|
@ -20,14 +20,15 @@ import (
|
||||
"math/rand"
|
||||
"net"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/d2g/dhcp4"
|
||||
"github.com/d2g/dhcp4client"
|
||||
"github.com/vishvananda/netlink"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/ns"
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
"github.com/containernetworking/plugins/pkg/ns"
|
||||
)
|
||||
|
||||
// RFC 2131 suggests using exponential backoff, starting with 4sec
|
||||
@ -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,7 +108,9 @@ 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()
|
||||
}
|
||||
|
||||
@ -291,9 +295,27 @@ func (l *DHCPLease) Gateway() net.IP {
|
||||
return parseRouter(l.opts)
|
||||
}
|
||||
|
||||
func (l *DHCPLease) Routes() []types.Route {
|
||||
routes := parseRoutes(l.opts)
|
||||
return append(routes, parseCIDRRoutes(l.opts)...)
|
||||
func (l *DHCPLease) Routes() []*types.Route {
|
||||
routes := []*types.Route{}
|
||||
|
||||
// RFC 3442 states that if Classless Static Routes (option 121)
|
||||
// exist, we ignore Static Routes (option 33) and the Router/Gateway.
|
||||
opt121_routes := parseCIDRRoutes(l.opts)
|
||||
if len(opt121_routes) > 0 {
|
||||
return append(routes, opt121_routes...)
|
||||
}
|
||||
|
||||
// Append Static Routes
|
||||
routes = append(routes, parseRoutes(l.opts)...)
|
||||
|
||||
// The CNI spec says even if there is a gateway specified, we must
|
||||
// add a default route in the routes section.
|
||||
if gw := l.Gateway(); gw != nil {
|
||||
_, defaultRoute, _ := net.ParseCIDR("0.0.0.0/0")
|
||||
routes = append(routes, &types.Route{Dst: *defaultRoute, GW: gw})
|
||||
}
|
||||
|
||||
return routes
|
||||
}
|
||||
|
||||
// jitter returns a random value within [-span, span) range
|
||||
|
@ -15,31 +15,53 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/rpc"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
const socketPath = "/run/cni/dhcp.sock"
|
||||
|
||||
func main() {
|
||||
if len(os.Args) > 1 && os.Args[1] == "daemon" {
|
||||
runDaemon()
|
||||
var pidfilePath string
|
||||
var hostPrefix 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.Parse(os.Args[2:])
|
||||
|
||||
if err := runDaemon(pidfilePath, hostPrefix); err != nil {
|
||||
log.Printf(err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
} else {
|
||||
skel.PluginMain(cmdAdd, cmdDel)
|
||||
skel.PluginMain(cmdAdd, cmdDel, version.All)
|
||||
}
|
||||
}
|
||||
|
||||
func cmdAdd(args *skel.CmdArgs) error {
|
||||
result := types.Result{}
|
||||
if err := rpcCall("DHCP.Allocate", args, &result); err != nil {
|
||||
// Plugin must return result in same version as specified in netconf
|
||||
versionDecoder := &version.ConfigDecoder{}
|
||||
confVersion, err := versionDecoder.Decode(args.StdinData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return result.Print()
|
||||
|
||||
result := ¤t.Result{}
|
||||
if err := rpcCall("DHCP.Allocate", args, result); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return types.PrintResult(result, confVersion)
|
||||
}
|
||||
|
||||
func cmdDel(args *skel.CmdArgs) error {
|
||||
|
@ -40,17 +40,17 @@ func classfulSubnet(sn net.IP) net.IPNet {
|
||||
}
|
||||
}
|
||||
|
||||
func parseRoutes(opts dhcp4.Options) []types.Route {
|
||||
func parseRoutes(opts dhcp4.Options) []*types.Route {
|
||||
// StaticRoutes format: pairs of:
|
||||
// Dest = 4 bytes; Classful IP subnet
|
||||
// Router = 4 bytes; IP address of router
|
||||
|
||||
routes := []types.Route{}
|
||||
routes := []*types.Route{}
|
||||
if opt, ok := opts[dhcp4.OptionStaticRoute]; ok {
|
||||
for len(opt) >= 8 {
|
||||
sn := opt[0:4]
|
||||
r := opt[4:8]
|
||||
rt := types.Route{
|
||||
rt := &types.Route{
|
||||
Dst: classfulSubnet(sn),
|
||||
GW: r,
|
||||
}
|
||||
@ -62,10 +62,10 @@ func parseRoutes(opts dhcp4.Options) []types.Route {
|
||||
return routes
|
||||
}
|
||||
|
||||
func parseCIDRRoutes(opts dhcp4.Options) []types.Route {
|
||||
func parseCIDRRoutes(opts dhcp4.Options) []*types.Route {
|
||||
// See RFC4332 for format (http://tools.ietf.org/html/rfc3442)
|
||||
|
||||
routes := []types.Route{}
|
||||
routes := []*types.Route{}
|
||||
if opt, ok := opts[dhcp4.OptionClasslessRouteFormat]; ok {
|
||||
for len(opt) >= 5 {
|
||||
width := int(opt[0])
|
||||
@ -89,7 +89,7 @@ func parseCIDRRoutes(opts dhcp4.Options) []types.Route {
|
||||
|
||||
gw := net.IP(opt[octets+1 : octets+5])
|
||||
|
||||
rt := types.Route{
|
||||
rt := &types.Route{
|
||||
Dst: net.IPNet{
|
||||
IP: net.IP(sn),
|
||||
Mask: net.CIDRMask(width, 32),
|
||||
|
@ -22,16 +22,16 @@ import (
|
||||
"github.com/d2g/dhcp4"
|
||||
)
|
||||
|
||||
func validateRoutes(t *testing.T, routes []types.Route) {
|
||||
expected := []types.Route{
|
||||
types.Route{
|
||||
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{
|
||||
&types.Route{
|
||||
Dst: net.IPNet{
|
||||
IP: net.IPv4(192, 168, 1, 0),
|
||||
Mask: net.CIDRMask(24, 32),
|
||||
|
@ -1,86 +1,142 @@
|
||||
# host-local IP address manager
|
||||
# host-local IP address management plugin
|
||||
|
||||
host-local IPAM allocates IPv4 and IPv6 addresses out of a specified address range.
|
||||
host-local IPAM allocates IPv4 and IPv6 addresses out of a specified address range. Optionally,
|
||||
it can include a DNS configuration from a `resolv.conf` file on the host.
|
||||
|
||||
## Usage
|
||||
## Overview
|
||||
|
||||
### Obtain an IP
|
||||
host-local IPAM plugin allocates ip addresses out of a set of address ranges.
|
||||
It stores the state locally on the host filesystem, therefore ensuring uniqueness of IP addresses on a single host.
|
||||
|
||||
Given the following network configuration:
|
||||
The allocator can allocate multiple ranges, and supports sets of multiple (disjoint)
|
||||
subnets. The allocation strategy is loosely round-robin within each range set.
|
||||
|
||||
```
|
||||
## Example configurations
|
||||
|
||||
Note that the key `ranges` is a list of range sets. That is to say, the length
|
||||
of the top-level array is the number of addresses returned. The second-level
|
||||
array is a set of subnets to use as a pool of possible addresses.
|
||||
|
||||
This example configuration returns 2 IP addresses.
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "default",
|
||||
"ipam": {
|
||||
"type": "host-local",
|
||||
"subnet": "203.0.113.0/24"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Using the command line interface
|
||||
|
||||
```
|
||||
$ export CNI_COMMAND=ADD
|
||||
$ export CNI_CONTAINERID=f81d4fae-7dec-11d0-a765-00a0c91e6bf6
|
||||
$ ./host-local < $conf
|
||||
```
|
||||
|
||||
```
|
||||
{
|
||||
"ip4": {
|
||||
"ip": "203.0.113.1/24"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Backends
|
||||
|
||||
By default ipmanager stores IP allocations on the local filesystem using the IP address as the file name and the ID as contents. For example:
|
||||
|
||||
```
|
||||
$ ls /var/lib/cni/networks/default
|
||||
```
|
||||
```
|
||||
203.0.113.1 203.0.113.2
|
||||
```
|
||||
|
||||
```
|
||||
$ cat /var/lib/cni/networks/default/203.0.113.1
|
||||
```
|
||||
```
|
||||
f81d4fae-7dec-11d0-a765-00a0c91e6bf6
|
||||
```
|
||||
|
||||
## Configuration Files
|
||||
|
||||
|
||||
```
|
||||
{
|
||||
"name": "ipv6",
|
||||
"ipam": {
|
||||
"type": "host-local",
|
||||
"subnet": "3ffe:ffff:0:01ff::/64",
|
||||
"range-start": "3ffe:ffff:0:01ff::0010",
|
||||
"range-end": "3ffe:ffff:0:01ff::0020",
|
||||
"routes": [
|
||||
{ "dst": "3ffe:ffff:0:01ff::1/64" }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```
|
||||
{
|
||||
"name": "ipv4",
|
||||
"ipam": {
|
||||
"type": "host-local",
|
||||
"subnet": "203.0.113.1/24",
|
||||
"range-start": "203.0.113.10",
|
||||
"range-end": "203.0.113.20",
|
||||
"ranges": [
|
||||
[
|
||||
{
|
||||
"subnet": "10.10.0.0/16",
|
||||
"rangeStart": "10.10.1.20",
|
||||
"rangeEnd": "10.10.3.50",
|
||||
"gateway": "10.10.0.254"
|
||||
},
|
||||
{
|
||||
"subnet": "172.16.5.0/24"
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"subnet": "3ffe:ffff:0:01ff::/64",
|
||||
"rangeStart": "3ffe:ffff:0:01ff::0010",
|
||||
"rangeEnd": "3ffe:ffff:0:01ff::0020"
|
||||
}
|
||||
]
|
||||
],
|
||||
"routes": [
|
||||
{ "dst": "203.0.113.0/24" }
|
||||
]
|
||||
{ "dst": "0.0.0.0/0" },
|
||||
{ "dst": "192.168.0.0/16", "gw": "10.10.5.1" },
|
||||
{ "dst": "3ffe:ffff:0:01ff::1/64" }
|
||||
],
|
||||
"dataDir": "/run/my-orchestrator/container-ipam-state"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Previous versions of the `host-local` allocator did not support the `ranges`
|
||||
property, and instead expected a single range on the top level. This is
|
||||
deprecated but still supported.
|
||||
```json
|
||||
{
|
||||
"ipam": {
|
||||
"type": "host-local",
|
||||
"subnet": "3ffe:ffff:0:01ff::/64",
|
||||
"rangeStart": "3ffe:ffff:0:01ff::0010",
|
||||
"rangeEnd": "3ffe:ffff:0:01ff::0020",
|
||||
"routes": [
|
||||
{ "dst": "3ffe:ffff:0:01ff::1/64" }
|
||||
],
|
||||
"resolvConf": "/etc/resolv.conf"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
We can test it out on the command-line:
|
||||
|
||||
```bash
|
||||
$ echo '{ "cniVersion": "0.3.1", "name": "examplenet", "ipam": { "type": "host-local", "ranges": [ [{"subnet": "203.0.113.0/24"}], [{"subnet": "2001:db8:1::/64"}]], "dataDir": "/tmp/cni-example" } }' | CNI_COMMAND=ADD CNI_CONTAINERID=example CNI_NETNS=/dev/null CNI_IFNAME=dummy0 CNI_PATH=. ./host-local
|
||||
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"ips": [
|
||||
{
|
||||
"version": "4",
|
||||
"address": "203.0.113.2/24",
|
||||
"gateway": "203.0.113.1"
|
||||
},
|
||||
{
|
||||
"version": "6",
|
||||
"address": "2001:db8:1::2/64",
|
||||
"gateway": "2001:db8:1::1"
|
||||
}
|
||||
],
|
||||
"dns": {}
|
||||
}
|
||||
```
|
||||
|
||||
## Network configuration reference
|
||||
|
||||
* `type` (string, required): "host-local".
|
||||
* `routes` (string, optional): list of routes to add to the container namespace. Each route is a dictionary with "dst" and optional "gw" fields. If "gw" is omitted, value of "gateway" will be used.
|
||||
* `resolvConf` (string, optional): Path to a `resolv.conf` on the host to parse and return as the DNS configuration
|
||||
* `dataDir` (string, optional): Path to a directory to use for maintaining state, e.g. which IPs have been allocated to which containers
|
||||
* `ranges`, (array, required, nonempty) an array of arrays of range objects:
|
||||
* `subnet` (string, required): CIDR block to allocate out of.
|
||||
* `rangeStart` (string, optional): IP inside of "subnet" from which to start allocating addresses. Defaults to ".2" IP inside of the "subnet" block.
|
||||
* `rangeEnd` (string, optional): IP inside of "subnet" with which to end allocating addresses. Defaults to ".254" IP inside of the "subnet" block for ipv4, ".255" for IPv6
|
||||
* `gateway` (string, optional): IP inside of "subnet" to designate as the gateway. Defaults to ".1" IP inside of the "subnet" block.
|
||||
|
||||
Older versions of the `host-local` plugin did not support the `ranges` array. Instead,
|
||||
all the properties in the `range` object were top-level. This is still supported but deprecated.
|
||||
|
||||
## Supported arguments
|
||||
The following [CNI_ARGS](https://github.com/containernetworking/cni/blob/master/SPEC.md#parameters) are supported:
|
||||
|
||||
* `ip`: request a specific IP address from a subnet.
|
||||
|
||||
The following [args conventions](https://github.com/containernetworking/cni/blob/master/CONVENTIONS.md) are supported:
|
||||
|
||||
* `ips` (array of strings): A list of custom IPs to attempt to allocate
|
||||
|
||||
The following [Capability Args](https://github.com/containernetworking/cni/blob/master/CONVENTIONS.md) are supported:
|
||||
|
||||
* `ipRanges`: The exact same as the `ranges` array - a list of address pools
|
||||
|
||||
### Custom IP allocation
|
||||
For every requested custom IP, the `host-local` allocator will request that IP
|
||||
if it falls within one of the `range` objects. Thus it is possible to specify
|
||||
multiple custom IPs and multiple ranges.
|
||||
|
||||
If any requested IPs cannot be reserved, either because they are already in use
|
||||
or are not part of a specified range, the plugin will return an error.
|
||||
|
||||
|
||||
## Files
|
||||
|
||||
Allocated IP addresses are stored as files in `/var/lib/cni/networks/$NETWORK_NAME`.
|
||||
The path can be customized with the `dataDir` option listed above. Environments
|
||||
where IPs are released automatically on reboot (e.g. running containers are not
|
||||
restored) may wish to specify `/var/run/cni` or another tmpfs mounted directory
|
||||
instead.
|
||||
|
@ -1,165 +0,0 @@
|
||||
// Copyright 2015 CNI authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/ip"
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
"github.com/containernetworking/cni/plugins/ipam/host-local/backend"
|
||||
)
|
||||
|
||||
type IPAllocator struct {
|
||||
start net.IP
|
||||
end net.IP
|
||||
conf *IPAMConfig
|
||||
store backend.Store
|
||||
}
|
||||
|
||||
func NewIPAllocator(conf *IPAMConfig, store backend.Store) (*IPAllocator, error) {
|
||||
var (
|
||||
start net.IP
|
||||
end net.IP
|
||||
err error
|
||||
)
|
||||
start, end, err = networkRange((*net.IPNet)(&conf.Subnet))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// skip the .0 address
|
||||
start = ip.NextIP(start)
|
||||
|
||||
if conf.RangeStart != nil {
|
||||
if err := validateRangeIP(conf.RangeStart, (*net.IPNet)(&conf.Subnet)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
start = conf.RangeStart
|
||||
}
|
||||
if conf.RangeEnd != nil {
|
||||
if err := validateRangeIP(conf.RangeEnd, (*net.IPNet)(&conf.Subnet)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// RangeEnd is inclusive
|
||||
end = ip.NextIP(conf.RangeEnd)
|
||||
}
|
||||
|
||||
return &IPAllocator{start, end, conf, store}, nil
|
||||
}
|
||||
|
||||
func validateRangeIP(ip net.IP, ipnet *net.IPNet) error {
|
||||
if !ipnet.Contains(ip) {
|
||||
return fmt.Errorf("%s not in network: %s", ip, ipnet)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Returns newly allocated IP along with its config
|
||||
func (a *IPAllocator) Get(id string) (*types.IPConfig, error) {
|
||||
a.store.Lock()
|
||||
defer a.store.Unlock()
|
||||
|
||||
gw := a.conf.Gateway
|
||||
if gw == nil {
|
||||
gw = ip.NextIP(a.conf.Subnet.IP)
|
||||
}
|
||||
|
||||
var requestedIP net.IP
|
||||
if a.conf.Args != nil {
|
||||
requestedIP = a.conf.Args.IP
|
||||
}
|
||||
|
||||
if requestedIP != nil {
|
||||
if gw != nil && gw.Equal(a.conf.Args.IP) {
|
||||
return nil, fmt.Errorf("requested IP must differ gateway IP")
|
||||
}
|
||||
|
||||
subnet := net.IPNet{
|
||||
IP: a.conf.Subnet.IP,
|
||||
Mask: a.conf.Subnet.Mask,
|
||||
}
|
||||
err := validateRangeIP(requestedIP, &subnet)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
reserved, err := a.store.Reserve(id, requestedIP)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if reserved {
|
||||
return &types.IPConfig{
|
||||
IP: net.IPNet{IP: requestedIP, Mask: a.conf.Subnet.Mask},
|
||||
Gateway: gw,
|
||||
Routes: a.conf.Routes,
|
||||
}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("requested IP address %q is not available in network: %s", requestedIP, a.conf.Name)
|
||||
}
|
||||
|
||||
for cur := a.start; !cur.Equal(a.end); cur = ip.NextIP(cur) {
|
||||
// don't allocate gateway IP
|
||||
if gw != nil && cur.Equal(gw) {
|
||||
continue
|
||||
}
|
||||
|
||||
reserved, err := a.store.Reserve(id, cur)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if reserved {
|
||||
return &types.IPConfig{
|
||||
IP: net.IPNet{IP: cur, Mask: a.conf.Subnet.Mask},
|
||||
Gateway: gw,
|
||||
Routes: a.conf.Routes,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("no IP addresses available in network: %s", a.conf.Name)
|
||||
}
|
||||
|
||||
// Releases all IPs allocated for the container with given ID
|
||||
func (a *IPAllocator) Release(id string) error {
|
||||
a.store.Lock()
|
||||
defer a.store.Unlock()
|
||||
|
||||
return a.store.ReleaseByID(id)
|
||||
}
|
||||
|
||||
func networkRange(ipnet *net.IPNet) (net.IP, net.IP, error) {
|
||||
if ipnet.IP == nil {
|
||||
return nil, nil, fmt.Errorf("missing field %q in IPAM configuration", "subnet")
|
||||
}
|
||||
ip := ipnet.IP.To4()
|
||||
if ip == nil {
|
||||
ip = ipnet.IP.To16()
|
||||
if ip == nil {
|
||||
return nil, nil, fmt.Errorf("IP not v4 nor v6")
|
||||
}
|
||||
}
|
||||
|
||||
if len(ip) != len(ipnet.Mask) {
|
||||
return nil, nil, fmt.Errorf("IPNet IP and Mask version mismatch")
|
||||
}
|
||||
|
||||
var end net.IP
|
||||
for i := 0; i < len(ip); i++ {
|
||||
end = append(end, ip[i]|^ipnet.Mask[i])
|
||||
}
|
||||
return ipnet.IP, end, nil
|
||||
}
|
217
plugins/ipam/host-local/backend/allocator/allocator.go
Normal file
217
plugins/ipam/host-local/backend/allocator/allocator.go
Normal file
@ -0,0 +1,217 @@
|
||||
// Copyright 2015 CNI authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package allocator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/types/current"
|
||||
"github.com/containernetworking/plugins/pkg/ip"
|
||||
"github.com/containernetworking/plugins/plugins/ipam/host-local/backend"
|
||||
)
|
||||
|
||||
type IPAllocator struct {
|
||||
rangeset *RangeSet
|
||||
store backend.Store
|
||||
rangeID string // Used for tracking last reserved ip
|
||||
}
|
||||
|
||||
func NewIPAllocator(s *RangeSet, store backend.Store, id int) *IPAllocator {
|
||||
return &IPAllocator{
|
||||
rangeset: s,
|
||||
store: store,
|
||||
rangeID: strconv.Itoa(id),
|
||||
}
|
||||
}
|
||||
|
||||
// Get alocates an IP
|
||||
func (a *IPAllocator) Get(id string, requestedIP net.IP) (*current.IPConfig, error) {
|
||||
a.store.Lock()
|
||||
defer a.store.Unlock()
|
||||
|
||||
var reservedIP *net.IPNet
|
||||
var gw net.IP
|
||||
|
||||
if requestedIP != nil {
|
||||
if err := canonicalizeIP(&requestedIP); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r, err := a.rangeset.RangeFor(requestedIP)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if requestedIP.Equal(r.Gateway) {
|
||||
return nil, fmt.Errorf("requested ip %s is subnet's gateway", requestedIP.String())
|
||||
}
|
||||
|
||||
reserved, err := a.store.Reserve(id, requestedIP, a.rangeID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !reserved {
|
||||
return nil, fmt.Errorf("requested IP address %s is not available in range set %s", requestedIP, a.rangeset.String())
|
||||
}
|
||||
reservedIP = &net.IPNet{IP: requestedIP, Mask: r.Subnet.Mask}
|
||||
gw = r.Gateway
|
||||
|
||||
} else {
|
||||
iter, err := a.GetIter()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for {
|
||||
reservedIP, gw = iter.Next()
|
||||
if reservedIP == nil {
|
||||
break
|
||||
}
|
||||
|
||||
reserved, err := a.store.Reserve(id, reservedIP.IP, a.rangeID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if reserved {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if reservedIP == nil {
|
||||
return nil, fmt.Errorf("no IP addresses available in range set: %s", a.rangeset.String())
|
||||
}
|
||||
version := "4"
|
||||
if reservedIP.IP.To4() == nil {
|
||||
version = "6"
|
||||
}
|
||||
|
||||
return ¤t.IPConfig{
|
||||
Version: version,
|
||||
Address: *reservedIP,
|
||||
Gateway: gw,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Release clears all IPs allocated for the container with given ID
|
||||
func (a *IPAllocator) Release(id string) error {
|
||||
a.store.Lock()
|
||||
defer a.store.Unlock()
|
||||
|
||||
return a.store.ReleaseByID(id)
|
||||
}
|
||||
|
||||
type RangeIter struct {
|
||||
rangeset *RangeSet
|
||||
|
||||
// The current range id
|
||||
rangeIdx int
|
||||
|
||||
// Our current position
|
||||
cur net.IP
|
||||
|
||||
// The IP and range index where we started iterating; if we hit this again, we're done.
|
||||
startIP net.IP
|
||||
startRange int
|
||||
}
|
||||
|
||||
// GetIter encapsulates the strategy for this allocator.
|
||||
// We use a round-robin strategy, attempting to evenly use the whole set.
|
||||
// More specifically, a crash-looping container will not see the same IP until
|
||||
// the entire range has been run through.
|
||||
// We may wish to consider avoiding recently-released IPs in the future.
|
||||
func (a *IPAllocator) GetIter() (*RangeIter, error) {
|
||||
iter := RangeIter{
|
||||
rangeset: a.rangeset,
|
||||
}
|
||||
|
||||
// Round-robin by trying to allocate from the last reserved IP + 1
|
||||
startFromLastReservedIP := false
|
||||
|
||||
// We might get a last reserved IP that is wrong if the range indexes changed.
|
||||
// This is not critical, we just lose round-robin this one time.
|
||||
lastReservedIP, err := a.store.LastReservedIP(a.rangeID)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
log.Printf("Error retrieving last reserved ip: %v", err)
|
||||
} else if lastReservedIP != nil {
|
||||
startFromLastReservedIP = a.rangeset.Contains(lastReservedIP)
|
||||
}
|
||||
|
||||
// Find the range in the set with this IP
|
||||
if startFromLastReservedIP {
|
||||
for i, r := range *a.rangeset {
|
||||
if r.Contains(lastReservedIP) {
|
||||
iter.rangeIdx = i
|
||||
iter.startRange = i
|
||||
|
||||
// We advance the cursor on every Next(), so the first call
|
||||
// to next() will return lastReservedIP + 1
|
||||
iter.cur = lastReservedIP
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
iter.rangeIdx = 0
|
||||
iter.startRange = 0
|
||||
iter.startIP = (*a.rangeset)[0].RangeStart
|
||||
}
|
||||
return &iter, nil
|
||||
}
|
||||
|
||||
// Next returns the next IP, its mask, and its gateway. Returns nil
|
||||
// if the iterator has been exhausted
|
||||
func (i *RangeIter) Next() (*net.IPNet, net.IP) {
|
||||
r := (*i.rangeset)[i.rangeIdx]
|
||||
|
||||
// If this is the first time iterating and we're not starting in the middle
|
||||
// of the range, then start at rangeStart, which is inclusive
|
||||
if i.cur == nil {
|
||||
i.cur = r.RangeStart
|
||||
i.startIP = i.cur
|
||||
if i.cur.Equal(r.Gateway) {
|
||||
return i.Next()
|
||||
}
|
||||
return &net.IPNet{IP: i.cur, Mask: r.Subnet.Mask}, r.Gateway
|
||||
}
|
||||
|
||||
// If we've reached the end of this range, we need to advance the range
|
||||
// RangeEnd is inclusive as well
|
||||
if i.cur.Equal(r.RangeEnd) {
|
||||
i.rangeIdx += 1
|
||||
i.rangeIdx %= len(*i.rangeset)
|
||||
r = (*i.rangeset)[i.rangeIdx]
|
||||
|
||||
i.cur = r.RangeStart
|
||||
} else {
|
||||
i.cur = ip.NextIP(i.cur)
|
||||
}
|
||||
|
||||
if i.startIP == nil {
|
||||
i.startIP = i.cur
|
||||
} else if i.rangeIdx == i.startRange && i.cur.Equal(i.startIP) {
|
||||
// IF we've looped back to where we started, give up
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if i.cur.Equal(r.Gateway) {
|
||||
return i.Next()
|
||||
}
|
||||
|
||||
return &net.IPNet{IP: i.cur, Mask: r.Subnet.Mask}, r.Gateway
|
||||
}
|
@ -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 allocator_test
|
||||
|
||||
import (
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAllocator(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Allocator Suite")
|
||||
}
|
333
plugins/ipam/host-local/backend/allocator/allocator_test.go
Normal file
333
plugins/ipam/host-local/backend/allocator/allocator_test.go
Normal file
@ -0,0 +1,333 @@
|
||||
// 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 allocator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
"github.com/containernetworking/cni/pkg/types/current"
|
||||
fakestore "github.com/containernetworking/plugins/plugins/ipam/host-local/backend/testing"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
type AllocatorTestCase struct {
|
||||
subnets []string
|
||||
ipmap map[string]string
|
||||
expectResult string
|
||||
lastIP string
|
||||
}
|
||||
|
||||
func mkalloc() IPAllocator {
|
||||
p := RangeSet{
|
||||
Range{Subnet: mustSubnet("192.168.1.0/29")},
|
||||
}
|
||||
p.Canonicalize()
|
||||
store := fakestore.NewFakeStore(map[string]string{}, map[string]net.IP{})
|
||||
|
||||
alloc := IPAllocator{
|
||||
rangeset: &p,
|
||||
store: store,
|
||||
rangeID: "rangeid",
|
||||
}
|
||||
|
||||
return alloc
|
||||
}
|
||||
|
||||
func (t AllocatorTestCase) run(idx int) (*current.IPConfig, error) {
|
||||
fmt.Fprintln(GinkgoWriter, "Index:", idx)
|
||||
p := RangeSet{}
|
||||
for _, s := range t.subnets {
|
||||
subnet, err := types.ParseCIDR(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p = append(p, Range{Subnet: types.IPNet(*subnet)})
|
||||
}
|
||||
|
||||
Expect(p.Canonicalize()).To(BeNil())
|
||||
|
||||
store := fakestore.NewFakeStore(t.ipmap, map[string]net.IP{"rangeid": net.ParseIP(t.lastIP)})
|
||||
|
||||
alloc := IPAllocator{
|
||||
rangeset: &p,
|
||||
store: store,
|
||||
rangeID: "rangeid",
|
||||
}
|
||||
|
||||
return alloc.Get("ID", nil)
|
||||
}
|
||||
|
||||
var _ = Describe("host-local ip allocator", func() {
|
||||
Context("RangeIter", func() {
|
||||
It("should loop correctly from the beginning", func() {
|
||||
a := mkalloc()
|
||||
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}))
|
||||
Expect(r.nextip()).To(Equal(net.IP{192, 168, 1, 4}))
|
||||
Expect(r.nextip()).To(Equal(net.IP{192, 168, 1, 5}))
|
||||
Expect(r.nextip()).To(Equal(net.IP{192, 168, 1, 6}))
|
||||
Expect(r.nextip()).To(BeNil())
|
||||
})
|
||||
|
||||
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")
|
||||
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}))
|
||||
Expect(r.nextip()).To(Equal(net.IP{192, 168, 1, 4}))
|
||||
Expect(r.nextip()).To(Equal(net.IP{192, 168, 1, 5}))
|
||||
Expect(r.nextip()).To(Equal(net.IP{192, 168, 1, 6}))
|
||||
Expect(r.nextip()).To(BeNil())
|
||||
})
|
||||
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")
|
||||
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}))
|
||||
Expect(r.nextip()).To(Equal(net.IP{192, 168, 1, 6}))
|
||||
Expect(r.nextip()).To(Equal(net.IP{192, 168, 1, 2}))
|
||||
Expect(r.nextip()).To(Equal(net.IP{192, 168, 1, 3}))
|
||||
Expect(r.nextip()).To(BeNil())
|
||||
})
|
||||
})
|
||||
|
||||
Context("when has free ip", func() {
|
||||
It("should allocate ips in round robin", func() {
|
||||
testCases := []AllocatorTestCase{
|
||||
// fresh start
|
||||
{
|
||||
subnets: []string{"10.0.0.0/29"},
|
||||
ipmap: map[string]string{},
|
||||
expectResult: "10.0.0.2",
|
||||
lastIP: "",
|
||||
},
|
||||
{
|
||||
subnets: []string{"2001:db8:1::0/64"},
|
||||
ipmap: map[string]string{},
|
||||
expectResult: "2001:db8:1::2",
|
||||
lastIP: "",
|
||||
},
|
||||
{
|
||||
subnets: []string{"10.0.0.0/30"},
|
||||
ipmap: map[string]string{},
|
||||
expectResult: "10.0.0.2",
|
||||
lastIP: "",
|
||||
},
|
||||
{
|
||||
subnets: []string{"10.0.0.0/29"},
|
||||
ipmap: map[string]string{
|
||||
"10.0.0.2": "id",
|
||||
},
|
||||
expectResult: "10.0.0.3",
|
||||
lastIP: "",
|
||||
},
|
||||
// next ip of last reserved ip
|
||||
{
|
||||
subnets: []string{"10.0.0.0/29"},
|
||||
ipmap: map[string]string{},
|
||||
expectResult: "10.0.0.6",
|
||||
lastIP: "10.0.0.5",
|
||||
},
|
||||
{
|
||||
subnets: []string{"10.0.0.0/29"},
|
||||
ipmap: map[string]string{
|
||||
"10.0.0.4": "id",
|
||||
"10.0.0.5": "id",
|
||||
},
|
||||
expectResult: "10.0.0.6",
|
||||
lastIP: "10.0.0.3",
|
||||
},
|
||||
// round robin to the beginning
|
||||
{
|
||||
subnets: []string{"10.0.0.0/29"},
|
||||
ipmap: map[string]string{
|
||||
"10.0.0.6": "id",
|
||||
},
|
||||
expectResult: "10.0.0.2",
|
||||
lastIP: "10.0.0.5",
|
||||
},
|
||||
// lastIP is out of range
|
||||
{
|
||||
subnets: []string{"10.0.0.0/29"},
|
||||
ipmap: map[string]string{
|
||||
"10.0.0.2": "id",
|
||||
},
|
||||
expectResult: "10.0.0.3",
|
||||
lastIP: "10.0.0.128",
|
||||
},
|
||||
// subnet is completely full except for lastip
|
||||
// wrap around and reserve lastIP
|
||||
{
|
||||
subnets: []string{"10.0.0.0/29"},
|
||||
ipmap: map[string]string{
|
||||
"10.0.0.2": "id",
|
||||
"10.0.0.4": "id",
|
||||
"10.0.0.5": "id",
|
||||
"10.0.0.6": "id",
|
||||
},
|
||||
expectResult: "10.0.0.3",
|
||||
lastIP: "10.0.0.3",
|
||||
},
|
||||
// alocate from multiple subnets
|
||||
{
|
||||
subnets: []string{"10.0.0.0/30", "10.0.1.0/30"},
|
||||
expectResult: "10.0.0.2",
|
||||
ipmap: map[string]string{},
|
||||
},
|
||||
// advance to next subnet
|
||||
{
|
||||
subnets: []string{"10.0.0.0/30", "10.0.1.0/30"},
|
||||
lastIP: "10.0.0.2",
|
||||
expectResult: "10.0.1.2",
|
||||
ipmap: map[string]string{},
|
||||
},
|
||||
// Roll to start subnet
|
||||
{
|
||||
subnets: []string{"10.0.0.0/30", "10.0.1.0/30", "10.0.2.0/30"},
|
||||
lastIP: "10.0.2.2",
|
||||
expectResult: "10.0.0.2",
|
||||
ipmap: map[string]string{},
|
||||
},
|
||||
}
|
||||
|
||||
for idx, tc := range testCases {
|
||||
res, err := tc.run(idx)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(res.Address.IP.String()).To(Equal(tc.expectResult))
|
||||
}
|
||||
})
|
||||
|
||||
It("should not allocate the broadcast address", func() {
|
||||
alloc := mkalloc()
|
||||
for i := 2; i < 7; i++ {
|
||||
res, err := alloc.Get("ID", 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)
|
||||
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)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(res.Address.String()).To(Equal("192.168.1.2/29"))
|
||||
|
||||
err = alloc.Release("ID")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
res, err = alloc.Get("ID", nil)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(res.Address.String()).To(Equal("192.168.1.3/29"))
|
||||
|
||||
})
|
||||
|
||||
Context("when requesting a specific IP", func() {
|
||||
It("must allocate the requested IP", func() {
|
||||
alloc := mkalloc()
|
||||
requestedIP := net.IP{192, 168, 1, 5}
|
||||
res, err := alloc.Get("ID", requestedIP)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(res.Address.IP.String()).To(Equal(requestedIP.String()))
|
||||
})
|
||||
|
||||
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)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(res.Address.IP.String()).To(Equal(requestedIP.String()))
|
||||
|
||||
_, err = alloc.Get("ID", 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`))
|
||||
})
|
||||
|
||||
It("must return an error when the requested IP is after RangeEnd", 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)
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
|
||||
It("must return an error when the requested IP is before RangeStart", 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)
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
Context("when out of ips", func() {
|
||||
It("returns a meaningful error", func() {
|
||||
testCases := []AllocatorTestCase{
|
||||
{
|
||||
subnets: []string{"10.0.0.0/30"},
|
||||
ipmap: map[string]string{
|
||||
"10.0.0.2": "id",
|
||||
},
|
||||
},
|
||||
{
|
||||
subnets: []string{"10.0.0.0/29"},
|
||||
ipmap: map[string]string{
|
||||
"10.0.0.2": "id",
|
||||
"10.0.0.3": "id",
|
||||
"10.0.0.4": "id",
|
||||
"10.0.0.5": "id",
|
||||
"10.0.0.6": "id",
|
||||
},
|
||||
},
|
||||
{
|
||||
subnets: []string{"10.0.0.0/30", "10.0.1.0/30"},
|
||||
ipmap: map[string]string{
|
||||
"10.0.0.2": "id",
|
||||
"10.0.1.2": "id",
|
||||
},
|
||||
},
|
||||
}
|
||||
for idx, tc := range testCases {
|
||||
_, err := tc.run(idx)
|
||||
Expect(err).NotTo(BeNil())
|
||||
Expect(err.Error()).To(HavePrefix("no IP addresses available in range set"))
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// nextip is a convenience function used for testing
|
||||
func (i *RangeIter) nextip() net.IP {
|
||||
c, _ := i.Next()
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return c.IP
|
||||
}
|
160
plugins/ipam/host-local/backend/allocator/config.go
Normal file
160
plugins/ipam/host-local/backend/allocator/config.go
Normal file
@ -0,0 +1,160 @@
|
||||
// Copyright 2015 CNI authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package allocator
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
types020 "github.com/containernetworking/cni/pkg/types/020"
|
||||
)
|
||||
|
||||
// The top-level network config - IPAM plugins are passed the full configuration
|
||||
// of the calling plugin, not just the IPAM section.
|
||||
type Net struct {
|
||||
Name string `json:"name"`
|
||||
CNIVersion string `json:"cniVersion"`
|
||||
IPAM *IPAMConfig `json:"ipam"`
|
||||
RuntimeConfig struct { // The capability arg
|
||||
IPRanges []RangeSet `json:"ipRanges,omitempty"`
|
||||
} `json:"runtimeConfig,omitempty"`
|
||||
Args *struct {
|
||||
A *IPAMArgs `json:"cni"`
|
||||
} `json:"args"`
|
||||
}
|
||||
|
||||
// IPAMConfig represents the IP related network configuration.
|
||||
// This nests Range because we initially only supported a single
|
||||
// range directly, and wish to preserve backwards compatability
|
||||
type IPAMConfig struct {
|
||||
*Range
|
||||
Name string
|
||||
Type string `json:"type"`
|
||||
Routes []*types.Route `json:"routes"`
|
||||
DataDir string `json:"dataDir"`
|
||||
ResolvConf string `json:"resolvConf"`
|
||||
Ranges []RangeSet `json:"ranges"`
|
||||
IPArgs []net.IP `json:"-"` // Requested IPs from CNI_ARGS and args
|
||||
}
|
||||
|
||||
type IPAMEnvArgs struct {
|
||||
types.CommonArgs
|
||||
IP net.IP `json:"ip,omitempty"`
|
||||
}
|
||||
|
||||
type IPAMArgs struct {
|
||||
IPs []net.IP `json:"ips"`
|
||||
}
|
||||
|
||||
type RangeSet []Range
|
||||
|
||||
type Range struct {
|
||||
RangeStart net.IP `json:"rangeStart,omitempty"` // The first ip, inclusive
|
||||
RangeEnd net.IP `json:"rangeEnd,omitempty"` // The last ip, inclusive
|
||||
Subnet types.IPNet `json:"subnet"`
|
||||
Gateway net.IP `json:"gateway,omitempty"`
|
||||
}
|
||||
|
||||
// NewIPAMConfig creates a NetworkConfig from the given network name.
|
||||
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")
|
||||
}
|
||||
|
||||
// Parse custom IP from both env args *and* the top-level args config
|
||||
if envArgs != "" {
|
||||
e := IPAMEnvArgs{}
|
||||
err := types.LoadArgs(envArgs, &e)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
if e.IP != nil {
|
||||
n.IPAM.IPArgs = []net.IP{e.IP}
|
||||
}
|
||||
}
|
||||
|
||||
if n.Args != nil && n.Args.A != nil && len(n.Args.A.IPs) != 0 {
|
||||
n.IPAM.IPArgs = append(n.IPAM.IPArgs, n.Args.A.IPs...)
|
||||
}
|
||||
|
||||
for idx, _ := range n.IPAM.IPArgs {
|
||||
if err := canonicalizeIP(&n.IPAM.IPArgs[idx]); err != nil {
|
||||
return nil, "", fmt.Errorf("cannot understand ip: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// If a single range (old-style config) is specified, prepend it to
|
||||
// the Ranges array
|
||||
if n.IPAM.Range != nil && n.IPAM.Range.Subnet.IP != nil {
|
||||
n.IPAM.Ranges = append([]RangeSet{{*n.IPAM.Range}}, n.IPAM.Ranges...)
|
||||
}
|
||||
n.IPAM.Range = nil
|
||||
|
||||
// If a range is supplied as a runtime config, prepend it to the Ranges
|
||||
if len(n.RuntimeConfig.IPRanges) > 0 {
|
||||
n.IPAM.Ranges = append(n.RuntimeConfig.IPRanges, n.IPAM.Ranges...)
|
||||
}
|
||||
|
||||
if len(n.IPAM.Ranges) == 0 {
|
||||
return nil, "", fmt.Errorf("no IP ranges specified")
|
||||
}
|
||||
|
||||
// Validate all ranges
|
||||
numV4 := 0
|
||||
numV6 := 0
|
||||
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)
|
||||
}
|
||||
|
||||
if n.IPAM.Ranges[i][0].RangeStart.To4() != nil {
|
||||
numV4++
|
||||
} else {
|
||||
numV6++
|
||||
}
|
||||
}
|
||||
|
||||
// CNI spec 0.2.0 and below supported only one v4 and v6 address
|
||||
if numV4 > 1 || numV6 > 1 {
|
||||
for _, v := range types020.SupportedVersions {
|
||||
if n.CNIVersion == v {
|
||||
return nil, "", fmt.Errorf("CNI version %v does not support more than 1 address per family", n.CNIVersion)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for overlaps
|
||||
l := len(n.IPAM.Ranges)
|
||||
for i, p1 := range n.IPAM.Ranges[:l-1] {
|
||||
for j, p2 := range n.IPAM.Ranges[i+1:] {
|
||||
if p1.Overlaps(&p2) {
|
||||
return nil, "", fmt.Errorf("range set %d overlaps with %d", i, (i + j + 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Copy net name into IPAM so not to drag Net struct around
|
||||
n.IPAM.Name = n.Name
|
||||
|
||||
return n.IPAM, n.CNIVersion, nil
|
||||
}
|
382
plugins/ipam/host-local/backend/allocator/config_test.go
Normal file
382
plugins/ipam/host-local/backend/allocator/config_test.go
Normal file
@ -0,0 +1,382 @@
|
||||
// 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 allocator
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("IPAM config", func() {
|
||||
It("Should parse an old-style config", func() {
|
||||
input := `{
|
||||
"cniVersion": "0.3.1",
|
||||
"name": "mynet",
|
||||
"type": "ipvlan",
|
||||
"master": "foo0",
|
||||
"ipam": {
|
||||
"type": "host-local",
|
||||
"subnet": "10.1.2.0/24",
|
||||
"rangeStart": "10.1.2.9",
|
||||
"rangeEnd": "10.1.2.20",
|
||||
"gateway": "10.1.2.30"
|
||||
}
|
||||
}`
|
||||
conf, version, err := LoadIPAMConfig([]byte(input), "")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(version).Should(Equal("0.3.1"))
|
||||
|
||||
Expect(conf).To(Equal(&IPAMConfig{
|
||||
Name: "mynet",
|
||||
Type: "host-local",
|
||||
Ranges: []RangeSet{
|
||||
RangeSet{
|
||||
{
|
||||
RangeStart: net.IP{10, 1, 2, 9},
|
||||
RangeEnd: net.IP{10, 1, 2, 20},
|
||||
Gateway: net.IP{10, 1, 2, 30},
|
||||
Subnet: types.IPNet{
|
||||
IP: net.IP{10, 1, 2, 0},
|
||||
Mask: net.CIDRMask(24, 32),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
})
|
||||
|
||||
It("Should parse a new-style config", func() {
|
||||
input := `{
|
||||
"cniVersion": "0.3.1",
|
||||
"name": "mynet",
|
||||
"type": "ipvlan",
|
||||
"master": "foo0",
|
||||
"ipam": {
|
||||
"type": "host-local",
|
||||
"ranges": [
|
||||
[
|
||||
{
|
||||
"subnet": "10.1.2.0/24",
|
||||
"rangeStart": "10.1.2.9",
|
||||
"rangeEnd": "10.1.2.20",
|
||||
"gateway": "10.1.2.30"
|
||||
},
|
||||
{
|
||||
"subnet": "10.1.4.0/24"
|
||||
}
|
||||
],
|
||||
[{
|
||||
"subnet": "11.1.2.0/24",
|
||||
"rangeStart": "11.1.2.9",
|
||||
"rangeEnd": "11.1.2.20",
|
||||
"gateway": "11.1.2.30"
|
||||
}]
|
||||
]
|
||||
}
|
||||
}`
|
||||
conf, version, err := LoadIPAMConfig([]byte(input), "")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(version).Should(Equal("0.3.1"))
|
||||
|
||||
Expect(conf).To(Equal(&IPAMConfig{
|
||||
Name: "mynet",
|
||||
Type: "host-local",
|
||||
Ranges: []RangeSet{
|
||||
{
|
||||
{
|
||||
RangeStart: net.IP{10, 1, 2, 9},
|
||||
RangeEnd: net.IP{10, 1, 2, 20},
|
||||
Gateway: net.IP{10, 1, 2, 30},
|
||||
Subnet: types.IPNet{
|
||||
IP: net.IP{10, 1, 2, 0},
|
||||
Mask: net.CIDRMask(24, 32),
|
||||
},
|
||||
},
|
||||
{
|
||||
RangeStart: net.IP{10, 1, 4, 1},
|
||||
RangeEnd: net.IP{10, 1, 4, 254},
|
||||
Gateway: net.IP{10, 1, 4, 1},
|
||||
Subnet: types.IPNet{
|
||||
IP: net.IP{10, 1, 4, 0},
|
||||
Mask: net.CIDRMask(24, 32),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
{
|
||||
RangeStart: net.IP{11, 1, 2, 9},
|
||||
RangeEnd: net.IP{11, 1, 2, 20},
|
||||
Gateway: net.IP{11, 1, 2, 30},
|
||||
Subnet: types.IPNet{
|
||||
IP: net.IP{11, 1, 2, 0},
|
||||
Mask: net.CIDRMask(24, 32),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
})
|
||||
|
||||
It("Should parse a mixed config with runtime args", func() {
|
||||
input := `{
|
||||
"cniVersion": "0.3.1",
|
||||
"name": "mynet",
|
||||
"type": "ipvlan",
|
||||
"master": "foo0",
|
||||
"runtimeConfig": {
|
||||
"irrelevant": "a",
|
||||
"ipRanges": [
|
||||
[{ "subnet": "12.1.3.0/24" }]
|
||||
]
|
||||
},
|
||||
"ipam": {
|
||||
"type": "host-local",
|
||||
"subnet": "10.1.2.0/24",
|
||||
"rangeStart": "10.1.2.9",
|
||||
"rangeEnd": "10.1.2.20",
|
||||
"gateway": "10.1.2.30",
|
||||
"ranges": [[
|
||||
{
|
||||
"subnet": "11.1.2.0/24",
|
||||
"rangeStart": "11.1.2.9",
|
||||
"rangeEnd": "11.1.2.20",
|
||||
"gateway": "11.1.2.30"
|
||||
}
|
||||
]]
|
||||
}
|
||||
}`
|
||||
conf, version, err := LoadIPAMConfig([]byte(input), "")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(version).Should(Equal("0.3.1"))
|
||||
|
||||
Expect(conf).To(Equal(&IPAMConfig{
|
||||
Name: "mynet",
|
||||
Type: "host-local",
|
||||
Ranges: []RangeSet{
|
||||
{ // The RuntimeConfig should always be first
|
||||
{
|
||||
RangeStart: net.IP{12, 1, 3, 1},
|
||||
RangeEnd: net.IP{12, 1, 3, 254},
|
||||
Gateway: net.IP{12, 1, 3, 1},
|
||||
Subnet: types.IPNet{
|
||||
IP: net.IP{12, 1, 3, 0},
|
||||
Mask: net.CIDRMask(24, 32),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
{
|
||||
RangeStart: net.IP{10, 1, 2, 9},
|
||||
RangeEnd: net.IP{10, 1, 2, 20},
|
||||
Gateway: net.IP{10, 1, 2, 30},
|
||||
Subnet: types.IPNet{
|
||||
IP: net.IP{10, 1, 2, 0},
|
||||
Mask: net.CIDRMask(24, 32),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
{
|
||||
RangeStart: net.IP{11, 1, 2, 9},
|
||||
RangeEnd: net.IP{11, 1, 2, 20},
|
||||
Gateway: net.IP{11, 1, 2, 30},
|
||||
Subnet: types.IPNet{
|
||||
IP: net.IP{11, 1, 2, 0},
|
||||
Mask: net.CIDRMask(24, 32),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
})
|
||||
|
||||
It("Should parse CNI_ARGS env", func() {
|
||||
input := `{
|
||||
"cniVersion": "0.3.1",
|
||||
"name": "mynet",
|
||||
"type": "ipvlan",
|
||||
"master": "foo0",
|
||||
"ipam": {
|
||||
"type": "host-local",
|
||||
"ranges": [[
|
||||
{
|
||||
"subnet": "10.1.2.0/24",
|
||||
"rangeStart": "10.1.2.9",
|
||||
"rangeEnd": "10.1.2.20",
|
||||
"gateway": "10.1.2.30"
|
||||
}
|
||||
]]
|
||||
}
|
||||
}`
|
||||
|
||||
envArgs := "IP=10.1.2.10"
|
||||
|
||||
conf, _, err := LoadIPAMConfig([]byte(input), envArgs)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(conf.IPArgs).To(Equal([]net.IP{{10, 1, 2, 10}}))
|
||||
|
||||
})
|
||||
|
||||
It("Should parse config args", func() {
|
||||
input := `{
|
||||
"cniVersion": "0.3.1",
|
||||
"name": "mynet",
|
||||
"type": "ipvlan",
|
||||
"master": "foo0",
|
||||
"args": {
|
||||
"cni": {
|
||||
"ips": [ "10.1.2.11", "11.11.11.11", "2001:db8:1::11"]
|
||||
}
|
||||
},
|
||||
"ipam": {
|
||||
"type": "host-local",
|
||||
"ranges": [
|
||||
[{
|
||||
"subnet": "10.1.2.0/24",
|
||||
"rangeStart": "10.1.2.9",
|
||||
"rangeEnd": "10.1.2.20",
|
||||
"gateway": "10.1.2.30"
|
||||
}],
|
||||
[{
|
||||
"subnet": "11.1.2.0/24",
|
||||
"rangeStart": "11.1.2.9",
|
||||
"rangeEnd": "11.1.2.20",
|
||||
"gateway": "11.1.2.30"
|
||||
}],
|
||||
[{
|
||||
"subnet": "2001:db8:1::/64"
|
||||
}]
|
||||
]
|
||||
}
|
||||
}`
|
||||
|
||||
envArgs := "IP=10.1.2.10"
|
||||
|
||||
conf, _, err := LoadIPAMConfig([]byte(input), envArgs)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(conf.IPArgs).To(Equal([]net.IP{
|
||||
{10, 1, 2, 10},
|
||||
{10, 1, 2, 11},
|
||||
{11, 11, 11, 11},
|
||||
net.ParseIP("2001:db8:1::11"),
|
||||
}))
|
||||
})
|
||||
|
||||
It("Should detect overlap between rangesets", func() {
|
||||
input := `{
|
||||
"cniVersion": "0.3.1",
|
||||
"name": "mynet",
|
||||
"type": "ipvlan",
|
||||
"master": "foo0",
|
||||
"ipam": {
|
||||
"type": "host-local",
|
||||
"ranges": [
|
||||
[
|
||||
{"subnet": "10.1.2.0/24"},
|
||||
{"subnet": "10.2.2.0/24"}
|
||||
],
|
||||
[
|
||||
{ "subnet": "10.1.4.0/24"},
|
||||
{ "subnet": "10.1.6.0/24"},
|
||||
{ "subnet": "10.1.8.0/24"},
|
||||
{ "subnet": "10.1.2.0/24"}
|
||||
]
|
||||
]
|
||||
}
|
||||
}`
|
||||
_, _, err := LoadIPAMConfig([]byte(input), "")
|
||||
Expect(err).To(MatchError("range set 0 overlaps with 1"))
|
||||
})
|
||||
|
||||
It("Should detect overlap within rangeset", func() {
|
||||
input := `{
|
||||
"cniVersion": "0.3.1",
|
||||
"name": "mynet",
|
||||
"type": "ipvlan",
|
||||
"master": "foo0",
|
||||
"ipam": {
|
||||
"type": "host-local",
|
||||
"ranges": [
|
||||
[
|
||||
{ "subnet": "10.1.0.0/22" },
|
||||
{ "subnet": "10.1.2.0/24" }
|
||||
]
|
||||
]
|
||||
}
|
||||
}`
|
||||
_, _, err := LoadIPAMConfig([]byte(input), "")
|
||||
Expect(err).To(MatchError("invalid range set 0: subnets 10.1.0.1-10.1.3.254 and 10.1.2.1-10.1.2.254 overlap"))
|
||||
})
|
||||
|
||||
It("should error on rangesets with different families", func() {
|
||||
input := `{
|
||||
"cniVersion": "0.3.1",
|
||||
"name": "mynet",
|
||||
"type": "ipvlan",
|
||||
"master": "foo0",
|
||||
"ipam": {
|
||||
"type": "host-local",
|
||||
"ranges": [
|
||||
[
|
||||
{ "subnet": "10.1.0.0/22" },
|
||||
{ "subnet": "2001:db8:5::/64" }
|
||||
]
|
||||
]
|
||||
}
|
||||
}`
|
||||
_, _, err := LoadIPAMConfig([]byte(input), "")
|
||||
Expect(err).To(MatchError("invalid range set 0: mixed address families"))
|
||||
|
||||
})
|
||||
|
||||
It("Should should error on too many ranges", func() {
|
||||
input := `{
|
||||
"cniVersion": "0.2.0",
|
||||
"name": "mynet",
|
||||
"type": "ipvlan",
|
||||
"master": "foo0",
|
||||
"ipam": {
|
||||
"type": "host-local",
|
||||
"ranges": [
|
||||
[{"subnet": "10.1.2.0/24"}],
|
||||
[{"subnet": "11.1.2.0/24"}]
|
||||
]
|
||||
}
|
||||
}`
|
||||
_, _, err := LoadIPAMConfig([]byte(input), "")
|
||||
Expect(err).To(MatchError("CNI version 0.2.0 does not support more than 1 address per family"))
|
||||
})
|
||||
|
||||
It("Should allow one v4 and v6 range for 0.2.0", func() {
|
||||
input := `{
|
||||
"cniVersion": "0.2.0",
|
||||
"name": "mynet",
|
||||
"type": "ipvlan",
|
||||
"master": "foo0",
|
||||
"ipam": {
|
||||
"type": "host-local",
|
||||
"ranges": [
|
||||
[{"subnet": "10.1.2.0/24"}],
|
||||
[{"subnet": "2001:db8:1::/24"}]
|
||||
]
|
||||
}
|
||||
}`
|
||||
_, _, err := LoadIPAMConfig([]byte(input), "")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
})
|
164
plugins/ipam/host-local/backend/allocator/range.go
Normal file
164
plugins/ipam/host-local/backend/allocator/range.go
Normal file
@ -0,0 +1,164 @@
|
||||
// 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 allocator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
"github.com/containernetworking/plugins/pkg/ip"
|
||||
)
|
||||
|
||||
// Canonicalize takes a given range and ensures that all information is consistent,
|
||||
// filling out Start, End, and Gateway with sane values if missing
|
||||
func (r *Range) Canonicalize() error {
|
||||
if err := canonicalizeIP(&r.Subnet.IP); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Can't create an allocator for a network with no addresses, eg
|
||||
// a /32 or /31
|
||||
ones, masklen := r.Subnet.Mask.Size()
|
||||
if ones > masklen-2 {
|
||||
return fmt.Errorf("Network %s too small to allocate from", (*net.IPNet)(&r.Subnet).String())
|
||||
}
|
||||
|
||||
if len(r.Subnet.IP) != len(r.Subnet.Mask) {
|
||||
return fmt.Errorf("IPNet IP and Mask version mismatch")
|
||||
}
|
||||
|
||||
// If the gateway is nil, claim .1
|
||||
if r.Gateway == nil {
|
||||
r.Gateway = ip.NextIP(r.Subnet.IP)
|
||||
} else {
|
||||
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),
|
||||
// otherwise use the first free IP (i.e. .1) - this will conflict with the
|
||||
// gateway but we skip it in the iterator
|
||||
if r.RangeStart != nil {
|
||||
if err := canonicalizeIP(&r.RangeStart); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !r.Contains(r.RangeStart) {
|
||||
return fmt.Errorf("RangeStart %s not in network %s", r.RangeStart.String(), (*net.IPNet)(&r.Subnet).String())
|
||||
}
|
||||
} else {
|
||||
r.RangeStart = ip.NextIP(r.Subnet.IP)
|
||||
}
|
||||
|
||||
// RangeEnd: If specified, verify sanity. Otherwise, add a sensible default
|
||||
// (e.g. for a /24: .254 if IPv4, ::255 if IPv6)
|
||||
if r.RangeEnd != nil {
|
||||
if err := canonicalizeIP(&r.RangeEnd); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !r.Contains(r.RangeEnd) {
|
||||
return fmt.Errorf("RangeEnd %s not in network %s", r.RangeEnd.String(), (*net.IPNet)(&r.Subnet).String())
|
||||
}
|
||||
} else {
|
||||
r.RangeEnd = lastIP(r.Subnet)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsValidIP checks if a given ip is a valid, allocatable address in a given Range
|
||||
func (r *Range) Contains(addr net.IP) bool {
|
||||
if err := canonicalizeIP(&addr); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
subnet := (net.IPNet)(r.Subnet)
|
||||
|
||||
// Not the same address family
|
||||
if len(addr) != len(r.Subnet.IP) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Not in network
|
||||
if !subnet.Contains(addr) {
|
||||
return false
|
||||
}
|
||||
|
||||
// We ignore nils here so we can use this function as we initialize the range.
|
||||
if r.RangeStart != nil {
|
||||
// Before the range start
|
||||
if ip.Cmp(addr, r.RangeStart) < 0 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if r.RangeEnd != nil {
|
||||
if ip.Cmp(addr, r.RangeEnd) > 0 {
|
||||
// After the range end
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Overlaps returns true if there is any overlap between ranges
|
||||
func (r *Range) Overlaps(r1 *Range) bool {
|
||||
// different familes
|
||||
if len(r.RangeStart) != len(r1.RangeStart) {
|
||||
return false
|
||||
}
|
||||
|
||||
return r.Contains(r1.RangeStart) ||
|
||||
r.Contains(r1.RangeEnd) ||
|
||||
r1.Contains(r.RangeStart) ||
|
||||
r1.Contains(r.RangeEnd)
|
||||
}
|
||||
|
||||
func (r *Range) String() string {
|
||||
return fmt.Sprintf("%s-%s", r.RangeStart.String(), r.RangeEnd.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)
|
||||
}
|
||||
|
||||
// Determine the last IP of a subnet, excluding the broadcast if IPv4
|
||||
func lastIP(subnet types.IPNet) net.IP {
|
||||
var end net.IP
|
||||
for i := 0; i < len(subnet.IP); i++ {
|
||||
end = append(end, subnet.IP[i]|^subnet.Mask[i])
|
||||
}
|
||||
if subnet.IP.To4() != nil {
|
||||
end[3]--
|
||||
}
|
||||
|
||||
return end
|
||||
}
|
97
plugins/ipam/host-local/backend/allocator/range_set.go
Normal file
97
plugins/ipam/host-local/backend/allocator/range_set.go
Normal file
@ -0,0 +1,97 @@
|
||||
// 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 allocator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Contains returns true if any range in this set contains an IP
|
||||
func (s *RangeSet) Contains(addr net.IP) bool {
|
||||
r, _ := s.RangeFor(addr)
|
||||
return r != nil
|
||||
}
|
||||
|
||||
// RangeFor finds the range that contains an IP, or nil if not found
|
||||
func (s *RangeSet) RangeFor(addr net.IP) (*Range, error) {
|
||||
if err := canonicalizeIP(&addr); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, r := range *s {
|
||||
if r.Contains(addr) {
|
||||
return &r, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("%s not in range set %s", addr.String(), s.String())
|
||||
}
|
||||
|
||||
// Overlaps returns true if any ranges in any set overlap with this one
|
||||
func (s *RangeSet) Overlaps(p1 *RangeSet) bool {
|
||||
for _, r := range *s {
|
||||
for _, r1 := range *p1 {
|
||||
if r.Overlaps(&r1) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Canonicalize ensures the RangeSet is in a standard form, and detects any
|
||||
// invalid input. Call Range.Canonicalize() on every Range in the set
|
||||
func (s *RangeSet) Canonicalize() error {
|
||||
if len(*s) == 0 {
|
||||
return fmt.Errorf("empty range set")
|
||||
}
|
||||
|
||||
fam := 0
|
||||
for i, _ := range *s {
|
||||
if err := (*s)[i].Canonicalize(); err != nil {
|
||||
return err
|
||||
}
|
||||
if i == 0 {
|
||||
fam = len((*s)[i].RangeStart)
|
||||
} else {
|
||||
if fam != len((*s)[i].RangeStart) {
|
||||
return fmt.Errorf("mixed address families")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure none of the ranges in the set overlap
|
||||
l := len(*s)
|
||||
for i, r1 := range (*s)[:l-1] {
|
||||
for _, r2 := range (*s)[i+1:] {
|
||||
if r1.Overlaps(&r2) {
|
||||
return fmt.Errorf("subnets %s and %s overlap", r1.String(), r2.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *RangeSet) String() string {
|
||||
out := []string{}
|
||||
for _, r := range *s {
|
||||
out = append(out, r.String())
|
||||
}
|
||||
|
||||
return strings.Join(out, ",")
|
||||
}
|
70
plugins/ipam/host-local/backend/allocator/range_set_test.go
Normal file
70
plugins/ipam/host-local/backend/allocator/range_set_test.go
Normal file
@ -0,0 +1,70 @@
|
||||
// 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 allocator
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("range sets", func() {
|
||||
It("should detect set membership correctly", func() {
|
||||
p := RangeSet{
|
||||
Range{Subnet: mustSubnet("192.168.0.0/24")},
|
||||
Range{Subnet: mustSubnet("172.16.1.0/24")},
|
||||
}
|
||||
|
||||
err := p.Canonicalize()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
Expect(p.Contains(net.IP{192, 168, 0, 55})).To(BeTrue())
|
||||
|
||||
r, err := p.RangeFor(net.IP{192, 168, 0, 55})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(r).To(Equal(&p[0]))
|
||||
|
||||
r, err = p.RangeFor(net.IP{192, 168, 99, 99})
|
||||
Expect(r).To(BeNil())
|
||||
Expect(err).To(MatchError("192.168.99.99 not in range set 192.168.0.1-192.168.0.254,172.16.1.1-172.16.1.254"))
|
||||
|
||||
})
|
||||
|
||||
It("should discover overlaps within a set", func() {
|
||||
p := RangeSet{
|
||||
{Subnet: mustSubnet("192.168.0.0/20")},
|
||||
{Subnet: mustSubnet("192.168.2.0/24")},
|
||||
}
|
||||
|
||||
err := p.Canonicalize()
|
||||
Expect(err).To(MatchError("subnets 192.168.0.1-192.168.15.254 and 192.168.2.1-192.168.2.254 overlap"))
|
||||
})
|
||||
|
||||
It("should discover overlaps outside a set", func() {
|
||||
p1 := RangeSet{
|
||||
{Subnet: mustSubnet("192.168.0.0/20")},
|
||||
}
|
||||
p2 := RangeSet{
|
||||
{Subnet: mustSubnet("192.168.2.0/24")},
|
||||
}
|
||||
|
||||
p1.Canonicalize()
|
||||
p2.Canonicalize()
|
||||
|
||||
Expect(p1.Overlaps(&p2)).To(BeTrue())
|
||||
Expect(p2.Overlaps(&p1)).To(BeTrue())
|
||||
})
|
||||
})
|
209
plugins/ipam/host-local/backend/allocator/range_test.go
Normal file
209
plugins/ipam/host-local/backend/allocator/range_test.go
Normal file
@ -0,0 +1,209 @@
|
||||
// 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 allocator
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/ginkgo/extensions/table"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("IP ranges", func() {
|
||||
It("should generate sane defaults for ipv4", func() {
|
||||
snstr := "192.0.2.0/24"
|
||||
r := Range{Subnet: mustSubnet(snstr)}
|
||||
|
||||
err := r.Canonicalize()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
Expect(r).To(Equal(Range{
|
||||
Subnet: mustSubnet(snstr),
|
||||
RangeStart: net.IP{192, 0, 2, 1},
|
||||
RangeEnd: net.IP{192, 0, 2, 254},
|
||||
Gateway: net.IP{192, 0, 2, 1},
|
||||
}))
|
||||
})
|
||||
It("should generate sane defaults for a smaller ipv4 subnet", func() {
|
||||
snstr := "192.0.2.0/25"
|
||||
r := Range{Subnet: mustSubnet(snstr)}
|
||||
|
||||
err := r.Canonicalize()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
Expect(r).To(Equal(Range{
|
||||
Subnet: mustSubnet(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() {
|
||||
snstr := "2001:DB8:1::/64"
|
||||
r := Range{Subnet: mustSubnet(snstr)}
|
||||
|
||||
err := r.Canonicalize()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
Expect(r).To(Equal(Range{
|
||||
Subnet: mustSubnet(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"),
|
||||
}))
|
||||
})
|
||||
|
||||
It("Should reject a network that's too small", func() {
|
||||
r := Range{Subnet: mustSubnet("192.0.2.0/31")}
|
||||
err := r.Canonicalize()
|
||||
Expect(err).Should(MatchError("Network 192.0.2.0/31 too small to allocate from"))
|
||||
})
|
||||
|
||||
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")}
|
||||
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")}
|
||||
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"),
|
||||
RangeStart: net.ParseIP("192.0.2.50"),
|
||||
RangeEnd: net.ParseIP("192.0.2.40"),
|
||||
}
|
||||
err = r.Canonicalize()
|
||||
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() {
|
||||
r := Range{
|
||||
Subnet: mustSubnet("192.0.2.0/24"),
|
||||
RangeStart: net.ParseIP("192.0.2.40"),
|
||||
RangeEnd: net.ParseIP("192.0.2.50"),
|
||||
Gateway: net.ParseIP("192.0.2.254"),
|
||||
}
|
||||
err := r.Canonicalize()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
Expect(r).To(Equal(Range{
|
||||
Subnet: mustSubnet("192.0.2.0/24"),
|
||||
RangeStart: net.IP{192, 0, 2, 40},
|
||||
RangeEnd: net.IP{192, 0, 2, 50},
|
||||
Gateway: net.IP{192, 0, 2, 254},
|
||||
}))
|
||||
})
|
||||
|
||||
It("should accept v4 IPs in range and reject IPs out of range", func() {
|
||||
r := Range{
|
||||
Subnet: mustSubnet("192.0.2.0/24"),
|
||||
RangeStart: net.ParseIP("192.0.2.40"),
|
||||
RangeEnd: net.ParseIP("192.0.2.50"),
|
||||
Gateway: net.ParseIP("192.0.2.254"),
|
||||
}
|
||||
err := r.Canonicalize()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
Expect(r.Contains(net.ParseIP("192.0.3.0"))).Should(BeFalse())
|
||||
|
||||
Expect(r.Contains(net.ParseIP("192.0.2.39"))).Should(BeFalse())
|
||||
Expect(r.Contains(net.ParseIP("192.0.2.40"))).Should(BeTrue())
|
||||
Expect(r.Contains(net.ParseIP("192.0.2.50"))).Should(BeTrue())
|
||||
Expect(r.Contains(net.ParseIP("192.0.2.51"))).Should(BeFalse())
|
||||
})
|
||||
|
||||
It("should accept v6 IPs in range and reject IPs out of range", func() {
|
||||
r := Range{
|
||||
Subnet: mustSubnet("2001:DB8:1::/64"),
|
||||
RangeStart: net.ParseIP("2001:db8:1::40"),
|
||||
RangeEnd: net.ParseIP("2001:db8:1::50"),
|
||||
}
|
||||
err := r.Canonicalize()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(r.Contains(net.ParseIP("2001:db8:2::"))).Should(BeFalse())
|
||||
|
||||
Expect(r.Contains(net.ParseIP("2001:db8:1::39"))).Should(BeFalse())
|
||||
Expect(r.Contains(net.ParseIP("2001:db8:1::40"))).Should(BeTrue())
|
||||
Expect(r.Contains(net.ParseIP("2001:db8:1::50"))).Should(BeTrue())
|
||||
Expect(r.Contains(net.ParseIP("2001:db8:1::51"))).Should(BeFalse())
|
||||
})
|
||||
|
||||
DescribeTable("Detecting overlap",
|
||||
func(r1 Range, r2 Range, expected bool) {
|
||||
r1.Canonicalize()
|
||||
r2.Canonicalize()
|
||||
|
||||
// operation should be commutative
|
||||
Expect(r1.Overlaps(&r2)).To(Equal(expected))
|
||||
Expect(r2.Overlaps(&r1)).To(Equal(expected))
|
||||
},
|
||||
Entry("non-overlapping",
|
||||
Range{Subnet: mustSubnet("10.0.0.0/24")},
|
||||
Range{Subnet: mustSubnet("10.0.1.0/24")},
|
||||
false),
|
||||
Entry("different families",
|
||||
// Note that the bits overlap
|
||||
Range{Subnet: mustSubnet("0.0.0.0/24")},
|
||||
Range{Subnet: mustSubnet("::/24")},
|
||||
false),
|
||||
Entry("Identical",
|
||||
Range{Subnet: mustSubnet("10.0.0.0/24")},
|
||||
Range{Subnet: mustSubnet("10.0.0.0/24")},
|
||||
true),
|
||||
Entry("Containing",
|
||||
Range{Subnet: mustSubnet("10.0.0.0/20")},
|
||||
Range{Subnet: mustSubnet("10.0.1.0/24")},
|
||||
true),
|
||||
Entry("same subnet, non overlapping start + end",
|
||||
Range{
|
||||
Subnet: mustSubnet("10.0.0.0/24"),
|
||||
RangeEnd: net.ParseIP("10.0.0.127"),
|
||||
},
|
||||
Range{
|
||||
Subnet: mustSubnet("10.0.0.0/24"),
|
||||
RangeStart: net.ParseIP("10.0.0.128"),
|
||||
},
|
||||
false),
|
||||
Entry("same subnet, overlapping start + end",
|
||||
Range{
|
||||
Subnet: mustSubnet("10.0.0.0/24"),
|
||||
RangeEnd: net.ParseIP("10.0.0.127"),
|
||||
},
|
||||
Range{
|
||||
Subnet: mustSubnet("10.0.0.0/24"),
|
||||
RangeStart: net.ParseIP("10.0.0.127"),
|
||||
},
|
||||
true),
|
||||
)
|
||||
})
|
||||
|
||||
func mustSubnet(s string) types.IPNet {
|
||||
n, err := types.ParseCIDR(s)
|
||||
if err != nil {
|
||||
Fail(err.Error())
|
||||
}
|
||||
canonicalizeIP(&n.IP)
|
||||
return types.IPNet(*n)
|
||||
}
|
@ -19,18 +19,32 @@ import (
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/containernetworking/plugins/plugins/ipam/host-local/backend"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
const lastIPFilePrefix = "last_reserved_ip."
|
||||
|
||||
var defaultDataDir = "/var/lib/cni/networks"
|
||||
|
||||
// Store is a simple disk-backed store that creates one file per IP
|
||||
// address in a given directory. The contents of the file are the container ID.
|
||||
type Store struct {
|
||||
FileLock
|
||||
*FileLock
|
||||
dataDir string
|
||||
}
|
||||
|
||||
func New(network string) (*Store, error) {
|
||||
dir := filepath.Join(defaultDataDir, network)
|
||||
if err := os.MkdirAll(dir, 0644); err != nil {
|
||||
// Store implements the Store interface
|
||||
var _ backend.Store = &Store{}
|
||||
|
||||
func New(network, dataDir string) (*Store, error) {
|
||||
if dataDir == "" {
|
||||
dataDir = defaultDataDir
|
||||
}
|
||||
dir := filepath.Join(dataDir, network)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@ -38,11 +52,12 @@ func New(network string) (*Store, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Store{*lk, dir}, nil
|
||||
return &Store{lk, dir}, nil
|
||||
}
|
||||
|
||||
func (s *Store) Reserve(id string, ip net.IP) (bool, error) {
|
||||
fname := filepath.Join(s.dataDir, ip.String())
|
||||
func (s *Store) Reserve(id string, ip net.IP, rangeID string) (bool, error) {
|
||||
fname := GetEscapedPath(s.dataDir, ip.String())
|
||||
|
||||
f, err := os.OpenFile(fname, os.O_RDWR|os.O_EXCL|os.O_CREATE, 0644)
|
||||
if os.IsExist(err) {
|
||||
return false, nil
|
||||
@ -50,7 +65,7 @@ func (s *Store) Reserve(id string, ip net.IP) (bool, error) {
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if _, err := f.WriteString(id); err != nil {
|
||||
if _, err := f.WriteString(strings.TrimSpace(id)); err != nil {
|
||||
f.Close()
|
||||
os.Remove(f.Name())
|
||||
return false, err
|
||||
@ -59,11 +74,27 @@ func (s *Store) Reserve(id string, ip net.IP) (bool, error) {
|
||||
os.Remove(f.Name())
|
||||
return false, err
|
||||
}
|
||||
// store the reserved ip in lastIPFile
|
||||
ipfile := GetEscapedPath(s.dataDir, lastIPFilePrefix+rangeID)
|
||||
err = ioutil.WriteFile(ipfile, []byte(ip.String()), 0644)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// LastReservedIP returns the last reserved IP if exists
|
||||
func (s *Store) LastReservedIP(rangeID string) (net.IP, error) {
|
||||
ipfile := GetEscapedPath(s.dataDir, lastIPFilePrefix+rangeID)
|
||||
data, err := ioutil.ReadFile(ipfile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return net.ParseIP(string(data)), nil
|
||||
}
|
||||
|
||||
func (s *Store) Release(ip net.IP) error {
|
||||
return os.Remove(filepath.Join(s.dataDir, ip.String()))
|
||||
return os.Remove(GetEscapedPath(s.dataDir, ip.String()))
|
||||
}
|
||||
|
||||
// N.B. This function eats errors to be tolerant and
|
||||
@ -77,7 +108,7 @@ func (s *Store) ReleaseByID(id string) error {
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if string(data) == id {
|
||||
if strings.TrimSpace(string(data)) == strings.TrimSpace(id) {
|
||||
if err := os.Remove(path); err != nil {
|
||||
return nil
|
||||
}
|
||||
@ -86,3 +117,10 @@ func (s *Store) ReleaseByID(id string) error {
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func GetEscapedPath(dataDir string, fname string) string {
|
||||
if runtime.GOOS == "windows" {
|
||||
fname = strings.Replace(fname, ":", "_", -1)
|
||||
}
|
||||
return filepath.Join(dataDir, fname)
|
||||
}
|
||||
|
27
plugins/ipam/host-local/backend/disk/disk_suite_test.go
Normal file
27
plugins/ipam/host-local/backend/disk/disk_suite_test.go
Normal file
@ -0,0 +1,27 @@
|
||||
// Copyright 2016 CNI authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package disk
|
||||
|
||||
import (
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLock(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Disk Suite")
|
||||
}
|
@ -15,18 +15,28 @@
|
||||
package disk
|
||||
|
||||
import (
|
||||
"github.com/alexflint/go-filemutex"
|
||||
"os"
|
||||
"syscall"
|
||||
"path"
|
||||
)
|
||||
|
||||
// FileLock wraps os.File to be used as a lock using flock
|
||||
type FileLock struct {
|
||||
f *os.File
|
||||
f *filemutex.FileMutex
|
||||
}
|
||||
|
||||
// NewFileLock opens file/dir at path and returns unlocked FileLock object
|
||||
func NewFileLock(path string) (*FileLock, error) {
|
||||
f, err := os.Open(path)
|
||||
func NewFileLock(lockPath string) (*FileLock, error) {
|
||||
fi, err := os.Stat(lockPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if fi.IsDir() {
|
||||
lockPath = path.Join(lockPath, "lock")
|
||||
}
|
||||
|
||||
f, err := filemutex.New(lockPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -34,17 +44,16 @@ func NewFileLock(path string) (*FileLock, error) {
|
||||
return &FileLock{f}, nil
|
||||
}
|
||||
|
||||
// Close closes underlying file
|
||||
func (l *FileLock) Close() error {
|
||||
return l.f.Close()
|
||||
}
|
||||
|
||||
// Lock acquires an exclusive lock
|
||||
func (l *FileLock) Lock() error {
|
||||
return syscall.Flock(int(l.f.Fd()), syscall.LOCK_EX)
|
||||
return l.f.Lock()
|
||||
}
|
||||
|
||||
// Unlock releases the lock
|
||||
func (l *FileLock) Unlock() error {
|
||||
return syscall.Flock(int(l.f.Fd()), syscall.LOCK_UN)
|
||||
return l.f.Unlock()
|
||||
}
|
||||
|
63
plugins/ipam/host-local/backend/disk/lock_test.go
Normal file
63
plugins/ipam/host-local/backend/disk/lock_test.go
Normal file
@ -0,0 +1,63 @@
|
||||
// Copyright 2016 CNI authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package disk
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("Lock Operations", func() {
|
||||
It("locks a file path", func() {
|
||||
dir, err := ioutil.TempDir("", "")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
// create a dummy file to lock
|
||||
path := filepath.Join(dir, "x")
|
||||
f, err := os.OpenFile(path, os.O_RDONLY|os.O_CREATE, 0666)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
err = f.Close()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
// now use it to lock
|
||||
m, err := NewFileLock(path)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
err = m.Lock()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
err = m.Unlock()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
|
||||
It("locks a folder path", func() {
|
||||
dir, err := ioutil.TempDir("", "")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
// use the folder to lock
|
||||
m, err := NewFileLock(dir)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
err = m.Lock()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
err = m.Unlock()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
})
|
@ -20,7 +20,8 @@ type Store interface {
|
||||
Lock() error
|
||||
Unlock() error
|
||||
Close() error
|
||||
Reserve(id string, ip net.IP) (bool, error)
|
||||
Reserve(id string, ip net.IP, rangeID string) (bool, error)
|
||||
LastReservedIP(rangeID string) (net.IP, error)
|
||||
Release(ip net.IP) error
|
||||
ReleaseByID(id string) error
|
||||
}
|
||||
|
86
plugins/ipam/host-local/backend/testing/fake_store.go
Normal file
86
plugins/ipam/host-local/backend/testing/fake_store.go
Normal file
@ -0,0 +1,86 @@
|
||||
// Copyright 2015 CNI authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package testing
|
||||
|
||||
import (
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"github.com/containernetworking/plugins/plugins/ipam/host-local/backend"
|
||||
)
|
||||
|
||||
type FakeStore struct {
|
||||
ipMap map[string]string
|
||||
lastReservedIP map[string]net.IP
|
||||
}
|
||||
|
||||
// FakeStore implements the Store interface
|
||||
var _ backend.Store = &FakeStore{}
|
||||
|
||||
func NewFakeStore(ipmap map[string]string, lastIPs map[string]net.IP) *FakeStore {
|
||||
return &FakeStore{ipmap, lastIPs}
|
||||
}
|
||||
|
||||
func (s *FakeStore) Lock() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *FakeStore) Unlock() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *FakeStore) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *FakeStore) Reserve(id string, ip net.IP, rangeID string) (bool, error) {
|
||||
key := ip.String()
|
||||
if _, ok := s.ipMap[key]; !ok {
|
||||
s.ipMap[key] = id
|
||||
s.lastReservedIP[rangeID] = ip
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (s *FakeStore) LastReservedIP(rangeID string) (net.IP, error) {
|
||||
ip, ok := s.lastReservedIP[rangeID]
|
||||
if !ok {
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
return ip, nil
|
||||
}
|
||||
|
||||
func (s *FakeStore) Release(ip net.IP) error {
|
||||
delete(s.ipMap, ip.String())
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *FakeStore) ReleaseByID(id string) error {
|
||||
toDelete := []string{}
|
||||
for k, v := range s.ipMap {
|
||||
if v == id {
|
||||
toDelete = append(toDelete, k)
|
||||
}
|
||||
}
|
||||
for _, ip := range toDelete {
|
||||
delete(s.ipMap, ip)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *FakeStore) SetIPMap(m map[string]string) {
|
||||
s.ipMap = m
|
||||
}
|
@ -1,70 +0,0 @@
|
||||
// Copyright 2015 CNI authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
)
|
||||
|
||||
// IPAMConfig represents the IP related network configuration.
|
||||
type IPAMConfig struct {
|
||||
Name string
|
||||
Type string `json:"type"`
|
||||
RangeStart net.IP `json:"rangeStart"`
|
||||
RangeEnd net.IP `json:"rangeEnd"`
|
||||
Subnet types.IPNet `json:"subnet"`
|
||||
Gateway net.IP `json:"gateway"`
|
||||
Routes []types.Route `json:"routes"`
|
||||
Args *IPAMArgs `json:"-"`
|
||||
}
|
||||
|
||||
type IPAMArgs struct {
|
||||
types.CommonArgs
|
||||
IP net.IP `json:"ip,omitempty"`
|
||||
}
|
||||
|
||||
type Net struct {
|
||||
Name string `json:"name"`
|
||||
IPAM *IPAMConfig `json:"ipam"`
|
||||
}
|
||||
|
||||
// NewIPAMConfig creates a NetworkConfig from the given network name.
|
||||
func LoadIPAMConfig(bytes []byte, args string) (*IPAMConfig, error) {
|
||||
n := Net{}
|
||||
if err := json.Unmarshal(bytes, &n); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if args != "" {
|
||||
n.IPAM.Args = &IPAMArgs{}
|
||||
err := types.LoadArgs(args, n.IPAM.Args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if n.IPAM == nil {
|
||||
return nil, fmt.Errorf("%q missing 'ipam' key")
|
||||
}
|
||||
|
||||
// Copy net name into IPAM so not to drag Net struct around
|
||||
n.IPAM.Name = n.Name
|
||||
|
||||
return n.IPAM, nil
|
||||
}
|
64
plugins/ipam/host-local/dns.go
Normal file
64
plugins/ipam/host-local/dns.go
Normal file
@ -0,0 +1,64 @@
|
||||
// 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 (
|
||||
"bufio"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
)
|
||||
|
||||
// parseResolvConf parses an existing resolv.conf in to a DNS struct
|
||||
func parseResolvConf(filename string) (*types.DNS, error) {
|
||||
fp, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dns := types.DNS{}
|
||||
scanner := bufio.NewScanner(fp)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
line = strings.TrimSpace(line)
|
||||
|
||||
// Skip comments, empty lines
|
||||
if len(line) == 0 || line[0] == '#' || line[0] == ';' {
|
||||
continue
|
||||
}
|
||||
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 2 {
|
||||
continue
|
||||
}
|
||||
switch fields[0] {
|
||||
case "nameserver":
|
||||
dns.Nameservers = append(dns.Nameservers, fields[1])
|
||||
case "domain":
|
||||
dns.Domain = fields[1]
|
||||
case "search":
|
||||
dns.Search = append(dns.Search, fields[1:]...)
|
||||
case "options":
|
||||
dns.Options = append(dns.Options, fields[1:]...)
|
||||
}
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &dns, nil
|
||||
}
|
80
plugins/ipam/host-local/dns_test.go
Normal file
80
plugins/ipam/host-local/dns_test.go
Normal file
@ -0,0 +1,80 @@
|
||||
// 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 (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("parsing resolv.conf", func() {
|
||||
It("parses a simple resolv.conf file", func() {
|
||||
contents := `
|
||||
nameserver 192.0.2.0
|
||||
nameserver 192.0.2.1
|
||||
`
|
||||
dns, err := parse(contents)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(*dns).Should(Equal(types.DNS{Nameservers: []string{"192.0.2.0", "192.0.2.1"}}))
|
||||
})
|
||||
It("ignores comments", func() {
|
||||
dns, err := parse(`
|
||||
nameserver 192.0.2.0
|
||||
;nameserver 192.0.2.1
|
||||
`)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(*dns).Should(Equal(types.DNS{Nameservers: []string{"192.0.2.0"}}))
|
||||
})
|
||||
It("parses all fields", func() {
|
||||
dns, err := parse(`
|
||||
nameserver 192.0.2.0
|
||||
nameserver 192.0.2.2
|
||||
domain example.com
|
||||
;nameserver comment
|
||||
#nameserver comment
|
||||
search example.net example.org
|
||||
search example.gov
|
||||
options one two three
|
||||
options four
|
||||
`)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(*dns).Should(Equal(types.DNS{
|
||||
Nameservers: []string{"192.0.2.0", "192.0.2.2"},
|
||||
Domain: "example.com",
|
||||
Search: []string{"example.net", "example.org", "example.gov"},
|
||||
Options: []string{"one", "two", "three", "four"},
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
||||
func parse(contents string) (*types.DNS, error) {
|
||||
f, err := ioutil.TempFile("", "host_local_resolv")
|
||||
defer f.Close()
|
||||
defer os.Remove(f.Name())
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, err := f.WriteString(contents); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return parseResolvConf(f.Name())
|
||||
}
|
27
plugins/ipam/host-local/host_local_suite_test.go
Normal file
27
plugins/ipam/host-local/host_local_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 TestHostLocal(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "HostLocal Suite")
|
||||
}
|
538
plugins/ipam/host-local/host_local_test.go
Normal file
538
plugins/ipam/host-local/host_local_test.go
Normal file
@ -0,0 +1,538 @@
|
||||
// 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 (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/skel"
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
"github.com/containernetworking/cni/pkg/types/020"
|
||||
"github.com/containernetworking/cni/pkg/types/current"
|
||||
"github.com/containernetworking/plugins/pkg/testutils"
|
||||
|
||||
"github.com/containernetworking/plugins/plugins/ipam/host-local/backend/disk"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("host-local Operations", func() {
|
||||
It("allocates and releases addresses with ADD/DEL", func() {
|
||||
const ifname string = "eth0"
|
||||
const nspath string = "/some/where"
|
||||
|
||||
tmpDir, err := 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": "foo0",
|
||||
"ipam": {
|
||||
"type": "host-local",
|
||||
"dataDir": "%s",
|
||||
"resolvConf": "%s/resolv.conf",
|
||||
"ranges": [
|
||||
[{ "subnet": "10.1.2.0/24" }, {"subnet": "10.2.2.0/24"}],
|
||||
[{ "subnet": "2001:db8:1::0/64" }]
|
||||
],
|
||||
"routes": [
|
||||
{"dst": "0.0.0.0/0"},
|
||||
{"dst": "::/0"},
|
||||
{"dst": "192.168.0.0/16", "gw": "1.1.1.1"},
|
||||
{"dst": "2001:db8:2::0/64", "gw": "2001:db8:3::1"}
|
||||
]
|
||||
}
|
||||
}`, tmpDir, tmpDir)
|
||||
|
||||
args := &skel.CmdArgs{
|
||||
ContainerID: "dummy",
|
||||
Netns: nspath,
|
||||
IfName: ifname,
|
||||
StdinData: []byte(conf),
|
||||
}
|
||||
|
||||
// Allocate the IP
|
||||
r, raw, err := testutils.CmdAddWithResult(nspath, ifname, []byte(conf), 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.1.2.2/24"),
|
||||
Gateway: net.ParseIP("10.1.2.1"),
|
||||
}))
|
||||
|
||||
Expect(*result.IPs[1]).To(Equal(
|
||||
current.IPConfig{
|
||||
Version: "6",
|
||||
Address: mustCIDR("2001:db8:1::2/64"),
|
||||
Gateway: net.ParseIP("2001:db8:1::1"),
|
||||
},
|
||||
))
|
||||
Expect(len(result.IPs)).To(Equal(2))
|
||||
|
||||
Expect(result.Routes).To(Equal([]*types.Route{
|
||||
{Dst: mustCIDR("0.0.0.0/0"), GW: nil},
|
||||
{Dst: mustCIDR("::/0"), GW: nil},
|
||||
{Dst: mustCIDR("192.168.0.0/16"), GW: net.ParseIP("1.1.1.1")},
|
||||
{Dst: mustCIDR("2001:db8:2::0/64"), GW: net.ParseIP("2001:db8:3::1")},
|
||||
}))
|
||||
|
||||
ipFilePath1 := filepath.Join(tmpDir, "mynet", "10.1.2.2")
|
||||
contents, err := ioutil.ReadFile(ipFilePath1)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(string(contents)).To(Equal("dummy"))
|
||||
|
||||
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"))
|
||||
|
||||
lastFilePath1 := filepath.Join(tmpDir, "mynet", "last_reserved_ip.0")
|
||||
contents, err = ioutil.ReadFile(lastFilePath1)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(string(contents)).To(Equal("10.1.2.2"))
|
||||
|
||||
lastFilePath2 := filepath.Join(tmpDir, "mynet", "last_reserved_ip.1")
|
||||
contents, err = ioutil.ReadFile(lastFilePath2)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(string(contents)).To(Equal("2001:db8:1::2"))
|
||||
// Release the IP
|
||||
err = testutils.CmdDelWithResult(nspath, ifname, func() error {
|
||||
return cmdDel(args)
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
_, err = os.Stat(ipFilePath1)
|
||||
Expect(err).To(HaveOccurred())
|
||||
_, err = os.Stat(ipFilePath2)
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
|
||||
It("doesn't error when passed an unknown ID on DEL", func() {
|
||||
const ifname string = "eth0"
|
||||
const nspath string = "/some/where"
|
||||
|
||||
tmpDir, err := getTmpDir()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
conf := fmt.Sprintf(`{
|
||||
"cniVersion": "0.3.0",
|
||||
"name": "mynet",
|
||||
"type": "ipvlan",
|
||||
"master": "foo0",
|
||||
"ipam": {
|
||||
"type": "host-local",
|
||||
"subnet": "10.1.2.0/24",
|
||||
"dataDir": "%s"
|
||||
}
|
||||
}`, tmpDir)
|
||||
|
||||
args := &skel.CmdArgs{
|
||||
ContainerID: "dummy",
|
||||
Netns: nspath,
|
||||
IfName: ifname,
|
||||
StdinData: []byte(conf),
|
||||
}
|
||||
|
||||
// Release the IP
|
||||
err = testutils.CmdDelWithResult(nspath, ifname, func() error {
|
||||
return cmdDel(args)
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
|
||||
It("allocates and releases an address with ADD/DEL and 0.1.0 config", func() {
|
||||
const ifname string = "eth0"
|
||||
const nspath string = "/some/where"
|
||||
|
||||
tmpDir, err := getTmpDir()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
err = ioutil.WriteFile(filepath.Join(tmpDir, "resolv.conf"), []byte("nameserver 192.0.2.3"), 0644)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
conf := fmt.Sprintf(`{
|
||||
"cniVersion": "0.1.0",
|
||||
"name": "mynet",
|
||||
"type": "ipvlan",
|
||||
"master": "foo0",
|
||||
"ipam": {
|
||||
"type": "host-local",
|
||||
"subnet": "10.1.2.0/24",
|
||||
"dataDir": "%s",
|
||||
"resolvConf": "%s/resolv.conf"
|
||||
}
|
||||
}`, tmpDir, tmpDir)
|
||||
|
||||
args := &skel.CmdArgs{
|
||||
ContainerID: "dummy",
|
||||
Netns: nspath,
|
||||
IfName: ifname,
|
||||
StdinData: []byte(conf),
|
||||
}
|
||||
|
||||
// Allocate the IP
|
||||
r, raw, err := testutils.CmdAddWithResult(nspath, ifname, []byte(conf), func() error {
|
||||
return cmdAdd(args)
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(strings.Index(string(raw), "\"ip4\":")).Should(BeNumerically(">", 0))
|
||||
|
||||
result, err := types020.GetResult(r)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
expectedAddress, err := types.ParseCIDR("10.1.2.2/24")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
expectedAddress.IP = expectedAddress.IP.To16()
|
||||
Expect(result.IP4.IP).To(Equal(*expectedAddress))
|
||||
Expect(result.IP4.Gateway).To(Equal(net.ParseIP("10.1.2.1")))
|
||||
|
||||
ipFilePath := filepath.Join(tmpDir, "mynet", "10.1.2.2")
|
||||
contents, err := ioutil.ReadFile(ipFilePath)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(string(contents)).To(Equal("dummy"))
|
||||
|
||||
lastFilePath := filepath.Join(tmpDir, "mynet", "last_reserved_ip.0")
|
||||
contents, err = ioutil.ReadFile(lastFilePath)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(string(contents)).To(Equal("10.1.2.2"))
|
||||
|
||||
Expect(result.DNS).To(Equal(types.DNS{Nameservers: []string{"192.0.2.3"}}))
|
||||
|
||||
// Release the IP
|
||||
err = testutils.CmdDelWithResult(nspath, ifname, func() error {
|
||||
return cmdDel(args)
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
_, err = os.Stat(ipFilePath)
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
|
||||
It("ignores whitespace in disk files", func() {
|
||||
const ifname string = "eth0"
|
||||
const nspath string = "/some/where"
|
||||
|
||||
tmpDir, err := getTmpDir()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
conf := fmt.Sprintf(`{
|
||||
"cniVersion": "0.3.1",
|
||||
"name": "mynet",
|
||||
"type": "ipvlan",
|
||||
"master": "foo0",
|
||||
"ipam": {
|
||||
"type": "host-local",
|
||||
"subnet": "10.1.2.0/24",
|
||||
"dataDir": "%s"
|
||||
}
|
||||
}`, tmpDir)
|
||||
|
||||
args := &skel.CmdArgs{
|
||||
ContainerID: " dummy\n ",
|
||||
Netns: nspath,
|
||||
IfName: ifname,
|
||||
StdinData: []byte(conf),
|
||||
}
|
||||
|
||||
// Allocate the IP
|
||||
r, _, err := testutils.CmdAddWithResult(nspath, ifname, []byte(conf), func() error {
|
||||
return cmdAdd(args)
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
result, err := current.GetResult(r)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
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"))
|
||||
|
||||
// Release the IP
|
||||
err = testutils.CmdDelWithResult(nspath, ifname, func() error {
|
||||
return cmdDel(args)
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
_, err = os.Stat(ipFilePath)
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
|
||||
It("does not output an error message upon initial subnet creation", func() {
|
||||
const ifname string = "eth0"
|
||||
const nspath string = "/some/where"
|
||||
|
||||
tmpDir, err := getTmpDir()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
conf := fmt.Sprintf(`{
|
||||
"cniVersion": "0.2.0",
|
||||
"name": "mynet",
|
||||
"type": "ipvlan",
|
||||
"master": "foo0",
|
||||
"ipam": {
|
||||
"type": "host-local",
|
||||
"subnet": "10.1.2.0/24",
|
||||
"dataDir": "%s"
|
||||
}
|
||||
}`, tmpDir)
|
||||
|
||||
args := &skel.CmdArgs{
|
||||
ContainerID: "testing",
|
||||
Netns: nspath,
|
||||
IfName: ifname,
|
||||
StdinData: []byte(conf),
|
||||
}
|
||||
|
||||
// Allocate the IP
|
||||
_, out, err := testutils.CmdAddWithResult(nspath, ifname, []byte(conf), func() error {
|
||||
return cmdAdd(args)
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(strings.Index(string(out), "Error retriving last reserved ip")).To(Equal(-1))
|
||||
})
|
||||
|
||||
It("allocates a custom IP when requested by config args", func() {
|
||||
const ifname string = "eth0"
|
||||
const nspath string = "/some/where"
|
||||
|
||||
tmpDir, err := getTmpDir()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
conf := fmt.Sprintf(`{
|
||||
"cniVersion": "0.3.1",
|
||||
"name": "mynet",
|
||||
"type": "ipvlan",
|
||||
"master": "foo0",
|
||||
"ipam": {
|
||||
"type": "host-local",
|
||||
"dataDir": "%s",
|
||||
"ranges": [
|
||||
[{ "subnet": "10.1.2.0/24" }]
|
||||
]
|
||||
},
|
||||
"args": {
|
||||
"cni": {
|
||||
"ips": ["10.1.2.88"]
|
||||
}
|
||||
}
|
||||
}`, tmpDir)
|
||||
|
||||
args := &skel.CmdArgs{
|
||||
ContainerID: "dummy",
|
||||
Netns: nspath,
|
||||
IfName: ifname,
|
||||
StdinData: []byte(conf),
|
||||
}
|
||||
|
||||
// Allocate the IP
|
||||
r, _, err := testutils.CmdAddWithResult(nspath, ifname, []byte(conf), func() error {
|
||||
return cmdAdd(args)
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
result, err := current.GetResult(r)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(result.IPs).To(HaveLen(1))
|
||||
Expect(result.IPs[0].Address.IP).To(Equal(net.ParseIP("10.1.2.88")))
|
||||
})
|
||||
|
||||
It("allocates custom IPs from multiple ranges", func() {
|
||||
const ifname string = "eth0"
|
||||
const nspath string = "/some/where"
|
||||
|
||||
tmpDir, err := getTmpDir()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
err = ioutil.WriteFile(filepath.Join(tmpDir, "resolv.conf"), []byte("nameserver 192.0.2.3"), 0644)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
conf := fmt.Sprintf(`{
|
||||
"cniVersion": "0.3.1",
|
||||
"name": "mynet",
|
||||
"type": "ipvlan",
|
||||
"master": "foo0",
|
||||
"ipam": {
|
||||
"type": "host-local",
|
||||
"dataDir": "%s",
|
||||
"ranges": [
|
||||
[{ "subnet": "10.1.2.0/24" }],
|
||||
[{ "subnet": "10.1.3.0/24" }]
|
||||
]
|
||||
},
|
||||
"args": {
|
||||
"cni": {
|
||||
"ips": ["10.1.2.88", "10.1.3.77"]
|
||||
}
|
||||
}
|
||||
}`, tmpDir)
|
||||
|
||||
args := &skel.CmdArgs{
|
||||
ContainerID: "dummy",
|
||||
Netns: nspath,
|
||||
IfName: ifname,
|
||||
StdinData: []byte(conf),
|
||||
}
|
||||
|
||||
// Allocate the IP
|
||||
r, _, err := testutils.CmdAddWithResult(nspath, ifname, []byte(conf), func() error {
|
||||
return cmdAdd(args)
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
result, err := current.GetResult(r)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(result.IPs).To(HaveLen(2))
|
||||
Expect(result.IPs[0].Address.IP).To(Equal(net.ParseIP("10.1.2.88")))
|
||||
Expect(result.IPs[1].Address.IP).To(Equal(net.ParseIP("10.1.3.77")))
|
||||
})
|
||||
|
||||
It("allocates custom IPs from multiple protocols", func() {
|
||||
const ifname string = "eth0"
|
||||
const nspath string = "/some/where"
|
||||
|
||||
tmpDir, err := getTmpDir()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
err = ioutil.WriteFile(filepath.Join(tmpDir, "resolv.conf"), []byte("nameserver 192.0.2.3"), 0644)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
conf := fmt.Sprintf(`{
|
||||
"cniVersion": "0.3.1",
|
||||
"name": "mynet",
|
||||
"type": "ipvlan",
|
||||
"master": "foo0",
|
||||
"ipam": {
|
||||
"type": "host-local",
|
||||
"dataDir": "%s",
|
||||
"ranges": [
|
||||
[{"subnet":"172.16.1.0/24"}, { "subnet": "10.1.2.0/24" }],
|
||||
[{ "subnet": "2001:db8:1::/24" }]
|
||||
]
|
||||
},
|
||||
"args": {
|
||||
"cni": {
|
||||
"ips": ["10.1.2.88", "2001:db8:1::999"]
|
||||
}
|
||||
}
|
||||
}`, tmpDir)
|
||||
|
||||
args := &skel.CmdArgs{
|
||||
ContainerID: "dummy",
|
||||
Netns: nspath,
|
||||
IfName: ifname,
|
||||
StdinData: []byte(conf),
|
||||
}
|
||||
|
||||
// Allocate the IP
|
||||
r, _, err := testutils.CmdAddWithResult(nspath, ifname, []byte(conf), func() error {
|
||||
return cmdAdd(args)
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
result, err := current.GetResult(r)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(result.IPs).To(HaveLen(2))
|
||||
Expect(result.IPs[0].Address.IP).To(Equal(net.ParseIP("10.1.2.88")))
|
||||
Expect(result.IPs[1].Address.IP).To(Equal(net.ParseIP("2001:db8:1::999")))
|
||||
})
|
||||
|
||||
It("fails if a requested custom IP is not used", func() {
|
||||
const ifname string = "eth0"
|
||||
const nspath string = "/some/where"
|
||||
|
||||
tmpDir, err := getTmpDir()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
conf := fmt.Sprintf(`{
|
||||
"cniVersion": "0.3.1",
|
||||
"name": "mynet",
|
||||
"type": "ipvlan",
|
||||
"master": "foo0",
|
||||
"ipam": {
|
||||
"type": "host-local",
|
||||
"dataDir": "%s",
|
||||
"ranges": [
|
||||
[{ "subnet": "10.1.2.0/24" }],
|
||||
[{ "subnet": "10.1.3.0/24" }]
|
||||
]
|
||||
},
|
||||
"args": {
|
||||
"cni": {
|
||||
"ips": ["10.1.2.88", "10.1.2.77"]
|
||||
}
|
||||
}
|
||||
}`, tmpDir)
|
||||
|
||||
args := &skel.CmdArgs{
|
||||
ContainerID: "dummy",
|
||||
Netns: nspath,
|
||||
IfName: ifname,
|
||||
StdinData: []byte(conf),
|
||||
}
|
||||
|
||||
// Allocate the IP
|
||||
_, _, err = testutils.CmdAddWithResult(nspath, ifname, []byte(conf), func() error {
|
||||
return cmdAdd(args)
|
||||
})
|
||||
Expect(err).To(HaveOccurred())
|
||||
// Need to match prefix, because ordering is not guaranteed
|
||||
Expect(err.Error()).To(HavePrefix("failed to allocate all requested IPs: 10.1.2."))
|
||||
})
|
||||
})
|
||||
|
||||
func getTmpDir() (string, error) {
|
||||
tmpDir, err := ioutil.TempDir("", "host_local_artifacts")
|
||||
if err == nil {
|
||||
tmpDir = filepath.ToSlash(tmpDir)
|
||||
}
|
||||
|
||||
return tmpDir, err
|
||||
}
|
||||
|
||||
func mustCIDR(s string) net.IPNet {
|
||||
ip, n, err := net.ParseCIDR(s)
|
||||
n.IP = ip
|
||||
if err != nil {
|
||||
Fail(err.Error())
|
||||
}
|
||||
|
||||
return *n
|
||||
}
|
@ -15,60 +15,126 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/containernetworking/cni/plugins/ipam/host-local/backend/disk"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/containernetworking/plugins/plugins/ipam/host-local/backend/allocator"
|
||||
"github.com/containernetworking/plugins/plugins/ipam/host-local/backend/disk"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
func main() {
|
||||
skel.PluginMain(cmdAdd, cmdDel)
|
||||
skel.PluginMain(cmdAdd, cmdDel, version.All)
|
||||
}
|
||||
|
||||
func cmdAdd(args *skel.CmdArgs) error {
|
||||
ipamConf, err := LoadIPAMConfig(args.StdinData, args.Args)
|
||||
ipamConf, confVersion, err := allocator.LoadIPAMConfig(args.StdinData, args.Args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
store, err := disk.New(ipamConf.Name)
|
||||
result := ¤t.Result{}
|
||||
|
||||
if ipamConf.ResolvConf != "" {
|
||||
dns, err := parseResolvConf(ipamConf.ResolvConf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
result.DNS = *dns
|
||||
}
|
||||
|
||||
store, err := disk.New(ipamConf.Name, ipamConf.DataDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
allocator, err := NewIPAllocator(ipamConf, store)
|
||||
if err != nil {
|
||||
return err
|
||||
// Keep the allocators we used, so we can release all IPs if an error
|
||||
// occurs after we start allocating
|
||||
allocs := []*allocator.IPAllocator{}
|
||||
|
||||
// Store all requested IPs in a map, so we can easily remove ones we use
|
||||
// and error if some remain
|
||||
requestedIPs := map[string]net.IP{} //net.IP cannot be a key
|
||||
|
||||
for _, ip := range ipamConf.IPArgs {
|
||||
requestedIPs[ip.String()] = ip
|
||||
}
|
||||
|
||||
ipConf, err := allocator.Get(args.ContainerID)
|
||||
if err != nil {
|
||||
return err
|
||||
for idx, rangeset := range ipamConf.Ranges {
|
||||
allocator := allocator.NewIPAllocator(&rangeset, store, idx)
|
||||
|
||||
// Check to see if there are any custom IPs requested in this range.
|
||||
var requestedIP net.IP
|
||||
for k, ip := range requestedIPs {
|
||||
if rangeset.Contains(ip) {
|
||||
requestedIP = ip
|
||||
delete(requestedIPs, k)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
ipConf, err := allocator.Get(args.ContainerID, requestedIP)
|
||||
if err != nil {
|
||||
// Deallocate all already allocated IPs
|
||||
for _, alloc := range allocs {
|
||||
_ = alloc.Release(args.ContainerID)
|
||||
}
|
||||
return fmt.Errorf("failed to allocate for range %d: %v", idx, err)
|
||||
}
|
||||
|
||||
allocs = append(allocs, allocator)
|
||||
|
||||
result.IPs = append(result.IPs, ipConf)
|
||||
}
|
||||
|
||||
r := &types.Result{
|
||||
IP4: ipConf,
|
||||
// If an IP was requested that wasn't fulfilled, fail
|
||||
if len(requestedIPs) != 0 {
|
||||
for _, alloc := range allocs {
|
||||
_ = alloc.Release(args.ContainerID)
|
||||
}
|
||||
errstr := "failed to allocate all requested IPs:"
|
||||
for _, ip := range requestedIPs {
|
||||
errstr = errstr + " " + ip.String()
|
||||
}
|
||||
return fmt.Errorf(errstr)
|
||||
}
|
||||
return r.Print()
|
||||
|
||||
result.Routes = ipamConf.Routes
|
||||
|
||||
return types.PrintResult(result, confVersion)
|
||||
}
|
||||
|
||||
func cmdDel(args *skel.CmdArgs) error {
|
||||
ipamConf, err := LoadIPAMConfig(args.StdinData, args.Args)
|
||||
ipamConf, _, err := allocator.LoadIPAMConfig(args.StdinData, args.Args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
store, err := disk.New(ipamConf.Name)
|
||||
store, err := disk.New(ipamConf.Name, ipamConf.DataDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
allocator, err := NewIPAllocator(ipamConf, store)
|
||||
if err != nil {
|
||||
return err
|
||||
// Loop through all ranges, releasing all IPs, even if an error occurs
|
||||
var errors []string
|
||||
for idx, rangeset := range ipamConf.Ranges {
|
||||
ipAllocator := allocator.NewIPAllocator(&rangeset, store, idx)
|
||||
|
||||
err := ipAllocator.Release(args.ContainerID)
|
||||
if err != nil {
|
||||
errors = append(errors, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
return allocator.Release(args.ContainerID)
|
||||
if errors != nil {
|
||||
return fmt.Errorf(strings.Join(errors, ";"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
10
plugins/linux_only.txt
Normal file
10
plugins/linux_only.txt
Normal file
@ -0,0 +1,10 @@
|
||||
plugins/ipam/dhcp
|
||||
plugins/main/bridge
|
||||
plugins/main/host-device
|
||||
plugins/main/ipvlan
|
||||
plugins/main/loopback
|
||||
plugins/main/macvlan
|
||||
plugins/main/ptp
|
||||
plugins/main/vlan
|
||||
plugins/meta/portmap
|
||||
plugins/meta/tuning
|
@ -18,6 +18,7 @@ If the bridge is missing, the plugin will create one on first use and, if gatewa
|
||||
"type": "bridge",
|
||||
"bridge": "mynet0",
|
||||
"isDefaultGateway": true,
|
||||
"forceAddress": false,
|
||||
"ipMasq": true,
|
||||
"hairpinMode": true,
|
||||
"ipam": {
|
||||
@ -34,7 +35,9 @@ If the bridge is missing, the plugin will create one on first use and, if gatewa
|
||||
* `bridge` (string, optional): name of the bridge to use/create. Defaults to "cni0".
|
||||
* `isGateway` (boolean, optional): assign an IP address to the bridge. Defaults to false.
|
||||
* `isDefaultGateway` (boolean, optional): Sets isGateway to true and makes the assigned IP the default route. Defaults to false.
|
||||
* `forceAddress` (boolean, optional): Indicates if a new IP address should be set if the previous value has been changed. Defaults to false.
|
||||
* `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.
|
||||
* `promiscMode` (boolean, optional): set promiscuous mode on the bridge. Defaults to false.
|
@ -22,12 +22,17 @@ import (
|
||||
"runtime"
|
||||
"syscall"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/ip"
|
||||
"github.com/containernetworking/cni/pkg/ipam"
|
||||
"github.com/containernetworking/cni/pkg/ns"
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/skel"
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
"github.com/containernetworking/cni/pkg/utils"
|
||||
"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"
|
||||
)
|
||||
|
||||
@ -35,12 +40,20 @@ const defaultBrName = "cni0"
|
||||
|
||||
type NetConf struct {
|
||||
types.NetConf
|
||||
BrName string `json:"bridge"`
|
||||
IsGW bool `json:"isGateway"`
|
||||
IsDefaultGW bool `json:"isDefaultGateway"`
|
||||
IPMasq bool `json:"ipMasq"`
|
||||
MTU int `json:"mtu"`
|
||||
HairpinMode bool `json:"hairpinMode"`
|
||||
BrName string `json:"bridge"`
|
||||
IsGW bool `json:"isGateway"`
|
||||
IsDefaultGW bool `json:"isDefaultGateway"`
|
||||
ForceAddress bool `json:"forceAddress"`
|
||||
IPMasq bool `json:"ipMasq"`
|
||||
MTU int `json:"mtu"`
|
||||
HairpinMode bool `json:"hairpinMode"`
|
||||
PromiscMode bool `json:"promiscMode"`
|
||||
}
|
||||
|
||||
type gwInfo struct {
|
||||
gws []net.IPNet
|
||||
family int
|
||||
defaultRouteFound bool
|
||||
}
|
||||
|
||||
func init() {
|
||||
@ -50,38 +63,133 @@ func init() {
|
||||
runtime.LockOSThread()
|
||||
}
|
||||
|
||||
func loadNetConf(bytes []byte) (*NetConf, error) {
|
||||
func loadNetConf(bytes []byte) (*NetConf, string, error) {
|
||||
n := &NetConf{
|
||||
BrName: defaultBrName,
|
||||
}
|
||||
if err := json.Unmarshal(bytes, n); err != nil {
|
||||
return nil, fmt.Errorf("failed to load netconf: %v", err)
|
||||
return nil, "", fmt.Errorf("failed to load netconf: %v", err)
|
||||
}
|
||||
return n, nil
|
||||
return n, n.CNIVersion, nil
|
||||
}
|
||||
|
||||
func ensureBridgeAddr(br *netlink.Bridge, ipn *net.IPNet) error {
|
||||
addrs, err := netlink.AddrList(br, syscall.AF_INET)
|
||||
// calcGateways processes the results from the IPAM plugin and does the
|
||||
// following for each IP family:
|
||||
// - Calculates and compiles a list of gateway addresses
|
||||
// - Adds a default route if needed
|
||||
func calcGateways(result *current.Result, n *NetConf) (*gwInfo, *gwInfo, error) {
|
||||
|
||||
gwsV4 := &gwInfo{}
|
||||
gwsV6 := &gwInfo{}
|
||||
|
||||
for _, ipc := range result.IPs {
|
||||
|
||||
// Determine if this config is IPv4 or IPv6
|
||||
var gws *gwInfo
|
||||
defaultNet := &net.IPNet{}
|
||||
switch {
|
||||
case ipc.Address.IP.To4() != nil:
|
||||
gws = gwsV4
|
||||
gws.family = netlink.FAMILY_V4
|
||||
defaultNet.IP = net.IPv4zero
|
||||
case len(ipc.Address.IP) == net.IPv6len:
|
||||
gws = gwsV6
|
||||
gws.family = netlink.FAMILY_V6
|
||||
defaultNet.IP = net.IPv6zero
|
||||
default:
|
||||
return nil, nil, fmt.Errorf("Unknown IP object: %v", ipc)
|
||||
}
|
||||
defaultNet.Mask = net.IPMask(defaultNet.IP)
|
||||
|
||||
// All IPs currently refer to the container interface
|
||||
ipc.Interface = current.Int(2)
|
||||
|
||||
// If not provided, calculate the gateway address corresponding
|
||||
// to the selected IP address
|
||||
if ipc.Gateway == nil && n.IsGW {
|
||||
ipc.Gateway = calcGatewayIP(&ipc.Address)
|
||||
}
|
||||
|
||||
// Add a default route for this family using the current
|
||||
// gateway address if necessary.
|
||||
if n.IsDefaultGW && !gws.defaultRouteFound {
|
||||
for _, route := range result.Routes {
|
||||
if route.GW != nil && defaultNet.String() == route.Dst.String() {
|
||||
gws.defaultRouteFound = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !gws.defaultRouteFound {
|
||||
result.Routes = append(
|
||||
result.Routes,
|
||||
&types.Route{Dst: *defaultNet, GW: ipc.Gateway},
|
||||
)
|
||||
gws.defaultRouteFound = true
|
||||
}
|
||||
}
|
||||
|
||||
// Append this gateway address to the list of gateways
|
||||
if n.IsGW {
|
||||
gw := net.IPNet{
|
||||
IP: ipc.Gateway,
|
||||
Mask: ipc.Address.Mask,
|
||||
}
|
||||
gws.gws = append(gws.gws, gw)
|
||||
}
|
||||
}
|
||||
return gwsV4, gwsV6, nil
|
||||
}
|
||||
|
||||
func ensureBridgeAddr(br *netlink.Bridge, 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)
|
||||
}
|
||||
|
||||
// if there're no addresses on the bridge, it's ok -- we'll add one
|
||||
if len(addrs) > 0 {
|
||||
ipnStr := ipn.String()
|
||||
for _, a := range addrs {
|
||||
// string comp is actually easiest for doing IPNet comps
|
||||
if a.IPNet.String() == ipnStr {
|
||||
return nil
|
||||
ipnStr := ipn.String()
|
||||
for _, a := range addrs {
|
||||
|
||||
// string comp is actually easiest for doing IPNet comps
|
||||
if a.IPNet.String() == ipnStr {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Multiple IPv6 addresses are allowed on the bridge if the
|
||||
// corresponding subnets do not overlap. For IPv4 or for
|
||||
// overlapping IPv6 subnets, reconfigure the IP address if
|
||||
// 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 {
|
||||
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.Name, ipn.String())
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return fmt.Errorf("could not set bridge's mac: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func deleteBridgeAddr(br *netlink.Bridge, 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 nil
|
||||
}
|
||||
|
||||
@ -97,7 +205,7 @@ func bridgeByName(name string) (*netlink.Bridge, error) {
|
||||
return br, nil
|
||||
}
|
||||
|
||||
func ensureBridge(brName string, mtu int) (*netlink.Bridge, error) {
|
||||
func ensureBridge(brName string, mtu int, promiscMode bool) (*netlink.Bridge, error) {
|
||||
br := &netlink.Bridge{
|
||||
LinkAttrs: netlink.LinkAttrs{
|
||||
Name: brName,
|
||||
@ -110,18 +218,24 @@ func ensureBridge(brName string, mtu int) (*netlink.Bridge, error) {
|
||||
},
|
||||
}
|
||||
|
||||
if err := netlink.LinkAdd(br); err != nil {
|
||||
if err != syscall.EEXIST {
|
||||
return nil, fmt.Errorf("could not add %q: %v", brName, err)
|
||||
}
|
||||
err := netlink.LinkAdd(br)
|
||||
if err != nil && err != syscall.EEXIST {
|
||||
return nil, fmt.Errorf("could not add %q: %v", brName, err)
|
||||
}
|
||||
|
||||
// it's ok if the device already exists as long as config is similar
|
||||
br, err = bridgeByName(brName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if promiscMode {
|
||||
if err := netlink.SetPromiscOn(br); err != nil {
|
||||
return nil, fmt.Errorf("could not set promiscuous mode on %q: %v", brName, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Re-fetch link to read all attributes and if it already existed,
|
||||
// ensure it's really a bridge with similar configuration
|
||||
br, err = bridgeByName(brName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := netlink.LinkSetUp(br); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -129,40 +243,44 @@ func ensureBridge(brName string, mtu int) (*netlink.Bridge, error) {
|
||||
return br, nil
|
||||
}
|
||||
|
||||
func setupVeth(netns ns.NetNS, br *netlink.Bridge, ifName string, mtu int, hairpinMode bool) error {
|
||||
var hostVethName string
|
||||
func setupVeth(netns ns.NetNS, br *netlink.Bridge, ifName string, mtu int, hairpinMode bool) (*current.Interface, *current.Interface, error) {
|
||||
contIface := ¤t.Interface{}
|
||||
hostIface := ¤t.Interface{}
|
||||
|
||||
err := netns.Do(func(hostNS ns.NetNS) error {
|
||||
// create the veth pair in the container and move host end into host netns
|
||||
hostVeth, _, err := ip.SetupVeth(ifName, mtu, hostNS)
|
||||
hostVeth, containerVeth, err := ip.SetupVeth(ifName, mtu, hostNS)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
hostVethName = hostVeth.Attrs().Name
|
||||
contIface.Name = containerVeth.Name
|
||||
contIface.Mac = containerVeth.HardwareAddr.String()
|
||||
contIface.Sandbox = netns.Path()
|
||||
hostIface.Name = hostVeth.Name
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// need to lookup hostVeth again as its index has changed during ns move
|
||||
hostVeth, err := netlink.LinkByName(hostVethName)
|
||||
hostVeth, err := netlink.LinkByName(hostIface.Name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to lookup %q: %v", hostVethName, err)
|
||||
return nil, nil, fmt.Errorf("failed to lookup %q: %v", hostIface.Name, err)
|
||||
}
|
||||
hostIface.Mac = hostVeth.Attrs().HardwareAddr.String()
|
||||
|
||||
// connect host veth end to the bridge
|
||||
if err = netlink.LinkSetMaster(hostVeth, br); err != nil {
|
||||
return fmt.Errorf("failed to connect %q to bridge %v: %v", hostVethName, br.Attrs().Name, err)
|
||||
if err := netlink.LinkSetMaster(hostVeth, br); err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to connect %q to bridge %v: %v", hostVeth.Attrs().Name, br.Attrs().Name, err)
|
||||
}
|
||||
|
||||
// set hairpin mode
|
||||
if err = netlink.LinkSetHairpin(hostVeth, hairpinMode); err != nil {
|
||||
return fmt.Errorf("failed to setup hairpin mode for %v: %v", hostVethName, err)
|
||||
return nil, nil, fmt.Errorf("failed to setup hairpin mode for %v: %v", hostVeth.Attrs().Name, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
return hostIface, contIface, nil
|
||||
}
|
||||
|
||||
func calcGatewayIP(ipn *net.IPNet) net.IP {
|
||||
@ -170,18 +288,42 @@ func calcGatewayIP(ipn *net.IPNet) net.IP {
|
||||
return ip.NextIP(nid)
|
||||
}
|
||||
|
||||
func setupBridge(n *NetConf) (*netlink.Bridge, error) {
|
||||
func setupBridge(n *NetConf) (*netlink.Bridge, *current.Interface, error) {
|
||||
// create bridge if necessary
|
||||
br, err := ensureBridge(n.BrName, n.MTU)
|
||||
br, err := ensureBridge(n.BrName, n.MTU, n.PromiscMode)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create bridge %q: %v", n.BrName, err)
|
||||
return nil, nil, fmt.Errorf("failed to create bridge %q: %v", n.BrName, err)
|
||||
}
|
||||
|
||||
return br, nil
|
||||
return br, ¤t.Interface{
|
||||
Name: br.Attrs().Name,
|
||||
Mac: br.Attrs().HardwareAddr.String(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// disableIPV6DAD disables IPv6 Duplicate Address Detection (DAD)
|
||||
// for an interface, if the interface does not support enhanced_dad.
|
||||
// We do this because interfaces with hairpin mode will see their own DAD packets
|
||||
func disableIPV6DAD(ifName string) error {
|
||||
// ehanced_dad sends a nonce with the DAD packets, so that we can safely
|
||||
// ignore ourselves
|
||||
enh, err := ioutil.ReadFile(fmt.Sprintf("/proc/sys/net/ipv6/conf/%s/enhanced_dad", ifName))
|
||||
if err == nil && string(enh) == "1\n" {
|
||||
return nil
|
||||
}
|
||||
f := fmt.Sprintf("/proc/sys/net/ipv6/conf/%s/accept_dad", ifName)
|
||||
return ioutil.WriteFile(f, []byte("0"), 0644)
|
||||
}
|
||||
|
||||
func enableIPForward(family int) error {
|
||||
if family == netlink.FAMILY_V4 {
|
||||
return ip.EnableIP4Forward()
|
||||
}
|
||||
return ip.EnableIP6Forward()
|
||||
}
|
||||
|
||||
func cmdAdd(args *skel.CmdArgs) error {
|
||||
n, err := loadNetConf(args.StdinData)
|
||||
n, cniVersion, err := loadNetConf(args.StdinData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -190,7 +332,11 @@ func cmdAdd(args *skel.CmdArgs) error {
|
||||
n.IsGW = true
|
||||
}
|
||||
|
||||
br, err := setupBridge(n)
|
||||
if n.HairpinMode && n.PromiscMode {
|
||||
return fmt.Errorf("cannot set hairpin mode and promiscous mode at the same time.")
|
||||
}
|
||||
|
||||
br, brInterface, err := setupBridge(n)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -201,86 +347,118 @@ func cmdAdd(args *skel.CmdArgs) error {
|
||||
}
|
||||
defer netns.Close()
|
||||
|
||||
if err = setupVeth(netns, br, args.IfName, n.MTU, n.HairpinMode); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// run the IPAM plugin and get back the config to apply
|
||||
result, err := ipam.ExecAdd(n.IPAM.Type, args.StdinData)
|
||||
hostInterface, containerInterface, err := setupVeth(netns, br, args.IfName, n.MTU, n.HairpinMode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO: make this optional when IPv6 is supported
|
||||
if result.IP4 == nil {
|
||||
return errors.New("IPAM plugin returned missing IPv4 config")
|
||||
// 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
|
||||
}
|
||||
|
||||
if result.IP4.Gateway == nil && n.IsGW {
|
||||
result.IP4.Gateway = calcGatewayIP(&result.IP4.IP)
|
||||
// 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 {
|
||||
// set the default gateway if requested
|
||||
if n.IsDefaultGW {
|
||||
_, defaultNet, err := net.ParseCIDR("0.0.0.0/0")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, route := range result.IP4.Routes {
|
||||
if defaultNet.String() == route.Dst.String() {
|
||||
if route.GW != nil && !route.GW.Equal(result.IP4.Gateway) {
|
||||
return fmt.Errorf(
|
||||
"isDefaultGateway ineffective because IPAM sets default route via %q",
|
||||
route.GW,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result.IP4.Routes = append(
|
||||
result.IP4.Routes,
|
||||
types.Route{Dst: *defaultNet, GW: result.IP4.Gateway},
|
||||
)
|
||||
|
||||
// TODO: IPV6
|
||||
contVeth, err := net.InterfaceByName(args.IfName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return ipam.ConfigureIface(args.IfName, result)
|
||||
// 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 {
|
||||
gwn := &net.IPNet{
|
||||
IP: result.IP4.Gateway,
|
||||
Mask: result.IP4.IP.Mask,
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
if err = ensureBridgeAddr(br, gwn); err != nil {
|
||||
return err
|
||||
}
|
||||
err = ensureBridgeAddr(br, gws.family, &gw, n.ForceAddress)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set bridge addr: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := ip.EnableIP4Forward(); err != nil {
|
||||
return fmt.Errorf("failed to enable forwarding: %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)
|
||||
if err = ip.SetupIPMasq(ip.Network(&result.IP4.IP), chain, comment); err != nil {
|
||||
return err
|
||||
for _, ipc := range result.IPs {
|
||||
if err = ip.SetupIPMasq(ip.Network(&ipc.Address), chain, comment); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Refetch the bridge since its MAC address may change when the first
|
||||
// veth is added or after its IP address is set
|
||||
br, err = bridgeByName(n.BrName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
brInterface.Mac = br.Attrs().HardwareAddr.String()
|
||||
|
||||
result.DNS = n.DNS
|
||||
return result.Print()
|
||||
|
||||
return types.PrintResult(result, cniVersion)
|
||||
}
|
||||
|
||||
func cmdDel(args *skel.CmdArgs) error {
|
||||
n, err := loadNetConf(args.StdinData)
|
||||
n, _, err := loadNetConf(args.StdinData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -289,12 +467,23 @@ func cmdDel(args *skel.CmdArgs) error {
|
||||
return err
|
||||
}
|
||||
|
||||
var ipn *net.IPNet
|
||||
if args.Netns == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// There is a netns so try to clean up. Delete can be called multiple times
|
||||
// so don't return an error if the device is already removed.
|
||||
// If the device isn't there then don't try to clean up IP masq either.
|
||||
var ipnets []*net.IPNet
|
||||
err = ns.WithNetNSPath(args.Netns, func(_ ns.NetNS) error {
|
||||
var err error
|
||||
ipn, err = ip.DelLinkByNameAddr(args.IfName, netlink.FAMILY_V4)
|
||||
ipnets, err = ip.DelLinkByNameAddr(args.IfName)
|
||||
if err != nil && err == ip.ErrLinkNotFound {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -302,14 +491,16 @@ func cmdDel(args *skel.CmdArgs) error {
|
||||
if n.IPMasq {
|
||||
chain := utils.FormatChainName(n.Name, args.ContainerID)
|
||||
comment := utils.FormatComment(n.Name, args.ContainerID)
|
||||
if err = ip.TeardownIPMasq(ipn, chain, comment); err != nil {
|
||||
return err
|
||||
for _, ipn := range ipnets {
|
||||
if err := ip.TeardownIPMasq(ipn, chain, comment); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
|
||||
func main() {
|
||||
skel.PluginMain(cmdAdd, cmdDel)
|
||||
skel.PluginMain(cmdAdd, cmdDel, version.All)
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
21
plugins/main/host-device/README.md
Normal file
21
plugins/main/host-device/README.md
Normal file
@ -0,0 +1,21 @@
|
||||
# host-device
|
||||
Move an already-existing device in to 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.
|
||||
|
||||
The device can be specified with any one of three properties:
|
||||
* `device`: The device name, e.g. `eth0`, `can0`
|
||||
* `hwaddr`: A MAC address
|
||||
* `kernelpath`: The kernel device kobj, e.g. `/sys/devices/pci0000:00/0000:00:1f.6`
|
||||
|
||||
For this plugin, `CNI_IFNAME` will be ignored. Upon DEL, the device will be moved back.
|
||||
|
||||
A sample configuration might look like:
|
||||
|
||||
```json
|
||||
{
|
||||
"cniVersion": "0.3.1",
|
||||
"device": "enp0s1"
|
||||
}
|
||||
```
|
216
plugins/main/host-device/host-device.go
Normal file
216
plugins/main/host-device/host-device.go
Normal file
@ -0,0 +1,216 @@
|
||||
// Copyright 2015 CNI authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"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/ns"
|
||||
"github.com/vishvananda/netlink"
|
||||
)
|
||||
|
||||
type NetConf struct {
|
||||
types.NetConf
|
||||
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
|
||||
}
|
||||
|
||||
func init() {
|
||||
// this ensures that main runs only on main thread (thread group leader).
|
||||
// since namespace ops (unshare, setns) are done for a single thread, we
|
||||
// must ensure that the goroutine does not jump from OS thread to thread
|
||||
runtime.LockOSThread()
|
||||
}
|
||||
|
||||
func loadConf(bytes []byte) (*NetConf, error) {
|
||||
n := &NetConf{}
|
||||
if err := json.Unmarshal(bytes, n); err != nil {
|
||||
return nil, fmt.Errorf("failed to load netconf: %v", err)
|
||||
}
|
||||
if n.Device == "" && n.HWAddr == "" && n.KernelPath == "" {
|
||||
return nil, fmt.Errorf(`specify either "device", "hwaddr" or "kernelpath"`)
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func cmdAdd(args *skel.CmdArgs) error {
|
||||
cfg, err := loadConf(args.StdinData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
containerNs, err := ns.GetNS(args.Netns)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open netns %q: %v", args.Netns, err)
|
||||
}
|
||||
defer containerNs.Close()
|
||||
|
||||
hostDev, err := getLink(cfg.Device, cfg.HWAddr, cfg.KernelPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find host device: %v", err)
|
||||
}
|
||||
|
||||
contDev, err := moveLinkIn(hostDev, containerNs, args.IfName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to move link %v", err)
|
||||
}
|
||||
return printLink(contDev, cfg.CNIVersion, containerNs)
|
||||
}
|
||||
|
||||
func cmdDel(args *skel.CmdArgs) error {
|
||||
_, err := loadConf(args.StdinData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
containerNs, err := ns.GetNS(args.Netns)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open netns %q: %v", args.Netns, err)
|
||||
}
|
||||
defer containerNs.Close()
|
||||
|
||||
if err := moveLinkOut(containerNs, args.IfName); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func moveLinkIn(hostDev netlink.Link, containerNs ns.NetNS, ifName string) (netlink.Link, error) {
|
||||
if err := netlink.LinkSetNsFd(hostDev, int(containerNs.Fd())); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var contDev netlink.Link
|
||||
if err := containerNs.Do(func(_ ns.NetNS) error {
|
||||
var err error
|
||||
contDev, err = netlink.LinkByName(hostDev.Attrs().Name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find %q: %v", hostDev.Attrs().Name, err)
|
||||
}
|
||||
// Save host device name into the container device's alias property
|
||||
if err := netlink.LinkSetAlias(contDev, hostDev.Attrs().Name); err != nil {
|
||||
return fmt.Errorf("failed to set alias to %q: %v", hostDev.Attrs().Name, err)
|
||||
}
|
||||
// Rename container device to respect args.IfName
|
||||
if err := netlink.LinkSetName(contDev, ifName); err != nil {
|
||||
return fmt.Errorf("failed to rename device %q to %q: %v", hostDev.Attrs().Name, ifName, err)
|
||||
}
|
||||
// Retrieve link again to get up-to-date name and attributes
|
||||
contDev, err = netlink.LinkByName(ifName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find %q: %v", ifName, err)
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return contDev, nil
|
||||
}
|
||||
|
||||
func moveLinkOut(containerNs ns.NetNS, ifName string) error {
|
||||
defaultNs, err := ns.GetCurrentNS()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer defaultNs.Close()
|
||||
|
||||
return containerNs.Do(func(_ ns.NetNS) error {
|
||||
dev, err := netlink.LinkByName(ifName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find %q: %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)
|
||||
}
|
||||
if err := netlink.LinkSetNsFd(dev, int(defaultNs.Fd())); err != nil {
|
||||
return fmt.Errorf("failed to move %q to host netns: %v", dev.Attrs().Alias, err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func printLink(dev netlink.Link, cniVersion string, containerNs ns.NetNS) error {
|
||||
result := current.Result{
|
||||
CNIVersion: current.ImplementedSpecVersion,
|
||||
Interfaces: []*current.Interface{
|
||||
{
|
||||
Name: dev.Attrs().Name,
|
||||
Mac: dev.Attrs().HardwareAddr.String(),
|
||||
Sandbox: containerNs.Path(),
|
||||
},
|
||||
},
|
||||
}
|
||||
return types.PrintResult(&result, cniVersion)
|
||||
}
|
||||
|
||||
func getLink(devname, hwaddr, kernelpath string) (netlink.Link, error) {
|
||||
links, err := netlink.LinkList()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list node links: %v", err)
|
||||
}
|
||||
|
||||
if len(devname) > 0 {
|
||||
return netlink.LinkByName(devname)
|
||||
} else if len(hwaddr) > 0 {
|
||||
hwAddr, err := net.ParseMAC(hwaddr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse MAC address %q: %v", hwaddr, err)
|
||||
}
|
||||
|
||||
for _, link := range links {
|
||||
if bytes.Equal(link.Attrs().HardwareAddr, hwAddr) {
|
||||
return link, nil
|
||||
}
|
||||
}
|
||||
} else if len(kernelpath) > 0 {
|
||||
if !filepath.IsAbs(kernelpath) || !strings.HasPrefix(kernelpath, "/sys/devices/") {
|
||||
return nil, fmt.Errorf("kernel device path %q must be absolute and begin with /sys/devices/", kernelpath)
|
||||
}
|
||||
netDir := filepath.Join(kernelpath, "net")
|
||||
files, err := ioutil.ReadDir(netDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to find network devices at %q", netDir)
|
||||
}
|
||||
|
||||
// Grab the first device from eg /sys/devices/pci0000:00/0000:00:19.0/net
|
||||
for _, file := range files {
|
||||
// Make sure it's really an interface
|
||||
for _, l := range links {
|
||||
if file.Name() == l.Attrs().Name {
|
||||
return l, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("failed to find physical interface")
|
||||
}
|
||||
|
||||
func main() {
|
||||
skel.PluginMain(cmdAdd, cmdDel, version.All)
|
||||
}
|
27
plugins/main/host-device/host-device_suite_test.go
Normal file
27
plugins/main/host-device/host-device_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 TestVlan(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "host-device Suite")
|
||||
}
|
155
plugins/main/host-device/host-device_test.go
Normal file
155
plugins/main/host-device/host-device_test.go
Normal file
@ -0,0 +1,155 @@
|
||||
// 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 main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/skel"
|
||||
"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/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/vishvananda/netlink"
|
||||
)
|
||||
|
||||
var _ = Describe("base functionality", func() {
|
||||
var originalNS ns.NetNS
|
||||
var ifname string
|
||||
|
||||
BeforeEach(func() {
|
||||
var err error
|
||||
originalNS, err = ns.NewNS()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
ifname = fmt.Sprintf("dummy-%x", rand.Int31())
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
originalNS.Close()
|
||||
})
|
||||
|
||||
It("Works with a valid config", 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 := ns.NewNS()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
CNI_IFNAME := "eth0"
|
||||
conf := fmt.Sprintf(`{
|
||||
"cniVersion": "0.3.0",
|
||||
"name": "cni-plugin-host-device-test",
|
||||
"type": "host-device",
|
||||
"device": %q
|
||||
}`, ifname)
|
||||
args := &skel.CmdArgs{
|
||||
ContainerID: "dummy",
|
||||
Netns: targetNS.Path(),
|
||||
IfName: CNI_IFNAME,
|
||||
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) })
|
||||
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: CNI_IFNAME,
|
||||
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(CNI_IFNAME)
|
||||
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())
|
||||
|
||||
// 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) })
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
_, err := netlink.LinkByName(ifname)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
return nil
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
})
|
||||
|
||||
It("fails an invalid config", func() {
|
||||
conf := `{
|
||||
"cniVersion": "0.3.0",
|
||||
"name": "cni-plugin-host-device-test",
|
||||
"type": "host-device"
|
||||
}`
|
||||
|
||||
args := &skel.CmdArgs{
|
||||
ContainerID: "dummy",
|
||||
Netns: originalNS.Path(),
|
||||
IfName: ifname,
|
||||
StdinData: []byte(conf),
|
||||
}
|
||||
_, _, err := testutils.CmdAddWithResult(originalNS.Path(), ifname, []byte(conf), func() error { return cmdAdd(args) })
|
||||
Expect(err).To(MatchError(`specify either "device", "hwaddr" or "kernelpath"`))
|
||||
|
||||
})
|
||||
|
||||
})
|
@ -18,7 +18,7 @@ Because all ipvlan interfaces share the MAC address with the host interface, DHC
|
||||
"master": "eth0",
|
||||
"ipam": {
|
||||
"type": "host-local",
|
||||
"subnet": "10.1.2.0/24",
|
||||
"subnet": "10.1.2.0/24"
|
||||
}
|
||||
}
|
||||
```
|
||||
@ -27,10 +27,10 @@ Because all ipvlan interfaces share the MAC address with the host interface, DHC
|
||||
|
||||
* `name` (string, required): the name of the network.
|
||||
* `type` (string, required): "ipvlan".
|
||||
* `master` (string, required): name of the host interface to enslave.
|
||||
* `mode` (string, optional): one of "l2", "l3". Defaults to "l2".
|
||||
* `master` (string, required unless chained): name of the host interface to enslave.
|
||||
* `mode` (string, optional): one of "l2", "l3", "l3s". Defaults to "l2".
|
||||
* `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 unless chained): IPAM configuration to be used for this network.
|
||||
|
||||
## Notes
|
||||
|
||||
@ -38,3 +38,8 @@ Because all ipvlan interfaces share the MAC address with the host interface, DHC
|
||||
Therefore the container will not be able to reach the host via `ipvlan` interface.
|
||||
Be sure to also have container join a network that provides connectivity to the host (e.g. `ptp`).
|
||||
* A single master interface can not be enslaved by both `macvlan` and `ipvlan`.
|
||||
* For IP allocation schemes that cannot be interface agnostic, the ipvlan plugin
|
||||
can be chained with an earlier plugin that handles this logic. If `master` is
|
||||
omitted, then the previous Result must contain a single interface name for the
|
||||
ipvlan plugin to enslave. If `ipam` is omitted, then the previous Result is used
|
||||
to configure the ipvlan interface.
|
@ -20,16 +20,24 @@ import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/ip"
|
||||
"github.com/containernetworking/cni/pkg/ipam"
|
||||
"github.com/containernetworking/cni/pkg/ns"
|
||||
"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"
|
||||
)
|
||||
|
||||
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"`
|
||||
@ -42,15 +50,38 @@ func init() {
|
||||
runtime.LockOSThread()
|
||||
}
|
||||
|
||||
func loadConf(bytes []byte) (*NetConf, error) {
|
||||
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)
|
||||
return nil, "", fmt.Errorf("failed to load netconf: %v", err)
|
||||
}
|
||||
// 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 {
|
||||
return nil, "", fmt.Errorf("could not parse prevResult: %v", err)
|
||||
}
|
||||
n.RawPrevResult = nil
|
||||
n.PrevResult, err = current.NewResultFromResult(res)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("could not convert result to current version: %v", err)
|
||||
}
|
||||
}
|
||||
if n.Master == "" {
|
||||
return nil, fmt.Errorf(`"master" field is required. It specifies the host interface name to virtualize`)
|
||||
if n.PrevResult == 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
|
||||
} else {
|
||||
return nil, "", fmt.Errorf("chained master failure. PrevResult lacks a single named interface")
|
||||
}
|
||||
}
|
||||
return n, nil
|
||||
return n, n.CNIVersion, nil
|
||||
}
|
||||
|
||||
func modeFromString(s string) (netlink.IPVlanMode, error) {
|
||||
@ -59,27 +90,31 @@ func modeFromString(s string) (netlink.IPVlanMode, error) {
|
||||
return netlink.IPVLAN_MODE_L2, nil
|
||||
case "l3":
|
||||
return netlink.IPVLAN_MODE_L3, nil
|
||||
case "l3s":
|
||||
return netlink.IPVLAN_MODE_L3S, nil
|
||||
default:
|
||||
return 0, fmt.Errorf("unknown ipvlan mode: %q", s)
|
||||
}
|
||||
}
|
||||
|
||||
func createIpvlan(conf *NetConf, ifName string, netns ns.NetNS) error {
|
||||
func createIpvlan(conf *NetConf, ifName string, netns ns.NetNS) (*current.Interface, error) {
|
||||
ipvlan := ¤t.Interface{}
|
||||
|
||||
mode, err := modeFromString(conf.Mode)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m, err := netlink.LinkByName(conf.Master)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to lookup master %q: %v", conf.Master, err)
|
||||
return nil, fmt.Errorf("failed to lookup master %q: %v", conf.Master, err)
|
||||
}
|
||||
|
||||
// due to kernel bug we have to create with tmpname or it might
|
||||
// collide with the name on the host and error out
|
||||
tmpName, err := ip.RandomVethName()
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mv := &netlink.IPVlan{
|
||||
@ -93,20 +128,35 @@ func createIpvlan(conf *NetConf, ifName string, netns ns.NetNS) error {
|
||||
}
|
||||
|
||||
if err := netlink.LinkAdd(mv); err != nil {
|
||||
return fmt.Errorf("failed to create ipvlan: %v", err)
|
||||
return nil, fmt.Errorf("failed to create ipvlan: %v", err)
|
||||
}
|
||||
|
||||
return netns.Do(func(_ ns.NetNS) error {
|
||||
err := renameLink(tmpName, ifName)
|
||||
err = netns.Do(func(_ ns.NetNS) error {
|
||||
err := ip.RenameLink(tmpName, ifName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to rename ipvlan to %q: %v", ifName, err)
|
||||
}
|
||||
ipvlan.Name = ifName
|
||||
|
||||
// Re-fetch ipvlan to get all properties/attributes
|
||||
contIpvlan, err := netlink.LinkByName(ipvlan.Name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to refetch ipvlan %q: %v", ipvlan.Name, err)
|
||||
}
|
||||
ipvlan.Mac = contIpvlan.Attrs().HardwareAddr.String()
|
||||
ipvlan.Sandbox = netns.Path()
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ipvlan, nil
|
||||
}
|
||||
|
||||
func cmdAdd(args *skel.CmdArgs) error {
|
||||
n, err := loadConf(args.StdinData)
|
||||
n, cniVersion, err := loadConf(args.StdinData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -117,18 +167,38 @@ func cmdAdd(args *skel.CmdArgs) error {
|
||||
}
|
||||
defer netns.Close()
|
||||
|
||||
if err = createIpvlan(n, args.IfName, netns); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// run the IPAM plugin and get back the config to apply
|
||||
result, err := ipam.ExecAdd(n.IPAM.Type, args.StdinData)
|
||||
ipvlanInterface, err := createIpvlan(n, args.IfName, netns)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if result.IP4 == nil {
|
||||
return errors.New("IPAM plugin returned missing IPv4 config")
|
||||
|
||||
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 {
|
||||
// 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
|
||||
}
|
||||
// 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")
|
||||
}
|
||||
}
|
||||
for _, ipc := range result.IPs {
|
||||
// All addresses belong to the ipvlan interface
|
||||
ipc.Interface = current.Int(0)
|
||||
}
|
||||
|
||||
result.Interfaces = []*current.Interface{ipvlanInterface}
|
||||
|
||||
err = netns.Do(func(_ ns.NetNS) error {
|
||||
return ipam.ConfigureIface(args.IfName, result)
|
||||
@ -138,34 +208,42 @@ func cmdAdd(args *skel.CmdArgs) error {
|
||||
}
|
||||
|
||||
result.DNS = n.DNS
|
||||
return result.Print()
|
||||
|
||||
return types.PrintResult(result, cniVersion)
|
||||
}
|
||||
|
||||
func cmdDel(args *skel.CmdArgs) error {
|
||||
n, err := loadConf(args.StdinData)
|
||||
n, _, err := loadConf(args.StdinData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = ipam.ExecDel(n.IPAM.Type, args.StdinData)
|
||||
if err != nil {
|
||||
return err
|
||||
// On chained invocation, IPAM block can be empty
|
||||
if n.IPAM.Type != "" {
|
||||
err = ipam.ExecDel(n.IPAM.Type, args.StdinData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return ns.WithNetNSPath(args.Netns, func(_ ns.NetNS) error {
|
||||
return ip.DelLinkByName(args.IfName)
|
||||
if args.Netns == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// There is a netns so try to clean up. Delete can be called multiple times
|
||||
// so don't return an error if the device is already removed.
|
||||
err = ns.WithNetNSPath(args.Netns, func(_ ns.NetNS) error {
|
||||
if err := ip.DelLinkByName(args.IfName); err != nil {
|
||||
if err != ip.ErrLinkNotFound {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func renameLink(curName, newName string) error {
|
||||
link, err := netlink.LinkByName(curName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return netlink.LinkSetName(link, newName)
|
||||
return err
|
||||
}
|
||||
|
||||
func main() {
|
||||
skel.PluginMain(cmdAdd, cmdDel)
|
||||
skel.PluginMain(cmdAdd, cmdDel, version.All)
|
||||
}
|
||||
|
@ -16,11 +16,14 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"syscall"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/ns"
|
||||
"github.com/containernetworking/cni/pkg/skel"
|
||||
"github.com/containernetworking/cni/pkg/testutils"
|
||||
"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"
|
||||
|
||||
@ -30,6 +33,79 @@ import (
|
||||
|
||||
const MASTER_NAME = "eth0"
|
||||
|
||||
func ipvlanAddDelTest(conf, IFNAME string, originalNS ns.NetNS) {
|
||||
targetNs, err := ns.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.CmdAddWithResult(targetNs.Path(), IFNAME, []byte(conf), 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())
|
||||
|
||||
err = originalNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
err = testutils.CmdDelWithResult(targetNs.Path(), IFNAME, 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())
|
||||
}
|
||||
|
||||
var _ = Describe("ipvlan Operations", func() {
|
||||
var originalNS ns.NetNS
|
||||
|
||||
@ -63,8 +139,9 @@ var _ = Describe("ipvlan Operations", func() {
|
||||
It("creates an ipvlan link in a non-default namespace", func() {
|
||||
conf := &NetConf{
|
||||
NetConf: types.NetConf{
|
||||
Name: "testConfig",
|
||||
Type: "ipvlan",
|
||||
CNIVersion: "0.3.1",
|
||||
Name: "testConfig",
|
||||
Type: "ipvlan",
|
||||
},
|
||||
Master: MASTER_NAME,
|
||||
Mode: "l2",
|
||||
@ -79,10 +156,11 @@ var _ = Describe("ipvlan Operations", func() {
|
||||
err = originalNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
err := createIpvlan(conf, "foobar0", targetNs)
|
||||
_, err := createIpvlan(conf, "foobar0", targetNs)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
return nil
|
||||
})
|
||||
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Make sure ipvlan link exists in the target namespace
|
||||
@ -101,6 +179,52 @@ var _ = Describe("ipvlan Operations", func() {
|
||||
const IFNAME = "ipvl0"
|
||||
|
||||
conf := fmt.Sprintf(`{
|
||||
"cniVersion": "0.3.1",
|
||||
"name": "mynet",
|
||||
"type": "ipvlan",
|
||||
"master": "%s",
|
||||
"ipam": {
|
||||
"type": "host-local",
|
||||
"subnet": "10.1.2.0/24"
|
||||
}
|
||||
}`, MASTER_NAME)
|
||||
|
||||
ipvlanAddDelTest(conf, IFNAME, originalNS)
|
||||
})
|
||||
|
||||
It("configures and deconfigures an iplvan link with ADD/DEL when chained", func() {
|
||||
const IFNAME = "ipvl0"
|
||||
|
||||
conf := fmt.Sprintf(`{
|
||||
"cniVersion": "0.3.1",
|
||||
"name": "mynet",
|
||||
"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)
|
||||
|
||||
ipvlanAddDelTest(conf, IFNAME, originalNS)
|
||||
})
|
||||
|
||||
It("deconfigures an unconfigured ipvlan link with DEL", func() {
|
||||
const IFNAME = "ipvl0"
|
||||
|
||||
conf := fmt.Sprintf(`{
|
||||
"cniVersion": "0.3.0",
|
||||
"name": "mynet",
|
||||
"type": "ipvlan",
|
||||
"master": "%s",
|
||||
@ -121,28 +245,6 @@ var _ = Describe("ipvlan Operations", func() {
|
||||
StdinData: []byte(conf),
|
||||
}
|
||||
|
||||
err = originalNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
_, err := testutils.CmdAddWithResult(targetNs.Path(), IFNAME, func() error {
|
||||
return cmdAdd(args)
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
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))
|
||||
return nil
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
err = originalNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
@ -153,16 +255,5 @@ var _ = Describe("ipvlan Operations", func() {
|
||||
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())
|
||||
})
|
||||
})
|
||||
|
@ -15,9 +15,10 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/containernetworking/cni/pkg/ns"
|
||||
"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/ns"
|
||||
"github.com/vishvananda/netlink"
|
||||
)
|
||||
|
||||
@ -40,7 +41,7 @@ func cmdAdd(args *skel.CmdArgs) error {
|
||||
return err // not tested
|
||||
}
|
||||
|
||||
result := types.Result{}
|
||||
result := current.Result{}
|
||||
return result.Print()
|
||||
}
|
||||
|
||||
@ -67,5 +68,5 @@ func cmdDel(args *skel.CmdArgs) error {
|
||||
}
|
||||
|
||||
func main() {
|
||||
skel.PluginMain(cmdAdd, cmdDel)
|
||||
skel.PluginMain(cmdAdd, cmdDel, version.All)
|
||||
}
|
||||
|
@ -32,7 +32,7 @@ func TestLoopback(t *testing.T) {
|
||||
|
||||
var _ = BeforeSuite(func() {
|
||||
var err error
|
||||
pathToLoPlugin, err = gexec.Build("github.com/containernetworking/cni/plugins/main/loopback")
|
||||
pathToLoPlugin, err = gexec.Build("github.com/containernetworking/plugins/plugins/main/loopback")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
|
||||
|
@ -20,7 +20,7 @@ import (
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/ns"
|
||||
"github.com/containernetworking/plugins/pkg/ns"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/onsi/gomega/gbytes"
|
||||
@ -49,7 +49,7 @@ var _ = Describe("Loopback", func() {
|
||||
fmt.Sprintf("CNI_ARGS=%s", "none"),
|
||||
fmt.Sprintf("CNI_PATH=%s", "/some/test/path"),
|
||||
}
|
||||
command.Stdin = strings.NewReader("this doesn't matter")
|
||||
command.Stdin = strings.NewReader(`{ "cniVersion": "0.1.0" }`)
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
|
@ -18,14 +18,18 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"runtime"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/ip"
|
||||
"github.com/containernetworking/cni/pkg/ipam"
|
||||
"github.com/containernetworking/cni/pkg/ns"
|
||||
"github.com/containernetworking/cni/pkg/skel"
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
"github.com/containernetworking/cni/pkg/utils/sysctl"
|
||||
"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/sysctl"
|
||||
"github.com/j-keck/arping"
|
||||
"github.com/vishvananda/netlink"
|
||||
)
|
||||
|
||||
@ -47,15 +51,15 @@ func init() {
|
||||
runtime.LockOSThread()
|
||||
}
|
||||
|
||||
func loadConf(bytes []byte) (*NetConf, error) {
|
||||
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)
|
||||
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`)
|
||||
return nil, "", fmt.Errorf(`"master" field is required. It specifies the host interface name to virtualize`)
|
||||
}
|
||||
return n, nil
|
||||
return n, n.CNIVersion, nil
|
||||
}
|
||||
|
||||
func modeFromString(s string) (netlink.MacvlanMode, error) {
|
||||
@ -73,22 +77,24 @@ func modeFromString(s string) (netlink.MacvlanMode, error) {
|
||||
}
|
||||
}
|
||||
|
||||
func createMacvlan(conf *NetConf, ifName string, netns ns.NetNS) error {
|
||||
func createMacvlan(conf *NetConf, ifName string, netns ns.NetNS) (*current.Interface, error) {
|
||||
macvlan := ¤t.Interface{}
|
||||
|
||||
mode, err := modeFromString(conf.Mode)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m, err := netlink.LinkByName(conf.Master)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to lookup master %q: %v", conf.Master, err)
|
||||
return nil, fmt.Errorf("failed to lookup master %q: %v", conf.Master, err)
|
||||
}
|
||||
|
||||
// due to kernel bug we have to create with tmpName or it might
|
||||
// collide with the name on the host and error out
|
||||
tmpName, err := ip.RandomVethName()
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mv := &netlink.Macvlan{
|
||||
@ -102,10 +108,10 @@ func createMacvlan(conf *NetConf, ifName string, netns ns.NetNS) error {
|
||||
}
|
||||
|
||||
if err := netlink.LinkAdd(mv); err != nil {
|
||||
return fmt.Errorf("failed to create macvlan: %v", err)
|
||||
return nil, fmt.Errorf("failed to create macvlan: %v", err)
|
||||
}
|
||||
|
||||
return netns.Do(func(_ ns.NetNS) error {
|
||||
err = netns.Do(func(_ ns.NetNS) error {
|
||||
// TODO: duplicate following lines for ipv6 support, when it will be added in other places
|
||||
ipv4SysctlValueName := fmt.Sprintf(IPv4InterfaceArpProxySysctlTemplate, tmpName)
|
||||
if _, err := sysctl.Sysctl(ipv4SysctlValueName, "1"); err != nil {
|
||||
@ -114,17 +120,32 @@ func createMacvlan(conf *NetConf, ifName string, netns ns.NetNS) error {
|
||||
return fmt.Errorf("failed to set proxy_arp on newly added interface %q: %v", tmpName, err)
|
||||
}
|
||||
|
||||
err := renameLink(tmpName, ifName)
|
||||
err := ip.RenameLink(tmpName, ifName)
|
||||
if err != nil {
|
||||
_ = netlink.LinkDel(mv)
|
||||
return fmt.Errorf("failed to rename macvlan to %q: %v", ifName, err)
|
||||
}
|
||||
macvlan.Name = ifName
|
||||
|
||||
// Re-fetch macvlan to get all properties/attributes
|
||||
contMacvlan, err := netlink.LinkByName(ifName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to refetch macvlan %q: %v", ifName, err)
|
||||
}
|
||||
macvlan.Mac = contMacvlan.Attrs().HardwareAddr.String()
|
||||
macvlan.Sandbox = netns.Path()
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return macvlan, nil
|
||||
}
|
||||
|
||||
func cmdAdd(args *skel.CmdArgs) error {
|
||||
n, err := loadConf(args.StdinData)
|
||||
n, cniVersion, err := loadConf(args.StdinData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -135,32 +156,77 @@ func cmdAdd(args *skel.CmdArgs) error {
|
||||
}
|
||||
defer netns.Close()
|
||||
|
||||
if err = createMacvlan(n, args.IfName, netns); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// run the IPAM plugin and get back the config to apply
|
||||
result, err := ipam.ExecAdd(n.IPAM.Type, args.StdinData)
|
||||
macvlanInterface, err := createMacvlan(n, args.IfName, netns)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if result.IP4 == nil {
|
||||
return errors.New("IPAM plugin returned missing IPv4 config")
|
||||
|
||||
// Delete link if err to avoid link leak in this ns
|
||||
defer func() {
|
||||
if err != nil {
|
||||
netns.Do(func(_ ns.NetNS) error {
|
||||
return ip.DelLinkByName(args.IfName)
|
||||
})
|
||||
}
|
||||
}()
|
||||
|
||||
// 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 {
|
||||
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 {
|
||||
return ipam.ConfigureIface(args.IfName, result)
|
||||
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
|
||||
}
|
||||
|
||||
result.DNS = n.DNS
|
||||
return result.Print()
|
||||
|
||||
return types.PrintResult(result, cniVersion)
|
||||
}
|
||||
|
||||
func cmdDel(args *skel.CmdArgs) error {
|
||||
n, err := loadConf(args.StdinData)
|
||||
n, _, err := loadConf(args.StdinData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -170,20 +236,24 @@ func cmdDel(args *skel.CmdArgs) error {
|
||||
return err
|
||||
}
|
||||
|
||||
return ns.WithNetNSPath(args.Netns, func(_ ns.NetNS) error {
|
||||
return ip.DelLinkByName(args.IfName)
|
||||
})
|
||||
}
|
||||
|
||||
func renameLink(curName, newName string) error {
|
||||
link, err := netlink.LinkByName(curName)
|
||||
if err != nil {
|
||||
return err
|
||||
if args.Netns == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
return netlink.LinkSetName(link, newName)
|
||||
// There is a netns so try to clean up. Delete can be called multiple times
|
||||
// so don't return an error if the device is already removed.
|
||||
err = ns.WithNetNSPath(args.Netns, func(_ ns.NetNS) error {
|
||||
if err := ip.DelLinkByName(args.IfName); err != nil {
|
||||
if err != ip.ErrLinkNotFound {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func main() {
|
||||
skel.PluginMain(cmdAdd, cmdDel)
|
||||
skel.PluginMain(cmdAdd, cmdDel, version.All)
|
||||
}
|
||||
|
@ -16,11 +16,14 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"syscall"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/ns"
|
||||
"github.com/containernetworking/cni/pkg/skel"
|
||||
"github.com/containernetworking/cni/pkg/testutils"
|
||||
"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"
|
||||
|
||||
@ -63,8 +66,9 @@ var _ = Describe("macvlan Operations", func() {
|
||||
It("creates an macvlan link in a non-default namespace", func() {
|
||||
conf := &NetConf{
|
||||
NetConf: types.NetConf{
|
||||
Name: "testConfig",
|
||||
Type: "macvlan",
|
||||
CNIVersion: "0.3.1",
|
||||
Name: "testConfig",
|
||||
Type: "macvlan",
|
||||
},
|
||||
Master: MASTER_NAME,
|
||||
Mode: "bridge",
|
||||
@ -78,7 +82,7 @@ var _ = Describe("macvlan Operations", func() {
|
||||
err = originalNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
err = createMacvlan(conf, "foobar0", targetNs)
|
||||
_, err = createMacvlan(conf, "foobar0", targetNs)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
return nil
|
||||
})
|
||||
@ -100,6 +104,7 @@ var _ = Describe("macvlan Operations", func() {
|
||||
const IFNAME = "macvl0"
|
||||
|
||||
conf := fmt.Sprintf(`{
|
||||
"cniVersion": "0.3.1",
|
||||
"name": "mynet",
|
||||
"type": "macvlan",
|
||||
"master": "%s",
|
||||
@ -120,14 +125,21 @@ var _ = Describe("macvlan Operations", func() {
|
||||
StdinData: []byte(conf),
|
||||
}
|
||||
|
||||
// Make sure macvlan link exists in the target namespace
|
||||
var result *current.Result
|
||||
err = originalNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
_, err := testutils.CmdAddWithResult(targetNs.Path(), IFNAME, func() error {
|
||||
r, _, err := testutils.CmdAddWithResult(targetNs.Path(), IFNAME, []byte(conf), 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())
|
||||
@ -139,6 +151,14 @@ var _ = Describe("macvlan Operations", func() {
|
||||
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())
|
||||
@ -165,4 +185,42 @@ var _ = Describe("macvlan Operations", func() {
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
|
||||
It("deconfigures an unconfigured macvlan link with DEL", func() {
|
||||
const IFNAME = "macvl0"
|
||||
|
||||
conf := fmt.Sprintf(`{
|
||||
"cniVersion": "0.3.0",
|
||||
"name": "mynet",
|
||||
"type": "macvlan",
|
||||
"master": "%s",
|
||||
"ipam": {
|
||||
"type": "host-local",
|
||||
"subnet": "10.1.2.0/24"
|
||||
}
|
||||
}`, MASTER_NAME)
|
||||
|
||||
targetNs, err := ns.NewNS()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
defer targetNs.Close()
|
||||
|
||||
args := &skel.CmdArgs{
|
||||
ContainerID: "dummy",
|
||||
Netns: targetNs.Path(),
|
||||
IfName: IFNAME,
|
||||
StdinData: []byte(conf),
|
||||
}
|
||||
|
||||
err = originalNS.Do(func(ns.NetNS) error {
|
||||
defer GinkgoRecover()
|
||||
|
||||
err := testutils.CmdDelWithResult(targetNs.Path(), IFNAME, func() error {
|
||||
return cmdDel(args)
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
return nil
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
})
|
||||
})
|
||||
|
@ -7,6 +7,7 @@ The host-local IPAM plugin can be used to allocate an IP address to the containe
|
||||
The traffic of the container interface will be routed through the interface of the host.
|
||||
|
||||
## Example network configuration
|
||||
|
||||
```
|
||||
{
|
||||
"name": "mynet",
|
||||
@ -19,6 +20,7 @@ The traffic of the container interface will be routed through the interface of t
|
||||
"nameservers": [ "10.1.1.1", "8.8.8.8" ]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Network configuration reference
|
||||
|
||||
@ -27,4 +29,4 @@ The traffic of the container interface will be routed through the interface of t
|
||||
* `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 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](/SPEC.md#result).
|
||||
* `dns` (dictionary, optional): DNS information to return as described in the [Result](https://github.com/containernetworking/cni/blob/master/SPEC.md#result).
|
@ -22,14 +22,16 @@ import (
|
||||
"os"
|
||||
"runtime"
|
||||
|
||||
"github.com/vishvananda/netlink"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/ip"
|
||||
"github.com/containernetworking/cni/pkg/ipam"
|
||||
"github.com/containernetworking/cni/pkg/ns"
|
||||
"github.com/containernetworking/cni/pkg/skel"
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
"github.com/containernetworking/cni/pkg/utils"
|
||||
"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"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@ -45,7 +47,7 @@ type NetConf struct {
|
||||
MTU int `json:"mtu"`
|
||||
}
|
||||
|
||||
func setupContainerVeth(netns, ifName string, mtu int, pr *types.Result) (string, error) {
|
||||
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.
|
||||
// Next best thing would be to let it ARP but set interface to 192.168.3.5/32 and
|
||||
@ -57,93 +59,128 @@ func setupContainerVeth(netns, ifName string, mtu int, pr *types.Result) (string
|
||||
// "192.168.3.1/32 dev $ifName" and "192.168.3.0/24 via 192.168.3.1 dev $ifName".
|
||||
// In other words we force all traffic to ARP via the gateway except for GW itself.
|
||||
|
||||
var hostVethName string
|
||||
err := ns.WithNetNSPath(netns, func(hostNS ns.NetNS) error {
|
||||
hostVeth, _, err := ip.SetupVeth(ifName, mtu, hostNS)
|
||||
hostInterface := ¤t.Interface{}
|
||||
containerInterface := ¤t.Interface{}
|
||||
|
||||
err := netns.Do(func(hostNS ns.NetNS) error {
|
||||
hostVeth, contVeth0, err := ip.SetupVeth(ifName, mtu, hostNS)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hostInterface.Name = hostVeth.Name
|
||||
hostInterface.Mac = hostVeth.HardwareAddr.String()
|
||||
containerInterface.Name = contVeth0.Name
|
||||
containerInterface.Mac = contVeth0.HardwareAddr.String()
|
||||
containerInterface.Sandbox = netns.Path()
|
||||
|
||||
for _, ipc := range pr.IPs {
|
||||
// All addresses apply to the container veth interface
|
||||
ipc.Interface = current.Int(1)
|
||||
}
|
||||
|
||||
pr.Interfaces = []*current.Interface{hostInterface, containerInterface}
|
||||
|
||||
if err = ipam.ConfigureIface(ifName, pr); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
contVeth, err := netlink.LinkByName(ifName)
|
||||
contVeth, err := net.InterfaceByName(ifName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to look up %q: %v", ifName, err)
|
||||
}
|
||||
|
||||
// Delete the route that was automatically added
|
||||
route := netlink.Route{
|
||||
LinkIndex: contVeth.Attrs().Index,
|
||||
Dst: &net.IPNet{
|
||||
IP: pr.IP4.IP.IP.Mask(pr.IP4.IP.Mask),
|
||||
Mask: pr.IP4.IP.Mask,
|
||||
},
|
||||
Scope: netlink.SCOPE_NOWHERE,
|
||||
}
|
||||
|
||||
if err := netlink.RouteDel(&route); err != nil {
|
||||
return fmt.Errorf("failed to delete route %v: %v", route, err)
|
||||
}
|
||||
|
||||
for _, r := range []netlink.Route{
|
||||
netlink.Route{
|
||||
LinkIndex: contVeth.Attrs().Index,
|
||||
for _, ipc := range pr.IPs {
|
||||
// Delete the route that was automatically added
|
||||
route := netlink.Route{
|
||||
LinkIndex: contVeth.Index,
|
||||
Dst: &net.IPNet{
|
||||
IP: pr.IP4.Gateway,
|
||||
Mask: net.CIDRMask(32, 32),
|
||||
IP: ipc.Address.IP.Mask(ipc.Address.Mask),
|
||||
Mask: ipc.Address.Mask,
|
||||
},
|
||||
Scope: netlink.SCOPE_LINK,
|
||||
Src: pr.IP4.IP.IP,
|
||||
},
|
||||
netlink.Route{
|
||||
LinkIndex: contVeth.Attrs().Index,
|
||||
Dst: &net.IPNet{
|
||||
IP: pr.IP4.IP.IP.Mask(pr.IP4.IP.Mask),
|
||||
Mask: pr.IP4.IP.Mask,
|
||||
Scope: netlink.SCOPE_NOWHERE,
|
||||
}
|
||||
|
||||
if err := netlink.RouteDel(&route); err != nil {
|
||||
return fmt.Errorf("failed to delete route %v: %v", route, err)
|
||||
}
|
||||
|
||||
addrBits := 32
|
||||
if ipc.Version == "6" {
|
||||
addrBits = 128
|
||||
}
|
||||
|
||||
for _, r := range []netlink.Route{
|
||||
netlink.Route{
|
||||
LinkIndex: contVeth.Index,
|
||||
Dst: &net.IPNet{
|
||||
IP: ipc.Gateway,
|
||||
Mask: net.CIDRMask(addrBits, addrBits),
|
||||
},
|
||||
Scope: netlink.SCOPE_LINK,
|
||||
Src: ipc.Address.IP,
|
||||
},
|
||||
Scope: netlink.SCOPE_UNIVERSE,
|
||||
Gw: pr.IP4.Gateway,
|
||||
Src: pr.IP4.IP.IP,
|
||||
},
|
||||
} {
|
||||
if err := netlink.RouteAdd(&r); err != nil {
|
||||
return fmt.Errorf("failed to add route %v: %v", r, err)
|
||||
netlink.Route{
|
||||
LinkIndex: contVeth.Index,
|
||||
Dst: &net.IPNet{
|
||||
IP: ipc.Address.IP.Mask(ipc.Address.Mask),
|
||||
Mask: ipc.Address.Mask,
|
||||
},
|
||||
Scope: netlink.SCOPE_UNIVERSE,
|
||||
Gw: ipc.Gateway,
|
||||
Src: ipc.Address.IP,
|
||||
},
|
||||
} {
|
||||
if err := netlink.RouteAdd(&r); err != nil {
|
||||
return fmt.Errorf("failed to add route %v: %v", r, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hostVethName = hostVeth.Attrs().Name
|
||||
// Send a gratuitous arp for all v4 addresses
|
||||
for _, ipc := range pr.IPs {
|
||||
if ipc.Version == "4" {
|
||||
_ = arping.GratuitousArpOverIface(ipc.Address.IP, *contVeth)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
return hostVethName, err
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return hostInterface, containerInterface, nil
|
||||
}
|
||||
|
||||
func setupHostVeth(vethName string, ipConf *types.IPConfig) error {
|
||||
func setupHostVeth(vethName string, result *current.Result) error {
|
||||
// hostVeth moved namespaces and may have a new ifindex
|
||||
veth, err := netlink.LinkByName(vethName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to lookup %q: %v", vethName, err)
|
||||
}
|
||||
|
||||
// TODO(eyakubovich): IPv6
|
||||
ipn := &net.IPNet{
|
||||
IP: ipConf.Gateway,
|
||||
Mask: net.CIDRMask(32, 32),
|
||||
}
|
||||
addr := &netlink.Addr{IPNet: ipn, Label: ""}
|
||||
if err = netlink.AddrAdd(veth, addr); err != nil {
|
||||
return fmt.Errorf("failed to add IP addr (%#v) to veth: %v", ipn, err)
|
||||
}
|
||||
for _, ipc := range result.IPs {
|
||||
maskLen := 128
|
||||
if ipc.Address.IP.To4() != nil {
|
||||
maskLen = 32
|
||||
}
|
||||
|
||||
ipn = &net.IPNet{
|
||||
IP: ipConf.IP.IP,
|
||||
Mask: net.CIDRMask(32, 32),
|
||||
}
|
||||
// dst happens to be the same as IP/net of host veth
|
||||
if err = ip.AddHostRoute(ipn, nil, veth); err != nil && !os.IsExist(err) {
|
||||
return fmt.Errorf("failed to add route on host: %v", err)
|
||||
ipn := &net.IPNet{
|
||||
IP: ipc.Gateway,
|
||||
Mask: net.CIDRMask(maskLen, maskLen),
|
||||
}
|
||||
addr := &netlink.Addr{IPNet: ipn, Label: ""}
|
||||
if err = netlink.AddrAdd(veth, addr); err != nil {
|
||||
return fmt.Errorf("failed to add IP addr (%#v) to veth: %v", ipn, err)
|
||||
}
|
||||
|
||||
ipn = &net.IPNet{
|
||||
IP: ipc.Address.IP,
|
||||
Mask: net.CIDRMask(maskLen, maskLen),
|
||||
}
|
||||
// dst happens to be the same as IP/net of host veth
|
||||
if err = ip.AddHostRoute(ipn, nil, veth); err != nil && !os.IsExist(err) {
|
||||
return fmt.Errorf("failed to add route on host: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
@ -155,38 +192,54 @@ func cmdAdd(args *skel.CmdArgs) error {
|
||||
return fmt.Errorf("failed to load netconf: %v", err)
|
||||
}
|
||||
|
||||
if err := ip.EnableIP4Forward(); err != nil {
|
||||
return fmt.Errorf("failed to enable forwarding: %v", err)
|
||||
}
|
||||
|
||||
// run the IPAM plugin and get back the config to apply
|
||||
result, err := ipam.ExecAdd(conf.IPAM.Type, args.StdinData)
|
||||
r, err := ipam.ExecAdd(conf.IPAM.Type, args.StdinData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if result.IP4 == nil {
|
||||
return errors.New("IPAM plugin returned missing IPv4 config")
|
||||
}
|
||||
|
||||
hostVethName, err := setupContainerVeth(args.Netns, args.IfName, conf.MTU, result)
|
||||
// Convert whatever the IPAM result was into the current Result type
|
||||
result, err := current.NewResultFromResult(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = setupHostVeth(hostVethName, result.IP4); err != nil {
|
||||
if len(result.IPs) == 0 {
|
||||
return errors.New("IPAM plugin returned missing IP config")
|
||||
}
|
||||
|
||||
if err := ip.EnableForward(result.IPs); err != nil {
|
||||
return fmt.Errorf("Could not enable IP forwarding: %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()
|
||||
|
||||
hostInterface, containerInterface, err := setupContainerVeth(netns, args.IfName, conf.MTU, result)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = setupHostVeth(hostInterface.Name, result); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if conf.IPMasq {
|
||||
chain := utils.FormatChainName(conf.Name, args.ContainerID)
|
||||
comment := utils.FormatComment(conf.Name, args.ContainerID)
|
||||
if err = ip.SetupIPMasq(&result.IP4.IP, chain, comment); err != nil {
|
||||
return err
|
||||
for _, ipc := range result.IPs {
|
||||
if err = ip.SetupIPMasq(&ipc.Address, chain, comment); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result.DNS = conf.DNS
|
||||
return result.Print()
|
||||
result.Interfaces = []*current.Interface{hostInterface, containerInterface}
|
||||
|
||||
return types.PrintResult(result, conf.CNIVersion)
|
||||
}
|
||||
|
||||
func cmdDel(args *skel.CmdArgs) error {
|
||||
@ -199,27 +252,38 @@ func cmdDel(args *skel.CmdArgs) error {
|
||||
return err
|
||||
}
|
||||
|
||||
var ipn *net.IPNet
|
||||
if args.Netns == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// There is a netns so try to clean up. Delete can be called multiple times
|
||||
// so don't return an error if the device is already removed.
|
||||
// If the device isn't there then don't try to clean up IP masq either.
|
||||
var ipnets []*net.IPNet
|
||||
err := ns.WithNetNSPath(args.Netns, func(_ ns.NetNS) error {
|
||||
var err error
|
||||
ipn, err = ip.DelLinkByNameAddr(args.IfName, netlink.FAMILY_V4)
|
||||
ipnets, err = ip.DelLinkByNameAddr(args.IfName)
|
||||
if err != nil && err == ip.ErrLinkNotFound {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if conf.IPMasq {
|
||||
if len(ipnets) != 0 && conf.IPMasq {
|
||||
chain := utils.FormatChainName(conf.Name, args.ContainerID)
|
||||
comment := utils.FormatComment(conf.Name, args.ContainerID)
|
||||
if err = ip.TeardownIPMasq(ipn, chain, comment); err != nil {
|
||||
return err
|
||||
for _, ipn := range ipnets {
|
||||
err = ip.TeardownIPMasq(ipn, chain, comment)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
|
||||
func main() {
|
||||
skel.PluginMain(cmdAdd, cmdDel)
|
||||
skel.PluginMain(cmdAdd, cmdDel, version.All)
|
||||
}
|
||||
|
27
plugins/main/ptp/ptp_suite_test.go
Normal file
27
plugins/main/ptp/ptp_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 TestPtp(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "ptp Suite")
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user