Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions api/core/v1alpha2/finalizers.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,5 @@ const (
FinalizerVMBDACleanup = "virtualization.deckhouse.io/vmbda-cleanup"
FinalizerMACAddressCleanup = "virtualization.deckhouse.io/vmmac-cleanup"
FinalizerMACAddressLeaseCleanup = "virtualization.deckhouse.io/vmmacl-cleanup"
FinalizerNodeUSBDeviceCleanup = "virtualization.deckhouse.io/nodeusbdevice-cleanup"
)
106 changes: 106 additions & 0 deletions api/core/v1alpha2/node_device_usb.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*
Copyright 2024 Flant JSC

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package v1alpha2

import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// NodeUSBDevice represents a USB device discovered on a specific node in the cluster.
// This resource is created automatically by the DRA (Dynamic Resource Allocation) system
// when a USB device is detected on a node.
// +genclient
// +kubebuilder:object:root=true
// +kubebuilder:metadata:labels={heritage=deckhouse,module=virtualization}
// +kubebuilder:resource:categories={virtualization},scope=Namespaced,shortName={nusb},singular=nodeusbdevice
// +kubebuilder:subresource:status
// +kubebuilder:printcolumn:name="Node",type=string,JSONPath=`.status.nodeName`
// +kubebuilder:printcolumn:name="Ready",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].status`
// +kubebuilder:printcolumn:name="Assigned",type=string,JSONPath=`.status.conditions[?(@.type=="Assigned")].status`
// +kubebuilder:printcolumn:name="Namespace",type=string,JSONPath=`.spec.assignedNamespace`
// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type NodeUSBDevice struct {
metav1.TypeMeta `json:",inline"`

metav1.ObjectMeta `json:"metadata,omitempty"`

Spec NodeUSBDeviceSpec `json:"spec"`

Status NodeUSBDeviceStatus `json:"status,omitempty"`
}

// NodeUSBDeviceList provides the needed parameters
// for requesting a list of NodeUSBDevices from the system.
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type NodeUSBDeviceList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata"`

// Items provides a list of NodeUSBDevices.
Items []NodeUSBDevice `json:"items"`
}

type NodeUSBDeviceSpec struct {
// Namespace in which the device usage is allowed. By default, created with an empty value "".
// When set, a corresponding USBDevice resource is created in this namespace.
// +kubebuilder:default:=""
AssignedNamespace string `json:"assignedNamespace,omitempty"`
}

type NodeUSBDeviceStatus struct {
// All device attributes obtained through DRA for the device.
Attributes NodeUSBDeviceAttributes `json:"attributes,omitempty"`
// Name of the node where the USB device is located.
NodeName string `json:"nodeName,omitempty"`
// The latest available observations of an object's current state.
Conditions []metav1.Condition `json:"conditions,omitempty"`
}

// NodeUSBDeviceAttributes contains all attributes of a USB device.
type NodeUSBDeviceAttributes struct {
// BCD (Binary Coded Decimal) device version.
BCD string `json:"bcd,omitempty"`
// USB bus number.
Bus string `json:"bus,omitempty"`
// USB device number on the bus.
DeviceNumber string `json:"deviceNumber,omitempty"`
// Device path in the filesystem.
DevicePath string `json:"devicePath,omitempty"`
// Major device number.
Major int `json:"major,omitempty"`
// Minor device number.
Minor int `json:"minor,omitempty"`
// Device name.
Name string `json:"name,omitempty"`
// USB vendor ID in hexadecimal format.
VendorID string `json:"vendorID,omitempty"`
// USB product ID in hexadecimal format.
ProductID string `json:"productID,omitempty"`
// Device serial number.
Serial string `json:"serial,omitempty"`
// Device manufacturer name.
Manufacturer string `json:"manufacturer,omitempty"`
// Device product name.
Product string `json:"product,omitempty"`
// Node name where the device is located.
NodeName string `json:"nodeName,omitempty"`
// Hash calculated based on all main attributes. Required to uniquely match
// the resource with a resource from the slice.
Hash string `json:"hash,omitempty"`
}

Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ import (
"github.com/deckhouse/virtualization-controller/pkg/controller/vmiplease"
"github.com/deckhouse/virtualization-controller/pkg/controller/vmmac"
"github.com/deckhouse/virtualization-controller/pkg/controller/vmmaclease"
"github.com/deckhouse/virtualization-controller/pkg/controller/nodeusbdevice"
"github.com/deckhouse/virtualization-controller/pkg/controller/vmop"
"github.com/deckhouse/virtualization-controller/pkg/controller/vmrestore"
"github.com/deckhouse/virtualization-controller/pkg/controller/vmsnapshot"
Expand Down Expand Up @@ -375,6 +376,12 @@ func main() {
os.Exit(1)
}

nodeusbdeviceLogger := logger.NewControllerLogger(nodeusbdevice.ControllerName, logLevel, logOutput, logDebugVerbosity, logDebugControllerList)
if _, err = nodeusbdevice.NewController(ctx, mgr, nodeusbdeviceLogger); err != nil {
log.Error(err.Error())
os.Exit(1)
}

vdsnapshotLogger := logger.NewControllerLogger(vdsnapshot.ControllerName, logLevel, logOutput, logDebugVerbosity, logDebugControllerList)
if _, err = vdsnapshot.NewController(ctx, mgr, vdsnapshotLogger, virtClient); err != nil {
log.Error(err.Error())
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
Copyright 2025 Flant JSC

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package internal

import (
"context"
"fmt"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/reconcile"

"github.com/deckhouse/virtualization-controller/pkg/controller/conditions"
"github.com/deckhouse/virtualization-controller/pkg/controller/nodeusbdevice/internal/state"
"github.com/deckhouse/virtualization-controller/pkg/eventrecord"
)

const (
nameAssignedHandler = "AssignedHandler"
)

func NewAssignedHandler(client client.Client, recorder eventrecord.EventRecorderLogger) *AssignedHandler {
return &AssignedHandler{
client: client,
recorder: recorder,
}
}

type AssignedHandler struct {
client client.Client
recorder eventrecord.EventRecorderLogger
}

func (h *AssignedHandler) Handle(ctx context.Context, s state.NodeUSBDeviceState) (reconcile.Result, error) {
nodeUSBDevice := s.NodeUSBDevice()

if nodeUSBDevice.IsEmpty() {
return reconcile.Result{}, nil
}

current := nodeUSBDevice.Current()
changed := nodeUSBDevice.Changed()

assignedNamespace := changed.Spec.AssignedNamespace
previousNamespace := current.Spec.AssignedNamespace

// Update Assigned condition
var reason, message string
var status metav1.ConditionStatus

if assignedNamespace != "" {
// TODO: When USBDevice resource is defined, create/check it here
// For now, just mark as Assigned when namespace is set
reason = "Assigned"
message = fmt.Sprintf("Namespace %s is assigned for the device", assignedNamespace)
status = metav1.ConditionTrue
} else {
reason = "Available"
message = "No namespace is assigned for the device"
status = metav1.ConditionFalse
}

cb := conditions.NewConditionBuilder("Assigned").
Generation(changed.Generation).
Status(status).
Reason(reason).
Message(message)

conditions.SetCondition(cb, &changed.Status.Conditions)

return reconcile.Result{}, nil
}

// TODO: Implement USBDevice creation/deletion when USBDevice resource is defined

func (h *AssignedHandler) Name() string {
return nameAssignedHandler
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
Copyright 2025 Flant JSC

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package internal

import (
"context"

k8serrors "k8s.io/apimachinery/pkg/api/errors"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/reconcile"

"github.com/deckhouse/virtualization-controller/pkg/controller/nodeusbdevice/internal/state"
"github.com/deckhouse/virtualization-controller/pkg/eventrecord"
"github.com/deckhouse/virtualization/api/core/v1alpha2"
)

const (
nameDeletionHandler = "DeletionHandler"
)

func NewDeletionHandler(client client.Client, recorder eventrecord.EventRecorderLogger) *DeletionHandler {
return &DeletionHandler{
client: client,
recorder: recorder,
}
}

type DeletionHandler struct {
client client.Client
recorder eventrecord.EventRecorderLogger
}

func (h *DeletionHandler) Handle(ctx context.Context, s state.NodeUSBDeviceState) (reconcile.Result, error) {
nodeUSBDevice := s.NodeUSBDevice()

if nodeUSBDevice.IsEmpty() {
return reconcile.Result{}, nil
}

current := nodeUSBDevice.Current()
changed := nodeUSBDevice.Changed()

// Add finalizer if not deleting
if current.GetDeletionTimestamp().IsZero() {
controllerutil.AddFinalizer(changed, v1alpha2.FinalizerNodeUSBDeviceCleanup)
return reconcile.Result{}, nil
}

// TODO: When USBDevice resource is defined, delete it from namespace here
// Resource is being deleted - clean up USBDevice in namespace
// if current.Spec.AssignedNamespace != "" {
// if err := h.deleteUSBDevice(ctx, current.Spec.AssignedNamespace, current); err != nil {
// return reconcile.Result{}, err
// }
// }

// Remove finalizer
controllerutil.RemoveFinalizer(changed, v1alpha2.FinalizerNodeUSBDeviceCleanup)

return reconcile.Result{}, nil
}

// TODO: Implement USBDevice deletion when USBDevice resource is defined

func (h *DeletionHandler) Name() string {
return nameDeletionHandler
}
Loading
Loading