diff --git a/.gitignore b/.gitignore index ae343f44fd..ea43c942a8 100644 --- a/.gitignore +++ b/.gitignore @@ -60,3 +60,5 @@ retry/ # nodejs node_modules/ package-lock.json + +static-libs/ diff --git a/Taskfile.yaml b/Taskfile.yaml index 93eef4dcee..32615e6126 100644 --- a/Taskfile.yaml +++ b/Taskfile.yaml @@ -165,8 +165,7 @@ tasks: "ports": [ { "containerPort": 2345, "name": "dlv" } ], "readinessProbe": null, "livenessProbe": null, - "command": null, - "args": [] + "command": null }, { "name": "proxy", @@ -264,8 +263,7 @@ tasks: "ports": [ { "containerPort": 2345, "name": "dlv" } ], "readinessProbe": null, "livenessProbe": null, - "command": null, - "args": [] + "command": null }, { "name": "proxy", diff --git a/api/client/generated/clientset/versioned/typed/core/v1alpha2/fake/fake_virtualmachine_expansion.go b/api/client/generated/clientset/versioned/typed/core/v1alpha2/fake/fake_virtualmachine_expansion.go index 470f7bf441..184ba60924 100644 --- a/api/client/generated/clientset/versioned/typed/core/v1alpha2/fake/fake_virtualmachine_expansion.go +++ b/api/client/generated/clientset/versioned/typed/core/v1alpha2/fake/fake_virtualmachine_expansion.go @@ -54,3 +54,7 @@ func (c *fakeVirtualMachines) RemoveVolume(ctx context.Context, name string, opt func (c *fakeVirtualMachines) CancelEvacuation(ctx context.Context, name string, dryRun []string) error { return nil } + +func (c *fakeVirtualMachines) USBRedir(ctx context.Context, name string) (corev1alpha2.StreamInterface, error) { + return nil, nil +} diff --git a/api/client/generated/clientset/versioned/typed/core/v1alpha2/virtualmachine_expansion.go b/api/client/generated/clientset/versioned/typed/core/v1alpha2/virtualmachine_expansion.go index 3fcd693016..6ac745cba4 100644 --- a/api/client/generated/clientset/versioned/typed/core/v1alpha2/virtualmachine_expansion.go +++ b/api/client/generated/clientset/versioned/typed/core/v1alpha2/virtualmachine_expansion.go @@ -35,6 +35,7 @@ type VirtualMachineExpansion interface { AddVolume(ctx context.Context, name string, opts v1alpha2.VirtualMachineAddVolume) error RemoveVolume(ctx context.Context, name string, opts v1alpha2.VirtualMachineRemoveVolume) error CancelEvacuation(ctx context.Context, name string, dryRun []string) error + USBRedir(ctx context.Context, name string) (StreamInterface, error) } type SerialConsoleOptions struct { @@ -81,3 +82,7 @@ func (c *virtualMachines) RemoveVolume(ctx context.Context, name string, opts v1 func (c *virtualMachines) CancelEvacuation(ctx context.Context, name string, dryRun []string) error { return fmt.Errorf("not implemented") } + +func (c *virtualMachines) USBRedir(ctx context.Context, name string) (StreamInterface, error) { + return nil, fmt.Errorf("not implemented") +} diff --git a/api/client/kubeclient/async.go b/api/client/kubeclient/async.go index 61ed3f56d0..530ef0a1df 100644 --- a/api/client/kubeclient/async.go +++ b/api/client/kubeclient/async.go @@ -127,10 +127,7 @@ func asyncSubresourceHelper( case err = <-errChan: return nil, err case ws := <-aws.Connection: - return &wsStreamer{ - conn: ws, - done: done, - }, nil + return newWebsocketStreamer(ws, done), nil } } diff --git a/api/client/kubeclient/client.go b/api/client/kubeclient/client.go index bb233e5eac..63f9c7c483 100644 --- a/api/client/kubeclient/client.go +++ b/api/client/kubeclient/client.go @@ -24,7 +24,6 @@ import ( "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" - "github.com/deckhouse/virtualization/api/client/generated/clientset/versioned" virtualizationv1alpha2 "github.com/deckhouse/virtualization/api/client/generated/clientset/versioned/typed/core/v1alpha2" coreinstall "github.com/deckhouse/virtualization/api/core/install" subinstall "github.com/deckhouse/virtualization/api/subresources/install" @@ -52,72 +51,22 @@ func init() { type Client interface { kubernetes.Interface - ClusterVirtualImages() virtualizationv1alpha2.ClusterVirtualImageInterface - VirtualMachines(namespace string) virtualizationv1alpha2.VirtualMachineInterface - VirtualImages(namespace string) virtualizationv1alpha2.VirtualImageInterface - VirtualDisks(namespace string) virtualizationv1alpha2.VirtualDiskInterface - VirtualMachineBlockDeviceAttachments(namespace string) virtualizationv1alpha2.VirtualMachineBlockDeviceAttachmentInterface - VirtualMachineIPAddresses(namespace string) virtualizationv1alpha2.VirtualMachineIPAddressInterface - VirtualMachineIPAddressLeases() virtualizationv1alpha2.VirtualMachineIPAddressLeaseInterface - VirtualMachineOperations(namespace string) virtualizationv1alpha2.VirtualMachineOperationInterface - VirtualMachineClasses() virtualizationv1alpha2.VirtualMachineClassInterface - VirtualMachineMACAddresses(namespace string) virtualizationv1alpha2.VirtualMachineMACAddressInterface - VirtualMachineMACAddressLeases() virtualizationv1alpha2.VirtualMachineMACAddressLeaseInterface + virtualizationv1alpha2.VirtualizationV1alpha2Interface } type client struct { kubernetes.Interface + virtualizationv1alpha2.VirtualizationV1alpha2Interface config *rest.Config shallowCopy *rest.Config restClient *rest.RESTClient - virtClient *versioned.Clientset } func (c client) VirtualMachines(namespace string) virtualizationv1alpha2.VirtualMachineInterface { return &vm{ - VirtualMachineInterface: c.virtClient.VirtualizationV1alpha2().VirtualMachines(namespace), + VirtualMachineInterface: c.VirtualizationV1alpha2Interface.VirtualMachines(namespace), restClient: c.restClient, config: c.config, namespace: namespace, resource: "virtualmachines", } } - -func (c client) ClusterVirtualImages() virtualizationv1alpha2.ClusterVirtualImageInterface { - return c.virtClient.VirtualizationV1alpha2().ClusterVirtualImages() -} - -func (c client) VirtualImages(namespace string) virtualizationv1alpha2.VirtualImageInterface { - return c.virtClient.VirtualizationV1alpha2().VirtualImages(namespace) -} - -func (c client) VirtualDisks(namespace string) virtualizationv1alpha2.VirtualDiskInterface { - return c.virtClient.VirtualizationV1alpha2().VirtualDisks(namespace) -} - -func (c client) VirtualMachineBlockDeviceAttachments(namespace string) virtualizationv1alpha2.VirtualMachineBlockDeviceAttachmentInterface { - return c.virtClient.VirtualizationV1alpha2().VirtualMachineBlockDeviceAttachments(namespace) -} - -func (c client) VirtualMachineIPAddresses(namespace string) virtualizationv1alpha2.VirtualMachineIPAddressInterface { - return c.virtClient.VirtualizationV1alpha2().VirtualMachineIPAddresses(namespace) -} - -func (c client) VirtualMachineIPAddressLeases() virtualizationv1alpha2.VirtualMachineIPAddressLeaseInterface { - return c.virtClient.VirtualizationV1alpha2().VirtualMachineIPAddressLeases() -} - -func (c client) VirtualMachineOperations(namespace string) virtualizationv1alpha2.VirtualMachineOperationInterface { - return c.virtClient.VirtualizationV1alpha2().VirtualMachineOperations(namespace) -} - -func (c client) VirtualMachineClasses() virtualizationv1alpha2.VirtualMachineClassInterface { - return c.virtClient.VirtualizationV1alpha2().VirtualMachineClasses() -} - -func (c client) VirtualMachineMACAddresses(namespace string) virtualizationv1alpha2.VirtualMachineMACAddressInterface { - return c.virtClient.VirtualizationV1alpha2().VirtualMachineMACAddresses(namespace) -} - -func (c client) VirtualMachineMACAddressLeases() virtualizationv1alpha2.VirtualMachineMACAddressLeaseInterface { - return c.virtClient.VirtualizationV1alpha2().VirtualMachineMACAddressLeases() -} diff --git a/api/client/kubeclient/config.go b/api/client/kubeclient/config.go index 11738c025e..dcdf4bddc5 100644 --- a/api/client/kubeclient/config.go +++ b/api/client/kubeclient/config.go @@ -79,11 +79,11 @@ func GetClientFromRESTConfig(config *rest.Config) (Client, error) { return nil, err } return &client{ - Interface: clientset, - config: config, - shallowCopy: &shallowCopy, - restClient: restClient, - virtClient: virtClient, + Interface: clientset, + VirtualizationV1alpha2Interface: virtClient.VirtualizationV1alpha2(), + config: config, + shallowCopy: &shallowCopy, + restClient: restClient, }, nil } diff --git a/api/client/kubeclient/streamer.go b/api/client/kubeclient/streamer.go index 216bfd2f0e..f953e34bc4 100644 --- a/api/client/kubeclient/streamer.go +++ b/api/client/kubeclient/streamer.go @@ -75,7 +75,7 @@ func (c *wsConn) SetDeadline(t time.Time) error { return c.Conn.SetReadDeadline(t) } -func NewWebsocketStreamer(conn *websocket.Conn, done chan struct{}) *wsStreamer { +func newWebsocketStreamer(conn *websocket.Conn, done chan struct{}) *wsStreamer { return &wsStreamer{ conn: conn, done: done, diff --git a/api/client/kubeclient/vm.go b/api/client/kubeclient/vm.go index 2eb665ab57..259332234d 100644 --- a/api/client/kubeclient/vm.go +++ b/api/client/kubeclient/vm.go @@ -165,3 +165,7 @@ func (v vm) CancelEvacuation(ctx context.Context, name string, dryRun []string) } return c.Do(ctx).Error() } + +func (v vm) USBRedir(_ context.Context, name string) (virtualizationv1alpha2.StreamInterface, error) { + return asyncSubresourceHelper(v.config, v.resource, v.namespace, name, "usbredir", url.Values{}) +} diff --git a/api/subresources/register.go b/api/subresources/register.go index 9ee6a8786f..57a1cab08d 100644 --- a/api/subresources/register.go +++ b/api/subresources/register.go @@ -57,6 +57,7 @@ func addKnownTypes(scheme *runtime.Scheme) error { &VirtualMachineFreeze{}, &VirtualMachineUnfreeze{}, &VirtualMachineCancelEvacuation{}, + &VirtualMachineUSBRedir{}, ) return nil } diff --git a/api/subresources/types.go b/api/subresources/types.go index 5120aa5eb9..155d9b02e6 100644 --- a/api/subresources/types.go +++ b/api/subresources/types.go @@ -88,3 +88,9 @@ type VirtualMachineCancelEvacuation struct { DryRun []string } + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +type VirtualMachineUSBRedir struct { + metav1.TypeMeta +} diff --git a/api/subresources/v1alpha2/register.go b/api/subresources/v1alpha2/register.go index 944a906c66..13ef1d7a54 100644 --- a/api/subresources/v1alpha2/register.go +++ b/api/subresources/v1alpha2/register.go @@ -59,6 +59,7 @@ func addKnownTypes(scheme *runtime.Scheme) error { &VirtualMachineFreeze{}, &VirtualMachineUnfreeze{}, &VirtualMachineCancelEvacuation{}, + &VirtualMachineUSBRedir{}, ) return nil } diff --git a/api/subresources/v1alpha2/types.go b/api/subresources/v1alpha2/types.go index ddbbb77f8a..d94f18dd0a 100644 --- a/api/subresources/v1alpha2/types.go +++ b/api/subresources/v1alpha2/types.go @@ -94,3 +94,10 @@ type VirtualMachineCancelEvacuation struct { DryRun []string `json:"dryRun,omitempty"` } + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +k8s:conversion-gen:explicit-from=net/url.Values + +type VirtualMachineUSBRedir struct { + metav1.TypeMeta `json:",inline"` +} diff --git a/api/subresources/v1alpha2/zz_generated.conversion.go b/api/subresources/v1alpha2/zz_generated.conversion.go index d0572c32b0..bdead8d84b 100644 --- a/api/subresources/v1alpha2/zz_generated.conversion.go +++ b/api/subresources/v1alpha2/zz_generated.conversion.go @@ -108,6 +108,16 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } + if err := s.AddGeneratedConversionFunc((*VirtualMachineUSBRedir)(nil), (*subresources.VirtualMachineUSBRedir)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha2_VirtualMachineUSBRedir_To_subresources_VirtualMachineUSBRedir(a.(*VirtualMachineUSBRedir), b.(*subresources.VirtualMachineUSBRedir), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*subresources.VirtualMachineUSBRedir)(nil), (*VirtualMachineUSBRedir)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_subresources_VirtualMachineUSBRedir_To_v1alpha2_VirtualMachineUSBRedir(a.(*subresources.VirtualMachineUSBRedir), b.(*VirtualMachineUSBRedir), scope) + }); err != nil { + return err + } if err := s.AddGeneratedConversionFunc((*VirtualMachineUnfreeze)(nil), (*subresources.VirtualMachineUnfreeze)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1alpha2_VirtualMachineUnfreeze_To_subresources_VirtualMachineUnfreeze(a.(*VirtualMachineUnfreeze), b.(*subresources.VirtualMachineUnfreeze), scope) }); err != nil { @@ -158,6 +168,11 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } + if err := s.AddGeneratedConversionFunc((*url.Values)(nil), (*VirtualMachineUSBRedir)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_url_Values_To_v1alpha2_VirtualMachineUSBRedir(a.(*url.Values), b.(*VirtualMachineUSBRedir), scope) + }); err != nil { + return err + } if err := s.AddGeneratedConversionFunc((*url.Values)(nil), (*VirtualMachineUnfreeze)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_url_Values_To_v1alpha2_VirtualMachineUnfreeze(a.(*url.Values), b.(*VirtualMachineUnfreeze), scope) }); err != nil { @@ -460,6 +475,35 @@ func Convert_url_Values_To_v1alpha2_VirtualMachineRemoveVolume(in *url.Values, o return autoConvert_url_Values_To_v1alpha2_VirtualMachineRemoveVolume(in, out, s) } +func autoConvert_v1alpha2_VirtualMachineUSBRedir_To_subresources_VirtualMachineUSBRedir(in *VirtualMachineUSBRedir, out *subresources.VirtualMachineUSBRedir, s conversion.Scope) error { + return nil +} + +// Convert_v1alpha2_VirtualMachineUSBRedir_To_subresources_VirtualMachineUSBRedir is an autogenerated conversion function. +func Convert_v1alpha2_VirtualMachineUSBRedir_To_subresources_VirtualMachineUSBRedir(in *VirtualMachineUSBRedir, out *subresources.VirtualMachineUSBRedir, s conversion.Scope) error { + return autoConvert_v1alpha2_VirtualMachineUSBRedir_To_subresources_VirtualMachineUSBRedir(in, out, s) +} + +func autoConvert_subresources_VirtualMachineUSBRedir_To_v1alpha2_VirtualMachineUSBRedir(in *subresources.VirtualMachineUSBRedir, out *VirtualMachineUSBRedir, s conversion.Scope) error { + return nil +} + +// Convert_subresources_VirtualMachineUSBRedir_To_v1alpha2_VirtualMachineUSBRedir is an autogenerated conversion function. +func Convert_subresources_VirtualMachineUSBRedir_To_v1alpha2_VirtualMachineUSBRedir(in *subresources.VirtualMachineUSBRedir, out *VirtualMachineUSBRedir, s conversion.Scope) error { + return autoConvert_subresources_VirtualMachineUSBRedir_To_v1alpha2_VirtualMachineUSBRedir(in, out, s) +} + +func autoConvert_url_Values_To_v1alpha2_VirtualMachineUSBRedir(in *url.Values, out *VirtualMachineUSBRedir, s conversion.Scope) error { + // WARNING: Field TypeMeta does not have json tag, skipping. + + return nil +} + +// Convert_url_Values_To_v1alpha2_VirtualMachineUSBRedir is an autogenerated conversion function. +func Convert_url_Values_To_v1alpha2_VirtualMachineUSBRedir(in *url.Values, out *VirtualMachineUSBRedir, s conversion.Scope) error { + return autoConvert_url_Values_To_v1alpha2_VirtualMachineUSBRedir(in, out, s) +} + func autoConvert_v1alpha2_VirtualMachineUnfreeze_To_subresources_VirtualMachineUnfreeze(in *VirtualMachineUnfreeze, out *subresources.VirtualMachineUnfreeze, s conversion.Scope) error { return nil } diff --git a/api/subresources/v1alpha2/zz_generated.deepcopy.go b/api/subresources/v1alpha2/zz_generated.deepcopy.go index 65bcb843ec..11ae1ff1a9 100644 --- a/api/subresources/v1alpha2/zz_generated.deepcopy.go +++ b/api/subresources/v1alpha2/zz_generated.deepcopy.go @@ -212,6 +212,31 @@ func (in *VirtualMachineRemoveVolume) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VirtualMachineUSBRedir) DeepCopyInto(out *VirtualMachineUSBRedir) { + *out = *in + out.TypeMeta = in.TypeMeta + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VirtualMachineUSBRedir. +func (in *VirtualMachineUSBRedir) DeepCopy() *VirtualMachineUSBRedir { + if in == nil { + return nil + } + out := new(VirtualMachineUSBRedir) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *VirtualMachineUSBRedir) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *VirtualMachineUnfreeze) DeepCopyInto(out *VirtualMachineUnfreeze) { *out = *in diff --git a/api/subresources/zz_generated.deepcopy.go b/api/subresources/zz_generated.deepcopy.go index c09a9c8b33..4e9cacea83 100644 --- a/api/subresources/zz_generated.deepcopy.go +++ b/api/subresources/zz_generated.deepcopy.go @@ -212,6 +212,31 @@ func (in *VirtualMachineRemoveVolume) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VirtualMachineUSBRedir) DeepCopyInto(out *VirtualMachineUSBRedir) { + *out = *in + out.TypeMeta = in.TypeMeta + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VirtualMachineUSBRedir. +func (in *VirtualMachineUSBRedir) DeepCopy() *VirtualMachineUSBRedir { + if in == nil { + return nil + } + out := new(VirtualMachineUSBRedir) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *VirtualMachineUSBRedir) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *VirtualMachineUnfreeze) DeepCopyInto(out *VirtualMachineUnfreeze) { *out = *in diff --git a/images/qemu/werf.inc.yaml b/images/qemu/werf.inc.yaml index 6c693b48be..12fdd876b3 100644 --- a/images/qemu/werf.inc.yaml +++ b/images/qemu/werf.inc.yaml @@ -108,7 +108,7 @@ altLibraries: - libnfs-devel - libblkio-devel libpmem-devel - libdaxctl-devel -- libcacard-devel libusbredir-devel libepoxy-devel libgbm-devel +- libcacard-devel libusbredir-devel libusbredir libepoxy-devel libgbm-devel - libvitastor-devel libiscsi-devel glusterfs-coreutils - libglusterfs11-api-devel - libvdeplug-devel @@ -325,6 +325,7 @@ shell: --enable-selinux \ --enable-slirp \ --enable-snappy \ + --enable-spice \ --enable-spice-protocol \ --enable-system \ --enable-tcg \ diff --git a/images/virt-launcher/werf.inc.yaml b/images/virt-launcher/werf.inc.yaml index fc98fe7205..4df60bef31 100644 --- a/images/virt-launcher/werf.inc.yaml +++ b/images/virt-launcher/werf.inc.yaml @@ -284,6 +284,7 @@ shell: LIBS+=" /usr/lib64/libtpms* /usr/lib64/libjson* /usr/lib64/libfuse*" LIBS+=" /usr/lib64/libxml2.s* /usr/lib64/libgcc_s* /usr/lib64/libaudit*" LIBS+=" /usr/lib64/libisoburn.s* /usr/lib64/libacl.s*" + LIBS+=" /usr/lib64/libusbredir*" echo "Relocate additional libs for files in /VBINS" ./relocate_binaries.sh -i "$FILES" -o /VBINS diff --git a/images/virtualization-artifact/pkg/apiserver/api/generated/openapi/zz_generated.openapi.go b/images/virtualization-artifact/pkg/apiserver/api/generated/openapi/zz_generated.openapi.go index 0e08b8ca39..1658f9d42a 100644 --- a/images/virtualization-artifact/pkg/apiserver/api/generated/openapi/zz_generated.openapi.go +++ b/images/virtualization-artifact/pkg/apiserver/api/generated/openapi/zz_generated.openapi.go @@ -49,6 +49,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/deckhouse/virtualization/api/subresources/v1alpha2.VirtualMachineFreeze": schema_virtualization_api_subresources_v1alpha2_VirtualMachineFreeze(ref), "github.com/deckhouse/virtualization/api/subresources/v1alpha2.VirtualMachinePortForward": schema_virtualization_api_subresources_v1alpha2_VirtualMachinePortForward(ref), "github.com/deckhouse/virtualization/api/subresources/v1alpha2.VirtualMachineRemoveVolume": schema_virtualization_api_subresources_v1alpha2_VirtualMachineRemoveVolume(ref), + "github.com/deckhouse/virtualization/api/subresources/v1alpha2.VirtualMachineUSBRedir": schema_virtualization_api_subresources_v1alpha2_VirtualMachineUSBRedir(ref), "github.com/deckhouse/virtualization/api/subresources/v1alpha2.VirtualMachineUnfreeze": schema_virtualization_api_subresources_v1alpha2_VirtualMachineUnfreeze(ref), "github.com/deckhouse/virtualization/api/subresources/v1alpha2.VirtualMachineVNC": schema_virtualization_api_subresources_v1alpha2_VirtualMachineVNC(ref), "k8s.io/apimachinery/pkg/apis/meta/v1.APIGroup": schema_pkg_apis_meta_v1_APIGroup(ref), @@ -957,6 +958,32 @@ func schema_virtualization_api_subresources_v1alpha2_VirtualMachineRemoveVolume( } } +func schema_virtualization_api_subresources_v1alpha2_VirtualMachineUSBRedir(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + } +} + func schema_virtualization_api_subresources_v1alpha2_VirtualMachineUnfreeze(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ diff --git a/images/virtualization-artifact/pkg/apiserver/api/install.go b/images/virtualization-artifact/pkg/apiserver/api/install.go index 40a5acc6ee..6674458fef 100644 --- a/images/virtualization-artifact/pkg/apiserver/api/install.go +++ b/images/virtualization-artifact/pkg/apiserver/api/install.go @@ -66,6 +66,7 @@ func Build(store *storage.VirtualMachineStorage) genericapiserver.APIGroupInfo { "virtualmachines/freeze": store.FreezeREST(), "virtualmachines/unfreeze": store.UnfreezeREST(), "virtualmachines/cancelevacuation": store.CancelEvacuationREST(), + "virtualmachines/usbredir": store.USBRedirREST(), } apiGroupInfo.VersionedResourcesStorageMap[subv1alpha2.SchemeGroupVersion.Version] = resourcesV1alpha2 return apiGroupInfo diff --git a/images/virtualization-artifact/pkg/apiserver/registry/vm/rest/usbredir.go b/images/virtualization-artifact/pkg/apiserver/registry/vm/rest/usbredir.go new file mode 100644 index 0000000000..c378cb531d --- /dev/null +++ b/images/virtualization-artifact/pkg/apiserver/registry/vm/rest/usbredir.go @@ -0,0 +1,100 @@ +/* +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 rest + +import ( + "context" + "fmt" + "net/http" + "net/url" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/registry/rest" + + "github.com/deckhouse/virtualization-controller/pkg/tls/certmanager" + virtlisters "github.com/deckhouse/virtualization/api/client/generated/listers/core/v1alpha2" + "github.com/deckhouse/virtualization/api/subresources" +) + +type USBRedirREST struct { + vmLister virtlisters.VirtualMachineLister + proxyCertManager certmanager.CertificateManager + kubevirt KubevirtAPIServerConfig +} + +var ( + _ rest.Storage = &USBRedirREST{} + _ rest.Connecter = &USBRedirREST{} +) + +func NewUSBRedirREST(vmLister virtlisters.VirtualMachineLister, kubevirt KubevirtAPIServerConfig, proxyCertManager certmanager.CertificateManager) *USBRedirREST { + return &USBRedirREST{ + vmLister: vmLister, + kubevirt: kubevirt, + proxyCertManager: proxyCertManager, + } +} + +func (r USBRedirREST) New() runtime.Object { + return &subresources.VirtualMachineUSBRedir{} +} + +// Destroy implements rest.Storage interface +func (r USBRedirREST) Destroy() { +} + +func (r USBRedirREST) Connect(ctx context.Context, name string, opts runtime.Object, responder rest.Responder) (http.Handler, error) { + options, ok := opts.(*subresources.VirtualMachineUSBRedir) + if !ok { + return nil, fmt.Errorf("invalid options object: %#v", opts) + } + location, transport, err := USBRedirLocation(ctx, r.vmLister, name, options, r.kubevirt, r.proxyCertManager) + if err != nil { + return nil, err + } + handler := newThrottledUpgradeAwareProxyHandler(location, transport, true, responder, r.kubevirt.ServiceAccount) + return handler, nil +} + +// NewConnectOptions implements rest.Connecter interface +func (r USBRedirREST) NewConnectOptions() (runtime.Object, bool, string) { + return &subresources.VirtualMachineUSBRedir{}, false, "" +} + +// ConnectMethods implements rest.Connecter interface +func (r USBRedirREST) ConnectMethods() []string { + return upgradeableMethods +} + +func USBRedirLocation( + ctx context.Context, + getter virtlisters.VirtualMachineLister, + name string, + _ *subresources.VirtualMachineUSBRedir, + kubevirt KubevirtAPIServerConfig, + proxyCertManager certmanager.CertificateManager, +) (*url.URL, *http.Transport, error) { + return streamLocation( + ctx, + getter, + name, + newKVVMIPather("usbredir"), + kubevirt, + proxyCertManager, + virtualMachineShouldBeRunning, + ) +} diff --git a/images/virtualization-artifact/pkg/apiserver/registry/vm/storage/storage.go b/images/virtualization-artifact/pkg/apiserver/registry/vm/storage/storage.go index d024fbb9e3..19a42df39c 100644 --- a/images/virtualization-artifact/pkg/apiserver/registry/vm/storage/storage.go +++ b/images/virtualization-artifact/pkg/apiserver/registry/vm/storage/storage.go @@ -43,6 +43,7 @@ type VirtualMachineStorage struct { freeze *vmrest.FreezeREST unfreeze *vmrest.UnfreezeREST cancelEvacuation *vmrest.CancelEvacuationREST + usbRedir *vmrest.USBRedirREST vmClient versionedv1alpha2.VirtualMachinesGetter } @@ -70,6 +71,7 @@ func NewStorage( freeze: vmrest.NewFreezeREST(vmLister, kubevirt, proxyCertManager), unfreeze: vmrest.NewUnfreezeREST(vmLister, kubevirt, proxyCertManager), cancelEvacuation: vmrest.NewCancelEvacuationREST(vmLister, kubevirt, proxyCertManager), + usbRedir: vmrest.NewUSBRedirREST(vmLister, kubevirt, proxyCertManager), vmClient: vmClient, } } @@ -106,6 +108,10 @@ func (store VirtualMachineStorage) CancelEvacuationREST() *vmrest.CancelEvacuati return store.cancelEvacuation } +func (store VirtualMachineStorage) USBRedirREST() *vmrest.USBRedirREST { + return store.usbRedir +} + // New implements rest.Storage interface func (store VirtualMachineStorage) New() runtime.Object { return &subv1alpha2.VirtualMachine{} diff --git a/images/virtualization-artifact/pkg/controller/kvbuilder/kvvm.go b/images/virtualization-artifact/pkg/controller/kvbuilder/kvvm.go index 07c3df0628..fa5d6e993b 100644 --- a/images/virtualization-artifact/pkg/controller/kvbuilder/kvvm.go +++ b/images/virtualization-artifact/pkg/controller/kvbuilder/kvvm.go @@ -650,3 +650,7 @@ func (b *KVVM) SetMetadata(metadata metav1.ObjectMeta) { func (b *KVVM) SetUpdateVolumesStrategy(strategy *virtv1.UpdateVolumesStrategy) { b.Resource.Spec.UpdateVolumesStrategy = strategy } + +func (b *KVVM) SetClientPassthroughDevices() { + b.Resource.Spec.Template.Spec.Domain.Devices.ClientPassthrough = &virtv1.ClientPassthroughDevices{} +} diff --git a/images/virtualization-artifact/pkg/controller/kvbuilder/kvvm_utils.go b/images/virtualization-artifact/pkg/controller/kvbuilder/kvvm_utils.go index 034d25714a..67c701f89a 100644 --- a/images/virtualization-artifact/pkg/controller/kvbuilder/kvvm_utils.go +++ b/images/virtualization-artifact/pkg/controller/kvbuilder/kvvm_utils.go @@ -111,6 +111,7 @@ func ApplyVirtualMachineSpec( return err } + kvvm.SetClientPassthroughDevices() kvvm.SetMetadata(vm.ObjectMeta) setNetwork(kvvm, networkSpec) kvvm.SetTablet("default-0") diff --git a/images/virtualization-artifact/pkg/controller/service/mock.go b/images/virtualization-artifact/pkg/controller/service/mock.go index f6640ebfaa..c987ad9d95 100644 --- a/images/virtualization-artifact/pkg/controller/service/mock.go +++ b/images/virtualization-artifact/pkg/controller/service/mock.go @@ -65,6 +65,7 @@ import ( storagev1alpha1 "k8s.io/client-go/kubernetes/typed/storage/v1alpha1" storagev1beta1 "k8s.io/client-go/kubernetes/typed/storage/v1beta1" storagemigrationv1alpha1 "k8s.io/client-go/kubernetes/typed/storagemigration/v1alpha1" + "k8s.io/client-go/rest" "sigs.k8s.io/controller-runtime/pkg/client" "sync" ) @@ -880,6 +881,9 @@ var _ VirtClient = &VirtClientMock{} // PolicyV1beta1Func: func() policyv1beta1.PolicyV1beta1Interface { // panic("mock out the PolicyV1beta1 method") // }, +// RESTClientFunc: func() rest.Interface { +// panic("mock out the RESTClient method") +// }, // RbacV1Func: func() rbacv1.RbacV1Interface { // panic("mock out the RbacV1 method") // }, @@ -919,6 +923,9 @@ var _ VirtClient = &VirtClientMock{} // StoragemigrationV1alpha1Func: func() storagemigrationv1alpha1.StoragemigrationV1alpha1Interface { // panic("mock out the StoragemigrationV1alpha1 method") // }, +// VirtualDiskSnapshotsFunc: func(namespace string) corev1alpha2.VirtualDiskSnapshotInterface { +// panic("mock out the VirtualDiskSnapshots method") +// }, // VirtualDisksFunc: func(namespace string) corev1alpha2.VirtualDiskInterface { // panic("mock out the VirtualDisks method") // }, @@ -946,6 +953,15 @@ var _ VirtClient = &VirtClientMock{} // VirtualMachineOperationsFunc: func(namespace string) corev1alpha2.VirtualMachineOperationInterface { // panic("mock out the VirtualMachineOperations method") // }, +// VirtualMachineRestoresFunc: func(namespace string) corev1alpha2.VirtualMachineRestoreInterface { +// panic("mock out the VirtualMachineRestores method") +// }, +// VirtualMachineSnapshotOperationsFunc: func(namespace string) corev1alpha2.VirtualMachineSnapshotOperationInterface { +// panic("mock out the VirtualMachineSnapshotOperations method") +// }, +// VirtualMachineSnapshotsFunc: func(namespace string) corev1alpha2.VirtualMachineSnapshotInterface { +// panic("mock out the VirtualMachineSnapshots method") +// }, // VirtualMachinesFunc: func(namespace string) corev1alpha2.VirtualMachineInterface { // panic("mock out the VirtualMachines method") // }, @@ -1088,6 +1104,9 @@ type VirtClientMock struct { // PolicyV1beta1Func mocks the PolicyV1beta1 method. PolicyV1beta1Func func() policyv1beta1.PolicyV1beta1Interface + // RESTClientFunc mocks the RESTClient method. + RESTClientFunc func() rest.Interface + // RbacV1Func mocks the RbacV1 method. RbacV1Func func() rbacv1.RbacV1Interface @@ -1127,6 +1146,9 @@ type VirtClientMock struct { // StoragemigrationV1alpha1Func mocks the StoragemigrationV1alpha1 method. StoragemigrationV1alpha1Func func() storagemigrationv1alpha1.StoragemigrationV1alpha1Interface + // VirtualDiskSnapshotsFunc mocks the VirtualDiskSnapshots method. + VirtualDiskSnapshotsFunc func(namespace string) corev1alpha2.VirtualDiskSnapshotInterface + // VirtualDisksFunc mocks the VirtualDisks method. VirtualDisksFunc func(namespace string) corev1alpha2.VirtualDiskInterface @@ -1154,6 +1176,15 @@ type VirtClientMock struct { // VirtualMachineOperationsFunc mocks the VirtualMachineOperations method. VirtualMachineOperationsFunc func(namespace string) corev1alpha2.VirtualMachineOperationInterface + // VirtualMachineRestoresFunc mocks the VirtualMachineRestores method. + VirtualMachineRestoresFunc func(namespace string) corev1alpha2.VirtualMachineRestoreInterface + + // VirtualMachineSnapshotOperationsFunc mocks the VirtualMachineSnapshotOperations method. + VirtualMachineSnapshotOperationsFunc func(namespace string) corev1alpha2.VirtualMachineSnapshotOperationInterface + + // VirtualMachineSnapshotsFunc mocks the VirtualMachineSnapshots method. + VirtualMachineSnapshotsFunc func(namespace string) corev1alpha2.VirtualMachineSnapshotInterface + // VirtualMachinesFunc mocks the VirtualMachines method. VirtualMachinesFunc func(namespace string) corev1alpha2.VirtualMachineInterface @@ -1291,6 +1322,9 @@ type VirtClientMock struct { // PolicyV1beta1 holds details about calls to the PolicyV1beta1 method. PolicyV1beta1 []struct { } + // RESTClient holds details about calls to the RESTClient method. + RESTClient []struct { + } // RbacV1 holds details about calls to the RbacV1 method. RbacV1 []struct { } @@ -1330,6 +1364,11 @@ type VirtClientMock struct { // StoragemigrationV1alpha1 holds details about calls to the StoragemigrationV1alpha1 method. StoragemigrationV1alpha1 []struct { } + // VirtualDiskSnapshots holds details about calls to the VirtualDiskSnapshots method. + VirtualDiskSnapshots []struct { + // Namespace is the namespace argument value. + Namespace string + } // VirtualDisks holds details about calls to the VirtualDisks method. VirtualDisks []struct { // Namespace is the namespace argument value. @@ -1369,6 +1408,21 @@ type VirtClientMock struct { // Namespace is the namespace argument value. Namespace string } + // VirtualMachineRestores holds details about calls to the VirtualMachineRestores method. + VirtualMachineRestores []struct { + // Namespace is the namespace argument value. + Namespace string + } + // VirtualMachineSnapshotOperations holds details about calls to the VirtualMachineSnapshotOperations method. + VirtualMachineSnapshotOperations []struct { + // Namespace is the namespace argument value. + Namespace string + } + // VirtualMachineSnapshots holds details about calls to the VirtualMachineSnapshots method. + VirtualMachineSnapshots []struct { + // Namespace is the namespace argument value. + Namespace string + } // VirtualMachines holds details about calls to the VirtualMachines method. VirtualMachines []struct { // Namespace is the namespace argument value. @@ -1419,6 +1473,7 @@ type VirtClientMock struct { lockNodeV1beta1 sync.RWMutex lockPolicyV1 sync.RWMutex lockPolicyV1beta1 sync.RWMutex + lockRESTClient sync.RWMutex lockRbacV1 sync.RWMutex lockRbacV1alpha1 sync.RWMutex lockRbacV1beta1 sync.RWMutex @@ -1432,6 +1487,7 @@ type VirtClientMock struct { lockStorageV1alpha1 sync.RWMutex lockStorageV1beta1 sync.RWMutex lockStoragemigrationV1alpha1 sync.RWMutex + lockVirtualDiskSnapshots sync.RWMutex lockVirtualDisks sync.RWMutex lockVirtualImages sync.RWMutex lockVirtualMachineBlockDeviceAttachments sync.RWMutex @@ -1441,6 +1497,9 @@ type VirtClientMock struct { lockVirtualMachineMACAddressLeases sync.RWMutex lockVirtualMachineMACAddresses sync.RWMutex lockVirtualMachineOperations sync.RWMutex + lockVirtualMachineRestores sync.RWMutex + lockVirtualMachineSnapshotOperations sync.RWMutex + lockVirtualMachineSnapshots sync.RWMutex lockVirtualMachines sync.RWMutex } @@ -2632,6 +2691,33 @@ func (mock *VirtClientMock) PolicyV1beta1Calls() []struct { return calls } +// RESTClient calls RESTClientFunc. +func (mock *VirtClientMock) RESTClient() rest.Interface { + if mock.RESTClientFunc == nil { + panic("VirtClientMock.RESTClientFunc: method is nil but VirtClient.RESTClient was just called") + } + callInfo := struct { + }{} + mock.lockRESTClient.Lock() + mock.calls.RESTClient = append(mock.calls.RESTClient, callInfo) + mock.lockRESTClient.Unlock() + return mock.RESTClientFunc() +} + +// RESTClientCalls gets all the calls that were made to RESTClient. +// Check the length with: +// +// len(mockedVirtClient.RESTClientCalls()) +func (mock *VirtClientMock) RESTClientCalls() []struct { +} { + var calls []struct { + } + mock.lockRESTClient.RLock() + calls = mock.calls.RESTClient + mock.lockRESTClient.RUnlock() + return calls +} + // RbacV1 calls RbacV1Func. func (mock *VirtClientMock) RbacV1() rbacv1.RbacV1Interface { if mock.RbacV1Func == nil { @@ -2983,6 +3069,38 @@ func (mock *VirtClientMock) StoragemigrationV1alpha1Calls() []struct { return calls } +// VirtualDiskSnapshots calls VirtualDiskSnapshotsFunc. +func (mock *VirtClientMock) VirtualDiskSnapshots(namespace string) corev1alpha2.VirtualDiskSnapshotInterface { + if mock.VirtualDiskSnapshotsFunc == nil { + panic("VirtClientMock.VirtualDiskSnapshotsFunc: method is nil but VirtClient.VirtualDiskSnapshots was just called") + } + callInfo := struct { + Namespace string + }{ + Namespace: namespace, + } + mock.lockVirtualDiskSnapshots.Lock() + mock.calls.VirtualDiskSnapshots = append(mock.calls.VirtualDiskSnapshots, callInfo) + mock.lockVirtualDiskSnapshots.Unlock() + return mock.VirtualDiskSnapshotsFunc(namespace) +} + +// VirtualDiskSnapshotsCalls gets all the calls that were made to VirtualDiskSnapshots. +// Check the length with: +// +// len(mockedVirtClient.VirtualDiskSnapshotsCalls()) +func (mock *VirtClientMock) VirtualDiskSnapshotsCalls() []struct { + Namespace string +} { + var calls []struct { + Namespace string + } + mock.lockVirtualDiskSnapshots.RLock() + calls = mock.calls.VirtualDiskSnapshots + mock.lockVirtualDiskSnapshots.RUnlock() + return calls +} + // VirtualDisks calls VirtualDisksFunc. func (mock *VirtClientMock) VirtualDisks(namespace string) corev1alpha2.VirtualDiskInterface { if mock.VirtualDisksFunc == nil { @@ -3256,6 +3374,102 @@ func (mock *VirtClientMock) VirtualMachineOperationsCalls() []struct { return calls } +// VirtualMachineRestores calls VirtualMachineRestoresFunc. +func (mock *VirtClientMock) VirtualMachineRestores(namespace string) corev1alpha2.VirtualMachineRestoreInterface { + if mock.VirtualMachineRestoresFunc == nil { + panic("VirtClientMock.VirtualMachineRestoresFunc: method is nil but VirtClient.VirtualMachineRestores was just called") + } + callInfo := struct { + Namespace string + }{ + Namespace: namespace, + } + mock.lockVirtualMachineRestores.Lock() + mock.calls.VirtualMachineRestores = append(mock.calls.VirtualMachineRestores, callInfo) + mock.lockVirtualMachineRestores.Unlock() + return mock.VirtualMachineRestoresFunc(namespace) +} + +// VirtualMachineRestoresCalls gets all the calls that were made to VirtualMachineRestores. +// Check the length with: +// +// len(mockedVirtClient.VirtualMachineRestoresCalls()) +func (mock *VirtClientMock) VirtualMachineRestoresCalls() []struct { + Namespace string +} { + var calls []struct { + Namespace string + } + mock.lockVirtualMachineRestores.RLock() + calls = mock.calls.VirtualMachineRestores + mock.lockVirtualMachineRestores.RUnlock() + return calls +} + +// VirtualMachineSnapshotOperations calls VirtualMachineSnapshotOperationsFunc. +func (mock *VirtClientMock) VirtualMachineSnapshotOperations(namespace string) corev1alpha2.VirtualMachineSnapshotOperationInterface { + if mock.VirtualMachineSnapshotOperationsFunc == nil { + panic("VirtClientMock.VirtualMachineSnapshotOperationsFunc: method is nil but VirtClient.VirtualMachineSnapshotOperations was just called") + } + callInfo := struct { + Namespace string + }{ + Namespace: namespace, + } + mock.lockVirtualMachineSnapshotOperations.Lock() + mock.calls.VirtualMachineSnapshotOperations = append(mock.calls.VirtualMachineSnapshotOperations, callInfo) + mock.lockVirtualMachineSnapshotOperations.Unlock() + return mock.VirtualMachineSnapshotOperationsFunc(namespace) +} + +// VirtualMachineSnapshotOperationsCalls gets all the calls that were made to VirtualMachineSnapshotOperations. +// Check the length with: +// +// len(mockedVirtClient.VirtualMachineSnapshotOperationsCalls()) +func (mock *VirtClientMock) VirtualMachineSnapshotOperationsCalls() []struct { + Namespace string +} { + var calls []struct { + Namespace string + } + mock.lockVirtualMachineSnapshotOperations.RLock() + calls = mock.calls.VirtualMachineSnapshotOperations + mock.lockVirtualMachineSnapshotOperations.RUnlock() + return calls +} + +// VirtualMachineSnapshots calls VirtualMachineSnapshotsFunc. +func (mock *VirtClientMock) VirtualMachineSnapshots(namespace string) corev1alpha2.VirtualMachineSnapshotInterface { + if mock.VirtualMachineSnapshotsFunc == nil { + panic("VirtClientMock.VirtualMachineSnapshotsFunc: method is nil but VirtClient.VirtualMachineSnapshots was just called") + } + callInfo := struct { + Namespace string + }{ + Namespace: namespace, + } + mock.lockVirtualMachineSnapshots.Lock() + mock.calls.VirtualMachineSnapshots = append(mock.calls.VirtualMachineSnapshots, callInfo) + mock.lockVirtualMachineSnapshots.Unlock() + return mock.VirtualMachineSnapshotsFunc(namespace) +} + +// VirtualMachineSnapshotsCalls gets all the calls that were made to VirtualMachineSnapshots. +// Check the length with: +// +// len(mockedVirtClient.VirtualMachineSnapshotsCalls()) +func (mock *VirtClientMock) VirtualMachineSnapshotsCalls() []struct { + Namespace string +} { + var calls []struct { + Namespace string + } + mock.lockVirtualMachineSnapshots.RLock() + calls = mock.calls.VirtualMachineSnapshots + mock.lockVirtualMachineSnapshots.RUnlock() + return calls +} + // VirtualMachines calls VirtualMachinesFunc. func (mock *VirtClientMock) VirtualMachines(namespace string) corev1alpha2.VirtualMachineInterface { if mock.VirtualMachinesFunc == nil { diff --git a/src/cli/Taskfile.yaml b/src/cli/Taskfile.yaml index 776ccb788f..d59999a281 100644 --- a/src/cli/Taskfile.yaml +++ b/src/cli/Taskfile.yaml @@ -5,19 +5,38 @@ version: "3" silent: true tasks: - build: + build-tiny: desc: "Build d8v cli" cmds: - - go build -o .out/d8v cmd/main.go - install: + - CGO_ENABLED=0 go build -o .out/d8v cmd/main.go + + build: + desc: "Build d8v cli [CGO]" + cmds: + - mkdir -p .out + - CGO_ENABLED=1 go build -tags cgo -o .out/d8v cmd/main.go + + install-tiny: desc: "Install d8v cli to ~/.local/bin" + deps: [build-tiny] + cmds: + - _install + + install: + desc: "Install d8v cli to ~/.local/bin [CGO]" deps: [build] + cmds: + - _install + + _install: + internal: true cmds: - echo "Check that ~/.local/bin in your PATH" - echo "Installing d8v to ~/.local/bin" - mkdir -p ~/.local/bin - cp .out/d8v ~/.local/bin/d8v - task: clean + clean: desc: "Clean up build artifacts" cmds: diff --git a/src/cli/internal/cmd/usbredir/client.go b/src/cli/internal/cmd/usbredir/client.go new file mode 100644 index 0000000000..f665b8710c --- /dev/null +++ b/src/cli/internal/cmd/usbredir/client.go @@ -0,0 +1,148 @@ +/* +Copyright 2018 The KubeVirt Authors +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. + +Initially copied from https://github.com/kubevirt/kubevirt/blob/v1.6.2/pkg/virtctl/usbredir/client.go +*/ + +package usbredir + +import ( + "context" + "fmt" + "io" + "log/slog" + "net" + "time" + + "github.com/deckhouse/virtualization/api/client/generated/clientset/versioned/typed/core/v1alpha2" +) + +type Client struct { + inputReader *io.PipeReader + inputWriter *io.PipeWriter + outputReader *io.PipeReader + outputWriter *io.PipeWriter + + usbRedirector Redirector + remoteStream v1alpha2.StreamInterface + + // channels + done chan struct{} + stream chan error + remote chan error +} + +func NewClient(remoteStream v1alpha2.StreamInterface, redirector Redirector) *Client { + inReader, inWriter := io.Pipe() + outReader, outWriter := io.Pipe() + return &Client{ + inputReader: inReader, + inputWriter: inWriter, + outputReader: outReader, + outputWriter: outWriter, + + usbRedirector: redirector, + remoteStream: remoteStream, + } +} + +func (c *Client) Redirect(ctx context.Context) error { + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + return fmt.Errorf("can't listen on random port: %w", err) + } + defer ln.Close() + + c.startRemoteStream(ctx) + c.startProxyUSBRedir(ctx, ln) + + address := ln.Addr().String() + + usbRedirectorCh := make(chan error) + go func() { + defer close(c.done) + usbRedirectorCh <- c.usbRedirector.Redirect(ctx, address) + }() + + select { + case err = <-c.stream: + case err = <-usbRedirectorCh: + case err = <-c.remote: + case <-ctx.Done(): + } + return err +} + +func (c *Client) startRemoteStream(ctx context.Context) { + c.stream = make(chan error) + + go func() { + defer c.outputWriter.Close() + select { + case c.stream <- c.remoteStream.Stream( + v1alpha2.StreamOptions{ + In: c.inputReader, + Out: c.outputWriter, + }, + ): + case <-ctx.Done(): + } + }() +} + +func (c *Client) startProxyUSBRedir(ctx context.Context, listener net.Listener) { + c.done = make(chan struct{}, 1) + c.remote = make(chan error) + go func() { + defer c.inputWriter.Close() + start := time.Now() + + usbredirConn, err := listener.Accept() + if err != nil { + slog.Info("Failed to accept connection", slog.Any("err", err)) + c.remote <- err + return + } + defer usbredirConn.Close() + + slog.Info("Connected to usbredir at", slog.Any("time", time.Since(start))) + + stream := make(chan error) + // write to local usbredir from pipeOutReader + go func() { + _, err := io.Copy(usbredirConn, c.outputReader) + stream <- err + }() + + // read from local usbredir towards pipeInWriter + go func() { + _, err := io.Copy(c.inputWriter, usbredirConn) + stream <- err + }() + + select { + case <-c.done: // Wait for local usbredir to complete + case err = <-stream: // Wait for remote connection to close + if err == nil { + // Remote connection closed, report this as error + err = fmt.Errorf("remote connection has closed") + } + // Wait for local usbredir to complete + c.remote <- err + case <-ctx.Done(): + } + }() +} diff --git a/src/cli/internal/cmd/usbredir/redirector.go b/src/cli/internal/cmd/usbredir/redirector.go new file mode 100644 index 0000000000..1f63b5d19f --- /dev/null +++ b/src/cli/internal/cmd/usbredir/redirector.go @@ -0,0 +1,98 @@ +package usbredir + +import ( + "context" + "fmt" + "net" + "os/exec" + "strconv" + + "github.com/deckhouse/virtualization/src/cli/pkg/usbredir" +) + +type Redirector interface { + Redirect(ctx context.Context, address string) error +} + +type usbToolRedirector struct { + bus int + deviceNum int + verbosity int + bin string + sudo bool +} + +func newUsbToolRedirector(bus, deviceNum, verbosity int, bin string, sudo bool) *usbToolRedirector { + return &usbToolRedirector{ + bus: bus, + deviceNum: deviceNum, + verbosity: verbosity, + bin: bin, + sudo: sudo, + } +} + +func (u usbToolRedirector) Redirect(ctx context.Context, address string) error { + if _, err := exec.LookPath(u.bin); err != nil { + return fmt.Errorf("error on finding %s in $PATH: %s", u.bin, err.Error()) + } + + var ( + command string + args []string + ) + + device := fmt.Sprintf("%d-%d", u.bus, u.deviceNum) + + if u.sudo { + command = "sudo" + args = []string{u.bin, "--device", device, "--to", address} + } else { + command = u.bin + args = []string{"--device", device, "--to", address} + } + + if u.verbosity > 0 { + args = append(args, "--verbose", strconv.Itoa(u.verbosity)) + } + + output, err := exec.CommandContext(ctx, command, args...).CombinedOutput() + if err != nil { + return fmt.Errorf("failed to invoke usbredir: %w: %s", err, string(output)) + } + return nil +} + +func newNativeUsbRedirector(bus, deviceNum, verbosity int) *nativeUsbRedirector { + return &nativeUsbRedirector{ + bus: bus, + deviceNum: deviceNum, + verbosity: verbosity, + } +} + +type nativeUsbRedirector struct { + bus int + deviceNum int + verbosity int +} + +func (u nativeUsbRedirector) Redirect(ctx context.Context, address string) error { + host, portStr, err := net.SplitHostPort(address) + if err != nil { + return fmt.Errorf("invalid address: %w", err) + } + port, err := strconv.Atoi(portStr) + if err != nil { + return fmt.Errorf("invalid port: %w", err) + } + + config := usbredir.Config{ + Bus: u.bus, + DeviceNum: u.deviceNum, + Address: host, + Port: port, + Verbosity: u.verbosity, + } + return usbredir.Run(ctx, config) +} diff --git a/src/cli/internal/cmd/usbredir/usbredir.go b/src/cli/internal/cmd/usbredir/usbredir.go new file mode 100644 index 0000000000..30b6f4caad --- /dev/null +++ b/src/cli/internal/cmd/usbredir/usbredir.go @@ -0,0 +1,123 @@ +/* +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 usbredir + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" + + "github.com/deckhouse/virtualization/src/cli/internal/clientconfig" + "github.com/deckhouse/virtualization/src/cli/internal/templates" +) + +const usbRedirectClient string = "usbredirect" + +func NewCommand() *cobra.Command { + usbRedir := &USBRedir{} + cmd := &cobra.Command{ + Use: "usb-redirect (VirtualMachine)", + Short: "Redirect USB devices to a virtual machine.", + Example: usbRedir.Usage(), + Args: templates.ExactArgs("usb-redirect", 1), + RunE: usbRedir.Run, + } + + cmd.SetUsageTemplate(templates.UsageTemplate()) + usbRedir.AddFlags(cmd.Flags()) + + _ = cmd.MarkFlagRequired("device") + _ = cmd.MarkFlagRequired("bus") + + return cmd +} + +type USBRedir struct { + device int + bus int + redirVerbosity int + tool bool + toolSudo bool +} + +func (c *USBRedir) AddFlags(fs *pflag.FlagSet) { + fs.IntVarP(&c.device, "device", "d", 0, "(required) The device you want to redirect.") + fs.IntVarP(&c.bus, "bus", "b", 0, "(required) The bus of the device you want to redirect.") + fs.IntVar(&c.redirVerbosity, "verbosity", 1, "(optional) Verbosity level of the usbredirect.") + fs.BoolVar(&c.tool, "tool", false, "(optional) Use subtool usbredirect.") + fs.BoolVar(&c.toolSudo, "sudo", false, "(optional) Use sudo to run the usbredirect subtool.") +} + +func (c *USBRedir) Validate() error { + if c.device == 0 { + return fmt.Errorf("device is required") + } + if c.bus == 0 { + return fmt.Errorf("bus is required") + } + if c.tool && !c.toolSudo && os.Getuid() != 0 { + return fmt.Errorf("sudo is required to run the command as root") + } + return nil +} + +func (c *USBRedir) Usage() string { + return `# Find the device you want to redirect (linux): + ❯ lsusb | grep Transcend + Bus 004 Device 003: ID 8564:1000 Transcend Information, Inc. JetFlash + + # Redirect it with bus-device: + {{ProgramName}} usbredir myvm --bus 4 --device 3 + ` +} + +func (c *USBRedir) newRedirector() Redirector { + if c.tool { + return newUsbToolRedirector(c.bus, c.device, c.redirVerbosity, usbRedirectClient, c.toolSudo) + } + return newNativeUsbRedirector(c.bus, c.device, c.redirVerbosity) +} + +func (c *USBRedir) Run(cmd *cobra.Command, args []string) error { + if err := c.Validate(); err != nil { + return err + } + + client, defaultNamespace, _, err := clientconfig.ClientAndNamespaceFromContext(cmd.Context()) + if err != nil { + return err + } + + namespace, name, err := templates.ParseTarget(args[0]) + if err != nil { + return err + } + if namespace == "" { + namespace = defaultNamespace + } + + stream, err := client.VirtualMachines(namespace).USBRedir(cmd.Context(), name) + if err != nil { + return err + } + + redir := NewClient(stream, c.newRedirector()) + + return redir.Redirect(cmd.Context()) +} diff --git a/src/cli/pkg/command/virtualization.go b/src/cli/pkg/command/virtualization.go index 403c0047c2..b845419531 100644 --- a/src/cli/pkg/command/virtualization.go +++ b/src/cli/pkg/command/virtualization.go @@ -36,6 +36,7 @@ import ( "github.com/deckhouse/virtualization/src/cli/internal/cmd/portforward" "github.com/deckhouse/virtualization/src/cli/internal/cmd/scp" "github.com/deckhouse/virtualization/src/cli/internal/cmd/ssh" + "github.com/deckhouse/virtualization/src/cli/internal/cmd/usbredir" "github.com/deckhouse/virtualization/src/cli/internal/cmd/vnc" "github.com/deckhouse/virtualization/src/cli/internal/comp" "github.com/deckhouse/virtualization/src/cli/internal/templates" @@ -92,6 +93,7 @@ func NewCommand(programName string) *cobra.Command { lifecycle.NewStopCommand(), lifecycle.NewRestartCommand(), lifecycle.NewEvictCommand(), + usbredir.NewCommand(), optionsCmd, ) diff --git a/src/cli/pkg/usbredir/config.go b/src/cli/pkg/usbredir/config.go new file mode 100644 index 0000000000..afa3701249 --- /dev/null +++ b/src/cli/pkg/usbredir/config.go @@ -0,0 +1,32 @@ +package usbredir + +import "errors" + +type Config struct { + Vendor int + Product int + Bus int + DeviceNum int + Port int + Verbosity int + KeepAlive bool + Address string +} + +func (c Config) Validate() error { + if c.Address == "" { + return errors.New("address is required") + } + if c.Port <= 0 || c.Port > 65535 { + return errors.New("port must be between 1 and 65535") + } + + byBus := c.Bus != 0 && c.DeviceNum != 0 + byVendor := c.Vendor != 0 && c.Product != 0 + + if !byBus && !byVendor { + return errors.New("either (bus,deviceNum) or (vendor,product) must be set") + } + + return nil +} diff --git a/src/cli/pkg/usbredir/usbredir.go b/src/cli/pkg/usbredir/usbredir.go new file mode 100644 index 0000000000..af15ce33fb --- /dev/null +++ b/src/cli/pkg/usbredir/usbredir.go @@ -0,0 +1,710 @@ +//go:build cgo + +package usbredir + +/* +// Initially copied from https://gitlab.freedesktop.org/spice/usbredir/-/blob/usbredir-0.15.0/tools/usbredirect.c?ref_type=tags + +#cgo pkg-config: libusbredirhost libusbredirparser-0.5 libusb-1.0 glib-2.0 gio-2.0 + +#define G_LOG_DOMAIN "usbredirect" +#define G_LOG_USE_STRUCTURED + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef G_OS_UNIX +#include +#endif + + +typedef struct device { + int vendor; + int product; + int bus; + int device_number; +} device; + +typedef struct net_settings { + char *addr; + int port; + bool keepalive; +} net_settings; + +typedef struct redirect { + device device; + net_settings net_settings; + + bool by_bus; + bool is_client; + bool watch_inout; + int verbosity; + + struct usbredirhost *usbredirhost; + GSocketConnection *connection; + GThread *event_thread; + int event_thread_run; + int watch_server_id; + GIOChannel *io_channel; + + GMainLoop *main_loop; +} redirect; + +static void create_watch(redirect *self); + +typedef struct { + device device; + net_settings net_settings; + int verbosity; +} usbredir_config; + + +static bool +by_bus(device *device) { + return device != NULL && device->bus > 0 && device->device_number > 0; +} + +static bool +validate_device_from_config(usbredir_config *config) +{ + bool by_bus_flag = config->device.bus > 0 && config->device.device_number > 0; + if (by_bus_flag) { + return config->device.bus > 0 && config->device.device_number > 0; + } + + if (config->device.vendor <= 0 || config->device.vendor > 0xffff || + config->device.product < 0 || config->device.product > 0xffff) { + return false; + } + + return true; +} + +static bool +is_valid_config(usbredir_config *config) { + if (!config || !config->net_settings.addr) { + return false; + } + + if (!validate_device_from_config(config)) { + return false; + } + + return true; +} + +static redirect * +new_redirect(usbredir_config *config) +{ + redirect *self = NULL; + if (!is_valid_config(config)) { + return self; + } + + self = g_new0(redirect, 1); + self->watch_inout = true; + self->device = config->device; + self->by_bus = by_bus(&self->device); + self->is_client = true; + self->net_settings = config->net_settings; + + return self; +} + +static gpointer +thread_handle_libusb_events(gpointer user_data) +{ + redirect *self = (redirect *) user_data; + + int res = 0; + const char *desc = ""; + while (g_atomic_int_get(&self->event_thread_run)) { + res = libusb_handle_events(NULL); + if (res && res != LIBUSB_ERROR_INTERRUPTED) { + desc = libusb_strerror(res); + g_warning("Error handling USB events: %s [%i]", desc, res); + break; + } + } + if (self->event_thread_run) { + g_debug("%s: the thread aborted, %s(%d)", __FUNCTION__, desc, res); + } + return NULL; +} + +#if LIBUSBX_API_VERSION >= 0x01000107 +static void LIBUSB_CALL +debug_libusb_cb(libusb_context *ctx, enum libusb_log_level level, const char *msg) +{ + GLogLevelFlags glog_level; + + switch(level) { + case LIBUSB_LOG_LEVEL_ERROR: + glog_level = G_LOG_LEVEL_ERROR; + break; + case LIBUSB_LOG_LEVEL_WARNING: + glog_level = G_LOG_LEVEL_WARNING; + break; + case LIBUSB_LOG_LEVEL_INFO: + glog_level = G_LOG_LEVEL_INFO; + break; + case LIBUSB_LOG_LEVEL_DEBUG: + glog_level = G_LOG_LEVEL_DEBUG; + break; + default: + g_warn_if_reached(); + return; + } + + // Do not print the '\n' line feed + size_t len = strlen(msg); + len = (msg[len - 1] == '\n') ? len - 1 : len; + g_log_structured(G_LOG_DOMAIN, glog_level, "MESSAGE", "%.*s", len - 1, msg); +} +#endif + +static void +usbredir_log_cb(void *priv, int level, const char *msg) +{ + GLogLevelFlags glog_level; + + switch(level) { + case usbredirparser_error: + glog_level = G_LOG_LEVEL_ERROR; + break; + case usbredirparser_warning: + glog_level = G_LOG_LEVEL_WARNING; + break; + case usbredirparser_info: + glog_level = G_LOG_LEVEL_INFO; + break; + case usbredirparser_debug: + case usbredirparser_debug_data: + glog_level = G_LOG_LEVEL_DEBUG; + break; + default: + g_warn_if_reached(); + return; + } + g_log_structured(G_LOG_DOMAIN, glog_level, "MESSAGE", msg); +} + +static void +update_watch(redirect *self) +{ + const bool watch_inout = usbredirhost_has_data_to_write(self->usbredirhost) != 0; + if (watch_inout == self->watch_inout) { + return; + } + g_clear_pointer(&self->io_channel, g_io_channel_unref); + g_source_remove(self->watch_server_id); + self->watch_server_id = 0; + self->watch_inout = watch_inout; + + create_watch(self); +} + + +static int +usbredir_read_cb(void *priv, uint8_t *data, int count) +{ + redirect *self = (redirect *) priv; + GIOStream *iostream = G_IO_STREAM(self->connection); + GError *err = NULL; + + GPollableInputStream *instream = G_POLLABLE_INPUT_STREAM(g_io_stream_get_input_stream(iostream)); + gssize nbytes = g_pollable_input_stream_read_nonblocking(instream, + data, + count, + NULL, + &err); + if (nbytes <= 0) { + if (g_error_matches(err, G_IO_ERROR, G_IO_ERROR_WOULD_BLOCK)) { + // Try again later + nbytes = 0; + } else { + if (err != NULL) { + g_warning("Failure at %s: %s", __func__, err->message); + } + g_main_loop_quit(self->main_loop); + } + g_clear_error(&err); + } + return nbytes; +} + +static int +usbredir_write_cb(void *priv, uint8_t *data, int count) +{ + redirect *self = (redirect *) priv; + GIOStream *iostream = G_IO_STREAM(self->connection); + GError *err = NULL; + + GPollableOutputStream *outstream = G_POLLABLE_OUTPUT_STREAM(g_io_stream_get_output_stream(iostream)); + gssize nbytes = g_pollable_output_stream_write_nonblocking(outstream, + data, + count, + NULL, + &err); + if (nbytes <= 0) { + if (g_error_matches(err, G_IO_ERROR, G_IO_ERROR_WOULD_BLOCK)) { + // Try again later + nbytes = 0; + update_watch(self); + } else { + if (err != NULL) { + g_warning("Failure at %s: %s", __func__, err->message); + } + g_main_loop_quit(self->main_loop); + } + g_clear_error(&err); + } + return nbytes; +} + +static void +usbredir_write_flush_cb(void *user_data) +{ + redirect *self = (redirect *) user_data; + if (!self || !self->usbredirhost) { + return; + } + + int ret = usbredirhost_write_guest_data(self->usbredirhost); + if (ret < 0) { + g_critical("%s: Failed to write to guest", __func__); + g_main_loop_quit(self->main_loop); + } +} + +static void +*usbredir_alloc_lock(void) +{ + GMutex *mutex; + + mutex = g_new0(GMutex, 1); + g_mutex_init(mutex); + + return mutex; +} + +static void +usbredir_free_lock(void *user_data) +{ + GMutex *mutex = user_data; + + g_mutex_clear(mutex); + g_free(mutex); +} + +static void +usbredir_lock_lock(void *user_data) +{ + GMutex *mutex = user_data; + + g_mutex_lock(mutex); +} + +static void +usbredir_unlock_lock(void *user_data) +{ + GMutex *mutex = user_data; + + g_mutex_unlock(mutex); +} + +static gboolean +connection_handle_io_cb(GIOChannel *source, GIOCondition condition, gpointer user_data) +{ + redirect *self = (redirect *) user_data; + + if (condition & G_IO_ERR || condition & G_IO_HUP) { + g_warning("Connection: err=%d, hup=%d - exiting", (condition & G_IO_ERR), (condition & G_IO_HUP)); + goto end; + } + + if (condition & G_IO_IN) { + int ret = usbredirhost_read_guest_data(self->usbredirhost); + if (ret < 0) { + g_critical("%s: Failed to read guest", __func__); + goto end; + } + } + // try to write data in any case, to avoid having another iteration and + // creation of another watch if there is space in output buffer + if (usbredirhost_has_data_to_write(self->usbredirhost) != 0) { + int ret = usbredirhost_write_guest_data(self->usbredirhost); + if (ret < 0) { + g_critical("%s: Failed to write to guest", __func__); + goto end; + } + } + + // update the watch if needed + update_watch(self); + return G_SOURCE_CONTINUE; + +end: + g_main_loop_quit(self->main_loop); + return G_SOURCE_REMOVE; +} + +static void +create_watch(redirect *self) +{ + GSocket *socket = g_socket_connection_get_socket(self->connection); + int socket_fd = g_socket_get_fd(socket); + + g_assert_null(self->io_channel); + self->io_channel = +#ifdef G_OS_UNIX + g_io_channel_unix_new(socket_fd); +#else + g_io_channel_win32_new_socket(socket_fd); +#endif + + g_assert_cmpint(self->watch_server_id, ==, 0); + self->watch_server_id = g_io_add_watch(self->io_channel, + G_IO_IN | G_IO_HUP | G_IO_ERR | (self->watch_inout ? G_IO_OUT : 0), + connection_handle_io_cb, + self); +} + +static bool +can_claim_usb_device(libusb_device *dev, libusb_device_handle **handle) +{ + int ret = libusb_open(dev, handle); + if (ret != 0) { + g_debug("Failed to open device"); + return false; + } + + // Opening is not enough. We need to check if device can be claimed + // for I/O operations + struct libusb_config_descriptor *config = NULL; + ret = libusb_get_active_config_descriptor(dev, &config); + if (ret != 0 || config == NULL) { + g_debug("Failed to get active descriptor"); + goto fail; + } + +#if LIBUSBX_API_VERSION >= 0x01000102 + libusb_set_auto_detach_kernel_driver(*handle, 1); +#endif + + int i; + for (i = 0; i < config->bNumInterfaces; i++) { + int interface_num = config->interface[i].altsetting[0].bInterfaceNumber; +#if LIBUSBX_API_VERSION < 0x01000102 + ret = libusb_detach_kernel_driver(*handle, interface_num); + if (ret != 0 && ret != LIBUSB_ERROR_NOT_FOUND + && ret != LIBUSB_ERROR_NOT_SUPPORTED) { + g_error("failed to detach driver from interface %d: %s", + interface_num, libusb_error_name(ret)); + goto fail; + } +#endif + ret = libusb_claim_interface(*handle, interface_num); + if (ret != 0) { + g_debug("Could not claim interface"); + goto fail; + } + ret = libusb_release_interface(*handle, interface_num); + if (ret != 0) { + g_debug("Could not release interface"); + goto fail; + } + } + + libusb_free_config_descriptor(config); + return true; + +fail: + libusb_free_config_descriptor(config); + libusb_close(*handle); + *handle = NULL; + return false; +} + +static libusb_device_handle * +open_usb_device(redirect *self) +{ + struct libusb_device **devs; + struct libusb_device_handle *dev_handle = NULL; + size_t i, ndevices; + + ndevices = libusb_get_device_list(NULL, &devs); + for (i = 0; i < ndevices; i++) { + struct libusb_device_descriptor desc; + if (libusb_get_device_descriptor(devs[i], &desc) != 0) { + g_warning("Failed to get descriptor"); + continue; + } + + if (self->by_bus && + (self->device.bus != libusb_get_bus_number(devs[i]) || + self->device.device_number != libusb_get_device_address(devs[i]))) { + continue; + } + + if (!self->by_bus && + (self->device.vendor != desc.idVendor || + self->device.product != desc.idProduct)) { + continue; + } + + if (can_claim_usb_device(devs[i], &dev_handle)) { + break; + } + } + + libusb_free_device_list(devs, 1); + return dev_handle; +} + +static gboolean +connection_incoming_cb(GSocketService *service, + GSocketConnection *client_connection, + GObject *source_object, + gpointer user_data) +{ + redirect *self = (redirect *) user_data; + + // Check if there is already an active connection + if (self->connection != NULL) { + g_warning("Rejecting new connection: already connected to a client"); + return G_SOURCE_REMOVE; + } + + self->connection = g_object_ref(client_connection); + + // Add a GSource watch to handle polling for us and handle IO in the callback + GSocket *connection_socket = g_socket_connection_get_socket(self->connection); + g_socket_set_keepalive(connection_socket, self->net_settings.keepalive); + create_watch(self); + return G_SOURCE_REMOVE; +} + +int usbredir_run(usbredir_config *config) +{ + GError *err = NULL; + + if (libusb_init(NULL)) { + g_warning("Could not init libusb\n"); + goto err_init; + } + + redirect *self = new_redirect(config); + if (!self) { + goto err_init; + } + +#if LIBUSBX_API_VERSION >= 0x01000107 + // This was introduced in 1.0.23 + libusb_set_log_cb(NULL, debug_libusb_cb, LIBUSB_LOG_CB_GLOBAL); +#endif + +#ifdef G_OS_WIN32 + // WinUSB is the default by backwards compatibility so this is needed to + // switch to USBDk backend. +# if LIBUSBX_API_VERSION >= 0x01000106 + libusb_set_option(NULL, LIBUSB_OPTION_USE_USBDK); +# endif +#endif + + // TODO: setup handle signals + + + libusb_device_handle *device_handle = open_usb_device(self); + if (!device_handle) { + g_printerr("Failed to open device!\n"); + goto err_init; + } + + + // As per doc below, we are not using hotplug so we must first call + // libusb_open() and then we can start the event thread. + // + // http://libusb.sourceforge.net/api-1.0/group__libusb__asyncio.html#eventthread + // + // The event thread is a must for Windows while on Unix we would ge okay + // getting the fds and polling oursevelves. + g_atomic_int_set(&self->event_thread_run, TRUE); + self->event_thread = g_thread_try_new("usbredirect-libusb-event-thread", + thread_handle_libusb_events, + self, + &err); + if (!self->event_thread) { + g_warning("Error starting event thread: %s", err->message); + libusb_close(device_handle); + goto err_init; + } + + self->usbredirhost = usbredirhost_open_full(NULL, + device_handle, + usbredir_log_cb, + usbredir_read_cb, + usbredir_write_cb, + usbredir_write_flush_cb, + usbredir_alloc_lock, + usbredir_lock_lock, + usbredir_unlock_lock, + usbredir_free_lock, + self, + "usbredir-go/1.0", + self->verbosity, + 0); + if (!self->usbredirhost) { + g_warning("Error starting usbredirhost"); + goto err_init; + } + + + // Only allow libusb logging if log verbosity is uredirparser_debug_data + // (or higher), otherwise we disable it here while keeping usbredir's logs enable. + if (config->verbosity < usbredirparser_debug_data) { +#if LIBUSBX_API_VERSION >= 0x01000106 + int ret = libusb_set_option(NULL, LIBUSB_OPTION_LOG_LEVEL, LIBUSB_LOG_LEVEL_NONE); + if (ret != LIBUSB_SUCCESS) { + g_warning("error disabling libusb log level: %s", libusb_error_name(ret)); + goto end; + } +#else + libusb_set_debug(NULL, LIBUSB_LOG_LEVEL_NONE); +#endif + } + + if (self->is_client) { + // Connect to a remote sever using usbredir to redirect the usb device + GSocketClient *client = g_socket_client_new(); + self->connection = g_socket_client_connect_to_host(client, + self->net_settings.addr, + self->net_settings.port, + NULL, + &err); + g_object_unref(client); + if (err != NULL) { + g_warning("Failed to connect to the server: %s", err->message); + goto end; + } + + GSocket *connection_socket = g_socket_connection_get_socket(self->connection); + g_socket_set_keepalive(connection_socket, self->net_settings.keepalive); + create_watch(self); + } else { + GSocketService *socket_service; + + socket_service = g_socket_service_new(); + GInetAddress *iaddr = g_inet_address_new_from_string(self->net_settings.addr); + if (iaddr == NULL) { + g_warning("Failed to parse IP: %s", self->net_settings.addr); + goto end; + } + + GSocketAddress *saddr = g_inet_socket_address_new(iaddr, self->net_settings.port); + g_object_unref(iaddr); + + g_socket_listener_add_address(G_SOCKET_LISTENER(socket_service), + saddr, + G_SOCKET_TYPE_STREAM, + G_SOCKET_PROTOCOL_TCP, + NULL, + NULL, + &err); + if (err != NULL) { + g_warning("Failed to run as TCP server: %s", err->message); + goto end; + } + + g_signal_connect(socket_service, + "incoming", G_CALLBACK(connection_incoming_cb), + self); + } + + self->main_loop = g_main_loop_new(NULL, FALSE); + g_main_loop_run(self->main_loop); + g_clear_pointer(&self->main_loop, g_main_loop_unref); + + g_atomic_int_set(&self->event_thread_run, FALSE); + if (self->event_thread) { + libusb_interrupt_event_handler(NULL); + g_thread_join(self->event_thread); + self->event_thread = NULL; + } + +end: + g_clear_pointer(&self->usbredirhost, usbredirhost_close); + g_clear_object(&self->connection); + g_free(self); +err_init: + libusb_exit(NULL); + + if (err != NULL) { + g_error_free(err); + return 1; + } + + return 0; +} +*/ +import "C" + +import ( + "context" + "errors" + "time" + "unsafe" +) + +func Run(ctx context.Context, config Config) error { + if err := config.Validate(); err != nil { + return err + } + + cConfig := C.usbredir_config{ + device: C.device{ + vendor: C.int(config.Vendor), + product: C.int(config.Product), + bus: C.int(config.Bus), + device_number: C.int(config.DeviceNum), + }, + net_settings: C.net_settings{ + addr: C.CString(config.Address), + port: C.int(config.Port), + keepalive: C.bool(config.KeepAlive), + }, + verbosity: C.int(config.Verbosity), + } + defer C.free(unsafe.Pointer(cConfig.net_settings.addr)) + + done := make(chan error, 1) + go func() { + //nolint:gocritic // C.usbredir_run returns 0 on success + if C.usbredir_run(&cConfig) == 0 { + done <- nil + } else { + done <- errors.New("failed to run usbredir") + } + }() + + const timeout = 10 * time.Second + + select { + case <-ctx.Done(): + select { + case err := <-done: + return err + case <-time.After(timeout): + return errors.New("usbredir timeout") + } + case err := <-done: + return err + } +} diff --git a/src/cli/pkg/usbredir/usbredir_no_cgo.go b/src/cli/pkg/usbredir/usbredir_no_cgo.go new file mode 100644 index 0000000000..6533979fa7 --- /dev/null +++ b/src/cli/pkg/usbredir/usbredir_no_cgo.go @@ -0,0 +1,16 @@ +//go:build !cgo + +package usbredir + +import ( + "context" + "errors" +) + +func Run(ctx context.Context, config Config) error { + if err := config.Validate(); err != nil { + return err + } + + return errors.New("usbredir functionality is not available: build with CGO_ENABLED=1 to enable native USB redirection support") +} diff --git a/templates/rbacv2/use/capabilities/access_usbredir.yaml b/templates/rbacv2/use/capabilities/access_usbredir.yaml new file mode 100644 index 0000000000..78abd0bec1 --- /dev/null +++ b/templates/rbacv2/use/capabilities/access_usbredir.yaml @@ -0,0 +1,18 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + heritage: deckhouse + module: virtualization + rbac.deckhouse.io/aggregate-to-virtualization-as: user + rbac.deckhouse.io/kind: use + name: d8:use:capability:virtualization:access_usbredir +rules: + - apiGroups: + - subresources.virtualization.deckhouse.io + resources: + - virtualmachines/usbredir + verbs: + - get + - create + - update diff --git a/templates/virtualization-api/rbac-for-us.yaml b/templates/virtualization-api/rbac-for-us.yaml index 680efa9bee..dd8eec6c5e 100644 --- a/templates/virtualization-api/rbac-for-us.yaml +++ b/templates/virtualization-api/rbac-for-us.yaml @@ -72,6 +72,7 @@ rules: - virtualmachineinstances/unfreeze - virtualmachineinstances/addvolume - virtualmachineinstances/removevolume + - virtualmachineinstances/usbredir verbs: - get - patch @@ -100,6 +101,7 @@ rules: - virtualmachines/removevolume - virtualmachines/unfreeze - virtualmachines/vnc + - virtualmachines/usbredir verbs: - get - patch