diff --git a/lib-utilities/common/constants.go b/lib-utilities/common/constants.go index 5947e408c..6f8da46aa 100644 --- a/lib-utilities/common/constants.go +++ b/lib-utilities/common/constants.go @@ -206,7 +206,7 @@ const ( CreateRemoteAccountService = "CreateRemoteAccountService" UpdateRemoteAccountService = "UpdateRemoteAccountService" DeleteRemoteAccountService = "DeleteRemoteAccountService" - + InstallLicenseService = "InstallLicenseService" // constants for log SessionToken = "sessiontoken" SessionUserID = "sessionuserid" diff --git a/svc-licenses/lcommon/common.go b/svc-licenses/lcommon/common.go index 6975a3f8c..8a5a84020 100644 --- a/svc-licenses/lcommon/common.go +++ b/svc-licenses/lcommon/common.go @@ -20,15 +20,20 @@ import ( "fmt" "io/ioutil" "net/http" + "runtime" "strconv" "strings" + "time" "github.com/ODIM-Project/ODIM/lib-persistence-manager/persistencemgr" "github.com/ODIM-Project/ODIM/lib-utilities/common" "github.com/ODIM-Project/ODIM/lib-utilities/config" "github.com/ODIM-Project/ODIM/lib-utilities/errors" + "github.com/ODIM-Project/ODIM/lib-utilities/logs" l "github.com/ODIM-Project/ODIM/lib-utilities/logs" + taskproto "github.com/ODIM-Project/ODIM/lib-utilities/proto/task" "github.com/ODIM-Project/ODIM/lib-utilities/response" + "github.com/ODIM-Project/ODIM/lib-utilities/services" "github.com/ODIM-Project/ODIM/svc-licenses/model" ) @@ -37,6 +42,13 @@ var ( ConfigFilePath string ) +// PluginTaskInfo hold the task information from plugin +type PluginTaskInfo struct { + Location string + PluginIP string + PluginServerName string +} + // GetAllKeysFromTable fetches all keys in a given table func GetAllKeysFromTable(table string, dbtype persistencemgr.DbType) ([]string, error) { conn, err := persistencemgr.GetDBConnection(dbtype) @@ -112,8 +124,11 @@ func GetPluginData(pluginID string) (*model.Plugin, *errors.Error) { } // ContactPlugin is commons which handles the request and response of Contact Plugin usage -func ContactPlugin(ctx context.Context, req model.PluginContactRequest, errorMessage string) ([]byte, string, model.ResponseStatus, error) { +func ContactPlugin(ctx context.Context, req model.PluginContactRequest, + errorMessage string) ([]byte, string, PluginTaskInfo, model.ResponseStatus, error) { + var resp model.ResponseStatus + var pluginTaskInfo PluginTaskInfo var err error pluginResponse, err := callPlugin(ctx, req) if err != nil { @@ -125,41 +140,52 @@ func ContactPlugin(ctx context.Context, req model.PluginContactRequest, errorMes resp.StatusCode = http.StatusServiceUnavailable resp.StatusMessage = response.CouldNotEstablishConnection resp.MsgArgs = []interface{}{"https://" + req.Plugin.IP + ":" + req.Plugin.Port + req.OID} - return nil, "", resp, fmt.Errorf(errorMessage) + return nil, "", pluginTaskInfo, resp, fmt.Errorf(errorMessage) } } defer pluginResponse.Body.Close() + body, err := ioutil.ReadAll(pluginResponse.Body) if err != nil { errorMessage := "error while trying to read response body: " + err.Error() resp.StatusCode = http.StatusInternalServerError resp.StatusMessage = errors.InternalError l.LogWithFields(ctx).Warn(errorMessage) - return nil, "", resp, fmt.Errorf(errorMessage) + return nil, "", pluginTaskInfo, resp, fmt.Errorf(errorMessage) } - if pluginResponse.StatusCode != http.StatusCreated && pluginResponse.StatusCode != http.StatusOK { + if pluginResponse.StatusCode == http.StatusAccepted { + pluginTaskInfo.Location = pluginResponse.Header.Get("Location") + pluginTaskInfo.PluginIP = pluginResponse.Header.Get(common.XForwardedFor) + } + + if pluginResponse.StatusCode != http.StatusCreated && + pluginResponse.StatusCode != http.StatusOK && + pluginResponse.StatusCode != http.StatusAccepted { if pluginResponse.StatusCode == http.StatusUnauthorized { errorMessage += "error: invalid resource username/password" resp.StatusCode = int32(pluginResponse.StatusCode) resp.StatusMessage = response.ResourceAtURIUnauthorized resp.MsgArgs = []interface{}{"https://" + req.Plugin.IP + ":" + req.Plugin.Port + req.OID} l.LogWithFields(ctx).Warn(errorMessage) - return nil, "", resp, fmt.Errorf(errorMessage) + return nil, "", pluginTaskInfo, resp, fmt.Errorf(errorMessage) } errorMessage += string(body) resp.StatusCode = int32(pluginResponse.StatusCode) resp.StatusMessage = response.InternalError l.LogWithFields(ctx).Warn(errorMessage) - return body, "", resp, fmt.Errorf(errorMessage) + return body, "", pluginTaskInfo, resp, fmt.Errorf(errorMessage) } + resp.StatusCode = int32(pluginResponse.StatusCode) + resp.StatusMessage = response.Success + data := string(body) //replacing the resposne with north bound translation URL for key, value := range config.Data.URLTranslation.NorthBoundURL { data = strings.Replace(data, key, value, -1) } - return []byte(data), pluginResponse.Header.Get("X-Auth-Token"), resp, nil + return []byte(data), pluginResponse.Header.Get("X-Auth-Token"), pluginTaskInfo, resp, nil } // getPluginStatus checks the status of given plugin in configured interval @@ -215,6 +241,7 @@ func GenericSave(ctx context.Context, body []byte, table string, key string) err return nil } +// GetIDsFromURI will return the manager ID from server URI func GetIDsFromURI(uri string) (string, string, error) { lastChar := uri[len(uri)-1:] if lastChar == "/" { @@ -228,6 +255,7 @@ func GetIDsFromURI(uri string) (string, string, error) { return ids[0], ids[1], nil } +// TrackConfigFileChanges monitors the config changes using fsnotfiy func TrackConfigFileChanges(errChan chan error) { eventChan := make(chan interface{}) format := config.Data.LogFormat @@ -250,3 +278,34 @@ func TrackConfigFileChanges(errChan chan error) { } } } + +// UpdateTask update the task with the given data +func UpdateTask(ctx context.Context, taskData common.TaskData) error { + var res map[string]interface{} + if err := json.Unmarshal([]byte(taskData.TaskRequest), &res); err != nil { + l.Log.Error(err) + } + reqStr := logs.MaskRequestBody(res) + + respBody, _ := json.Marshal(taskData.Response.Body) + payLoad := &taskproto.Payload{ + HTTPHeaders: taskData.Response.Header, + HTTPOperation: taskData.HTTPMethod, + JSONBody: reqStr, + StatusCode: taskData.Response.StatusCode, + TargetURI: taskData.TargetURI, + ResponseBody: respBody, + } + + err := services.UpdateTask(ctx, taskData.TaskID, taskData.TaskState, taskData.TaskStatus, taskData.PercentComplete, payLoad, time.Now()) + if err != nil && (err.Error() == common.Cancelling) { + // We cant do anything here as the task has done it work completely, we cant reverse it. + //Unless if we can do opposite/reverse action for delete server which is add server. + services.UpdateTask(ctx, taskData.TaskID, common.Cancelled, taskData.TaskStatus, taskData.PercentComplete, payLoad, time.Now()) + if taskData.PercentComplete == 0 { + return fmt.Errorf("error while starting the task: %v", err) + } + runtime.Goexit() + } + return nil +} diff --git a/svc-licenses/licenses/common.go b/svc-licenses/licenses/common.go index 7fd263dc2..10644fac0 100644 --- a/svc-licenses/licenses/common.go +++ b/svc-licenses/licenses/common.go @@ -36,13 +36,17 @@ type ExternalInterface struct { // External struct holds the function pointers all outboud services type External struct { - ContactClient func(context.Context, string, string, string, string, interface{}, map[string]string) (*http.Response, error) - Auth func(string, []string, []string) (response.RPC, error) - DevicePassword func([]byte) ([]byte, error) - GetPluginData func(string) (*model.Plugin, *errors.Error) - ContactPlugin func(context.Context, model.PluginContactRequest, string) ([]byte, string, model.ResponseStatus, error) + ContactClient func(context.Context, string, string, string, string, interface{}, map[string]string) (*http.Response, error) + Auth func(string, []string, []string) (response.RPC, error) + DevicePassword func([]byte) ([]byte, error) + GetPluginData func(string) (*model.Plugin, *errors.Error) + ContactPlugin func(context.Context, model.PluginContactRequest, string) ([]byte, string, + lcommon.PluginTaskInfo, model.ResponseStatus, error) GetTarget func(string) (*model.Target, *errors.Error) GetSessionUserName func(string) (string, error) + CreateTask func(ctx context.Context, sessionUserName string) (string, error) + CreateChildTask func(context.Context, string, string) (string, error) + UpdateTask func(context.Context, common.TaskData) error GenericSave func(context.Context, []byte, string, string) error } @@ -63,6 +67,9 @@ func GetExternalInterface() *ExternalInterface { ContactPlugin: lcommon.ContactPlugin, GetTarget: lcommon.GetTarget, GetSessionUserName: services.GetSessionUserName, + CreateTask: services.CreateTask, + CreateChildTask: services.CreateChildTask, + UpdateTask: lcommon.UpdateTask, GenericSave: lcommon.GenericSave, }, DB: DB{ @@ -71,3 +78,16 @@ func GetExternalInterface() *ExternalInterface { }, } } + +func fillTaskData(taskID, targetURI, request string, resp response.RPC, taskState string, taskStatus string, percentComplete int32, httpMethod string) common.TaskData { + return common.TaskData{ + TaskID: taskID, + TargetURI: targetURI, + TaskRequest: request, + Response: resp, + TaskState: taskState, + TaskStatus: taskStatus, + PercentComplete: percentComplete, + HTTPMethod: httpMethod, + } +} diff --git a/svc-licenses/licenses/common_test.go b/svc-licenses/licenses/common_test.go index 262103f2d..f7453d1cf 100644 --- a/svc-licenses/licenses/common_test.go +++ b/svc-licenses/licenses/common_test.go @@ -27,6 +27,7 @@ import ( "github.com/ODIM-Project/ODIM/lib-utilities/common" "github.com/ODIM-Project/ODIM/lib-utilities/errors" "github.com/ODIM-Project/ODIM/lib-utilities/response" + lcommon "github.com/ODIM-Project/ODIM/svc-licenses/lcommon" "github.com/ODIM-Project/ODIM/svc-licenses/model" ) @@ -50,13 +51,16 @@ func TestGetExternalInterface(t *testing.T) { func mockGetExternalInterface() *ExternalInterface { return &ExternalInterface{ External: External{ - Auth: mockIsAuthorized, - ContactClient: mockContactClient, - GetTarget: mockGetTarget, - GetPluginData: mockGetPluginData, - ContactPlugin: mockContactPlugin, - DevicePassword: stubDevicePassword, - GenericSave: stubGenericSave, + Auth: mockIsAuthorized, + ContactClient: mockContactClient, + GetTarget: mockGetTarget, + GetPluginData: mockGetPluginData, + ContactPlugin: mockContactPlugin, + DevicePassword: stubDevicePassword, + CreateTask: mockCreateTask, + CreateChildTask: mockCreateChildTask, + UpdateTask: mockUpdateTask, + GenericSave: stubGenericSave, }, DB: DB{ GetAllKeysFromTable: mockGetAllKeysFromTable, @@ -65,10 +69,32 @@ func mockGetExternalInterface() *ExternalInterface { } } -func mockContactPlugin(ctx context.Context, req model.PluginContactRequest, errorMessage string) ([]byte, string, model.ResponseStatus, error) { +func mockCreateTask(ctx context.Context, sessionID string) (string, error) { + return "task12345", nil +} + +func mockCreateChildTask(ctx context.Context, sessionID, taskID string) (string, error) { + switch taskID { + case "taskWithoutChild": + return "", fmt.Errorf("subtask cannot created") + case "subTaskWithSlash": + return "someSubTaskID/", nil + default: + return "someSubTaskID", nil + } +} + +func mockUpdateTask(ctx context.Context, task common.TaskData) error { + if task.TaskID == "invalid" { + return fmt.Errorf("task with this ID not found") + } + return nil +} + +func mockContactPlugin(ctx context.Context, req model.PluginContactRequest, errorMessage string) ([]byte, string, lcommon.PluginTaskInfo, model.ResponseStatus, error) { var responseStatus model.ResponseStatus - return []byte(`{"Attributes":"sample"}`), "token", responseStatus, nil + return []byte(`{"Attributes":"sample"}`), "token", lcommon.PluginTaskInfo{}, responseStatus, nil } func stubGenericSave(ctx context.Context, reqBody []byte, table string, uuid string) error { diff --git a/svc-licenses/licenses/licenses.go b/svc-licenses/licenses/licenses.go index dff7e77c5..5b46623f3 100644 --- a/svc-licenses/licenses/licenses.go +++ b/svc-licenses/licenses/licenses.go @@ -18,7 +18,10 @@ import ( "context" "encoding/base64" "encoding/json" + "fmt" "net/http" + "runtime" + "strconv" "strings" dmtf "github.com/ODIM-Project/ODIM/lib-dmtf/model" @@ -27,13 +30,14 @@ import ( l "github.com/ODIM-Project/ODIM/lib-utilities/logs" licenseproto "github.com/ODIM-Project/ODIM/lib-utilities/proto/licenses" "github.com/ODIM-Project/ODIM/lib-utilities/response" + "github.com/ODIM-Project/ODIM/lib-utilities/services" lcommon "github.com/ODIM-Project/ODIM/svc-licenses/lcommon" "github.com/ODIM-Project/ODIM/svc-licenses/model" ) var ( - JsonUnMarshalFunc = json.Unmarshal - JsonMarshalFunc = json.Marshal + jsonUnMarshalFunc = json.Unmarshal + jsonMarshalFunc = json.Marshal ) // GetLicenseService to get license service details @@ -117,46 +121,159 @@ func (e *ExternalInterface) GetLicenseResource(ctx context.Context, req *license } // InstallLicenseService to install license -func (e *ExternalInterface) InstallLicenseService(ctx context.Context, req *licenseproto.InstallLicenseRequest) response.RPC { +func (e *ExternalInterface) InstallLicenseService(ctx context.Context, req *licenseproto.InstallLicenseRequest, sessionUserName, taskID string) { var resp response.RPC - var contactRequest model.PluginContactRequest var installreq dmtf.LicenseInstallRequest + var percentComplete int32 + + targetURI := "/redfish/v1/LicenseService/Licenses" + + taskInfo := &common.TaskUpdateInfo{Context: ctx, TaskID: taskID, TargetURI: targetURI, + UpdateTask: e.External.UpdateTask, TaskRequest: string(req.RequestBody)} - genErr := JsonUnMarshalFunc(req.RequestBody, &installreq) + genErr := jsonUnMarshalFunc(req.RequestBody, &installreq) if genErr != nil { errMsg := "Unable to unmarshal the install license request" + genErr.Error() l.LogWithFields(ctx).Error(errMsg) - return common.GeneralError(http.StatusBadRequest, response.InternalError, errMsg, nil, nil) + common.GeneralError(http.StatusBadRequest, response.InternalError, errMsg, nil, taskInfo) + return } if installreq.Links == nil { errMsg := "Invalid request,mandatory field Links missing" l.LogWithFields(ctx).Error(errMsg) - return common.GeneralError(http.StatusBadRequest, response.PropertyMissing, errMsg, []interface{}{"Links"}, nil) + common.GeneralError(http.StatusBadRequest, response.PropertyMissing, errMsg, []interface{}{"Links"}, taskInfo) + return } else if installreq.LicenseString == "" { errMsg := "Invalid request, mandatory field LicenseString is missing" l.LogWithFields(ctx).Error(errMsg) - return common.GeneralError(http.StatusBadRequest, response.PropertyMissing, errMsg, []interface{}{"LicenseString"}, nil) - + common.GeneralError(http.StatusBadRequest, response.PropertyMissing, errMsg, []interface{}{"LicenseString"}, taskInfo) + return } else if len(installreq.Links.Link) == 0 { errMsg := "Invalid request, mandatory field AuthorizedDevices links is missing" l.LogWithFields(ctx).Error(errMsg) - return common.GeneralError(http.StatusBadRequest, response.PropertyMissing, errMsg, []interface{}{"LicenseString"}, nil) + common.GeneralError(http.StatusBadRequest, response.PropertyMissing, errMsg, []interface{}{"LicenseString"}, taskInfo) + return + } + + linksMap, errStatusCode, err := e.getManagerLinksMap(ctx, installreq.Links.Link) + if err != nil { + l.LogWithFields(ctx).Error(err) + common.GeneralError(errStatusCode, response.InternalError, err.Error(), nil, taskInfo) + } + l.LogWithFields(ctx).Info("Map with manager Links: ", linksMap) + partialResultFlag := false + subTaskChannel := make(chan int32, len(linksMap)) + for serverURI := range linksMap { + uuid, managerID, err := lcommon.GetIDsFromURI(serverURI) + if err != nil { + errMsg := "error while trying to get system ID from " + serverURI + ": " + err.Error() + l.LogWithFields(ctx).Error(errMsg) + common.GeneralError(http.StatusNotFound, response.ResourceNotFound, errMsg, []interface{}{"SystemID", serverURI}, taskInfo) + return + } + + encodedKey := base64.StdEncoding.EncodeToString([]byte(installreq.LicenseString)) + managerURI := "/redfish/v1/Managers/" + managerID + reqPostBody := map[string]interface{}{"LicenseString": encodedKey, "AuthorizedDevices": managerURI} + reqBody, _ := json.Marshal(reqPostBody) + var threadID int = 1 + ctxt := context.WithValue(ctx, common.ThreadName, common.SendRequest) + ctxt = context.WithValue(ctxt, common.ThreadID, strconv.Itoa(threadID)) + go e.sendRequest(ctx, uuid, sessionUserName, taskID, serverURI, reqBody, subTaskChannel) + threadID++ + } + + resp.StatusCode = http.StatusOK + for i := 0; i < len(linksMap); i++ { + select { + case statusCode := <-subTaskChannel: + if statusCode != http.StatusOK { + if statusCode != http.StatusAccepted { + partialResultFlag = true + } + if resp.StatusCode < statusCode { + resp.StatusCode = statusCode + } + } + if i < len(linksMap)-1 && statusCode != http.StatusAccepted { + percentComplete := int32(((i + 1) / len(linksMap)) * 100) + var task = fillTaskData(taskID, targetURI, string(req.RequestBody), resp, common.Running, common.OK, percentComplete, http.MethodPost) + err := e.External.UpdateTask(ctx, task) + if err != nil && err.Error() == common.Cancelling { + task = fillTaskData(taskID, targetURI, string(req.RequestBody), resp, common.Cancelled, common.OK, percentComplete, http.MethodPost) + e.External.UpdateTask(ctx, task) + runtime.Goexit() + } + + } + } + } + + taskStatus := common.OK + if partialResultFlag { + taskStatus = common.Warning + } + + if resp.StatusCode == http.StatusAccepted { + return + } + percentComplete = 100 + if resp.StatusCode != http.StatusOK { + errMsg := "One or more of the Install License requests failed. for more information please check SubTasks in URI: /redfish/v1/TaskService/Tasks/" + taskID + l.LogWithFields(ctx).Warn(errMsg) + switch resp.StatusCode { + case http.StatusUnauthorized: + common.GeneralError(http.StatusUnauthorized, response.ResourceAtURIUnauthorized, errMsg, []interface{}{fmt.Sprintf("%v", linksMap)}, taskInfo) + return + case http.StatusNotFound: + common.GeneralError(http.StatusNotFound, response.ResourceNotFound, errMsg, []interface{}{"option", "Licenses"}, taskInfo) + return + case http.StatusBadRequest: + common.GeneralError(http.StatusBadRequest, response.PropertyUnknown, errMsg, []interface{}{"Licenses"}, taskInfo) + return + default: + common.GeneralError(http.StatusInternalServerError, response.InternalError, errMsg, nil, taskInfo) + return + } } + + l.LogWithFields(ctx).Info("All Install License requests successfully completed. for more information please check SubTasks in URI: /redfish/v1/TaskService/Tasks/" + taskID) + resp.StatusMessage = response.Success + resp.StatusCode = http.StatusOK + args := response.Args{ + Code: resp.StatusMessage, + Message: "Request completed successfully", + } + resp.Body = args.CreateGenericErrorResponse() + + var task = fillTaskData(taskID, targetURI, string(req.RequestBody), resp, common.Completed, taskStatus, percentComplete, http.MethodPost) + err = e.External.UpdateTask(ctx, task) + if err != nil && err.Error() == common.Cancelling { + task = fillTaskData(taskID, targetURI, string(req.RequestBody), resp, common.Cancelled, common.Critical, percentComplete, http.MethodPost) + e.External.UpdateTask(ctx, task) + runtime.Goexit() + } + respBody := fmt.Sprintf("%v", resp.Body) + l.LogWithFields(ctx).Debugf("final response for install license request: %s", string(respBody)) +} + +func (e *ExternalInterface) getManagerLinksMap(ctx context.Context, links []*dmtf.Link) (map[string]bool, int32, error) { var serverURI string var err error var managerLink []string + var errStatusCode int32 linksMap := make(map[string]bool) - for _, serverIDs := range installreq.Links.Link { + for _, serverIDs := range links { serverURI = serverIDs.Oid switch { case strings.Contains(serverURI, "Systems"): managerLink, err = e.getManagerURL(serverURI) if err != nil { - errMsg := "Unable to get manager link" - l.LogWithFields(ctx).Error(errMsg) - return common.GeneralError(http.StatusInternalServerError, response.InternalError, errMsg, nil, nil) + errMsg := "Unable to get manager link for " + serverURI + errStatusCode = http.StatusNotFound + return linksMap, errStatusCode, fmt.Errorf(errMsg) } for _, link := range managerLink { linksMap[link] = true @@ -166,98 +283,145 @@ func (e *ExternalInterface) InstallLicenseService(ctx context.Context, req *lice case strings.Contains(serverURI, "Aggregates"): managerLink, err = e.getDetailsFromAggregate(ctx, serverURI) if err != nil { - errMsg := "Unable to get manager link from aggregates" - l.LogWithFields(ctx).Error(errMsg) - return common.GeneralError(http.StatusInternalServerError, response.InternalError, errMsg, nil, nil) + errMsg := "Unable to get manager link from aggregates for " + serverURI + errStatusCode = http.StatusNotFound + return linksMap, errStatusCode, fmt.Errorf(errMsg) } for _, link := range managerLink { linksMap[link] = true } default: - errMsg := "Invalid AuthorizedDevices links" - l.LogWithFields(ctx).Error(errMsg) - return common.GeneralError(http.StatusBadRequest, response.InternalError, errMsg, nil, nil) + errMsg := "Unable to get manager link from aggregates for " + serverURI + errStatusCode = http.StatusBadRequest + return linksMap, errStatusCode, fmt.Errorf(errMsg) } } - l.LogWithFields(ctx).Info("Map with manager Links: ", linksMap) + return linksMap, errStatusCode, nil +} - for serverURI := range linksMap { - uuid, managerID, err := lcommon.GetIDsFromURI(serverURI) - if err != nil { - errMsg := "error while trying to get system ID from " + serverURI + ": " + err.Error() - l.LogWithFields(ctx).Error(errMsg) - return common.GeneralError(http.StatusNotFound, response.ResourceNotFound, errMsg, []interface{}{"SystemID", serverURI}, nil) - } - // Get target device Credentials from using device UUID - target, targetErr := e.External.GetTarget(uuid) - if targetErr != nil { - errMsg := targetErr.Error() - l.LogWithFields(ctx).Error(errMsg) - return common.GeneralError(http.StatusNotFound, response.ResourceNotFound, errMsg, []interface{}{"target", uuid}, nil) - } +// sendRequest request the plugin to install license and handles the response from plugin +func (e *ExternalInterface) sendRequest(ctx context.Context, uuid, sessionUserName, taskID, serverURI string, requestBody []byte, + subTaskChannel chan<- int32) { - decryptedPasswordByte, err := e.External.DevicePassword(target.Password) - if err != nil { - errMsg := "error while trying to decrypt device password: " + err.Error() - l.LogWithFields(ctx).Error(errMsg) - return common.GeneralError(http.StatusInternalServerError, response.InternalError, errMsg, nil, nil) - } - target.Password = decryptedPasswordByte + var percentComplete int32 + var resp response.RPC + subTaskURI, err := e.External.CreateChildTask(ctx, sessionUserName, taskID) + if err != nil { + subTaskChannel <- http.StatusInternalServerError + l.LogWithFields(ctx).Warn("Unable to create sub task: " + err.Error()) + return + } + var subTaskID string + strArray := strings.Split(subTaskURI, "/") + if strings.HasSuffix(subTaskURI, "/") { + subTaskID = strArray[len(strArray)-2] + } else { + subTaskID = strArray[len(strArray)-1] + } - // Get the Plugin info - plugin, errs := e.External.GetPluginData(target.PluginID) - if errs != nil { - errMsg := "error while getting plugin data: " + errs.Error() - l.LogWithFields(ctx).Error(errMsg) - return common.GeneralError(http.StatusNotFound, response.ResourceNotFound, errMsg, []interface{}{"PluginData", target.PluginID}, nil) - } - l.LogWithFields(ctx).Info("Plugin info: ", plugin) + taskInfo := &common.TaskUpdateInfo{Context: ctx, TaskID: subTaskID, TargetURI: serverURI, UpdateTask: e.External.UpdateTask, TaskRequest: string(requestBody)} - encodedKey := base64.StdEncoding.EncodeToString([]byte(installreq.LicenseString)) - managerURI := "/redfish/v1/Managers/" + managerID - reqPostBody := map[string]interface{}{"LicenseString": encodedKey, "AuthorizedDevices": managerURI} - reqBody, _ := json.Marshal(reqPostBody) + var contactRequest model.PluginContactRequest - contactRequest.Plugin = *plugin - contactRequest.ContactClient = e.External.ContactClient - contactRequest.Plugin.ID = target.PluginID - contactRequest.HTTPMethodType = http.MethodPost + // Get target device Credentials from using device UUID + target, targetErr := e.External.GetTarget(uuid) + if targetErr != nil { + subTaskChannel <- http.StatusNotFound + errMsg := targetErr.Error() + l.LogWithFields(ctx).Error(errMsg) + common.GeneralError(http.StatusNotFound, response.ResourceNotFound, errMsg, []interface{}{"target", uuid}, taskInfo) + return + } - if strings.EqualFold(plugin.PreferredAuthType, "XAuthToken") { - contactRequest.DeviceInfo = map[string]interface{}{ - "UserName": plugin.Username, - "Password": string(plugin.Password), - } - contactRequest.OID = "/ODIM/v1/Sessions" - _, token, getResponse, err := lcommon.ContactPlugin(ctx, contactRequest, "error while logging in to plugin: ") - if err != nil { - errMsg := err.Error() - l.LogWithFields(ctx).Error(errMsg) - return common.GeneralError(getResponse.StatusCode, getResponse.StatusMessage, errMsg, getResponse.MsgArgs, nil) - } - contactRequest.Token = token - } else { - contactRequest.LoginCredentials = map[string]string{ - "UserName": plugin.Username, - "Password": string(plugin.Password), - } + decryptedPasswordByte, err := e.External.DevicePassword(target.Password) + if err != nil { + subTaskChannel <- http.StatusInternalServerError + errMsg := "error while trying to decrypt device password: " + err.Error() + l.LogWithFields(ctx).Error(errMsg) + return + } + target.Password = decryptedPasswordByte + // Get the Plugin info + plugin, errs := e.External.GetPluginData(target.PluginID) + if errs != nil { + subTaskChannel <- http.StatusNotFound + errMsg := "error while getting plugin data: " + errs.Error() + l.LogWithFields(ctx).Error(errMsg) + common.GeneralError(http.StatusNotFound, response.ResourceNotFound, errMsg, []interface{}{"PluginData", target.PluginID}, taskInfo) + return + } + l.LogWithFields(ctx).Info("Plugin info: ", plugin) + + contactRequest.Plugin = *plugin + contactRequest.ContactClient = e.External.ContactClient + contactRequest.Plugin.ID = target.PluginID + contactRequest.HTTPMethodType = http.MethodPost + + if strings.EqualFold(plugin.PreferredAuthType, "XAuthToken") { + contactRequest.DeviceInfo = map[string]interface{}{ + "UserName": plugin.Username, + "Password": string(plugin.Password), } - target.PostBody = []byte(reqBody) - contactRequest.DeviceInfo = target - contactRequest.OID = "/ODIM/v1/LicenseService/Licenses" - contactRequest.PostBody = reqBody - _, _, getResponse, err := e.External.ContactPlugin(ctx, contactRequest, "error while installing license: ") + contactRequest.OID = "/ODIM/v1/Sessions" + _, token, _, getResponse, err := lcommon.ContactPlugin(ctx, contactRequest, "error while logging in to plugin: ") if err != nil { + subTaskChannel <- getResponse.StatusCode errMsg := err.Error() l.LogWithFields(ctx).Error(errMsg) - return common.GeneralError(getResponse.StatusCode, getResponse.StatusMessage, errMsg, getResponse.MsgArgs, nil) + common.GeneralError(getResponse.StatusCode, getResponse.StatusMessage, errMsg, getResponse.MsgArgs, taskInfo) + return + } + contactRequest.Token = token + } else { + contactRequest.LoginCredentials = map[string]string{ + "UserName": plugin.Username, + "Password": string(plugin.Password), } - l.LogWithFields(ctx).Info("Install license response: ", getResponse) + } + target.PostBody = []byte(requestBody) + contactRequest.DeviceInfo = target + contactRequest.OID = "/ODIM/v1/LicenseService/Licenses" + contactRequest.PostBody = requestBody + _, _, pluginTaskInfo, getResponse, err := e.External.ContactPlugin(ctx, contactRequest, "error while installing license: ") + if err != nil { + subTaskChannel <- getResponse.StatusCode + errMsg := err.Error() + l.LogWithFields(ctx).Error(errMsg) + common.GeneralError(getResponse.StatusCode, getResponse.StatusMessage, errMsg, getResponse.MsgArgs, taskInfo) + return + } + l.LogWithFields(ctx).Infof("Install license response: status code : %v, message: %s", + getResponse.StatusCode, getResponse.StatusMessage) - resp.StatusCode = http.StatusNoContent - return resp + if getResponse.StatusCode == http.StatusAccepted { + services.SavePluginTaskInfo(ctx, pluginTaskInfo.PluginIP, plugin.IP, + subTaskID, pluginTaskInfo.Location) + subTaskChannel <- http.StatusAccepted + return + } + + if getResponse.StatusCode > http.StatusMultipleChoices { + resp.StatusCode = getResponse.StatusCode + subTaskChannel <- getResponse.StatusCode + percentComplete = 100 + task := fillTaskData(subTaskID, serverURI, string(requestBody), resp, common.Completed, common.Warning, percentComplete, http.MethodPost) + e.External.UpdateTask(ctx, task) + return + } + + resp.StatusCode = http.StatusOK + percentComplete = 100 + + subTaskChannel <- http.StatusOK + var task = fillTaskData(subTaskID, serverURI, string(requestBody), resp, common.Completed, common.OK, percentComplete, http.MethodPost) + err = e.External.UpdateTask(ctx, task) + if err != nil && err.Error() == common.Cancelling { + var task = fillTaskData(subTaskID, serverURI, string(requestBody), resp, common.Cancelled, common.Critical, percentComplete, http.MethodPost) + e.External.UpdateTask(ctx, task) + } + return } func (e *ExternalInterface) getDetailsFromAggregate(ctx context.Context, aggregateURI string) ([]string, error) { @@ -267,11 +431,11 @@ func (e *ExternalInterface) getDetailsFromAggregate(ctx context.Context, aggrega if err != nil { return nil, err } - jsonStr, jerr := JsonMarshalFunc(respData) + jsonStr, jerr := jsonMarshalFunc(respData) if jerr != nil { return nil, jerr } - jerr = JsonUnMarshalFunc([]byte(jsonStr), &resource) + jerr = jsonUnMarshalFunc([]byte(jsonStr), &resource) if jerr != nil { return nil, jerr } @@ -298,7 +462,7 @@ func (e *ExternalInterface) getManagerURL(systemURI string) ([]string, error) { if err != nil { return nil, err } - jerr := JsonUnMarshalFunc([]byte(respData.(string)), &resource) + jerr := jsonUnMarshalFunc([]byte(respData.(string)), &resource) if jerr != nil { return nil, jerr } diff --git a/svc-licenses/licenses/licenses_test.go b/svc-licenses/licenses/licenses_test.go index 8db67b1a9..5c3c56ce3 100644 --- a/svc-licenses/licenses/licenses_test.go +++ b/svc-licenses/licenses/licenses_test.go @@ -120,18 +120,14 @@ func TestInstallLicenseService(t *testing.T) { } }`)} e := mockGetExternalInterface() - response := e.InstallLicenseService(ctx, req) - - assert.Equal(t, http.StatusNoContent, int(response.StatusCode), "Status code should be StatusNoContent.") + e.InstallLicenseService(ctx, req, "user", "task12345") } func TestInstallLicenseService_InvalidRequest(t *testing.T) { ctx := mockContext() req := &licenseproto.InstallLicenseRequest{} e := mockGetExternalInterface() - response := e.InstallLicenseService(ctx, req) - - assert.Equal(t, http.StatusBadRequest, int(response.StatusCode), "Status code should be StatusBadRequest.") + e.InstallLicenseService(ctx, req, "user", "task12345") } func TestInstallLicenseService_EmptyLinks(t *testing.T) { @@ -141,9 +137,7 @@ func TestInstallLicenseService_EmptyLinks(t *testing.T) { "LicenseString": "XXX-XXX-XXX-XXX-XXX" }`)} e := mockGetExternalInterface() - response := e.InstallLicenseService(ctx, req) - - assert.Equal(t, http.StatusBadRequest, int(response.StatusCode), "Status code should be StatusBadRequest.") + e.InstallLicenseService(ctx, req, "user", "task12345") } func TestInstallLicenseService_InvalidManager(t *testing.T) { @@ -158,9 +152,7 @@ func TestInstallLicenseService_InvalidManager(t *testing.T) { } }`)} e := mockGetExternalInterface() - response := e.InstallLicenseService(ctx, req) - - assert.Equal(t, http.StatusInternalServerError, int(response.StatusCode), "Status code should be StatusInternalServerError.") + e.InstallLicenseService(ctx, req, "user", "task12345") } func TestInstallLicenseService_InvalidAuthorizedDevices(t *testing.T) { @@ -175,9 +167,7 @@ func TestInstallLicenseService_InvalidAuthorizedDevices(t *testing.T) { } }`)} e := mockGetExternalInterface() - response := e.InstallLicenseService(ctx, req) - - assert.Equal(t, http.StatusBadRequest, int(response.StatusCode), "Status code should be StatusBadRequest.") + e.InstallLicenseService(ctx, req, "user", "task12345") } func TestInstallLicenseService_ManagerURL(t *testing.T) { @@ -192,9 +182,7 @@ func TestInstallLicenseService_ManagerURL(t *testing.T) { } }`)} e := mockGetExternalInterface() - response := e.InstallLicenseService(ctx, req) - - assert.Equal(t, http.StatusNoContent, int(response.StatusCode), "Status code should be StatusNoContent.") + e.InstallLicenseService(ctx, req, "user", "task12345") } func TestInstallLicenseService_Agrregates(t *testing.T) { @@ -212,9 +200,7 @@ func TestInstallLicenseService_Agrregates(t *testing.T) { } }`)} e := mockGetExternalInterface() - response := e.InstallLicenseService(ctx, req) - - assert.Equal(t, http.StatusNoContent, int(response.StatusCode), "Status code should be StatusNoContent.") + e.InstallLicenseService(ctx, req, "user", "task12345") } func TestInstallLicenseService_Agrregates_InvalidURI(t *testing.T) { @@ -230,7 +216,5 @@ func TestInstallLicenseService_Agrregates_InvalidURI(t *testing.T) { } }`)} e := mockGetExternalInterface() - response := e.InstallLicenseService(ctx, req) - - assert.Equal(t, http.StatusInternalServerError, int(response.StatusCode), "Status code should be StatusNoContent.") + e.InstallLicenseService(ctx, req, "user", "task12345") } diff --git a/svc-licenses/rpc/common.go b/svc-licenses/rpc/common.go index 4c2306398..37aeb9cdd 100644 --- a/svc-licenses/rpc/common.go +++ b/svc-licenses/rpc/common.go @@ -17,7 +17,11 @@ package rpc import ( "context" "encoding/json" + "fmt" + "net/http" + "strings" + "github.com/ODIM-Project/ODIM/lib-utilities/common" l "github.com/ODIM-Project/ODIM/lib-utilities/logs" licenseproto "github.com/ODIM-Project/ODIM/lib-utilities/proto/licenses" "github.com/ODIM-Project/ODIM/lib-utilities/response" @@ -61,3 +65,51 @@ func generateRPCResponse(rpcResp response.RPC, licenseResp *licenseproto.GetLice Body: bytes, } } + +// CreateTaskAndResponse will create the task for corresponding request using +// the RPC call to task service and it will prepare custom task response to the user +// The function returns the ID of created task back. +func CreateTaskAndResponse(ctx context.Context, l *Licenses, sessionToken string, resp *licenseproto.GetLicenseResponse) (string, string, error) { + sessionUserName, err := l.connector.External.GetSessionUserName(sessionToken) + if err != nil { + errMsg := "Unable to get session username: " + err.Error() + fillProtoResponse(ctx, resp, common.GeneralError(http.StatusUnauthorized, + response.NoValidSession, errMsg, nil, nil)) + return "", "", fmt.Errorf(errMsg) + } + + // Task Service using RPC and get the taskID + taskURI, err := l.connector.External.CreateTask(ctx, sessionUserName) + if err != nil { + errMsg := "Unable to create task: " + err.Error() + fillProtoResponse(ctx, resp, common.GeneralError(http.StatusInternalServerError, + response.InternalError, errMsg, nil, nil)) + return "", "", fmt.Errorf(errMsg) + } + taskID := strings.TrimPrefix(taskURI, "/redfish/v1/TaskService/Tasks/") + // return 202 Accepted + var rpcResp = response.RPC{ + StatusCode: http.StatusAccepted, + StatusMessage: response.TaskStarted, + Header: map[string]string{ + "Location": "/taskmon/" + taskID, + }, + } + + generateTaskRespone(taskID, taskURI, &rpcResp) + fillProtoResponse(ctx, resp, rpcResp) + return sessionUserName, taskID, nil +} + +func generateTaskRespone(taskID, taskURI string, rpcResp *response.RPC) { + commonResponse := response.Response{ + OdataType: common.TaskType, + ID: taskID, + Name: "Task " + taskID, + OdataContext: "/redfish/v1/$metadata#Task.Task", + OdataID: taskURI, + } + commonResponse.MessageArgs = []string{taskID} + commonResponse.CreateGenericResponse(rpcResp.StatusMessage) + rpcResp.Body = commonResponse +} diff --git a/svc-licenses/rpc/licenses.go b/svc-licenses/rpc/licenses.go index 3da1770b9..088a6ee01 100644 --- a/svc-licenses/rpc/licenses.go +++ b/svc-licenses/rpc/licenses.go @@ -18,6 +18,7 @@ import ( "context" "net/http" "os" + "strconv" "github.com/ODIM-Project/ODIM/lib-utilities/common" lgr "github.com/ODIM-Project/ODIM/lib-utilities/logs" @@ -90,6 +91,15 @@ func (l *Licenses) InstallLicenseService(ctx context.Context, req *licenseproto. fillProtoResponse(ctx, resp, authResp) return resp, nil } - fillProtoResponse(ctx, resp, l.connector.InstallLicenseService(ctx, req)) + sessionUserName, taskID, err := CreateTaskAndResponse(ctx, l, req.SessionToken, resp) + if err != nil { + lgr.LogWithFields(ctx).Error(err) + return resp, nil + } + + var threadID int = 1 + ctxt := context.WithValue(ctx, common.ThreadName, common.InstallLicenseService) + ctxt = context.WithValue(ctxt, common.ThreadID, strconv.Itoa(threadID)) + go l.connector.InstallLicenseService(ctx, req, sessionUserName, taskID) return resp, nil } diff --git a/svc-licenses/rpc/licenses_test.go b/svc-licenses/rpc/licenses_test.go index 811347f40..e6e44c135 100644 --- a/svc-licenses/rpc/licenses_test.go +++ b/svc-licenses/rpc/licenses_test.go @@ -27,6 +27,7 @@ import ( "github.com/ODIM-Project/ODIM/lib-utilities/errors" licenseproto "github.com/ODIM-Project/ODIM/lib-utilities/proto/licenses" "github.com/ODIM-Project/ODIM/lib-utilities/response" + lcommon "github.com/ODIM-Project/ODIM/svc-licenses/lcommon" licenseService "github.com/ODIM-Project/ODIM/svc-licenses/licenses" "github.com/ODIM-Project/ODIM/svc-licenses/model" ) @@ -34,13 +35,17 @@ import ( func mockGetExternalInterface() *licenseService.ExternalInterface { return &licenseService.ExternalInterface{ External: licenseService.External{ - Auth: mockIsAuthorized, - ContactClient: mockContactClient, - GetTarget: mockGetTarget, - GetPluginData: mockGetPluginData, - ContactPlugin: mockContactPlugin, - DevicePassword: stubDevicePassword, - GenericSave: stubGenericSave, + Auth: mockIsAuthorized, + ContactClient: mockContactClient, + GetTarget: mockGetTarget, + GetPluginData: mockGetPluginData, + ContactPlugin: mockContactPlugin, + DevicePassword: stubDevicePassword, + CreateTask: mockCreateTask, + GetSessionUserName: mockGetSessionUserName, + CreateChildTask: mockCreateChildTask, + UpdateTask: mockUpdateTask, + GenericSave: stubGenericSave, }, DB: licenseService.DB{ GetAllKeysFromTable: mockGetAllKeysFromTable, @@ -49,10 +54,40 @@ func mockGetExternalInterface() *licenseService.ExternalInterface { } } -func mockContactPlugin(ctx context.Context, req model.PluginContactRequest, errorMessage string) ([]byte, string, model.ResponseStatus, error) { +func mockGetSessionUserName(token string) (string, error) { + if token == "notValidToken" { + return "", fmt.Errorf("invalidToken") + } + return "user", nil +} + +func mockCreateTask(ctx context.Context, sessionID string) (string, error) { + return "task12345", nil +} + +func mockCreateChildTask(ctx context.Context, sessionID, taskID string) (string, error) { + switch taskID { + case "taskWithoutChild": + return "", fmt.Errorf("subtask cannot created") + case "subTaskWithSlash": + return "someSubTaskID/", nil + default: + return "someSubTaskID", nil + } +} + +func mockUpdateTask(ctx context.Context, task common.TaskData) error { + if task.TaskID == "invalid" { + return fmt.Errorf("task with this ID not found") + } + return nil +} + +func mockContactPlugin(ctx context.Context, req model.PluginContactRequest, + errorMessage string) ([]byte, string, lcommon.PluginTaskInfo, model.ResponseStatus, error) { var responseStatus model.ResponseStatus - return []byte(`{"Attributes":"sample"}`), "token", responseStatus, nil + return []byte(`{"Attributes":"sample"}`), "token", lcommon.PluginTaskInfo{}, responseStatus, nil } func stubGenericSave(ctx context.Context, reqBody []byte, table string, uuid string) error { diff --git a/svc-task/thandle/thandle.go b/svc-task/thandle/thandle.go index b24550fc8..aa0892115 100644 --- a/svc-task/thandle/thandle.go +++ b/svc-task/thandle/thandle.go @@ -1212,8 +1212,12 @@ func (ts *TasksRPC) updateParentTask(ctx context.Context, taskID, taskStatus, ta parentTask.PercentComplete = 100 parentTask.StatusCode = http.StatusOK parentTask.TaskStatus = common.OK - parentTask.TaskResponse = payLoad.ResponseBody + parentTask.StatusCode = http.StatusOK + resp := tcommon.GetTaskResponse(http.StatusOK, response.Success) + body, _ := json.Marshal(resp.Body) + parentTask.TaskResponse = body ts.UpdateTaskQueue(parentTask) + l.LogWithFields(ctx).Debugf("All tasks are completed !") return nil } s := make([]interface{}, len(childIDs)) @@ -1239,7 +1243,9 @@ func (ts *TasksRPC) updateParentTask(ctx context.Context, taskID, taskStatus, ta parentTask.TaskStatus = common.OK parentTask.PercentComplete = 100 parentTask.StatusCode = http.StatusOK - parentTask.TaskResponse = payLoad.ResponseBody + resp := tcommon.GetTaskResponse(http.StatusOK, response.Success) + body, _ := json.Marshal(resp.Body) + parentTask.TaskResponse = body ts.UpdateTaskQueue(parentTask) } @@ -1265,7 +1271,7 @@ func (ts *TasksRPC) ProcessTaskEvents(data interface{}) bool { // plugin IP, and plugin task ID pluginTask, err := tmodel.GetPluginTaskInfo(taskID) if err != nil { - l.Log.Error("error while processing task event", err.Error()) + l.Log.Error("error while processing task event :", err.Error()) return false }