From 158866f436cd190943e913e2f9eb426b83b7269d Mon Sep 17 00:00:00 2001 From: Alexander Ding Date: Wed, 12 Oct 2022 21:54:39 +0000 Subject: [PATCH] feat: migrate system API group to library model --- integrationtests/system_test.go | 83 +++++++++++++++++++++++++++++++ pkg/system/api/api.go | 88 +++++++++++++++++++++++++++++++++ pkg/system/api/types.go | 12 +++++ pkg/system/system.go | 80 ++++++++++++++++++++++++++++++ pkg/system/types.go | 70 ++++++++++++++++++++++++++ 5 files changed, 333 insertions(+) create mode 100644 pkg/system/api/api.go create mode 100644 pkg/system/api/types.go create mode 100644 pkg/system/system.go create mode 100644 pkg/system/types.go diff --git a/integrationtests/system_test.go b/integrationtests/system_test.go index f746835a..382e780a 100644 --- a/integrationtests/system_test.go +++ b/integrationtests/system_test.go @@ -8,8 +8,12 @@ import ( "strings" "testing" + system "github.com/kubernetes-csi/csi-proxy/pkg/system" + systemapi "github.com/kubernetes-csi/csi-proxy/pkg/system/api" + "github.com/kubernetes-csi/csi-proxy/client/api/system/v1alpha1" v1alpha1client "github.com/kubernetes-csi/csi-proxy/client/groups/system/v1alpha1" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -99,6 +103,85 @@ func TestServiceCommands(t *testing.T) { } +func TestSystem(t *testing.T) { + t.Run("GetBIOSSerialNumber", func(t *testing.T) { + client, err := system.New(systemapi.New()) + require.Nil(t, err) + + request := &system.GetBIOSSerialNumberRequest{} + response, err := client.GetBIOSSerialNumber(context.TODO(), request) + require.Nil(t, err) + require.NotNil(t, response) + + result, err := exec.Command("wmic", "bios", "get", "serialnumber").Output() + require.Nil(t, err) + + t.Logf("The serial number is %s", response.SerialNumber) + + resultString := string(result) + require.True(t, strings.Contains(resultString, response.SerialNumber)) + }) + + t.Run("GetService", func(t *testing.T) { + const ServiceName = "MSiSCSI" + client, err := system.New(systemapi.New()) + require.Nil(t, err) + + // Make sure service is stopped + _, err = runPowershellCmd(t, fmt.Sprintf(`Stop-Service -Name "%s"`, ServiceName)) + require.NoError(t, err) + assertServiceStopped(t, ServiceName) + + request := &system.GetServiceRequest{Name: ServiceName} + response, err := client.GetService(context.TODO(), request) + require.NoError(t, err) + require.NotNil(t, response) + + out, err := runPowershellCmd(t, fmt.Sprintf(`Get-Service -Name "%s" `+ + `| Select-Object DisplayName, Status, StartType | ConvertTo-Json`, + ServiceName)) + require.NoError(t, err) + + var serviceInfo = struct { + DisplayName string `json:"DisplayName"` + Status uint32 `json:"Status"` + StartType uint32 `json:"StartType"` + }{} + + err = json.Unmarshal([]byte(out), &serviceInfo) + require.NoError(t, err, "failed unmarshalling json out=%v", out) + + assert.Equal(t, serviceInfo.Status, uint32(response.Status)) + assert.Equal(t, system.SERVICE_STATUS_STOPPED, response.Status) + assert.Equal(t, serviceInfo.StartType, uint32(response.StartType)) + assert.Equal(t, serviceInfo.DisplayName, response.DisplayName) + }) + + t.Run("Stop/Start Service", func(t *testing.T) { + const ServiceName = "MSiSCSI" + client, err := system.New(systemapi.New()) + require.Nil(t, err) + + _, err = runPowershellCmd(t, fmt.Sprintf(`Stop-Service -Name "%s"`, ServiceName)) + require.NoError(t, err) + assertServiceStopped(t, ServiceName) + + startReq := &system.StartServiceRequest{Name: ServiceName} + startResp, err := client.StartService(context.TODO(), startReq) + + assert.NoError(t, err) + assert.NotNil(t, startResp) + assertServiceStarted(t, ServiceName) + + stopReq := &system.StopServiceRequest{Name: ServiceName} + stopResp, err := client.StopService(context.TODO(), stopReq) + + assert.NoError(t, err) + assert.NotNil(t, stopResp) + assertServiceStopped(t, ServiceName) + }) +} + func assertServiceStarted(t *testing.T, serviceName string) { assertServiceStatus(t, serviceName, "Running") } diff --git a/pkg/system/api/api.go b/pkg/system/api/api.go new file mode 100644 index 00000000..d239234e --- /dev/null +++ b/pkg/system/api/api.go @@ -0,0 +1,88 @@ +package api + +import ( + "encoding/json" + "fmt" + "os/exec" + "strings" + + "github.com/kubernetes-csi/csi-proxy/pkg/utils" +) + +// Implements the System OS API calls. All code here should be very simple +// pass-through to the OS APIs. Any logic around the APIs should go in +// pkg/system/system.go so that logic can be easily unit-tested +// without requiring specific OS environments. + +type API interface { + GetBIOSSerialNumber() (string, error) + GetService(name string) (*ServiceInfo, error) + StartService(name string) error + StopService(name string, force bool) error +} + +type systemAPI struct{} + +func New() API { + return systemAPI{} +} + +func (systemAPI) GetBIOSSerialNumber() (string, error) { + // Taken from Kubernetes vSphere cloud provider + // https://github.com/kubernetes/kubernetes/blob/103e926604de6f79161b78af3e792d0ed282bc06/staging/src/k8s.io/legacy-cloud-providers/vsphere/vsphere_util_windows.go#L28 + result, err := exec.Command("wmic", "bios", "get", "serialnumber").Output() + if err != nil { + return "", err + } + lines := strings.FieldsFunc(string(result), func(r rune) bool { + switch r { + case '\n', '\r': + return true + default: + return false + } + }) + if len(lines) != 2 { + return "", fmt.Errorf("received unexpected value retrieving host uuid: %q", string(result)) + } + return lines[1], nil +} + +func (systemAPI) GetService(name string) (*ServiceInfo, error) { + script := `Get-Service -Name $env:ServiceName | Select-Object DisplayName, Status, StartType | ` + + `ConvertTo-JSON` + cmdEnv := fmt.Sprintf("ServiceName=%s", name) + out, err := utils.RunPowershellCmd(script, cmdEnv) + if err != nil { + return nil, fmt.Errorf("error querying service name=%s. cmd: %s, output: %s, error: %v", name, script, string(out), err) + } + + var serviceInfo ServiceInfo + err = json.Unmarshal(out, &serviceInfo) + if err != nil { + return nil, err + } + + return &serviceInfo, nil +} + +func (systemAPI) StartService(name string) error { + script := `Start-Service -Name $env:ServiceName` + cmdEnv := fmt.Sprintf("ServiceName=%s", name) + out, err := utils.RunPowershellCmd(script, cmdEnv) + if err != nil { + return fmt.Errorf("error starting service name=%s. cmd: %s, output: %s, error: %v", name, script, string(out), err) + } + + return nil +} + +func (systemAPI) StopService(name string, force bool) error { + script := `Stop-Service -Name $env:ServiceName -Force:$([System.Convert]::ToBoolean($env:Force))` + out, err := utils.RunPowershellCmd(script, fmt.Sprintf("ServiceName=%s", name), fmt.Sprintf("Force=%t", force)) + if err != nil { + return fmt.Errorf("error stopping service name=%s. cmd: %s, output: %s, error: %v", name, script, string(out), err) + } + + return nil +} diff --git a/pkg/system/api/types.go b/pkg/system/api/types.go new file mode 100644 index 00000000..246c5187 --- /dev/null +++ b/pkg/system/api/types.go @@ -0,0 +1,12 @@ +package api + +type ServiceInfo struct { + // Service display name + DisplayName string `json:"DisplayName"` + + // Service start type + StartType uint32 `json:"StartType"` + + // Service status + Status uint32 `json:"Status"` +} diff --git a/pkg/system/system.go b/pkg/system/system.go new file mode 100644 index 00000000..8e08e122 --- /dev/null +++ b/pkg/system/system.go @@ -0,0 +1,80 @@ +package system + +import ( + "context" + + systemapi "github.com/kubernetes-csi/csi-proxy/pkg/system/api" + "k8s.io/klog/v2" +) + +type System struct { + hostAPI systemapi.API +} + +type Interface interface { + GetBIOSSerialNumber(context.Context, *GetBIOSSerialNumberRequest) (*GetBIOSSerialNumberResponse, error) + GetService(context.Context, *GetServiceRequest) (*GetServiceResponse, error) + StartService(context.Context, *StartServiceRequest) (*StartServiceResponse, error) + StopService(context.Context, *StopServiceRequest) (*StopServiceResponse, error) +} + +// check that System implements Interface +var _ Interface = &System{} + +func New(hostAPI systemapi.API) (*System, error) { + return &System{ + hostAPI: hostAPI, + }, nil +} + +func (s *System) GetBIOSSerialNumber(context context.Context, request *GetBIOSSerialNumberRequest) (*GetBIOSSerialNumberResponse, error) { + klog.V(4).Infof("calling GetBIOSSerialNumber") + response := &GetBIOSSerialNumberResponse{} + serialNumber, err := s.hostAPI.GetBIOSSerialNumber() + if err != nil { + klog.Errorf("failed GetBIOSSerialNumber: %v", err) + return response, err + } + + response.SerialNumber = serialNumber + return response, nil +} + +func (s *System) GetService(context context.Context, request *GetServiceRequest) (*GetServiceResponse, error) { + klog.V(4).Infof("calling GetService name=%s", request.Name) + response := &GetServiceResponse{} + info, err := s.hostAPI.GetService(request.Name) + if err != nil { + klog.Errorf("failed GetService: %v", err) + return response, err + } + + response.DisplayName = info.DisplayName + response.StartType = Startype(info.StartType) + response.Status = ServiceStatus(info.Status) + return response, nil +} + +func (s *System) StartService(context context.Context, request *StartServiceRequest) (*StartServiceResponse, error) { + klog.V(4).Infof("calling StartService name=%s", request.Name) + response := &StartServiceResponse{} + err := s.hostAPI.StartService(request.Name) + if err != nil { + klog.Errorf("failed StartService: %v", err) + return response, err + } + + return response, nil +} + +func (s *System) StopService(context context.Context, request *StopServiceRequest) (*StopServiceResponse, error) { + klog.V(4).Infof("calling StopService name=%s", request.Name) + response := &StopServiceResponse{} + err := s.hostAPI.StopService(request.Name, request.Force) + if err != nil { + klog.Errorf("failed StopService: %v", err) + return response, err + } + + return response, nil +} diff --git a/pkg/system/types.go b/pkg/system/types.go new file mode 100644 index 00000000..e33ad859 --- /dev/null +++ b/pkg/system/types.go @@ -0,0 +1,70 @@ +package system + +type GetBIOSSerialNumberRequest struct { +} + +type GetBIOSSerialNumberResponse struct { + SerialNumber string +} + +type StartServiceRequest struct { + // Service name (as listed in System\CCS\Services keys) + Name string +} + +type StartServiceResponse struct { + // Intentionally empty +} + +type StopServiceRequest struct { + // Service name (as listed in System\CCS\Services keys) + Name string + + // Forces stopping of services that has dependent services + Force bool +} + +type StopServiceResponse struct { + // Intentionally empty +} + +type ServiceStatus uint32 + +const ( + SERVICE_STATUS_UNKNOWN ServiceStatus = iota + SERVICE_STATUS_STOPPED + SERVICE_STATUS_START_PENDING + SERVICE_STATUS_STOP_PENDING + SERVICE_STATUS_RUNNING + SERVICE_STATUS_CONTINUE_PENDING + SERVICE_STATUS_PAUSE_PENDING + SERVICE_STATUS_PAUSED +) + +type Startype uint32 + +const ( + START_TYPE_BOOT Startype = iota + START_TYPE_SYSTEM + START_TYPE_AUTOMATIC + START_TYPE_MANUAL + START_TYPE_DISABLED +) + +type GetServiceRequest struct { + // Service name (as listed in System\CCS\Services keys) + Name string +} + +type GetServiceResponse struct { + // Service display name + DisplayName string + + // Service start type. + // Used to control whether a service will start on boot, and if so on which + // boot phase. + StartType Startype + + // Service status, e.g. stopped, running, paused + Status ServiceStatus +}