podman/libpod/network/cni/cni_conversion.go

379 lines
11 KiB
Go

// +build linux
package cni
import (
"encoding/json"
"io/ioutil"
"net"
"os"
"path/filepath"
"strconv"
"strings"
"syscall"
"time"
"github.com/containernetworking/cni/libcni"
internalutil "github.com/containers/podman/v3/libpod/network/internal/util"
"github.com/containers/podman/v3/libpod/network/types"
"github.com/containers/podman/v3/libpod/network/util"
pkgutil "github.com/containers/podman/v3/pkg/util"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
func createNetworkFromCNIConfigList(conf *libcni.NetworkConfigList, confPath string) (*types.Network, error) {
network := types.Network{
Name: conf.Name,
ID: getNetworkIDFromName(conf.Name),
Labels: map[string]string{},
Options: map[string]string{},
IPAMOptions: map[string]string{},
}
cniJSON := make(map[string]interface{})
err := json.Unmarshal(conf.Bytes, &cniJSON)
if err != nil {
return nil, errors.Wrapf(err, "failed to unmarshal network config %s", conf.Name)
}
if args, ok := cniJSON["args"]; ok {
if key, ok := args.(map[string]interface{}); ok {
// read network labels and options from the conf file
network.Labels = getNetworkArgsFromConfList(key, podmanLabelKey)
network.Options = getNetworkArgsFromConfList(key, podmanOptionsKey)
}
}
f, err := os.Stat(confPath)
if err != nil {
return nil, err
}
stat := f.Sys().(*syscall.Stat_t)
network.Created = time.Unix(int64(stat.Ctim.Sec), int64(stat.Ctim.Nsec))
firstPlugin := conf.Plugins[0]
network.Driver = firstPlugin.Network.Type
switch firstPlugin.Network.Type {
case types.BridgeNetworkDriver:
var bridge hostLocalBridge
err := json.Unmarshal(firstPlugin.Bytes, &bridge)
if err != nil {
return nil, errors.Wrapf(err, "failed to unmarshal the bridge plugin config in %s", confPath)
}
network.NetworkInterface = bridge.BrName
// if isGateway is false we have an internal network
if !bridge.IsGW {
network.Internal = true
}
// set network options
if bridge.MTU != 0 {
network.Options["mtu"] = strconv.Itoa(bridge.MTU)
}
if bridge.Vlan != 0 {
network.Options["vlan"] = strconv.Itoa(bridge.Vlan)
}
err = convertIPAMConfToNetwork(&network, bridge.IPAM, confPath)
if err != nil {
return nil, err
}
case types.MacVLANNetworkDriver, types.IPVLANNetworkDriver:
var vlan VLANConfig
err := json.Unmarshal(firstPlugin.Bytes, &vlan)
if err != nil {
return nil, errors.Wrapf(err, "failed to unmarshal the macvlan plugin config in %s", confPath)
}
network.NetworkInterface = vlan.Master
// set network options
if vlan.MTU != 0 {
network.Options["mtu"] = strconv.Itoa(vlan.MTU)
}
if vlan.Mode != "" {
network.Options["mode"] = vlan.Mode
}
err = convertIPAMConfToNetwork(&network, vlan.IPAM, confPath)
if err != nil {
return nil, err
}
default:
// A warning would be good but users would get this warning every time so keep this at info level.
logrus.Infof("Unsupported CNI config type %s in %s, this network can still be used but inspect or list cannot show all information",
firstPlugin.Network.Type, confPath)
}
// check if the dnsname plugin is configured
network.DNSEnabled = findPluginByName(conf.Plugins, "dnsname")
return &network, nil
}
func findPluginByName(plugins []*libcni.NetworkConfig, name string) bool {
for _, plugin := range plugins {
if plugin.Network.Type == name {
return true
}
}
return false
}
// convertIPAMConfToNetwork converts A cni IPAMConfig to libpod network subnets.
// It returns an array of subnets and an extra bool if dhcp is configured.
func convertIPAMConfToNetwork(network *types.Network, ipam ipamConfig, confPath string) error {
if ipam.PluginType == types.DHCPIPAMDriver {
network.IPAMOptions["driver"] = types.DHCPIPAMDriver
return nil
}
if ipam.PluginType != types.HostLocalIPAMDriver {
return errors.Errorf("unsupported ipam plugin %s in %s", ipam.PluginType, confPath)
}
network.IPAMOptions["driver"] = types.HostLocalIPAMDriver
for _, r := range ipam.Ranges {
for _, ipam := range r {
s := types.Subnet{}
// Do not use types.ParseCIDR() because we want the ip to be
// the network address and not a random ip in the sub.
_, sub, err := net.ParseCIDR(ipam.Subnet)
if err != nil {
return err
}
s.Subnet = types.IPNet{IPNet: *sub}
// gateway
var gateway net.IP
if ipam.Gateway != "" {
gateway = net.ParseIP(ipam.Gateway)
if gateway == nil {
return errors.Errorf("failed to parse gateway ip %s", ipam.Gateway)
}
// convert to 4 byte if ipv4
util.NormalizeIP(&gateway)
} else if !network.Internal {
// only add a gateway address if the network is not internal
gateway, err = util.FirstIPInSubnet(sub)
if err != nil {
return errors.Errorf("failed to get first ip in subnet %s", sub.String())
}
}
s.Gateway = gateway
var rangeStart net.IP
var rangeEnd net.IP
if ipam.RangeStart != "" {
rangeStart = net.ParseIP(ipam.RangeStart)
if rangeStart == nil {
return errors.Errorf("failed to parse range start ip %s", ipam.RangeStart)
}
}
if ipam.RangeEnd != "" {
rangeEnd = net.ParseIP(ipam.RangeEnd)
if rangeEnd == nil {
return errors.Errorf("failed to parse range end ip %s", ipam.RangeEnd)
}
}
if rangeStart != nil || rangeEnd != nil {
s.LeaseRange = &types.LeaseRange{}
s.LeaseRange.StartIP = rangeStart
s.LeaseRange.EndIP = rangeEnd
}
if util.IsIPv6(s.Subnet.IP) {
network.IPv6Enabled = true
}
network.Subnets = append(network.Subnets, s)
}
}
return nil
}
// getNetworkArgsFromConfList returns the map of args in a conflist, argType should be labels or options
func getNetworkArgsFromConfList(args map[string]interface{}, argType string) map[string]string {
if args, ok := args[argType]; ok {
if labels, ok := args.(map[string]interface{}); ok {
result := make(map[string]string, len(labels))
for k, v := range labels {
if v, ok := v.(string); ok {
result[k] = v
}
}
return result
}
}
return map[string]string{}
}
// createCNIConfigListFromNetwork will create a cni config file from the given network.
// It returns the cni config and the path to the file where the config was written.
// Set writeToDisk to false to only add this network into memory.
func (n *cniNetwork) createCNIConfigListFromNetwork(network *types.Network, writeToDisk bool) (*libcni.NetworkConfigList, string, error) {
var (
routes []ipamRoute
ipamRanges [][]ipamLocalHostRangeConf
ipamConf ipamConfig
err error
)
if len(network.Subnets) > 0 {
for _, subnet := range network.Subnets {
route, err := newIPAMDefaultRoute(util.IsIPv6(subnet.Subnet.IP))
if err != nil {
return nil, "", err
}
routes = append(routes, route)
ipam := newIPAMLocalHostRange(subnet.Subnet, subnet.LeaseRange, subnet.Gateway)
ipamRanges = append(ipamRanges, []ipamLocalHostRangeConf{*ipam})
}
ipamConf = newIPAMHostLocalConf(routes, ipamRanges)
} else {
ipamConf = ipamConfig{PluginType: "dhcp"}
}
vlan := 0
mtu := 0
vlanPluginMode := ""
for k, v := range network.Options {
switch k {
case "mtu":
mtu, err = internalutil.ParseMTU(v)
if err != nil {
return nil, "", err
}
case "vlan":
vlan, err = internalutil.ParseVlan(v)
if err != nil {
return nil, "", err
}
case "mode":
switch network.Driver {
case types.MacVLANNetworkDriver:
if !pkgutil.StringInSlice(v, []string{"", "bridge", "private", "vepa", "passthru"}) {
return nil, "", errors.Errorf("unknown macvlan mode %q", v)
}
case types.IPVLANNetworkDriver:
if !pkgutil.StringInSlice(v, []string{"", "l2", "l3", "l3s"}) {
return nil, "", errors.Errorf("unknown ipvlan mode %q", v)
}
default:
return nil, "", errors.Errorf("cannot set option \"mode\" with driver %q", network.Driver)
}
vlanPluginMode = v
default:
return nil, "", errors.Errorf("unsupported network option %s", k)
}
}
isGateway := true
ipMasq := true
if network.Internal {
isGateway = false
ipMasq = false
}
// create CNI plugin configuration
// explicitly use CNI version 0.4.0 here, to use v1.0.0 at least containernetwork-plugins-1.0.1 has to be installed
// the dnsname plugin also needs to be updated for 1.0.0
// TODO change to 1.0.0 when most distros support it
ncList := newNcList(network.Name, "0.4.0", network.Labels, network.Options)
var plugins []interface{}
switch network.Driver {
case types.BridgeNetworkDriver:
bridge := newHostLocalBridge(network.NetworkInterface, isGateway, ipMasq, mtu, vlan, ipamConf)
plugins = append(plugins, bridge, newPortMapPlugin(), newFirewallPlugin(), newTuningPlugin())
// if we find the dnsname plugin we add configuration for it
if hasDNSNamePlugin(n.cniPluginDirs) && network.DNSEnabled {
// Note: in the future we might like to allow for dynamic domain names
plugins = append(plugins, newDNSNamePlugin(defaultPodmanDomainName))
}
case types.MacVLANNetworkDriver:
plugins = append(plugins, newVLANPlugin(types.MacVLANNetworkDriver, network.NetworkInterface, vlanPluginMode, mtu, ipamConf))
case types.IPVLANNetworkDriver:
plugins = append(plugins, newVLANPlugin(types.IPVLANNetworkDriver, network.NetworkInterface, vlanPluginMode, mtu, ipamConf))
default:
return nil, "", errors.Errorf("driver %q is not supported by cni", network.Driver)
}
ncList["plugins"] = plugins
b, err := json.MarshalIndent(ncList, "", " ")
if err != nil {
return nil, "", err
}
cniPathName := ""
if writeToDisk {
cniPathName = filepath.Join(n.cniConfigDir, network.Name+".conflist")
err = ioutil.WriteFile(cniPathName, b, 0644)
if err != nil {
return nil, "", err
}
f, err := os.Stat(cniPathName)
if err != nil {
return nil, "", err
}
stat := f.Sys().(*syscall.Stat_t)
network.Created = time.Unix(int64(stat.Ctim.Sec), int64(stat.Ctim.Nsec))
} else {
network.Created = time.Now()
}
config, err := libcni.ConfListFromBytes(b)
if err != nil {
return nil, "", err
}
return config, cniPathName, nil
}
func convertSpecgenPortsToCNIPorts(ports []types.PortMapping) ([]cniPortMapEntry, error) {
cniPorts := make([]cniPortMapEntry, 0, len(ports))
for _, port := range ports {
if port.Protocol == "" {
return nil, errors.New("port protocol should not be empty")
}
protocols := strings.Split(port.Protocol, ",")
for _, protocol := range protocols {
if !pkgutil.StringInSlice(protocol, []string{"tcp", "udp", "sctp"}) {
return nil, errors.Errorf("unknown port protocol %s", protocol)
}
cniPort := cniPortMapEntry{
HostPort: int(port.HostPort),
ContainerPort: int(port.ContainerPort),
HostIP: port.HostIP,
Protocol: protocol,
}
cniPorts = append(cniPorts, cniPort)
for i := 1; i < int(port.Range); i++ {
cniPort := cniPortMapEntry{
HostPort: int(port.HostPort) + i,
ContainerPort: int(port.ContainerPort) + i,
HostIP: port.HostIP,
Protocol: protocol,
}
cniPorts = append(cniPorts, cniPort)
}
}
}
return cniPorts, nil
}
func removeMachinePlugin(conf *libcni.NetworkConfigList) *libcni.NetworkConfigList {
plugins := make([]*libcni.NetworkConfig, 0, len(conf.Plugins))
for _, net := range conf.Plugins {
if net.Network.Type != "podman-machine" {
plugins = append(plugins, net)
}
}
conf.Plugins = plugins
return conf
}