Skip to content
Merged
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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@
*.swp
.idea
*.DS_Store
.env
64 changes: 63 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,45 @@ NAME STATUS
devstack clusterReady
```

For KVM based source clusters a sample definition is as follows:

```yaml
apiVersion: migration.harvesterhci.io/v1beta1
kind: KVMSource
metadata:
name: kvm
namespace: default
spec:
libvirtURI: qemu+ssh://<KVM_HOST>/system
credentials:
name: kvm-credentials
namespace: default
```

The secret contains the credentials for the KVM host:

```yaml
apiVersion: v1
kind: Secret
metadata:
name: kvm-credentials
namespace: default
type: Opaque
stringData:
privateKey: |
-----BEGIN RSA PRIVATE KEY-----
...
-----END RSA PRIVATE KEY-----
```

KVM source reconcile process, attempts to list VM's in the cluster, and marks the source as ready

```shell
$ kubectl get kvmsource.migration
NAME STATUS
kvm clusterReady
```

### VirtualMachimeImport
The VirtualMachineImport crd provides a way for users to define the source VM and mapping to the actual source cluster to perform the VM export-import from.

Expand Down Expand Up @@ -131,7 +170,7 @@ $ kubectl get virtualmachineimport.migration
NAME STATUS
alpine-export-test virtualMachineRunning
openstack-cirros-test virtualMachineRunning

kvm-export-test virtualMachineRunning
```

Similarly, users can define a VirtualMachineImport for Openstack source as well:
Expand All @@ -158,6 +197,25 @@ spec:

*NOTE:* Openstack allows users to have multiple instances with the same name. In such a scenario the users are advised to use the Instance ID. The reconcile logic tries to perform a lookup from name to ID when a name is used.

And VirtualMachineImport for KMV as well:

```yaml
apiVersion: migration.harvesterhci.io/v1beta1
kind: VirtualMachineImport
metadata:
name: kvm-demo
namespace: default
spec:
virtualMachineName: kvm-demo
networkMapping:
- sourceNetwork: "default"
destinationNetwork: "default/vlan1"
sourceCluster:
kind: KVMSource
name: kvm
namespace: default
apiVersion: migration.harvesterhci.io/v1beta1
```

## Testing
Currently basic integration tests are available under `tests/integration`
Expand All @@ -183,5 +241,9 @@ export OS_USERNAME="openstack-username"
export OS_PASSWORD="openstack-password"
export OS_VM_NAME="openstack-export-test-vm-name"
export OS_REGION_NAME="openstack-region"
export KVM_LIBVIRT_URI="qemu+ssh://<KVM_USER>@<KVM_HOST>"
export KVM_SSH_USER="KVM user"
export KVM_SSH_PRIVATE_KEY_PATH="path to KVM ssh key"
export SKIP_VCSIM=true #When testing for KVM only
export KUBECONFIG="kubeconfig-for-harvester-cluster"
```
9 changes: 6 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@ require (
github.com/onsi/ginkgo/v2 v2.23.3
github.com/onsi/gomega v1.37.0
github.com/ory/dockertest/v3 v3.9.1
github.com/pkg/sftp v1.13.10
github.com/rancher/lasso v0.2.3
github.com/rancher/wrangler/v3 v3.2.2
github.com/sirupsen/logrus v1.9.3
github.com/stretchr/testify v1.10.0
github.com/vmware/govmomi v0.52.0
golang.org/x/crypto v0.41.0
golang.org/x/sync v0.16.0
k8s.io/api v0.33.1
k8s.io/apiextensions-apiserver v0.33.1
Expand All @@ -26,6 +28,7 @@ require (
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397
kubevirt.io/api v1.5.0
kubevirt.io/kubevirt v1.5.0
libvirt.org/go/libvirtxml v1.11010.0
sigs.k8s.io/cluster-api v1.9.4
sigs.k8s.io/controller-runtime v0.20.2
)
Expand Down Expand Up @@ -75,6 +78,7 @@ require (
github.com/jinzhu/copier v0.4.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kr/fs v0.1.0 // indirect
github.com/kubernetes-csi/external-snapshotter/client/v4 v4.2.0 // indirect
github.com/longhorn/go-common-libs v0.0.0-20250215052214-151615b29f8e // indirect
github.com/mailru/easyjson v0.9.0 // indirect
Expand Down Expand Up @@ -112,13 +116,12 @@ require (
go.uber.org/mock v0.5.2 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/crypto v0.40.0 // indirect
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect
golang.org/x/mod v0.26.0 // indirect
golang.org/x/net v0.42.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/sys v0.34.0 // indirect
golang.org/x/term v0.33.0 // indirect
golang.org/x/sys v0.35.0 // indirect
golang.org/x/term v0.34.0 // indirect
golang.org/x/text v0.28.0 // indirect
golang.org/x/time v0.11.0 // indirect
golang.org/x/tools v0.35.0 // indirect
Expand Down
18 changes: 12 additions & 6 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,8 @@ github.com/k8snetworkplumbingwg/network-attachment-definition-client v1.7.5 h1:C
github.com/k8snetworkplumbingwg/network-attachment-definition-client v1.7.5/go.mod h1:CM7HAH5PNuIsqjMN0fGc1ydM74Uj+0VZFhob620nklw=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
Expand Down Expand Up @@ -318,6 +320,8 @@ github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.13.10 h1:+5FbKNTe5Z9aspU88DPIKJ9z2KZoaGCu6Sr6kKR/5mU=
github.com/pkg/sftp v1.13.10/go.mod h1:bJ1a7uDhrX/4OII+agvy28lzRvQrmIQuaHrcI1HbeGA=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
Expand Down Expand Up @@ -434,8 +438,8 @@ golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI=
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ=
Expand Down Expand Up @@ -565,8 +569,8 @@ golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/telemetry v0.0.0-20240208230135-b75ee8823808/go.mod h1:KG1lNk5ZFNssSZLrpVb4sMXKMpGwGXOxSG3rnu2gZQQ=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0=
Expand All @@ -592,8 +596,8 @@ golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk=
golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg=
golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0=
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
Expand Down Expand Up @@ -747,6 +751,8 @@ kubevirt.io/controller-lifecycle-operator-sdk/api v0.0.0-20220329064328-f3cc58c6
kubevirt.io/controller-lifecycle-operator-sdk/api v0.0.0-20220329064328-f3cc58c6ed90/go.mod h1:018lASpFYBsYN6XwmA2TIrPCx6e0gviTd/ZNtSitKgc=
kubevirt.io/kubevirt v1.6.0 h1:xk7NgCHB3PVedEbuR7J+ehfR6ihmoDrjpl8tG3on3VE=
kubevirt.io/kubevirt v1.6.0/go.mod h1:tu6AWqWL1BdGQccdsy8aNsmoqzWWHBf1yDpiZrgSEQo=
libvirt.org/go/libvirtxml v1.11010.0 h1:lGUv6OQ4gz5Hm7F40G+swxmK/kcrMZGQ3M8/S+UyhME=
libvirt.org/go/libvirtxml v1.11010.0/go.mod h1:7Oq2BLDstLr/XtoQD8Fr3mfDNrzlI3utYKySXF2xkng=
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.32.1 h1:Cf+ed5N8038zbsaXFO7mKQDi/+VcSRafb0jM84KX5so=
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.32.1/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw=
sigs.k8s.io/cluster-api v1.9.4 h1:pa2Ho50F9Js/Vv/Jy11TcpmGiqY2ukXCoDj/dY25Y7M=
Expand Down
1 change: 1 addition & 0 deletions pkg/apis/migration.harvesterhci.io/v1beta1/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const (
KindVmwareSource string = "vmwaresource"
KindOvaSource string = "ovasource"
KindOpenstackSource string = "openstacksource"
KindKVMSource string = "kvmsource"
)

type ClusterStatus string
Expand Down
106 changes: 106 additions & 0 deletions pkg/apis/migration.harvesterhci.io/v1beta1/kvm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package v1beta1

import (
"time"

corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/utils/ptr"

"github.com/harvester/vm-import-controller/pkg/apis/common"
)

const (
DefaultSSHTimeoutSeconds = 30
DefaultVirshConnectionURI = ""
)

// +genclient
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

type KVMSource struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec KVMSourceSpec `json:"spec"`
Status KVMSourceStatus `json:"status,omitempty"`
}

type KVMSourceSpec struct {
// The SSH host URI to connect to. If no port is specified, then default
// SSH port 22 will be used.
// E.g., `ssh://user@hostname:2222`
EndpointAddress string `json:"endpoint"`

// Additional options.
KVMSourceOptions `json:",inline"`

// The referenced `Secret` should contain the following keys:
// - username: (optional) The username to authenticate at the specified server.
// - password: (optional) The password to authenticate at the specified server.
// - privateKey: (optional) The private key to authenticate at the specified server.
// One of the authentication fields password or privateKey must be specified.
Credentials corev1.SecretReference `json:"credentials"`
}

type KVMSourceStatus struct {
Status ClusterStatus `json:"status,omitempty"`
// +optional
Conditions []common.Condition `json:"conditions,omitempty"`
}

type KVMSourceOptions struct {
// +optional
// Timeout is the maximum amount of time in seconds for the SSH connection
// to establish. A timeout of zero means no timeout.
// Defaults to 30 seconds.
SSHTimeoutSeconds *int `json:"sshTimeoutSeconds,omitempty"`

// +optional
// The connection URI to be used by the `virsh` command that is executed on
// the host specified by the endpoint address.
// E.g., `qemu:///system`
// See https://libvirt.org/uri.html#local-hypervisor-uris for more information.
VirshConnectionURI *string `json:"virshConnectionURI"`
}

func (s *KVMSource) NamespacedName() string {
return types.NamespacedName{
Namespace: s.Namespace,
Name: s.Name,
}.String()
}

func (s *KVMSource) ClusterStatus() ClusterStatus {
return s.Status.Status
}

func (s *KVMSource) HasSecret() bool {
return true
}

func (s *KVMSource) SecretReference() *corev1.SecretReference {
return &s.Spec.Credentials
}

func (s *KVMSource) GetKind() string {
return KindKVMSource
}

func (s *KVMSource) GetConnectionInfo() (string, string) {
return s.Spec.EndpointAddress, ""
}

func (s *KVMSource) GetOptions() interface{} {
return s.Spec.KVMSourceOptions
}

// GetSSHTimeout returns the SSH timeout duration.
func (s *KVMSourceOptions) GetSSHTimeout() time.Duration {
return time.Duration(ptr.Deref(s.SSHTimeoutSeconds, DefaultSSHTimeoutSeconds)) * time.Second
}

// GetVirshConnectionURI returns the virsh connection URI.
func (s *KVMSourceOptions) GetVirshConnectionURI() string {
return ptr.Deref(s.VirshConnectionURI, DefaultVirshConnectionURI)
}
Loading
Loading