Repository: dell/csi-baremetal (go)

Issue:
Support custom WBT settings pass
**Is your feature request related to a problem? Please describe.**
CSI should have an opportunity to set custom WBT value for device.

**Describe the solution you'd like**
Options:
- disable/enable for kernel versions range
- restore default value

**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.

**Additional context**
Add any other context or screenshots about the feature request here.


Pull request description:
[ISSUE-635] Support WBT disabling (release)
### Resolves #635

Changes from https://github.com/dell/csi-baremetal/pull/638

## PR checklist
- [x] Add link to the issue
- [x] Choose Project
- [x] Choose PR label
- [x] New unit tests added
- [x] Modified code has meaningful comments
- [x] All TODOs are linked with the issues
- [ ] All comments are resolved

## Testing
In https://github.com/dell/csi-baremetal/pull/638


Patch:
diff --git a/cmd/node/main.go b/cmd/node/main.go
index 72b29e1..f80017c 100644
--- a/cmd/node/main.go
+++ b/cmd/node/main.go
@@ -28,6 +28,7 @@ import (
 
 	"google.golang.org/grpc"
 	"google.golang.org/grpc/health/grpc_health_v1"
+	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 	clientgoscheme "k8s.io/client-go/kubernetes/scheme"
 	_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
@@ -52,6 +53,7 @@ import (
 	"github.com/dell/csi-baremetal/pkg/events"
 	"github.com/dell/csi-baremetal/pkg/metrics"
 	"github.com/dell/csi-baremetal/pkg/node"
+	"github.com/dell/csi-baremetal/pkg/node/wbt"
 	grpc_prometheus "github.com/grpc-ecosystem/go-grpc-prometheus"
 	"github.com/prometheus/client_golang/prometheus"
 	"github.com/prometheus/client_golang/prometheus/promhttp"
@@ -140,6 +142,11 @@ func main() {
 		logger.Fatalf("fail to prepare event recorder: %v", err)
 	}
 
+	wbtWatcher, err := prepareWbtWatcher(k8SClient, eventRecorder, *nodeName, logger)
+	if err != nil {
+		logger.Fatalf("fail to prepare wbt watcher: %v", err)
+	}
+
 	// Wait till all events are sent/handled
 	defer eventRecorder.Wait()
 
@@ -191,6 +198,9 @@ func main() {
 	// wait for readiness
 	waitForVolumeManagerReadiness(csiNodeService, logger)
 
+	// start to updating Wbt Config
+	wbtWatcher.StartWatch(csiNodeService)
+
 	logger.Info("Starting handle CSI calls ...")
 	if err := csiUDSServer.RunServer(); err != nil && err != grpc.ErrServerStopped {
 		logger.Fatalf("fail to serve: %v", err)
@@ -329,3 +339,16 @@ func prepareEventRecorder(nodeName string, logger *logrus.Logger) (*events.Recor
 	}
 	return eventRecorder, nil
 }
+
+func prepareWbtWatcher(client k8sClient.Client, eventsRecorder *events.Recorder, nodeName string, logger *logrus.Logger) (*wbt.ConfWatcher, error) {
+	k8sNode := &corev1.Node{}
+	err := client.Get(context.Background(), k8sClient.ObjectKey{Name: nodeName}, k8sNode)
+	if err != nil {
+		return nil, err
+	}
+
+	nodeKernel := k8sNode.Status.NodeInfo.KernelVersion
+	ll := logger.WithField("componentName", "WbtWatcher")
+
+	return wbt.NewConfWatcher(client, eventsRecorder, ll, nodeKernel), nil
+}
diff --git a/docs/custom-wbt-settings.md b/docs/custom-wbt-settings.md
new file mode 100644
index 0000000..2617dba
--- /dev/null
+++ b/docs/custom-wbt-settings.md
@@ -0,0 +1,74 @@
+# Proposal: Support option to change WBT settings
+
+Last updated: 07.12.2021
+
+
+## Abstract
+
+CSI should have an opportunity to automate managing of WBT (writeback throttling) value for specific SCs.
+
+## Background
+WBT feature was introduced in 4.10 kernel and have a [bug in some versions](https://access.redhat.com/solutions/4904681).
+Storage Systems, which use Baremetal CSI, may have performance issues due to incorrect WBT settings.
+Recommended workaround in disabling this feature.
+So it is specific for each device (sdb, sdc, ...), CSI should have an opportunity to control it on Volume or Pod creation stage.
+
+## Proposal
+
+#### Operator part
+
+Add ConfigMap to manage WBT settings in csi-baremetal-deployment helm chart.
+CSI Node will check it every 60 seconds and update existing one.
+
+```
+data:
+  config.yaml: |-
+    enable: true
+    # Value to set (uint32), 0 means disabling
+    wbt_lat_usec_value: 0
+    acceptable_volume_options:
+    # Block volumes don't take any impact from WBT
+      modes:
+        - FS
+      # It is risky to change WBT settings for LVG Volumes
+      storage_types:
+        - HDD
+        - SSD
+        - NVME
+
+# The list of acceptable kernel versions
+# Is some nodes are not in, user should add values manually via editting CM
+  acceptable_kernels.yaml: |-
+    node_kernel_versions:
+      - 4.18.0-193.65.2.el8_2.x86_64
+```
+
+#### Node Part
+
+- Node Stage ->
+1. Check WBT configuration
+2. Scan current value (`cat /sys/block/<drive>/queue/wbt_lat_usec`)
+3. Set passed one, if it's not equal (`echo <value> > /sys/block/<drive>/queue/wbt_lat_usec`)
+4. Add `wbt-changed=yes` annotation on Volume
+5. Send `WBTValueSetFailed` Event if error
+
+- Node UnStage ->
+1. Check `wbt-changed=yes` annotation on Volume
+2. Restore default (`echo "-1" > /sys/block/<drive>/queue/wbt_lat_usec`) (It was performed even if unmount/removeDir errors)
+3. Delete annotation
+4. Send `WBTValueRestoreFailed` Event if error
+
+## Rationale
+
+We could trigger WBT disabling via PVC annotation as for fake-attach feature.
+
+## Compatibility
+
+WBT is supported only for 4.10 kernel version and above. Link - https://cateee.net/lkddb/web-lkddb/BLK_WBT.html
+
+## Open issues (if applicable)
+
+ID | Name | Descriptions | Status | Comments
+---| -----| -------------| ------ | --------
+ISSUE-1 | Should we enable WBT disabling by default for specific kernel version? | We could describe the kernel version map, where it is necessary  |   |  The kernel version map is not clarified
+ISSUE-2 | Is it correct to switch WBT settings on Stage/UnStage? | Other candidates - CreateVolume/DeleteVolume, Publish/UnPublish | RESOLVED  | In Create/DeleteVolume case settings won't be persisted on node reboot. When volume is shared across the pods you will have to manipulate WBT multiple times on PublishVolume
diff --git a/pkg/eventing/eventing.go b/pkg/eventing/eventing.go
index e849884..8c4c091 100644
--- a/pkg/eventing/eventing.go
+++ b/pkg/eventing/eventing.go
@@ -193,4 +193,20 @@ var (
 		severity:    ErrorType,
 		symptomCode: NoneSymptomCode,
 	}
+
+	WBTValueSetFailed = &EventDescription{
+		reason:      "WBTValueSetFailed",
+		severity:    ErrorType,
+		symptomCode: NoneSymptomCode,
+	}
+	WBTValueRestoreFailed = &EventDescription{
+		reason:      "WBTValueRestoreFailed",
+		severity:    WarningType,
+		symptomCode: NoneSymptomCode,
+	}
+	WBTConfigMapUpdateFailed = &EventDescription{
+		reason:      "WBTConfigMapUpdateFailed",
+		severity:    WarningType,
+		symptomCode: NoneSymptomCode,
+	}
 )
diff --git a/pkg/mocks/executors.go b/pkg/mocks/executors.go
index dccd08e..dbb1b33 100644
--- a/pkg/mocks/executors.go
+++ b/pkg/mocks/executors.go
@@ -183,7 +183,7 @@ func (g *GoMockExecutor) RunCmdWithAttempts(cmd interface{}, attempts int, timeo
 
 // RunCmd simulates execution of a command with OnCommand where user can set what the method should return
 func (g *GoMockExecutor) RunCmd(cmd interface{}, opts ...command.Options) (string, string, error) {
-	args := g.Mock.Called(cmd.(string))
+	args := g.Mock.Called(cmd)
 	return args.String(0), args.String(1), args.Error(2)
 }
 
diff --git a/pkg/mocks/linuxutils/wbt.go b/pkg/mocks/linuxutils/wbt.go
new file mode 100644
index 0000000..cfc651a
--- /dev/null
+++ b/pkg/mocks/linuxutils/wbt.go
@@ -0,0 +1,35 @@
+/*
+Copyright © 2020 Dell Inc. or its subsidiaries. All Rights Reserved.
+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 linuxutils
+
+import (
+	"github.com/stretchr/testify/mock"
+)
+
+// MockWrapWbt is a mock implementation of WrapWbt
+type MockWrapWbt struct {
+	mock.Mock
+}
+
+// SetValue is a mock implementations
+func (m *MockWrapWbt) SetValue(device string, value uint32) error {
+	args := m.Mock.Called(device, value)
+	return args.Error(0)
+}
+
+// RestoreDefault is a mock implementations
+func (m *MockWrapWbt) RestoreDefault(device string) error {
+	args := m.Mock.Called(device)
+	return args.Error(0)
+}
diff --git a/pkg/node/node.go b/pkg/node/node.go
index c1553bf..35ca6df 100644
--- a/pkg/node/node.go
+++ b/pkg/node/node.go
@@ -54,6 +54,9 @@ const (
 
 	fakeAttachVolumeAnnotation = "fake-attach"
 	fakeAttachVolumeKey        = "yes"
+
+	wbtChangedVolumeAnnotation = "wbt-changed"
+	wbtChangedVolumeKey        = "yes"
 )
 
 // CSINodeService is the implementation of NodeServer interface from GO CSI specification.
@@ -237,6 +240,16 @@ func (s *CSINodeService) NodeStageVolume(ctx context.Context, req *csi.NodeStage
 			"Fake-attach cleared for volume with ID %s", volumeID)
 	}
 
+	if newStatus == apiV1.VolumeReady && s.VolumeManager.checkWbtChangingEnable(ctx, volumeCR) {
+		if err := s.VolumeManager.setWbtValue(volumeCR); err != nil {
+			ll.Errorf("Unable to set custom WBT value for volume %s: %v", volumeCR.Name, err)
+			s.VolumeManager.recorder.Eventf(volumeCR, eventing.WBTValueSetFailed,
+				"Unable to set custom WBT value for volume %s", volumeCR.Name)
+		} else {
+			volumeCR.Annotations[wbtChangedVolumeAnnotation] = wbtChangedVolumeKey
+		}
+	}
+
 	if currStatus != apiV1.VolumeReady {
 		volumeCR.Spec.CSIStatus = newStatus
 		if err := s.k8sClient.UpdateCR(ctx, volumeCR); err != nil {
@@ -311,6 +324,15 @@ func (s *CSINodeService) NodeUnstageVolume(ctx context.Context, req *csi.NodeUns
 		}
 	}
 
+	if val, ok := volumeCR.Annotations[wbtChangedVolumeAnnotation]; ok && val == wbtChangedVolumeKey {
+		delete(volumeCR.Annotations, wbtChangedVolumeAnnotation)
+		if err := s.VolumeManager.restoreWbtValue(volumeCR); err != nil {
+			ll.Errorf("Unable to restore WBT value for volume %s: %v", volumeCR.Name, err)
+			s.VolumeManager.recorder.Eventf(volumeCR, eventing.WBTValueSetFailed,
+				"Unable to restore WBT value for volume %s", volumeCR.Name)
+		}
+	}
+
 	ctxWithID := context.WithValue(context.Background(), base.RequestUUID, req.GetVolumeId())
 	if updateErr := s.k8sClient.UpdateCR(ctxWithID, volumeCR); updateErr != nil {
 		ll.Errorf("Unable to update volume CR: %v", updateErr)
diff --git a/pkg/node/volumemgr.go b/pkg/node/volumemgr.go
index 73f7642..8555cce 100644
--- a/pkg/node/volumemgr.go
+++ b/pkg/node/volumemgr.go
@@ -19,6 +19,7 @@ package node
 import (
 	"context"
 	"fmt"
+	"reflect"
 	"regexp"
 	"strconv"
 	"strings"
@@ -58,6 +59,8 @@ import (
 	metricsC "github.com/dell/csi-baremetal/pkg/metrics/common"
 	p "github.com/dell/csi-baremetal/pkg/node/provisioners"
 	"github.com/dell/csi-baremetal/pkg/node/provisioners/utilwrappers"
+	wbtconf "github.com/dell/csi-baremetal/pkg/node/wbt/common"
+	wbtops "github.com/dell/csi-baremetal/pkg/node/wbt/operations"
 )
 
 const (
@@ -102,6 +105,9 @@ type VolumeManager struct {
 	lvmOps lvm.WrapLVM
 	// uses for running lsblk util
 	listBlk lsblk.WrapLsblk
+	// uses for disable/enable WBT
+	wbtOps    wbtops.WrapWbt
+	wbtConfig *wbtconf.WbtConfig
 
 	// uses for searching suitable Available Capacity
 	acProvider common.AvailableCapacityOperations
@@ -201,6 +207,7 @@ func NewVolumeManager(
 	partImpl := ph.NewWrapPartitionImpl(executor, logger)
 	lvm := lvm.NewLVM(executor, logger)
 	fsOps := utilwrappers.NewFSOperationsImpl(executor, logger)
+	wbtOps := wbtops.NewWbt(executor)
 
 	vm := &VolumeManager{
 		k8sClient:      k8sClient,
@@ -217,6 +224,7 @@ func NewVolumeManager(
 		lvmOps:                 lvm,
 		listBlk:                lsblk.NewLSBLK(logger),
 		partOps:                partImpl,
+		wbtOps:                 wbtOps,
 		nodeID:                 nodeID,
 		nodeName:               nodeName,
 		log:                    logger.WithField("component", "VolumeManager"),
@@ -1183,30 +1191,36 @@ func (m *VolumeManager) changeDriveIsCleanField(drive *drivecrd.Drive, clean boo
 	}
 }
 
-func (m *VolumeManager) isPVCNeedFakeAttach(volumeID string) bool {
+func (m *VolumeManager) getPVCForVolume(volumeID string) (*corev1.PersistentVolumeClaim, error) {
 	ctxWithID := context.WithValue(context.Background(), base.RequestUUID, volumeID)
 
 	pv := &corev1.PersistentVolume{}
 	if err := m.k8sClient.Get(ctxWithID, k8sCl.ObjectKey{Name: volumeID}, pv); err != nil {
 		m.log.Errorf("Failed to get Persistent Volume %s: %v", volumeID, err)
-		return false
+		return nil, err
 	}
 
 	pvcName := pv.Spec.ClaimRef.Name
 	pvcNamespace := pv.Spec.ClaimRef.Namespace
-
 	pvc := &corev1.PersistentVolumeClaim{}
 	if err := m.k8sClient.Get(ctxWithID, k8sCl.ObjectKey{Name: pvcName, Namespace: pvcNamespace}, pvc); err != nil {
 		m.log.Errorf("Failed to get Persistent Volume Claim %s in namespace %s: %v", pvcName, pvcNamespace, err)
-		return false
+		return nil, err
 	}
 
 	m.log.Debugf("PVC %s/%s was found for PV with ID - %s", pvc.Namespace, pvc.Name, pv.Name)
+	return pvc, nil
+}
 
+func (m *VolumeManager) isPVCNeedFakeAttach(volumeID string) bool {
+	pvc, err := m.getPVCForVolume(volumeID)
+	if err != nil {
+		m.log.Errorf("Failed to get Persistent Volume Claim for Volume %s: %+v", volumeID, err)
+		return false
+	}
 	if value, ok := pvc.Annotations[fakeAttachAnnotation]; ok && value == fakeAttachAllowKey {
 		return true
 	}
-
 	return false
 }
 
@@ -1226,3 +1240,104 @@ func (m *VolumeManager) overrideDriveHealth(drive *api.Drive, overriddenHealth,
 			driveCRName, overriddenHealth, apiV1.HealthGood, apiV1.HealthSuspect, apiV1.HealthBad, apiV1.HealthUnknown)
 	}
 }
+
+func (m *VolumeManager) setWbtValue(vol *volumecrd.Volume) error {
+	device, err := m.findDeviceName(vol)
+	if err != nil {
+		return err
+	}
+
+	err = m.wbtOps.SetValue(device, m.wbtConfig.Value)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (m *VolumeManager) restoreWbtValue(vol *volumecrd.Volume) error {
+	device, err := m.findDeviceName(vol)
+	if err != nil {
+		return err
+	}
+
+	err = m.wbtOps.RestoreDefault(device)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (m *VolumeManager) checkWbtChangingEnable(ctx context.Context, vol *volumecrd.Volume) bool {
+	if m.wbtConfig == nil {
+		return false
+	}
+
+	if !m.wbtConfig.Enable {
+		return false
+	}
+
+	isModeAcceptable := false
+	for _, mode := range m.wbtConfig.VolumeOptions.Modes {
+		if mode == vol.Spec.Mode {
+			isModeAcceptable = true
+			break
+		}
+	}
+	if !isModeAcceptable {
+		m.log.Infof("Skip wbt value changing: volume %s has mode %s, acceptable: %v", vol.Name, vol.Spec.Mode, m.wbtConfig.VolumeOptions.Modes)
+		return false
+	}
+
+	volumeID := vol.Name
+	pv := &corev1.PersistentVolume{}
+	if err := m.k8sClient.Get(ctx, k8sCl.ObjectKey{Name: volumeID}, pv); err != nil {
+		m.log.Errorf("Failed to get Persistent Volume %s: %v", volumeID, err)
+		return false
+	}
+	volSC := pv.Spec.StorageClassName
+
+	isSCAcceptable := false
+	for _, sc := range m.wbtConfig.VolumeOptions.StorageClasses {
+		if sc == volSC {
+			isSCAcceptable = true
+			break
+		}
+	}
+	if !isSCAcceptable {
+		m.log.Infof("Skip wbt value changing: volume %s has sc %s, acceptable: %v", vol.Name, volSC, m.wbtConfig.VolumeOptions.StorageClasses)
+		return false
+	}
+
+	return true
+}
+
+func (m *VolumeManager) findDeviceName(vol *volumecrd.Volume) (string, error) {
+	drive, err := m.crHelper.GetDriveCRByVolume(vol)
+	if err != nil {
+		return "", err
+	}
+	if drive == nil {
+		return "", fmt.Errorf("drive %s is not found", vol.Spec.Location)
+	}
+
+	// expected device path - /dev/<device>
+	splitedPath := regexp.MustCompile(`[A-Za-z0-9]+`).FindAllString(drive.Spec.Path, -1)
+	if len(splitedPath) != 2 {
+		return "", fmt.Errorf("drive path %s is not parsable as /dev/<device>", drive.Spec.Path)
+	}
+	if splitedPath[0] != "dev" {
+		return "", fmt.Errorf("drive path %s is not parsable as /dev/<device>", drive.Spec.Path)
+	}
+
+	return splitedPath[1], nil
+}
+
+// SetWbtConfig changes Wbt Config for vlmgr instance
+func (m *VolumeManager) SetWbtConfig(conf *wbtconf.WbtConfig) {
+	if m.wbtConfig == nil || !reflect.DeepEqual(*m.wbtConfig, *conf) {
+		m.log.Infof("Wbt config changed: %+v", *conf)
+		m.wbtConfig = conf
+	}
+}
diff --git a/pkg/node/wbt/common/config.go b/pkg/node/wbt/common/config.go
new file mode 100644
index 0000000..eea82e2
--- /dev/null
+++ b/pkg/node/wbt/common/config.go
@@ -0,0 +1,23 @@
+package common
+
+// WbtConfig is a part of WBT ConfigMap
+// contains changing value and options to select acceptable volume
+type WbtConfig struct {
+	Enable        bool          `yaml:"enable"`
+	Value         uint32        `yaml:"wbt_lat_usec_value"`
+	VolumeOptions VolumeOptions `yaml:"acceptable_volume_options"`
+}
+
+// VolumeOptions contains options to select acceptable volume
+type VolumeOptions struct {
+	Modes          []string `yaml:"modes"`
+	StorageClasses []string `yaml:"storage_classes"`
+}
+
+// AcceptableKernelsConfig is a part of WBT ConfigMap
+// contains the list of kernel versions from nodes,
+// which should be able to set custom WBT value
+type AcceptableKernelsConfig struct {
+	EnableForAll   bool     `yaml:"enable_for_all"`
+	KernelVersions []string `yaml:"node_kernel_versions"`
+}
diff --git a/pkg/node/wbt/operations/wbt_ops.go b/pkg/node/wbt/operations/wbt_ops.go
new file mode 100644
index 0000000..0b3ab37
--- /dev/null
+++ b/pkg/node/wbt/operations/wbt_ops.go
@@ -0,0 +1,94 @@
+/*
+Copyright © 2020 Dell Inc. or its subsidiaries. All Rights Reserved.
+
+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 operations
+
+import (
+	"fmt"
+	"os/exec"
+	"strconv"
+	"strings"
+
+	"github.com/dell/csi-baremetal/pkg/base/command"
+)
+
+// TODO - remove cat and echo usage (https://github.com/dell/csi-baremetal/issues/653)
+const (
+	wbtValuePath = "/sys/block/%s/queue/wbt_lat_usec"
+	// isWBTEnabledCmdImpl is a CMD to check current WBT value
+	checkWbtValueCmd = "cat " + wbtValuePath
+	// enableWBTCmdImpl is a CMD to check current WBT value
+	restoreWbtValueCmd = "echo -1 > " + wbtValuePath
+	// disableWBTCmdImpl is a CMD to check current WBT value
+	setWbtValueCmd = "echo %d > " + wbtValuePath
+	// exec cmd via sh
+	defaultShellCmd = "sh"
+	shellCmdOption  = "-c"
+
+	invalidArgError = "Invalid argument"
+)
+
+// WrapWbt is an interface that encapsulates operation with WBT
+type WrapWbt interface {
+	SetValue(device string, value uint32) error
+	RestoreDefault(device string) error
+}
+
+// Wbt is WrapWbt implementation with CMD executor
+type Wbt struct {
+	e command.CmdExecutor
+}
+
+// NewWbt returns Wbt instance
+func NewWbt(e command.CmdExecutor) *Wbt {
+	return &Wbt{e: e}
+}
+
+// SetValue checks Wbt value for given device and change it if not equal
+// Example output: sh -c echo <value> /sys/block/<device>/queue/wbt_lat_usec
+func (w *Wbt) SetValue(device string, value uint32) error {
+	strOut, stdErr, err := w.e.RunCmd(fmt.Sprintf(checkWbtValueCmd, device))
+	if err != nil {
+		// Invalid argument is acceptable error, means that value for WBT is not set yet
+		if !strings.Contains(stdErr, invalidArgError) {
+			return err
+		}
+	}
+
+	if strOut == strconv.Itoa(int(value)) {
+		return nil
+	}
+
+	cmd := exec.Command(defaultShellCmd, shellCmdOption, fmt.Sprintf(setWbtValueCmd, value, device))
+	_, _, err = w.e.RunCmd(cmd)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// RestoreDefault restores default Wbt value for given device
+// Example output: sh -c echo -1 /sys/block/<device>/queue/wbt_lat_usec
+func (w *Wbt) RestoreDefault(device string) error {
+	cmd := exec.Command(defaultShellCmd, shellCmdOption, fmt.Sprintf(restoreWbtValueCmd, device))
+	_, _, err := w.e.RunCmd(cmd)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
diff --git a/pkg/node/wbt/wbt_watcher.go b/pkg/node/wbt/wbt_watcher.go
new file mode 100644
index 0000000..c08c121
--- /dev/null
+++ b/pkg/node/wbt/wbt_watcher.go
@@ -0,0 +1,117 @@
+package wbt
+
+import (
+	"context"
+	"io/ioutil"
+	"os"
+	"time"
+
+	"github.com/sirupsen/logrus"
+	"gopkg.in/yaml.v2"
+	corev1 "k8s.io/api/core/v1"
+	k8sClient "sigs.k8s.io/controller-runtime/pkg/client"
+
+	"github.com/dell/csi-baremetal/pkg/eventing"
+	"github.com/dell/csi-baremetal/pkg/events"
+	"github.com/dell/csi-baremetal/pkg/node"
+	"github.com/dell/csi-baremetal/pkg/node/wbt/common"
+)
+
+const (
+	confPath    = "/etc/node_config/wbt-config.yaml"
+	kernelsPath = "/etc/node_config/wbt-acceptable_kernels.yaml"
+
+	watchTimeout = 60 * time.Second
+
+	podNameEnv      = "POD_NAME"
+	podNamespaceEnv = "NAMESPACE"
+)
+
+// ConfWatcher is watcher to update WBT changing configuration in VolumeManager
+type ConfWatcher struct {
+	client            k8sClient.Client
+	eventsRecorder    *events.Recorder
+	log               *logrus.Entry
+	nodeKernelVersion string
+}
+
+// NewConfWatcher create new WBT Config Watcher with node kernel version
+func NewConfWatcher(client k8sClient.Client, eventsRecorder *events.Recorder, log *logrus.Entry, nodeKernelVersion string) *ConfWatcher {
+	return &ConfWatcher{
+		client:            client,
+		eventsRecorder:    eventsRecorder,
+		log:               log,
+		nodeKernelVersion: nodeKernelVersion,
+	}
+}
+
+// StartWatch tries to read Config for WBT changing from ConfigMap
+// Set conf in VolumeManager if success
+func (w *ConfWatcher) StartWatch(cns *node.CSINodeService) {
+	go func() {
+		for {
+			wbtConf, err := w.readConfig()
+			if err != nil {
+				w.log.Errorf("unable to read WBT config: %+v", err)
+				w.sendErrorConfigmapEvent()
+				cns.SetWbtConfig(&common.WbtConfig{Enable: false})
+			} else {
+				cns.SetWbtConfig(wbtConf)
+			}
+			time.Sleep(watchTimeout)
+		}
+	}()
+}
+
+func (w *ConfWatcher) readConfig() (*common.WbtConfig, error) {
+	kernels := &common.AcceptableKernelsConfig{}
+	kernelsFile, err := ioutil.ReadFile(kernelsPath)
+	if err != nil {
+		return nil, err
+	}
+	err = yaml.Unmarshal(kernelsFile, kernels)
+	if err != nil {
+		return nil, err
+	}
+
+	conf := &common.WbtConfig{}
+	confFile, err := ioutil.ReadFile(confPath)
+	if err != nil {
+		return nil, err
+	}
+	err = yaml.Unmarshal(confFile, conf)
+	if err != nil {
+		return nil, err
+	}
+
+	if !kernels.EnableForAll {
+		isKernelInList := false
+		for _, kernelVersion := range kernels.KernelVersions {
+			if kernelVersion == w.nodeKernelVersion {
+				isKernelInList = true
+				break
+			}
+		}
+		if !isKernelInList {
+			conf.Enable = false
+		}
+	}
+
+	return conf, nil
+}
+
+func (w *ConfWatcher) sendErrorConfigmapEvent() {
+	podName := os.Getenv(podNameEnv)
+	podNamespace := os.Getenv(podNamespaceEnv)
+
+	ctx := context.Background()
+
+	pod := &corev1.Pod{}
+	if err := w.client.Get(ctx, k8sClient.ObjectKey{Name: podName, Namespace: podNamespace}, pod); err != nil {
+		w.log.Errorf("Failed to get Pod %s in Namespace %s: %+v", podName, podNamespace, err)
+		return
+	}
+
+	w.eventsRecorder.Eventf(pod, eventing.WBTConfigMapUpdateFailed,
+		"Failed to get info from Node ConfigMap")
+}


Test patch:
diff --git a/pkg/node/common_test_data.go b/pkg/node/common_test_data.go
index 6bbd435..6608fa3 100644
--- a/pkg/node/common_test_data.go
+++ b/pkg/node/common_test_data.go
@@ -54,8 +54,8 @@ var (
 
 	testLogger  = getTestLogger()
 	testCtx     = context.Background()
-	disk1       = api.Drive{UUID: uuid.New().String(), SerialNumber: "hdd1", Size: 1024 * 1024 * 1024 * 500, NodeId: nodeID}
-	disk2       = api.Drive{UUID: uuid.New().String(), SerialNumber: "hdd2", Size: 1024 * 1024 * 1024 * 200, NodeId: nodeID}
+	disk1       = api.Drive{UUID: uuid.New().String(), SerialNumber: "hdd1", Size: 1024 * 1024 * 1024 * 500, NodeId: nodeID, Path: "/dev/sda"}
+	disk2       = api.Drive{UUID: uuid.New().String(), SerialNumber: "hdd2", Size: 1024 * 1024 * 1024 * 200, NodeId: nodeID, Path: "/dev/sda"}
 	testAC1Name = fmt.Sprintf("%s-%s", nodeID, strings.ToLower(disk1.UUID))
 	testAC1     = accrd.AvailableCapacity{
 		TypeMeta:   k8smetav1.TypeMeta{Kind: "AvailableCapacity", APIVersion: apiV1.APIV1Version},
diff --git a/pkg/node/node_test.go b/pkg/node/node_test.go
index 086f851..f21ee8e 100644
--- a/pkg/node/node_test.go
+++ b/pkg/node/node_test.go
@@ -42,8 +42,10 @@ import (
 	"github.com/dell/csi-baremetal/pkg/base/util"
 	csibmnodeconst "github.com/dell/csi-baremetal/pkg/crcontrollers/node/common"
 	"github.com/dell/csi-baremetal/pkg/mocks"
+	mocklu "github.com/dell/csi-baremetal/pkg/mocks/linuxutils"
 	mockProv "github.com/dell/csi-baremetal/pkg/mocks/provisioners"
 	p "github.com/dell/csi-baremetal/pkg/node/provisioners"
+	wbtcommon "github.com/dell/csi-baremetal/pkg/node/wbt/common"
 	"github.com/dell/csi-baremetal/pkg/testutils"
 )
 
@@ -52,6 +54,7 @@ var (
 	prov   *mockProv.MockProvisioner
 	fsOps  *mockProv.MockFsOpts
 	volOps *mocks.VolumeOperationsMock
+	wbtOps *mocklu.MockWrapWbt
 )
 
 func setVariables() {
@@ -59,12 +62,14 @@ func setVariables() {
 	prov = &mockProv.MockProvisioner{}
 	fsOps = &mockProv.MockFsOpts{}
 	volOps = &mocks.VolumeOperationsMock{}
+	wbtOps = &mocklu.MockWrapWbt{}
 	node.provisioners = map[p.VolumeType]p.Provisioner{
 		p.DriveBasedVolumeType: prov,
 		p.LVMBasedVolumeType:   prov,
 	}
 	node.fsOps = fsOps
 	node.svc = volOps
+	node.wbtOps = wbtOps
 }
 
 func TestCSINodeService(t *testing.T) {
@@ -822,6 +827,161 @@ var _ = Describe("CSINodeService Fake-Attach", func() {
 	})
 })
 
+var _ = Describe("CSINodeService Wbt Configuration", func() {
+	BeforeEach(func() {
+		setVariables()
+	})
+	Context("NodeStage() ", func() {
+		It("success", func() {
+			// testVolume2 has Create status
+			req := getNodeStageRequest(testVolume2.Id, *testVolumeCap)
+			partitionPath := "/partition/path/for/volume1"
+			prov.On("GetVolumePath", testVolume2).Return(partitionPath, nil)
+			fsOps.On("PrepareAndPerformMount",
+				partitionPath, path.Join(req.GetStagingTargetPath(), stagingFileName), true, false).
+				Return(nil)
+
+			var (
+				volumeMode        = ""
+				volumeSC          = "csi-baremetal-sc-hdd"
+				wbtValue   uint32 = 0
+				wbtConf           = &wbtcommon.WbtConfig{
+					Enable: true,
+					Value:  wbtValue,
+					VolumeOptions: wbtcommon.VolumeOptions{
+						Modes:          []string{volumeMode},
+						StorageClasses: []string{volumeSC},
+					},
+				}
+				device = "sda" //testDrive.Spec.Path = "/dev/sda"
+			)
+			node.SetWbtConfig(wbtConf)
+			wbtOps.On("SetValue", device, wbtValue).Return(nil)
+
+			pv := &corev1.PersistentVolume{}
+			pv.Name = testVolume2.Id
+			pv.Spec.StorageClassName = volumeSC
+			err := node.k8sClient.Create(testCtx, pv)
+			Expect(err).To(BeNil())
+
+			resp, err := node.NodeStageVolume(testCtx, req)
+			Expect(resp).NotTo(BeNil())
+			Expect(err).To(BeNil())
+			// check volume CR status
+			volumeCR := &vcrd.Volume{}
+			err = node.k8sClient.ReadCR(testCtx, testVolume2.Id, "", volumeCR)
+			Expect(err).To(BeNil())
+			Expect(volumeCR.Spec.CSIStatus).To(Equal(apiV1.VolumeReady))
+			Expect(volumeCR.Annotations[wbtChangedVolumeAnnotation]).To(Equal(wbtChangedVolumeKey))
+		})
+		It("failed", func() {
+			// testVolume2 has Create status
+			req := getNodeStageRequest(testVolume2.Id, *testVolumeCap)
+			partitionPath := "/partition/path/for/volume1"
+			prov.On("GetVolumePath", testVolume2).Return(partitionPath, nil)
+			fsOps.On("PrepareAndPerformMount",
+				partitionPath, path.Join(req.GetStagingTargetPath(), stagingFileName), true, false).
+				Return(nil)
+
+			var (
+				volumeMode = ""
+				volumeSC   = "csi-baremetal-sc-hdd"
+				wbtConf    = &wbtcommon.WbtConfig{
+					Enable: true,
+					VolumeOptions: wbtcommon.VolumeOptions{
+						Modes:          []string{volumeMode},
+						StorageClasses: []string{volumeSC},
+					},
+				}
+				wbtValue uint32 = 0
+				device          = "sda" //testDrive.Spec.Path = "/dev/sda"
+				someErr         = fmt.Errorf("some err")
+			)
+			node.SetWbtConfig(wbtConf)
+			wbtOps.On("SetValue", device, wbtValue).Return(someErr)
+
+			pv := &corev1.PersistentVolume{}
+			pv.Name = testVolume2.Id
+			pv.Spec.StorageClassName = volumeSC
+			err := node.k8sClient.Create(testCtx, pv)
+			Expect(err).To(BeNil())
+
+			resp, err := node.NodeStageVolume(testCtx, req)
+			Expect(resp).NotTo(BeNil())
+			Expect(err).To(BeNil())
+			// check volume CR status
+			volumeCR := &vcrd.Volume{}
+			err = node.k8sClient.ReadCR(testCtx, testVolume2.Id, "", volumeCR)
+			Expect(err).To(BeNil())
+			Expect(volumeCR.Spec.CSIStatus).To(Equal(apiV1.VolumeReady))
+			_, ok := volumeCR.Annotations[wbtChangedVolumeAnnotation]
+			Expect(ok).To(BeFalse())
+		})
+	})
+
+	Context("NodeUnStage()", func() {
+		It("success", func() {
+			req := getNodeUnstageRequest(testV1ID, stagePath)
+			targetPath := path.Join(req.GetStagingTargetPath(), stagingFileName)
+			fsOps.On("UnmountWithCheck", targetPath).Return(nil)
+			fsOps.On("RmDir", targetPath).Return(nil)
+
+			vol1 := &vcrd.Volume{}
+			err := node.k8sClient.ReadCR(testCtx, testVolume1.Id, testNs, vol1)
+			Expect(err).To(BeNil())
+			vol1.Annotations = map[string]string{wbtChangedVolumeAnnotation: wbtChangedVolumeKey}
+			err = node.k8sClient.UpdateCR(testCtx, vol1)
+			Expect(err).To(BeNil())
+
+			var (
+				device = "sda" //testDrive.Spec.Path = "/dev/sda"
+			)
+			wbtOps.On("RestoreDefault", device).Return(nil)
+
+			resp, err := node.NodeUnstageVolume(testCtx, req)
+			Expect(resp).NotTo(BeNil())
+			Expect(err).To(BeNil())
+			// check owners and CSI status
+			volumeCR := &vcrd.Volume{}
+			err = node.k8sClient.ReadCR(testCtx, testV1ID, "", volumeCR)
+			Expect(err).To(BeNil())
+			Expect(volumeCR.Spec.CSIStatus).To(Equal(apiV1.Created))
+			_, ok := volumeCR.Annotations[wbtChangedVolumeAnnotation]
+			Expect(ok).To(Equal(false))
+		})
+		It("failed", func() {
+			req := getNodeUnstageRequest(testV1ID, stagePath)
+			targetPath := path.Join(req.GetStagingTargetPath(), stagingFileName)
+			fsOps.On("UnmountWithCheck", targetPath).Return(nil)
+			fsOps.On("RmDir", targetPath).Return(nil)
+
+			vol1 := &vcrd.Volume{}
+			err := node.k8sClient.ReadCR(testCtx, testVolume1.Id, testNs, vol1)
+			Expect(err).To(BeNil())
+			vol1.Annotations = map[string]string{wbtChangedVolumeAnnotation: wbtChangedVolumeKey}
+			err = node.k8sClient.UpdateCR(testCtx, vol1)
+			Expect(err).To(BeNil())
+
+			var (
+				device  = "sda" //testDrive.Spec.Path = "/dev/sda"
+				someErr = fmt.Errorf("some err")
+			)
+			wbtOps.On("RestoreDefault", device).Return(someErr)
+
+			resp, err := node.NodeUnstageVolume(testCtx, req)
+			Expect(resp).NotTo(BeNil())
+			Expect(err).To(BeNil())
+			// check owners and CSI status
+			volumeCR := &vcrd.Volume{}
+			err = node.k8sClient.ReadCR(testCtx, testV1ID, "", volumeCR)
+			Expect(err).To(BeNil())
+			Expect(volumeCR.Spec.CSIStatus).To(Equal(apiV1.Created))
+			_, ok := volumeCR.Annotations[wbtChangedVolumeAnnotation]
+			Expect(ok).To(Equal(false))
+		})
+	})
+})
+
 func getNodePublishRequest(volumeID, targetPath string, volumeCap csi.VolumeCapability) *csi.NodePublishVolumeRequest {
 	return &csi.NodePublishVolumeRequest{
 		VolumeId:          volumeID,
diff --git a/pkg/node/wbt/operations/wbt_ops_test.go b/pkg/node/wbt/operations/wbt_ops_test.go
new file mode 100644
index 0000000..910fcac
--- /dev/null
+++ b/pkg/node/wbt/operations/wbt_ops_test.go
@@ -0,0 +1,135 @@
+package operations
+
+import (
+	"fmt"
+	"os/exec"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+
+	"github.com/dell/csi-baremetal/pkg/mocks"
+)
+
+func TestWbt_SetValue(t *testing.T) {
+
+	t.Run("success", func(t *testing.T) {
+		var (
+			output          = "75000"
+			emptyErr        = ""
+			device          = "sdb"
+			value    uint32 = 0
+			cmdCheck        = fmt.Sprintf(checkWbtValueCmd, device)
+			cmdSet          = exec.Command(defaultShellCmd, shellCmdOption, fmt.Sprintf(setWbtValueCmd, value, device))
+		)
+
+		e := &mocks.GoMockExecutor{}
+		e.On("RunCmd", cmdCheck).Return(output, emptyErr, nil)
+		e.On("RunCmd", cmdSet).Return("", emptyErr, nil)
+
+		wbt := NewWbt(e)
+		err := wbt.SetValue(device, value)
+		assert.Nil(t, err)
+	})
+	t.Run("passed value equals to current", func(t *testing.T) {
+		var (
+			output          = "75000"
+			emptyErr        = ""
+			device          = "sdb"
+			value    uint32 = 75000
+			cmdCheck        = fmt.Sprintf(checkWbtValueCmd, device)
+		)
+
+		e := &mocks.GoMockExecutor{}
+		e.On("RunCmd", cmdCheck).Return(output, emptyErr, nil)
+
+		wbt := NewWbt(e)
+		err := wbt.SetValue(device, value)
+		assert.Nil(t, err)
+	})
+	t.Run("check fail", func(t *testing.T) {
+		var (
+			output          = "75000"
+			errStr          = "some error"
+			device          = "sdb"
+			value    uint32 = 0
+			cmdCheck        = fmt.Sprintf(checkWbtValueCmd, device)
+		)
+
+		e := &mocks.GoMockExecutor{}
+		e.On("RunCmd", cmdCheck).Return(output, errStr, fmt.Errorf("err: %s", errStr))
+
+		wbt := NewWbt(e)
+		err := wbt.SetValue(device, value)
+		assert.NotNil(t, err)
+	})
+	t.Run("check fail with invalid arg", func(t *testing.T) {
+		var (
+			output          = "75000"
+			errStr          = invalidArgError
+			emptyErr        = ""
+			device          = "sdb"
+			value    uint32 = 0
+			cmdCheck        = fmt.Sprintf(checkWbtValueCmd, device)
+			cmdSet          = exec.Command(defaultShellCmd, shellCmdOption, fmt.Sprintf(setWbtValueCmd, value, device))
+		)
+
+		e := &mocks.GoMockExecutor{}
+		e.On("RunCmd", cmdCheck).Return(output, errStr, fmt.Errorf("err: %s", errStr))
+		e.On("RunCmd", cmdSet).Return("", emptyErr, nil)
+
+		wbt := NewWbt(e)
+		err := wbt.SetValue(device, value)
+		assert.Nil(t, err)
+	})
+	t.Run("set fail", func(t *testing.T) {
+		var (
+			output          = "75000"
+			errStr          = "some error"
+			emptyErr        = ""
+			device          = "sdb"
+			value    uint32 = 0
+			cmdCheck        = fmt.Sprintf(checkWbtValueCmd, device)
+			cmdSet          = exec.Command(defaultShellCmd, shellCmdOption, fmt.Sprintf(setWbtValueCmd, value, device))
+		)
+
+		e := &mocks.GoMockExecutor{}
+		e.On("RunCmd", cmdCheck).Return(output, emptyErr, nil)
+		e.On("RunCmd", cmdSet).Return("", errStr, fmt.Errorf("err: %s", errStr))
+
+		wbt := NewWbt(e)
+		err := wbt.SetValue(device, value)
+		assert.NotNil(t, err)
+	})
+}
+
+func TestWbt_RestoreDefault(t *testing.T) {
+
+	t.Run("success", func(t *testing.T) {
+		var (
+			emptyErr = ""
+			device   = "sdb"
+			cmdSet   = exec.Command(defaultShellCmd, shellCmdOption, fmt.Sprintf(restoreWbtValueCmd, device))
+		)
+
+		e := &mocks.GoMockExecutor{}
+		e.On("RunCmd", cmdSet).Return("", emptyErr, nil)
+
+		wbt := NewWbt(e)
+		err := wbt.RestoreDefault(device)
+		assert.Nil(t, err)
+	})
+	t.Run("fail", func(t *testing.T) {
+		var (
+			errStr = "some error"
+			device = "sdb"
+			cmdSet = exec.Command(defaultShellCmd, shellCmdOption, fmt.Sprintf(restoreWbtValueCmd, device))
+		)
+
+		e := &mocks.GoMockExecutor{}
+		e.On("RunCmd", cmdSet).Return("", errStr, fmt.Errorf("err: %s", errStr))
+
+		wbt := NewWbt(e)
+		err := wbt.RestoreDefault(device)
+		assert.NotNil(t, err)
+	})
+}