diff --git a/go.mod b/go.mod index ea5911b2e7..9bb745f142 100644 --- a/go.mod +++ b/go.mod @@ -22,10 +22,10 @@ require ( github.com/stretchr/testify v1.11.1 github.com/vmware-tanzu/vm-operator/api v1.9.1-0.20250923172217-bf5a74e51c65 github.com/vmware-tanzu/vm-operator/external/byok v0.0.0-20250509154507-b93e51fc90fa - github.com/vmware/govmomi v0.53.0-alpha.0.0.20251203213634-99f18b71ea8e + github.com/vmware/govmomi v0.53.0-alpha.0.0.20260418033738-262be50854f2 go.uber.org/zap v1.27.1 golang.org/x/sync v0.19.0 - golang.org/x/sys v0.39.0 + golang.org/x/sys v0.40.0 google.golang.org/grpc v1.79.3 google.golang.org/protobuf v1.36.11 gopkg.in/gcfg.v1 v1.2.3 @@ -87,13 +87,13 @@ require ( go.uber.org/multierr v1.11.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/mod v0.31.0 // indirect - golang.org/x/net v0.48.0 // indirect + golang.org/x/mod v0.32.0 // indirect + golang.org/x/net v0.49.0 // indirect golang.org/x/oauth2 v0.34.0 // indirect - golang.org/x/term v0.38.0 // indirect - golang.org/x/text v0.32.0 // indirect + golang.org/x/term v0.39.0 // indirect + golang.org/x/text v0.34.0 // indirect golang.org/x/time v0.9.0 // indirect - golang.org/x/tools v0.40.0 // indirect + golang.org/x/tools v0.41.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/go.sum b/go.sum index f141871a95..a40d3e5030 100644 --- a/go.sum +++ b/go.sum @@ -174,8 +174,8 @@ github.com/vmware-tanzu/vm-operator/api v1.9.1-0.20250923172217-bf5a74e51c65 h1: github.com/vmware-tanzu/vm-operator/api v1.9.1-0.20250923172217-bf5a74e51c65/go.mod h1:nWTPpxfe4gHuuYuFcrs86+NMxfkqPk3a3IlvI8TCWak= github.com/vmware-tanzu/vm-operator/external/byok v0.0.0-20250509154507-b93e51fc90fa h1:4MKu14YJ7J54O6QKmT4ds5EUpysWLLtQRMff73cVkmU= github.com/vmware-tanzu/vm-operator/external/byok v0.0.0-20250509154507-b93e51fc90fa/go.mod h1:8tiuyYslzjLIUmOlXZuGKQdQP2ZgWGCVhVeyptmZYnk= -github.com/vmware/govmomi v0.53.0-alpha.0.0.20251203213634-99f18b71ea8e h1:TG9xuPu9N29Ak1gNs85VsMImNv1bd2l0yNfAMc3imOU= -github.com/vmware/govmomi v0.53.0-alpha.0.0.20251203213634-99f18b71ea8e/go.mod h1:FM3GTg002dFFN7l2/hNS0YWC4f78HTw4kvgUwAE52cM= +github.com/vmware/govmomi v0.53.0-alpha.0.0.20260418033738-262be50854f2 h1:uZyUn9ASbYtzL3oWA/bEKQYyO2Mm2+9qNg++Nv3XQCI= +github.com/vmware/govmomi v0.53.0-alpha.0.0.20260418033738-262be50854f2/go.mod h1:0F3hChqXDrSQQnjfSiCqRE5lPD4aZlbOtKG4uroq2a4= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= @@ -206,29 +206,29 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= -golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= -golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= +golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= +golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= -golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= +golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= -golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= -golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= +golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= -golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= -golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= +golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= +golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= diff --git a/manifests/supervisorcluster/1.33/cns-csi.yaml b/manifests/supervisorcluster/1.33/cns-csi.yaml index 2cd5e537b4..4bf3c949fd 100644 --- a/manifests/supervisorcluster/1.33/cns-csi.yaml +++ b/manifests/supervisorcluster/1.33/cns-csi.yaml @@ -141,6 +141,9 @@ rules: - apiGroups: ["iaas.vmware.com"] resources: ["capabilities"] verbs: ["get", "list", "watch"] + - apiGroups: ["cnsdp.vmware.com"] + resources: ["cbtconfigs"] + verbs: ["get", "list", "watch"] --- kind: ClusterRoleBinding apiVersion: rbac.authorization.k8s.io/v1 diff --git a/manifests/supervisorcluster/1.34/cns-csi.yaml b/manifests/supervisorcluster/1.34/cns-csi.yaml index 2cd5e537b4..4bf3c949fd 100644 --- a/manifests/supervisorcluster/1.34/cns-csi.yaml +++ b/manifests/supervisorcluster/1.34/cns-csi.yaml @@ -141,6 +141,9 @@ rules: - apiGroups: ["iaas.vmware.com"] resources: ["capabilities"] verbs: ["get", "list", "watch"] + - apiGroups: ["cnsdp.vmware.com"] + resources: ["cbtconfigs"] + verbs: ["get", "list", "watch"] --- kind: ClusterRoleBinding apiVersion: rbac.authorization.k8s.io/v1 diff --git a/pkg/apis/cbtconfig/v1alpha1/cbtconfig_types.go b/pkg/apis/cbtconfig/v1alpha1/cbtconfig_types.go new file mode 100644 index 0000000000..624c0a770a --- /dev/null +++ b/pkg/apis/cbtconfig/v1alpha1/cbtconfig_types.go @@ -0,0 +1,70 @@ +/* +Copyright 2026 The Kubernetes Authors. + +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 v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// CBTConfigSpec defines the desired state of CBTConfig +type CBTConfigSpec struct { + // Cbt is requested to be set true/false in the namespace. + Enabled bool `json:"enabled"` +} + +// CBTConfigStatus defines the observed state of CBTConfig. +type CBTConfigStatus struct { + // The cbt status of the namespace. + Enabled *bool `json:"enabled,omitempty"` + + // The last error encountered during import operation, if any. + Error string `json:"error,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:resource:scope=Namespaced + +// CBTConfig is the Schema for the namespace level CBT enablement API. +type CBTConfig struct { + metav1.TypeMeta `json:",inline"` + + // metadata is a standard object metadata + // +optional + metav1.ObjectMeta `json:"metadata,omitempty"` + + // spec defines the desired state of CBTConfig + // +required + Spec CBTConfigSpec `json:"spec"` + + // status defines the observed state of CBTConfig + // +optional + Status CBTConfigStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// CBTConfigList contains a list of CBTConfig +type CBTConfigList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []CBTConfig `json:"items"` +} + +func init() { + SchemeBuilder.Register(&CBTConfig{}, &CBTConfigList{}) +} diff --git a/pkg/apis/cbtconfig/v1alpha1/groupversion_info.go b/pkg/apis/cbtconfig/v1alpha1/groupversion_info.go new file mode 100644 index 0000000000..e24fb24b8a --- /dev/null +++ b/pkg/apis/cbtconfig/v1alpha1/groupversion_info.go @@ -0,0 +1,48 @@ +/* +Copyright 2026 The Kubernetes Authors. + +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 v1alpha1 contains API Schema definitions for the cnsdp v1alpha1 API group +// +kubebuilder:object:generate=true +// +groupName=cnsdp.vmware.com +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +const ( + // GroupName is the API group for CBTConfig resources. + GroupName = "cnsdp.vmware.com" + + // Version is the API version for CBTConfig resources. + Version = "v1alpha1" + + // CBTConfigResource is the plural resource name for CBTConfig, + // used when constructing GroupVersionResource for dynamic clients. + CBTConfigResource = "cbtconfigs" +) + +var ( + // GroupVersion is group version used to register these objects + GroupVersion = schema.GroupVersion{Group: GroupName, Version: Version} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) diff --git a/pkg/apis/cbtconfig/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/cbtconfig/v1alpha1/zz_generated.deepcopy.go new file mode 100644 index 0000000000..31f85bdf7e --- /dev/null +++ b/pkg/apis/cbtconfig/v1alpha1/zz_generated.deepcopy.go @@ -0,0 +1,119 @@ +//go:build !ignore_autogenerated + +/* +Copyright 2026 The Kubernetes Authors. + +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. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CBTConfig) DeepCopyInto(out *CBTConfig) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CBTConfig. +func (in *CBTConfig) DeepCopy() *CBTConfig { + if in == nil { + return nil + } + out := new(CBTConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *CBTConfig) 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 *CBTConfigList) DeepCopyInto(out *CBTConfigList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]CBTConfig, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CBTConfigList. +func (in *CBTConfigList) DeepCopy() *CBTConfigList { + if in == nil { + return nil + } + out := new(CBTConfigList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *CBTConfigList) 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 *CBTConfigSpec) DeepCopyInto(out *CBTConfigSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CBTConfigSpec. +func (in *CBTConfigSpec) DeepCopy() *CBTConfigSpec { + if in == nil { + return nil + } + out := new(CBTConfigSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CBTConfigStatus) DeepCopyInto(out *CBTConfigStatus) { + *out = *in + if in.Enabled != nil { + in, out := &in.Enabled, &out.Enabled + *out = new(bool) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CBTConfigStatus. +func (in *CBTConfigStatus) DeepCopy() *CBTConfigStatus { + if in == nil { + return nil + } + out := new(CBTConfigStatus) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/common/cns-lib/volume/manager.go b/pkg/common/cns-lib/volume/manager.go index b6c4018b73..a21a82d7bf 100644 --- a/pkg/common/cns-lib/volume/manager.go +++ b/pkg/common/cns-lib/volume/manager.go @@ -131,6 +131,10 @@ type Manager interface { RetrieveVStorageObject(ctx context.Context, volumeID string) (*vim25types.VStorageObject, error) // ProtectVolumeFromVMDeletion sets keepAfterDeleteVm control flag on migrated volume ProtectVolumeFromVMDeletion(ctx context.Context, volumeID string) error + // SetVolumeControlFlags sets control flags for a given volume. + SetVolumeControlFlags(ctx context.Context, volumeID string, controlFlags []string) error + // ClearVolumeControlFlags clears control flags for a given volume. + ClearVolumeControlFlags(ctx context.Context, volumeID string, controlFlags []string) error // CreateSnapshot helps create a snapshot for a block volume CreateSnapshot(ctx context.Context, volumeID string, desc string, extraParams interface{}) (*CnsSnapshotInfo, error) // DeleteSnapshot helps delete a snapshot for a block volume @@ -3649,6 +3653,90 @@ func (m *defaultManager) ProtectVolumeFromVMDeletion(ctx context.Context, volume return nil } +// SetVolumeControlFlags sets control flags for the given volume. +func (m *defaultManager) SetVolumeControlFlags(ctx context.Context, volumeID string, controlFlags []string) error { + ctx, cancelFunc := ensureOperationContextHasATimeout(ctx) + defer cancelFunc() + internalSetVolumeControlFlags := func() error { + log := logger.GetLogger(ctx) + err := validateManager(ctx, m) + if err != nil { + log.Errorf("failed to validate volume manager with err: %+v", err) + return err + } + + err = m.virtualCenter.ConnectCns(ctx) + if err != nil { + log.Errorf("ConnectCns failed with err: %+v", err) + return err + } + + log.Infof("Setting control flags %v for volumeID: %q", controlFlags, volumeID) + err = m.virtualCenter.CnsClient.SetVolumeControlFlags(ctx, []cnstypes.CnsVolumeControlFlagsSpec{{ + VolumeId: cnstypes.CnsVolumeId{Id: volumeID}, + ControlFlags: controlFlags, + }}) + if err != nil { + log.Errorf("failed to set control flags %v for volumeID %q with err: %v", controlFlags, volumeID, err) + return err + } + log.Infof("Successfully set control flags %v for volumeID: %q", controlFlags, volumeID) + return nil + } + start := time.Now() + err := internalSetVolumeControlFlags() + if err != nil { + prometheus.CnsControlOpsHistVec.WithLabelValues(prometheus.PrometheusCnsSetVolumeControlFlagsOpType, + prometheus.PrometheusFailStatus).Observe(time.Since(start).Seconds()) + } else { + prometheus.CnsControlOpsHistVec.WithLabelValues(prometheus.PrometheusCnsSetVolumeControlFlagsOpType, + prometheus.PrometheusPassStatus).Observe(time.Since(start).Seconds()) + } + return err +} + +// ClearVolumeControlFlags clears control flags for the given volume. +func (m *defaultManager) ClearVolumeControlFlags(ctx context.Context, volumeID string, controlFlags []string) error { + ctx, cancelFunc := ensureOperationContextHasATimeout(ctx) + defer cancelFunc() + internalClearVolumeControlFlags := func() error { + log := logger.GetLogger(ctx) + err := validateManager(ctx, m) + if err != nil { + log.Errorf("failed to validate volume manager with err: %+v", err) + return err + } + + err = m.virtualCenter.ConnectCns(ctx) + if err != nil { + log.Errorf("ConnectCns failed with err: %+v", err) + return err + } + + log.Infof("Clearing control flags %v for volumeID: %q", controlFlags, volumeID) + err = m.virtualCenter.CnsClient.ClearVolumeControlFlags(ctx, []cnstypes.CnsVolumeControlFlagsSpec{{ + VolumeId: cnstypes.CnsVolumeId{Id: volumeID}, + ControlFlags: controlFlags, + }}) + if err != nil { + log.Errorf("failed to clear control flags %v for volumeID %q with err: %v", controlFlags, volumeID, err) + return err + } + log.Infof("Successfully cleared control flags %v for volumeID: %q", controlFlags, volumeID) + return nil + } + start := time.Now() + err := internalClearVolumeControlFlags() + if err != nil { + prometheus.CnsControlOpsHistVec.WithLabelValues(prometheus.PrometheusCnsClearVolumeControlFlagsOpType, + prometheus.PrometheusFailStatus).Observe(time.Since(start).Seconds()) + } else { + prometheus.CnsControlOpsHistVec.WithLabelValues(prometheus.PrometheusCnsClearVolumeControlFlagsOpType, + prometheus.PrometheusPassStatus).Observe(time.Since(start).Seconds()) + } + return err +} + // GetAllManagerInstances returns all Manager instances func GetAllManagerInstances(ctx context.Context) map[string]*defaultManager { newManagerInstanceMap := make(map[string]*defaultManager) diff --git a/pkg/common/cns-lib/volume/manager_mock.go b/pkg/common/cns-lib/volume/manager_mock.go index e3fa1abc5d..65c9635ee1 100644 --- a/pkg/common/cns-lib/volume/manager_mock.go +++ b/pkg/common/cns-lib/volume/manager_mock.go @@ -141,6 +141,16 @@ func (m MockManager) ProtectVolumeFromVMDeletion(ctx context.Context, volumeID s panic("implement me") } +func (m MockManager) SetVolumeControlFlags(ctx context.Context, volumeID string, controlFlags []string) error { + //TODO implement me + panic("implement me") +} + +func (m MockManager) ClearVolumeControlFlags(ctx context.Context, volumeID string, controlFlags []string) error { + //TODO implement me + panic("implement me") +} + func (m MockManager) CreateSnapshot(ctx context.Context, volumeID string, desc string, extraParams interface{}) (*CnsSnapshotInfo, error) { //TODO implement me diff --git a/pkg/common/prometheus/metrics.go b/pkg/common/prometheus/metrics.go index 0037594d7e..fef81a6da4 100644 --- a/pkg/common/prometheus/metrics.go +++ b/pkg/common/prometheus/metrics.go @@ -74,6 +74,10 @@ const ( PrometheusCnsQueryAllVolumeOpType = "query-all-volume" // PrometheusCnsQueryVolumeInfoOpType represents the QueryVolumeInfo operation. PrometheusCnsQueryVolumeInfoOpType = "query-volume-info" + // PrometheusCnsSetVolumeControlFlagsOpType represents the SetVolumeControlFlags operation. + PrometheusCnsSetVolumeControlFlagsOpType = "set-volume-control-flags" + // PrometheusCnsClearVolumeControlFlagsOpType represents the ClearVolumeControlFlags operation. + PrometheusCnsClearVolumeControlFlagsOpType = "clear-volume-control-flags" // PrometheusCnsRelocateVolumeOpType represents the RelocateVolume operation. PrometheusCnsRelocateVolumeOpType = "relocate-volume" // PrometheusCnsConfigureVolumeACLOpType represents the ConfigureVolumeAcl operation. diff --git a/pkg/common/unittestcommon/types.go b/pkg/common/unittestcommon/types.go index 29c93d98c3..189e676f9c 100644 --- a/pkg/common/unittestcommon/types.go +++ b/pkg/common/unittestcommon/types.go @@ -187,6 +187,12 @@ func (m *MockVolumeManager) RetrieveVStorageObject(ctx context.Context, func (m *MockVolumeManager) ProtectVolumeFromVMDeletion(ctx context.Context, volumeID string) error { return nil } +func (m *MockVolumeManager) SetVolumeControlFlags(ctx context.Context, volumeID string, controlFlags []string) error { + return nil +} +func (m *MockVolumeManager) ClearVolumeControlFlags(ctx context.Context, volumeID string, controlFlags []string) error { + return nil +} func (m *MockVolumeManager) CreateSnapshot(ctx context.Context, volumeID string, desc string, extraParams interface{}) (*cnsvolume.CnsSnapshotInfo, error) { return nil, nil diff --git a/pkg/csi/service/common/constants.go b/pkg/csi/service/common/constants.go index 4807a15e94..48480e9b1a 100644 --- a/pkg/csi/service/common/constants.go +++ b/pkg/csi/service/common/constants.go @@ -490,6 +490,13 @@ const ( // HighPVNodeDensity is an FSS for guest cluster nodes that, when enabled, // raises MAX_VOLUMES_PER_NODE from 59 to 255 in NodeGetInfo responses. HighPVNodeDensity = "high-pv-node-density" + // CSI_Backup_API is an WCP capability for Changed Block Tracking(CBT) support in CNS-CSI. + // This is a feature to support Changed Block Tracking (CBT) for efficient + // backup and restore operations using CSI SnapshotMetadata service. + // This enables GetMetadataAllocated and GetMetadataDelta RPCs. + CSI_Backup_API = "supports_CSI_Backup_API" + // CSI_Backup_API_FSS is an FSS for Changed Block Tracking(CBT) support in pvCSI + CSI_Backup_API_FSS = "CSI_Backup_API" ) var WCPFeatureStates = map[string]struct{}{ @@ -507,6 +514,7 @@ var WCPFeatureStates = map[string]struct{}{ MultipleClustersPerVsphereZone: {}, FileVolumesWithVmService: {}, VsanFileVolumeService: {}, + CSI_Backup_API: {}, } // WCPFeatureStatesSupportsLateEnablement contains capabilities that can be enabled later @@ -522,6 +530,7 @@ var WCPFeatureStatesSupportsLateEnablement = map[string]struct{}{ SharedDiskFss: {}, FileVolumesWithVmService: {}, VsanFileVolumeService: {}, + CSI_Backup_API: {}, } // WCPFeatureAssociatedWithPVCSI contains FSS name used in PVCSI and associated WCP Capability name on a @@ -532,4 +541,5 @@ var WCPFeatureStateAssociatedWithPVCSI = map[string]string{ WorkloadDomainIsolationFSS: WorkloadDomainIsolation, LinkedCloneSupportFSS: LinkedCloneSupport, VsanFileVolumeServiceSupportFSS: VsanFileVolumeService, + CSI_Backup_API_FSS: CSI_Backup_API, } diff --git a/pkg/csi/service/common/util.go b/pkg/csi/service/common/util.go index e6776e0c88..df462d5ea7 100644 --- a/pkg/csi/service/common/util.go +++ b/pkg/csi/service/common/util.go @@ -29,13 +29,16 @@ import ( "github.com/container-storage-interface/spec/lib/go/csi" pbmtypes "github.com/vmware/govmomi/pbm/types" "github.com/vmware/govmomi/vim25/types" + apierrors "k8s.io/apimachinery/pkg/api/errors" apiMeta "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/dynamic" crconfig "sigs.k8s.io/controller-runtime/pkg/client/config" + cbtconfigv1alpha1 "sigs.k8s.io/vsphere-csi-driver/v3/pkg/apis/cbtconfig/v1alpha1" cnsvolume "sigs.k8s.io/vsphere-csi-driver/v3/pkg/common/cns-lib/volume" cnsvsphere "sigs.k8s.io/vsphere-csi-driver/v3/pkg/common/cns-lib/vsphere" cnsconfig "sigs.k8s.io/vsphere-csi-driver/v3/pkg/common/config" @@ -520,3 +523,42 @@ func GetCNSVolumeInfoPatch(ctx context.Context, CapacityInMb int64, volumeId str } return patch, nil } + +// IsCBTEnabledForNamespace reports whether any CBTConfig in the namespace has status.enabled true. +func IsCBTEnabledForNamespace(ctx context.Context, pvcNamespace string) (bool, error) { + cfg, err := crconfig.GetConfig() + if err != nil { + return false, fmt.Errorf("failed to get Kubernetes config. Err: %w", err) + } + log := logger.GetLogger(ctx) + dynClient, err := dynamic.NewForConfig(cfg) + if err != nil { + return false, fmt.Errorf("failed to create dynamic client using config. Err: %w", err) + } + + gvr := cbtconfigv1alpha1.GroupVersion.WithResource(cbtconfigv1alpha1.CBTConfigResource) + unstructuredList, err := dynClient.Resource(gvr).Namespace(pvcNamespace).List(ctx, metav1.ListOptions{}) + if err != nil { + // CRD may not be registered yet, or the API version may be absent from discovery. + if apiMeta.IsNoMatchError(err) || apierrors.IsNotFound(err) { + log.Debugf("CBTConfig CR is not registered in namespace %s", pvcNamespace) + return false, nil + } + return false, fmt.Errorf("failed to list CBTConfig CRs in namespace %s: %w", pvcNamespace, err) + } + + var cbtConfigList cbtconfigv1alpha1.CBTConfigList + err = runtime.DefaultUnstructuredConverter.FromUnstructured(unstructuredList.UnstructuredContent(), &cbtConfigList) + if err != nil { + return false, fmt.Errorf("failed to convert unstructured list to CBTConfigList: %w", err) + } + + for _, item := range cbtConfigList.Items { + if item.Status.Enabled != nil && *item.Status.Enabled { + log.Debugf("CBT is enabled for namespace %s", pvcNamespace) + return true, nil + } + } + + return false, nil +} diff --git a/pkg/csi/service/common/vsphereutil.go b/pkg/csi/service/common/vsphereutil.go index 2b00776465..3b9842769a 100644 --- a/pkg/csi/service/common/vsphereutil.go +++ b/pkg/csi/service/common/vsphereutil.go @@ -1385,3 +1385,40 @@ func createCryptoSpec(oldKeyID, newKeyID *vim25types.CryptoKeyId) vim25types.Bas return &vim25types.CryptoSpecShallowRecrypt{NewKeyId: *newKeyID} } + +// VolumeChangedBlockTrackingEnabled reports whether CNS has changed-block tracking +// enabled for the volume. It returns an error when QueryVolume fails or returns +// no matching volume. +func VolumeChangedBlockTrackingEnabled(ctx context.Context, + volumeManager cnsvolume.Manager, volumeID string) (bool, error) { + queryRes, err := volumeManager.QueryVolume(ctx, cnstypes.CnsQueryFilter{ + VolumeIds: []cnstypes.CnsVolumeId{{Id: volumeID}}, + }) + if err != nil { + return false, err + } + if len(queryRes.Volumes) == 0 { + return false, fmt.Errorf("no CNS volume returned for volume ID %q", volumeID) + } + return queryRes.Volumes[0].ChangedBlockTracking == cnstypes.CnsVolumeCBTStatusEnabled, nil +} + +// SetVolumeCbtFlagsUtil is the helper function to set CBT flags for the given volume. +func SetVolumeCbtFlagsUtil(ctx context.Context, volumeManager cnsvolume.Manager, volumeID string) error { + err := volumeManager.SetVolumeControlFlags(ctx, volumeID, + []string{string(cnstypes.CnsVolumeControlFlagsEnableChangedBlockTracking)}) + if err != nil { + return err + } + return nil +} + +// ClearVolumeCbtFlagsUtil is the helper function to clear CBT flags for the given volume. +func ClearVolumeCbtFlagsUtil(ctx context.Context, volumeManager cnsvolume.Manager, volumeID string) error { + err := volumeManager.ClearVolumeControlFlags(ctx, volumeID, + []string{string(cnstypes.CnsVolumeControlFlagsEnableChangedBlockTracking)}) + if err != nil { + return err + } + return nil +} diff --git a/pkg/csi/service/common/vsphereutil_test.go b/pkg/csi/service/common/vsphereutil_test.go index 15de32eb84..2ceb368cfe 100644 --- a/pkg/csi/service/common/vsphereutil_test.go +++ b/pkg/csi/service/common/vsphereutil_test.go @@ -96,6 +96,12 @@ func (m *mockVolumeManager) RetrieveVStorageObject(ctx context.Context, func (m *mockVolumeManager) ProtectVolumeFromVMDeletion(ctx context.Context, volumeID string) error { return nil } +func (m *mockVolumeManager) SetVolumeControlFlags(ctx context.Context, volumeID string, controlFlags []string) error { + return nil +} +func (m *mockVolumeManager) ClearVolumeControlFlags(ctx context.Context, volumeID string, controlFlags []string) error { + return nil +} func (m *mockVolumeManager) CreateSnapshot(ctx context.Context, volumeID string, desc string, extraParams interface{}) (*cnsvolume.CnsSnapshotInfo, error) { return nil, nil diff --git a/pkg/csi/service/wcp/controller.go b/pkg/csi/service/wcp/controller.go index 0611fa5d0b..3306f8488c 100644 --- a/pkg/csi/service/wcp/controller.go +++ b/pkg/csi/service/wcp/controller.go @@ -87,6 +87,8 @@ var ( isPodVMOnStretchSupervisorFSSEnabled bool // IsMultipleClustersPerVsphereZoneFSSEnabled is true when supports_multiple_clusters_per_zone FSS is enabled. IsMultipleClustersPerVsphereZoneFSSEnabled bool + // isCSIBackupAPIEnabled is true when supports_CSI_Backup_API FSS is enabled. + isCSIBackupAPIEnabled bool ) var getCandidateDatastores = cnsvsphere.GetCandidateDatastoresInCluster @@ -199,6 +201,14 @@ func (c *controller) Init(config *cnsconfig.Config, version string) error { go commonco.ContainerOrchestratorUtility.HandleLateEnablementOfCapability(ctx, cnstypes.CnsClusterFlavorWorkload, common.VsanFileVolumeService, "", "") } + isCSIBackupAPIEnabled = commonco.ContainerOrchestratorUtility.IsFSSEnabled(ctx, common.CSI_Backup_API) + if !isCSIBackupAPIEnabled { + log.Info("CSI Backup API feature flag is disabled. Handling late enablement.") + go commonco.ContainerOrchestratorUtility.HandleLateEnablementOfCapability(ctx, cnstypes.CnsClusterFlavorWorkload, + common.CSI_Backup_API, "", "") + } else { + log.Info("CSI Backup API feature flag is enabled.") + } if idempotencyHandlingEnabled { log.Info("CSI Volume manager idempotency handling feature flag is enabled.") operationStore, err = cnsvolumeoperationrequest.InitVolumeOperationRequestInterface(ctx, @@ -1139,6 +1149,23 @@ func (c *controller) createBlockVolume(ctx context.Context, req *csi.CreateVolum } } + if isCSIBackupAPIEnabled { + if pvcNamespace, ok := req.Parameters[common.AttributePvcNamespace]; ok && pvcNamespace != "" { + enableCbt, err := common.IsCBTEnabledForNamespace(ctx, pvcNamespace) + if err != nil { + log.Warnf("failed to get enable CBT for namespace %s with error %+v", pvcNamespace, err) + } else if enableCbt { + err = common.SetVolumeCbtFlagsUtil(ctx, c.manager.VolumeManager, volumeInfo.VolumeID.Id) + if err != nil { + log.Warnf("failed to set CBT flags for volume %s with error %+v", volumeInfo.VolumeID.Id, err) + } + } + } else { + log.Warnf("failed to set CBT flag for volume %s due to missing PVC namespace from request parameters", + volumeInfo.VolumeID.Id) + } + } + return resp, "", nil } @@ -1920,6 +1947,30 @@ func (c *controller) ControllerPublishVolume(ctx context.Context, req *csi.Contr volumeAttachment) } + isBlockRequest := !isFileVolumeRequestInWcp(ctx, []*csi.VolumeCapability{req.GetVolumeCapability()}) + if isCSIBackupAPIEnabled && isBlockRequest { + _, pvcNamespace, ok := commonco.ContainerOrchestratorUtility.GetPVCNameFromCSIVolumeID(req.VolumeId) + if ok && pvcNamespace != "" { + nsEnableCbt, nsErr := common.IsCBTEnabledForNamespace(ctx, pvcNamespace) + if nsErr != nil { + log.Warnf("failed to get CBTConfig for namespace %s with error %+v", pvcNamespace, nsErr) + } + if nsEnableCbt { + volEnableCbt, qErr := common.VolumeChangedBlockTrackingEnabled(ctx, c.manager.VolumeManager, req.VolumeId) + if qErr != nil { + log.Warnf("failed to query CBT status for volume %s with error %+v", req.VolumeId, qErr) + } + if volEnableCbt { + log.Debugf("volume %s already has CBT enabled", req.VolumeId) + } else { + if setErr := common.SetVolumeCbtFlagsUtil(ctx, c.manager.VolumeManager, req.VolumeId); setErr != nil { + log.Warnf("skipped set CBT flags error %+v for volume %s", setErr, req.VolumeId) + } + } + } + } + } + vmuuid, err := getPodVMUUID(ctx, req.VolumeId, req.NodeId) if err != nil { if e, ok := status.FromError(err); ok { diff --git a/pkg/syncer/cnsoperator/controller/cnsregistervolume/cnsregistervolume_controller_test.go b/pkg/syncer/cnsoperator/controller/cnsregistervolume/cnsregistervolume_controller_test.go index 895f9ec073..331fbd4aef 100644 --- a/pkg/syncer/cnsoperator/controller/cnsregistervolume/cnsregistervolume_controller_test.go +++ b/pkg/syncer/cnsoperator/controller/cnsregistervolume/cnsregistervolume_controller_test.go @@ -160,6 +160,14 @@ func (m *mockVolumeManager) ProtectVolumeFromVMDeletion(ctx context.Context, vol panic("implement me") } +func (m *mockVolumeManager) SetVolumeControlFlags(ctx context.Context, volumeID string, controlFlags []string) error { + return nil +} + +func (m *mockVolumeManager) ClearVolumeControlFlags(ctx context.Context, volumeID string, controlFlags []string) error { + return nil +} + func (m *mockVolumeManager) CreateSnapshot(ctx context.Context, volumeID string, desc string, extraParams interface{}) (*cnsvolume.CnsSnapshotInfo, error) { //TODO implement me diff --git a/pkg/syncer/fullsync_test.go b/pkg/syncer/fullsync_test.go index 0074cbcc9d..15ad12254d 100644 --- a/pkg/syncer/fullsync_test.go +++ b/pkg/syncer/fullsync_test.go @@ -900,6 +900,18 @@ func (m *mockVolumeManagerForFullSync) ProtectVolumeFromVMDeletion(ctx context.C return nil } +func (m *mockVolumeManagerForFullSync) SetVolumeControlFlags( + ctx context.Context, volumeID string, controlFlags []string, +) error { + return nil +} + +func (m *mockVolumeManagerForFullSync) ClearVolumeControlFlags( + ctx context.Context, volumeID string, controlFlags []string, +) error { + return nil +} + func (m *mockVolumeManagerForFullSync) QuerySnapshots( ctx context.Context, snapshotQueryFilter cnstypes.CnsSnapshotQueryFilter, ) (*cnstypes.CnsSnapshotQueryResult, error) {