Compare commits

..

628 Commits

Author SHA1 Message Date
9f96827c7c Merge pull request #369 from Random-Liu/cherrypick-#366-0.7
[Backport v0.7] Fix: failed to set bridge addr: could not add IP address to \"cni0\": file exists
2019-08-13 15:00:55 +02:00
16f134bb58 Fix a race condition in the bridge plugin.
Signed-off-by: Lantao Liu <lantaol@google.com>
2019-08-12 10:17:42 -07:00
a62711a5da Merge pull request #269 from squeed/portmap-prepend
Portmap: append, rather than prepend, entry rules
2019-03-15 11:54:57 -05:00
f8fcb3525f Portmap: append, rather than prepend, entry rules
This means that portmapped connections can be more easily controlled /
firewalled.
2019-03-06 17:17:33 +01:00
9ebe139e77 Merge pull request #194 from squeed/revert-iptables
Revert "vendor: bump coreos/go-iptables"
2018-10-03 10:50:40 -05:00
68326ab5bb Merge pull request #201 from squeed/v0.7-revert
Revert "Merge pull request #180 from squeed/v0.7-bump-iptables"
2018-09-12 10:05:26 -05:00
01ff074ef3 Revert "Merge pull request #180 from squeed/v0.7-bump-iptables"
This reverts commit 19f2f28178, reversing
changes made to 72b62babee.
2018-09-10 14:04:49 +02:00
011c7652c5 Revert "vendor: bump coreos/go-iptables"
This reverts commit ffb78af3af.
2018-08-24 14:24:54 +02:00
19f2f28178 Merge pull request #180 from squeed/v0.7-bump-iptables
vendor: bump coreos/go-iptables
2018-08-03 17:31:42 +02:00
ffb78af3af vendor: bump coreos/go-iptables
Pick up iptables-nftables support.
2018-08-03 15:03:35 +02:00
72b62babee Merge pull request #141 from squeed/missing-mac
pkg/ip: re-fetch the created link to return creation-time parameters
2018-04-12 11:09:35 +02:00
26dafaa097 plugins/ptp: test for valid data in Interfaces field 2018-04-11 15:19:20 +02:00
13e6a4b2ba plugins/bridge: Make stricter assertions about the return data 2018-04-11 15:10:39 +02:00
00b072dd0b pkg/ip: re-fetch the created link to return creation-time parameters
Fixes: #140
2018-04-11 15:04:38 +02:00
dd8ff8a5cf Merge pull request #113 from nyren/master
Explicitly enable IPv6 sysctl
2018-02-14 16:11:11 +00:00
08ec2995ba Explicitly enable IPv6 sysctl
If CNI is about to configure an IPv6 address, make sure IPv6 is not
disabled through the "disable_ipv6" sysctl setting.

This is a workaround for Docker 17.06 and later which sets
disable_ipv6=1 for all interfaces even if ipv6 is enabled in Docker.
2018-02-13 20:11:06 +01:00
136399f078 Merge pull request #117 from dcbw/dhcp-cleanup
dhcp plugin testcases and bug fixes
2018-02-09 19:23:51 +01:00
596480eadc dhcp: remove leases when they are released
Don't lock around the Stop() operation though, as that may take
a while and block other operations.  That may mean we call Stop()
multiple times, but the Lease object should handle that correctly
itself.
2018-02-08 11:17:38 -06:00
61c3c42107 dhcp: don't try to close the lease stop channel multiple times
Fixes: https://github.com/containernetworking/plugins/issues/105
2018-02-08 11:17:38 -06:00
b522ed6aae plugins/dhcp: add testcase 2018-02-08 11:17:38 -06:00
26ef6e312d vendor: add d2g/dhcp4server 2018-02-07 12:48:08 -06:00
461d433911 Merge pull request #116 from squeed/release-bump-go
release: bump golang version
2018-02-07 14:44:43 +01:00
84a01001be release: bump golang version 2018-02-07 13:16:30 +01:00
e2f063b534 Merge pull request #88 from lyft/ipvlan-master-intf-ipam
ipvlan: support enslaving an interface returned by ipam
2018-01-31 17:17:13 +01:00
9e5836047c Merge pull request #104 from mzahorik/master
Append default route and process route options in compliance with RFC 3442
2018-01-31 15:08:57 +01:00
808d4e20ae Append a default route to the CNI reply if there's a gateway advertised.
Classless static routes (DHCP option 121) are now processed first.
If CSRs exist, static routes (DHCP option 33) and the gateway default
route are ignored as per RFC 3442.
2018-01-26 10:06:26 -05:00
3468364f7e Merge branch 'master' into ipvlan-master-intf-ipam 2018-01-25 15:06:06 -08:00
412b6d3128 Merge pull request #111 from s1061123/add_procfsprefx
Add -hostprefix in DHCP daemon to run the daemon as container
2018-01-25 12:00:51 +01:00
9604565b22 Add -hostprefix in DHCP daemon to run the daemon as container
This diff adds -hostprefix option in dhcp daemon. This option
could be used to run dhcp daemon as container because container
cannot touch host's netns directly. The diff changes dhcp daemon
to touch procfs mounted to another path, like '/hostfs/proc'.
2018-01-25 02:00:43 +09:00
2a0736c748 Merge pull request #97 from oilbeater/fix/link-leak
delete link and ip if err when cmdAdd to avoid resource leak.
2018-01-24 16:38:44 +00:00
2eba56ad52 Merge pull request #100 from squeed/range-arg
ipam/host-local: Accept ip ranges as a runtime argument
2018-01-24 15:45:39 +01:00
d228f980e1 Merge pull request #103 from alice02/fix/skip_settleaddress
pkg/ipam: Skip ip.SettleAddresses if only IPv4 is used
2018-01-22 12:07:41 +01:00
6aa21c431e Merge pull request #107 from zhsj/fix-ipforward
pkg/ip: don't write to /proc/sys if ipforward enabled
2018-01-20 21:38:23 -06:00
c42470bc79 Merge pull request #110 from dcbw/host-device-honor-ifname
host-device: respect CNI_IFNAME/args.IfName
2018-01-19 19:05:29 +01:00
ffc591e242 host-device: respect CNI_IFNAME/args.IfName
On ADD save the host device's name into its IFLA_ALIAS property and
rename the device to the requested CNI_IFNAME inside the container
to conform to the CNI specification.  On DEL rename the device to
the original name and move it back into the host namespace.
2018-01-17 14:30:22 -06:00
59f9976017 pkg/ip: don't write to /proc/sys if ipforward enabled
This enables setup in a container env like systemd nspawn
where /proc/sys is mouted as read only.

Signed-off-by: Shengjing Zhu <i@zhsj.me>
2018-01-18 01:52:49 +08:00
c26961a990 Merge pull request #108 from jingax10/ipvlan_plugin_branch
Update IPVLAN modes by adding l3s in README.
2018-01-17 08:04:57 -08:00
8ebea58550 Update IPVLAN modes by adding l3s in README. 2018-01-12 12:06:48 -08:00
97664d8a6a pkg/ipam: Skip ip.SettleAddresses if only IPv4 is used
This change improves the performance of the ipam.ConfigureIface.
Some plugins are slow because of the ip.SettleAddress in ipam.ConfigureIface.
It seems to be only needed for IPv6, so should be skipped if only IPv4 is used.
2017-12-26 16:48:57 +09:00
5c7e7c0913 ipvlan: support chaining for master interface and IP configuration
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 from the ipvlan configuration, 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.
2017-12-23 10:59:48 -08:00
b03d23a4fa ipam/host-local: Accept ip ranges as a runtime argument
This allows for the runtime to dynamically request IP ranges.

Fixes: #95
2017-12-11 13:51:01 +01:00
03e316b07b Merge pull request #99 from rosenhouse/add-appveyor-badge
README: add Windows CI badge
2017-12-06 18:21:47 +01:00
ecdd827d3a README: add badge for appveyor (Windows CI) 2017-12-03 14:22:36 -08:00
1f02326d56 delete link and ip if err when cmdAdd to avoid resource leak. 2017-11-27 15:26:07 +08:00
92c634042c Merge pull request #93 from squeed/host-device
plugins/main/host-device: generate result, fix DEL, other cleanups
2017-11-22 10:09:32 -06:00
2c05055101 Merge pull request #81 from squeed/portmap-hairpin
portmap: support hairpin, improve performance
2017-11-15 17:34:35 +01:00
5e830efb20 plugins/main/host-local: generate result, fix DEL, other cleanups
This plugin needed some cleaning up: it didn't generate output, and
didn't test DEL. Add those things, plus a README.
2017-11-15 16:00:32 +01:00
73fdc87395 Merge pull request #92 from squeed/host-device
plugins/host_device: move to "main" folder
2017-11-14 22:08:16 -08:00
d07d2aaf71 plugins/host_device: move to "main" folder 2017-11-13 18:50:20 +01:00
6c2ef734c2 Merge pull request #77 from rakelkar/rakelkar/host-local-windows
host-local: Update host-local IPAM to support Windows
2017-11-13 07:37:10 -08:00
47668f6d64 host-local: Update host-local IPAM to support Windows 2017-11-11 15:17:45 -08:00
fbced0cccb Merge pull request #84 from rosenhouse/add-windows-ci
Enable Windows CI (Appveyor)
2017-11-10 18:59:16 -08:00
99f6be0319 Enable Windows CI (Appveyor)
- start list of linux_only plugins; ignore them when testing on Windows
- Isolate linux-only code by filename suffix
- Remove stub (NotImplemented) functions
- other misc. fixes for Windows compatibility
2017-11-10 08:09:29 -08:00
b09e0d28a7 Merge pull request #89 from dcbw/del-link-by-name-addr-fix
pkg/ip: don't return error from DelLinkByNameAddr() if no addresses exist
2017-11-10 17:06:29 +01:00
5576f3120e portmap: support hairpin, improve performance
This change improves the performance of the portmap plugin and fixes
hairpin, when a container is mapped back to itself.

Performance is improved by using a multiport test to reduce rule
traversal, and by using a masquerade mark.

Hairpin is fixed by enabling masquerading for hairpin traffic.
2017-11-10 16:56:52 +01:00
449700f7ea pkg/ip: don't return error from DelLinkByNameAddr() if no addresses exist
For some reason no addresses on the interface returned an error, despite
having a testcase that explicitly tested for success.
2017-11-07 16:07:04 -06:00
4779f1d2bf ipvlan: support enslaving an interface returned by ipam
For IP allocation schemes that cannot be interface agnostic, master can be set
to "ipam". In this configuration, the IPAM plugin is required to return a single
interface name for the ipvlan plugin to enslave.
2017-11-01 10:14:04 -07:00
7f98c94613 Merge pull request #65 from rosenhouse/golang-to-1.9
Golang versions: add 1.9, drop 1.7
2017-10-18 15:36:34 -07:00
596b44301b Vagrantfile: update to golang 1.9.1 2017-10-11 21:19:16 +02:00
0063a1b9d0 Merge pull request #78 from rmohr/dhcp
Don't let DHCP IPAM plugin fail on missing lease
2017-10-11 14:50:19 +01:00
cc71426592 Don't let DHCP delete fail on missing lease
There are at least two reasons why a lease is not present:

 * The dhcp ipam daemon was restarted
 * On add the IPAM plugin failed

Don't fail the IPAM invocation when the lease is not present, to allow
proper device cleanup on CNI delete invocations.
2017-10-11 14:29:16 +02:00
e256564546 Merge pull request #74 from rosenhouse/host-device-fixes
Host-device fixes
2017-09-13 11:41:14 +02:00
c238c93b5e host-device plugin: result is valid JSON
test:
- feed valid config JSON to plugin
- execute plugin inside the namespace with the test device
2017-09-12 21:01:58 -07:00
25ca6ccb52 host-device: do not swallow netlink errors 2017-09-12 20:53:35 -07:00
5e46a66c89 Fix go get github.com/containernetworking/plugins.
Signed-off-by: Lantao Liu <lantaol@google.com>
2017-09-12 05:08:38 +00:00
6be2e8a0e2 Merge pull request #3 from trusch/master
added host-device plugin which adds a specified link to container
2017-09-11 18:51:21 +02:00
b24225fc17 Merge pull request #72 from rosenhouse/move-echosvr
testing: move echosvr into testutils
2017-09-07 14:22:03 +02:00
d8f2fd7a3c testing: move echosvr into testutils 2017-09-06 19:10:48 -07:00
1396ab0bab Merge pull request #63 from squeed/v6-fixes
Fix ipmasq teardown on v6-only interfaces
2017-09-06 15:36:24 -05:00
92babd4a3d Merge pull request #71 from rosenhouse/fix-portmap-integ-test
portmap integration test: echo server runs in separate process
2017-09-06 13:13:03 -05:00
7a62515407 pkg/ip: Fix ipmasq teardown on v6-only interfaces 2017-09-06 20:02:41 +02:00
008024125a portmap integration test: echo server runs in separate process
this way we're not mixing goroutines and namespaces
2017-09-05 23:36:12 -07:00
556e509097 Merge pull request #64 from rosenhouse/travis-ginkgo
travis: run with ginkgo -p instead of go test
2017-08-31 14:28:55 +02:00
dda9c2b1b0 travis: run with ginkgo -p instead of go test
may help reduce test-pollution due to namespace-affinity

see http://onsi.github.io/ginkgo/#parallel-specs
2017-08-30 20:22:12 -07:00
0e3df2961c Merge pull request #67 from rosenhouse/test-all-packages
Test all packages
2017-08-30 19:32:48 -07:00
e1ea7f5ecb Test all non-vendored packages 2017-08-30 14:47:10 -07:00
8fe8460c72 Merge pull request #66 from rosenhouse/filelock-vet
host-local FileLock is used by value instead of by reference
2017-08-30 08:21:28 -07:00
92e62b9f4d test.sh: cover host-local disk backend for go test and go vet 2017-08-30 06:52:29 -07:00
2f957864ea host-local disk backend store uses FileLock by reference
- this change fixes go vet warnings for the package
2017-08-30 06:52:29 -07:00
b49379d284 Merge pull request #62 from squeed/bridge-fixes
bridge: various fixes
2017-08-29 21:00:21 -07:00
9769434a13 Golang versions: add 1.9, drop 1.7 2017-08-29 20:15:50 -07:00
a124fb36e6 bridge: various fixes
* Don't set the MAC, send gratuitous arp instead
* Set the bridge's MAC to itself
* Only disable DAD when necessary
2017-08-28 18:12:49 +02:00
9fb22524a1 Merge pull request #54 from squeed/clean-build
Release: clean the builddir when building
2017-08-21 15:39:28 +02:00
2d7d680874 Release: clean the builddir when building 2017-08-14 14:42:50 +02:00
7480240de9 Merge pull request #50 from squeed/host-local-multi
ipam/host-local: support sets of disjoint ranges
2017-08-11 11:59:55 -05:00
27d027a6d3 ipam/host-local: support sets of disjoint ranges
In real-world address allocations, disjoint address ranges are common.
Therefore, the host-local allocator should support them.

This change still allows for multiple IPs in a single configuration, but
also allows for a "set of subnets."

Fixes: #45
2017-08-09 19:02:08 +02:00
5544d9ced0 Merge pull request #47 from dcbw/tuning-prev-result
tuning: pass prevResult through
2017-07-31 15:19:46 -05:00
998a0f6c6a tuning: pass prevResult through 2017-07-26 14:39:01 -05:00
20bc33abc5 Merge pull request #44 from verizonlabs/build-on-osx
If we're on OS X then have Go cross compile for us
2017-07-21 11:48:06 -07:00
bde6140771 If we're on OS X then have Go cross compile for us 2017-07-20 12:03:30 -04:00
a714098daf Merge pull request #35 from dnardo/bridge-plugin
bridge: add support for promiscuous mode
2017-07-12 08:30:00 -07:00
409be3d304 Merge pull request #40 from squeed/portmap-timeout
plugins/portmap: fix test flake
2017-07-12 08:18:09 -07:00
3b41be3784 Merge pull request #38 from tomdee/release-process
Add release process
2017-07-12 08:11:39 -07:00
b7b0e6c1d4 Add release process
Copied from containernetworking/cni

The only difference is that the release archives also include the the
word "plugin"
2017-07-11 13:57:49 -07:00
3745ee2d3f plugins/portmap: fix test flake
The source address selection was random, and sometimes we picked a
source address that the container didn't have a route to. Adding a
default route fixes that!
2017-07-06 16:12:24 +02:00
56d8984bbc Merge pull request #37 from containernetworking/travis-badge
README.md: Add travis badge
2017-07-06 15:19:15 +02:00
e7328869fa Merge pull request #39 from dcbw/ptp-fix-test
ptp: fix testcase for Interface int-pointer changes
2017-07-05 16:39:48 -05:00
3c9f1b16d6 Merge pull request #36 from tomdee/vendor-libcni-v0.6.0-rc1
Update vendor to v0.6.0-rc1
2017-07-05 16:36:50 -05:00
0b9dbef185 ptp: fix testcase for Interface int-pointer changes 2017-07-05 16:26:19 -05:00
55cdf255db README.md: Add travis badge 2017-07-05 11:49:07 -07:00
ba6a709062 Update vendor to v0.6.0-rc1 2017-07-05 11:46:00 -07:00
9d08b04a3a bridge: add support for promiscuous mode 2017-07-05 09:51:10 -07:00
a690f775ae Merge pull request #25 from squeed/ptp-v6
ptp: add ipv6 support
2017-07-05 17:30:23 +02:00
15f6798bf9 Merge pull request #34 from dnardo/netlink-update
vendor: Update vishvanana/netlink dependency.
2017-07-05 08:29:53 -07:00
74054da7c5 Merge pull request #28 from dcbw/update-for-interface-change
pkg,plugins: update for Result struct Interface index changes
2017-07-05 10:29:05 -05:00
7716c027cf vendor: Update vishvanana/netlink dependency.
This updates the netlink dependency to include recent updates, including a fix when setting prosmic mode on a bridge and additions for creating qdisc/classes/filters. This is necessary for some upcoming additions to CNI
2017-06-30 15:18:01 -07:00
3daee3214d ptp: add ipv6 support
* Wait for addresses to leave tentative state before setting routes
* Enable forwarding correctly
* Set up masquerading according to the active protocol
2017-06-30 12:06:57 +02:00
f8134be6ef Merge pull request #33 from squeed/bump-iptables
vendor: bump go-iptables to 0.2.0
2017-06-30 11:59:44 +02:00
0da6780449 pkg,plugins: update for Result struct Interface index changes
It's a pointer now, so we need to use the helper function to set
the field and also test for nil before accessing it.
2017-06-29 13:40:36 -05:00
8185e7c59c vendor: bump go-iptables to 0.2.0 2017-06-29 16:12:17 +02:00
ca3f28fa9e host-device: cleanup + completed tests; 2017-06-29 11:44:59 +02:00
121362601b vendor: bump cni to 0.5.2-git49d814c 2017-06-28 12:06:37 -05:00
5a7a8b3069 Merge pull request #24 from squeed/flannel-return
flannel: set the cniVersion on the delegated config
2017-06-28 08:45:02 -05:00
f01230d75f Merge pull request #10 from leblancd/v6_bridge_plugin
bridge: Add support for IPv6 to bridge plugin
2017-06-28 08:34:53 -05:00
ffdc748861 bridge: Add support for IPv6 to bridge plugin
This change adds support for IPv6 container/pod addresses to the CNI
bridge plugin, both for dual-stack (IPv4 + IPv6) and for IPv6-only
network configurations.

The proposed changes support multiple IPv6 addresses on a container
interface. If isGW is configured, the bridge will also be configured with
gateway addresses for each IPv6 subnet.

Please note that both the dual-stack functionality and support for multiple
IPv6 container/gateway addresses depends upon containernetworking/cni
PR 451 "ipam/host-local: support multiple IP ranges".
This change could potentially be committed independently from this host-local
plugin change, however the dual-stack and multiple IPv6 address
functionality that is enabled by this change can't be exercised/tested
until the host-local plugin change is committed.

There are some IPv6 unit test cases that are currently commented out
in the proposed changes because these test cases will fail without the
prior commits of the multiple IP range host-local change.

This pull request includes a temporary workaround for Kubernetes
Issue #32291 (Container IPv6 address is marked as duplicate, or dadfailed).
The problem is that kubelet enables hairpin mode on bridge veth
interfaces. Hairpin mode causes the container/pod to see echos of its
IPv6 neighbor solicitation packets, so that it declares duplicate address
detection (DAD) failure. The long-term fix is to use enhanced-DAD
when that feature is readily available in kernels. The short-term fix is
to disable IPv6 DAD in the container. Unfortunately, this has to be done
unconditionally (i.e. without a check for whether hairpin mode is enabled)
because hairpin mode is turned on by kubelet after the CNI bridge plugin
has completed cmdAdd processing. Disabling DAD should be okay if
IPv6 addresses are guaranteed to be unique (which is the case for
host-local IPAM plugin).
2017-06-21 12:47:38 -04:00
8c5e754617 flannel: set the cniVersion on the delegated config
Fixes: #23
2017-06-16 19:04:07 +02:00
5bbff37294 Merge pull request #16 from squeed/gratuitous-arp
ptp, macvlan: don't change mac address; send gratuitous arp
2017-06-16 18:34:37 +02:00
485f151536 Merge pull request #22 from squeed/portmap-plugin
portmap: Don't accept a port number of 0
2017-06-16 18:33:40 +02:00
494053dd98 Merge pull request #11 from dcbw/dhcp-pidfile
dhcp: add --pidfile option to DHCP client daemon
2017-06-15 22:48:42 -07:00
83ec9bb73f portmap: Don't accept a port number of 0
This also adds more testing around configuration parsing.
2017-06-15 14:00:04 +02:00
e2558a03bb Merge pull request #12 from squeed/host-local-multi
ipam/host-local: support multiple IP ranges
2017-06-14 21:55:30 -05:00
a38dd5f7a0 dhcp: add --pidfile option to DHCP client daemon 2017-06-14 21:41:43 -05:00
45650feb32 Merge pull request #14 from rosenhouse/more-ns-warning-redux
pkg/ns readme: expand on danger of ns switching in long-lived programs
2017-06-14 16:18:26 +01:00
2e9e87732f ipam/host-local: support multiple IP ranges
This change allows the host-local allocator to allocate multiple IPs.
This is intended to enable dual-stack, but is not limited to only two
subnets or separate address families.
2017-06-12 21:14:44 +02:00
7cda9af13f Merge pull request #17 from tklauser/x-sys-unix-setns
pkg/ns: use unix.Setns() instead of open coding it
2017-06-08 08:39:41 -07:00
9c7aeff02c pkg/ns: use unix.Setns() instead of open coding it
Use unix.Setns() instead of manually re-implementing it using
unix.Syscall().
2017-06-08 14:48:58 +02:00
98cafb157c Merge pull request #15 from rosenhouse/contribute-vagrantfile
Add CONTRIBUTING and Vagrantfile
2017-06-07 07:26:01 -07:00
e76165b44f ptp, macvlan: don't change mac address; send gratuitous arp 2017-06-07 16:08:41 +02:00
94e6489466 vendor: add j-keck/arping 2017-06-07 15:40:26 +02:00
9a96aa2981 add Vagrantfile to support cross-platform development
Update CONTRIBUTING to reflect plugin split
2017-06-06 20:13:31 -07:00
a860d4d244 contributing doc: copy in from cni repo 2017-06-06 19:20:19 -07:00
6f45630cc1 pkg/ns readme: expand on danger of ns switching in long-lived programs
- redux of https://github.com/containernetworking/cni/pull/331 since the
plugin/lib split
- added link to new blog post about this from Weave folks (thanks!)
2017-06-06 19:05:00 -07:00
e8bea554c5 Merge pull request #13 from squeed/build-fixes
build: some minor post-repo-split fixes; add fastbuild
2017-06-02 10:23:03 +02:00
43a24515ee build: some minor post-repo-split fixes; add fastbuild 2017-06-01 18:51:46 +02:00
0997c532a1 Merge pull request #1 from squeed/portmap-plugin
iptables-based portmapping plugin
2017-06-01 13:05:29 +01:00
a7aaf0e377 plugins/meta/portmap: add an iptables-based host port mapping plugin 2017-06-01 10:06:28 +02:00
22cda76afb vendor: add mattn/go-shellwords 2017-05-31 19:21:03 +02:00
b47d5b7c0a vendor: add libcni 2017-05-31 18:32:38 +02:00
539174f8c8 vendor: update coreos/go-iptables 2017-05-31 18:32:38 +02:00
8816992fcb Merge pull request #9 from squeed/move-pkg
Move libraries from cni only used by plugins.
2017-05-31 10:11:59 -05:00
4e2f467e21 vendor: remove moved cni/pkg/... libraries 2017-05-23 16:12:53 +02:00
3c436520be move most of cni/pkg to plugins/pkg: delete code staying in cni/pkg
This moves the following packages to this repository:
* ip
* ipam
* ns
* testutils
* utils
2017-05-23 16:12:49 +02:00
8b43e90949 Add 'pkg/' from commit 'a11be4d7596203874b742b6597caf255204c56c3'
git-subtree-dir: pkg
git-subtree-mainline: dcf7368eea
git-subtree-split: a11be4d759
2017-05-19 19:43:02 +02:00
dcf7368eea Merge pull request #6 from squeed/merge-docs
Documentation folder merge
2017-05-19 16:16:01 +02:00
0c1c1b7d8c Merge pull request #8 from squeed/bump-cni
vendor: bump cni to v0.5.2
2017-05-19 15:54:54 +02:00
f5e4c117d1 vendor: bump cni to v0.5.2 2017-05-19 14:38:26 +02:00
c9e3fc4890 Add references to all plugins to the README 2017-05-16 16:46:24 +02:00
9f52fc0374 Move documentation folder to individual READMEs 2017-05-16 16:46:15 +02:00
3ac78df0b6 Add 'Documentation/' from commit '5bb59a12b80dc99552bebf0394a067807f72f947'
git-subtree-dir: Documentation
git-subtree-mainline: db8f94a509
git-subtree-split: 5bb59a12b8
2017-05-15 17:13:31 +02:00
db8f94a509 Rename build & test, take two 2017-05-15 16:50:24 +02:00
1430c43243 Rename build script to avoid conflict with bazel 2017-05-15 16:45:58 +02:00
f130db5b61 Merge pull request #5 from dcbw/add-cni-plugins
plugins: add plugins from containernetworking/cni
2017-05-15 12:27:40 +02:00
d2792f264e plugins: add plugins from containernetworking/cni
Plugins prepared from the containernetworking/cni repo as follows:

1) git reset --hard 1a9288c3c0
2) git remove everything not in plugins/
3) git remove plugins/test
4) git merge into containernetworking/plugins repo
5) adjust import paths for containernetworking/cni -> containernetworking/plugins
2017-05-14 23:12:45 -05:00
4e0349a7e7 Remove everything except for plugins in preparation for import to plugins repo 2017-05-14 23:04:53 -05:00
4af68521cd plugins: move vlan to plugins/main/ 2017-05-09 22:47:26 -05:00
7cab2fd317 vendor: add github.com/d2g 2017-05-09 22:45:44 -05:00
be5f1f723e vendor: add coreos/go-systemd 2017-05-09 22:45:44 -05:00
8c27b64f13 vendor: add gomega/gbytes and gomega/gexec 2017-05-09 22:45:44 -05:00
af9127b7ea vendor: bump CNI to 1a9288c3c0 2017-05-09 22:45:44 -05:00
1a9288c3c0 Merge pull request #450 from gunjan5/spec-version-mismatch
cniVersion and config structure mismatch
2017-05-08 11:12:25 +02:00
e03c76b852 Merge pull request #445 from feiskyer/cniversion
Add cniVersion to Result
2017-05-05 17:47:26 -05:00
efe4f37afa cniVersion and config structure mismatch 2017-05-05 14:50:58 -07:00
c44a76ddd3 Merge pull request #4 from containernetworking/plugins-travis
travis integration
2017-05-05 17:10:19 +02:00
b686cad062 Merge pull request #447 from gunjan5/invalid-spec-json
Fix invalid json
2017-05-04 16:06:04 -07:00
5e3be5a44f fix invalid json 2017-05-04 15:59:11 -07:00
1e3715157c travis integration
- go build instead of go install to support cross-compile to bin dir
- shellcheck fixes for build script
2017-05-03 12:22:14 -07:00
a11be4d759 Fix testings accross the project
Signed-off-by: Pengfei Ni <feiskyer@gmail.com>
2017-05-03 23:21:44 +08:00
f197c01b62 Fix testings accross the project
Signed-off-by: Pengfei Ni <feiskyer@gmail.com>
2017-05-03 23:21:44 +08:00
3cd4e26aeb Add cniVersion to Result
Signed-off-by: Pengfei Ni <feiskyer@gmail.com>
2017-05-03 21:38:42 +08:00
27a5b994c2 Add cniVersion to Result
Signed-off-by: Pengfei Ni <feiskyer@gmail.com>
2017-05-03 21:38:42 +08:00
f2faf549b4 [host-device] integrated getLink() function which maps either devicename, hw-addr or kernelpath to a link object; 2017-05-02 13:21:50 +02:00
231f6c4c53 Merge pull request #433 from dunjut/master
scripts: cleanup() should rm net container in docker-run.sh
2017-04-28 20:58:17 -05:00
a6c96acda4 Merge pull request #440 from feiskyer/cap
cnitool: support capablity args
2017-04-28 20:55:31 -05:00
6099d8c84c added host-device plugin which adds a specified link to the container network namespace; 2017-04-28 09:34:22 +02:00
c11286b835 Merge pull request #2 from dcbw/vlan
plugins: add a simple VLAN plugin
2017-04-26 12:54:05 -05:00
010c8a9503 Merge pull request #439 from dnardo/netlink-update
vendor: Update vishvanana/netlink dependency.
2017-04-25 10:24:05 -05:00
73a2d131ee cnitool: support capablity args
Signed-off-by: Pengfei Ni <feiskyer@gmail.com>
2017-04-25 17:35:03 +08:00
21d96393ea Merge pull request #391 from tangle329/master
Validate rangeStart and rangeEnd specified in conf
2017-04-24 22:00:07 -05:00
8eddd61fed Merge pull request #435 from micahhausler/patch-1
Fixed table formatting
2017-04-24 11:21:33 -07:00
0de29de33e Update netlink dependency. 2017-04-24 10:31:24 -07:00
87fbabeeef Fixed table formatting 2017-04-21 15:22:56 -04:00
du
6ee2048ec2 scripts: cleanup() should rm net container in docker-run.sh 2017-04-21 18:27:46 +08:00
279cae5ec5 Merge pull request #432 from dunjut/master
README: build the plugins using ./build.sh, not ./build
2017-04-21 11:12:49 +01:00
du
43c11dda15 README: build the plugins using ./build.sh, not ./build 2017-04-21 15:29:57 +08:00
076080eedf vlan: add VLAN plugin 2017-04-19 13:41:12 -05:00
6b3bf7ea7b vendor: add CNI invoke, ip, ipam, and hwaddr packages 2017-04-18 23:49:26 -05:00
7309649a66 test: CmdAddWithResult sets CNI_PATH from PATH
To actually use CNI plugins in the given CNI_PATH, we need to add
CNI_PATH to PATH because CmdAddWithResult() does this:

os.Setenv("CNI_PATH", os.Getenv("PATH"))
2017-04-18 23:49:26 -05:00
9ef6f5f723 test: make tests actually work when packages have vendored imports
Go's "..." syntax (eg, ./plugins/...) doesn't traverse symlinks, so
go test wasn't finding the vendor/ directory for imports.  To get around
that we have to specify each testable package specifically rather
than use "...".
2017-04-18 23:40:46 -05:00
4a2f3ddd5d Merge pull request #430 from kris-nova/remove-proposal
Wondering if this is still a proposal?
2017-04-16 11:13:24 -07:00
392e1bbd10 Changing Proposal->Specification in Spec 2017-04-16 11:01:23 -06:00
5bb59a12b8 Merge pull request #419 from asridharan/cnitool
Added documentation for `cnitool`
2017-04-07 10:20:07 -07:00
137b4975ec Merge pull request #419 from asridharan/cnitool
Added documentation for `cnitool`
2017-04-07 10:20:07 -07:00
7d3fe6923b Documentation: Added documentation for cnitool.
Added documentation to simplify the usage of `cnitool` and the
settings expected to be used with `cnitool`.
2017-04-05 21:40:24 +00:00
382686ed63 Documentation: Added documentation for cnitool.
Added documentation to simplify the usage of `cnitool` and the
settings expected to be used with `cnitool`.
2017-04-05 21:40:24 +00:00
d427963784 libcni: Fixed tests that were checking error strings.
Checking error strings makes these tests flaky, especially if the
error string is changed in libcni. Have gone ahead an introduced a new
error type `NoConfigsFoundError` and the Match is against the error
type making it more deterministic.
2017-04-05 21:40:24 +00:00
5b0d6a0c8f libcni: Improved error messages.
Improved the error reporting to be more descriptive when configuration
files are not filed while using the cni-tool.
2017-04-05 21:40:16 +00:00
361ec957fb Merge pull request #413 from dcbw/spec-fix-ips
spec/plugins: fix 'ip'->'ips' in the spec, bump to 0.3.1
2017-04-05 14:39:44 -07:00
29ba54c045 spec/plugins: fix 'ip'->'ips' in the spec, bump to 0.3.1 2017-04-05 12:46:24 -05:00
8447ebc83a spec/plugins: fix 'ip'->'ips' in the spec, bump to 0.3.1 2017-04-05 12:46:24 -05:00
9445711fa5 spec/plugins: fix 'ip'->'ips' in the spec, bump to 0.3.1 2017-04-05 12:46:24 -05:00
d4bbce1865 Merge pull request #423 from rosenhouse/update-travis
travis: shift forward to Go 1.8 and 1.7
2017-04-05 09:26:49 -07:00
ff92701693 Merge pull request #421 from kshafiee/patch-1
Added entry for CNI-Genie
2017-04-05 09:24:21 -07:00
a39d6845fa travis: shift forward to Go 1.8 and 1.7 2017-04-05 09:11:35 -07:00
d66edc3180 Merge pull request #400 from lstoll/lstoll-rename-build-script
Rename build script to avoid conflict with bazel
2017-04-05 11:03:23 -05:00
aaba1b0224 Added entry for CNI-Genie
CNI-Genie enables orchestrators (kubernetes, mesos) for seamless connectivity to choice of CNI plugins (calico, canal, romana, weave) configured on a Node
2017-04-03 12:00:02 -07:00
470e86eb8b Update community sync detail 2017-03-31 14:38:48 -07:00
73c6616946 Merge pull request #414 from kad/s390x
Enable s390x build
2017-03-27 10:10:06 -07:00
61f3b4b315 Enable s390x build 2017-03-23 00:51:29 +02:00
0799f5732f Merge pull request #412 from tomdee/safe-del
plugins/*: Don't error if the device doesn't exist
2017-03-22 09:47:29 -07:00
58b78e72fd plugins/*: Don't error if the device doesn't exist
I wasn't able to test or update the dhcp plugin but from a code read it
should be fine. All the other plugins are tested and fixed
2017-03-22 08:52:29 -07:00
13824487c6 plugins/*: Don't error if the device doesn't exist
I wasn't able to test or update the dhcp plugin but from a code read it
should be fine. All the other plugins are tested and fixed
2017-03-22 08:52:29 -07:00
699380d687 Merge pull request #407 from aaronlevy/del-err
plugins/meta/flannel: If net config is missing do not return err on DEL
2017-03-21 10:43:40 -07:00
74d4cbed76 plugins/meta/flannel: If net config is missing do not return err on DEL 2017-03-17 13:37:33 -07:00
b87126377a Merge pull request #396 from cf-container-networking/vendored-types-leakage
pkg/ip: do not leak types from vendored netlink package
2017-03-16 19:20:40 -07:00
c235b448d4 Merge pull request #401 from chrismarino/patch-1
Added Romana to list of CNI providers...
2017-03-15 12:38:04 -07:00
c1c7826922 Added Romana to list of CNI providers...
Added Romana to list of CNI providers.
2017-03-15 12:10:21 -07:00
1b9caefba5 Rename build script to avoid conflict with bazel
It's difficult to include this repository using bazel, because
the file named "build" conflicts with new_go_repository generation
on case-insensitive filesystems (ref
https://github.com/bazelbuild/rules_go/issues/234). This change
renames the file to something that doesn't conflict, and also
renames the test script for consistency.
2017-03-15 15:59:44 +11:00
bb322572eb pkg/ip: improve docstring for SetupVeth 2017-03-13 11:27:12 -07:00
f51dd618c5 pkg/ip: improve docstring for SetupVeth 2017-03-13 11:27:12 -07:00
559ffa1111 Add simple testing infrastructure 2017-03-13 18:05:55 +01:00
168a5a0210 godeps: initial creation 2017-03-13 14:36:20 +01:00
d62b9a0584 sample: create sample plugin 2017-03-13 14:31:58 +01:00
71d96ea91c pkg/ip: SetupVeth returns net.Interface 2017-03-10 09:49:46 -08:00
e4a0583d7a pkg/ip: SetupVeth returns net.Interface 2017-03-10 09:49:46 -08:00
fb4a7f477b Initial commit 2017-03-10 16:46:52 +01:00
1b65890795 Validate rangeStart and rangeEnd specified in conf
Signed-off-by: Tang Le <tangle3@wanda.cn>
2017-03-10 12:51:05 +08:00
b07d768f12 pkg/ip: do not leak types from vendored netlink package
The exported function SetupVeth now returns a package-defined type.

Signed-off-by: Gabe Rosenhouse <grosenhouse@pivotal.io>
2017-03-09 13:55:15 -08:00
58c834c4f3 pkg/ip: do not leak types from vendored netlink package
The exported function SetupVeth now returns a package-defined type.

Signed-off-by: Gabe Rosenhouse <grosenhouse@pivotal.io>
2017-03-09 13:55:15 -08:00
4f36e5994e Merge pull request #393 from containernetworking/community-sync-readme
readme.md: Add link to community sync
2017-03-08 16:46:44 -08:00
5d5b16f0b3 readme.md: Add link to community sync 2017-03-08 15:54:15 -08:00
4ce9b019aa Merge pull request #366 from ehazlett/ipam-host-local-initial-reserved-ip
Do not error if last reserved not found after initial creation
2017-03-06 07:13:06 -08:00
ac50624393 fix unrelated failing tests
Signed-off-by: Evan Hazlett <ejhazlett@gmail.com>
2017-03-02 15:37:43 -05:00
2678be24d0 add test for ensuring initial subnet creation does not contain an error
Signed-off-by: Evan Hazlett <ejhazlett@gmail.com>
2017-03-02 15:16:09 -05:00
474ba16901 do not error if last_reserved_ip is missing for host local ipam
Signed-off-by: Evan Hazlett <ejhazlett@gmail.com>
2017-03-02 13:59:21 -05:00
1f6efc28b5 Merge pull request #387 from tangle329/master
Check n.IPAM before use it in LoadIPAMConfig function
2017-03-02 09:23:02 -08:00
75a5cf1bf0 Merge pull request #373 from dcbw/conflist-runtime-config
spec,libcni: add support for injecting runtimeConfig into plugin stdin data
2017-03-02 11:17:00 -06:00
1003822e53 Check n.IPAM before use it in LoadIPAMConfig function
Signed-off-by: Tang Le <tangle3@wanda.cn>
2017-03-02 10:25:19 +08:00
9b8a0e0c64 spec,libcni: add support for injecting runtimeConfig into plugin stdin data
Add a new CapabilityArgs member to the RuntimeConf struct which runtimes can
use to pass arbitrary capability-based keys to the plugin.  Elements of this
member will be filtered against the plugin's advertised capabilities (from
its config JSON) and then added to a new "runtimeConfig" top-level map added
to the config JSON sent to the plugin on stdin.

Also "runtime_config"->"runtimeConfig" in CONVENTIONS.md to make
capitalization consistent with other CNI config keys like "cniVersion".
2017-03-01 10:49:40 -06:00
71c4a60741 spec,libcni: add support for injecting runtimeConfig into plugin stdin data
Add a new CapabilityArgs member to the RuntimeConf struct which runtimes can
use to pass arbitrary capability-based keys to the plugin.  Elements of this
member will be filtered against the plugin's advertised capabilities (from
its config JSON) and then added to a new "runtimeConfig" top-level map added
to the config JSON sent to the plugin on stdin.

Also "runtime_config"->"runtimeConfig" in CONVENTIONS.md to make
capitalization consistent with other CNI config keys like "cniVersion".
2017-03-01 10:49:40 -06:00
6008de1ffd Merge pull request #382 from rosenhouse/doc-spec-upgrade
Documentation: guidance for upgrading to CNI Spec v0.3.0
2017-03-01 08:45:56 -08:00
c806f3d62b Minor rewording about default config version 2017-02-28 19:44:25 +01:00
09e5feae1f Minor rewording about default config version 2017-02-28 19:44:25 +01:00
0b1b85413e Merge pull request #383 from dcbw/test-fixes
tests: misc test fixes
2017-02-27 16:13:45 -06:00
7d8c23dd59 Merge pull request #383 from dcbw/test-fixes
tests: misc test fixes
2017-02-27 16:13:45 -06:00
d6d012b88f Merge pull request #374 from squeed/config-list-upconvert
libcni: up-convert a Config to a ConfigList when no other configs are found.
2017-02-27 15:10:37 -06:00
1f4a8c2802 Merge pull request #377 from squeed/ptp-sandbox
plugins/main/ptp: set the Sandbox property on the response
2017-02-27 15:06:57 -06:00
fc174a6635 Merge pull request #380 from bboreham/patch-1
Add Bryan Boreham as maintainer
2017-02-27 15:06:26 -06:00
2f1bfcee63 pkg/types: misc current types testcase cleanups 2017-02-27 13:28:07 -06:00
35153e7de8 pkg/types: misc current types testcase cleanups 2017-02-27 13:28:07 -06:00
2a6d12b206 pkg/testutils: return errors after restoring stdout
Ensures Ginkgo is able to print detailed failure messages instead
of them being captured by the pipe.
2017-02-27 13:27:19 -06:00
5a984f0dee pkg/testutils: return errors after restoring stdout
Ensures Ginkgo is able to print detailed failure messages instead
of them being captured by the pipe.
2017-02-27 13:27:19 -06:00
94e02e0768 Merge pull request #375 from aaithal/complieOnNonLinuxPlatforms
pkg/ns: refactored so that builds succeed on non-linux platforms
2017-02-27 11:46:22 -06:00
f15d521b2a docs: table formatting is hard 2017-02-27 07:57:00 -08:00
bbc23ea761 docs: table formatting is hard 2017-02-27 07:57:00 -08:00
5124ac4759 docs: fill-out and correct version conversion table 2017-02-27 07:53:09 -08:00
b6e9bf8372 docs: fill-out and correct version conversion table 2017-02-27 07:53:09 -08:00
2aa863dab7 docs: minor improvements to spec-upgrades 2017-02-27 07:17:37 -08:00
379d47760c docs: minor improvements to spec-upgrades 2017-02-27 07:17:37 -08:00
1061ef1eb9 docs: add small upgrade instructions 2017-02-27 14:57:31 +01:00
119d6f20f5 docs: add small upgrade instructions 2017-02-27 14:57:31 +01:00
bdd70561ad docs: minor improvements to 0.3.0 upgrade guidance 2017-02-27 14:27:56 +01:00
70c153eb41 docs: minor improvements to 0.3.0 upgrade guidance 2017-02-27 14:27:56 +01:00
5b6d955695 docs: Edits to v0.3.0 upgrade guidance 2017-02-26 16:53:52 -08:00
fa53446854 docs: Edits to v0.3.0 upgrade guidance 2017-02-26 16:53:52 -08:00
f658c0b4b3 docs: CNI versioning for 0.3.0 upgrade 2017-02-26 13:26:14 -08:00
328ea56e75 docs: CNI versioning for 0.3.0 upgrade 2017-02-26 13:26:14 -08:00
6e6ad53ea8 libcni: up-convert a Config to a ConfigList when no other configs are found. 2017-02-24 19:44:06 +01:00
1bc8141105 Merge pull request #379 from Intel-Corp/master
README: List multus as 3rd party plugin
2017-02-23 10:40:59 -08:00
460e1c5be3 vendor: Update vishvanana/netlink dependency 2017-02-23 09:53:11 -08:00
7d5619d277 pkg/ip: refactored so that builds succeed on non-linux platforms
moved functions that depend on linux packages into a separate file and added
nop methods with a build tag for non-linux platforms in a new file.
2017-02-23 09:52:13 -08:00
7eeb21a999 pkg/ip: refactored so that builds succeed on non-linux platforms
moved functions that depend on linux packages into a separate file and added
nop methods with a build tag for non-linux platforms in a new file.
2017-02-23 09:52:13 -08:00
46deb7b708 pkg/ns, pkg/types: refactored non linux build fix code to
Make GetCurrentNS platform specific instead of getCurrentThreadNetNSPath
2017-02-23 09:50:48 -08:00
0dc0845c51 pkg/ns, pkg/types: refactored non linux build fix code to
Make GetCurrentNS platform specific instead of getCurrentThreadNetNSPath
2017-02-23 09:50:48 -08:00
aafc728ec0 Merge pull request #371 from rosenhouse/spec-netconf-routes
spec: Remove `routes` from Network Configuration
2017-02-22 20:32:41 -06:00
32246b315c Replace Michael Bridgen with Bryan Boreham 2017-02-22 17:06:04 +00:00
2c33eb4f22 README: List multus as 3rd party plugin
refer #343 issue
2017-02-22 16:13:12 +00:00
9f03c4bc2e Merge pull request #376 from zackhsi/fix-grammar
Fix grammar
2017-02-20 17:51:38 -08:00
7fa16f18d9 plugins/main/ptp: set the Sandbox property on the response 2017-02-21 00:28:19 +01:00
c783679114 Fix grammar 2017-02-20 11:21:49 -08:00
dff808c98e pkg/ns: refactored so that builds succeed on non-linux platforms
moved functions that depend on linux packages (sys/unix) into a separate file
and added nop methods with a build tag for non-linux platforms in a new file.
2017-02-20 10:00:55 -08:00
c3cde44480 pkg/ns: refactored so that builds succeed on non-linux platforms
moved functions that depend on linux packages (sys/unix) into a separate file
and added nop methods with a build tag for non-linux platforms in a new file.
2017-02-20 10:00:55 -08:00
3ba89873ac Merge pull request #372 from rosenhouse/consolidate-host-local-docs
docs: consolidate host-local documentation
2017-02-20 06:33:32 -08:00
98eb0d93a1 docs: consolidate host-local documentation
- remove the README.md in the plugins/ipam/host-local (no other plugin
  has this)
- merge that info into the main Documentation/host-local.md
2017-02-15 15:20:13 -08:00
eb2ff7fe57 docs: consolidate host-local documentation
- remove the README.md in the plugins/ipam/host-local (no other plugin
  has this)
- merge that info into the main Documentation/host-local.md
2017-02-15 15:20:13 -08:00
31dafa2f7a spec: Remove routes from Network Configuration
- this is supported by some IPAM plugins but is not required of all of
  them
2017-02-15 15:02:23 -08:00
6f3b0abcc7 Merge pull request #369 from containernetworking/port-mapping
CONVENTIONS.md: Update details on port-mappings
2017-02-14 11:47:59 -08:00
e3da6e1cfb More markups 2017-02-13 10:14:45 -08:00
95ca9d393a Update with feedback 2017-02-09 14:12:33 -08:00
3fbf94a90a CONVENTIONS.md: Update details on port-mappings
After discussion with the CNI maintainers and representatives from Mesos and Kubernetes we've settled on this approach for passing the dynamic port mapping information from runtimes to plugins.

Including it in the plugin config allows "operators" to signal that they expect port mapping information to be passed to a specific plugin (or plugins). This follows the model that config passed in the plugin config should be acted upon by a plugin.

Runtimes can optionally return an error to users if they find no plugin in the chain that supports port mappings.

I'm sure we'd all like to see this merged soon so I'll try to turn any feedback quickly (and I'm happy to get any and all feedback - spellings, unclear working, wrong JSON terminology etc..)
2017-02-08 12:43:04 -08:00
970de4b9e3 Merge pull request #361 from ofiliz/issue-360
invoke: Enable plugin file names with extensions
2017-02-08 10:15:49 -08:00
f85ac247e6 Merge pull request #361 from ofiliz/issue-360
invoke: Enable plugin file names with extensions
2017-02-08 10:15:49 -08:00
a35f8ae914 invoke: Enable plugin file names with extensions
A CNI network configuration file contains the plugin's executable file name.
Some platforms like Windows require a file name extension for executables.
This causes unnecessary burden on admins as they now have to maintain two
versions of each type of netconfig file, which differ only by the ".exe"
extension. A much simpler design is for invoke package to also look for
well-known extensions on platforms that require it. Existing tests are
improved and new tests are added to cover the new behavior.

Fixes #360
2017-02-04 12:01:47 -08:00
271be6c8df invoke: Enable plugin file names with extensions
A CNI network configuration file contains the plugin's executable file name.
Some platforms like Windows require a file name extension for executables.
This causes unnecessary burden on admins as they now have to maintain two
versions of each type of netconfig file, which differ only by the ".exe"
extension. A much simpler design is for invoke package to also look for
well-known extensions on platforms that require it. Existing tests are
improved and new tests are added to cover the new behavior.

Fixes #360
2017-02-04 12:01:47 -08:00
fc7de21fe2 Merge pull request #364 from pires/build_tag
pkg/utils/sysctl/sysctl_linux.go: couple fixes
2017-02-03 14:18:39 -05:00
2e90a8cc33 pkg/utils/sysctl/sysctl_linux.go: fix typo. 2017-02-03 05:39:04 +00:00
4c0b8f7054 pkg/utils/sysctl/sysctl_linux.go: fix typo. 2017-02-03 05:39:04 +00:00
162b73e44f pkg/utils/sysctl/sysctl_linux.go: fix build tag. 2017-02-03 05:38:53 +00:00
246300ccfe pkg/utils/sysctl/sysctl_linux.go: fix build tag. 2017-02-03 05:38:53 +00:00
e663e98444 Merge pull request #359 from ofiliz/issue-355
libcni, pkg\invoke: Use OS-specific list separator when parsing CNI_PATH
2017-02-02 12:35:07 -08:00
46e17b26ff spec, libcni, pkg/invoke: Use OS-agnostic separator when parsing CNI_PATH
Hardcoding the list separator character as ":" causes CNI to fail when parsing
CNI_PATH on other operating systems. For example, Windows uses ";" as list
separator because ":" can legally appear in paths such as "C:\path\to\file".
This change replaces use of ":" with OS-agnostic APIs or os.PathListSeparator.

Fixes #358
2017-01-28 16:47:58 -08:00
0d9a1d70f8 spec, libcni, pkg/invoke: Use OS-agnostic separator when parsing CNI_PATH
Hardcoding the list separator character as ":" causes CNI to fail when parsing
CNI_PATH on other operating systems. For example, Windows uses ";" as list
separator because ":" can legally appear in paths such as "C:\path\to\file".
This change replaces use of ":" with OS-agnostic APIs or os.PathListSeparator.

Fixes #358
2017-01-28 16:47:58 -08:00
c4271dba67 Merge pull request #145 from dcbw/pass-ifname-back
Pass interface name and MAC back to runtime
2017-01-25 11:20:57 -08:00
06b397912b spec/plugins: return interface details and multiple IP addresses to runtime
Updates the spec and plugins to return an array of interfaces and IP details
to the runtime including:

- interface names and MAC addresses configured by the plugin
- whether the interfaces are sandboxed (container/VM) or host (bridge, veth, etc)
- multiple IP addresses configured by IPAM and which interface they
have been assigned to

Returning interface details is useful for runtimes, as well as allowing
more flexible chaining of CNI plugins themselves.  For example, some
meta plugins may need to know the host-side interface to be able to
apply firewall or traffic shaping rules to the container.
2017-01-25 11:31:18 -06:00
d5acb127b8 spec/plugins: return interface details and multiple IP addresses to runtime
Updates the spec and plugins to return an array of interfaces and IP details
to the runtime including:

- interface names and MAC addresses configured by the plugin
- whether the interfaces are sandboxed (container/VM) or host (bridge, veth, etc)
- multiple IP addresses configured by IPAM and which interface they
have been assigned to

Returning interface details is useful for runtimes, as well as allowing
more flexible chaining of CNI plugins themselves.  For example, some
meta plugins may need to know the host-side interface to be able to
apply firewall or traffic shaping rules to the container.
2017-01-25 11:31:18 -06:00
b89b56dc97 pkg/ipam: add testcases 2017-01-25 11:31:18 -06:00
befad17174 pkg/ipam: add testcases 2017-01-25 11:31:18 -06:00
e6adfe939a macvlan/ipvlan: use common RenameLink method 2017-01-25 11:31:18 -06:00
b0b896f79a plugins/flannel: organize test JSON alphabetically
Otherwise the test fails, since Go's JSON marshaller prints
dict items alphabetically in its String() call.
2017-01-25 11:31:18 -06:00
0d19b79260 types: make Result an interface and move existing Result to separate package 2017-01-25 11:31:18 -06:00
ad2a5ccb61 macvlan/ipvlan: use common RenameLink method 2017-01-25 11:31:18 -06:00
dae1177b53 testutils: pass netConf in for version operations; pass raw result out for tests 2017-01-25 11:31:18 -06:00
befb95977c types: make Result an interface and move existing Result to separate package 2017-01-25 11:31:18 -06:00
cb4cd0e12c testutils: pass netConf in for version operations; pass raw result out for tests 2017-01-25 11:31:18 -06:00
dcadf4f9b5 Merge pull request #355 from rosenhouse/skel-main-errors
skel: adds PluginMainWithError which returns a *types.Error
2017-01-23 07:42:57 -08:00
2515b8c712 skel: adds PluginMainWithError which returns a *types.Error
Enables plugins to do their own error-handling and cleanup logic
2017-01-22 16:57:42 -08:00
af5c2f303a skel: adds PluginMainWithError which returns a *types.Error
Enables plugins to do their own error-handling and cleanup logic
2017-01-22 16:57:42 -08:00
1acc927ab0 Merge pull request #352 from containernetworking/conventions
Documentation: Add conventions doc
2017-01-20 13:50:24 -08:00
04f357344d Update CONVENTIONS.md 2017-01-20 12:49:00 -08:00
e476ab7189 Merge pull request #346 from dcbw/ordered-chaining
api/plugins: implement ordered plugin chaining
2017-01-20 08:35:37 -08:00
13534e505c api,libcni: add network config list-based plugin chaining
Using a new ".configlist" file format that allows specifying
a list of CNI network configurations to run, add new libcni
helper functions to call each plugin in the list, injecting
the overall name, CNI version, and previous plugin's Result
structure into the configuration of the next plugin.
2017-01-19 22:28:21 -06:00
0c2a034f01 api,libcni: add network config list-based plugin chaining
Using a new ".configlist" file format that allows specifying
a list of CNI network configurations to run, add new libcni
helper functions to call each plugin in the list, injecting
the overall name, CNI version, and previous plugin's Result
structure into the configuration of the next plugin.
2017-01-19 22:28:21 -06:00
7e16c86a52 spec: add network configuration list specification 2017-01-19 22:28:20 -06:00
947b79e334 Spec/Conventions: Update to include plugin config 2017-01-19 18:05:28 -08:00
0a1b48f520 noop: allow specifying debug file in config JSON
Chaining sends different config JSON to each plugin, but the same
environment, and if we want to test multiple noop plugin runs in
the same chain we need a way of telling each run to use a different
debug file.
2017-01-19 09:56:42 -06:00
d716d73444 Documentation: Add conventions doc 2017-01-17 15:53:24 -08:00
530dd71e19 Merge pull request #334 from squeed/host-local-dns
ipam/host-local: add ResolvConf argument for DNS configuration
2017-01-12 17:06:04 -08:00
fa58dd8889 Merge pull request #350 from rosenhouse/spec-version
spec: notice of version
2017-01-12 13:42:25 -08:00
20fde00ea6 spec: notice of version 2017-01-11 18:49:10 -08:00
cd16252299 ipam/host-local: add ResolvConf argument for DNS configuration
This adds the option `resolvConf` to the host-local IPAM configuration.
If specified, the plugin will try to parse the file as a resolv.conf(5)
type file and return it in the DNS response.
2017-01-11 18:48:35 +01:00
5cde14cd7b ipam/host-local: add ResolvConf argument for DNS configuration
This adds the option `resolvConf` to the host-local IPAM configuration.
If specified, the plugin will try to parse the file as a resolv.conf(5)
type file and return it in the DNS response.
2017-01-11 18:48:35 +01:00
4406607649 Merge pull request #115 from aanm/implementing-ipam-as-lib
ipam/host-local: Move allocator and config to backend
2017-01-10 13:06:46 -08:00
f60111b093 ipam/host-local: Move allocator and config to backend
Signed-off-by: André Martins <aanm90@gmail.com>
2016-12-21 04:43:21 +00:00
98ff61aac3 Merge pull request #325 from squeed/update-maintainer
MAINTAINERS: hi CaseyC!
2016-12-19 11:02:20 -08:00
6dc01a772b MAINTAINERS: hi CaseyC! 2016-12-19 12:38:29 -05:00
e3bcc3e41b Merge pull request #345 from rosenhouse/travis-go-versions
travis: roll forward the versions of Go that we test
2016-12-16 08:06:17 -08:00
bc1d61e3db travis: roll forward the versions of Go that we test 2016-12-15 18:41:07 -08:00
652bae1deb Merge pull request #314 from rosenhouse/noop-helpful-message
plugins/noop: return a helpful message for test authors
2016-12-15 18:35:43 -08:00
47f6604b5a Merge pull request #344 from dcbw/interface-checks
all: assert internal objects implement interfaces
2016-12-15 18:34:57 -08:00
1c1cf2faf7 Merge branch 'master' into interface-checks 2016-12-15 18:20:34 -08:00
ef00f717a6 Merge pull request #341 from dcbw/host-local-trim
host-local: trim whitespace from container IDs and disk file contents
2016-12-15 18:18:25 -08:00
06fd1369aa all: assert internal objects implement interfaces 2016-12-14 17:09:01 -06:00
5852c60bc4 all: assert internal objects implement interfaces 2016-12-14 17:09:01 -06:00
20ae9e156d Merge pull request #340 from vzcambria/master
Fix README.md host-local IPAM range start/end typo
2016-12-09 18:22:08 -06:00
76028d7f8c host-local: trim whitespace from container IDs and disk file contents
It doesn't seem like container IDs should really have whitespace or
newlines in them.  As a complete edge-case, manipulating the host-local
store's IP reservations with 'echo' puts a newline at the end, which
caused matching to fail in ReleaseByID().  Don't ask...
2016-12-09 18:16:38 -06:00
2ba6d20007 Fix README.md host-local IPAM range start/end typo 2016-12-06 10:40:55 -05:00
accac60053 plugins/noop: return a helpful message for test authors
Signed-off-by: Gabe Rosenhouse <grosenhouse@pivotal.io>
2016-12-02 15:04:01 -08:00
b0808a006a Merge pull request #335 from JulienBalestra/json_ext
libcni conf: support .json file extension
2016-12-02 13:33:36 -06:00
a85593ce0d libcni conf: support .json file extension
Content of the files are json, the configuration load should support .json
2016-11-23 10:48:32 +01:00
de002ff25a Merge pull request #322 from cf-container-networking/flannel-cni-stateDir
Allow flannel CNI plugin stateDir to be configurable
2016-11-18 15:27:35 -06:00
475581873d flannel: rename stateDir to dataDir
Rename StateDir to DataDir for flannel CNI plugin
2016-11-17 15:54:29 -06:00
6eac0ee904 flannel: rename stateDir to dataDir
Rename StateDir to DataDir for flannel CNI plugin
2016-11-17 15:54:29 -06:00
46e3a6b7be Merge pull request #330 from bboreham/ns-warning
Expand description of goroutine+netns problems
2016-11-17 08:00:31 -08:00
44ef109483 Expand description of goroutine+netns problems 2016-11-17 15:45:38 +00:00
c20f895fa8 Expand description of goroutine+netns problems 2016-11-17 15:45:38 +00:00
ba6c5af5dc Merge pull request #327 from dcbw/host-local-tests
host-local: add DataDir IPAM conf option and use it for testcases
2016-11-14 08:17:40 -08:00
15de81eac6 flannel: updated flannel test to use pkg/testutils
reworked the flannel test to use testutils CmdAddWithResult and
CmdDelWithResult
2016-11-13 12:16:32 -06:00
8b827d74b3 Merge pull request #328 from markstgodard/noop-cni-args
noop plugin: support multiple CNI_ARGS
2016-11-10 22:24:58 -08:00
48d3b46eb2 noop plugin: support multiple CNI_ARGS
Updated noop plugin to parse the CNI_ARGS pairs, to allow more than just
the DEBUG arg.
2016-11-10 16:04:42 -06:00
7b2006eed8 Merge pull request #324 from lstoll/lstoll-ipvlan-l3s
ipvlan: Support L3-Symmetric mode
2016-11-10 12:53:59 -06:00
e085226f82 host-local: add DataDir IPAM conf option and use it for testcases
Add an e2e host-local plugin testcase, which requires being able
to pass the datadir into the plugin so we can erase it later.
We're not always guaranteed to have access to the default data
dir location, plus it should probably be configurable anyway.
2016-11-10 12:12:16 -06:00
ffbf708406 flannel: update flannel documentation
add optional 'stateDir' to flannel docs
2016-11-09 09:34:35 -06:00
e6113c6517 flannel: update flannel documentation
add optional 'stateDir' to flannel docs
2016-11-09 09:34:35 -06:00
79643855f7 Merge pull request #307 from squeed/backwards-compatability-tests
testing: test invocation of newer plugins with an older libcni
2016-11-07 14:25:09 -08:00
f0daefa63d flannel: add unit tests for config loading
backfill unit tests to add coverage for loadFlannelNetConf and
loadFlannelSubnetEnv
2016-11-05 23:27:14 -05:00
9201f3f1d9 ipvlan: Support ipvlan l3s mode.
This landed in kernel in 4fbae7d
2016-11-05 13:41:07 -07:00
2ad1d04561 vendor: github.com/vishvananda/netlink@a1f8555
Update to current master, to get IPVLAN L3s def.
2016-11-05 13:41:01 -07:00
fa264e6e36 flannel: add integration test suite
add new test for flannel plugin that delegates to the noop plugin and
validates that 'storeDir' can be configurable
2016-11-05 10:47:07 -05:00
b463642ac0 Allow flannel CNI plugin stateDir to be configurable
- Add optional 'stateDir' to flannel NetConf, if not present default to
/var/lib/cni/flannel

Signed-off-by: Jay Dunkelberger <ldunkelberger@pivotal.io>
2016-11-03 17:10:13 -05:00
61d3f4515a testing: test invocation of newer plugins with an older libcni 2016-10-25 16:33:35 +02:00
8c6f6e0a60 testing: test invocation of newer plugins with an older libcni 2016-10-25 16:33:35 +02:00
0e09ad29df Merge pull request #312 from tomdee/slack
Add slack badge and link
2016-10-24 16:25:48 -07:00
bb8bcf08a8 Add slack badge and link 2016-10-24 16:23:04 -07:00
194f8ce6ea Merge pull request #311 from rkamudhan/patch-2
Update ipvlan.md
2016-10-23 16:23:55 -07:00
17b2a6d5e2 Update ipvlan.md
invalid json format
2016-10-23 23:18:44 +01:00
6e77c7f4f7 Update ipvlan.md
invalid json format
2016-10-23 23:18:44 +01:00
bff05e7572 Merge pull request #308 from rosenhouse/fix-skel-tests-containerid
skel tests: correct name of CNI_CONTAINERID in tests of env vars
2016-10-17 15:02:27 -07:00
cfb2ff3dcd skel tests: correct name of CNI_CONTAINERID in tests of env vars 2016-10-16 17:18:57 -07:00
8be31e1c6f skel tests: correct name of CNI_CONTAINERID in tests of env vars 2016-10-16 17:18:57 -07:00
3450f08611 Merge pull request #305 from yuewko/add_infoblox_link
Add link to Infoblox IPAM plugin
2016-10-16 17:08:51 -07:00
c24fce2ae8 Add link to Infoblox IPAM plugin
Added link to Infoblox IPAM plugin in README.
2016-10-11 11:07:30 -07:00
d872391998 Merge pull request #304 from danp/patch-1
docs/ptp: fix markdown formatting
2016-10-09 22:39:31 -07:00
0ee270c769 docs/ptp: fix markdown formatting 2016-10-09 21:22:27 -03:00
85ab5c0f21 docs/ptp: fix markdown formatting 2016-10-09 21:22:27 -03:00
af88b7c6cf Merge pull request #301 from rosenhouse/backwards-compatibility-tests
A test verifies that libcni is backwards compatible with legacy plugins
2016-10-06 10:10:02 -05:00
165ca6f719 travis: git clone at great depth
ensures that git commits referenced by backwards compatibility tests are
always available in CI.
2016-10-05 20:48:53 -07:00
0087f302ea Merge pull request #302 from rosenhouse/readme-3rd-party-plugins
3rd party plugins belong in their own repos
2016-10-03 08:39:23 -07:00
b2ef4aa1a7 docs: on contributing plugins to this repo 2016-10-02 23:44:59 -07:00
b07b226c97 testing: adds basic test of backwards compatibility 2016-10-02 22:18:53 -07:00
51b51a0182 testing: adds basic test of backwards compatibility 2016-10-02 22:18:53 -07:00
a29fc24f11 Merge pull request #296 from rosenhouse/plugins-require-versioned-config
Plugins validate cniVersion of NetConf
2016-09-22 17:11:34 -05:00
b5e0a4ef2e Merge pull request #298 from freehan/fix-host-local-30
host-local: fix allocation of last address in a /30
2016-09-22 09:13:32 -05:00
52e4358cbd host-local: fix allocation of last address in a /30 2016-09-21 13:10:05 -07:00
e70e6034d2 Merge pull request #299 from tgraf/cilium-link
List cilium as 3rd party plugin
2016-09-19 14:31:54 -07:00
f721eaf6ed skel: use named constant for Incompatible CNI Version error code 2016-09-19 13:25:33 -07:00
5b696f3307 skel: use named constant for Incompatible CNI Version error code 2016-09-19 13:25:33 -07:00
0135e2751e versioning: ipam config is not versioned 2016-09-19 13:25:28 -07:00
04ba7c7761 versioning: plugins require version match with config
infer version 0.1.0 when config is missing an explicit "cniVersion" field
2016-09-19 13:00:49 -07:00
fba37620e0 versioning: plugins require version match with config
infer version 0.1.0 when config is missing an explicit "cniVersion" field
2016-09-19 13:00:49 -07:00
1e4e8fdb5d versioning: add basic version decode for network config 2016-09-19 12:57:08 -07:00
64bbcd8cf7 versioning: add basic version decode for network config 2016-09-19 12:57:08 -07:00
75d983ec25 README: List cilium as 3rd party plugin
Signed-off-by: Thomas Graf <tgraf@suug.ch>
2016-09-19 13:45:39 +02:00
921292ff77 skel: Plugins require a cniVersion in the NetConf 2016-09-18 21:30:57 -07:00
fd150a4c97 skel: Plugins require a cniVersion in the NetConf 2016-09-18 21:30:57 -07:00
56032390fe Merge pull request #287 from rosenhouse/multi-version
Plugins report a list of supported versions
2016-09-12 13:44:14 -07:00
3f4011b3ec Merge pull request #295 from rosenhouse/update-readme-cloudfoundry-link
readme: update link to CloudFoundry usage of CNI
2016-09-08 00:47:04 -04:00
d32ccb1899 readme: update link to CloudFoundry usage of CNI 2016-09-06 23:48:48 -04:00
de91f31ae7 versioning: revert spec version to 0.2.0 2016-09-06 15:37:42 -04:00
7958b9f0cc versioning: revert spec version to 0.2.0 2016-09-06 15:37:42 -04:00
bf6948da19 versioning: misc cleanups
highlights:
 - NetConf struct finally includes cniVersion field
 - improve test coverage of current version report behavior
 - godoc a few key functions
 - allow tests to control version list reported by no-op plugin
2016-09-06 15:35:58 -04:00
d5e2e375d4 versioning: misc cleanups
highlights:
 - NetConf struct finally includes cniVersion field
 - improve test coverage of current version report behavior
 - godoc a few key functions
 - allow tests to control version list reported by no-op plugin
2016-09-06 15:35:58 -04:00
07a8a28637 Merge pull request #294 from dcbw/host-local-ranges
Fix handling of host-local IP address ranges
2016-09-05 23:24:42 -04:00
95a9ea0bd2 host-local: don't allocate the broadcast address or allow invalid networks
There aren't any IPs to allocate in /32 or /31 networks, so don't allow them.
2016-09-02 17:20:09 -05:00
959af1e6ab host-local: ensure requested IPs are within the given range
And also make sure that RangeStart and RangeEnd are sane.
2016-09-02 16:52:29 -05:00
dfc4f7cd2a host-local: don't allocate past RangeEnd
When RangeEnd is given, a.end = RangeEnd+1.

If when getSearchRange() is called and lastReservedIP equals
RangeEnd, a.nextIP() only compares lastReservedIP (which in this
example is RangeEnd) against a.end (which in this example is
RangeEnd+1) and they clearly don't match, so a.nextIP() returns
start=RangeEnd+1 and end=RangeEnd.

Get() happily allocates RangeEnd+1 because it only compares 'cur'
to the end returned by getSearchRange(), not to a.end, and thus
allocates past RangeEnd.

Since a.end is inclusive (eg, host-local will allocate a.end) the
fix is to simply set a.end equal to RangeEnd.
2016-09-02 16:37:34 -05:00
2f97407396 invoke: better name and unit test coverage for GetVersionInfo 2016-09-02 16:39:01 -04:00
bf31ed1591 invoke: better name and unit test coverage for GetVersionInfo 2016-09-02 16:39:01 -04:00
f467134838 invoke: correctly infer version for 0.1.0-vintage plugins
Older plugins return a known error when issued the VERSION command.
Capture this error and report it as a 0.1.0 version plugin.
2016-09-02 16:02:07 -04:00
97192fc979 invoke: correctly infer version for 0.1.0-vintage plugins
Older plugins return a known error when issued the VERSION command.
Capture this error and report it as a 0.1.0 version plugin.
2016-09-02 16:02:07 -04:00
2142c076ab versioning: adds tooling to compile a program against a given old CNI version
Allows us to write tests that cover interactions between components of
differing versions
2016-09-02 16:02:02 -04:00
deb4466041 versioning: adds tooling to compile a program against a given old CNI version
Allows us to write tests that cover interactions between components of
differing versions
2016-09-02 16:02:02 -04:00
bd3ade0c54 pkg/invoke: refactor plugin exec and backfill unit tests 2016-09-02 15:59:25 -04:00
dea1c6e44d pkg/invoke: refactor plugin exec and backfill unit tests 2016-09-02 15:59:25 -04:00
383c84031e versioning: document meaning of 'Legacy' version support 2016-09-02 11:26:17 -04:00
adf28a84c6 versioning: document meaning of 'Legacy' version support 2016-09-02 11:26:17 -04:00
b1e254d901 versioning: plugins report a list of supported versions
Further progress on versioning support (Issue #266).
Bump CNI spec version to 0.3.0
2016-09-02 11:26:17 -04:00
536cb5b99b versioning: plugins report a list of supported versions
Further progress on versioning support (Issue #266).
Bump CNI spec version to 0.3.0
2016-09-02 11:26:17 -04:00
c5e39a87f7 Merge pull request #293 from rosenhouse/backfill-invoke-exec-tests
Backfill tests for plugin execution
2016-08-31 20:49:08 -04:00
63ace9e496 Merge pull request #293 from rosenhouse/backfill-invoke-exec-tests
Backfill tests for plugin execution
2016-08-31 20:49:08 -04:00
e0ea82b229 Merge pull request #290 from asridharan/dcos
Fixed the check for network namespace path.
2016-08-31 18:19:02 -05:00
26241a517a Merge pull request #290 from asridharan/dcos
Fixed the check for network namespace path.
2016-08-31 18:19:02 -05:00
7281d5792a pkg/ns: fixed the check for network namespace path.
The expectation on older kernels (< 3.19) was to have the network
namespace always be a directory. This is not true if the network
namespace is bind mounted to a file, and will make the plugin fail
erroneously in such cases. The fix is to remove this assumption
completely and just do a basic check on the file system types being
returned.

Fixes #288
2016-08-31 21:41:00 +00:00
1ce5e54e99 pkg/ns: fixed the check for network namespace path.
The expectation on older kernels (< 3.19) was to have the network
namespace always be a directory. This is not true if the network
namespace is bind mounted to a file, and will make the plugin fail
erroneously in such cases. The fix is to remove this assumption
completely and just do a basic check on the file system types being
returned.

Fixes #288
2016-08-31 21:41:00 +00:00
4d77bc177c Merge pull request #291 from tomdee/SetupVeth
pkg/ip: Ensure that SetupVeth returns correct hostVeth
2016-08-31 16:32:09 -05:00
31ef82276a pkg/ip: Ensure that SetupVeth returns correct hostVeth
The veth is moved from the container NS to the host NS.
This is handled by the code that sets the link to UP but the wrong
hostVeth is returned to the calling code.
2016-08-31 14:17:25 -07:00
726c6b4578 pkg/ip: Ensure that SetupVeth returns correct hostVeth
The veth is moved from the container NS to the host NS.
This is handled by the code that sets the link to UP but the wrong
hostVeth is returned to the calling code.
2016-08-31 14:17:25 -07:00
d3ecadb860 invoke: backfill tests for plugin execution 2016-08-30 23:52:47 -04:00
0e675fa14f invoke: backfill tests for plugin execution 2016-08-30 23:52:47 -04:00
9d5e6e60e7 Merge pull request #241 from luxas/add_platforms
Add arm64 and ppc64le as new platforms
2016-08-13 15:11:28 -07:00
39f8436a8b release: Release binaries for arm, arm64 and ppc64le too
Modify the releasing script to cross-compile for the new architectures, but also keep backwards-compability

ref #209
2016-08-13 12:45:04 +03:00
21d25959ee travis: Cross-compile for arm, arm64 and ppc64le on every build
Cross-compile cni for arm, arm64 and ppc64le with go1.6 only
Allow go tip to fail
Set fast_finish to true, which means travis will instantly return build failure when any of the required builds fail

ref #209
2016-08-13 12:44:45 +03:00
fc58b79b45 build: Update the build script to make it possible to build for other architectures
This makes it possible to cross-compile cni like so:
$ GOARCH=arm ./build
$ GOARCH=arm64 ./build
$ GOARCH=ppc64le ./build

ref #209
2016-08-13 12:44:28 +03:00
488db88fd1 vendor: Bump golang.org/x/sys/unix to a newer version
The current vendor of sys/unix is really old, and doesn't work on arm64 and ppc64le
Updating to the latest version might also fix other issues

ref #209
2016-08-13 12:44:04 +03:00
787d59ab5d Merge pull request #267 from rosenhouse/version-cmd
Adds VERSION command
2016-08-12 15:06:22 -07:00
415d707faf Merge pull request #267 from rosenhouse/version-cmd
Adds VERSION command
2016-08-12 15:06:22 -07:00
349d66d51c Merge pull request #267 from rosenhouse/version-cmd
Adds VERSION command
2016-08-12 15:06:22 -07:00
5a67f5d6fb Merge pull request #273 from zreigz/reconfigure-bridge-ip
Reconfigure bridge IP address
2016-08-12 15:04:22 -07:00
60bb795c2e Merge pull request #274 from lukasredynk/update_netlink
Update of netlink version
2016-08-12 13:07:26 -07:00
113dfd61bd Merge pull request #280 from prateekgogia/bug-fix-155
pkg/ip: Return correct error if container name provided exists, and test cases
2016-08-12 13:04:49 -07:00
3c1d4581c4 pkg/ip: Return correct error if container name provided exists, and test cases
If interface name for a container provided by a user is already present,
Veth creation fails with incorrect error.
If os.IsExist error is returned by makeVethPair:
* Check for peer name, if exists generate another random peer name,
* else, IsExist error is due to container interface present, return error.

Fixes #155
2016-08-11 18:51:03 +00:00
ad8a052dd3 pkg/ip: Return correct error if container name provided exists, and test cases
If interface name for a container provided by a user is already present,
Veth creation fails with incorrect error.
If os.IsExist error is returned by makeVethPair:
* Check for peer name, if exists generate another random peer name,
* else, IsExist error is due to container interface present, return error.

Fixes #155
2016-08-11 18:51:03 +00:00
8cb3a9323a libcni: add util function InjectConf 2016-08-08 13:21:25 +02:00
7c579af789 Merge pull request #277 from steveeJ/test-allocator-reserveip
ipam/host-local/allocator tests: cover requested IP
2016-08-02 14:43:43 -07:00
08529639f4 documentation: add description for forceAddress parameter 2016-08-02 07:33:44 +02:00
ee64ac74bc documentation: add description for forceAddress parameter 2016-08-02 07:33:44 +02:00
d09b18dac4 plugins: reconfigure bridge IP address
Add possibility to reconfigure bridge IP address when there is a new value.
New boolean flag added to net configuration to force IP change if it is need.
Otherwise code behaves as previously and throws error
2016-08-02 07:33:17 +02:00
054fa9e42d ipam/host-local/allocator tests: cover requested IP
Further:
* improve error handling
2016-08-01 18:13:15 -07:00
f5ead7969a Merge pull request #254 from steveeJ/ip-based-macaddr
pkg/ipam: IP based macaddr
2016-08-01 17:43:49 -07:00
22a0ddc906 pkg/utils/hwaddr tests: cover v4 in v6 addr 2016-08-01 16:50:40 -07:00
c0ac3913a1 pkg/utils/hwaddr tests: cover v4 in v6 addr 2016-08-01 16:50:40 -07:00
35ce5d68e0 Update of netlink version
This is required for
https://github.com/containernetworking/cni/issues/251 because version of
netlink used doesn't support creating tap devices.
2016-08-01 14:04:03 +02:00
c1ff202179 Merge pull request #272 from rosenhouse/types-of-users
README: distinguish between runtimes and plugins
2016-07-25 16:35:58 -07:00
5a52316ab5 plugins/{bridge,macvlan} test: ensure hardware addr
* bridge: Test the following interface's hardware address for the CNI specific
prefix:
  - bridge with IP address
  - container veth
* plugins/macvlan test: ensure hardware addr
2016-07-22 15:40:00 -07:00
00f27577d5 pkg/ip link_test: ensure SetHWAddrByIP has an effect 2016-07-22 15:34:54 -07:00
f68cea27bc pkg/ip link_test: ensure SetHWAddrByIP has an effect 2016-07-22 15:34:54 -07:00
55fd81f775 plugins/ptp: set the host veth hwaddr correctly 2016-07-22 15:34:54 -07:00
c317888ac5 pkg/ip: use iface name in SetHWAddrByIP 2016-07-22 15:34:53 -07:00
7d19c01818 pkg/ip: use iface name in SetHWAddrByIP 2016-07-22 15:34:53 -07:00
1a04d9abba pkg/ip tests: cover SetupVeth and DelLinkByName* 2016-07-22 15:34:53 -07:00
8e1c215116 pkg/ip tests: cover SetupVeth and DelLinkByName* 2016-07-22 15:34:53 -07:00
b6137de993 plugins: set MAC addresses based on IP
This will give deterministic MAC addresses for all interfaces CNI
creates and manages the IP for:
* bridge: container veth and host bridge
* macvlan: container veth
* ptp: container veth and host veth
2016-07-22 15:34:53 -07:00
924b30b57d plugins: set MAC addresses based on IP
This will give deterministic MAC addresses for all interfaces CNI
creates and manages the IP for:
* bridge: container veth and host bridge
* macvlan: container veth
* ptp: container veth and host veth
2016-07-22 15:34:53 -07:00
c3e8563e77 pkg/utils/hwaddr: migrate code from IPAM pkg 2016-07-22 15:18:38 -07:00
3a1354cff6 pkg/utils/hwaddr: migrate code from IPAM pkg 2016-07-22 15:18:38 -07:00
7ea07a6405 pkg/ipam{,test}: improve error handling and tests
* _suite.go and _test.go file should be in the same package, using the
  _test package for that, which requires some fields and methods to be
  exported
* Introduce error type for cleaner error handling
* test adaptions for error type checking
2016-07-22 15:17:22 -07:00
96867eae05 pkg/ipam{,test}: improve error handling and tests
* _suite.go and _test.go file should be in the same package, using the
  _test package for that, which requires some fields and methods to be
  exported
* Introduce error type for cleaner error handling
* test adaptions for error type checking
2016-07-22 15:17:22 -07:00
75f1fa2260 add tests for generateHardwardAddr 2016-07-22 15:01:57 -07:00
1c1424c472 add tests for generateHardwardAddr 2016-07-22 15:01:57 -07:00
c19b82efc2 configure mac address based on assigned ip 2016-07-22 14:57:52 -07:00
b2e53181d1 configure mac address based on assigned ip 2016-07-22 14:57:52 -07:00
39a8fe4766 Merge pull request #270 from rosenhouse/integration-tests
Add integration tests of libcni
2016-07-22 13:36:20 -07:00
c657c61767 plugins: cleanup tests of no-op plugin 2016-07-22 13:30:55 -07:00
424a8b38cf README: distinguish between runtimes and plugins
Adds a link to the 3rd party SR-IOV plugin.
2016-07-22 13:22:35 -07:00
04fbde4ce8 docs: fix tuning doc, correct the result json
it does not report the version, it never did
2016-07-21 13:06:05 -07:00
dfd2c142ae docs: fix tuning doc, correct the result json
it does not report the version, it never did
2016-07-21 13:06:05 -07:00
7bc96f05bb spec: add version command and increment version to 0.2.0 2016-07-21 12:59:08 -07:00
0506bae8fc spec: remove CNI_VERSION env var, was never been implemented
No one uses this, and it is causing confusion (see #150)
2016-07-20 13:39:51 -07:00
bacdc3668d libcni: add integration test coverage 2016-07-15 13:01:08 -07:00
6496749565 plugins: adds new no-op plugin that may be used as a test-double
Plugin can be configured to record all inputs and to respond with
arbitrary stdout or error message.  Will support upcoming integration
testing.
2016-07-15 12:48:02 -07:00
5835c2bbb1 plugins: adds new no-op plugin that may be used as a test-double
Plugin can be configured to record all inputs and to respond with
arbitrary stdout or error message.  Will support upcoming integration
testing.
2016-07-15 12:48:02 -07:00
7f098f7c4a Merge pull request #269 from rosenhouse/skel-dependency-injection
Refactor skel with dependency injection
2016-07-15 12:45:18 -07:00
728071e7d1 misc: fix up copyright dates 2016-07-14 16:09:27 -07:00
a2aff8c6a8 misc: fix up copyright dates 2016-07-14 16:09:27 -07:00
f484990a00 Merge pull request #263 from feiskyer/ptp-test
plugins/main/ptp: add functional tests
2016-07-14 14:32:05 -06:00
30c99d17cc pkg/skel: plugins now respond to VERSION command
To support CNI spec versioning, plugins must be able to report version
information to container runtimes.
2016-07-14 00:24:32 -04:00
63755d7c57 pkg/skel: plugins now respond to VERSION command
To support CNI spec versioning, plugins must be able to report version
information to container runtimes.
2016-07-14 00:24:32 -04:00
eda79f7645 pkg/skel: improve error message for missing CNI_COMMAND env var
This makes the error message for missing CNI_COMMAND consistent with
that of other required environment variables.
2016-07-14 00:24:32 -04:00
f436418525 pkg/skel: improve error message for missing CNI_COMMAND env var
This makes the error message for missing CNI_COMMAND consistent with
that of other required environment variables.
2016-07-14 00:24:32 -04:00
f2b6ec0375 pkg/skel: missing env var log lines appear in stderr
Previously, the log lines appeared in stdout before the JSON encoding of
the error message.  That would break JSON parsing of stdout.  Instead, we use
stderr for these unstructured logs, consistent with the CNI spec.
2016-07-14 00:24:27 -04:00
c17e700759 pkg/skel: missing env var log lines appear in stderr
Previously, the log lines appeared in stdout before the JSON encoding of
the error message.  That would break JSON parsing of stdout.  Instead, we use
stderr for these unstructured logs, consistent with the CNI spec.
2016-07-14 00:24:27 -04:00
3c43314926 pkg/skel: refactor to use dependency injection
Extract dependencies on os to enable more complete unit test coverage
2016-07-14 00:06:58 -04:00
791d259e55 pkg/skel: refactor to use dependency injection
Extract dependencies on os to enable more complete unit test coverage
2016-07-14 00:06:58 -04:00
cd14fc0e06 plugins/main/ptp: add functional tests 2016-07-11 09:25:36 +08:00
8a93673209 Merge pull request #264 from rosenhouse/test-with-vagrant
Add Vagrantfile, document how to run test suite in a vagrant VM
2016-07-06 11:33:32 -07:00
0c2b5f5139 Add Vagrantfile, document how to run test suite in a vagrant VM 2016-07-05 23:12:02 -07:00
b8e92ed030 Merge pull request #247 from tomdee/spec-args-field
SPEC: introduce "args" field and new error code
2016-06-14 15:31:23 -07:00
7b381a1af2 SPEC: introduce "args" field and new error code
Based on previous discussions on the CNI maintainers calls, the spec is
unclear on 1) when CNI_ARGS should be used and 2) the fact the dynamic
config can be passed in through the network JSON.

This PR makes it clear that per-container config can be passed in
through the network JSON, adding a top level `args` field into
which orchestrators can add additional metadata without worrying that
plugins might reject the additional data. It also allows for plugins to
reject unknown fields passed in at the top level.

Using JSON is preferable to CNI_ARGS since it allows namespaced and
structured data. CNI_ARGS is a flat list of KV pairs which has reserved
characters with no escaping rules defined.
CNI_ARGS may still be used by orchestrators that want the simplicity of
passing the network config JSON as specified by the user, unchanged
through to the CNI plugin. But for any kind of structured data, it's
recommended that the `args` field in the JSON is used instead.
2016-06-14 15:02:51 -07:00
4666a24b6d README: grammatical corrections 2016-06-14 09:54:15 -07:00
bd341ab5d1 pkg/ns: fix misspelling in comment 2016-06-14 09:52:58 -07:00
0136a73037 pkg/ns: fix misspelling in comment 2016-06-14 09:52:58 -07:00
3bee295ba5 Merge pull request #240 from steveeJ/unmarshall-string
pkg/types: unmarshal string tests
2016-06-13 18:30:24 -07:00
d48cb53715 pkg/types: cover string for unmarshal tests 2016-06-13 18:18:36 -07:00
3476000725 pkg/types: cover string for unmarshal tests 2016-06-13 18:18:36 -07:00
17dcf87589 Merge pull request #238 from tomdee/string-unmarshaller
pkg/types: Add UnmarshallableString type
2016-06-13 18:17:48 -07:00
c3c0803817 Merge pull request #243 from containernetworking/maintainers-update-email
MAINTAINERS: Update @tomdee email address
2016-06-10 01:41:42 +02:00
0ffad38994 MAINTAINERS: Update @tomdee email address 2016-06-09 16:32:26 -07:00
efc0f2987f pkg/types: Add UnmarshallableString type
Allow strings to be unmarshalled for CNI_ARGS

CNI_ARGS uses types.LoadArgs to populate a struct.
The fields in the struct must meet the TextUnmarshaler interface.

This code adds a UnmarshallableString type to assist with this.
2016-06-09 14:45:58 -07:00
031567c216 pkg/types: Add UnmarshallableString type
Allow strings to be unmarshalled for CNI_ARGS

CNI_ARGS uses types.LoadArgs to populate a struct.
The fields in the struct must meet the TextUnmarshaler interface.

This code adds a UnmarshallableString type to assist with this.
2016-06-09 14:45:58 -07:00
c6864e1060 Merge pull request #229 from steveeJ/cleanup-travis
travis: cleanup and bump go versions
2016-06-02 20:43:11 +02:00
2445a960a9 plugins/ipam: round robin ip allocation for host-local ipam
This changes the ip allocation logic to round robin. Before this, host-local IPAM searched for available IPs from start of subnet. Hence it tends to allocate IPs that had been used recently. This is not ideal since it may cause collisions.
2016-06-02 20:37:05 +02:00
5c3c171642 Merge pull request #239 from steveeJ/master
MAINTAINERS: remove @zachgersh, add @dcbw
2016-06-02 09:00:07 -07:00
4a292cd451 Merge pull request #218 from steveeJ/roadmap
Add ROADMAP document
2016-06-02 16:51:57 +01:00
f82af22b10 ROADMAP: initial version 2016-06-02 17:50:14 +02:00
4f9faf60bb MAINTAINERS: remove @zachgersh, add @dcbw
Thank you Zach for all the great work done on CNI, farewell!
At the same time we are happy to welcome Dan amongst us who has already
contributed lots of valuable work!
2016-06-02 01:15:24 +02:00
f44f4cf953 Merge pull request #219 from squaremo/more-contrib
docs: details on PR acceptance policy
2016-06-01 16:03:17 +02:00
ddc0ca4791 docs: details on PR acceptance policy 2016-06-01 12:23:21 +01:00
e90d8c12b1 Merge pull request #228 from jieyu/add_mesos
Added Mesos to CNI users.
2016-05-30 11:13:34 +01:00
ffff8ac2fd Merge pull request #231 from steveeJ/ns-verifiy-errors
pkg/ns: introduce error types to indicate NS verification
2016-05-27 15:57:40 +02:00
3a4124d257 pkg/ns: introduce error types indicate NS verification 2016-05-27 13:50:16 +02:00
35f3a090b2 pkg/ns: introduce error types indicate NS verification 2016-05-27 13:50:16 +02:00
131ecc4055 Merge pull request #230 from steveeJ/netns-optional-on-del
plugins: don't require CNI_NETNS for DEL command
2016-05-27 13:49:05 +02:00
cfedcfd8f2 skel/test: add case for empty NETNS 2016-05-27 12:26:42 +02:00
d582c9ce8f skel/test: add case for empty NETNS 2016-05-27 12:26:42 +02:00
5fe036b88a plugins: don't require CNI_NETNS for DEL command
This will allow to free up the IPAM allocations when the caller doesn't
have access to the network namespace anymore, e.g. due to a reboot.
2016-05-27 10:57:39 +02:00
72337159c1 plugins: don't require CNI_NETNS for DEL command
This will allow to free up the IPAM allocations when the caller doesn't
have access to the network namespace anymore, e.g. due to a reboot.
2016-05-27 10:57:39 +02:00
d7de8d4f98 pkg/skel: allow arg requriements specified by CMD 2016-05-27 10:56:24 +02:00
7f90f9d559 pkg/skel: allow arg requriements specified by CMD 2016-05-27 10:56:24 +02:00
d6674e0f49 README: add Mesos as CNI users 2016-05-26 09:42:10 -07:00
3b5d7a3bb0 travis: use go1.5.4 2016-05-26 15:33:58 +02:00
572fcc7076 travis: cleanup and bump go versions
* cleanup structure
* explicitly use go-{1.5.3,1.6.2}
2016-05-26 14:43:18 +02:00
6f63d9d707 Merge pull request #227 from steveeJ/ns-verify
pkg/ns: consider PROCFS during NS verification
2016-05-26 13:22:29 +02:00
2d47b0396d pkg/ns: consider PROCFS during NS verification
This is an attempt to bring compatibility with Kernel <3.19, where NSFS
where PROCFS was used for network namespaces.
2016-05-26 12:42:50 +02:00
3bab8a2805 pkg/ns: consider PROCFS during NS verification
This is an attempt to bring compatibility with Kernel <3.19, where NSFS
where PROCFS was used for network namespaces.
2016-05-26 12:42:50 +02:00
ea073fc4c7 pkg/ns: test IsNSFS() 2016-05-24 22:30:49 +02:00
f64cfb84f8 pkg/ns: test case for rejecting a non-ns nspath 2016-05-24 22:30:49 +02:00
39eace3c38 pkg/ns: verify netns when initialized with GetNS 2016-05-24 22:30:49 +02:00
a8d1029294 pkg/ns: add tests cases for Close()'d NS 2016-05-24 21:15:51 +02:00
40440ec887 pkg/ns: don't allow operations after Close() 2016-05-24 20:52:00 +02:00
8814631f80 bridge: add isDefaultGateway field
When isDefaultGateway is true it automatically sets isGateway to true.
The default route will be added via the (bridge's) gateway IP.
If a default gateway has been configured via IPAM in the same
configuration file, the plugin will error out.
2016-05-21 00:38:42 +02:00
fce81fbf78 testutils: add e2e testing helper code 2016-05-20 17:10:25 -05:00
f2922c5c1f ns: add interface, use it, and fix thread-related namespace switch issues
Add a namespace object interface for somewhat cleaner code when
creating and switching between network namespaces.  All created
namespaces are now mounted in /var/run/netns to ensure they
have persistent inodes and paths that can be passed around
between plugin components without relying on the current namespace
being correct.

Also remove the thread-locking arguments from the ns package
per https://github.com/appc/cni/issues/183 by doing all the namespace
changes in a separate goroutine that locks/unlocks itself, instead of
the caller having to track OS thread locking.
2016-05-20 17:10:25 -05:00
c019ed25d2 Merge pull request #199 from dcbw/fix-bridge-tx-queue-length
bridge: leave TX queue length as kernel default, not 0
2016-05-19 23:05:08 +02:00
3738692372 Merge pull request #200 from squaremo/simplify-readme
Simplify README preamble
2016-05-19 13:21:40 +01:00
0bd4bafcda Merge pull request #175. 2016-05-19 12:08:41 +02:00
734bf1ba5b pkg/ns: evaluate syscall number at compile-time
Previously this code used a run-time map lookup keyed by
runtime.GOOS/GOARCH.  This version uses conditional compilation to make
this choice at compile time, giving immediate feedback for unsupported
platforms.
2016-05-19 12:06:37 +02:00
5de15979c6 plugins/bridge: add support to set hairpin mode 2016-05-19 12:06:37 +02:00
5978cf8b88 pkg/ns: use correct syscall number on arm 2016-05-19 12:06:37 +02:00
b34210aa1c *: appc/cni -> containernetworking/cni
The project has been moved so internally we simply rename everything.
Consumers are recommended to update their vendored version of cni.
2016-05-19 12:06:36 +02:00
fc229c5090 *: appc/cni -> containernetworking/cni
The project has been moved so internally we simply rename everything.
Consumers are recommended to update their vendored version of cni.
2016-05-19 12:06:36 +02:00
5cbd217cbe pkg/utils: add functions to work with sysctl 2016-05-19 12:06:16 +02:00
19850efc3a Document use of goroutine and lockosthread in test helpers 2016-05-19 12:06:16 +02:00
5158edacef ns: fix reading net namespace in multi-threaded processes
/proc/self/ns/net gives the main thread's namespace, not necessarily
the namespace of the thread that's running the testcases.  This causes
sporadic failures of the tests.

For example, with a testcase reading inodes after switching netns:

/proc/27686/task/27689/ns/net 4026532565
/proc/self/ns/net 4026531969
/proc/27686/task/27689/ns/net 4026532565

See also:
008d17ae00

Running Suite: pkg/ns Suite
===========================
Random Seed: 1459953577
Will run 6 of 6 specs

• Failure [0.028 seconds]
Linux namespace operations
/cni/gopath/src/github.com/appc/cni/pkg/ns/ns_test.go:167
  WithNetNS
  /cni/gopath/src/github.com/appc/cni/pkg/ns/ns_test.go:166
    executes the callback within the target network namespace [It]
    /cni/gopath/src/github.com/appc/cni/pkg/ns/ns_test.go:97

    Expected
        <uint64>: 4026531969
    to equal
        <uint64>: 4026532565

    /cni/gopath/src/github.com/appc/cni/pkg/ns/ns_test.go:96
------------------------------
•••••

Summarizing 1 Failure:

[Fail] Linux namespace operations WithNetNS [It] executes the callback within the target network namespace
/cni/gopath/src/github.com/appc/cni/pkg/ns/ns_test.go:96

Ran 6 of 6 Specs in 0.564 seconds
FAIL! -- 5 Passed | 1 Failed | 0 Pending | 0 Skipped --- FAIL: TestNs (0.56s)
FAIL
2016-05-19 12:06:16 +02:00
5260862f80 Fix issues with MakeNetworkNS test helper 2016-05-19 12:06:16 +02:00
c77f7431fb Add basic unit tests of testhelpers 2016-05-19 12:06:16 +02:00
bd7b83dd98 Extract inode inspection functions into testhelpers 2016-05-19 12:06:16 +02:00
3358f16dd6 Extract testhelpers from loopback test suite 2016-05-19 12:06:16 +02:00
9079565e0d *: appc/cni -> containernetworking/cni
The project has been moved so internally we simply rename everything.
Consumers are recommended to update their vendored version of cni.
2016-05-06 16:40:27 +02:00
680d1f6ec2 *: appc/cni -> containernetworking/cni
The project has been moved so internally we simply rename everything.
Consumers are recommended to update their vendored version of cni.
2016-05-06 16:40:27 +02:00
c3a90e7682 Merge pull request #187 from appc/v0.3.0
merge unneeded branch v0.3.0 into master
2016-04-22 19:25:54 +02:00
f73261c431 Merge pull request #182 from rosenhouse/document-lockosthread
Document use of goroutine and LockOSThread in test helpers
2016-04-18 10:45:46 -07:00
c272c49555 ns: fix reading net namespace in multi-threaded processes
/proc/self/ns/net gives the main thread's namespace, not necessarily
the namespace of the thread that's running the testcases.  This causes
sporadic failures of the tests.

For example, with a testcase reading inodes after switching netns:

/proc/27686/task/27689/ns/net 4026532565
/proc/self/ns/net 4026531969
/proc/27686/task/27689/ns/net 4026532565

See also:
008d17ae00

Running Suite: pkg/ns Suite
===========================
Random Seed: 1459953577
Will run 6 of 6 specs

• Failure [0.028 seconds]
Linux namespace operations
/cni/gopath/src/github.com/appc/cni/pkg/ns/ns_test.go:167
  WithNetNS
  /cni/gopath/src/github.com/appc/cni/pkg/ns/ns_test.go:166
    executes the callback within the target network namespace [It]
    /cni/gopath/src/github.com/appc/cni/pkg/ns/ns_test.go:97

    Expected
        <uint64>: 4026531969
    to equal
        <uint64>: 4026532565

    /cni/gopath/src/github.com/appc/cni/pkg/ns/ns_test.go:96
------------------------------
•••••

Summarizing 1 Failure:

[Fail] Linux namespace operations WithNetNS [It] executes the callback within the target network namespace
/cni/gopath/src/github.com/appc/cni/pkg/ns/ns_test.go:96

Ran 6 of 6 Specs in 0.564 seconds
FAIL! -- 5 Passed | 1 Failed | 0 Pending | 0 Skipped --- FAIL: TestNs (0.56s)
FAIL
2016-04-18 10:35:23 -05:00
d701ca6c82 Document use of goroutine and lockosthread in test helpers 2016-04-17 20:27:02 -07:00
c085ec98fd Fix issues with MakeNetworkNS test helper 2016-04-17 19:47:54 -07:00
82851a860e Add basic unit tests of testhelpers 2016-04-17 19:25:39 -07:00
5f757b6af7 Extract inode inspection functions into testhelpers 2016-04-17 18:35:49 -07:00
4a79ac4cda Extract testhelpers from loopback test suite 2016-04-17 18:28:10 -07:00
e1bc41a42a pkg/utils: add functions to work with sysctl 2016-04-07 21:40:51 +02:00
11c0bfada8 *: add missing license headers + check 2016-04-01 15:35:21 +02:00
2c1633f8e9 pkg/utils: correct the test's expected chain names 2016-03-31 18:01:32 +02:00
6c9b5a361a *: add comment to iptables rules for ipmasq 2016-03-31 18:01:32 +02:00
42b74b9eb5 pkg/utils: remove unneeded condition in tests 2016-03-31 18:01:32 +02:00
9612d36615 pkg/utils: fix docstring 2016-03-31 18:01:32 +02:00
d4e088aad4 pkg/utils: split and unexport constants 2016-03-31 18:01:32 +02:00
f429750105 pkg/utils: fix docstring 2016-03-31 18:01:32 +02:00
f96a716154 pkg/utils: use constant for chain prefix 2016-03-31 18:01:32 +02:00
b4a2a1fa51 pkg/utils: use name+id for hash and extend tests 2016-03-31 18:01:32 +02:00
b87cf1ba9c pkg: add a function to generate chain names
Adds a function to generate chain names for use in iptables and
ports all drivers to use that function. Also adds tests for the
said function.
2016-03-31 18:01:32 +02:00
dc44feb5b5 pkg/types: add tests for args 2016-03-21 20:40:39 +01:00
fc83c24eb5 pkg/types: add IgnoreUnknown arg and logic
This commit adds a struct type CommonArgs that is to be embedded in
every plugin's argument struct. It contains a field named
"IgnoreUnknown" which will be parsed as a boolean and can be provided to
ignore unknown arguments passed to the plugin.
2016-03-21 20:36:05 +01:00
8cc424c5aa pkg/skel: add rudimentary unit tests
This is an attempt to testing the PluginMain() function of the skel pkg.
We should be able to do better by using a mockable interface for the
plugins, but this is a start.
2016-03-18 22:34:29 +01:00
ca978caee7 Update docstring on WithNetNS 2016-03-15 08:37:00 -07:00
b99854d124 WithNetNS restores original namespace when callback errors
- adds test coverage of WithNetNS in BDD-style
2016-03-15 01:51:58 -07:00
05683bf11d Add invoke.Delegate{Add,Del} for use by meta-plugins
The 'flannel' meta plugin delegates to other plugins to do the actual
OS-level work. It used the ipam.Exec{Add,Del} procedures for this
delegation, since those do precisely what's needed.

However this is a bit misleading, since the flannel plugin _isn't_
doing this for IPAM, and the ipam.Exec* procedures aren't doing
something specific to IPAM plugins.

So: anticipating that there may be more meta plugins that want to
delegate in the same way, this commit moves generic delegation
procedures to `pkg/invoke`, and makes the `pkg/ipam` procedures (still
used, accurately, in the non-meta plugins) shims.
2016-03-04 11:01:22 +00:00
e2673df403 Merge pull request #120 from zachgersh/find-better-error
Better error messages when plugin is not found
2016-03-02 11:14:50 +01:00
d2af995349 added the String method to Result type
Signed-off-by: André Martins <aanm90@gmail.com>
2016-02-19 17:40:46 +00:00
0b6ad716e8 new plugin: tuning
Allow users to tune net network parameters such as somaxconn.

With this patch, users can add a new network configuration:

> {
>   "name": "mytuning",
>   "type": "tuning",
>   "sysctl": {
>           "net.core.somaxconn": "500"
>   }
> }

The value /proc/sys/net/core/somaxconn will be set to 500 in the network
namespace but will remain unchanged on the host.

Only sysctl parameters that belong to the network subsystem can be
modified.

Related to: https://github.com/coreos/rkt/pull/2140
2016-02-18 15:29:12 +01:00
3382b459a7 Better error message when plugin cannot be found 2016-02-10 13:42:10 -08:00
a08b015fa5 *: reflect SPEC's DNS changes in implementation
* DNS is now a type which will result in a JSON dictionary in
  configurations and results
* Minor refactoring, making use of type embedding
2016-01-29 10:39:28 +01:00
c7db40fb4c docs/ptp: update example and DNS description 2016-01-29 10:39:28 +01:00
3bbc43f24d *: add "dns" field to the configuration
appc/cni#76 added a "dns" field in the result JSON. But before this
patch, the plugins had no way of knowing which name server to return.

There could be two ways of knowing which name server to return:
1. add it as an extra argument ("CNI_ARGS")
2. add it in the network configuration as a convenience (received via
   stdin)

I chose the second way because it is easier. In the case of rkt, it
means the user could just add the DNS name servers in
/etc/rkt/net.d/mynetwork.conf.
2016-01-26 18:54:56 +01:00
cf3c027960 *: add "dns" field to the configuration
appc/cni#76 added a "dns" field in the result JSON. But before this
patch, the plugins had no way of knowing which name server to return.

There could be two ways of knowing which name server to return:
1. add it as an extra argument ("CNI_ARGS")
2. add it in the network configuration as a convenience (received via
   stdin)

I chose the second way because it is easier. In the case of rkt, it
means the user could just add the DNS name servers in
/etc/rkt/net.d/mynetwork.conf.
2016-01-26 18:54:56 +01:00
6b58eda23a pkg/ip: fix typo in error message, s/iptabes/iptables/ 2015-12-16 13:20:19 -08:00
dd719a43ce flannel: doc fix and check for missing subnet.env fields
- Document that flannel outputs FLANNEL_NETWORK b/c we use it
- Check and error out if expecited fields are missing in subnetEnv file

Fixes #72
2015-10-07 15:38:12 -07:00
4bee884fae Change copyright from CoreOS to CNI authors
CNI is developed by more than just CoreOS, Inc
and the copyright is retained by all CNI
contributors (for their respective contributed code).
2015-09-29 11:51:33 -07:00
4da892a669 link: switch to host netns to set up host veth end 2015-09-24 18:49:27 +02:00
8525fef466 Revert "plugins/ptp: allow host veth to be UP"
This reverts commit 231d2d5a27.
2015-09-24 18:32:18 +02:00
8d3eb91a37 plugins/ptp: allow host veth to be UP 2015-09-24 10:57:36 +02:00
4c57a90da6 update ptp docs to reflect changes in plugin 2015-09-24 09:43:55 +02:00
ae3ceedd69 bug fix: exec of DEL cmd caused JSON decode error
When plugin is executed with a DEL command, it does not
print result to stdout unless there is an error. Therefore
it stdout bytes should not be passed to json.Unmarshal.
2015-09-18 10:30:10 -07:00
09a8148e36 Factor an API out into a module
This takes some of the machinery from CNI and from the rkt networking
code, and turns it into a library that can be linked into go apps.

Included is an example command-line application that uses the library,
called `cnitool`.

Other headline changes:

 * Plugin exec'ing is factored out

The motivation here is to factor out the protocol for invoking
plugins. To that end, a generalisation of the code from api.go and
pkg/plugin/ipam.go goes into pkg/invoke/exec.go.

 * Move argument-handling and conf-loading into public API

The fact that the arguments get turned into an environment for the
plugin is incidental to the API; so, provide a way of supplying them
as a struct or saying "just use the same arguments as I got" (the
latter is for IPAM plugins).
2015-09-16 10:14:39 +01:00
104e8b483e plugin/ipam: correct formatting of error message 2015-09-08 18:08:15 +00:00
b5db5d3075 IPAM plugin: improve error messages
Make it more clear that we failed to find an IPAM plugin.
Check for a missing plugin name and issue a more helpful error.
2015-09-08 18:08:15 +00:00
ac7cd562fd plugin/ipam: fix typo in error message 2015-09-07 15:43:34 -07:00
169f8e0a52 CNI_ARGS: use ';' to split args as documented 2015-09-05 18:58:58 +02:00
a75401a13b host-local: allow ip request via CNI_ARGS
A specific IP can now be requested via the environment variable CNI_ARGS, e.g.
`CNI_ARGS=ip=1.2.3.4`.
The plugin will try to reserve the specified IP.
If this is not successful the execution will fail.
2015-09-04 01:38:22 +02:00
d1a0905345 host-local: allow ip request via CNI_ARGS
A specific IP can now be requested via the environment variable CNI_ARGS, e.g.
`CNI_ARGS=ip=1.2.3.4`.
The plugin will try to reserve the specified IP.
If this is not successful the execution will fail.
2015-09-04 01:38:22 +02:00
a41e1a699e Fix a few spelling mistakes in the docs 2015-08-20 16:41:25 +01:00
77a2e9727c Merge pull request #34 from eyakubovich/document-plugins
Document the plugins
2015-07-22 09:02:39 -07:00
f741bc324b Rename veth plugin to ptp and add docs.
ptp (point-to-point) better describes the nature of the plugin
as veth pairs are also used by the bridge plugin.
2015-07-21 17:20:11 -07:00
507805f8c4 Document the plugins
Adds a .md file to the Documentation/ folder for each plugin.

Fixes #16
2015-07-21 17:19:18 -07:00
4f74b78041 enable net.ipv4.ip_forward in plugins that need it 2015-07-13 15:41:02 -07:00
6caffd48b3 No more path rewriting
Path rewriting causes too many problems when vendoring
vendored code. When CNI code is vendored into rkt,
godep has problems code already vendored by CNI.
2015-06-12 16:29:18 -07:00
58f6bc9c8b add flannel plugin
This introduces a notion of a "meta" plugin. A meta plugin
is one that delegates the actual work of setting up the interface
to the main plugin. The meta plugin is used to select and dynamically
configure the main plugin. The sequence of events, is as follows:

Given netconf like:

    {
	"name": "mynet",
	"type": "flannel",
	"delegate": {
	    "type": "bridge"
	}
    }

flannel fills in values like "mtu", "ipam.subnet" and delegates to
"bridge" main plugin. "bridge" plugin will operate as usual, calling
into ipam module for IP assignment.

Delegate dictionary should not contain "name" field as it will be
filled in by the flannel plugin.
2015-06-11 15:37:58 -07:00
31c0dbf110 Propagate json error object to the caller
When plugin errors out, it prints out a JSON object to stdout
describing the failure. This object needs to be propagated out
through the plugins and to the container runtime. This change
also adds Print method to both the result and error structs
for easy serialization to stdout.
2015-06-09 10:46:28 -07:00
a6a822268c add DHCP IPAM plugin
The plugin binary actually functions in two modes. The first mode
is a regular CNI plugin. The second mode (when stared with "daemon" arg)
runs a DHCP client daemon. When executed as a CNI plugin, it issues
an RPC request to the daemon for actual processing. The daemon is
required since a DHCP lease needs to be maintained by periodically
renewing it. One instance of the daemon can server arbitrary number
of containers/leases.
2015-05-21 13:36:51 -07:00
e59e2fd890 Merge pull request #9 from eyakubovich/errors
report errors via JSON
2015-05-08 15:08:03 -07:00
c7a1442830 report errors via JSON
This reflects the latest SPEC draft.
2015-05-08 15:07:09 -07:00
93be8e63f1 Do not use netns as ID or for entropy
ContainerID is now required so use that
or generate random bytes.

Fixes #5
2015-05-08 15:02:23 -07:00
c24708ff62 Add plugin code
This adds basic plugins.
"main" types: veth, bridge, macvlan
"ipam" type: host-local

The code has been ported over from github.com/coreos/rkt project
and adapted to fit the CNI spec.
2015-04-27 14:14:29 -07:00
531 changed files with 72336 additions and 9854 deletions

28
.appveyor.yml Normal file
View 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
View File

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

View File

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

View File

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

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

View File

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

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

View File

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

View File

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

@ -1,163 +1,26 @@
[![Build Status](https://travis-ci.org/containernetworking/cni.svg?branch=master)](https://travis-ci.org/containernetworking/cni)
[![Coverage Status](https://coveralls.io/repos/github/containernetworking/cni/badge.svg?branch=master)](https://coveralls.io/github/containernetworking/cni?branch=master)
[![Linux Build Status](https://travis-ci.org/containernetworking/plugins.svg?branch=master)](https://travis-ci.org/containernetworking/plugins)
[![Windows Build Status](https://ci.appveyor.com/api/projects/status/kcuubx0chr76ev86/branch/master?svg=true)](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
View 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
View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

@ -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
View 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
View 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 = &current.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 = &current.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())
})
})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

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

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

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

View File

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

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

View File

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

View 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{}))
})
})
})

View File

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

View File

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

View File

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

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

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

View File

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

View File

@ -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 := &current.Result{}
if err := rpcCall("DHCP.Allocate", args, result); err != nil {
return err
}
return types.PrintResult(result, confVersion)
}
func cmdDel(args *skel.CmdArgs) error {

View File

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

View File

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

View File

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

View File

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

View 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 &current.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
}

View 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 allocator_test
import (
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"testing"
)
func TestAllocator(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Allocator Suite")
}

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

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

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

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

View 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, ",")
}

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

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

View File

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

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

View File

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

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

View File

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

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

View File

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

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

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

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

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

View File

@ -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 := &current.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
View 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

View File

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

View File

@ -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 {
if err := netlink.AddrAdd(br, addr); err != nil && err != syscall.EEXIST {
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 := &current.Interface{}
hostIface := &current.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, &current.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

View 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"
}
```

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

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

View 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"`))
})
})

View File

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

View File

@ -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 := &current.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)
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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 := &current.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)
}

View File

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

View File

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

View File

@ -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 := &current.Interface{}
containerInterface := &current.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)
}

View 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