From a3e0b7d117251944375fd32449f6b26d65edf367 Mon Sep 17 00:00:00 2001 From: baude Date: Wed, 11 Nov 2020 09:45:07 -0600 Subject: add network connect|disconnect compat endpoints this enables the ability to connect and disconnect a container from a given network. it is only for the compatibility layer. some code had to be refactored to avoid circular imports. additionally, tests are being deferred temporarily due to some incompatibility/bug in either docker-py or our stack. Signed-off-by: baude --- libpod/container.go | 24 ++++++ libpod/define/errors.go | 3 + libpod/events.go | 12 +++ libpod/events/config.go | 8 ++ libpod/events/events.go | 8 ++ libpod/events/journal_linux.go | 6 ++ libpod/events/logfile.go | 2 +- libpod/networking_linux.go | 179 +++++++++++++++++++++++++++-------------- 8 files changed, 182 insertions(+), 60 deletions(-) (limited to 'libpod') diff --git a/libpod/container.go b/libpod/container.go index 9009a4ec8..4b9e6a5ba 100644 --- a/libpod/container.go +++ b/libpod/container.go @@ -206,6 +206,10 @@ type ContainerState struct { // and not delegated to the OCI runtime. ExtensionStageHooks map[string][]spec.Hook `json:"extensionStageHooks,omitempty"` + // NetInterfaceDescriptions describe the relationship between a CNI + // network and an interface names + NetInterfaceDescriptions ContainerNetworkDescriptions `json:"networkDescriptions,omitempty"` + // containerPlatformState holds platform-specific container state. containerPlatformState } @@ -244,6 +248,10 @@ type ContainerImageVolume struct { ReadWrite bool `json:"rw"` } +// ContainerNetworkDescriptions describes the relationship between the CNI +// network and the ethN where N is an integer +type ContainerNetworkDescriptions map[string]int + // Config accessors // Unlocked @@ -1102,3 +1110,19 @@ func (c *Container) networksByNameIndex() (map[string]int, error) { } return networkNamesByIndex, nil } + +// add puts the new given CNI network name into the tracking map +// and assigns it a new integer based on the map length +func (d ContainerNetworkDescriptions) add(networkName string) { + d[networkName] = len(d) +} + +// getInterfaceByName returns a formatted interface name for a given +// network along with a bool as to whether the network existed +func (d ContainerNetworkDescriptions) getInterfaceByName(networkName string) (string, bool) { + val, exists := d[networkName] + if !exists { + return "", exists + } + return fmt.Sprintf("eth%d", val), exists +} diff --git a/libpod/define/errors.go b/libpod/define/errors.go index 471827b7c..b96d36429 100644 --- a/libpod/define/errors.go +++ b/libpod/define/errors.go @@ -178,4 +178,7 @@ var ( // ErrStoreNotInitialized indicates that the container storage was never // initialized. ErrStoreNotInitialized = errors.New("the container storage was never initialized") + + // ErrNoNetwork indicates that a container has no net namespace, like network=none + ErrNoNetwork = errors.New("container has no network namespace") ) diff --git a/libpod/events.go b/libpod/events.go index 95317eb01..e199a3846 100644 --- a/libpod/events.go +++ b/libpod/events.go @@ -50,6 +50,18 @@ func (c *Container) newContainerExitedEvent(exitCode int32) { } } +// netNetworkEvent creates a new event based on a network connect/disconnect +func (c *Container) newNetworkEvent(status events.Status, netName string) { + e := events.NewEvent(status) + e.ID = c.ID() + e.Name = c.Name() + e.Type = events.Network + e.Network = netName + if err := c.runtime.eventer.Write(e); err != nil { + logrus.Errorf("unable to write pod event: %q", err) + } +} + // newPodEvent creates a new event for a libpod pod func (p *Pod) newPodEvent(status events.Status) { e := events.NewEvent(status) diff --git a/libpod/events/config.go b/libpod/events/config.go index 2ec3111fe..af09a65ae 100644 --- a/libpod/events/config.go +++ b/libpod/events/config.go @@ -30,6 +30,8 @@ type Event struct { Image string `json:",omitempty"` // Name where applicable Name string `json:",omitempty"` + // Network is the network name in a network event + Network string `json:"network,omitempty"` // Status describes the event that occurred Status Status // Time the event occurred @@ -101,6 +103,8 @@ const ( Container Type = "container" // Image - event is related to images Image Type = "image" + // Network - event is related to networks + Network Type = "network" // Pod - event is related to pods Pod Type = "pod" // System - event is related to Podman whole and not to any specific @@ -141,6 +145,10 @@ const ( LoadFromArchive Status = "loadfromarchive" // Mount ... Mount Status = "mount" + // NetworkConnect + NetworkConnect Status = "connect" + // NetworkDisconnect + NetworkDisconnect Status = "disconnect" // Pause ... Pause Status = "pause" // Prune ... diff --git a/libpod/events/events.go b/libpod/events/events.go index 42939d64c..4e7267af3 100644 --- a/libpod/events/events.go +++ b/libpod/events/events.go @@ -77,6 +77,8 @@ func (e *Event) ToHumanReadable() string { } } humanFormat += ")" + case Network: + humanFormat = fmt.Sprintf("%s %s %s %s (container=%s, name=%s)", e.Time, e.Type, e.Status, e.ID, e.ID, e.Network) case Image: humanFormat = fmt.Sprintf("%s %s %s %s %s", e.Time, e.Type, e.Status, e.ID, e.Name) case System: @@ -115,6 +117,8 @@ func StringToType(name string) (Type, error) { return Container, nil case Image.String(): return Image, nil + case Network.String(): + return Network, nil case Pod.String(): return Pod, nil case System.String(): @@ -162,6 +166,10 @@ func StringToStatus(name string) (Status, error) { return LoadFromArchive, nil case Mount.String(): return Mount, nil + case NetworkConnect.String(): + return NetworkConnect, nil + case NetworkDisconnect.String(): + return NetworkDisconnect, nil case Pause.String(): return Pause, nil case Prune.String(): diff --git a/libpod/events/journal_linux.go b/libpod/events/journal_linux.go index 5e3be8009..9a514e302 100644 --- a/libpod/events/journal_linux.go +++ b/libpod/events/journal_linux.go @@ -56,6 +56,9 @@ func (e EventJournalD) Write(ee Event) error { } m["PODMAN_LABELS"] = string(b) } + case Network: + m["PODMAN_ID"] = ee.ID + m["PODMAN_NETWORK_NAME"] = ee.Network case Volume: m["PODMAN_NAME"] = ee.Name } @@ -197,6 +200,9 @@ func newEventFromJournalEntry(entry *sdjournal.JournalEntry) (*Event, error) { / newEvent.Details = Details{Attributes: labels} } } + case Network: + newEvent.ID = entry.Fields["PODMAN_ID"] + newEvent.Network = entry.Fields["PODMAN_NETWORK_NAME"] case Image: newEvent.ID = entry.Fields["PODMAN_ID"] } diff --git a/libpod/events/logfile.go b/libpod/events/logfile.go index b70102450..57e38b815 100644 --- a/libpod/events/logfile.go +++ b/libpod/events/logfile.go @@ -76,7 +76,7 @@ func (e EventLogFile) Read(ctx context.Context, options ReadOptions) error { return err } switch event.Type { - case Image, Volume, Pod, System, Container: + case Image, Volume, Pod, System, Container, Network: // no-op default: return errors.Errorf("event type %s is not valid in %s", event.Type.String(), e.options.LogFilePath) diff --git a/libpod/networking_linux.go b/libpod/networking_linux.go index 3882e095a..8dce7c9fe 100644 --- a/libpod/networking_linux.go +++ b/libpod/networking_linux.go @@ -21,6 +21,7 @@ import ( cnitypes "github.com/containernetworking/cni/pkg/types/current" "github.com/containernetworking/plugins/pkg/ns" "github.com/containers/podman/v2/libpod/define" + "github.com/containers/podman/v2/libpod/events" "github.com/containers/podman/v2/libpod/network" "github.com/containers/podman/v2/pkg/errorhandling" "github.com/containers/podman/v2/pkg/netns" @@ -34,16 +35,16 @@ import ( ) // Get an OCICNI network config -func (r *Runtime) getPodNetwork(id, name, nsPath string, networks []string, ports []ocicni.PortMapping, staticIP net.IP, staticMAC net.HardwareAddr) ocicni.PodNetwork { +func (r *Runtime) getPodNetwork(id, name, nsPath string, networks []string, ports []ocicni.PortMapping, staticIP net.IP, staticMAC net.HardwareAddr, netDescriptions ContainerNetworkDescriptions) ocicni.PodNetwork { var networkKey string if len(networks) > 0 { - // This is inconsistent for >1 network, but it's probably the + // This is inconsistent for >1 ctrNetwork, but it's probably the // best we can do. networkKey = networks[0] } else { networkKey = r.netPlugin.GetDefaultNetworkName() } - network := ocicni.PodNetwork{ + ctrNetwork := ocicni.PodNetwork{ Name: name, Namespace: name, // TODO is there something else we should put here? We don't know about Kube namespaces ID: id, @@ -55,9 +56,12 @@ func (r *Runtime) getPodNetwork(id, name, nsPath string, networks []string, port // If we have extra networks, add them if len(networks) > 0 { - network.Networks = make([]ocicni.NetAttachment, len(networks)) + ctrNetwork.Networks = make([]ocicni.NetAttachment, len(networks)) for i, netName := range networks { - network.Networks[i].Name = netName + ctrNetwork.Networks[i].Name = netName + if eth, exists := netDescriptions.getInterfaceByName(netName); exists { + ctrNetwork.Networks[i].Ifname = eth + } } } @@ -66,8 +70,8 @@ func (r *Runtime) getPodNetwork(id, name, nsPath string, networks []string, port // it's just the default. if len(networks) == 0 { // If len(networks) == 0 this is guaranteed to be the - // default network. - network.Networks = []ocicni.NetAttachment{{Name: networkKey}} + // default ctrNetwork. + ctrNetwork.Networks = []ocicni.NetAttachment{{Name: networkKey}} } var rt ocicni.RuntimeConfig = ocicni.RuntimeConfig{PortMappings: ports} if staticIP != nil { @@ -76,12 +80,12 @@ func (r *Runtime) getPodNetwork(id, name, nsPath string, networks []string, port if staticMAC != nil { rt.MAC = staticMAC.String() } - network.RuntimeConfig = map[string]ocicni.RuntimeConfig{ + ctrNetwork.RuntimeConfig = map[string]ocicni.RuntimeConfig{ networkKey: rt, } } - return network + return ctrNetwork } // Create and configure a new network namespace for a container @@ -110,7 +114,12 @@ func (r *Runtime) configureNetNS(ctr *Container, ctrNS ns.NetNS) ([]*cnitypes.Re if err != nil { return nil, err } - podNetwork := r.getPodNetwork(ctr.ID(), podName, ctrNS.Path(), networks, ctr.config.PortMappings, requestedIP, requestedMAC) + + // Update container map of interface descriptions + if err := ctr.setupNetworkDescriptions(networks); err != nil { + return nil, err + } + podNetwork := r.getPodNetwork(ctr.ID(), podName, ctrNS.Path(), networks, ctr.config.PortMappings, requestedIP, requestedMAC, ctr.state.NetInterfaceDescriptions) aliases, err := ctr.runtime.state.GetAllNetworkAliases(ctr) if err != nil { return nil, err @@ -760,7 +769,7 @@ func (r *Runtime) teardownNetNS(ctr *Container) error { requestedMAC = ctr.config.StaticMAC } - podNetwork := r.getPodNetwork(ctr.ID(), ctr.Name(), ctr.state.NetNS.Path(), networks, ctr.config.PortMappings, requestedIP, requestedMAC) + podNetwork := r.getPodNetwork(ctr.ID(), ctr.Name(), ctr.state.NetNS.Path(), networks, ctr.config.PortMappings, requestedIP, requestedMAC, ContainerNetworkDescriptions{}) if err := r.netPlugin.TearDownPod(podNetwork); err != nil { return errors.Wrapf(err, "error tearing down CNI namespace configuration for container %s", ctr.ID()) @@ -934,6 +943,29 @@ func (c *Container) getContainerNetworkInfo() (*define.InspectNetworkSettings, e return settings, nil } +// setupNetworkDescriptions adds networks and eth values to the container's +// network descriptions +func (c *Container) setupNetworkDescriptions(networks []string) error { + // if the map is nil and we have networks + if c.state.NetInterfaceDescriptions == nil && len(networks) > 0 { + c.state.NetInterfaceDescriptions = make(ContainerNetworkDescriptions) + } + origLen := len(c.state.NetInterfaceDescriptions) + for _, n := range networks { + // if the network is not in the map, add it + if _, exists := c.state.NetInterfaceDescriptions[n]; !exists { + c.state.NetInterfaceDescriptions.add(n) + } + } + // if the map changed, we need to save the container state + if origLen != len(c.state.NetInterfaceDescriptions) { + if err := c.save(); err != nil { + return err + } + } + return nil +} + // resultToBasicNetworkConfig produces an InspectBasicNetworkConfig from a CNI // result func resultToBasicNetworkConfig(result *cnitypes.Result) (define.InspectBasicNetworkConfig, error) { @@ -984,19 +1016,14 @@ func (w *logrusDebugWriter) Write(p []byte) (int, error) { return len(p), nil } -// DisconnectContainerFromNetwork removes a container from its CNI network -func (r *Runtime) DisconnectContainerFromNetwork(nameOrID, netName string, force bool) error { - ctr, err := r.LookupContainer(nameOrID) +// NetworkDisconnect removes a container from the network +func (c *Container) NetworkDisconnect(nameOrID, netName string, force bool) error { + networks, err := c.networksByNameIndex() if err != nil { return err } - networks, err := ctr.networksByNameIndex() - if err != nil { - return err - } - - exists, err := network.Exists(r.config, netName) + exists, err := network.Exists(c.runtime.config, netName) if err != nil { return err } @@ -1009,48 +1036,48 @@ func (r *Runtime) DisconnectContainerFromNetwork(nameOrID, netName string, force return errors.Errorf("container %s is not connected to network %s", nameOrID, netName) } - ctr.lock.Lock() - defer ctr.lock.Unlock() - if err := ctr.syncContainer(); err != nil { + c.lock.Lock() + defer c.lock.Unlock() + if err := c.syncContainer(); err != nil { return err } - podConfig := r.getPodNetwork(ctr.ID(), ctr.Name(), ctr.state.NetNS.Path(), []string{netName}, ctr.config.PortMappings, nil, nil) - if err := r.netPlugin.TearDownPod(podConfig); err != nil { + if c.state.State != define.ContainerStateRunning { + return errors.Wrapf(define.ErrCtrStateInvalid, "cannot disconnect container %s from networks as it is not running", nameOrID) + } + if c.state.NetNS == nil { + return errors.Wrapf(define.ErrNoNetwork, "unable to disconnect %s from %s", nameOrID, netName) + } + podConfig := c.runtime.getPodNetwork(c.ID(), c.Name(), c.state.NetNS.Path(), []string{netName}, c.config.PortMappings, nil, nil, c.state.NetInterfaceDescriptions) + if err := c.runtime.netPlugin.TearDownPod(podConfig); err != nil { return err } - if err := r.state.NetworkDisconnect(ctr, netName); err != nil { + if err := c.runtime.state.NetworkDisconnect(c, netName); err != nil { return err } // update network status - networkStatus := ctr.state.NetworkStatus - // if len is one and we confirmed earlier that the container is in - // fact connected to the network, then just return an empty slice - if len(networkStatus) == 1 { - ctr.state.NetworkStatus = make([]*cnitypes.Result, 0) - } else { - // clip out the index of the network - networkStatus[len(networkStatus)-1], networkStatus[index] = networkStatus[index], networkStatus[len(networkStatus)-1] - // shorten the slice by one - ctr.state.NetworkStatus = networkStatus[:len(networkStatus)-1] + networkStatus := c.state.NetworkStatus + // clip out the index of the network + tmpNetworkStatus := make([]*cnitypes.Result, len(networkStatus)-1) + for k, v := range networkStatus { + if index != k { + tmpNetworkStatus = append(tmpNetworkStatus, v) + } } - return nil + c.state.NetworkStatus = tmpNetworkStatus + c.newNetworkEvent(events.NetworkDisconnect, netName) + return c.save() } -// ConnectContainerToNetwork connects a container to a CNI network -func (r *Runtime) ConnectContainerToNetwork(nameOrID, netName string, aliases []string) error { - ctr, err := r.LookupContainer(nameOrID) +// ConnnectNetwork connects a container to a given network +func (c *Container) NetworkConnect(nameOrID, netName string, aliases []string) error { + networks, err := c.networksByNameIndex() if err != nil { return err } - networks, err := ctr.networksByNameIndex() - if err != nil { - return err - } - - exists, err := network.Exists(r.config, netName) + exists, err := network.Exists(c.runtime.config, netName) if err != nil { return err } @@ -1058,25 +1085,34 @@ func (r *Runtime) ConnectContainerToNetwork(nameOrID, netName string, aliases [] return errors.Wrap(define.ErrNoSuchNetwork, netName) } - _, nameExists := networks[netName] - if !nameExists && len(networks) > 0 { - return errors.Errorf("container %s is not connected to network %s", nameOrID, netName) + c.lock.Lock() + defer c.lock.Unlock() + if err := c.syncContainer(); err != nil { + return err } - ctr.lock.Lock() - defer ctr.lock.Unlock() - if err := ctr.syncContainer(); err != nil { + if c.state.State != define.ContainerStateRunning { + return errors.Wrapf(define.ErrCtrStateInvalid, "cannot connect container %s to networks as it is not running", nameOrID) + } + if c.state.NetNS == nil { + return errors.Wrapf(define.ErrNoNetwork, "unable to connect %s to %s", nameOrID, netName) + } + if err := c.runtime.state.NetworkConnect(c, netName, aliases); err != nil { return err } - if err := r.state.NetworkConnect(ctr, netName, aliases); err != nil { + ctrNetworks, err := c.networks() + if err != nil { return err } - - podConfig := r.getPodNetwork(ctr.ID(), ctr.Name(), ctr.state.NetNS.Path(), []string{netName}, ctr.config.PortMappings, nil, nil) + // Update network descriptions + if err := c.setupNetworkDescriptions(ctrNetworks); err != nil { + return err + } + podConfig := c.runtime.getPodNetwork(c.ID(), c.Name(), c.state.NetNS.Path(), []string{netName}, c.config.PortMappings, nil, nil, c.state.NetInterfaceDescriptions) podConfig.Aliases = make(map[string][]string, 1) podConfig.Aliases[netName] = aliases - results, err := r.netPlugin.SetUpPod(podConfig) + results, err := c.runtime.netPlugin.SetUpPod(podConfig) if err != nil { return err } @@ -1094,11 +1130,11 @@ func (r *Runtime) ConnectContainerToNetwork(nameOrID, netName string, aliases [] } // update network status - networkStatus := ctr.state.NetworkStatus + networkStatus := c.state.NetworkStatus // if len is one and we confirmed earlier that the container is in // fact connected to the network, then just return an empty slice if len(networkStatus) == 0 { - ctr.state.NetworkStatus = append(ctr.state.NetworkStatus, networkResults...) + c.state.NetworkStatus = append(c.state.NetworkStatus, networkResults...) } else { // build a list of network names so we can sort and // get the new name's index @@ -1117,5 +1153,30 @@ func (r *Runtime) ConnectContainerToNetwork(nameOrID, netName string, aliases [] copy(networkStatus[index+1:], networkStatus[index:]) networkStatus[index] = networkResults[0] } - return nil + c.newNetworkEvent(events.NetworkConnect, netName) + return c.save() +} + +// DisconnectContainerFromNetwork removes a container from its CNI network +func (r *Runtime) DisconnectContainerFromNetwork(nameOrID, netName string, force bool) error { + if rootless.IsRootless() { + return errors.New("network connect is not enabled for rootless containers") + } + ctr, err := r.LookupContainer(nameOrID) + if err != nil { + return err + } + return ctr.NetworkDisconnect(nameOrID, netName, force) +} + +// ConnectContainerToNetwork connects a container to a CNI network +func (r *Runtime) ConnectContainerToNetwork(nameOrID, netName string, aliases []string) error { + if rootless.IsRootless() { + return errors.New("network disconnect is not enabled for rootless containers") + } + ctr, err := r.LookupContainer(nameOrID) + if err != nil { + return err + } + return ctr.NetworkConnect(nameOrID, netName, aliases) } -- cgit v1.2.3-54-g00ecf