diff options
Diffstat (limited to 'libpod')
-rw-r--r-- | libpod/network/config.go | 141 | ||||
-rw-r--r-- | libpod/network/create.go | 195 | ||||
-rw-r--r-- | libpod/network/devices.go | 63 | ||||
-rw-r--r-- | libpod/network/files.go | 174 | ||||
-rw-r--r-- | libpod/network/ip.go | 19 | ||||
-rw-r--r-- | libpod/network/lock.go | 26 | ||||
-rw-r--r-- | libpod/network/netconflist.go | 155 | ||||
-rw-r--r-- | libpod/network/netconflist_test.go | 38 | ||||
-rw-r--r-- | libpod/network/network.go | 225 | ||||
-rw-r--r-- | libpod/network/network_test.go | 35 | ||||
-rw-r--r-- | libpod/network/subnet.go | 78 | ||||
-rw-r--r-- | libpod/network/subnet_test.go | 35 | ||||
-rw-r--r-- | libpod/pod_api.go | 241 |
13 files changed, 1292 insertions, 133 deletions
diff --git a/libpod/network/config.go b/libpod/network/config.go new file mode 100644 index 000000000..a08e684d8 --- /dev/null +++ b/libpod/network/config.go @@ -0,0 +1,141 @@ +package network + +import ( + "encoding/json" + "net" + + "github.com/containers/storage/pkg/lockfile" +) + +// TODO once the containers.conf file stuff is worked out, this should be modified +// to honor defines in the containers.conf as well as overrides? + +const ( + // CNIConfigDir is the path where CNI config files exist + CNIConfigDir = "/etc/cni/net.d" + // CNIDeviceName is the default network device name and in + // reality should have an int appended to it (cni-podman4) + CNIDeviceName = "cni-podman" + // DefaultPodmanDomainName is used for the dnsname plugin to define + // a localized domain name for a created network + DefaultPodmanDomainName = "dns.podman" + // LockFileName is used for obtaining a lock and is appended + // to libpod's tmpdir in practice + LockFileName = "cni.lock" +) + +// CNILock is for preventing name collision and +// unpredictable results when doing some CNI operations. +type CNILock struct { + lockfile.Locker +} + +// GetDefaultPodmanNetwork outputs the default network for podman +func GetDefaultPodmanNetwork() (*net.IPNet, error) { + _, n, err := net.ParseCIDR("10.88.1.0/24") + return n, err +} + +// CNIPlugins is a way of marshalling a CNI network configuration to disk +type CNIPlugins interface { + Bytes() ([]byte, error) +} + +// HostLocalBridge describes a configuration for a bridge plugin +// https://github.com/containernetworking/plugins/tree/master/plugins/main/bridge#network-configuration-reference +type HostLocalBridge struct { + PluginType string `json:"type"` + BrName string `json:"bridge,omitempty"` + IsGW bool `json:"isGateway"` + IsDefaultGW bool `json:"isDefaultGateway,omitempty"` + ForceAddress bool `json:"forceAddress,omitempty"` + IPMasq bool `json:"ipMasq,omitempty"` + MTU int `json:"mtu,omitempty"` + HairpinMode bool `json:"hairpinMode,omitempty"` + PromiscMode bool `json:"promiscMode,omitempty"` + Vlan int `json:"vlan,omitempty"` + IPAM IPAMHostLocalConf `json:"ipam"` +} + +// Bytes outputs []byte +func (h *HostLocalBridge) Bytes() ([]byte, error) { + return json.MarshalIndent(h, "", "\t") +} + +// IPAMHostLocalConf describes an IPAM configuration +// https://github.com/containernetworking/plugins/tree/master/plugins/ipam/host-local#network-configuration-reference +type IPAMHostLocalConf struct { + PluginType string `json:"type"` + Routes []IPAMRoute `json:"routes,omitempty"` + ResolveConf string `json:"resolveConf,omitempty"` + DataDir string `json:"dataDir,omitempty"` + Ranges [][]IPAMLocalHostRangeConf `json:"ranges,omitempty"` +} + +// IPAMLocalHostRangeConf describes the new style IPAM ranges +type IPAMLocalHostRangeConf struct { + Subnet string `json:"subnet"` + RangeStart string `json:"rangeStart,omitempty"` + RangeEnd string `json:"rangeEnd,omitempty"` + Gateway string `json:"gateway,omitempty"` +} + +// Bytes outputs the configuration as []byte +func (i IPAMHostLocalConf) Bytes() ([]byte, error) { + return json.MarshalIndent(i, "", "\t") +} + +// IPAMRoute describes a route in an ipam config +type IPAMRoute struct { + Dest string `json:"dst"` +} + +// PortMapConfig describes the default portmapping config +type PortMapConfig struct { + PluginType string `json:"type"` + Capabilities map[string]bool `json:"capabilities"` +} + +// Bytes outputs the configuration as []byte +func (p PortMapConfig) Bytes() ([]byte, error) { + return json.MarshalIndent(p, "", "\t") +} + +// IPAMDHCP describes the ipamdhcp config +type IPAMDHCP struct { + DHCP string `json:"type"` +} + +// MacVLANConfig describes the macvlan config +type MacVLANConfig struct { + PluginType string `json:"type"` + Master string `json:"master"` + IPAM IPAMDHCP `json:"ipam"` +} + +// Bytes outputs the configuration as []byte +func (p MacVLANConfig) Bytes() ([]byte, error) { + return json.MarshalIndent(p, "", "\t") +} + +// FirewallConfig describes the firewall plugin +type FirewallConfig struct { + PluginType string `json:"type"` + Backend string `json:"backend"` +} + +// Bytes outputs the configuration as []byte +func (f FirewallConfig) Bytes() ([]byte, error) { + return json.MarshalIndent(f, "", "\t") +} + +// DNSNameConfig describes the dns container name resolution plugin config +type DNSNameConfig struct { + PluginType string `json:"type"` + DomainName string `json:"domainName"` +} + +// Bytes outputs the configuration as []byte +func (d DNSNameConfig) Bytes() ([]byte, error) { + return json.MarshalIndent(d, "", "\t") +} diff --git a/libpod/network/create.go b/libpod/network/create.go new file mode 100644 index 000000000..a9ed4c4ef --- /dev/null +++ b/libpod/network/create.go @@ -0,0 +1,195 @@ +package network + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + "path/filepath" + + "github.com/containernetworking/cni/pkg/version" + "github.com/containers/podman/v2/libpod" + "github.com/containers/podman/v2/pkg/domain/entities" + "github.com/containers/podman/v2/pkg/util" + "github.com/pkg/errors" +) + +func Create(name string, options entities.NetworkCreateOptions, r *libpod.Runtime) (*entities.NetworkCreateReport, error) { + var fileName string + if err := isSupportedDriver(options.Driver); err != nil { + return nil, err + } + config, err := r.GetConfig() + if err != nil { + return nil, err + } + // Acquire a lock for CNI + l, err := acquireCNILock(filepath.Join(config.Engine.TmpDir, LockFileName)) + if err != nil { + return nil, err + } + defer l.releaseCNILock() + if len(options.MacVLAN) > 0 { + fileName, err = createMacVLAN(r, name, options) + } else { + fileName, err = createBridge(r, name, options) + } + if err != nil { + return nil, err + } + return &entities.NetworkCreateReport{Filename: fileName}, nil +} + +// createBridge creates a CNI network +func createBridge(r *libpod.Runtime, name string, options entities.NetworkCreateOptions) (string, error) { + isGateway := true + ipMasq := true + subnet := &options.Subnet + ipRange := options.Range + runtimeConfig, err := r.GetConfig() + if err != nil { + return "", err + } + // if range is provided, make sure it is "in" network + if subnet.IP != nil { + // if network is provided, does it conflict with existing CNI or live networks + err = ValidateUserNetworkIsAvailable(runtimeConfig, subnet) + } else { + // if no network is provided, figure out network + subnet, err = GetFreeNetwork(runtimeConfig) + } + if err != nil { + return "", err + } + gateway := options.Gateway + if gateway == nil { + // if no gateway is provided, provide it as first ip of network + gateway = CalcGatewayIP(subnet) + } + // if network is provided and if gateway is provided, make sure it is "in" network + if options.Subnet.IP != nil && options.Gateway != nil { + if !subnet.Contains(gateway) { + return "", errors.Errorf("gateway %s is not in valid for subnet %s", gateway.String(), subnet.String()) + } + } + if options.Internal { + isGateway = false + ipMasq = false + } + + // if a range is given, we need to ensure it is "in" the network range. + if options.Range.IP != nil { + if options.Subnet.IP == nil { + return "", errors.New("you must define a subnet range to define an ip-range") + } + firstIP, err := FirstIPInSubnet(&options.Range) + if err != nil { + return "", err + } + lastIP, err := LastIPInSubnet(&options.Range) + if err != nil { + return "", err + } + if !subnet.Contains(firstIP) || !subnet.Contains(lastIP) { + return "", errors.Errorf("the ip range %s does not fall within the subnet range %s", options.Range.String(), subnet.String()) + } + } + bridgeDeviceName, err := GetFreeDeviceName(runtimeConfig) + if err != nil { + return "", err + } + + if len(name) > 0 { + netNames, err := GetNetworkNamesFromFileSystem(runtimeConfig) + if err != nil { + return "", err + } + if util.StringInSlice(name, netNames) { + return "", errors.Errorf("the network name %s is already used", name) + } + } else { + // If no name is given, we give the name of the bridge device + name = bridgeDeviceName + } + + ncList := NewNcList(name, version.Current()) + var plugins []CNIPlugins + var routes []IPAMRoute + + defaultRoute, err := NewIPAMDefaultRoute(IsIPv6(subnet.IP)) + if err != nil { + return "", err + } + routes = append(routes, defaultRoute) + ipamConfig, err := NewIPAMHostLocalConf(subnet, routes, ipRange, gateway) + if err != nil { + return "", err + } + + // TODO need to iron out the role of isDefaultGW and IPMasq + bridge := NewHostLocalBridge(bridgeDeviceName, isGateway, false, ipMasq, ipamConfig) + plugins = append(plugins, bridge) + plugins = append(plugins, NewPortMapPlugin()) + plugins = append(plugins, NewFirewallPlugin()) + // if we find the dnsname plugin, we add configuration for it + if HasDNSNamePlugin(runtimeConfig.Network.CNIPluginDirs) && !options.DisableDNS { + // Note: in the future we might like to allow for dynamic domain names + plugins = append(plugins, NewDNSNamePlugin(DefaultPodmanDomainName)) + } + ncList["plugins"] = plugins + b, err := json.MarshalIndent(ncList, "", " ") + if err != nil { + return "", err + } + if err := os.MkdirAll(GetCNIConfDir(runtimeConfig), 0755); err != nil { + return "", err + } + cniPathName := filepath.Join(GetCNIConfDir(runtimeConfig), fmt.Sprintf("%s.conflist", name)) + err = ioutil.WriteFile(cniPathName, b, 0644) + return cniPathName, err +} + +func createMacVLAN(r *libpod.Runtime, name string, options entities.NetworkCreateOptions) (string, error) { + var ( + plugins []CNIPlugins + ) + liveNetNames, err := GetLiveNetworkNames() + if err != nil { + return "", err + } + + config, err := r.GetConfig() + if err != nil { + return "", err + } + + // Make sure the host-device exists + if !util.StringInSlice(options.MacVLAN, liveNetNames) { + return "", errors.Errorf("failed to find network interface %q", options.MacVLAN) + } + if len(name) > 0 { + netNames, err := GetNetworkNamesFromFileSystem(config) + if err != nil { + return "", err + } + if util.StringInSlice(name, netNames) { + return "", errors.Errorf("the network name %s is already used", name) + } + } else { + name, err = GetFreeDeviceName(config) + if err != nil { + return "", err + } + } + ncList := NewNcList(name, version.Current()) + macvlan := NewMacVLANPlugin(options.MacVLAN) + plugins = append(plugins, macvlan) + ncList["plugins"] = plugins + b, err := json.MarshalIndent(ncList, "", " ") + if err != nil { + return "", err + } + cniPathName := filepath.Join(GetCNIConfDir(config), fmt.Sprintf("%s.conflist", name)) + err = ioutil.WriteFile(cniPathName, b, 0644) + return cniPathName, err +} diff --git a/libpod/network/devices.go b/libpod/network/devices.go new file mode 100644 index 000000000..a5d23fae4 --- /dev/null +++ b/libpod/network/devices.go @@ -0,0 +1,63 @@ +package network + +import ( + "fmt" + "os/exec" + + "github.com/containers/common/pkg/config" + "github.com/containers/podman/v2/pkg/util" + "github.com/containers/podman/v2/utils" + "github.com/sirupsen/logrus" +) + +// GetFreeDeviceName returns a device name that is unused; used when no network +// name is provided by user +func GetFreeDeviceName(config *config.Config) (string, error) { + var ( + deviceNum uint + deviceName string + ) + networkNames, err := GetNetworkNamesFromFileSystem(config) + if err != nil { + return "", err + } + liveNetworksNames, err := GetLiveNetworkNames() + if err != nil { + return "", err + } + bridgeNames, err := GetBridgeNamesFromFileSystem(config) + if err != nil { + return "", err + } + for { + deviceName = fmt.Sprintf("%s%d", CNIDeviceName, deviceNum) + logrus.Debugf("checking if device name %q exists in other cni networks", deviceName) + if util.StringInSlice(deviceName, networkNames) { + deviceNum++ + continue + } + logrus.Debugf("checking if device name %q exists in live networks", deviceName) + if util.StringInSlice(deviceName, liveNetworksNames) { + deviceNum++ + continue + } + logrus.Debugf("checking if device name %q already exists as a bridge name ", deviceName) + if !util.StringInSlice(deviceName, bridgeNames) { + break + } + deviceNum++ + } + return deviceName, nil +} + +// RemoveInterface removes an interface by the given name +func RemoveInterface(interfaceName string) error { + // Make sure we have the ip command on the system + ipPath, err := exec.LookPath("ip") + if err != nil { + return err + } + // Delete the network interface + _, err = utils.ExecCmd(ipPath, []string{"link", "del", interfaceName}...) + return err +} diff --git a/libpod/network/files.go b/libpod/network/files.go new file mode 100644 index 000000000..a2090491f --- /dev/null +++ b/libpod/network/files.go @@ -0,0 +1,174 @@ +package network + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "sort" + "strings" + + "github.com/containernetworking/cni/libcni" + "github.com/containernetworking/plugins/plugins/ipam/host-local/backend/allocator" + "github.com/containers/common/pkg/config" + "github.com/containers/podman/v2/libpod/define" + "github.com/pkg/errors" +) + +func GetCNIConfDir(configArg *config.Config) string { + if len(configArg.Network.NetworkConfigDir) < 1 { + dc, err := config.DefaultConfig() + if err != nil { + // Fallback to hard-coded dir + return CNIConfigDir + } + return dc.Network.NetworkConfigDir + } + return configArg.Network.NetworkConfigDir +} + +// LoadCNIConfsFromDir loads all the CNI configurations from a dir +func LoadCNIConfsFromDir(dir string) ([]*libcni.NetworkConfigList, error) { + files, err := libcni.ConfFiles(dir, []string{".conflist"}) + if err != nil { + return nil, err + } + sort.Strings(files) + + configs := make([]*libcni.NetworkConfigList, 0, len(files)) + for _, confFile := range files { + conf, err := libcni.ConfListFromFile(confFile) + if err != nil { + return nil, errors.Wrapf(err, "in %s", confFile) + } + configs = append(configs, conf) + } + return configs, nil +} + +// GetCNIConfigPathByName finds a CNI network by name and +// returns its configuration file path +func GetCNIConfigPathByName(config *config.Config, name string) (string, error) { + files, err := libcni.ConfFiles(GetCNIConfDir(config), []string{".conflist"}) + if err != nil { + return "", err + } + for _, confFile := range files { + conf, err := libcni.ConfListFromFile(confFile) + if err != nil { + return "", errors.Wrapf(err, "in %s", confFile) + } + if conf.Name == name { + return confFile, nil + } + } + return "", errors.Wrap(define.ErrNoSuchNetwork, fmt.Sprintf("unable to find network configuration for %s", name)) +} + +// ReadRawCNIConfByName reads the raw CNI configuration for a CNI +// network by name +func ReadRawCNIConfByName(config *config.Config, name string) ([]byte, error) { + confFile, err := GetCNIConfigPathByName(config, name) + if err != nil { + return nil, err + } + b, err := ioutil.ReadFile(confFile) + return b, err +} + +// GetCNIPlugins returns a list of plugins that a given network +// has in the form of a string +func GetCNIPlugins(list *libcni.NetworkConfigList) string { + plugins := make([]string, 0, len(list.Plugins)) + for _, plug := range list.Plugins { + plugins = append(plugins, plug.Network.Type) + } + return strings.Join(plugins, ",") +} + +// GetNetworksFromFilesystem gets all the networks from the cni configuration +// files +func GetNetworksFromFilesystem(config *config.Config) ([]*allocator.Net, error) { + var cniNetworks []*allocator.Net + + networks, err := LoadCNIConfsFromDir(GetCNIConfDir(config)) + if err != nil { + return nil, err + } + for _, n := range networks { + for _, cniplugin := range n.Plugins { + if cniplugin.Network.Type == "bridge" { + ipamConf := allocator.Net{} + if err := json.Unmarshal(cniplugin.Bytes, &ipamConf); err != nil { + return nil, err + } + cniNetworks = append(cniNetworks, &ipamConf) + break + } + } + } + return cniNetworks, nil +} + +// GetNetworkNamesFromFileSystem gets all the names from the cni network +// configuration files +func GetNetworkNamesFromFileSystem(config *config.Config) ([]string, error) { + networks, err := LoadCNIConfsFromDir(GetCNIConfDir(config)) + if err != nil { + return nil, err + } + networkNames := []string{} + for _, n := range networks { + networkNames = append(networkNames, n.Name) + } + return networkNames, nil +} + +// GetInterfaceNameFromConfig returns the interface name for the bridge plugin +func GetInterfaceNameFromConfig(path string) (string, error) { + var name string + conf, err := libcni.ConfListFromFile(path) + if err != nil { + return "", err + } + for _, cniplugin := range conf.Plugins { + if cniplugin.Network.Type == "bridge" { + plugin := make(map[string]interface{}) + if err := json.Unmarshal(cniplugin.Bytes, &plugin); err != nil { + return "", err + } + name = plugin["bridge"].(string) + break + } + } + if len(name) == 0 { + return "", errors.New("unable to find interface name for network") + } + return name, nil +} + +// GetBridgeNamesFromFileSystem is a convenience function to get all the bridge +// names from the configured networks +func GetBridgeNamesFromFileSystem(config *config.Config) ([]string, error) { + networks, err := LoadCNIConfsFromDir(GetCNIConfDir(config)) + if err != nil { + return nil, err + } + + bridgeNames := []string{} + for _, n := range networks { + var name string + // iterate network conflists + for _, cniplugin := range n.Plugins { + // iterate plugins + if cniplugin.Network.Type == "bridge" { + plugin := make(map[string]interface{}) + if err := json.Unmarshal(cniplugin.Bytes, &plugin); err != nil { + continue + } + name = plugin["bridge"].(string) + } + } + bridgeNames = append(bridgeNames, name) + } + return bridgeNames, nil +} diff --git a/libpod/network/ip.go b/libpod/network/ip.go new file mode 100644 index 000000000..ba93a0d05 --- /dev/null +++ b/libpod/network/ip.go @@ -0,0 +1,19 @@ +package network + +import ( + "net" + + "github.com/containernetworking/plugins/pkg/ip" +) + +// CalcGatewayIP takes a network and returns the first IP in it. +func CalcGatewayIP(ipn *net.IPNet) net.IP { + // taken from cni bridge plugin as it is not exported + nid := ipn.IP.Mask(ipn.Mask) + return ip.NextIP(nid) +} + +// IsIPv6 returns if netIP is IPv6. +func IsIPv6(netIP net.IP) bool { + return netIP != nil && netIP.To4() == nil +} diff --git a/libpod/network/lock.go b/libpod/network/lock.go new file mode 100644 index 000000000..0395359eb --- /dev/null +++ b/libpod/network/lock.go @@ -0,0 +1,26 @@ +package network + +import ( + "github.com/containers/storage" +) + +// acquireCNILock gets a lock that should be used in create and +// delete cases to avoid unwanted collisions in network names. +// TODO this uses a file lock and should be converted to shared memory +// when we have a more general shared memory lock in libpod +func acquireCNILock(lockPath string) (*CNILock, error) { + l, err := storage.GetLockfile(lockPath) + if err != nil { + return nil, err + } + l.Lock() + cnilock := CNILock{ + Locker: l, + } + return &cnilock, nil +} + +// ReleaseCNILock unlocks the previously held lock +func (l *CNILock) releaseCNILock() { + l.Unlock() +} diff --git a/libpod/network/netconflist.go b/libpod/network/netconflist.go new file mode 100644 index 000000000..8187fdb39 --- /dev/null +++ b/libpod/network/netconflist.go @@ -0,0 +1,155 @@ +package network + +import ( + "net" + "os" + "path/filepath" +) + +const ( + defaultIPv4Route = "0.0.0.0/0" + defaultIPv6Route = "::/0" +) + +// NcList describes a generic map +type NcList map[string]interface{} + +// NewNcList creates a generic map of values with string +// keys and adds in version and network name +func NewNcList(name, version string) NcList { + n := NcList{} + n["cniVersion"] = version + n["name"] = name + return n +} + +// NewHostLocalBridge creates a new LocalBridge for host-local +func NewHostLocalBridge(name string, isGateWay, isDefaultGW, ipMasq bool, ipamConf IPAMHostLocalConf) *HostLocalBridge { + hostLocalBridge := HostLocalBridge{ + PluginType: "bridge", + BrName: name, + IPMasq: ipMasq, + HairpinMode: true, + IPAM: ipamConf, + } + if isGateWay { + hostLocalBridge.IsGW = true + } + if isDefaultGW { + hostLocalBridge.IsDefaultGW = true + } + return &hostLocalBridge +} + +// NewIPAMHostLocalConf creates a new IPAMHostLocal configfuration +func NewIPAMHostLocalConf(subnet *net.IPNet, routes []IPAMRoute, ipRange net.IPNet, gw net.IP) (IPAMHostLocalConf, error) { + var ipamRanges [][]IPAMLocalHostRangeConf + ipamConf := IPAMHostLocalConf{ + PluginType: "host-local", + Routes: routes, + // Possible future support ? Leaving for clues + //ResolveConf: "", + //DataDir: "" + } + IPAMRange, err := newIPAMLocalHostRange(subnet, &ipRange, &gw) + if err != nil { + return ipamConf, err + } + ipamRanges = append(ipamRanges, IPAMRange) + ipamConf.Ranges = ipamRanges + return ipamConf, nil +} + +func newIPAMLocalHostRange(subnet *net.IPNet, ipRange *net.IPNet, gw *net.IP) ([]IPAMLocalHostRangeConf, error) { //nolint:interfacer + var ranges []IPAMLocalHostRangeConf + hostRange := IPAMLocalHostRangeConf{ + Subnet: subnet.String(), + } + // an user provided a range, we add it here + if ipRange.IP != nil { + first, err := FirstIPInSubnet(ipRange) + if err != nil { + return nil, err + } + last, err := LastIPInSubnet(ipRange) + if err != nil { + return nil, err + } + hostRange.RangeStart = first.String() + hostRange.RangeEnd = last.String() + } + if gw != nil { + hostRange.Gateway = gw.String() + } + ranges = append(ranges, hostRange) + return ranges, nil +} + +// NewIPAMRoute creates a new IPAM route configuration +func NewIPAMRoute(r *net.IPNet) IPAMRoute { //nolint:interfacer + return IPAMRoute{Dest: r.String()} +} + +// NewIPAMDefaultRoute creates a new IPAMDefault route of +// 0.0.0.0/0 for IPv4 or ::/0 for IPv6 +func NewIPAMDefaultRoute(isIPv6 bool) (IPAMRoute, error) { + route := defaultIPv4Route + if isIPv6 { + route = defaultIPv6Route + } + _, n, err := net.ParseCIDR(route) + if err != nil { + return IPAMRoute{}, err + } + return NewIPAMRoute(n), nil +} + +// NewPortMapPlugin creates a predefined, default portmapping +// configuration +func NewPortMapPlugin() PortMapConfig { + caps := make(map[string]bool) + caps["portMappings"] = true + p := PortMapConfig{ + PluginType: "portmap", + Capabilities: caps, + } + return p +} + +// NewFirewallPlugin creates a generic firewall plugin +func NewFirewallPlugin() FirewallConfig { + return FirewallConfig{ + PluginType: "firewall", + } +} + +// NewDNSNamePlugin creates the dnsname config with a given +// domainname +func NewDNSNamePlugin(domainName string) DNSNameConfig { + return DNSNameConfig{ + PluginType: "dnsname", + DomainName: domainName, + } +} + +// HasDNSNamePlugin looks to see if the dnsname cni plugin is present +func HasDNSNamePlugin(paths []string) bool { + for _, p := range paths { + if _, err := os.Stat(filepath.Join(p, "dnsname")); err == nil { + return true + } + } + return false +} + +// NewMacVLANPlugin creates a macvlanconfig with a given device name +func NewMacVLANPlugin(device string) MacVLANConfig { + i := IPAMDHCP{DHCP: "dhcp"} + + m := MacVLANConfig{ + PluginType: "macvlan", + Master: device, + IPAM: i, + } + return m +} diff --git a/libpod/network/netconflist_test.go b/libpod/network/netconflist_test.go new file mode 100644 index 000000000..5893bf985 --- /dev/null +++ b/libpod/network/netconflist_test.go @@ -0,0 +1,38 @@ +package network + +import ( + "reflect" + "testing" +) + +func TestNewIPAMDefaultRoute(t *testing.T) { + + tests := []struct { + name string + isIPv6 bool + want IPAMRoute + }{ + { + name: "IPv4 default route", + isIPv6: false, + want: IPAMRoute{defaultIPv4Route}, + }, + { + name: "IPv6 default route", + isIPv6: true, + want: IPAMRoute{defaultIPv6Route}, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + got, err := NewIPAMDefaultRoute(tt.isIPv6) + if err != nil { + t.Errorf("no error expected: %v", err) + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("NewIPAMDefaultRoute() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/libpod/network/network.go b/libpod/network/network.go new file mode 100644 index 000000000..7327a1a7d --- /dev/null +++ b/libpod/network/network.go @@ -0,0 +1,225 @@ +package network + +import ( + "encoding/json" + "net" + "os" + "path/filepath" + + "github.com/containernetworking/cni/pkg/types" + "github.com/containernetworking/plugins/plugins/ipam/host-local/backend/allocator" + "github.com/containers/common/pkg/config" + "github.com/containers/podman/v2/libpod/define" + "github.com/containers/podman/v2/pkg/util" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" +) + +// DefaultNetworkDriver is the default network type used +var DefaultNetworkDriver = "bridge" + +// SupportedNetworkDrivers describes the list of supported drivers +var SupportedNetworkDrivers = []string{DefaultNetworkDriver} + +// isSupportedDriver checks if the user provided driver is supported +func isSupportedDriver(driver string) error { + if util.StringInSlice(driver, SupportedNetworkDrivers) { + return nil + } + return errors.Errorf("driver '%s' is not supported", driver) +} + +// GetLiveNetworks returns a slice of networks representing what the system +// has defined as network interfaces +func GetLiveNetworks() ([]*net.IPNet, error) { + addrs, err := net.InterfaceAddrs() + if err != nil { + return nil, err + } + nets := make([]*net.IPNet, 0, len(addrs)) + for _, address := range addrs { + _, n, err := net.ParseCIDR(address.String()) + if err != nil { + return nil, err + } + nets = append(nets, n) + } + return nets, nil +} + +// GetLiveNetworkNames returns a list of network interfaces on the system +func GetLiveNetworkNames() ([]string, error) { + liveInterfaces, err := net.Interfaces() + if err != nil { + return nil, err + } + interfaceNames := make([]string, 0, len(liveInterfaces)) + for _, i := range liveInterfaces { + interfaceNames = append(interfaceNames, i.Name) + } + return interfaceNames, nil +} + +// GetFreeNetwork looks for a free network according to existing cni configuration +// files and network interfaces. +func GetFreeNetwork(config *config.Config) (*net.IPNet, error) { + networks, err := GetNetworksFromFilesystem(config) + if err != nil { + return nil, err + } + liveNetworks, err := GetLiveNetworks() + if err != nil { + return nil, err + } + nextNetwork, err := GetDefaultPodmanNetwork() + if err != nil { + return nil, err + } + logrus.Debugf("default network is %s", nextNetwork.String()) + for { + newNetwork, err := NextSubnet(nextNetwork) + if err != nil { + return nil, err + } + logrus.Debugf("checking if network %s intersects with other cni networks", nextNetwork.String()) + if intersectsConfig, _ := networkIntersectsWithNetworks(newNetwork, allocatorToIPNets(networks)); intersectsConfig { + logrus.Debugf("network %s is already being used by a cni configuration", nextNetwork.String()) + nextNetwork = newNetwork + continue + } + logrus.Debugf("checking if network %s intersects with any network interfaces", nextNetwork.String()) + if intersectsLive, _ := networkIntersectsWithNetworks(newNetwork, liveNetworks); !intersectsLive { + break + } + logrus.Debugf("network %s is being used by a network interface", nextNetwork.String()) + nextNetwork = newNetwork + } + return nextNetwork, nil +} + +func allocatorToIPNets(networks []*allocator.Net) []*net.IPNet { + var nets []*net.IPNet + for _, network := range networks { + if len(network.IPAM.Ranges) > 0 { + // this is the new IPAM range style + // append each subnet from ipam the rangeset + for _, r := range network.IPAM.Ranges[0] { + nets = append(nets, newIPNetFromSubnet(r.Subnet)) + } + } else { + // looks like the old, deprecated style + nets = append(nets, newIPNetFromSubnet(network.IPAM.Subnet)) + } + } + return nets +} + +func newIPNetFromSubnet(subnet types.IPNet) *net.IPNet { + n := net.IPNet{ + IP: subnet.IP, + Mask: subnet.Mask, + } + return &n +} + +func networkIntersectsWithNetworks(n *net.IPNet, networklist []*net.IPNet) (bool, *net.IPNet) { + for _, nw := range networklist { + if networkIntersect(n, nw) { + return true, nw + } + } + return false, nil +} + +func networkIntersect(n1, n2 *net.IPNet) bool { + return n2.Contains(n1.IP) || n1.Contains(n2.IP) +} + +// ValidateUserNetworkIsAvailable returns via an error if a network is available +// to be used +func ValidateUserNetworkIsAvailable(config *config.Config, userNet *net.IPNet) error { + if len(userNet.IP) == 0 || len(userNet.Mask) == 0 { + return errors.Errorf("network %s's ip or mask cannot be empty", userNet.String()) + } + + ones, bit := userNet.Mask.Size() + if ones == 0 || bit == 0 { + return errors.Errorf("network %s's mask is invalid", userNet.String()) + } + + networks, err := GetNetworksFromFilesystem(config) + if err != nil { + return err + } + liveNetworks, err := GetLiveNetworks() + if err != nil { + return err + } + logrus.Debugf("checking if network %s exists in cni networks", userNet.String()) + if intersectsConfig, _ := networkIntersectsWithNetworks(userNet, allocatorToIPNets(networks)); intersectsConfig { + return errors.Errorf("network %s is already being used by a cni configuration", userNet.String()) + } + logrus.Debugf("checking if network %s exists in any network interfaces", userNet.String()) + if intersectsLive, _ := networkIntersectsWithNetworks(userNet, liveNetworks); intersectsLive { + return errors.Errorf("network %s is being used by a network interface", userNet.String()) + } + return nil +} + +// RemoveNetwork removes a given network by name. If the network has container associated with it, that +// must be handled outside the context of this. +func RemoveNetwork(config *config.Config, name string) error { + l, err := acquireCNILock(filepath.Join(config.Engine.TmpDir, LockFileName)) + if err != nil { + return err + } + defer l.releaseCNILock() + cniPath, err := GetCNIConfigPathByName(config, name) + if err != nil { + return err + } + // Before we delete the configuration file, we need to make sure we can read and parse + // it to get the network interface name so we can remove that too + interfaceName, err := GetInterfaceNameFromConfig(cniPath) + if err != nil { + return errors.Wrapf(err, "failed to find network interface name in %q", cniPath) + } + liveNetworkNames, err := GetLiveNetworkNames() + if err != nil { + return errors.Wrapf(err, "failed to get live network names") + } + if util.StringInSlice(interfaceName, liveNetworkNames) { + if err := RemoveInterface(interfaceName); err != nil { + return errors.Wrapf(err, "failed to delete the network interface %q", interfaceName) + } + } + // Remove the configuration file + if err := os.Remove(cniPath); err != nil { + return errors.Wrapf(err, "failed to remove network configuration file %q", cniPath) + } + return nil +} + +// InspectNetwork reads a CNI config and returns its configuration +func InspectNetwork(config *config.Config, name string) (map[string]interface{}, error) { + b, err := ReadRawCNIConfByName(config, name) + if err != nil { + return nil, err + } + rawList := make(map[string]interface{}) + err = json.Unmarshal(b, &rawList) + return rawList, err +} + +// Exists says whether a given network exists or not; it meant +// specifically for restful responses so 404s can be used +func Exists(config *config.Config, name string) (bool, error) { + _, err := ReadRawCNIConfByName(config, name) + if err != nil { + if errors.Cause(err) == define.ErrNoSuchNetwork { + return false, nil + } + return false, err + } + return true, nil +} diff --git a/libpod/network/network_test.go b/libpod/network/network_test.go new file mode 100644 index 000000000..1969e792c --- /dev/null +++ b/libpod/network/network_test.go @@ -0,0 +1,35 @@ +package network + +import ( + "net" + "testing" +) + +func parseCIDR(n string) *net.IPNet { + _, parsedNet, _ := net.ParseCIDR(n) + return parsedNet +} + +func Test_networkIntersect(t *testing.T) { + type args struct { + n1 *net.IPNet + n2 *net.IPNet + } + tests := []struct { + name string + args args + want bool + }{ + {"16 and 24 intersects", args{n1: parseCIDR("192.168.0.0/16"), n2: parseCIDR("192.168.1.0/24")}, true}, + {"24 and 25 intersects", args{n1: parseCIDR("192.168.1.0/24"), n2: parseCIDR("192.168.1.0/25")}, true}, + {"Two 24s", args{n1: parseCIDR("192.168.1.0/24"), n2: parseCIDR("192.168.2.0/24")}, false}, + } + for _, tt := range tests { + test := tt + t.Run(tt.name, func(t *testing.T) { + if got := networkIntersect(test.args.n1, test.args.n2); got != test.want { + t.Errorf("networkIntersect() = %v, want %v", got, test.want) + } + }) + } +} diff --git a/libpod/network/subnet.go b/libpod/network/subnet.go new file mode 100644 index 000000000..90f0cdfce --- /dev/null +++ b/libpod/network/subnet.go @@ -0,0 +1,78 @@ +package network + +/* + The code in this was kindly contributed by Dan Williams(dcbw@redhat.com). Many thanks + for his contributions. +*/ + +import ( + "fmt" + "net" +) + +func incByte(subnet *net.IPNet, idx int, shift uint) error { + if idx < 0 { + return fmt.Errorf("no more subnets left") + } + if subnet.IP[idx] == 255 { + subnet.IP[idx] = 0 + return incByte(subnet, idx-1, 0) + } + subnet.IP[idx] += 1 << shift + return nil +} + +// NextSubnet returns subnet incremented by 1 +func NextSubnet(subnet *net.IPNet) (*net.IPNet, error) { + newSubnet := &net.IPNet{ + IP: subnet.IP, + Mask: subnet.Mask, + } + ones, bits := newSubnet.Mask.Size() + if ones == 0 { + return nil, fmt.Errorf("%s has only one subnet", subnet.String()) + } + zeroes := uint(bits - ones) + shift := zeroes % 8 + idx := ones/8 - 1 + if idx < 0 { + idx = 0 + } + if err := incByte(newSubnet, idx, shift); err != nil { + return nil, err + } + return newSubnet, nil +} + +// LastIPInSubnet gets the last IP in a subnet +func LastIPInSubnet(addr *net.IPNet) (net.IP, error) { //nolint:interfacer + // re-parse to ensure clean network address + _, cidr, err := net.ParseCIDR(addr.String()) + if err != nil { + return nil, err + } + + ones, bits := cidr.Mask.Size() + if ones == bits { + return FirstIPInSubnet(cidr) + } + hostStart := ones / 8 + // Handle the first host byte + cidr.IP[hostStart] |= 0xff & cidr.Mask[hostStart] + // Fill the rest with ones + for i := hostStart; i < len(cidr.IP); i++ { + cidr.IP[i] = 0xff + } + return cidr.IP, nil +} + +// FirstIPInSubnet gets the first IP in a subnet +func FirstIPInSubnet(addr *net.IPNet) (net.IP, error) { //nolint:interfacer + // re-parse to ensure clean network address + _, cidr, err := net.ParseCIDR(addr.String()) + if err != nil { + return nil, err + } + cidr.IP[len(cidr.IP)-1]++ + return cidr.IP, nil +} diff --git a/libpod/network/subnet_test.go b/libpod/network/subnet_test.go new file mode 100644 index 000000000..917c3be88 --- /dev/null +++ b/libpod/network/subnet_test.go @@ -0,0 +1,35 @@ +package network + +import ( + "net" + "reflect" + "testing" +) + +func TestNextSubnet(t *testing.T) { + type args struct { + subnet *net.IPNet + } + tests := []struct { + name string + args args + want *net.IPNet + wantErr bool + }{ + {"class b", args{subnet: parseCIDR("192.168.0.0/16")}, parseCIDR("192.169.0.0/16"), false}, + {"class c", args{subnet: parseCIDR("192.168.1.0/24")}, parseCIDR("192.168.2.0/24"), false}, + } + for _, tt := range tests { + test := tt + t.Run(test.name, func(t *testing.T) { + got, err := NextSubnet(test.args.subnet) + if (err != nil) != test.wantErr { + t.Errorf("NextSubnet() error = %v, wantErr %v", err, test.wantErr) + return + } + if !reflect.DeepEqual(got, test.want) { + t.Errorf("NextSubnet() got = %v, want %v", got, test.want) + } + }) + } +} diff --git a/libpod/pod_api.go b/libpod/pod_api.go index 0ae180356..f2ddba9c9 100644 --- a/libpod/pod_api.go +++ b/libpod/pod_api.go @@ -6,6 +6,7 @@ import ( "github.com/containers/podman/v2/libpod/define" "github.com/containers/podman/v2/libpod/events" "github.com/containers/podman/v2/pkg/cgroups" + "github.com/containers/podman/v2/pkg/parallel" "github.com/containers/podman/v2/pkg/rootless" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -99,47 +100,52 @@ func (p *Pod) StopWithTimeout(ctx context.Context, cleanup bool, timeout int) (m return nil, err } - ctrErrors := make(map[string]error) - // TODO: There may be cases where it makes sense to order stops based on // dependencies. Should we bother with this? - // Stop to all containers - for _, ctr := range allCtrs { - ctr.lock.Lock() + ctrErrChan := make(map[string]<-chan error) - if err := ctr.syncContainer(); err != nil { - ctr.lock.Unlock() - ctrErrors[ctr.ID()] = err - continue - } - - // Ignore containers that are not running - if ctr.state.State != define.ContainerStateRunning { - ctr.lock.Unlock() - continue - } - stopTimeout := ctr.config.StopTimeout - if timeout > -1 { - stopTimeout = uint(timeout) - } - if err := ctr.stop(stopTimeout); err != nil { - ctr.lock.Unlock() - ctrErrors[ctr.ID()] = err - continue - } + // Enqueue a function for each container with the parallel executor. + for _, ctr := range allCtrs { + c := ctr + logrus.Debugf("Adding parallel job to stop container %s", c.ID()) + retChan := parallel.Enqueue(ctx, func() error { + // TODO: Might be better to batch stop and cleanup + // together? + if timeout > -1 { + if err := c.StopWithTimeout(uint(timeout)); err != nil { + return err + } + } else { + if err := c.Stop(); err != nil { + return err + } + } - if cleanup { - if err := ctr.cleanup(ctx); err != nil { - ctrErrors[ctr.ID()] = err + if cleanup { + return c.Cleanup(ctx) } - } - ctr.lock.Unlock() + return nil + }) + + ctrErrChan[c.ID()] = retChan } p.newPodEvent(events.Stop) + ctrErrors := make(map[string]error) + + // Get returned error for every container we worked on + for id, channel := range ctrErrChan { + if err := <-channel; err != nil { + if errors.Cause(err) == define.ErrCtrStateInvalid || errors.Cause(err) == define.ErrCtrStopped { + continue + } + ctrErrors[id] = err + } + } + if len(ctrErrors) > 0 { return ctrErrors, errors.Wrapf(define.ErrPodPartialFail, "error stopping some containers") } @@ -169,45 +175,29 @@ func (p *Pod) Cleanup(ctx context.Context) (map[string]error, error) { return nil, err } - ctrErrors := make(map[string]error) + ctrErrChan := make(map[string]<-chan error) - // Clean up all containers + // Enqueue a function for each container with the parallel executor. for _, ctr := range allCtrs { - ctr.lock.Lock() - - if err := ctr.syncContainer(); err != nil { - ctr.lock.Unlock() - ctrErrors[ctr.ID()] = err - continue - } - - // Ignore containers that are running/paused - if !ctr.ensureState(define.ContainerStateConfigured, define.ContainerStateCreated, define.ContainerStateStopped, define.ContainerStateExited) { - ctr.lock.Unlock() - continue - } - - // Check for running exec sessions, ignore containers with them. - sessions, err := ctr.getActiveExecSessions() - if err != nil { - ctr.lock.Unlock() - ctrErrors[ctr.ID()] = err - continue - } - if len(sessions) > 0 { - ctr.lock.Unlock() - continue - } + c := ctr + logrus.Debugf("Adding parallel job to clean up container %s", c.ID()) + retChan := parallel.Enqueue(ctx, func() error { + return c.Cleanup(ctx) + }) - // TODO: Should we handle restart policy here? + ctrErrChan[c.ID()] = retChan + } - ctr.newContainerEvent(events.Cleanup) + ctrErrors := make(map[string]error) - if err := ctr.cleanup(ctx); err != nil { - ctrErrors[ctr.ID()] = err + // Get returned error for every container we worked on + for id, channel := range ctrErrChan { + if err := <-channel; err != nil { + if errors.Cause(err) == define.ErrCtrStateInvalid || errors.Cause(err) == define.ErrCtrStopped { + continue + } + ctrErrors[id] = err } - - ctr.lock.Unlock() } if len(ctrErrors) > 0 { @@ -229,7 +219,7 @@ func (p *Pod) Cleanup(ctx context.Context) (map[string]error, error) { // containers. The container ID is mapped to the error encountered. The error is // set to ErrPodPartialFail. // If both error and the map are nil, all containers were paused without error -func (p *Pod) Pause() (map[string]error, error) { +func (p *Pod) Pause(ctx context.Context) (map[string]error, error) { p.lock.Lock() defer p.lock.Unlock() @@ -252,37 +242,34 @@ func (p *Pod) Pause() (map[string]error, error) { return nil, err } - ctrErrors := make(map[string]error) + ctrErrChan := make(map[string]<-chan error) - // Pause to all containers + // Enqueue a function for each container with the parallel executor. for _, ctr := range allCtrs { - ctr.lock.Lock() + c := ctr + logrus.Debugf("Adding parallel job to pause container %s", c.ID()) + retChan := parallel.Enqueue(ctx, c.Pause) - if err := ctr.syncContainer(); err != nil { - ctr.lock.Unlock() - ctrErrors[ctr.ID()] = err - continue - } + ctrErrChan[c.ID()] = retChan + } - // Ignore containers that are not running - if ctr.state.State != define.ContainerStateRunning { - ctr.lock.Unlock() - continue - } + p.newPodEvent(events.Pause) - if err := ctr.pause(); err != nil { - ctr.lock.Unlock() - ctrErrors[ctr.ID()] = err - continue - } + ctrErrors := make(map[string]error) - ctr.lock.Unlock() + // Get returned error for every container we worked on + for id, channel := range ctrErrChan { + if err := <-channel; err != nil { + if errors.Cause(err) == define.ErrCtrStateInvalid || errors.Cause(err) == define.ErrCtrStopped { + continue + } + ctrErrors[id] = err + } } if len(ctrErrors) > 0 { return ctrErrors, errors.Wrapf(define.ErrPodPartialFail, "error pausing some containers") } - defer p.newPodEvent(events.Pause) return nil, nil } @@ -298,7 +285,7 @@ func (p *Pod) Pause() (map[string]error, error) { // containers. The container ID is mapped to the error encountered. The error is // set to ErrPodPartialFail. // If both error and the map are nil, all containers were unpaused without error. -func (p *Pod) Unpause() (map[string]error, error) { +func (p *Pod) Unpause(ctx context.Context) (map[string]error, error) { p.lock.Lock() defer p.lock.Unlock() @@ -311,38 +298,34 @@ func (p *Pod) Unpause() (map[string]error, error) { return nil, err } - ctrErrors := make(map[string]error) + ctrErrChan := make(map[string]<-chan error) - // Pause to all containers + // Enqueue a function for each container with the parallel executor. for _, ctr := range allCtrs { - ctr.lock.Lock() + c := ctr + logrus.Debugf("Adding parallel job to unpause container %s", c.ID()) + retChan := parallel.Enqueue(ctx, c.Unpause) - if err := ctr.syncContainer(); err != nil { - ctr.lock.Unlock() - ctrErrors[ctr.ID()] = err - continue - } + ctrErrChan[c.ID()] = retChan + } - // Ignore containers that are not paused - if ctr.state.State != define.ContainerStatePaused { - ctr.lock.Unlock() - continue - } + p.newPodEvent(events.Unpause) - if err := ctr.unpause(); err != nil { - ctr.lock.Unlock() - ctrErrors[ctr.ID()] = err - continue - } + ctrErrors := make(map[string]error) - ctr.lock.Unlock() + // Get returned error for every container we worked on + for id, channel := range ctrErrChan { + if err := <-channel; err != nil { + if errors.Cause(err) == define.ErrCtrStateInvalid || errors.Cause(err) == define.ErrCtrStopped { + continue + } + ctrErrors[id] = err + } } if len(ctrErrors) > 0 { return ctrErrors, errors.Wrapf(define.ErrPodPartialFail, "error unpausing some containers") } - - defer p.newPodEvent(events.Unpause) return nil, nil } @@ -411,7 +394,7 @@ func (p *Pod) Restart(ctx context.Context) (map[string]error, error) { // containers. The container ID is mapped to the error encountered. The error is // set to ErrPodPartialFail. // If both error and the map are nil, all containers were signalled successfully. -func (p *Pod) Kill(signal uint) (map[string]error, error) { +func (p *Pod) Kill(ctx context.Context, signal uint) (map[string]error, error) { p.lock.Lock() defer p.lock.Unlock() @@ -424,44 +407,36 @@ func (p *Pod) Kill(signal uint) (map[string]error, error) { return nil, err } - ctrErrors := make(map[string]error) + ctrErrChan := make(map[string]<-chan error) - // Send a signal to all containers + // Enqueue a function for each container with the parallel executor. for _, ctr := range allCtrs { - ctr.lock.Lock() - - if err := ctr.syncContainer(); err != nil { - ctr.lock.Unlock() - ctrErrors[ctr.ID()] = err - continue - } + c := ctr + logrus.Debugf("Adding parallel job to kill container %s", c.ID()) + retChan := parallel.Enqueue(ctx, func() error { + return c.Kill(signal) + }) - // Ignore containers that are not running - if ctr.state.State != define.ContainerStateRunning { - ctr.lock.Unlock() - continue - } + ctrErrChan[c.ID()] = retChan + } - if err := ctr.ociRuntime.KillContainer(ctr, signal, false); err != nil { - ctr.lock.Unlock() - ctrErrors[ctr.ID()] = err - continue - } + p.newPodEvent(events.Kill) - logrus.Debugf("Killed container %s with signal %d", ctr.ID(), signal) + ctrErrors := make(map[string]error) - ctr.state.StoppedByUser = true - if err := ctr.save(); err != nil { - ctrErrors[ctr.ID()] = err + // Get returned error for every container we worked on + for id, channel := range ctrErrChan { + if err := <-channel; err != nil { + if errors.Cause(err) == define.ErrCtrStateInvalid || errors.Cause(err) == define.ErrCtrStopped { + continue + } + ctrErrors[id] = err } - - ctr.lock.Unlock() } if len(ctrErrors) > 0 { return ctrErrors, errors.Wrapf(define.ErrPodPartialFail, "error killing some containers") } - defer p.newPodEvent(events.Kill) return nil, nil } |