Network Binding Plugin
[v1.4.0, Beta feature]
A modular plugin framework which integrates with Kubevirt to implement a network binding.
This document is aimed at plugin developers which would like to create a custom network binding and integrate it with Kubevirt.
Before proceeding with the reading of this developer guide, please read the Network Binding Plugins user guide.
Overview
Network Connectivity
In order for a VM to have access to external network(s), several layers need to be defined and configured, depending on the connectivity characteristics needs.
These layers include:
- Host connectivity: Network provider.
- Host to Pod connectivity: CNI.
- Pod to domain connectivity: Network Binding.
This guide focuses on the Network Binding portion.
Network Binding
The network binding defines how the domain (VM) network interface is wired in the VM pod through the domain to the guest.
The network binding includes:
- Domain vNIC configuration.
- Pod network configuration (optional).
- Services to deliver network details to the guest (optional). E.g. DHCP server to pass the IP configuration to the guest.
Plugins
The network bindings have been part of Kubevirt core API and codebase. With the increase of the number of network bindings added and frequent requests to tweak and change the existing network bindings, a decision has been made to create a network binding plugin infrastructure.
The plugin infrastructure provides means to compose a network binding plugin and integrate it into Kubevirt in a modular manner.
Kubevirt is providing several network binding plugins as references. The following plugins are available:
The Zero Code Plugin
The simplest network binding plugin is the one that requires no additional code. It takes advantage of the Domain Attachment Type option which provides a pre-defined core Kubevirt method to attach an interface to the domain.
The currently supported domain attachment type is tap (v1.1.1) which
builds a domain interface configuration that points to the tap/macvtap
existing interface.
Such a binding plugin assumes that the CNI used for the network connectivity
exposes in the pod a tap or macvtap (type) interface with a name corresponding
to the hashed network name:
pod<hash network name>(or plaineth0for the primary network)tap<hash network name>(or plaintap0for the primary network)
For secondary networks, Kubevirt virt-controller will define these names on the
pod Multus annotation ("k8s.v1.cni.cncf.io/networks"),
therefore there is no special action needed from the plugin author except
choosing a CNI that confirms with the CNI standard spec.
For clarification, the CNI used for the connectivity is the one defined
in the NetworkAttachmentDefinition object referenced from the VM network spec.
Configuration example
Given the following registered binding plugin:
apiVersion: kubevirt.io/v1
kind: KubeVirt
metadata:
name: kubevirt
namespace: kubevirt
spec:
configuration:
network:
binding:
my-zero-code-binding:
domainAttachmentType: tap
The VM interface/network spec will include a ref to the binding plugin
and the NetworkAttachmentDefinition named mynad:
apiVersion: kubevirt.io/v1
kind: VirtualMachine
metadata:
name: myvm
spec:
template:
spec:
domain:
devices:
interfaces:
- name: mynetwork
binding:
name: my-zero-code-binding
networks:
- name: mynetwork
multus:
networkName: mmynad
The mynad NetworkAttachmentDefinition defines cni-name as the CNI plugin
binary:
Note: The CNI plugin mentioned here is not related to the network binding plugin CNI (which is optional and described in detail later on).
kind: NetworkAttachmentDefinition
apiVersion: k8s.cni.cncf.io/v1
metadata:
name: mynad
spec:
config: '{
"cniVersion": "0.3.1",
"name": "some-name",
"type": "cni-name",
}'
Kubevirt will eventually create a domain interface configuration that
points to the tap (e.g. tap12345678) or pod interface (e.g. pod12345678).
If the interface name that starts with tap does not exist, it will use the one
with pod.
In addition, the mac address of the pod interface is copied to the domain configuration.
This is a snippet of the configuration:
<interface type='ethernet'>
<alias name='ua-mynetwork'/>
<target dev='pod12345678' managed='no'/>
<model type='virtio-non-transitional'/>
<mac address='12:34:56:78:9a:bc'/>
<mtu size='1500'/>
<rom enabled='no'/>
</interface>
A classic example of such a plugin is the macvtap plugin.
The Sidecar Plugin
When a standard domain attachment requires customization, or when additional services are needed (e.g. DHCP), a sidecar container may be executed to integrate with Kubevirt.
The sidecar container runs in parallel to the virt-launcher container which runs the hypervisor (libvirt and qemu).
Sidecar Protocol
Kubevirt supports an integration hook between the virt-launcher and sidecar containers that run in the same pod.
The integration is based on a client-server gRPC protocol. The server side is setup at the sidecar container and the client operates at the virt-launcher container.
From the sidecar perspective, the operation steps are as follows:
-
The sidecar container is defined as part of the virt-launcher pod, by the virt-controller. It includes a mounting point to
HookSocketsSharedDirectory. -
The sidecar starts by listening on an existing unix socket.
import "net"
import "os"
import "path/filepath"
import "kubevirt.io/kubevirt/pkg/hooks"
socketPath := filepath.Join(hooks.HookSocketsSharedDirectory, hookSocket)
socket, err := net.Listen("unix", socketPath)
defer os.Remove(socketPath)
- Then registers handlers for various command requests:
- Info: Returns information about the current server version and its supporting hook points. Clients use this information to correctly interact with the server (e.g. use the relevant requests supported).
- Callback server: Registers the services the server supports.
Depending on the version, the server side supports several operations exposed
as methods. E.g. the
OnDefineDomainmethod which expects a domain configuration and a VMI spec to be provided, returning an updated domain configuration as a result.
import "google.golang.org/grpc"
import info "kubevirt.io/kubevirt/pkg/hooks/info"
import v1alpha2 "kubevirt.io/kubevirt/pkg/hooks/v1alpha2"
server := grpc.NewServer([]grpc.ServerOption{}...)
info.RegisterInfoServer(server, srv.InfoServer{Version: "v1alpha2"})
v1alpha2.RegisterCallbacksServer(server, srv.V1alpha2Server{})
- Finally starting the gRPC server.
server.Serve(socket)
From the client perspective (at the virt-launcher container), it calls
the OnDefineDomain command just after generating the domain configuration
and before sending the domain configuration to the hypervisor (i.e. libvirt).
Therefore, the sidecar may implement any logic that mutates the domain configuration. In the context of the network binding, the sidecar will usually add a domain interface configuration.
Sidecar logic
A network binding sidecar will usually mutate the domain configuration based on the VMI specification.
The logic needs to be added to the OnDefineDomain.
It will usually have a structure similar to the following:
Note:
changeDomainis the function that accepts a domain and VMI spec, returning a new mutated domain spec.
import "kubevirt.io/api/core/v1"
import hooksv1alpha2 "kubevirt.io/kubevirt/pkg/hooks/v1alpha2"
import domainschema "kubevirt.io/kubevirt/pkg/virt-launcher/virtwrap/api"
type V1alpha2Server struct{}
func (s V1alpha2Server) OnDefineDomain(_ context.Context, params *hooksv1alpha2.OnDefineDomainParams) (*hooksv1alpha2.OnDefineDomainResult, error) {
vmi := &v1.VirtualMachineInstance{}
if err := json.Unmarshal(params.GetVmi(), vmi); err != nil {
return nil, fmt.Errorf("failed to unmarshal VMI: %v", err)
}
domainSpec := &domainschema.DomainSpec{
// Unmarshalling domain spec makes the XML namespace attribute empty.
// Some domain parameters requires namespace to be defined.
// e.g: https://libvirt.org/drvqemu.html#pass-through-of-arbitrary-qemu-commands
XmlNS: libvirtDomainQemuSchema,
}
if err := xml.Unmarshal(domainXML, domainSpec); err != nil {
return nil, fmt.Errorf("failed to unmarshal given domain spec: %v", err)
}
updatedDomainSpec, err := changeDomain(domainSpec, vmi)
if err != nil {
return nil, err
}
newDomainXML, err := xml.Marshal(updatedDomainSpec)
if err != nil {
return nil, fmt.Errorf("failed to marshal updated domain spec: %v", err)
}
return &hooksv1alpha2.OnDefineDomainResult{
DomainXML: newDomainXML,
}, nil
}
It is recommended to use the latest available hook version. The unused methods must be defined to satisfy the interface, preferably with a default non-error response.
Pod interface naming
The name of the network interface in the pod, to which the relevant network is to bind to, is expected to be known.
- For the primary network, the default
eth0naming is expected. - For the secondary networks, Kubevirt explicitly specifies the name through
the pods Multus annotation (
"k8s.v1.cni.cncf.io/networks"). The CNI plugin is expected to use the interface name inputted to define the in-pod interface name. The name used is generated by Kubevirt with the following format:pod<network name hash ID>.- Network name hash ID: The hash ID is generated based on the network name specified in the VM/VMI interface & network spec. To programmatically generate it, one can use the following code snippet:
import "kubevirt.io/kubevirt/pkg/network/namescheme"
ifaceName := namescheme.GenerateHashedInterfaceName(network_name)
Domain configuration
Changing the domain configuration usually involves the addition of a device or a change in an existing device.
Either way, it is important to understand which are the networking devices and what are the expectations from Kubevirt:
-
Domain API: "kubevirt.io/kubevirt/pkg/virt-launcher/virtwrap/api"
DomainSpec: The domain configuration spec structure, which is in sync with libvirt domain configuration.Devices:Interface: The network interface configuration spec.HostDevice: The general host device configuration spec. The host-device is used to pass-through devices from the host to the domain. SR-IOV virtual functions are a classic example for networking.
-
Device Alias: Kubevirt marks on the domain device spec an alias that references the logical network name as appearing in the VM/VMI spec.
- For SR-IOV devices, the alias also includes a prefix:
sriov-.
import domainschema "kubevirt.io/kubevirt/pkg/virt-launcher/virtwrap/api" deviceAlias := domainschema.NewUserDefinedAlias("sriov-" + "mynet1") - For SR-IOV devices, the alias also includes a prefix:
-
Parameters: Several configuration parameters of an interface from the VMI spec are passed to the domain device configuration directly (e.g. the mac address). The domain interface and host-device expose additional parameters which are not exposed through the VMI interface spec. Plugin authors may populate other domain parameters if needed, taking the values as hard-coded or from the VMI object (including annotation).
Sidecar Artifacts
The expected artifacts include:
- Documentation on how to register it to Kubevirt and what it does.
- A container image that includes the logic of a network binding plugin sidecar.
- Optionally, a link to a registry that contains the container image.
Note: In order to consume a sidecar, the image needs to be available in an accessible registry. Cluster admins may need to make the sidecar image available on their registry.
The Binding CNI Plugin
We have covered so far network binding plugins that utilize core domain attachment types and/or custom sidecars. While many network bindings can find these integration points sufficient, there is a missing piece.
Network bindings that require pod network changes, should use a CNI plugin.
Using a custom CNI plugin
A CNI plugin is a program that is called by the runtime when the pod is created. The program is expected to interpret a standard CNI format input and perform changes in the pod network namespace.
Note: The CNI plugin discussed in this section is used for implementing a network binding plugin, connecting the pod network to the guest. It is not to be confused with the CNI plugin that connects the pod to a network on the node.
Custom CNI plugin requirements
In order to call multiple times CNI plugins, the cluster requires Multus to be deployed.
In addition, the CNI plugin itself needs to be deployed on the cluster nodes.
Note: It is out of this document scope to elaborate on how a binary can be deployed on cluster nodes and kept up-to-date.
A network binding CNI plugin belongs to a binding, and it is accompanied by a Network Attachment Definition (NAD) that references the binary.
Creating a network binding CNI plugin
At the time of this writing, there is no known document or tutorial on how to compose a CNI plugin. However, there are many CNI plugins around to use as references.
We will take the same approach here and reference the passt binding CNI.
It is very simple to follow, performing minimal changes on the pod netns.
Ref: https://github.com/kubevirt/kubevirt/tree/release-1.1/cmd/cniplugins/passt-binding
Note: A CNI plugin outputs its results to
stdout, therefore avoid outputting logging tostdout(e.g. use offmt.Printf()) as it will corrupt the results.
Network Binding CNI Operation Flow
Understanding the details of how the binding CNI plugin integrates into a VM creation may assist in its proper usage.
The following steps occur when an interface is defined with a network binding plugin that uses a custom binding CNI:
- The
virt-controllerinspects the VMI spec and iterates over the network interfaces. - For each interface, if a network binding plugin is used with
a network-attachment-definition defined, the
virt-controllerwill add to thevirt-launcherpod network (multus) annotation a plugin entry that references the binding network-attachment-definition. - The binding entry includes:
- The network-attachment-definition name and namespace.
- The
cni-args, which allows passing arbitrary data to the CNI plugin. At the time of this writing, it is used to pass the logical network name. Identified by the field key:logicNetworkName.
- The
virt-controllercreates thevirt-launcherpod and eventually the runtime calls the CNI interface with the combined data from the network-attachment-definition and pod network annotation. - Multus acts as a CNI delegator, calling all CNI plugins, including Kubevirt
network binding CNI plugins.
- First, it calls the "regular" network CNI plugin which connects the pod to the relevant network on the pod.
- Secondly, it calls the network binding CNI plugin, if it exists, to configure the pod networking per the binding needs.
Note: The network binding CNI entry in the pod network annotation is injected after the "regular" connectivity entry.
Network Binding CNI Logic
The CNI plugin used for the network binding is focused on performing the needed changes in the pod network namespace. It is important to avoid using it to perform changes on the host side.
In order to identify on which network interface context the plugin is called,
the logicNetworkName can be used.
From it, the possible pod interface name can be calculated:
import "kubevirt.io/kubevirt/pkg/network/namescheme"
podIfaceName := namescheme.GenerateHashedInterfaceName(networkName)
It is possible to perform both interface specific and interface agnostic changes in the pod netns. It is important to consider that there may be other CNI plugins called in the chain which may collide with such changes. Therefore, it is important to take into consideration these factors and try to limit the changes to specific kernel interfaces as much as possible.
Network Binding CNI and sidecars
The need to share data from the CNI stage to the virt-launcher stage may arise.
There may be several options to implement this, one relatively simple
method could be to preserve the data on a dummy interface.
Aa a network binding plugin author, both the CNI plugin and the sidecar codebase
is available, therefore both can be in sync to share such information.
Migration Support
The VM migration operation is considered a core feature in Kubevirt and therefore the ability to support it needs to be addressed when designing a network binding plugin.
The following points should be addressed in relation to migration support:
- Kubevirt will allow migration only if the primary network supports it.
- For a network binding plugin to support migration, the
migrationmethodmust be specified in the Kubevirt CR. See the user-guide network binding plugin migration section on how to define it. - The backend
libvirtandqemuneeds to support migration for the specific device that is used in the domain.- The
hostdevicedevices in the domain do not support migration out-of-the-box. The network binding plugin infrastructure does not support migration for interfaces that have ahostdevicebackend.
- The
Note: Network interfaces with
hostdevicebackend require a special handling from Kubevirt. One such handling is to unplug the device at the source before migration is started and plugging it back at the destination after the migration completes. Future development will consider adding a new migration method to support migration for such interfaces.
Compute Resource Overhead
Some plugins may need additional resources to be added to the compute container of the virt-launcher pod.
It is possible to specify compute resource overhead that will be added to the compute container of virt-launcher pods
derived from virtual machines using the plugin.
Note: At the moment, only memory overhead requests are supported.
For a network binding plugin to support compute resource overhead, the computeResourceOverhead field
must be specified in the Kubevirt CR.
See the user-guide network binding plugin section on how to define it.
Network plugin user sockets
Some plugins may need to create additional sockets beyond the gRPC one used for control communication between the sidecar and compute containers. A common use case is establishing an extra communication channel between the compute container's virtualization stack and the network plugin running in the sidecar.
KubeVirt scans the /var/run/kubevirt-hooks directory in the sidecar for gRPC sockets to communicate with the sidecar.
Therefore, this directory must not contain any unrelated files or sockets.
To avoid conflicts, place additional sockets in a subdirectory under /var/run/kubevirt-hooks, such as /var/run/kubevirt-hooks/userdata.
Note: Some plugins may need to know the path accessible from the compute container for a specific sidecar.
In such case, use /var/run/kubevirt-hooks/<sidecar container name>. The sidecar's container name can be obtained from the CONTAINER_NAME environment variable.