diff --git a/Makefile b/Makefile index 1145da237..f7199890a 100644 --- a/Makefile +++ b/Makefile @@ -35,7 +35,7 @@ copy: build/odimra/odimra dep: copy build/odimra/makedep.sh -build-containers: dep +build-containers: dep cd build && ./run_pre_reqs.sh && docker-compose build --force-rm --build-arg ODIMRA_USER_ID=${ODIMRA_USER_ID} --build-arg ODIMRA_GROUP_ID=${ODIMRA_GROUP_ID} standup-containers: build-containers diff --git a/build/DELLPlugin/edit_config.sh b/build/DELLPlugin/edit_config.sh index cb83be13a..a12e29164 100755 --- a/build/DELLPlugin/edit_config.sh +++ b/build/DELLPlugin/edit_config.sh @@ -32,6 +32,8 @@ sed -i "s#\"PrivateKeyPath\".*#\"PrivateKeyPath\": \"$t/odimra_server.key\",#" sed -i "s#\"CertificatePath\".*#\"CertificatePath\": \"$t/odimra_server.crt\"#" /etc/dell_plugin_config/config_dell_plugin.json sed -i "s#\"LBHost\".*#\"LBHost\": \"$ip\",#" /etc/dell_plugin_config/config_dell_plugin.json sed -i "s#\"LBPort\".*#\"LBPort\": \"45008\"#" /etc/dell_plugin_config/config_dell_plugin.json +sed -i "s#\"InMemoryHost\".*#\"InMemoryHost\": \"redis\",#" /etc/dell_plugin_config/config_dell_plugin.json +sed -i "s#\"OnDiskHost\".*#\"OnDiskHost\": \"redis\",#" /etc/dell_plugin_config/config_dell_plugin.json sed -i "s#\"MessageQueueConfigFilePath\".*#\"MessageQueueConfigFilePath\": \"/etc/dell_plugin_config/platformconfig.toml\",#" /etc/dell_plugin_config/config_dell_plugin.json ########changes in platformconfig.toml file ###### diff --git a/lib-utilities/config/odimra_config.json b/lib-utilities/config/odimra_config.json index afd8e7ce7..c4133d187 100644 --- a/lib-utilities/config/odimra_config.json +++ b/lib-utilities/config/odimra_config.json @@ -127,6 +127,10 @@ { "ConnectionMethodType": "Redfish", "ConnectionMethodVariant":"Compute:BasicAuth:URP_v1.0.0" + }, + { + "ConnectionMethodType": "Redfish", + "ConnectionMethodVariant":"Compute:BasicAuth:DELL_v1.0.0" } ] } diff --git a/odim-controller/helmcharts/dellplugin/dellplugin/templates/configmaps.yaml b/odim-controller/helmcharts/dellplugin/dellplugin/templates/configmaps.yaml index cef638ba7..b6311c884 100644 --- a/odim-controller/helmcharts/dellplugin/dellplugin/templates/configmaps.yaml +++ b/odim-controller/helmcharts/dellplugin/dellplugin/templates/configmaps.yaml @@ -24,6 +24,27 @@ data: "PrivateKeyPath": "/etc/odimra_certs/odimra_server.key", "CertificatePath": "/etc/odimra_certs/odimra_server.crt" }, + "DBConf": { + "Protocol": "tcp", + {{ if eq .Values.odimra.haDeploymentEnabled false }} + "InMemoryHost": "redis-inmemory", + "OnDiskHost": "redis-ondisk", + "InMemoryMasterSet": "mymaster", + "OnDiskMasterSet": "mymaster", + {{ else }} + "InMemoryHost": "redis-ha-inmemory", + "OnDiskHost": "redis-ha-ondisk", + "InMemoryMasterSet": "primaryset", + "OnDiskMasterSet": "primaryset", + {{ end }} + "InMemoryPort": "6379", + "OnDiskPort": "6379", + "MaxIdleConns": 200, + "MaxActiveConns": 200, + "RedisHAEnabled": {{ .Values.odimra.haDeploymentEnabled }}, + "InMemorySentinelPort": "26379", + "OnDiskSentinelPort": "26379" + }, "TLSConf" : { "MinVersion": "TLS_1.2", "MaxVersion": "TLS_1.2", diff --git a/plugin-dell/config/config.go b/plugin-dell/config/config.go index 9baaba860..b73962fcc 100644 --- a/plugin-dell/config/config.go +++ b/plugin-dell/config/config.go @@ -39,6 +39,7 @@ type configModel struct { KeyCertConf *KeyCertConf `json:"KeyCertConf"` URLTranslation *URLTranslation `json:"URLTranslation"` TLSConf *TLSConf `json:"TLSConf"` + DBConf *DBConf `json:"DBConf"` } //PluginConf is for holding all the plugin related configurations @@ -50,6 +51,22 @@ type PluginConf struct { Password string `json:"Password"` } +// DBConf holds all DB related configurations +type DBConf struct { + Protocol string `json:"Protocol"` + InMemoryHost string `json:"InMemoryHost"` + InMemoryPort string `json:"InMemoryPort"` + OnDiskHost string `json:"OnDiskHost"` + OnDiskPort string `json:"OnDiskPort"` + MaxIdleConns int `json:"MaxIdleConns"` + MaxActiveConns int `json:"MaxActiveConns"` + RedisHAEnabled bool `json:"RedisHAEnabled"` + InMemorySentinelPort string `json:"InMemorySentinelPort"` + OnDiskSentinelPort string `json:"OnDiskSentinelPort"` + InMemoryMasterSet string `json:"InMemoryMasterSet"` + OnDiskMasterSet string `json:"OnDiskMasterSet"` +} + //LoadBalancerConf is for holding all load balancer related configurations type LoadBalancerConf struct { Host string `json:"LBHost"` diff --git a/plugin-dell/config/dell_config.json b/plugin-dell/config/dell_config.json index bbe0cbd0f..626f78ccb 100644 --- a/plugin-dell/config/dell_config.json +++ b/plugin-dell/config/dell_config.json @@ -17,6 +17,20 @@ "PrivateKeyPath": "", "CertificatePath": "" }, + "DBConf": { + "Protocol": "tcp", + "InMemoryHost": "localhost", + "InMemoryPort": "6379", + "OnDiskHost": "localhost", + "OnDiskPort": "6380", + "MaxIdleConns": 10, + "MaxActiveConns": 120, + "RedisHAEnabled": false, + "InMemorySentinelPort": "26379", + "OnDiskSentinelPort": "26379", + "InMemoryMasterSet": "mymaster", + "OnDiskMasterSet": "mymaster" + }, "TLSConf" : { "MinVersion": "TLS_1.2", "MaxVersion": "TLS_1.2", @@ -26,7 +40,7 @@ "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", - "TLS_RSA_WITH_AES_128_GCM_SHA256", + "TLS_RSA_WITH_AES_128_GCM_SHA256", "TLS_RSA_WITH_AES_256_GCM_SHA384" ] }, diff --git a/plugin-dell/dphandler/biosSettings.go b/plugin-dell/dphandler/biosSettings.go index 9463e6b6b..1d4a472b9 100644 --- a/plugin-dell/dphandler/biosSettings.go +++ b/plugin-dell/dphandler/biosSettings.go @@ -165,7 +165,7 @@ func createBiosResponse() []byte { Code: response.Success, Message: "See @Message.ExtendedInfo for more information.", MessageExtendedInfo: []dpresponse.MsgExtendedInfo{ - dpresponse.MsgExtendedInfo{ + { MessageID: response.Success, Message: "A system reset is required for BIOS settings changes to get affected", MessageArgs: []string{}, diff --git a/plugin-dell/dphandler/bootorder.go b/plugin-dell/dphandler/bootorder.go index 658c3e74c..ada76ea09 100644 --- a/plugin-dell/dphandler/bootorder.go +++ b/plugin-dell/dphandler/bootorder.go @@ -62,41 +62,7 @@ func SetDefaultBootOrder(ctx iris.Context) { Username: deviceDetails.Username, Password: string(deviceDetails.Password), } - /* - priv := []byte(dpmodel.PluginPrivateKey) - block, _ := pem.Decode(priv) - enc := x509.IsEncryptedPEMBlock(block) - b := block.Bytes - if enc { - log.Println("is encrypted pem block") - b, err = x509.DecryptPEMBlock(block, nil) - if err != nil { - log.Println("Error: " + err.Error()) - } - } - key, err := x509.ParsePKCS1PrivateKey(b) - if err != nil { - log.Println("Error: " + err.Error()) - } - - hash := sha512.New() - - plainText, err := rsa.DecryptOAEP( - hash, - rand.Reader, - key, - device.Password, - nil, - ) - if err != nil { - log.Println("Error while trying decrypt data: ", err) - ctx.StatusCode(http.StatusInternalServerError) - ctx.WriteString("Error while trying to decypt data") - return - } - device.Password = plainText - */ redfishClient, err := dputilities.GetRedfishClient() if err != nil { errMsg := "While trying to create the redfish client, got:" + err.Error() diff --git a/plugin-dell/dphandler/pluginStatus.go b/plugin-dell/dphandler/pluginStatus.go index af755c882..4a5c789c3 100644 --- a/plugin-dell/dphandler/pluginStatus.go +++ b/plugin-dell/dphandler/pluginStatus.go @@ -24,7 +24,6 @@ import ( "reflect" "sync" "time" - //"github.com/ODIM-Project/ODIM/lib-utilities/common" pluginConfig "github.com/ODIM-Project/ODIM/plugin-dell/config" "github.com/ODIM-Project/ODIM/plugin-dell/dpmodel" "github.com/ODIM-Project/ODIM/plugin-dell/dpresponse" @@ -48,8 +47,8 @@ func GetPluginStatus(ctx iris.Context) { } var messageQueueInfo []dpresponse.EmbQueue var resp = dpresponse.PluginStatusResponse{ - Comment: "Plugin Status Response", - Name: "Common Redfish Plugin Status", + Comment: "Dell Plugin Status Response", + Name: "Dell Redfish Plugin Status", Version: pluginConfig.Data.FirmwareVersion, } resp.Status = dputilities.Status @@ -205,7 +204,6 @@ func checkCreateSub(server dpmodel.DeviceData, startUpResponse chan map[string]s Destination: "https://" + pluginConfig.Data.LoadBalancerConf.Host + ":" + pluginConfig.Data.LoadBalancerConf.Port + pluginConfig.Data.EventConf.DestURI, EventTypes: server.EventSubscriptionInfo.EventTypes, Context: "Event Subscription", - // HTTPHeaders: reqPostBody.HTTPHeaders, Protocol: "Redfish", } device.PostBody, err = json.Marshal(req) @@ -229,7 +227,6 @@ func checkCreateSub(server dpmodel.DeviceData, startUpResponse chan map[string]s Destination: "https://" + pluginConfig.Data.LoadBalancerConf.Host + ":" + pluginConfig.Data.LoadBalancerConf.Port + pluginConfig.Data.EventConf.DestURI, EventTypes: []string{"Alert"}, Context: "Event Subscription", - // HTTPHeaders: reqPostBody.HTTPHeaders, Protocol: "Redfish", } device.PostBody, err = json.Marshal(req) diff --git a/plugin-dell/dphandler/redfishEvents.go b/plugin-dell/dphandler/redfishEvents.go index 7b8288028..33b39048a 100644 --- a/plugin-dell/dphandler/redfishEvents.go +++ b/plugin-dell/dphandler/redfishEvents.go @@ -19,19 +19,13 @@ import ( "encoding/json" "github.com/ODIM-Project/ODIM/lib-utilities/common" pluginConfig "github.com/ODIM-Project/ODIM/plugin-dell/config" + "github.com/ODIM-Project/ODIM/plugin-dell/dputilities" log "github.com/sirupsen/logrus" "net" "net/http" "strings" ) -var ( - // In Channel - In chan<- interface{} - // Out Channel - Out <-chan interface{} -) - // RedfishEvents receives the subscribed events from the south bound system // Then it will send the received data and ip to publish method // RedfishEvents receives the subscribed events from the south bound system @@ -76,14 +70,6 @@ func RedfishEvents(w http.ResponseWriter, r *http.Request) { } // Call writeEventToJobQueue to write events to worker pool - writeEventToJobQueue(event) + dputilities.WriteEventToJobQueue(event) w.WriteHeader(http.StatusOK) } - -// writeEventToJobQueue will write events to worker pool -func writeEventToJobQueue(event common.Events) { - var events []interface{} - events = append(events, event) - done := make(chan bool) - go common.RunWriteWorkers(In, events, 5, done) -} diff --git a/plugin-dell/dphandler/reset.go b/plugin-dell/dphandler/reset.go index 80a73c007..7d6af34a0 100644 --- a/plugin-dell/dphandler/reset.go +++ b/plugin-dell/dphandler/reset.go @@ -63,40 +63,7 @@ func ResetComputerSystem(ctx iris.Context) { Username: deviceDetails.Username, Password: string(deviceDetails.Password), } - /* - priv := []byte(dpmodel.PluginPrivateKey) - block, _ := pem.Decode(priv) - enc := x509.IsEncryptedPEMBlock(block) - b := block.Bytes - if enc { - log.Println("is encrypted pem block") - b, err = x509.DecryptPEMBlock(block, nil) - if err != nil { - log.Println("Error: " + err.Error()) - } - } - key, err := x509.ParsePKCS1PrivateKey(b) - if err != nil { - log.Println("Error: " + err.Error()) - } - - hash := sha512.New() - plainText, err := rsa.DecryptOAEP( - hash, - rand.Reader, - key, - device.Password, - nil, - ) - if err != nil { - log.Println("Error while trying decrypt data: ", err) - ctx.StatusCode(http.StatusInternalServerError) - ctx.WriteString("Error while trying to decypt data") - return - } - device.Password = plainText - */ var request map[string]interface{} err = json.Unmarshal(deviceDetails.PostBody, &request) resetType := request["ResetType"].(string) diff --git a/plugin-dell/dphandler/storage.go b/plugin-dell/dphandler/storage.go index d8360f19b..c911ef44a 100644 --- a/plugin-dell/dphandler/storage.go +++ b/plugin-dell/dphandler/storage.go @@ -17,7 +17,8 @@ package dphandler import ( "encoding/json" - "io/ioutil" + "fmt" + taskproto "github.com/ODIM-Project/ODIM/lib-utilities/proto/task" "net/http" "strconv" "strings" @@ -27,23 +28,26 @@ import ( "github.com/ODIM-Project/ODIM/lib-utilities/response" pluginConfig "github.com/ODIM-Project/ODIM/plugin-dell/config" "github.com/ODIM-Project/ODIM/plugin-dell/dpmodel" - "github.com/ODIM-Project/ODIM/plugin-dell/dpresponse" "github.com/ODIM-Project/ODIM/plugin-dell/dputilities" iris "github.com/kataras/iris/v12" log "github.com/sirupsen/logrus" ) +var taskService dputilities.TaskService = dputilities.GetTaskService() +var hardwareService volumeOnHardwareService = GetVolumeOnHardwareService() + //CreateVolume function is used for creating a volume under storage func CreateVolume(ctx iris.Context) { - //Get token from Request + // Get token from Request token := ctx.GetHeader("X-Auth-Token") uri := ctx.Request().RequestURI - //replacing the request url with south bound translation URL + + // Replacing the request url with south bound translation URL for key, value := range pluginConfig.Data.URLTranslation.SouthBoundURL { uri = strings.Replace(uri, key, value, -1) } - //Validating the token + // Validating the token if token != "" { flag := TokenValidation(token) if !flag { @@ -54,9 +58,18 @@ func CreateVolume(ctx iris.Context) { } } + // Create new task + taskURI, err := taskService.CreateTask() + if err != nil { + log.Errorf("Unable to create the task: %s", err.Error()) + ctx.StatusCode(http.StatusInternalServerError) + ctx.WriteString("Unable to create the task") + return + } + var deviceDetails dpmodel.Device - //Get device details from request - err := ctx.ReadJSON(&deviceDetails) + // Get device details from request + err = ctx.ReadJSON(&deviceDetails) if err != nil { log.Error("While trying to collect data from request: " + err.Error()) ctx.StatusCode(http.StatusBadRequest) @@ -79,6 +92,13 @@ func CreateVolume(ctx iris.Context) { systemID := ctx.Params().Get("id") storageInstance := ctx.Params().Get("id2") + isInvalid, res := validateRequest(reqBody) + if isInvalid { + ctx.StatusCode(int(res.StatusCode)) + _, _ = ctx.JSON(res.Body) + return + } + driveURI := reqBody.Drives[0].OdataID s := strings.Split(driveURI, "/") driveSystemID := s[4] @@ -92,104 +112,117 @@ func CreateVolume(ctx iris.Context) { } // Getting the firmware version of server before creating a new volume - var resp []byte managersURI := "/redfish/v1/Managers/" + systemID managersURI = strings.Replace(managersURI, "System", "iDRAC", -1) statusCode, verErrMsg := getFirmwareVersion(managersURI, device) if statusCode != http.StatusOK { log.Error(verErrMsg) - resp = createResponse(response.GeneralError, verErrMsg, response.GeneralError) - } else { - // Getting the list of volumes before creating a new volume - volStatusCode, volErrMsg, list1 := getVolumeCollection(uri, device) - if volStatusCode != http.StatusOK { - log.Error(volErrMsg) - ctx.StatusCode(volStatusCode) - ctx.WriteString(volErrMsg) - return - } + ctx.StatusCode(http.StatusBadRequest) + ctx.WriteString(verErrMsg) + return + } + + taskID := retrieveTaskID(taskURI) + go hardwareService.createVolume(device, taskID, uri, reqPostBody) + + ctx.Header("Location", "/taskmon/"+taskID) + ctx.StatusCode(http.StatusAccepted) +} + +func createVolume(device *dputilities.RedfishDevice, taskID, uri, requestBody string) { + updateTask(taskID, device.Host, uri, requestBody, dputilities.Running, dputilities.Ok, 0, http.MethodPost, http.StatusAccepted) + + // Getting the list of volumes before creating a new volume + volumeListBeforeCreate, err := getVolumeCollection(uri, device) + if err != nil { + updateTaskWithException(taskID, device.Host, uri, requestBody, http.MethodPost) + return + } + + // Send create Volume request to BMC + statusCode, header, _, err := queryDevice(uri, device, http.MethodPost) + if err != nil { + log.Errorf("Error while creating volume. StatusCode: %d, msg: %s", statusCode, err.Error()) + updateTaskWithException(taskID, device.Host, uri, requestBody, http.MethodPost) + return + } + + taskURI := header.Get("Location") + if taskURI == "" { + log.Errorf("missing location in volume create response header. Unable to track task - create volume might or might not finish successfully") + updateTask(taskID, device.Host, uri, requestBody, dputilities.Completed, dputilities.Warning, 100, http.MethodPost, http.StatusOK) + return + } + // Wait for create Volume task to change its state + err = waitForTaskToFinish(taskURI, device, taskID, uri, requestBody, http.MethodPost) + if err != nil { + return + } + + // Getting the list of volumes after creating a new volume + volumeListAfterCreate, err := getVolumeCollection(uri, device) + if err != nil { + updateTaskWithException(taskID, device.Host, uri, requestBody, http.MethodPost) + return + } - // calling device for creating a volume - var header http.Header - statusCode, header, resp, err = queryDevice(uri, device, http.MethodPost) + log.Info("volume was created successfully.") + updateTask(taskID, device.Host, uri, requestBody, dputilities.Completed, dputilities.Ok, 100, http.MethodPost, http.StatusCreated) + + // Getting the origin of condition for event + oriOfCondition := compareCollection(volumeListBeforeCreate, volumeListAfterCreate) + event := createEvent("Volume created Event", "ResourceAdded", + "Volume is created successfully", "ResourceEvent.1.0.3.ResourceCreated", oriOfCondition) + dputilities.ManualEvents(event, device.Host) +} + +func waitForTaskToFinish(taskURI string, device *dputilities.RedfishDevice, taskID string, uri string, requestBody string, + httpMethod string) error { + for { + statusCode, _, body, err := queryDevice(taskURI, device, http.MethodGet) if err != nil { - errMsg := "While trying to create volume, got: " + err.Error() - log.Error(errMsg) - ctx.StatusCode(statusCode) - ctx.WriteString(errMsg) + log.Errorf("Error while retrieving volume task. StatusCode: %d, msg: %s", statusCode, err.Error()) + updateTaskWithException(taskID, device.Host, uri, requestBody, httpMethod) + return err } - // If a OperationApplyTime is Immediate and create volume response contains any Location header then looping it to get final response - if (reqBody.OperationApplyTime == "" || reqBody.OperationApplyTime == "Immediate") && header.Get("Location") != "" { - taskURI := header.Get("Location") - //tracking the task id until reaches final state - for { - time.Sleep(10 * time.Second) - // calling device for creating a volume - statusCode, header, resp, err = queryDevice(taskURI, device, http.MethodGet) - if err != nil { - errorMessage := "While trying to get task id in create volume, got: " + err.Error() - log.Error(errorMessage) - ctx.StatusCode(statusCode) - ctx.WriteString(errorMessage) - return - } - if statusCode != http.StatusAccepted { - log.Info("Final Status of task id while creating a volume : " + strconv.Itoa(statusCode)) - break - } - } + volumeTask := new(dpmodel.Task) + err = json.Unmarshal(body, &volumeTask) + if err != nil { + log.Errorf("error while trying to unmarshal response data: " + err.Error()) + updateTaskWithException(taskID, device.Host, uri, requestBody, httpMethod) + return err } - // If volume addition is success then generating an event - if statusCode == http.StatusOK { - // Getting the list of volumes after creating a new volume - volStatusCode, volErrMsg, list2 := getVolumeCollection(uri, device) - if volStatusCode != http.StatusOK { - ctx.StatusCode(volStatusCode) - ctx.WriteString(volErrMsg) - return - } - // Getting the origin of condition for event - oriOfCondition := compareCollection(list1, list2) - // creating a event payload - event := common.MessageData{ - OdataType: "#Event.v1_2_1.Event", - Name: "Volume created Event", - Context: "/redfish/v1/$metadata#Event.Event", - Events: []common.Event{ - common.Event{ - EventType: "ResourceAdded", - EventID: "123", - Severity: "Critical", - EventTimestamp: time.Now().String(), - Message: "Volume is created successfully", - MessageID: "ResourceEvent.1.0.3.ResourceCreated", - OriginOfCondition: &common.Link{ - Oid: oriOfCondition, - }, - }, - }, - } - manualEvents(event, deviceDetails.Host) - resp = createResponse(response.Success, "The resource has been created successfully", response.Created) + + state, err := taskService.GetTaskState(volumeTask.TaskState) + if err != nil { + log.Errorf("error while trying to get task state from task: " + err.Error()) + updateTaskWithException(taskID, device.Host, uri, requestBody, httpMethod) + return err } - if reqBody.OperationApplyTime == "OnReset" && statusCode == http.StatusAccepted { - resp = createResponse(response.Success, "System reset is required", response.Success) - statusCode = http.StatusOK + switch state { + case dputilities.New, dputilities.Starting, dputilities.Running: + time.Sleep(5 * time.Second) + continue + case dputilities.Completed: + log.Infof("volume task is completed!") + return nil + default: + errorMsg := fmt.Sprintf("volume task finished with state %s, status code: %d", state.String(), statusCode) + log.Errorf(errorMsg) + updateTaskWithException(taskID, device.Host, uri, requestBody, http.MethodPost) + return fmt.Errorf(errorMsg) } } - - ctx.StatusCode(statusCode) - ctx.Write(resp) } // DeleteVolume function is used for deleting a volume under storage func DeleteVolume(ctx iris.Context) { - //Get token from Request + // Get token from Request token := ctx.GetHeader("X-Auth-Token") uri := ctx.Request().RequestURI - //replacing the request url with south bound translation URL + // Replacing the request url with south bound translation URL for key, value := range pluginConfig.Data.URLTranslation.SouthBoundURL { uri = strings.Replace(uri, key, value, -1) } @@ -197,7 +230,7 @@ func DeleteVolume(ctx iris.Context) { storageInstance := ctx.Params().Get("id2") uri = convertToSouthBoundURI(uri, storageInstance) - //Validating the token + // Validating the token if token != "" { flag := TokenValidation(token) if !flag { @@ -210,7 +243,7 @@ func DeleteVolume(ctx iris.Context) { var deviceDetails dpmodel.Device - //Get device details from request + // Get device details from request err := ctx.ReadJSON(&deviceDetails) if err != nil { log.Error("While trying to collect data from request, got: " + err.Error()) @@ -225,121 +258,74 @@ func DeleteVolume(ctx iris.Context) { PostBody: deviceDetails.PostBody, } - redfishClient, err := dputilities.GetRedfishClient() + taskURI, err := taskService.CreateTask() if err != nil { - errMsg := "While trying to create the redfish client, got:" + err.Error() - log.Error(errMsg) + log.Errorf("Unable to create the task: %s", err.Error()) ctx.StatusCode(http.StatusInternalServerError) - ctx.WriteString(errMsg) + ctx.WriteString("Unable to create the task") return } - resp, err := redfishClient.DeviceCall(device, uri, http.MethodDelete) + + taskID := retrieveTaskID(taskURI) + go hardwareService.deleteVolume(device, taskID, uri) + + ctx.StatusCode(http.StatusAccepted) + ctx.Header("Location", "/taskmon/"+taskID) +} + +func deleteVolume(device *dputilities.RedfishDevice, taskID, uri string) { + updateTask(taskID, device.Host, uri, "", dputilities.Running, dputilities.Ok, 0, http.MethodDelete, http.StatusAccepted) + + // Send delete Volume request to BMC + statusCode, header, _, err := queryDevice(uri, device, http.MethodDelete) if err != nil { - errorMessage := "While trying to delete volume, got: " + err.Error() - log.Error(errorMessage) - if resp == nil { - ctx.StatusCode(http.StatusInternalServerError) - ctx.WriteString(errorMessage) - return - } + log.Errorf("Error while deleting volume. StatusCode: %d, msg: %s", statusCode, err.Error()) + updateTaskWithException(taskID, device.Host, uri, "", http.MethodPost) + return } - // If a delete volume response contains any Location header then looping it to get final response - if resp.Header.Get("Location") != "" { - taskURI := resp.Header.Get("Location") - //tracking the task id until reaches final state - for { - time.Sleep(10 * time.Second) - resp, err = redfishClient.DeviceCall(device, taskURI, http.MethodGet) - if err != nil { - errorMessage := "While trying to get task id in delete volume, got: " + err.Error() - log.Error(errorMessage) - ctx.StatusCode(http.StatusInternalServerError) - ctx.WriteString(errorMessage) - return - } - if resp.StatusCode != http.StatusAccepted { - log.Info("Final Status of task id while deleting a volume : " + strconv.Itoa(resp.StatusCode)) - break - } - } + taskURI := header.Get("Location") + if taskURI == "" { + log.Errorf("missing location in volume delete response header. Unable to track task - delete volume might or might not finish successfully") + updateTask(taskID, device.Host, uri, "", dputilities.Completed, dputilities.Warning, 100, http.MethodPost, http.StatusOK) + return } - defer resp.Body.Close() - body, err := ioutil.ReadAll(resp.Body) + // Wait for delete volume task to complete. + err = waitForTaskToFinish(taskURI, device, taskID, uri, "", http.MethodDelete) if err != nil { - errorMessage := "While trying to delete volume, got: " + err.Error() - log.Error(errorMessage) - ctx.StatusCode(http.StatusInternalServerError) - ctx.WriteString(errorMessage) return } - // If volume deletion is success then generating an event - if resp.StatusCode == http.StatusOK { - event := common.MessageData{ - OdataType: "#Event.v1_2_1.Event", - Name: "Volume removed event", - Context: "/redfish/v1/$metadata#Event.Event", - Events: []common.Event{ - common.Event{ - EventType: "ResourceRemoved", - EventID: "123", - Severity: "Critical", - EventTimestamp: time.Now().String(), - Message: "Volume is deleted successfully", - MessageID: "ResourceEvent.1.0.3.ResourceRemoved", - OriginOfCondition: &common.Link{ - Oid: uri, - }, - }, - }, - } - manualEvents(event, deviceDetails.Host) - } - ctx.StatusCode(resp.StatusCode) - ctx.Write(body) -} + log.Infof("volume was deleted successfully.") + updateTask(taskID, device.Host, uri, "", dputilities.Completed, dputilities.Ok, 100, http.MethodDelete, http.StatusOK) -// manualEvents is used to generate an event based on the inputs provided -// It will send the received data and ip to publish method -func manualEvents(req common.MessageData, hostAddress string) { - request, _ := json.Marshal(req) - reqData := string(request) - //replacing the response with north bound translation URL - for key, value := range pluginConfig.Data.URLTranslation.NorthBoundURL { - reqData = strings.Replace(reqData, key, value, -1) - } - event := common.Events{ - IP: hostAddress, - Request: []byte(reqData), - } - // Call writeEventToJobQueue to write events to worker pool - writeEventToJobQueue(event) + event := createEvent("Volume removed event", "ResourceRemoved", "Volume is deleted successfully", + "ResourceEvent.1.0.3.ResourceRemoved", uri) + dputilities.ManualEvents(event, device.Host) } // getVolumeCollection lists all the available volumes in the device -func getVolumeCollection(uri string, device *dputilities.RedfishDevice) (int, string, []string) { +func getVolumeCollection(uri string, device *dputilities.RedfishDevice) ([]string, error) { // Getting the list of volumes already exist in the server statusCode, _, resp, err := queryDevice(uri, device, http.MethodGet) if err != nil { - errMsg := "While getting volume collection details during create volume, got: " + err.Error() - log.Error(errMsg) - return statusCode, errMsg, nil + log.Errorf("Error while fetching volume collection. StatusCode: %d, msg: %s", statusCode, err.Error()) + return nil, err } + var volumes dpmodel.VolumesCollection err = json.Unmarshal(resp, &volumes) if err != nil { - errMsg := "While trying to unmarshal response data in create volume, got: " + err.Error() - log.Error(errMsg) - return http.StatusInternalServerError, errMsg, nil + log.Errorf("error while trying to unmarshal response data in create volume: " + err.Error()) + return nil, err } var list []string for _, member := range volumes.Members { list = append(list, member.OdataID) } - return http.StatusOK, "", list + return list, nil } // compareCollection will compare 2 slices and return the unique item from list2 @@ -364,23 +350,64 @@ func compareCollection(list1, list2 []string) string { return result } -// createResponse is used for creating a final response for create volume -func createResponse(code, msg, msgID string) []byte { - resp := dpresponse.ErrorResopnse{ - Error: dpresponse.Error{ - Code: code, - Message: "See @Message.ExtendedInfo for more information.", - MessageExtendedInfo: []dpresponse.MsgExtendedInfo{ - dpresponse.MsgExtendedInfo{ - MessageID: msgID, - Message: msg, - MessageArgs: []string{}, +func updateTask(taskID, host, targetURI, request string, taskState dputilities.TaskState, taskStatus dputilities.TaskStatus, + percentComplete int32, httpMethod string, statusCode int) { + payLoad := &taskproto.Payload{ + HTTPOperation: httpMethod, + JSONBody: request, + TargetURI: targetURI, + StatusCode: int32(statusCode), + } + + err := taskService.UpdateTask(taskID, host, taskState, taskStatus, percentComplete, payLoad, time.Now()) + if err != nil { + log.Errorf("Unable to update task with ID: %s", taskID) + } +} + +func retrieveTaskID(taskURI string) string { + strArray := strings.Split(taskURI, "/") + if strings.HasSuffix(taskURI, "/") { + return strArray[len(strArray)-2] + } else { + return strArray[len(strArray)-1] + } +} + +func updateTaskWithException(taskID, host, uri, requestBody, method string) { + updateTask(taskID, host, uri, requestBody, dputilities.Exception, dputilities.Critical, 100, method, http.StatusInternalServerError) +} + +func validateRequest(requestBody dpmodel.Volume) (bool, response.RPC) { + if requestBody.Drives == nil || len(requestBody.Drives) == 0 { + return true, common.GeneralError(http.StatusBadRequest, response.PropertyMissing, "", []interface{}{"Drives"}, nil) + } + + if requestBody.Drives[0].OdataID == "" { + return true, common.GeneralError(http.StatusBadRequest, response.PropertyMissing, "", []interface{}{"@odata.id"}, nil) + } + + return false, response.RPC{} +} + +func createEvent(name string, eventType string, message string, messageID string, origin string) common.MessageData { + return common.MessageData{ + OdataType: "#Event.v1_2_1.Event", + Name: name, + Context: "/redfish/v1/$metadata#Event.Event", + Events: []common.Event{ + { + EventType: eventType, + Severity: "Critical", + EventTimestamp: time.Now().String(), + Message: message, + MessageID: messageID, + OriginOfCondition: &common.Link{ + Oid: origin, }, }, }, } - body, _ := json.Marshal(resp) - return body } // getFirmwareVersion of the device @@ -409,3 +436,23 @@ func getFirmwareVersion(uri string, device *dputilities.RedfishDevice) (int, str } return http.StatusOK, "" } + +type volumeOnHardwareService interface { + createVolume(device *dputilities.RedfishDevice, taskID, uri, requestBody string) + deleteVolume(device *dputilities.RedfishDevice, taskID, uri string) +} + +type volumeOnHardwareServiceImpl struct { +} + +func (h *volumeOnHardwareServiceImpl) createVolume(device *dputilities.RedfishDevice, taskID, uri, requestBody string) { + createVolume(device, taskID, uri, requestBody) +} + +func (h *volumeOnHardwareServiceImpl) deleteVolume(device *dputilities.RedfishDevice, taskID, uri string) { + deleteVolume(device, taskID, uri) +} + +func GetVolumeOnHardwareService() *volumeOnHardwareServiceImpl { + return &volumeOnHardwareServiceImpl{} +} diff --git a/plugin-dell/dphandler/storage_test.go b/plugin-dell/dphandler/storage_test.go index a51fe8f03..dc2e7a9d7 100644 --- a/plugin-dell/dphandler/storage_test.go +++ b/plugin-dell/dphandler/storage_test.go @@ -18,8 +18,13 @@ package dphandler import ( "encoding/json" "fmt" + taskproto "github.com/ODIM-Project/ODIM/lib-utilities/proto/task" + "github.com/ODIM-Project/ODIM/plugin-dell/dputilities" + "github.com/google/uuid" "net/http" + "sync" "testing" + "time" "github.com/ODIM-Project/ODIM/plugin-dell/config" "github.com/ODIM-Project/ODIM/plugin-dell/dpmodel" @@ -35,7 +40,7 @@ func mockDevice(username, password, url string, w http.ResponseWriter) { OdataType: "#VolumeCollection.VolumeCollection", Description: "Volume Collection view", Members: []dpmodel.OdataIDLink{ - dpmodel.OdataIDLink{ + { OdataID: "/redfish/v1/Systems/1/Storage/ArrayControllers-0/Volumes/1", }, }, @@ -51,6 +56,10 @@ func mockDevice(username, password, url string, w http.ResponseWriter) { FirmwareVersion: "4.39.10.00", } + volumeTask := dpmodel.Task{ + TaskState: "Completed", + } + if url == "/redfish/v1/Managers/1" { e, _ := json.Marshal(firmware) w.WriteHeader(http.StatusOK) @@ -67,6 +76,7 @@ func mockDevice(username, password, url string, w http.ResponseWriter) { if url == "/ODIM/v1/Systems/1/Storage/1/Volumes" && username == "admin" { e, _ := json.Marshal(volume) + w.Header().Add("Location", "/taskmon/1") w.WriteHeader(http.StatusOK) w.Write(e) return @@ -83,6 +93,17 @@ func mockDevice(username, password, url string, w http.ResponseWriter) { w.WriteHeader(http.StatusBadRequest) return } + if url == "/ODIM/v1/Systems/1/Storage/Volumes/1" && username == "admin" { + w.Header().Add("Location", "/taskmon/1") + w.WriteHeader(http.StatusOK) + return + } + if url == "/taskmon/1" && username == "admin" { + e, _ := json.Marshal(volumeTask) + w.WriteHeader(http.StatusOK) + w.Write(e) + return + } return } @@ -105,9 +126,10 @@ func TestCreateVolume(t *testing.T) { e := httptest.New(t, mockApp) reqPostBody := map[string]interface{}{ - "Name": "Volume_Test1", - "RAIDType": "RAID0", - "Drives": []dpmodel.OdataIDLink{{OdataID: "/ODIM/v1/Systems/5a9e8356-265c-413b-80d2-58210592d931:1/Storage/ArrayControllers-0/Drives/0"}}, + "Name": "Volume_Test1", + "RAIDType": "RAID0", + "VolumeType": "NonRedundant", + "Drives": []dpmodel.OdataIDLink{{OdataID: "/ODIM/v1/Systems/5a9e8356-265c-413b-80d2-58210592d931:1/Storage/ArrayControllers-0/Drives/0"}}, } reqBodyBytes, _ := json.Marshal(reqPostBody) requestBody := map[string]interface{}{ @@ -117,21 +139,28 @@ func TestCreateVolume(t *testing.T) { "PostBody": reqBodyBytes, } - //Unit Test for success scenario - e.POST("/redfish/v1/Systems/1/Storage/1/Volumes").WithJSON(requestBody).Expect().Status(http.StatusOK) + taskService = newTaskServiceMock() + hardwareMock := newVolumeOnHardwareServiceMock() + hardwareService = hardwareMock - //Case for invalid token + // Unit Test for success scenario + hardwareMock.wg.Add(1) + e.POST("/redfish/v1/Systems/1/Storage/1/Volumes").WithJSON(requestBody).Expect().Status(http.StatusAccepted) + hardwareMock.wg.Wait() + + // Case for invalid token e.POST("/redfish/v1/Systems/1/Storage/1/Volumes").WithHeader("X-Auth-Token", "token").WithJSON(requestBody).Expect().Status(http.StatusUnauthorized) - //unittest for bad request scenario + // Unit test for bad request scenario invalidRequestBody := "invalid" e.POST("/redfish/v1/Systems/1/Storage/1/Volumes").WithJSON(invalidRequestBody).Expect().Status(http.StatusBadRequest) // Unit test for firmware version less than 4.40 reqPostBody = map[string]interface{}{ - "Name": "Volume_Test2", - "RAIDType": "RAID0", - "Drives": []dpmodel.OdataIDLink{{OdataID: "/ODIM/v1/Systems/5a9e8356-265c-413b-80d2-58210592d931:2/Storage/ArrayControllers-0/Drives/0"}}, + "Name": "Volume_Test2", + "RAIDType": "RAID0", + "VolumeType": "NonRedundant", + "Drives": []dpmodel.OdataIDLink{{OdataID: "/ODIM/v1/Systems/5a9e8356-265c-413b-80d2-58210592d931:2/Storage/ArrayControllers-0/Drives/0"}}, } reqBodyBytes, _ = json.Marshal(reqPostBody) requestBody = map[string]interface{}{ @@ -140,11 +169,11 @@ func TestCreateVolume(t *testing.T) { "Password": []byte("P@$$w0rd"), "PostBody": reqBodyBytes, } - //Unit Test for firmware version less than 4.40 scenario + // Unit Test for firmware version less than 4.40 scenario e.POST("/redfish/v1/Systems/2/Storage/1/Volumes").WithJSON(requestBody).Expect().Status(http.StatusBadRequest) } -func TesDeleteVolume(t *testing.T) { +func TestDeleteVolume(t *testing.T) { config.SetUpMockConfig(t) deviceHost := "localhost" devicePort := "1234" @@ -156,7 +185,7 @@ func TesDeleteVolume(t *testing.T) { mockApp := iris.New() redfishRoutes := mockApp.Party("/redfish/v1") - redfishRoutes.Delete("/Systems/{id}/Storage/{id2}/Volumes/rid", CreateVolume) + redfishRoutes.Delete("/Systems/{id}/Storage/{id2}/Volumes/{rid}", DeleteVolume) dpresponse.PluginToken = "token" @@ -168,9 +197,58 @@ func TesDeleteVolume(t *testing.T) { "Password": []byte("P@$$w0rd"), } + taskService = newTaskServiceMock() + hardwareMock := newVolumeOnHardwareServiceMock() + hardwareService = hardwareMock + //Unit Test for success scenario - e.DELETE("/redfish/v1/Systems/1/Storage/1/Volumes/1").WithJSON(requestBody).Expect().Status(http.StatusOK) + hardwareMock.wg.Add(1) + e.DELETE("/redfish/v1/Systems/1/Storage/1/Volumes/1").WithJSON(requestBody).Expect().Status(http.StatusAccepted) + hardwareMock.wg.Wait() //Case for invalid token e.DELETE("/redfish/v1/Systems/1/Storage/1/Volumes/1").WithHeader("X-Auth-Token", "token").WithJSON(requestBody).Expect().Status(http.StatusUnauthorized) } + +type volumeOnHardwareServiceMock struct { + wg sync.WaitGroup +} + +func (hm *volumeOnHardwareServiceMock) createVolume(device *dputilities.RedfishDevice, taskID, uri, requestBody string) { + defer hm.wg.Done() + createVolume(device, taskID, uri, requestBody) +} + +func (hm *volumeOnHardwareServiceMock) deleteVolume(device *dputilities.RedfishDevice, taskID, uri string) { + defer hm.wg.Done() + deleteVolume(device, taskID, uri) +} + +func newVolumeOnHardwareServiceMock() *volumeOnHardwareServiceMock { + return &volumeOnHardwareServiceMock{} +} + +type taskServiceMock struct { + dputilities.TaskService +} + +func (ts *taskServiceMock) CreateTask() (string, error) { + return "task" + uuid.New().String(), nil +} + +func (ts *taskServiceMock) UpdateTask(taskID, host string, taskState dputilities.TaskState, taskStatus dputilities.TaskStatus, + percentComplete int32, payLoad *taskproto.Payload, endTime time.Time) error { + return nil +} + +func (ts *taskServiceMock) GetTaskState(state string) (dputilities.TaskState, error) { + return dputilities.Completed, nil +} + +func (ts *taskServiceMock) GetTaskStatus(status string) (dputilities.TaskStatus, error) { + return dputilities.Ok, nil +} + +func newTaskServiceMock() *taskServiceMock { + return &taskServiceMock{} +} diff --git a/plugin-dell/dpmodel/Task.go b/plugin-dell/dpmodel/Task.go new file mode 100644 index 000000000..cce2c7429 --- /dev/null +++ b/plugin-dell/dpmodel/Task.go @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2021 Intel Corporation + * + * 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 dpmodel + +type Task struct { + ID string `json:"Id"` + Description string `json:"Description"` + Name string `json:"Name"` + StartTime string `json:"StartTime"` + TaskState string `json:"TaskState"` + TaskStatus string `json:"TaskStatus"` + EndTime string + Messages []*Message `json:"Messages"` + Oem OemTask `json:"Oem"` +} + +// Message Model +type Message struct { + Message string `json:"Message"` + MessageID string `json:"MessageId"` + MessageArgs []string `json:"MessageArgs"` + Oem OemTask `json:"Oem"` + RelatedProperties []string `json:"RelatedProperties"` + Resolution string `json:"Resolution"` + Severity string `json:"Severity"` +} + +// Oem Model +type OemTask struct { + Dell DellTask `json:"Dell"` +} + +type DellTask struct { + CompletionTime string `json:"CompletionTime"` + Description string `json:"Description"` + EndTime string `json:"EndTime"` + Id string `json:"Id"` + JobState string `json:"JobState"` + JobType string `json:"JobType"` + Message string `json:"Message"` + MessageArgs []string `json:"MessageArgs"` + MessageId string `json:"MessageId"` + Name string `json:"Name"` + PercentComplete int `json:"PercentComplete"` + StartTime string `json:"StartTime"` + TargetSettingsURI string `json:"TargetSettingsURI"` +} diff --git a/plugin-dell/dpmodel/storage.go b/plugin-dell/dpmodel/storage.go index 5ac37b93d..e02951a7e 100644 --- a/plugin-dell/dpmodel/storage.go +++ b/plugin-dell/dpmodel/storage.go @@ -21,6 +21,7 @@ type Volume struct { RAIDType string `json:"RAIDType"` Drives []OdataIDLink `json:"Drives"` OperationApplyTime string `json:"@Redfish.OperationApplyTime"` + VolumeType string `json:"VolumeType"` } // OdataIDLink contains link to a resource diff --git a/plugin-dell/dputilities/events.go b/plugin-dell/dputilities/events.go new file mode 100644 index 000000000..f8479cd47 --- /dev/null +++ b/plugin-dell/dputilities/events.go @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2021 Intel Corporation + * + * 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 dputilities + +import ( + "encoding/json" + "github.com/ODIM-Project/ODIM/lib-utilities/common" + pluginConfig "github.com/ODIM-Project/ODIM/plugin-dell/config" + "strings" +) + +var ( + In chan<- interface{} + Out <-chan interface{} +) + +// manualEvents is used to generate an event based on the inputs provided +// It will send the received data and ip to publish method +func ManualEvents(req common.MessageData, hostAddress string) { + request, _ := json.Marshal(req) + reqData := string(request) + //replacing the response with north bound translation URL + for key, value := range pluginConfig.Data.URLTranslation.NorthBoundURL { + reqData = strings.Replace(reqData, key, value, -1) + } + event := common.Events{ + IP: hostAddress, + Request: []byte(reqData), + } + // Call writeEventToJobQueue to write events to worker pool + WriteEventToJobQueue(event) +} + +// writeEventToJobQueue will write events to worker pool +func WriteEventToJobQueue(event common.Events) { + var events []interface{} + //events := make([]interface{}, 0) + events = append(events, event) + done := make(chan bool) + go common.RunWriteWorkers(In, events, 1, done) +} diff --git a/plugin-dell/dputilities/taskService.go b/plugin-dell/dputilities/taskService.go new file mode 100644 index 000000000..577f5a7ff --- /dev/null +++ b/plugin-dell/dputilities/taskService.go @@ -0,0 +1,435 @@ +/* + * Copyright (c) 2021 Intel Corporation + * + * 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 dputilities + +import ( + "encoding/json" + "fmt" + "strings" + "time" + + "github.com/ODIM-Project/ODIM/lib-utilities/common" + "github.com/ODIM-Project/ODIM/lib-utilities/config" + taskproto "github.com/ODIM-Project/ODIM/lib-utilities/proto/task" + pluginConf "github.com/ODIM-Project/ODIM/plugin-dell/config" + "github.com/ODIM-Project/ODIM/plugin-dell/dpmodel" + "github.com/google/uuid" + log "github.com/sirupsen/logrus" +) + +const ( + // CompletedTaskIndex is a index name which is required for + // to build index for completed tasks + CompletedTaskIndex = "CompletedTaskIndex" + //CompletedTaskTable is a Table name for Completed Task + CompletedTaskTable = "CompletedTask" +) + +type TaskState int32 + +const ( + Completed TaskState = iota + Cancelling + Cancelled + Exception + Interrupted + New + Pending + Running + Service + Starting + Stopping + Suspended +) + +func (ts TaskState) String() string { + return [...]string{"Completed", "Cancelling", "Cancelled", "Exception", "Interrupted", "New", "Pending", "Running", + "Service", "Starting", "Stopping", "Suspended"}[ts] +} + +type TaskStatus int32 + +const ( + Critical TaskStatus = iota + Ok + Warning +) + +func (ts TaskStatus) String() string { + return [...]string{"Critical", "Ok", "Warning"}[ts] +} + +// Task struct with TaskState and TaskStatus enums +type Task struct { + ParentID string + ID string + URI string + UserName string + Name string + HidePayload bool + Payload Payload + PercentComplete int32 + TaskMonitor string + TaskState TaskState + TaskStatus TaskStatus + StatusCode int32 + TaskResponse []byte + Messages []*dpmodel.Message + StartTime time.Time + EndTime time.Time +} + +// Task struct representing Task in database +type TaskDb struct { + ParentID string + ID string + URI string + UserName string + Name string + HidePayload bool + Payload Payload + PercentComplete int32 + TaskMonitor string + TaskState string + TaskStatus string + StatusCode int32 + TaskResponse []byte + Messages []*dpmodel.Message + StartTime time.Time + EndTime time.Time +} + +// Payload contain information detailing the HTTP and JSON payload +//information for executing the task. +//This object shall not be included in the response if the HidePayload property +// is set to True. +type Payload struct { + HTTPHeaders map[string]string `json:"HttpHeaders"` + HTTPOperation string `json:"HttpOperation"` + JSONBody string `json:"JsonBody"` + TargetURI string `json:"TargetUri"` +} + +type TaskService interface { + CreateTask() (string, error) + UpdateTask(taskID, host string, taskState TaskState, taskStatus TaskStatus, percentComplete int32, + payLoad *taskproto.Payload, endTime time.Time) error + GetTaskState(state string) (TaskState, error) + GetTaskStatus(status string) (TaskStatus, error) +} + +type TaskServiceImpl struct { +} + +func GetTaskService() *TaskServiceImpl { + return &TaskServiceImpl{} +} + +func (ts *TaskServiceImpl) CreateTask() (string, error) { + userName := pluginConf.Data.PluginConf.UserName + + // Frame the model + currentTime := time.Now() + + task := TaskDb{ + UserName: userName, + ID: "task" + uuid.New().String(), + TaskState: New.String(), + TaskStatus: Ok.String(), + PercentComplete: 0, + StartTime: currentTime, + EndTime: currentTime, + } + task.Name = "Task " + task.ID + task.TaskMonitor = "/taskmon/" + task.ID + task.URI = "/redfish/v1/TaskService/Tasks/" + task.ID + + // Persist in the in-memory DB + err := persistTask(&task) + if err != nil { + log.Error("error while trying to insert the task details: " + err.Error()) + return "", err + } + // return the Task URI + return "/redfish/v1/TaskService/Tasks/" + task.ID, err +} + +// updateTaskUtil is a function to update the existing task and/or to create sub-task under a parent task. +// This function is to set task status, task end time along with task state based on the task state. +// Takes: +// taskID - Is of type string, containes task ID of the task to updated +// taskState - Is of type string, containes new sate of the task +// taskStatus - Is of type string, containes new status of the task +// endTime - Is of type time.Time, containses the endtime of the task +// Retruns: +// err of type error +// nil - On Success +// Non nil - On Failure +func (ts *TaskServiceImpl) UpdateTask(taskID, host string, taskState TaskState, taskStatus TaskStatus, percentComplete int32, payLoad *taskproto.Payload, endTime time.Time) error { + // Retrieve the task details using taskID + task, err := ts.getTaskFromDb(taskID) + if err != nil { + return fmt.Errorf("error while retrieving the task details from db: " + err.Error()) + } + //If the task is already in cancelled state, then updates are not allowed to it. + if task.TaskState == Cancelled || (task.TaskState == Cancelling && taskState != Cancelled) { + return fmt.Errorf("task is already cancelled or being cancelling") + } + + task.TaskState = taskState + task.TaskStatus = taskStatus + task.EndTime = endTime + task.PercentComplete = percentComplete + if payLoad != nil { + task.Payload.HTTPOperation = payLoad.HTTPOperation + task.Payload.HTTPHeaders = payLoad.HTTPHeaders + task.Payload.JSONBody = payLoad.JSONBody + task.Payload.TargetURI = payLoad.TargetURI + task.StatusCode = payLoad.StatusCode + task.TaskResponse = payLoad.ResponseBody + } + taskEventMessageID := "TaskEvent.1.0.1.Task" + taskState.String() + + // Update the task data in the InMemory DB + err = updateTaskStatus(task) + if err != nil { + return fmt.Errorf("error while updating the task to In-memory DB: %v" + err.Error()) + } + + event := common.MessageData{ + OdataType: "#Event.v1_2_1.Event", + Name: "Task status changed", + Context: "/redfish/v1/$metadata#Event.Event", + Events: []common.Event{ + { + EventType: "StatusChange", + Severity: "Ok", + EventTimestamp: time.Now().String(), + Message: "Task updated successfully", + MessageID: taskEventMessageID, + OriginOfCondition: &common.Link{ + Oid: task.URI, + }, + }, + }, + } + ManualEvents(event, host) + return err +} + +// PersistTask is to store the task data in db +// Takes: +// t pointer to Task to be stored. +// db of type common.DbType(int32) +func persistTask(t *TaskDb) error { + PrepareDbConfig() + connPool, err := common.GetDBConnection(common.InMemory) + if err != nil { + return fmt.Errorf("error while trying to connecting to DB: %v", err.Error()) + } + if err = connPool.Create("task", t.ID, t); err != nil { + return fmt.Errorf("error while trying to create new task: %v", err.Error()) + } + return nil +} + +// UpdateTaskStatus is to update the task data already present in db +// Takes: +// db of type common.DbType(int32) +// t of type *Task +// Returns: +// err of type error +// On Success - return nil value +// On Failure - return non nill value +func updateTaskStatus(t *Task) error { + connPool, err := common.GetDBConnection(common.InMemory) + if err != nil { + return fmt.Errorf("error while trying to connecting to DB: %v", err.Error()) + } + if _, err = connPool.Update("task", t.ID, convertToDbTask(t)); err != nil { + return fmt.Errorf("error while trying to update task: %v", err.Error()) + } + // Build Redis Index here if we dont do it in thandle + if t.TaskState == Completed && t.ParentID == "" { + taskIndexErr := buildCompletedTaskIndex(t, CompletedTaskTable) + if err != nil { + return taskIndexErr + } + } + return nil +} + +// GetTaskStatus is to retrieve the task data already present in db +// Takes: +// taskID of type string contains the task ID of the task to be retrieved from the db +// db of type common.DbType(int32) +// Returns: +// err of type error +// On Success - return nil value +// On Failure - return non nill value +// t of type *Task implicitly valid only when error is nil +func (ts *TaskServiceImpl) getTaskFromDb(taskID string) (*Task, error) { + connPool, err := common.GetDBConnection(common.InMemory) + if err != nil { + log.Error("error while trying to get the db connection") + return nil, fmt.Errorf("error while trying to connnect to DB: %v", err.Error()) + } + taskData, err := connPool.Read("task", taskID) + if err != nil { + return nil, fmt.Errorf("error while trying to read from DB: %v", err.Error()) + } + + task, errs := ts.unmarshalTask([]byte(taskData)) + if errs != nil { + return nil, fmt.Errorf("error while trying to unmarshal task data: %v", errs) + } + + return task, nil +} + +//BuildCompletedTaskIndex is used to build the index for Completed Task +func buildCompletedTaskIndex(completedTask *Task, table string) error { + conn, err := common.GetDBConnection(common.InMemory) + if err != nil { + return fmt.Errorf("error while trying to connecting to DB: %v", err.Error()) + } + key := completedTask.UserName + "::" + completedTask.EndTime.String() + "::" + completedTask.ID + createError := conn.CreateTaskIndex(CompletedTaskIndex, completedTask.EndTime.UnixNano(), key) + if createError != nil { + return fmt.Errorf("error while trying to create task index: %v", err) + } + return nil +} + +func (ts *TaskServiceImpl) unmarshalTask(taskData []byte) (*Task, error) { + dbTask := new(TaskDb) + errorMsg := "error while trying to unmarshal task data: %v" + if err := json.Unmarshal(taskData, &dbTask); err != nil { + return nil, fmt.Errorf(errorMsg, err) + } + + taskState, err := ts.GetTaskState(dbTask.TaskState) + if err != nil { + return nil, fmt.Errorf(errorMsg, err) + } + + taskStatus, err := ts.GetTaskStatus(dbTask.TaskStatus) + if err != nil { + return nil, fmt.Errorf(errorMsg, err) + } + + return &Task{ + ParentID: dbTask.ParentID, + ID: dbTask.ID, + URI: dbTask.URI, + UserName: dbTask.UserName, + Name: dbTask.Name, + HidePayload: dbTask.HidePayload, + Payload: dbTask.Payload, + PercentComplete: dbTask.PercentComplete, + TaskMonitor: dbTask.TaskMonitor, + TaskState: taskState, + TaskStatus: taskStatus, + StatusCode: dbTask.StatusCode, + TaskResponse: dbTask.TaskResponse, + Messages: dbTask.Messages, + StartTime: dbTask.StartTime, + EndTime: dbTask.EndTime, + }, nil +} + +func PrepareDbConfig() { + config.Data.DBConf = &config.DBConf{ + Protocol: pluginConf.Data.DBConf.Protocol, + InMemoryHost: pluginConf.Data.DBConf.InMemoryHost, + InMemoryPort: pluginConf.Data.DBConf.InMemoryPort, + OnDiskHost: pluginConf.Data.DBConf.OnDiskHost, + OnDiskPort: pluginConf.Data.DBConf.OnDiskPort, + MaxIdleConns: pluginConf.Data.DBConf.MaxIdleConns, + MaxActiveConns: pluginConf.Data.DBConf.MaxActiveConns, + RedisHAEnabled: pluginConf.Data.DBConf.RedisHAEnabled, + InMemorySentinelPort: pluginConf.Data.DBConf.InMemorySentinelPort, + InMemoryMasterSet: pluginConf.Data.DBConf.InMemoryMasterSet, + OnDiskMasterSet: pluginConf.Data.DBConf.OnDiskMasterSet, + OnDiskSentinelPort: pluginConf.Data.DBConf.OnDiskSentinelPort, + } +} + +func (ts *TaskServiceImpl) GetTaskState(state string) (TaskState, error) { + switch strings.ToLower(state) { + case strings.ToLower(Completed.String()): + return Completed, nil + case strings.ToLower(Cancelling.String()): + return Cancelling, nil + case strings.ToLower(Cancelled.String()): + return Cancelled, nil + case strings.ToLower(Exception.String()): + return Exception, nil + case strings.ToLower(Interrupted.String()): + return Interrupted, nil + case strings.ToLower(New.String()): + return New, nil + case strings.ToLower(Pending.String()): + return Pending, nil + case strings.ToLower(Running.String()): + return Running, nil + case strings.ToLower(Service.String()): + return Service, nil + case strings.ToLower(Starting.String()): + return Starting, nil + case strings.ToLower(Stopping.String()): + return Stopping, nil + case strings.ToLower(Suspended.String()): + return Suspended, nil + default: + return 0, fmt.Errorf("taskState not recognized") + } +} + +func (ts *TaskServiceImpl) GetTaskStatus(status string) (TaskStatus, error) { + switch strings.ToLower(status) { + case strings.ToLower(Critical.String()): + return Critical, nil + case strings.ToLower(Ok.String()): + return Ok, nil + case strings.ToLower(Warning.String()): + return Warning, nil + default: + return 0, fmt.Errorf("taskStatus not recognized") + } +} + +func convertToDbTask(task *Task) *TaskDb { + return &TaskDb{ + ParentID: task.ParentID, + ID: task.ID, + URI: task.URI, + UserName: task.UserName, + Name: task.Name, + HidePayload: task.HidePayload, + Payload: task.Payload, + PercentComplete: task.PercentComplete, + TaskMonitor: task.TaskMonitor, + TaskState: task.TaskState.String(), + TaskStatus: task.TaskStatus.String(), + StatusCode: task.StatusCode, + TaskResponse: task.TaskResponse, + Messages: task.Messages, + StartTime: task.StartTime, + EndTime: task.EndTime, + } +} diff --git a/plugin-dell/go.mod b/plugin-dell/go.mod index 048fc353a..eeec59ba4 100644 --- a/plugin-dell/go.mod +++ b/plugin-dell/go.mod @@ -8,8 +8,11 @@ require ( github.com/ODIM-Project/ODIM/lib-utilities v0.0.0-20201201072448-9772421f1b55 github.com/fsnotify/fsnotify v1.4.7 github.com/gofrs/uuid v3.2.0+incompatible + github.com/golang/protobuf v1.4.2 + github.com/google/uuid v1.1.2-0.20200519141726-cb32006e483f github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 // indirect github.com/kataras/iris/v12 v12.1.9-0.20200616210209-a85c83b70ad0 + github.com/micro/go-micro v1.13.2 github.com/satori/go.uuid v1.2.0 github.com/sirupsen/logrus v1.4.2 github.com/yudai/pp v2.0.1+incompatible // indirect diff --git a/plugin-dell/go.sum b/plugin-dell/go.sum index 849ffeec8..0935fe24f 100644 --- a/plugin-dell/go.sum +++ b/plugin-dell/go.sum @@ -135,6 +135,7 @@ github.com/go-cmd/cmd v1.0.5/go.mod h1:y8q8qlK5wQibcw63djSl/ntiHUHXHGdCkPk0j4QeW github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-ini/ini v1.44.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-log/log v0.1.0 h1:wudGTNsiGzrD5ZjgIkVZ517ugi2XRe9Q/xRCzwEO4/U= github.com/go-log/log v0.1.0/go.mod h1:4mBwpdRMFLiuXZDCwU2lKQFsoSCo72j3HqBK9d81N2M= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= @@ -333,12 +334,15 @@ github.com/mediocregopher/radix/v3 v3.5.0/go.mod h1:8FL3F6UQRXHXIBSPUs5h0RybMF8i github.com/mediocregopher/radix/v3 v3.5.1/go.mod h1:8FL3F6UQRXHXIBSPUs5h0RybMF8i4n7wVopoX3x7Bv8= github.com/mholt/certmagic v0.7.5/go.mod h1:91uJzK5K8IWtYQqTi5R2tsxV1pCde+wdGfaRaOZi6aQ= github.com/micro/cli v0.2.0/go.mod h1:jRT9gmfVKWSS6pkKcXQ8YhUyj6bzwxK8Fp5b0Y7qNnk= +github.com/micro/go-micro v1.13.2 h1:H+Z8rhazPpGYeUV1CxaLlo+O+wJeZqg/sjVVONmbr4o= github.com/micro/go-micro v1.13.2/go.mod h1:dbMgBQRxpTdBZPfr+sUKZsw7oY/7pf8TaM3Ud0/sDus= +github.com/micro/mdns v0.3.0 h1:bYycYe+98AXR3s8Nq5qvt6C573uFTDPIYzJemWON0QE= github.com/micro/mdns v0.3.0/go.mod h1:KJ0dW7KmicXU2BV++qkLlmHYcVv7/hHnbtguSWt9Aoc= github.com/microcosm-cc/bluemonday v1.0.2 h1:5lPfLTTAvAbtS0VqT+94yOtFnGfUWYyx0+iToC3Os3s= github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.1.3/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/miekg/dns v1.1.15 h1:CSSIDtllwGLMoA6zjdKnaE6Tx6eVUxQ29LUgGetiDCI= github.com/miekg/dns v1.1.15/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= @@ -399,6 +403,7 @@ github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR github.com/pierrec/lz4 v2.0.5+incompatible h1:2xWsjqPFWcplujydGg4WmhC/6fZqK42wMM8aXeqhl0I= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= diff --git a/plugin-dell/main.go b/plugin-dell/main.go index 8b43c0f75..f630dc8b7 100644 --- a/plugin-dell/main.go +++ b/plugin-dell/main.go @@ -36,12 +36,6 @@ import ( var subscriptionInfo []dpmodel.Device var log = logrus.New() -// TokenObject will contains the generated token and public key of odimra -type TokenObject struct { - AuthToken string `json:"authToken"` - PublicKey []byte `json:"publicKey"` -} - func main() { // verifying the uid of the user if uid := os.Geteuid(); uid == 0 { @@ -56,13 +50,18 @@ func main() { log.Fatal("While trying to set messagebus configuration, got: " + err.Error()) } + dputilities.PrepareDbConfig() + if err := common.CheckDBConnection(); err != nil { + log.Fatal("While trying to check DB connection health got: " + err.Error()) + } + // CreateJobQueue defines the queue which will act as an infinite buffer // In channel is an entry or input channel and the Out channel is an exit or output channel - dphandler.In, dphandler.Out = common.CreateJobQueue() + dputilities.In, dputilities.Out = common.CreateJobQueue() // RunReadWorkers will create a worker pool for doing a specific task // which is passed to it as Publish method after reading the data from the channel. - go common.RunReadWorkers(dphandler.Out, dpmessagebus.Publish, 5) + go common.RunReadWorkers(dputilities.Out, dpmessagebus.Publish, 5) configFilePath := os.Getenv("PLUGIN_CONFIG_FILE_PATH") if configFilePath == "" { @@ -284,6 +283,6 @@ func sendStartupEvent() { done := make(chan bool) events := []interface{}{event} - go common.RunWriteWorkers(dphandler.In, events, 1, done) + go common.RunWriteWorkers(dputilities.In, events, 1, done) log.Info("successfully sent startup event") } diff --git a/svc-systems/rpc/systems_test.go b/svc-systems/rpc/systems_test.go index 5c83de95d..6dc78c035 100644 --- a/svc-systems/rpc/systems_test.go +++ b/svc-systems/rpc/systems_test.go @@ -91,7 +91,7 @@ func contactPluginClient(url, method, token string, odataID string, body interfa if url == "https://localhost:9091/ODIM/v1/Systems/1/Storage/1/Volumes/1" { body := `{"MessageId": "` + response.Success + `"}` return &http.Response{ - StatusCode: http.StatusOK, + StatusCode: http.StatusNoContent, Body: ioutil.NopCloser(bytes.NewBufferString(body)), }, nil } diff --git a/svc-systems/scommon/common.go b/svc-systems/scommon/common.go index 756fa46f3..755af0f60 100644 --- a/svc-systems/scommon/common.go +++ b/svc-systems/scommon/common.go @@ -56,6 +56,7 @@ type PluginContactRequest struct { type ResponseStatus struct { StatusCode int32 StatusMessage string + Header http.Header } //ResourceInfoRequest hold the request of getting Resource @@ -228,12 +229,15 @@ func ContactPlugin(req PluginContactRequest, errorMessage string) ([]byte, strin } log.Info("Response" + string(body)) log.Info("response.StatusCode" + string(response.StatusCode)) - if response.StatusCode != http.StatusCreated && response.StatusCode != http.StatusOK { + if response.StatusCode != http.StatusCreated && response.StatusCode != http.StatusOK && response.StatusCode != http.StatusAccepted { resp.StatusCode = int32(response.StatusCode) log.Println(errorMessage) return body, "", resp, fmt.Errorf(errorMessage) } + resp.StatusCode = int32(response.StatusCode) + resp.Header = response.Header + data := string(body) //replacing the resposne with north bound translation URL for key, value := range config.Data.URLTranslation.NorthBoundURL { diff --git a/svc-systems/systems/storage.go b/svc-systems/systems/storage.go index dcb1abc8d..cbe2a930d 100644 --- a/svc-systems/systems/storage.go +++ b/svc-systems/systems/storage.go @@ -166,12 +166,24 @@ func (e *ExternalInterface) CreateVolume(req *systemsproto.VolumeRequest) respon resp.Header = map[string]string{ "Content-type": "application/json; charset=utf-8", } - resp.StatusCode = http.StatusOK + + for key, values := range getResponse.Header { + for _, value := range values { + resp.Header[key] = value + } + } + + resp.StatusCode = getResponse.StatusCode resp.StatusMessage = response.Success - err = json.Unmarshal(body, &resp.Body) - if err != nil { - return common.GeneralError(http.StatusInternalServerError, response.InternalError, err.Error(), nil, nil) + + // Unmarshal response body only when body is not empty. + if len(body) > 0 { + err = json.Unmarshal(body, &resp.Body) + if err != nil { + return common.GeneralError(http.StatusInternalServerError, response.InternalError, err.Error(), nil, nil) + } } + return resp } @@ -415,7 +427,13 @@ func (e *ExternalInterface) DeleteVolume(req *systemsproto.VolumeRequest) respon smodel.AddSystemResetInfo(key, "On") smodel.AddSystemResetInfo(collectionKey, "On") - resp.StatusCode = http.StatusNoContent + for key, values := range getResponse.Header { + for _, value := range values { + resp.Header[key] = value + } + } + + resp.StatusCode = getResponse.StatusCode resp.StatusMessage = response.Success return resp } diff --git a/svc-systems/systems/storage_test.go b/svc-systems/systems/storage_test.go index cae7790b4..f2d36e3bd 100644 --- a/svc-systems/systems/storage_test.go +++ b/svc-systems/systems/storage_test.go @@ -63,7 +63,7 @@ func contactPluginClient(url, method, token string, odataID string, body interfa if url == "https://localhost:9091/ODIM/v1/Systems/1/Storage/1/Volumes/1" { body := `{"MessageId": "` + response.Success + `"}` return &http.Response{ - StatusCode: http.StatusOK, + StatusCode: http.StatusNoContent, Body: ioutil.NopCloser(bytes.NewBufferString(body)), }, nil }