Skip to content

Commit

Permalink
hooks: create OnDefineDomain hook point and use it
Browse files Browse the repository at this point in the history
  • Loading branch information
phoracek committed Jul 2, 2018
1 parent 434c21e commit 38c2ab9
Show file tree
Hide file tree
Showing 9 changed files with 161 additions and 103 deletions.
15 changes: 13 additions & 2 deletions cluster/examples/hook-sidecar-consumer.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
status: {}
apiVersion: kubevirt.io/v1alpha1
kind: VirtualMachine
metadata:
Expand All @@ -18,14 +19,24 @@ spec:
bus: virtio
name: registrydisk
volumeName: registryvolume
- disk:
bus: virtio
name: cloudinitdisk
volumeName: cloudinitvolume
machine:
type: ""
resources:
requests:
memory: 64M
memory: 1024M
terminationGracePeriodSeconds: 0
volumes:
- name: registryvolume
registryDisk:
image: registry:5000/kubevirt/cirros-registry-disk-demo:devel
image: registry:5000/kubevirt/fedora-cloud-registry-disk-demo:devel
- cloudInitNoCloud:
userData: |-
#cloud-config
password: fedora
chpasswd: { expire: False }
name: cloudinitvolume
status: {}
55 changes: 23 additions & 32 deletions cmd/example-hook-sidecar/README.md
Original file line number Diff line number Diff line change
@@ -1,37 +1,28 @@
# KubeVirt SMBIOS hook sidecar

Example VM definition:
To use this hook, use following annotations:

```yaml
apiVersion: kubevirt.io/v1alpha2
kind: VirtualMachineInstance
metadata:
creationTimestamp: null
labels:
special: vm-hook-sidecar-consumer
name: vm-hook-sidecar-consumer
annotations:
# Request the hook sidecar
hooks.kubevirt.io/hookSidecars: '[{"image": "registry:5000/kubevirt/example-hook-sidecar:devel"}]'
# Overwrite base board manufacturer name
smbios.vm.kubevirt.io/baseBoardManufacturer: "Radical Edward"
spec:
domain:
devices:
disks:
- disk:
bus: virtio
name: registrydisk
volumeName: registryvolume
machine:
type: ""
resources:
requests:
memory: 64M
terminationGracePeriodSeconds: 0
volumes:
- name: registryvolume
registryDisk:
image: registry:5000/kubevirt/cirros-registry-disk-demo:devel
status: {}
annotations:
# Request the hook sidecar
hooks.kubevirt.io/hookSidecars: '[{"image": "registry:5000/kubevirt/example-hook-sidecar:devel"}]'
# Overwrite base board manufacturer name
smbios.vm.kubevirt.io/baseBoardManufacturer: "Radical Edward"
```
## Example
```shell
# Create a VM requesting the hook sidecar
cluster/kubectl.sh create -f cluster/examples/hook-sidecar-consumer.yml

# Once the VM is ready, connect to its display and login using name and password "fedora"
cluster/virtctl.sh vnc vm-hook-sidecar-consumer

# Install dmidecode
sudo dnf install -y dmidecode

# Check whether the base board manufacturer value was successfully overwritten
sudo dmidecode -s baseboard-manufacturer
```

18 changes: 6 additions & 12 deletions cmd/example-hook-sidecar/smbios.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func (s v1alpha1Server) OnDefineDomain(ctx context.Context, params *hooksV1alpha

vmJSON := params.GetVm()
vmSpec := vmSchema.VirtualMachine{}
err := json.Unmarshal([]byte(vmJSON), &vmSpec)
err := json.Unmarshal(vmJSON, &vmSpec)
if err != nil {
log.Log.Reason(err).Errorf("Failed to unmarshal given VM spec: %s", vmJSON)
panic(err)
Expand All @@ -54,45 +54,39 @@ func (s v1alpha1Server) OnDefineDomain(ctx context.Context, params *hooksV1alpha
annotations := vmSpec.GetAnnotations()

if _, found := annotations[baseBoardManufacturerAnnotation]; !found {
log.Log.Info("SMBIOS hook sidecar was requested, but no attributes provided. Returning original domain spec")
log.Log.Info("SM BIOS hook sidecar was requested, but no attributes provided. Returning original domain spec")
return &hooksV1alpha1.OnDefineDomainResult{
DomainXML: params.GetDomainXML(),
}, nil
}

domainXML := params.GetDomainXML()
domainSpec := domainSchema.DomainSpec{}
err = xml.Unmarshal([]byte(domainXML), &domainSpec)
err = xml.Unmarshal(domainXML, &domainSpec)
if err != nil {
log.Log.Reason(err).Errorf("Failed to unmarshal given domain spec: %s", domainXML)
panic(err)
}

if domainSpec.OS.SMBios == nil {
domainSpec.OS.SMBios = &domainSchema.SMBios{Mode: "sysinfo"}
} else {
domainSpec.OS.SMBios.Mode = "sysinfo"
}
domainSpec.OS.SMBios = &domainSchema.SMBios{Mode: "sysinfo"}

if domainSpec.SysInfo == nil {
domainSpec.SysInfo = &domainSchema.SysInfo{}
}
domainSpec.Type = "smbios"
domainSpec.SysInfo.Type = "smbios"
if baseBoardManufacturer, found := annotations[baseBoardManufacturerAnnotation]; found {
domainSpec.SysInfo.BaseBoard = append(domainSpec.SysInfo.BaseBoard, domainSchema.Entry{
Name: "manufacturer",
Value: baseBoardManufacturer,
})
}

newDomainXMLRaw, err := xml.Marshal(domainSpec)
newDomainXML, err := xml.Marshal(domainSpec)
if err != nil {
log.Log.Reason(err).Errorf("Failed to marshal updated domain spec: %s", domainSpec)
panic(err)
}

newDomainXML := string(newDomainXMLRaw[:])

log.Log.Info("Successfully updated original domain spec with requested SMBIOS attributes")

return &hooksV1alpha1.OnDefineDomainResult{
Expand Down
8 changes: 4 additions & 4 deletions pkg/hooks/info/api.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

114 changes: 86 additions & 28 deletions pkg/hooks/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package hooks

import (
"context"
"encoding/json"
"encoding/xml"
"fmt"
"io/ioutil"
"net"
Expand All @@ -12,24 +14,26 @@ import (

"google.golang.org/grpc"

"kubevirt.io/kubevirt/pkg/api/v1"
hooksInfo "kubevirt.io/kubevirt/pkg/hooks/info"
hooksV1alpha1 "kubevirt.io/kubevirt/pkg/hooks/v1alpha1"
"kubevirt.io/kubevirt/pkg/log"
domainSchema "kubevirt.io/kubevirt/pkg/virt-launcher/virtwrap/api"
virtwrapApi "kubevirt.io/kubevirt/pkg/virt-launcher/virtwrap/api"
)

type hookClient struct {
client interface{}
version string
name string
hookPoints []*hooksInfo.HookPoint
type callackClient struct {
SocketPath string
Version string
subsribedHookPoints []*hooksInfo.HookPoint
}

var manager *Manager
var once sync.Once

type Manager struct {
collected bool
callbacksPerHookPoint map[string][]*hookClient
callbacksPerHookPoint map[string][]*callackClient
}

func GetManager() *Manager {
Expand All @@ -55,8 +59,8 @@ func (m *Manager) Collect(numberOfRequestedHookSidecars uint) error {
return nil
}

func collectSideCarSockets(numberOfRequestedHookSidecars uint) (map[string][]*hookClient, error) {
callbacksPerHookPoint := make(map[string][]*hookClient)
func collectSideCarSockets(numberOfRequestedHookSidecars uint) (map[string][]*callackClient, error) {
callbacksPerHookPoint := make(map[string][]*callackClient)
processedSockets := make(map[string]bool)

for uint(len(processedSockets)) < numberOfRequestedHookSidecars {
Expand All @@ -70,7 +74,7 @@ func collectSideCarSockets(numberOfRequestedHookSidecars uint) (map[string][]*ho
continue
}

hookClient, notReady, err := processSideCarSocket(HookSocketsSharedDirectory + "/" + socket.Name())
callackClient, notReady, err := processSideCarSocket(HookSocketsSharedDirectory + "/" + socket.Name())
if notReady {
log.Log.Info("Sidecar server might not be ready yet, retrying in the next iteration")
continue
Expand All @@ -79,8 +83,8 @@ func collectSideCarSockets(numberOfRequestedHookSidecars uint) (map[string][]*ho
return nil, err
}

for _, hookPoint := range hookClient.hookPoints {
callbacksPerHookPoint[hookPoint.GetName()] = append(callbacksPerHookPoint[hookPoint.GetName()], hookClient)
for _, subsribedHookPoint := range callackClient.subsribedHookPoints {
callbacksPerHookPoint[subsribedHookPoint.GetName()] = append(callbacksPerHookPoint[subsribedHookPoint.GetName()], callackClient)
}

processedSockets[socket.Name()] = true
Expand All @@ -92,14 +96,8 @@ func collectSideCarSockets(numberOfRequestedHookSidecars uint) (map[string][]*ho
return callbacksPerHookPoint, nil
}

func processSideCarSocket(socketPath string) (*hookClient, bool, error) {
conn, err := grpc.Dial(
socketPath,
grpc.WithInsecure(),
grpc.WithDialer(func(addr string, timeout time.Duration) (net.Conn, error) {
return net.DialTimeout("unix", addr, timeout)
}),
)
func processSideCarSocket(socketPath string) (*callackClient, bool, error) {
conn, err := dialSocket(socketPath)
if err != nil {
log.Log.Reason(err).Infof("Failed to Dial hook socket: %s", socketPath)
return nil, true, nil
Expand All @@ -118,27 +116,87 @@ func processSideCarSocket(socketPath string) (*hookClient, bool, error) {
}

if _, found := versionsSet[hooksV1alpha1.Version]; found {
return &hookClient{
client: hooksV1alpha1.NewCallbacksClient(conn),
name: info.GetName(),
version: hooksV1alpha1.Version,
hookPoints: info.GetHookPoints(),
return &callackClient{
SocketPath: socketPath,
Version: hooksV1alpha1.Version,
subsribedHookPoints: info.GetHookPoints(),
}, false, nil
} else {
return nil, false, fmt.Errorf("Hook sidecar does not expose a supported version. Exposed versions: %v, supported versions: %s", versionsSet, hooksV1alpha1.Version)
}
}

func sortCallbacksPerHookPoint(callbacksPerHookPoint map[string][]*hookClient) {
func sortCallbacksPerHookPoint(callbacksPerHookPoint map[string][]*callackClient) {
for _, callbacks := range callbacksPerHookPoint {
for _, callback := range callbacks {
sort.Slice(callbacks, func(i, j int) bool {
if callback.hookPoints[i].Priority == callback.hookPoints[j].Priority {
return strings.Compare(callback.hookPoints[i].Name, callback.hookPoints[j].Name) < 0
if callback.subsribedHookPoints[i].Priority == callback.subsribedHookPoints[j].Priority {
return strings.Compare(callback.subsribedHookPoints[i].Name, callback.subsribedHookPoints[j].Name) < 0
} else {
return callback.hookPoints[i].Priority > callback.hookPoints[j].Priority
return callback.subsribedHookPoints[i].Priority > callback.subsribedHookPoints[j].Priority
}
})
}
}
}

func (m *Manager) OnDefineDomain(domainSpec *virtwrapApi.DomainSpec, vm *v1.VirtualMachine) (*virtwrapApi.DomainSpec, error) {
if !m.collected {
return nil, fmt.Errorf("Hook sidecars have not been collected yet")
}

if callbacks, found := m.callbacksPerHookPoint[hooksInfo.OnDefineDomainHookPointName]; found {
for _, callback := range callbacks {
if callback.Version == hooksV1alpha1.Version {
domainSpecXML, err := xml.Marshal(domainSpec)
if err != nil {
return nil, fmt.Errorf("Failed to marshal domain spec: %v", domainSpec)
}
vmJSON, err := json.Marshal(vm)
if err != nil {
return nil, fmt.Errorf("Failed to marshal VM spec: %v", vm)
}

conn, err := dialSocket(callback.SocketPath)
if err != nil {
log.Log.Reason(err).Infof("Failed to Dial hook socket: %s", callback.SocketPath)
return nil, err
}
defer conn.Close()

client := hooksV1alpha1.NewCallbacksClient(conn)

result, err := client.OnDefineDomain(context.Background(), &hooksV1alpha1.OnDefineDomainParams{
DomainXML: domainSpecXML,
Vm: vmJSON,
})
if err != nil {
return nil, err
}

newDomainSpecXML := result.GetDomainXML()
newDomainSpec := domainSchema.DomainSpec{}
err = xml.Unmarshal(newDomainSpecXML, &newDomainSpec)
if err != nil {
return nil, fmt.Errorf("Failed to unmarshal given domain spec: %s", newDomainSpecXML)
}

domainSpec = &newDomainSpec
} else {
panic("Should never happen, version compatibility check is done during Info call")
}
}
}

return domainSpec, nil
}

func dialSocket(socketPath string) (*grpc.ClientConn, error) {
return grpc.Dial(
socketPath,
grpc.WithInsecure(),
grpc.WithDialer(func(addr string, timeout time.Duration) (net.Conn, error) {
return net.DialTimeout("unix", addr, timeout)
}),
)
}
Loading

0 comments on commit 38c2ab9

Please sign in to comment.