diff --git a/CHANGELOG.md b/CHANGELOG.md index 1fec875..ba9cf95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,13 +9,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Added the machine id to the output of a pull request in the catalog +- Added the ability do do a catalog pull request without the need to specify the + local machine path, this will be taken from the user configuration in PD +- Added a spinner to the long running commands for pull and push to notify the + user that the command is still running +- Added a new endpoint to easily clone a virtual machine `/api/v1/machines/{id}/clone` +- Added the ability to **enable** and **disable** a host from the orchestrator +- Added the ability to configure the API **CORS policies** by passing environment variables + ### Fixed -### Changed +- Fixed an typo in the docker-compose file that would not allow the root password + to be updated +- Fixed an issue in the pull from the catalog where if there was an error the + system would crash +- Fixed an issue where the provider would not take into account the host with a + schema present +- Fixed a bug where the system would crash with a waiting group being negative +- Fixed a bug where queries could get stuck while saving to the database +- Fixed an issue where some credentials would be left behind in temporary files +- further security fixes to the codebase -### Deprecated +### Changed -### Removed +- Packer Templates and Vagrant box endpoints are now disabled by default due to security + concerns on remote execution of code, you can enable them by setting the environment + variable `ENABLE_PACKER_PLUGIN` and `ENABLE_VAGRANT_PLUGIN` to `true` ## [0.5.6] - 2024-03-14 diff --git a/docker-compose.yml b/docker-compose.yml index ebc1bae..66f3170 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,10 +1,8 @@ version: '3.9' -name: api +name: devops-service services: - api: + devops: build: . - ports: - - "80:80" environment: HMAC_SECRET: '' LOG_LEVEL: 'info' @@ -15,7 +13,7 @@ services: TLS_PRIVATE_KEY: '' API_PORT: '80' API_PREFIX: '/api' - ROOR_PASSWORD: '' + ROOT_PASSWORD: '' DISABLE_CATALOG_CACHING: 'false' TOKEN_DURATION_MINUTES: 60 MODE: api diff --git a/docs/docs/getting-started/configuration.md b/docs/docs/getting-started/configuration.md index 2effbb9..e8f1d92 100644 --- a/docs/docs/getting-started/configuration.md +++ b/docs/docs/getting-started/configuration.md @@ -56,6 +56,11 @@ The root object of the configuration file is the environment object, which conta | DISABLE_CATALOG_CACHING | Specifies whether the service should disable the catalog caching | false | | USE_ORCHESTRATOR_RESOURCES | Specifies whether the service is running in orchestrator mode, which allows the service to use the resources of the orchestrator | false | | ORCHESTRATOR_PULL_FREQUENCY_SECONDS | The frequency in seconds that the orchestrator will sync with the other hosts in seconds | 30 | +| CORS_ALLOWED_HEADERS | The headers that are allowed in the cors policy | "X-Requested-With, authorization, content-type" | +| CORS_ALLOWED_ORIGINS | The origins that are allowed in the cors policy | "*" | +| CORS_ALLOWED_METHODS | The methods that are allowed in the cors policy | "GET, HEAD, POST, PUT, DELETE, OPTIONS" | +| ENABLE_PACKER_PLUGIN | Specifies whether the service should enable the packer plugin | false | +| ENABLE_VAGRANT_PLUGIN | Specifies whether the service should enable the vagrant plugin | false | ### Json Web Tokens diff --git a/go.work.sum b/go.work.sum index 2d52dcf..a7ccd8a 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1,6 +1,7 @@ github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/briandowns/spinner v1.23.0/go.mod h1:rPG4gmXeN3wQV/TsAY4w8lPdIM6RX3yqeBQJSrbXjuE= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cjlapao/common-go-identity v0.0.3/go.mod h1:xuNepNCHVI/51Q6DQgNPYvx3HS0VaeEhGnp8YcDO/+I= diff --git a/src/basecontext/api_context.go b/src/basecontext/api_context.go index a0d0c56..3336551 100644 --- a/src/basecontext/api_context.go +++ b/src/basecontext/api_context.go @@ -19,4 +19,5 @@ type ApiContext interface { LogErrorf(format string, a ...interface{}) LogDebugf(format string, a ...interface{}) LogWarnf(format string, a ...interface{}) + LogTracef(format string, a ...interface{}) } diff --git a/src/basecontext/main.go b/src/basecontext/main.go index afc4d41..e257f9d 100644 --- a/src/basecontext/main.go +++ b/src/basecontext/main.go @@ -173,3 +173,17 @@ func (c *BaseContext) LogWarnf(format string, a ...interface{}) { msg += format common.Logger.Warn(msg, a...) } + +func (c *BaseContext) LogTracef(format string, a ...interface{}) { + // log is disabled, returning + if !c.shouldLog { + return + } + + msg := "" + if c.GetRequestId() != "" { + msg = "[" + c.GetRequestId() + "] " + } + msg += format + common.Logger.Trace(msg, a...) +} diff --git a/src/basecontext/test/mock_base_context.go b/src/basecontext/test/mock_base_context.go index 8fefec0..2a4c075 100644 --- a/src/basecontext/test/mock_base_context.go +++ b/src/basecontext/test/mock_base_context.go @@ -123,3 +123,10 @@ func (m *MockBaseContext) LogWarnf(format string, a ...interface{}) { m.callbackFunctions["LogWarnf"](value) } } + +func (m *MockBaseContext) LogTracef(format string, a ...interface{}) { + if m.callbackFunctions["LogTracef"] != nil { + value := fmt.Sprintf(format, a...) + m.callbackFunctions["LogTracef"](value) + } +} diff --git a/src/catalog/models/catalog_manifest_provider.go b/src/catalog/models/catalog_manifest_provider.go index 4e2891e..2859624 100644 --- a/src/catalog/models/catalog_manifest_provider.go +++ b/src/catalog/models/catalog_manifest_provider.go @@ -146,6 +146,15 @@ func (m *CatalogManifestProvider) Parse(connection string) error { } } + var schema string + if strings.HasPrefix(m.Host, "http://") || strings.HasPrefix(m.Host, "https://") { + schemaParts := strings.Split(m.Host, "://") + if len(schemaParts) == 2 { + schema = schemaParts[0] + m.Host = schemaParts[1] + } + } + if strings.ContainsAny(m.Host, "@") { parts := strings.Split(m.Host, "@") if len(parts) == 2 { @@ -175,6 +184,9 @@ func (m *CatalogManifestProvider) Parse(connection string) error { m.Host = parts[1] } } + if schema != "" { + m.Host = schema + "://" + m.Host + } } return nil diff --git a/src/catalog/models/pull_catalog_manifest.go b/src/catalog/models/pull_catalog_manifest.go index 4bb609a..dde9c44 100644 --- a/src/catalog/models/pull_catalog_manifest.go +++ b/src/catalog/models/pull_catalog_manifest.go @@ -1,11 +1,14 @@ package models import ( + "fmt" + "github.com/Parallels/prl-devops-service/basecontext" "github.com/Parallels/prl-devops-service/catalog/cleanupservice" "github.com/Parallels/prl-devops-service/config" "github.com/Parallels/prl-devops-service/constants" "github.com/Parallels/prl-devops-service/errors" + "github.com/Parallels/prl-devops-service/serviceprovider" "github.com/Parallels/prl-devops-service/serviceprovider/system" ) @@ -31,9 +34,6 @@ type PullCatalogManifestRequest struct { } func (r *PullCatalogManifestRequest) Validate() error { - if r.Path == "" { - return ErrPullMissingPath - } if r.CatalogId == "" { return ErrPullMissingCatalogId } @@ -60,17 +60,30 @@ func (r *PullCatalogManifestRequest) Validate() error { r.Owner = cfg.GetKey(constants.CURRENT_USER_ENV_VAR) } + if r.Path == "" { + prl := serviceprovider.Get().ParallelsDesktopService + if prl == nil { + return errors.New("Local Path is required and we are unable to determine it without Parallels Desktop Service") + } + userPath, err := prl.GetUserHome(ctx, r.Owner) + if err != nil { + return fmt.Errorf("unable to determine user %v home for path", r.Owner) + } + r.Path = userPath + } + return nil } type PullCatalogManifestResponse struct { ID string `json:"id"` - CatalogId string `json:"catalog_id"` - Version string `json:"version"` - Architecture string `json:"architecture"` - LocalPath string `json:"local_path"` - MachineName string `json:"machine_name"` - Manifest *VirtualMachineCatalogManifest `json:"manifest"` + MachineID string `json:"machine_id,omitempty"` + CatalogId string `json:"catalog_id,omitempty"` + Version string `json:"version,omitempty"` + Architecture string `json:"architecture,omitempty"` + LocalPath string `json:"local_path,omitempty"` + MachineName string `json:"machine_name,omitempty"` + Manifest *VirtualMachineCatalogManifest `json:"manifest,omitempty"` CleanupRequest *cleanupservice.CleanupRequest `json:"-"` Errors []error `json:"-"` } diff --git a/src/catalog/providers/aws_s3_bucket/main.go b/src/catalog/providers/aws_s3_bucket/main.go index 61b07ed..6cd3939 100644 --- a/src/catalog/providers/aws_s3_bucket/main.go +++ b/src/catalog/providers/aws_s3_bucket/main.go @@ -6,9 +6,11 @@ import ( "os" "path/filepath" "strings" + "time" "github.com/Parallels/prl-devops-service/basecontext" "github.com/Parallels/prl-devops-service/catalog/common" + "github.com/briandowns/spinner" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/credentials" @@ -137,6 +139,10 @@ func (s *AwsS3BucketProvider) PullFile(ctx basecontext.ApiContext, path string, remoteFilePath := strings.TrimPrefix(filepath.Join(path, filename), "/") destinationFilePath := filepath.Join(destination, filename) + loader := spinner.New(spinner.CharSets[9], 10*time.Second) + loader.Prefix = "Downloading file " + loader.Start() + // Create a new session using the default region and credentials. var err error session, err := s.createSession() @@ -164,6 +170,8 @@ func (s *AwsS3BucketProvider) PullFile(ctx basecontext.ApiContext, path string, return err } + loader.Stop() + return nil } diff --git a/src/catalog/pull.go b/src/catalog/pull.go index 8662a22..05d8d0a 100644 --- a/src/catalog/pull.go +++ b/src/catalog/pull.go @@ -239,11 +239,13 @@ func (s *CatalogManifestService) Pull(ctx basecontext.ApiContext, r *models.Pull cacheFileName := fmt.Sprintf("%s.pdpack", fileChecksum) needsPulling := false + // checking for the caching system to see if we need to pull the file if cfg.IsCatalogCachingEnable() { destinationFolder, err = cfg.CatalogCacheFolder() if err != nil { destinationFolder = r.Path } + if helper.FileExists(filepath.Join(destinationFolder, cacheFileName)) { ctx.LogInfof("File %v already exists in cache", fileName) } else { @@ -353,7 +355,7 @@ func (s *CatalogManifestService) renameMachineWithParallelsDesktop(ctx baseconte if !response.HasErrors() { ctx.LogInfof("Renaming machine %v to %v", r.MachineName, r.MachineName) - filter := fmt.Sprintf("home=%s", r.LocalMachineFolder) + filter := fmt.Sprintf("name=%s", r.MachineName) vms, err := parallelsDesktopSvc.GetVms(ctx, filter) if err != nil { ctx.LogErrorf("Error getting machine %v: %v", r.MachineName, err) @@ -363,25 +365,31 @@ func (s *CatalogManifestService) renameMachineWithParallelsDesktop(ctx baseconte } if len(vms) != 1 { - ctx.LogErrorf("Error getting machine %v: %v", r.MachineName, err) - response.AddError(err) + notFoundError := errors.Newf("Machine %v not found", r.MachineName) + ctx.LogErrorf("Error getting machine %v: %v", r.MachineName, notFoundError) + response.AddError(notFoundError) response.CleanupRequest.AddLocalFileCleanupOperation(r.LocalMachineFolder, true) return } - response.ID = vms[0].ID - renameRequest := api_models.RenameVirtualMachineRequest{ - ID: vms[0].ID, - CurrentName: vms[0].Name, - NewName: r.MachineName, - } + // Renaming only if the name is different + if vms[0].Name != r.MachineName { + response.ID = vms[0].ID + renameRequest := api_models.RenameVirtualMachineRequest{ + ID: vms[0].ID, + CurrentName: vms[0].Name, + NewName: r.MachineName, + } - if err := parallelsDesktopSvc.RenameVm(ctx, renameRequest); err != nil { - ctx.LogErrorf("Error renaming machine %v: %v", r.MachineName, err) - response.AddError(err) - response.CleanupRequest.AddLocalFileCleanupOperation(r.LocalMachineFolder, true) - return + if err := parallelsDesktopSvc.RenameVm(ctx, renameRequest); err != nil { + ctx.LogErrorf("Error renaming machine %v: %v", r.MachineName, err) + response.AddError(err) + response.CleanupRequest.AddLocalFileCleanupOperation(r.LocalMachineFolder, true) + return + } } + + response.MachineID = vms[0].ID } else { ctx.LogErrorf("Error renaming machine %v: %v", r.MachineName, response.Errors) } @@ -394,7 +402,7 @@ func (s *CatalogManifestService) startMachineWithParallelsDesktop(ctx basecontex if !response.HasErrors() { ctx.LogInfof("Starting machine %v to %v", r.MachineName, r.MachineName) - filter := fmt.Sprintf("home=%s", r.LocalMachineFolder) + filter := fmt.Sprintf("name=%s", r.MachineName) vms, err := parallelsDesktopSvc.GetVms(ctx, filter) if err != nil { ctx.LogErrorf("Error getting machine %v: %v", r.MachineName, err) @@ -404,8 +412,9 @@ func (s *CatalogManifestService) startMachineWithParallelsDesktop(ctx basecontex } if len(vms) != 1 { - ctx.LogErrorf("Error getting machine %v: %v", r.MachineName, err) - response.AddError(err) + notFoundError := errors.Newf("Machine %v not found", r.MachineName) + ctx.LogErrorf("Error getting machine %v: %v", r.MachineName, notFoundError) + response.AddError(notFoundError) response.CleanupRequest.AddLocalFileCleanupOperation(r.LocalMachineFolder, true) return } diff --git a/src/cmd/catalog.go b/src/cmd/catalog.go index 1130d75..54e6b45 100644 --- a/src/cmd/catalog.go +++ b/src/cmd/catalog.go @@ -3,6 +3,7 @@ package cmd import ( "fmt" "os" + "time" "github.com/Parallels/prl-devops-service/basecontext" "github.com/Parallels/prl-devops-service/constants" @@ -10,6 +11,7 @@ import ( "github.com/Parallels/prl-devops-service/pdfile/diagnostics" "github.com/Parallels/prl-devops-service/pdfile/models" "github.com/Parallels/prl-devops-service/serviceprovider/system" + "github.com/briandowns/spinner" "github.com/cjlapao/common-go/helper" ) @@ -41,10 +43,10 @@ func processCatalog(ctx basecontext.ApiContext, operation string, filePath strin case "list": processCatalogListCmd(ctx, filePath) case "push": - fmt.Println("Starting push...") + fmt.Println("Starting push, this can take a while...") processCatalogPushCmd(ctx, filePath) case "pull": - fmt.Println("Starting pull...") + fmt.Println("Starting pull, this can take a while...") processCatalogPullCmd(ctx, filePath) case "delete": fmt.Println("Not implemented yet") @@ -208,7 +210,13 @@ func processCatalogListCmd(ctx basecontext.ApiContext, filepath string) { func processCatalogPushCmd(ctx basecontext.ApiContext, filePath string) { svc := catalogInitPdFile(ctx, "push", filePath) + s := spinner.New(spinner.CharSets[9], 500*time.Millisecond) + s.Start() + time.Sleep(4 * time.Second) + out, diags := svc.Run(ctx) + + s.Stop() if diags.HasErrors() { for _, err := range diags.Errors() { fmt.Println(err) @@ -216,6 +224,9 @@ func processCatalogPushCmd(ctx basecontext.ApiContext, filePath string) { os.Exit(1) } + // Stop the progress bar by printing a new line + fmt.Println() + ctx.LogInfof("%v", out) } diff --git a/src/constants/main.go b/src/constants/main.go index 902d3db..b23e46b 100644 --- a/src/constants/main.go +++ b/src/constants/main.go @@ -58,6 +58,11 @@ const ( ORCHESTRATOR_PULL_FREQUENCY_SECONDS_ENV_VAR = "ORCHESTRATOR_PULL_FREQUENCY_SECONDS" DATABASE_FOLDER_ENV_VAR = "DATABASE_FOLDER" CATALOG_CACHE_FOLDER_ENV_VAR = "CATALOG_CACHE_FOLDER" + CORS_ALLOWED_HEADERS_ENV_VAR = "CORS_ALLOWED_HEADERS" + CORS_ALLOWED_METHODS_ENV_VAR = "CORS_ALLOWED_METHODS" + CORS_ALLOWED_ORIGINS_ENV_VAR = "CORS_ALLOWED_ORIGINS" + ENABLE_PACKER_PLUGIN_ENV_VAR = "ENABLE_PACKER_PLUGIN" + ENABLE_VAGRANT_PLUGIN_ENV_VAR = "ENABLE_VAGRANT_PLUGIN" ) const ( diff --git a/src/controllers/api_key.go b/src/controllers/api_key.go index 8c4af1e..4cef1a6 100644 --- a/src/controllers/api_key.go +++ b/src/controllers/api_key.go @@ -71,6 +71,7 @@ func registerApiKeysHandlers(ctx basecontext.ApiContext, version string) { func GetApiKeysHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) + defer Recover(ctx, r, w) dbService, err := serviceprovider.GetDatabaseService(ctx) if err != nil { ReturnApiError(ctx, w, models.NewFromErrorWithCode(err, http.StatusInternalServerError)) @@ -105,6 +106,7 @@ func GetApiKeysHandler() restapi.ControllerHandler { func DeleteApiKeyHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) + defer Recover(ctx, r, w) dbService, err := serviceprovider.GetDatabaseService(ctx) if err != nil { ReturnApiError(ctx, w, models.NewFromErrorWithCode(err, http.StatusInternalServerError)) @@ -139,6 +141,7 @@ func DeleteApiKeyHandler() restapi.ControllerHandler { func GetApiKeyHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) + defer Recover(ctx, r, w) dbService, err := serviceprovider.GetDatabaseService(ctx) if err != nil { ReturnApiError(ctx, w, models.NewFromErrorWithCode(err, http.StatusInternalServerError)) @@ -176,6 +179,7 @@ func GetApiKeyHandler() restapi.ControllerHandler { func CreateApiKeyHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) + defer Recover(ctx, r, w) var request models.ApiKeyRequest if err := http_helper.MapRequestBody(r, &request); err != nil { ReturnApiError(ctx, w, models.ApiErrorResponse{ @@ -232,6 +236,7 @@ func CreateApiKeyHandler() restapi.ControllerHandler { func RevokeApiKeyHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) + defer Recover(ctx, r, w) dbService, err := serviceprovider.GetDatabaseService(ctx) if err != nil { ReturnApiError(ctx, w, models.NewFromErrorWithCode(err, http.StatusInternalServerError)) diff --git a/src/controllers/authorization.go b/src/controllers/authorization.go index 7aefb5e..cb4093d 100644 --- a/src/controllers/authorization.go +++ b/src/controllers/authorization.go @@ -44,6 +44,7 @@ func registerAuthorizationHandlers(ctx basecontext.ApiContext, version string) { func GetTokenHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) + defer Recover(ctx, r, w) var request models.LoginRequest if err := http_helper.MapRequestBody(r, &request); err != nil { @@ -159,6 +160,7 @@ func GetTokenHandler() restapi.ControllerHandler { func ValidateTokenHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) + defer Recover(ctx, r, w) var request models.ValidateTokenRequest if err := http_helper.MapRequestBody(r, &request); err != nil { diff --git a/src/controllers/catalog.go b/src/controllers/catalog.go index 39638d9..2565432 100644 --- a/src/controllers/catalog.go +++ b/src/controllers/catalog.go @@ -153,6 +153,7 @@ func registerCatalogManifestHandlers(ctx basecontext.ApiContext, version string) func GetCatalogManifestsHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) + defer Recover(ctx, r, w) dbService, err := serviceprovider.GetDatabaseService(ctx) if err != nil { ReturnApiError(ctx, w, models.NewFromErrorWithCode(err, http.StatusInternalServerError)) @@ -216,6 +217,7 @@ func GetCatalogManifestsHandler() restapi.ControllerHandler { func GetCatalogManifestHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) + defer Recover(ctx, r, w) dbService, err := serviceprovider.GetDatabaseService(ctx) if err != nil { ReturnApiError(ctx, w, models.NewFromErrorWithCode(err, http.StatusInternalServerError)) @@ -261,6 +263,7 @@ func GetCatalogManifestHandler() restapi.ControllerHandler { func GetCatalogManifestVersionHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) + defer Recover(ctx, r, w) dbService, err := serviceprovider.GetDatabaseService(ctx) if err != nil { ReturnApiError(ctx, w, models.NewFromErrorWithCode(err, http.StatusInternalServerError)) @@ -301,6 +304,7 @@ func GetCatalogManifestVersionHandler() restapi.ControllerHandler { func GetCatalogManifestVersionArchitectureHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) + defer Recover(ctx, r, w) dbService, err := serviceprovider.GetDatabaseService(ctx) if err != nil { ReturnApiError(ctx, w, models.NewFromErrorWithCode(err, http.StatusInternalServerError)) @@ -342,6 +346,7 @@ func GetCatalogManifestVersionArchitectureHandler() restapi.ControllerHandler { func DownloadCatalogManifestVersionHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) + defer Recover(ctx, r, w) dbService, err := serviceprovider.GetDatabaseService(ctx) if err != nil { ReturnApiError(ctx, w, models.NewFromErrorWithCode(err, http.StatusInternalServerError)) @@ -404,6 +409,7 @@ func DownloadCatalogManifestVersionHandler() restapi.ControllerHandler { func TaintCatalogManifestVersionHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) + defer Recover(ctx, r, w) dbService, err := serviceprovider.GetDatabaseService(ctx) if err != nil { ReturnApiError(ctx, w, models.NewFromErrorWithCode(err, http.StatusInternalServerError)) @@ -467,6 +473,7 @@ func TaintCatalogManifestVersionHandler() restapi.ControllerHandler { func UnTaintCatalogManifestVersionHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) + defer Recover(ctx, r, w) dbService, err := serviceprovider.GetDatabaseService(ctx) if err != nil { ReturnApiError(ctx, w, models.NewFromErrorWithCode(err, http.StatusInternalServerError)) @@ -530,6 +537,7 @@ func UnTaintCatalogManifestVersionHandler() restapi.ControllerHandler { func RevokeCatalogManifestVersionHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) + defer Recover(ctx, r, w) dbService, err := serviceprovider.GetDatabaseService(ctx) if err != nil { ReturnApiError(ctx, w, models.NewFromErrorWithCode(err, http.StatusInternalServerError)) @@ -572,6 +580,7 @@ func RevokeCatalogManifestVersionHandler() restapi.ControllerHandler { func CreateCatalogManifestHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) + defer Recover(ctx, r, w) var request catalog_models.VirtualMachineCatalogManifest if err := http_helper.MapRequestBody(r, &request); err != nil { ReturnApiError(ctx, w, models.ApiErrorResponse{ @@ -623,6 +632,7 @@ func CreateCatalogManifestHandler() restapi.ControllerHandler { func DeleteCatalogManifestHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) + defer Recover(ctx, r, w) dbService, err := serviceprovider.GetDatabaseService(ctx) if err != nil { ReturnApiError(ctx, w, models.NewFromErrorWithCode(err, http.StatusInternalServerError)) @@ -670,6 +680,7 @@ func DeleteCatalogManifestHandler() restapi.ControllerHandler { func DeleteCatalogManifestVersionHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) + defer Recover(ctx, r, w) dbService, err := serviceprovider.GetDatabaseService(ctx) if err != nil { ReturnApiError(ctx, w, models.NewFromErrorWithCode(err, http.StatusInternalServerError)) @@ -719,6 +730,7 @@ func DeleteCatalogManifestVersionHandler() restapi.ControllerHandler { func DeleteCatalogManifestVersionArchitectureHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) + defer Recover(ctx, r, w) dbService, err := serviceprovider.GetDatabaseService(ctx) if err != nil { ReturnApiError(ctx, w, models.NewFromErrorWithCode(err, http.StatusInternalServerError)) @@ -767,6 +779,7 @@ func DeleteCatalogManifestVersionArchitectureHandler() restapi.ControllerHandler func PushCatalogManifestHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) + defer Recover(ctx, r, w) var request catalog_models.PushCatalogManifestRequest if err := http_helper.MapRequestBody(r, &request); err != nil { ReturnApiError(ctx, w, models.ApiErrorResponse{ @@ -819,6 +832,7 @@ func PushCatalogManifestHandler() restapi.ControllerHandler { func PullCatalogManifestHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) + defer Recover(ctx, r, w) var request catalog_models.PullCatalogManifestRequest if err := http_helper.MapRequestBody(r, &request); err != nil { ReturnApiError(ctx, w, models.ApiErrorResponse{ @@ -871,6 +885,7 @@ func PullCatalogManifestHandler() restapi.ControllerHandler { func ImportCatalogManifestHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) + defer Recover(ctx, r, w) var request catalog_models.ImportCatalogManifestRequest if err := http_helper.MapRequestBody(r, &request); err != nil { ReturnApiError(ctx, w, models.ApiErrorResponse{ diff --git a/src/controllers/claims.go b/src/controllers/claims.go index c8e7a57..cf28719 100644 --- a/src/controllers/claims.go +++ b/src/controllers/claims.go @@ -64,6 +64,7 @@ func registerClaimsHandlers(ctx basecontext.ApiContext, version string) { func GetClaimsHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) + defer Recover(ctx, r, w) dbService, err := serviceprovider.GetDatabaseService(ctx) if err != nil { ReturnApiError(ctx, w, models.NewFromErrorWithCode(err, http.StatusInternalServerError)) @@ -106,6 +107,7 @@ func GetClaimsHandler() restapi.ControllerHandler { func GetClaimHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) + defer Recover(ctx, r, w) dbService, err := serviceprovider.GetDatabaseService(ctx) if err != nil { ReturnApiError(ctx, w, models.NewFromErrorWithCode(err, http.StatusInternalServerError)) @@ -143,6 +145,7 @@ func GetClaimHandler() restapi.ControllerHandler { func CreateClaimHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) + defer Recover(ctx, r, w) var request models.ClaimRequest if err := http_helper.MapRequestBody(r, &request); err != nil { ReturnApiError(ctx, w, models.ApiErrorResponse{ @@ -194,6 +197,7 @@ func CreateClaimHandler() restapi.ControllerHandler { func DeleteClaimHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) + defer Recover(ctx, r, w) dbService, err := serviceprovider.GetDatabaseService(ctx) if err != nil { ReturnApiError(ctx, w, models.NewFromErrorWithCode(err, http.StatusInternalServerError)) diff --git a/src/controllers/common.go b/src/controllers/common.go index ce692db..ec8e82e 100644 --- a/src/controllers/common.go +++ b/src/controllers/common.go @@ -2,6 +2,7 @@ package controllers import ( "encoding/json" + "fmt" "net/http" "github.com/Parallels/prl-devops-service/basecontext" @@ -13,7 +14,18 @@ func GetFilterHeader(r *http.Request) string { } func GetBaseContext(r *http.Request) *basecontext.BaseContext { - return basecontext.NewBaseContextFromRequest(r) + ctx := basecontext.NewBaseContextFromRequest(r) + + return ctx +} + +func Recover(ctx basecontext.ApiContext, r *http.Request, w http.ResponseWriter) { + if err := recover(); err != nil { + ctx.LogErrorf("Recovered from panic: %v", err) + ReturnApiError(ctx, w, models.NewFromErrorWithCode(fmt.Errorf("Internal Server Error"), http.StatusInternalServerError)) + + fmt.Printf("Recovered from panic: %v", err) + } } func ReturnApiError(ctx basecontext.ApiContext, w http.ResponseWriter, err models.ApiErrorResponse) { diff --git a/src/controllers/config.go b/src/controllers/config.go index e5b6d78..0b9dfda 100644 --- a/src/controllers/config.go +++ b/src/controllers/config.go @@ -78,6 +78,7 @@ func registerConfigHandlers(ctx basecontext.ApiContext, version string) { func GetParallelsDesktopLicenseHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) + defer Recover(ctx, r, w) provider := serviceprovider.Get() if provider.ParallelsDesktopService == nil || !provider.ParallelsDesktopService.Installed() { ReturnApiError(ctx, w, models.ApiErrorResponse{ @@ -113,6 +114,7 @@ func GetParallelsDesktopLicenseHandler() restapi.ControllerHandler { func Install3rdPartyToolsHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) + defer Recover(ctx, r, w) var request models.InstallToolsRequest if err := http_helper.MapRequestBody(r, &request); err != nil { ReturnApiError(ctx, w, models.ApiErrorResponse{ @@ -174,6 +176,7 @@ func Install3rdPartyToolsHandler() restapi.ControllerHandler { func Uninstall3rdPartyToolsHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) + defer Recover(ctx, r, w) var request models.UninstallToolsRequest if err := http_helper.MapRequestBody(r, &request); err != nil { ReturnApiError(ctx, w, models.ApiErrorResponse{ @@ -231,6 +234,7 @@ func Uninstall3rdPartyToolsHandler() restapi.ControllerHandler { func RestartApiHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) + defer Recover(ctx, r, w) go restapi.Get().Restart() w.WriteHeader(http.StatusAccepted) ctx.LogInfof("Restart request accepted") @@ -250,6 +254,7 @@ func RestartApiHandler() restapi.ControllerHandler { func GetHardwareInfo() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) + defer Recover(ctx, r, w) provider := serviceprovider.Get() hardwareInfo, err := provider.ParallelsDesktopService.GetHardwareUsage(ctx) if err != nil { diff --git a/src/controllers/machines.go b/src/controllers/machines.go index 92bcbc8..44c5812 100644 --- a/src/controllers/machines.go +++ b/src/controllers/machines.go @@ -7,6 +7,7 @@ import ( "github.com/Parallels/prl-devops-service/basecontext" "github.com/Parallels/prl-devops-service/catalog" + "github.com/Parallels/prl-devops-service/config" "github.com/Parallels/prl-devops-service/constants" "github.com/Parallels/prl-devops-service/errors" "github.com/Parallels/prl-devops-service/mappers" @@ -160,6 +161,14 @@ func registerVirtualMachinesHandlers(ctx basecontext.ApiContext, version string) WithRequiredClaim(constants.UPDATE_VM_CLAIM). WithHandler(RenameVirtualMachineHandler()). Register() + + restapi.NewController(). + WithMethod(restapi.PUT). + WithVersion(version). + WithPath("/machines/{id}/clone"). + WithRequiredClaim(constants.CREATE_VM_CLAIM). + WithHandler(CloneVirtualMachineHandler()). + Register() } } @@ -177,6 +186,7 @@ func registerVirtualMachinesHandlers(ctx basecontext.ApiContext, version string) func GetVirtualMachinesHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) + defer Recover(ctx, r, w) provider := serviceprovider.Get() svc := provider.ParallelsDesktopService @@ -214,6 +224,7 @@ func GetVirtualMachinesHandler() restapi.ControllerHandler { func GetVirtualMachineHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) + defer Recover(ctx, r, w) provider := serviceprovider.Get() svc := provider.ParallelsDesktopService @@ -254,6 +265,7 @@ func GetVirtualMachineHandler() restapi.ControllerHandler { func StartVirtualMachineHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) + defer Recover(ctx, r, w) provider := serviceprovider.Get() svc := provider.ParallelsDesktopService @@ -292,6 +304,7 @@ func StartVirtualMachineHandler() restapi.ControllerHandler { func StopVirtualMachineHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) + defer Recover(ctx, r, w) provider := serviceprovider.Get() svc := provider.ParallelsDesktopService @@ -330,6 +343,7 @@ func StopVirtualMachineHandler() restapi.ControllerHandler { func RestartVirtualMachineHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) + defer Recover(ctx, r, w) provider := serviceprovider.Get() svc := provider.ParallelsDesktopService @@ -367,6 +381,7 @@ func RestartVirtualMachineHandler() restapi.ControllerHandler { func SuspendVirtualMachineHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) + defer Recover(ctx, r, w) provider := serviceprovider.Get() svc := provider.ParallelsDesktopService @@ -404,6 +419,7 @@ func SuspendVirtualMachineHandler() restapi.ControllerHandler { func ResumeMachineController() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) + defer Recover(ctx, r, w) provider := serviceprovider.Get() svc := provider.ParallelsDesktopService @@ -441,6 +457,7 @@ func ResumeMachineController() restapi.ControllerHandler { func ResetMachineController() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) + defer Recover(ctx, r, w) provider := serviceprovider.Get() svc := provider.ParallelsDesktopService @@ -478,6 +495,7 @@ func ResetMachineController() restapi.ControllerHandler { func PauseVirtualMachineHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) + defer Recover(ctx, r, w) provider := serviceprovider.Get() svc := provider.ParallelsDesktopService @@ -516,6 +534,7 @@ func PauseVirtualMachineHandler() restapi.ControllerHandler { func DeleteVirtualMachineHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) + defer Recover(ctx, r, w) provider := serviceprovider.Get() svc := provider.ParallelsDesktopService @@ -547,6 +566,7 @@ func DeleteVirtualMachineHandler() restapi.ControllerHandler { func GetVirtualMachineStatusHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) + defer Recover(ctx, r, w) provider := serviceprovider.Get() svc := provider.ParallelsDesktopService @@ -585,6 +605,7 @@ func GetVirtualMachineStatusHandler() restapi.ControllerHandler { func SetVirtualMachineHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) + defer Recover(ctx, r, w) var request models.VirtualMachineConfigRequest provider := serviceprovider.Get() svc := provider.ParallelsDesktopService @@ -636,6 +657,79 @@ func SetVirtualMachineHandler() restapi.ControllerHandler { } } +// @Summary Clones a virtual machine +// @Description This endpoint clones a virtual machine +// @Tags Machines +// @Produce json +// @Param id path string true "Machine ID" +// @Param configRequest body models.VirtualMachineCloneCommandRequest true "Machine Clone Request" +// @Success 200 {object} models.VirtualMachineCloneCommandResponse +// @Failure 400 {object} models.ApiErrorResponse +// @Failure 401 {object} models.OAuthErrorResponse +// @Security ApiKeyAuth +// @Security BearerAuth +// @Router /v1/machines/{id}/clone [put] +func CloneVirtualMachineHandler() restapi.ControllerHandler { + return func(w http.ResponseWriter, r *http.Request) { + ctx := GetBaseContext(r) + defer Recover(ctx, r, w) + var request models.VirtualMachineCloneCommandRequest + provider := serviceprovider.Get() + svc := provider.ParallelsDesktopService + + if err := http_helper.MapRequestBody(r, &request); err != nil { + ReturnApiError(ctx, w, models.ApiErrorResponse{ + Message: "Invalid request body: " + err.Error(), + Code: http.StatusBadRequest, + }) + } + if err := request.Validate(); err != nil { + ReturnApiError(ctx, w, models.ApiErrorResponse{ + Message: "Invalid request body: " + err.Error(), + Code: http.StatusBadRequest, + }) + return + } + + params := mux.Vars(r) + id := params["id"] + configure := models.VirtualMachineConfigRequest{ + Operations: []*models.VirtualMachineConfigRequestOperation{ + { + Group: "machine", + Operation: "clone", + Options: []*models.VirtualMachineConfigRequestOperationOption{ + { + Flag: "name", + Value: request.CloneName, + }, + }, + }, + }, + } + + if err := svc.ConfigureVm(ctx, id, &configure); err != nil { + ReturnApiError(ctx, w, models.NewFromError(err)) + return + } + + result := models.VirtualMachineCloneCommandResponse{} + + vmId, err := svc.GetVm(ctx, request.CloneName) + if err != nil { + ReturnApiError(ctx, w, models.NewFromError(err)) + return + } + + result.Id = vmId.ID + result.Status = "Success" + + w.WriteHeader(http.StatusOK) + _ = json.NewEncoder(w).Encode(result) + ctx.LogInfof("Machine %v cloned successfully to %v with id %v", id, request.CloneName, result.Id) + } +} + // @Summary Executes a command on a virtual machine // @Description This endpoint executes a command on a virtual machine // @Tags Machines @@ -651,6 +745,7 @@ func SetVirtualMachineHandler() restapi.ControllerHandler { func ExecuteCommandOnVirtualMachineHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) + defer Recover(ctx, r, w) var request models.VirtualMachineExecuteCommandRequest provider := serviceprovider.Get() svc := provider.ParallelsDesktopService @@ -697,6 +792,7 @@ func ExecuteCommandOnVirtualMachineHandler() restapi.ControllerHandler { func RenameVirtualMachineHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) + defer Recover(ctx, r, w) var request models.RenameVirtualMachineRequest provider := serviceprovider.Get() svc := provider.ParallelsDesktopService @@ -759,6 +855,7 @@ func RenameVirtualMachineHandler() restapi.ControllerHandler { func RegisterVirtualMachineHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) + defer Recover(ctx, r, w) var request models.RegisterVirtualMachineRequest provider := serviceprovider.Get() svc := provider.ParallelsDesktopService @@ -838,6 +935,7 @@ func RegisterVirtualMachineHandler() restapi.ControllerHandler { func UnregisterVirtualMachineHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) + defer Recover(ctx, r, w) var request models.UnregisterVirtualMachineRequest provider := serviceprovider.Get() svc := provider.ParallelsDesktopService @@ -883,6 +981,7 @@ func UnregisterVirtualMachineHandler() restapi.ControllerHandler { func CreateVirtualMachineHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) + defer Recover(ctx, r, w) var request models.CreateVirtualMachineRequest if err := http_helper.MapRequestBody(r, &request); err != nil { @@ -960,6 +1059,11 @@ func CreateVirtualMachineHandler() restapi.ControllerHandler { func createPackerTemplate(ctx basecontext.ApiContext, request models.CreateVirtualMachineRequest) (*models.CreateVirtualMachineResponse, error) { provider := serviceprovider.Get() + config := config.Get() + if !config.GetBoolKey(constants.ENABLE_VAGRANT_PLUGIN_ENV_VAR) { + return nil, errors.NewWithCode("Vagrant plugin is not enabled, please enable it before trying", 400) + } + parallelsDesktopService := provider.ParallelsDesktopService dbService, err := serviceprovider.GetDatabaseService(ctx) @@ -1004,6 +1108,10 @@ func createPackerTemplate(ctx basecontext.ApiContext, request models.CreateVirtu func createVagrantBox(ctx basecontext.ApiContext, request models.CreateVirtualMachineRequest) (*models.CreateVirtualMachineResponse, error) { provider := serviceprovider.Get() + config := config.Get() + if !config.GetBoolKey(constants.ENABLE_VAGRANT_PLUGIN_ENV_VAR) { + return nil, errors.NewWithCode("Vagrant plugin is not enabled, please enable it before trying", 400) + } vagrantService := provider.VagrantService parallelsDesktopService := provider.ParallelsDesktopService @@ -1148,11 +1256,11 @@ func createCatalogMachine(ctx basecontext.ApiContext, request models.CreateVirtu response = models.CreateVirtualMachineResponse{ Name: resultData.MachineName, - ID: resultData.ID, + ID: resultData.MachineID, Owner: request.Owner, } - vm, err := parallelsDesktopService.GetVm(ctx, resultData.ID) + vm, err := parallelsDesktopService.GetVm(ctx, response.ID) if err != nil { return nil, err } diff --git a/src/controllers/main.go b/src/controllers/main.go index 50244b5..8a3e7ff 100644 --- a/src/controllers/main.go +++ b/src/controllers/main.go @@ -16,6 +16,7 @@ func RegisterV1Handlers(ctx basecontext.ApiContext) error { registerVirtualMachinesHandlers(ctx, version) registerConfigHandlers(ctx, version) registerOrchestratorHostsHandlers(ctx, version) + registerPerformanceHandlers(ctx, version) return nil } diff --git a/src/controllers/orchestrator.go b/src/controllers/orchestrator.go index 0a3458d..d215145 100644 --- a/src/controllers/orchestrator.go +++ b/src/controllers/orchestrator.go @@ -2,28 +2,21 @@ package controllers import ( "encoding/json" - "errors" "net/http" - "strings" - "sync" - "time" "github.com/Parallels/prl-devops-service/basecontext" "github.com/Parallels/prl-devops-service/constants" - data_models "github.com/Parallels/prl-devops-service/data/models" - "github.com/Parallels/prl-devops-service/helpers" "github.com/Parallels/prl-devops-service/mappers" "github.com/Parallels/prl-devops-service/models" "github.com/Parallels/prl-devops-service/orchestrator" "github.com/Parallels/prl-devops-service/restapi" - "github.com/Parallels/prl-devops-service/serviceprovider" "github.com/cjlapao/common-go/helper/http_helper" "github.com/gorilla/mux" ) func registerOrchestratorHostsHandlers(ctx basecontext.ApiContext, version string) { - ctx.LogInfof("Registering version %s Claims handlers", version) + ctx.LogInfof("Registering version %s Orchestrator handlers", version) restapi.NewController(). WithMethod(restapi.GET). WithVersion(version).WithPath("orchestrator/hosts"). @@ -44,7 +37,7 @@ func registerOrchestratorHostsHandlers(ctx basecontext.ApiContext, version strin WithVersion(version). WithPath("/orchestrator/hosts"). WithRequiredClaim(constants.CREATE_CLAIM). - WithHandler(CreateOrchestratorHostHandler()). + WithHandler(RegisterOrchestratorHostHandler()). Register() restapi.NewController(). @@ -52,7 +45,23 @@ func registerOrchestratorHostsHandlers(ctx basecontext.ApiContext, version strin WithVersion(version). WithPath("/orchestrator/hosts/{id}"). WithRequiredClaim(constants.DELETE_CLAIM). - WithHandler(DeleteOrchestratorHostHandler()). + WithHandler(UnregisterOrchestratorHostHandler()). + Register() + + restapi.NewController(). + WithMethod(restapi.PUT). + WithVersion(version). + WithPath("/orchestrator/hosts/{id}/enable"). + WithRequiredClaim(constants.UPDATE_CLAIM). + WithHandler(EnableOrchestratorHostsHandler()). + Register() + + restapi.NewController(). + WithMethod(restapi.PUT). + WithVersion(version). + WithPath("/orchestrator/hosts/{id}/disable"). + WithRequiredClaim(constants.UPDATE_CLAIM). + WithHandler(DisableOrchestratorHostsHandler()). Register() restapi.NewController(). @@ -79,6 +88,54 @@ func registerOrchestratorHostsHandlers(ctx basecontext.ApiContext, version strin WithHandler(GetOrchestratorVirtualMachinesHandler()). Register() + restapi.NewController(). + WithMethod(restapi.GET). + WithVersion(version). + WithPath("/orchestrator/machines/{id}"). + WithRequiredClaim(constants.LIST_CLAIM). + WithHandler(GetOrchestratorVirtualMachineHandler()). + Register() + + restapi.NewController(). + WithMethod(restapi.DELETE). + WithVersion(version). + WithPath("/orchestrator/machines/{id}"). + WithRequiredClaim(constants.DELETE_CLAIM). + WithHandler(DeleteOrchestratorVirtualMachineHandler()). + Register() + + restapi.NewController(). + WithMethod(restapi.GET). + WithVersion(version). + WithPath("/orchestrator/machines/{id}/status"). + WithRequiredClaim(constants.LIST_CLAIM). + WithHandler(GetOrchestratorVirtualMachineStatusHandler()). + Register() + + restapi.NewController(). + WithMethod(restapi.PUT). + WithVersion(version). + WithPath("/orchestrator/machines/{id}/rename"). + WithRequiredClaim(constants.UPDATE_CLAIM). + WithHandler(RenameOrchestratorVirtualMachineHandler()). + Register() + + restapi.NewController(). + WithMethod(restapi.PUT). + WithVersion(version). + WithPath("/orchestrator/machines/{id}/set"). + WithRequiredClaim(constants.UPDATE_CLAIM). + WithHandler(SetOrchestratorVirtualMachineHandler()). + Register() + + restapi.NewController(). + WithMethod(restapi.PUT). + WithVersion(version). + WithPath("/orchestrator/machines/{id}/execute"). + WithRequiredClaim(constants.EXECUTE_COMMAND_VM_CLAIM). + WithHandler(ExecuteCommandOnVirtualMachineHandler()). + Register() + restapi.NewController(). WithMethod(restapi.GET). WithVersion(version). @@ -170,7 +227,7 @@ func registerOrchestratorHostsHandlers(ctx basecontext.ApiContext, version strin // @Summary Gets all hosts from the orchestrator // @Description This endpoint returns all hosts from the orchestrator -// @Tags Claims +// @Tags Orchestrator // @Produce json // @Success 200 {object} []models.OrchestratorHostResponse // @Failure 400 {object} models.ApiErrorResponse @@ -181,13 +238,11 @@ func registerOrchestratorHostsHandlers(ctx basecontext.ApiContext, version strin func GetOrchestratorHostsHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) - dbService, err := serviceprovider.GetDatabaseService(ctx) - if err != nil { - ReturnApiError(ctx, w, models.NewFromErrorWithCode(err, http.StatusInternalServerError)) - return - } - - dtoOrchestratorHosts, err := dbService.GetOrchestratorHosts(ctx, GetFilterHeader(r)) + defer Recover(ctx, r, w) + defer Recover(ctx, r, w) + filter := GetFilterHeader(r) + orchestratorSvc := orchestrator.NewOrchestratorService(ctx) + dtoOrchestratorHosts, err := orchestratorSvc.GetHosts(ctx, filter) if err != nil { ReturnApiError(ctx, w, models.NewFromError(err)) return @@ -202,38 +257,12 @@ func GetOrchestratorHostsHandler() restapi.ControllerHandler { } response := make([]models.OrchestratorHostResponse, 0) - orchestratorSvc := orchestrator.NewOrchestratorService(ctx) - - // // Checking the orchestrator hosts health - // for _, host := range dtoOrchestratorHosts { - // rHost := mappers.DtoOrchestratorHostToApiResponse(host) - // rHost.State = orchestratorSvc.GetHostHealthCheckState(&host) - - // response = append(response, rHost) - // } - - var wg sync.WaitGroup - mutex := sync.Mutex{} for _, host := range dtoOrchestratorHosts { - starTime := time.Now() - wg.Add(1) - go func(host data_models.OrchestratorHost) { - ctx.LogDebugf("Processing Host: %v\n", host.Host) - defer wg.Done() - - rHost := mappers.DtoOrchestratorHostToApiResponse(host) - rHost.State = orchestratorSvc.GetHostHealthCheckState(&host) - - mutex.Lock() - response = append(response, rHost) - mutex.Unlock() - ctx.LogDebugf("Processing Host: %v - Time: %v\n", host.Host, time.Since(starTime)) - }(host) + rHost := mappers.DtoOrchestratorHostToApiResponse(*host) + response = append(response, rHost) } - wg.Wait() - w.WriteHeader(http.StatusOK) _ = json.NewEncoder(w).Encode(response) ctx.LogInfof("Hosts returned successfully") @@ -254,25 +283,19 @@ func GetOrchestratorHostsHandler() restapi.ControllerHandler { func GetOrchestratorHostHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) - dbService, err := serviceprovider.GetDatabaseService(ctx) - if err != nil { - ReturnApiError(ctx, w, models.NewFromErrorWithCode(err, http.StatusInternalServerError)) - return - } + defer Recover(ctx, r, w) + orchestratorSvc := orchestrator.NewOrchestratorService(ctx) vars := mux.Vars(r) id := vars["id"] - dtoOrchestratorHost, err := dbService.GetOrchestratorHost(ctx, helpers.NormalizeString(id)) + host, err := orchestratorSvc.GetHost(ctx, id) if err != nil { - ReturnApiError(ctx, w, models.NewFromErrorWithCode(err, 404)) + ReturnApiError(ctx, w, models.NewFromError(err)) return } - // Validating the Health check probe of the host - orchestratorSvc := orchestrator.NewOrchestratorService(ctx) - dtoOrchestratorHost.State = orchestratorSvc.GetHostHealthCheckState(dtoOrchestratorHost) - response := mappers.DtoOrchestratorHostToApiResponse(*dtoOrchestratorHost) + response := mappers.DtoOrchestratorHostToApiResponse(*host) w.WriteHeader(http.StatusOK) _ = json.NewEncoder(w).Encode(response) @@ -280,8 +303,8 @@ func GetOrchestratorHostHandler() restapi.ControllerHandler { } } -// @Summary Creates a Host in the orchestrator -// @Description This endpoint creates a host in the orchestrator +// @Summary Register a Host in the orchestrator +// @Description This endpoint register a host in the orchestrator // @Tags Orchestrator // @Produce json // @Param hostRequest body models.OrchestratorHostRequest true "Host Request" @@ -291,9 +314,10 @@ func GetOrchestratorHostHandler() restapi.ControllerHandler { // @Security ApiKeyAuth // @Security BearerAuth // @Router /v1/orchestrator/hosts [post] -func CreateOrchestratorHostHandler() restapi.ControllerHandler { +func RegisterOrchestratorHostHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) + defer Recover(ctx, r, w) var request models.OrchestratorHostRequest if err := http_helper.MapRequestBody(r, &request); err != nil { ReturnApiError(ctx, w, models.ApiErrorResponse{ @@ -308,24 +332,11 @@ func CreateOrchestratorHostHandler() restapi.ControllerHandler { }) return } - - dbService, err := serviceprovider.GetDatabaseService(ctx) - if err != nil { - ReturnApiError(ctx, w, models.NewFromErrorWithCode(err, http.StatusInternalServerError)) - return - } - - oSvc := orchestrator.NewOrchestratorService(ctx) + orchestratorSvc := orchestrator.NewOrchestratorService(ctx) // checking if we can connect to host before adding it dtoRecord := mappers.ApiOrchestratorRequestToDto(request) - _, err = oSvc.GetHostHardwareInfo(&dtoRecord) - if err != nil { - ReturnApiError(ctx, w, models.NewFromError(err)) - return - } - - record, err := dbService.CreateOrchestratorHost(ctx, dtoRecord) + record, err := orchestratorSvc.RegisterHost(ctx, &dtoRecord) if err != nil { ReturnApiError(ctx, w, models.NewFromError(err)) return @@ -339,8 +350,8 @@ func CreateOrchestratorHostHandler() restapi.ControllerHandler { } } -// @Summary Delete a host from the orchestrator -// @Description This endpoint deletes a host from the orchestrator +// @Summary Unregister a host from the orchestrator +// @Description This endpoint unregister a host from the orchestrator // @Tags Orchestrator // @Produce json // @Param id path string true "Host ID" @@ -350,26 +361,85 @@ func CreateOrchestratorHostHandler() restapi.ControllerHandler { // @Security ApiKeyAuth // @Security BearerAuth // @Router /v1/orchestrator/hosts/{id} [delete] -func DeleteOrchestratorHostHandler() restapi.ControllerHandler { +func UnregisterOrchestratorHostHandler() restapi.ControllerHandler { + return func(w http.ResponseWriter, r *http.Request) { + ctx := GetBaseContext(r) + defer Recover(ctx, r, w) + orchestratorSvc := orchestrator.NewOrchestratorService(ctx) + + vars := mux.Vars(r) + id := vars["id"] + + orchestratorSvc.UnregisterHost(ctx, id) + + w.WriteHeader(http.StatusAccepted) + ctx.LogInfof("Orchestrator host deleted successfully") + } +} + +// @Summary Enable a host in the orchestrator +// @Description This endpoint will enable an existing host in the orchestrator +// @Tags Orchestrator +// @Produce json +// @Success 200 {object} models.OrchestratorHostResponse +// @Failure 400 {object} models.ApiErrorResponse +// @Failure 401 {object} models.OAuthErrorResponse +// @Security ApiKeyAuth +// @Security BearerAuth +// @Router /v1/orchestrator/hosts/{id}/enable [get] +func EnableOrchestratorHostsHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) - dbService, err := serviceprovider.GetDatabaseService(ctx) + defer Recover(ctx, r, w) + orchestratorSvc := orchestrator.NewOrchestratorService(ctx) + + vars := mux.Vars(r) + id := vars["id"] + + host, err := orchestratorSvc.EnableHost(ctx, id) if err != nil { - ReturnApiError(ctx, w, models.NewFromErrorWithCode(err, http.StatusInternalServerError)) + ReturnApiError(ctx, w, models.NewFromError(err)) return } + response := mappers.DtoOrchestratorHostToApiResponse(*host) + + w.WriteHeader(http.StatusOK) + _ = json.NewEncoder(w).Encode(response) + ctx.LogInfof("Host %v enabled successfully", id) + } +} + +// @Summary Disable a host in the orchestrator +// @Description This endpoint will disable an existing host in the orchestrator +// @Tags Orchestrator +// @Produce json +// @Success 200 {object} models.OrchestratorHostResponse +// @Failure 400 {object} models.ApiErrorResponse +// @Failure 401 {object} models.OAuthErrorResponse +// @Security ApiKeyAuth +// @Security BearerAuth +// @Router /v1/orchestrator/hosts/{id}/disable [get] +func DisableOrchestratorHostsHandler() restapi.ControllerHandler { + return func(w http.ResponseWriter, r *http.Request) { + ctx := GetBaseContext(r) + defer Recover(ctx, r, w) + orchestratorSvc := orchestrator.NewOrchestratorService(ctx) + vars := mux.Vars(r) id := vars["id"] - err = dbService.DeleteOrchestratorHost(ctx, id) + host, err := orchestratorSvc.DisableHost(ctx, id) if err != nil { ReturnApiError(ctx, w, models.NewFromError(err)) return } - w.WriteHeader(http.StatusAccepted) - ctx.LogInfof("Orchestrator host deleted successfully") + response := mappers.DtoOrchestratorHostToApiResponse(*host) + + w.WriteHeader(http.StatusOK) + _ = json.NewEncoder(w).Encode(response) + ctx.LogInfof("Host %v disabled successfully", id) } } @@ -386,31 +456,25 @@ func DeleteOrchestratorHostHandler() restapi.ControllerHandler { func GetOrchestratorOverviewHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) - dbService, err := serviceprovider.GetDatabaseService(ctx) + defer Recover(ctx, r, w) + orchestratorSvc := orchestrator.NewOrchestratorService(ctx) + result := make([]models.HostResourceOverviewResponse, 0) + resources, err := orchestratorSvc.GetResources(ctx) if err != nil { - ReturnApiError(ctx, w, models.NewFromErrorWithCode(err, http.StatusInternalServerError)) + ReturnApiError(ctx, w, models.NewFromError(err)) return } - response := models.HostResourceOverviewResponse{} - result := make([]models.HostResourceOverviewResponse, 0) - totalResources := dbService.GetOrchestratorTotalResources(ctx) - inUseResources := dbService.GetOrchestratorInUseResources(ctx) - availableResources := dbService.GetOrchestratorAvailableResources(ctx) - reservedResources := dbService.GetOrchestratorReservedResources(ctx) - - for key, value := range totalResources { - response.Total = mappers.MapApiHostResourceItemFromHostResourceItem(value) - response.TotalAvailable = mappers.MapApiHostResourceItemFromHostResourceItem(availableResources[key]) - response.TotalInUse = mappers.MapApiHostResourceItemFromHostResourceItem(inUseResources[key]) - response.TotalReserved = mappers.MapApiHostResourceItemFromHostResourceItem(reservedResources[key]) - response.CpuType = key - result = append(result, response) + for _, value := range resources { + item := models.HostResourceOverviewResponse{} + item.Total = mappers.MapApiHostResourceItemFromHostResourceItem(value.Total) + item.TotalAvailable = mappers.MapApiHostResourceItemFromHostResourceItem(value.TotalAvailable) + item.TotalInUse = mappers.MapApiHostResourceItemFromHostResourceItem(value.TotalInUse) + item.TotalReserved = mappers.MapApiHostResourceItemFromHostResourceItem(value.TotalReserved) + item.CpuType = value.CpuType + item.CpuBrand = value.CpuBrand + result = append(result, item) } - // response.Total = mappers.MapApiHostResourceItemFromHostResourceItem(totalResources) - // response.TotalAvailable = mappers.MapApiHostResourceItemFromHostResourceItem(availableResources) - // response.TotalInUse = mappers.MapApiHostResourceItemFromHostResourceItem(inUseResources) - // response.TotalReserved = mappers.MapApiHostResourceItemFromHostResourceItem(reservedResources) w.WriteHeader(http.StatusAccepted) _ = json.NewEncoder(w).Encode(result) @@ -418,76 +482,309 @@ func GetOrchestratorOverviewHandler() restapi.ControllerHandler { } } -// @Summary Get orchestrator host resources -// @Description This endpoint returns orchestrator host resources +// @Summary Get orchestrator host resources +// @Description This endpoint returns orchestrator host resources +// @Tags Orchestrator +// @Produce json +// @Param id path string true "Host ID" +// @Success 200 {object} models.HostResourceOverviewResponse +// @Failure 400 {object} models.ApiErrorResponse +// @Failure 401 {object} models.OAuthErrorResponse +// @Security ApiKeyAuth +// @Security BearerAuth +// @Router /v1/orchestrator/overview/{id}/resources [get] +func GetOrchestratorHostResourcesHandler() restapi.ControllerHandler { + return func(w http.ResponseWriter, r *http.Request) { + ctx := GetBaseContext(r) + defer Recover(ctx, r, w) + orchestratorSvc := orchestrator.NewOrchestratorService(ctx) + + vars := mux.Vars(r) + id := vars["id"] + + resources, err := orchestratorSvc.GetHostResources(ctx, id) + if err != nil { + ReturnApiError(ctx, w, models.NewFromError(err)) + return + } + + response := mappers.MapSystemUsageResponseFromHostResources(*resources) + + w.WriteHeader(http.StatusAccepted) + _ = json.NewEncoder(w).Encode(response) + ctx.LogInfof("Returned successfully the orchestrator host resources") + } +} + +// @Summary Get orchestrator Virtual Machines +// @Description This endpoint returns orchestrator Virtual Machines +// @Tags Orchestrator +// @Produce json +// @Success 200 {object} []models.ParallelsVM +// @Failure 400 {object} models.ApiErrorResponse +// @Failure 401 {object} models.OAuthErrorResponse +// @Security ApiKeyAuth +// @Security BearerAuth +// @Router /v1/orchestrator/machines [get] +func GetOrchestratorVirtualMachinesHandler() restapi.ControllerHandler { + return func(w http.ResponseWriter, r *http.Request) { + ctx := GetBaseContext(r) + filter := GetFilterHeader(r) + defer Recover(ctx, r, w) + orchestratorSvc := orchestrator.NewOrchestratorService(ctx) + + vms, err := orchestratorSvc.GetVirtualMachines(ctx, filter) + if err != nil { + ReturnApiError(ctx, w, models.NewFromError(err)) + return + } + + response := make([]models.ParallelsVM, 0) + for _, vm := range vms { + response = append(response, mappers.MapDtoVirtualMachineToApi(vm)) + } + + w.WriteHeader(http.StatusAccepted) + _ = json.NewEncoder(w).Encode(response) + ctx.LogInfof("Returned %v virtual machines from all hosts", len(response)) + } +} + +// @Summary Get orchestrator Virtual Machine +// @Description This endpoint returns orchestrator Virtual Machine by its ID +// @Tags Orchestrator +// @Produce json +// @Success 200 {object} models.ParallelsVM +// @Failure 400 {object} models.ApiErrorResponse +// @Failure 401 {object} models.OAuthErrorResponse +// @Security ApiKeyAuth +// @Security BearerAuth +// @Router /v1/orchestrator/machines/{id} [get] +func GetOrchestratorVirtualMachineHandler() restapi.ControllerHandler { + return func(w http.ResponseWriter, r *http.Request) { + ctx := GetBaseContext(r) + defer Recover(ctx, r, w) + orchestratorSvc := orchestrator.NewOrchestratorService(ctx) + + vars := mux.Vars(r) + id := vars["id"] + + vm, err := orchestratorSvc.GetVirtualMachine(ctx, id) + if err != nil { + ReturnApiError(ctx, w, models.NewFromError(err)) + return + } + + response := mappers.MapDtoVirtualMachineToApi(*vm) + + w.WriteHeader(http.StatusAccepted) + _ = json.NewEncoder(w).Encode(response) + ctx.LogInfof("Returned virtual machine %v from host", vm.ID, vm.HostId) + } +} + +// @Summary Deletes orchestrator virtual machine +// @Description This endpoint deletes orchestrator virtual machine +// @Tags Orchestrator +// @Produce json +// @Param id path string true "Virtual Machine ID" +// @Success 202 +// @Failure 400 {object} models.ApiErrorResponse +// @Failure 401 {object} models.OAuthErrorResponse +// @Security ApiKeyAuth +// @Security BearerAuth +// @Router /v1/orchestrator/machines/{id} [delete] +func DeleteOrchestratorVirtualMachineHandler() restapi.ControllerHandler { + return func(w http.ResponseWriter, r *http.Request) { + ctx := GetBaseContext(r) + defer Recover(ctx, r, w) + orchestratorSvc := orchestrator.NewOrchestratorService(ctx) + + vars := mux.Vars(r) + id := vars["id"] + + err := orchestratorSvc.DeleteVirtualMachine(ctx, id) + if err != nil { + ReturnApiError(ctx, w, models.NewFromError(err)) + return + } + + w.WriteHeader(http.StatusAccepted) + ctx.LogInfof("Successfully deleted the orchestrator virtual machine %s", id) + } +} + +// @Summary Get orchestrator virtual machine status +// @Description This endpoint returns orchestrator virtual machine status +// @Tags Orchestrator +// @Produce json +// @Param id path string true "Virtual Machine ID" +// @Success 200 {object} models.ParallelsVM +// @Failure 400 {object} models.ApiErrorResponse +// @Failure 401 {object} models.OAuthErrorResponse +// @Security ApiKeyAuth +// @Security BearerAuth +// @Router /v1/orchestrator/machines/{vmId}/status [get] +func GetOrchestratorVirtualMachineStatusHandler() restapi.ControllerHandler { + return func(w http.ResponseWriter, r *http.Request) { + ctx := GetBaseContext(r) + defer Recover(ctx, r, w) + orchestratorSvc := orchestrator.NewOrchestratorService(ctx) + + vars := mux.Vars(r) + id := vars["id"] + + response, err := orchestratorSvc.GetVirtualMachineStatus(ctx, id) + if err != nil { + ReturnApiError(ctx, w, models.NewFromError(err)) + return + } + + w.WriteHeader(http.StatusAccepted) + _ = json.NewEncoder(w).Encode(response) + ctx.LogInfof("Returned successfully the orchestrator virtual machine status") + } +} + +// @Summary Renames orchestrator virtual machine +// @Description This endpoint renames orchestrator virtual machine +// @Tags Orchestrator +// @Produce json +// @Param id path string true "Virtual Machine ID" +// @Success 200 {object} models.ParallelsVM +// @Failure 400 {object} models.ApiErrorResponse +// @Failure 401 {object} models.OAuthErrorResponse +// @Security ApiKeyAuth +// @Security BearerAuth +// @Router /v1/orchestrator/machines/{id}/rename [put] +func RenameOrchestratorVirtualMachineHandler() restapi.ControllerHandler { + return func(w http.ResponseWriter, r *http.Request) { + ctx := GetBaseContext(r) + defer Recover(ctx, r, w) + var request models.RenameVirtualMachineRequest + orchestratorSvc := orchestrator.NewOrchestratorService(ctx) + + vars := mux.Vars(r) + id := vars["id"] + request.ID = id + + if err := http_helper.MapRequestBody(r, &request); err != nil { + ReturnApiError(ctx, w, models.ApiErrorResponse{ + Message: "Invalid request body: " + err.Error(), + Code: http.StatusBadRequest, + }) + } + if err := request.Validate(); err != nil { + ReturnApiError(ctx, w, models.ApiErrorResponse{ + Message: "Invalid request body: " + err.Error(), + Code: http.StatusBadRequest, + }) + return + } + + response, err := orchestratorSvc.RenameVirtualMachine(ctx, id, request) + if err != nil { + ReturnApiError(ctx, w, models.NewFromError(err)) + return + } + + w.WriteHeader(http.StatusOK) + _ = json.NewEncoder(w).Encode(response) + ctx.LogInfof("Successfully renamed the orchestrator virtual machine %s", id) + } +} + +// @Summary Configures orchestrator virtual machine +// @Description This endpoint configures orchestrator virtual machine // @Tags Orchestrator // @Produce json -// @Param id path string true "Host ID" -// @Success 200 {object} models.HostResourceOverviewResponse -// @Failure 400 {object} models.ApiErrorResponse -// @Failure 401 {object} models.OAuthErrorResponse +// @Param id path string true "Virtual Machine ID" +// @Success 200 {object} models.VirtualMachineConfigResponse +// @Failure 400 {object} models.ApiErrorResponse +// @Failure 401 {object} models.OAuthErrorResponse // @Security ApiKeyAuth // @Security BearerAuth -// @Router /v1/orchestrator/overview/{id}/resources [get] -func GetOrchestratorHostResourcesHandler() restapi.ControllerHandler { +// @Router /v1/orchestrator/machines/{vmId}/set [put] +func SetOrchestratorVirtualMachineHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) - dbService, err := serviceprovider.GetDatabaseService(ctx) - if err != nil { - ReturnApiError(ctx, w, models.NewFromErrorWithCode(err, http.StatusInternalServerError)) - return - } + defer Recover(ctx, r, w) + var request models.VirtualMachineConfigRequest + orchestratorSvc := orchestrator.NewOrchestratorService(ctx) vars := mux.Vars(r) id := vars["id"] - host, err := dbService.GetOrchestratorHost(ctx, id) - if err != nil { - ReturnApiError(ctx, w, models.NewFromErrorWithCode(err, 404)) + if err := http_helper.MapRequestBody(r, &request); err != nil { + ReturnApiError(ctx, w, models.ApiErrorResponse{ + Message: "Invalid request body: " + err.Error(), + Code: http.StatusBadRequest, + }) + } + if err := request.Validate(); err != nil { + ReturnApiError(ctx, w, models.ApiErrorResponse{ + Message: "Invalid request body: " + err.Error(), + Code: http.StatusBadRequest, + }) return } - response := mappers.MapSystemUsageResponseFromHostResources(*host.Resources) + response, err := orchestratorSvc.ConfigureVirtualMachine(ctx, id, request) + if err != nil { + ReturnApiError(ctx, w, models.NewFromError(err)) + return + } - w.WriteHeader(http.StatusAccepted) + w.WriteHeader(http.StatusOK) _ = json.NewEncoder(w).Encode(response) - ctx.LogInfof("Returned successfully the orchestrator host resources") + ctx.LogInfof("Successfully configured the orchestrator virtual machine %s", id) } } -// @Summary Get orchestrator Virtual Machines -// @Description This endpoint returns orchestrator Virtual Machines +// @Summary Executes a command in a orchestrator virtual machine +// @Description This endpoint executes a command in a orchestrator virtual machine // @Tags Orchestrator // @Produce json -// @Success 200 {object} []models.ParallelsVM -// @Failure 400 {object} models.ApiErrorResponse -// @Failure 401 {object} models.OAuthErrorResponse +// @Param id path string true "Virtual Machine ID" +// @Success 200 {object} models.VirtualMachineConfigResponse +// @Failure 400 {object} models.ApiErrorResponse +// @Failure 401 {object} models.OAuthErrorResponse // @Security ApiKeyAuth // @Security BearerAuth -// @Router /v1/orchestrator/machines [get] -func GetOrchestratorVirtualMachinesHandler() restapi.ControllerHandler { +// @Router /v1/orchestrator/machines/{vmId}/execute [put] +func ExecutesOrchestratorVirtualMachineHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) - dbService, err := serviceprovider.GetDatabaseService(ctx) - if err != nil { - ReturnApiError(ctx, w, models.NewFromErrorWithCode(err, http.StatusInternalServerError)) + defer Recover(ctx, r, w) + var request models.VirtualMachineExecuteCommandRequest + orchestratorSvc := orchestrator.NewOrchestratorService(ctx) + + vars := mux.Vars(r) + id := vars["id"] + + if err := http_helper.MapRequestBody(r, &request); err != nil { + ReturnApiError(ctx, w, models.ApiErrorResponse{ + Message: "Invalid request body: " + err.Error(), + Code: http.StatusBadRequest, + }) + } + if err := request.Validate(); err != nil { + ReturnApiError(ctx, w, models.ApiErrorResponse{ + Message: "Invalid request body: " + err.Error(), + Code: http.StatusBadRequest, + }) return } - vms, err := dbService.GetOrchestratorVirtualMachines(ctx, GetFilterHeader(r)) + response, err := orchestratorSvc.ExecuteOnVirtualMachine(ctx, id, request) if err != nil { - ReturnApiError(ctx, w, models.NewFromErrorWithCode(err, 404)) + ReturnApiError(ctx, w, models.NewFromError(err)) return } - response := make([]models.ParallelsVM, 0) - for _, vm := range vms { - response = append(response, mappers.MapDtoVirtualMachineToApi(vm)) - } - - w.WriteHeader(http.StatusAccepted) + w.WriteHeader(http.StatusOK) _ = json.NewEncoder(w).Encode(response) - ctx.LogInfof("Returned successfully the orchestrator virtual machines") + ctx.LogInfof("Successfully executed command in the orchestrator virtual machine %s", id) } } @@ -505,16 +802,13 @@ func GetOrchestratorVirtualMachinesHandler() restapi.ControllerHandler { func GetOrchestratorHostVirtualMachinesHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) - dbService, err := serviceprovider.GetDatabaseService(ctx) - if err != nil { - ReturnApiError(ctx, w, models.NewFromErrorWithCode(err, http.StatusInternalServerError)) - return - } + defer Recover(ctx, r, w) + orchestratorSvc := orchestrator.NewOrchestratorService(ctx) vars := mux.Vars(r) id := vars["id"] - vms, err := dbService.GetOrchestratorHostVirtualMachines(ctx, id, GetFilterHeader(r)) + vms, err := orchestratorSvc.GetHostVirtualMachines(ctx, id, GetFilterHeader(r)) if err != nil { ReturnApiError(ctx, w, models.NewFromErrorWithCode(err, 404)) return @@ -522,7 +816,7 @@ func GetOrchestratorHostVirtualMachinesHandler() restapi.ControllerHandler { response := make([]models.ParallelsVM, 0) for _, vm := range vms { - response = append(response, mappers.MapDtoVirtualMachineToApi(vm)) + response = append(response, mappers.MapDtoVirtualMachineToApi(*vm)) } w.WriteHeader(http.StatusAccepted) @@ -546,17 +840,14 @@ func GetOrchestratorHostVirtualMachinesHandler() restapi.ControllerHandler { func GetOrchestratorHostVirtualMachineHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) - dbService, err := serviceprovider.GetDatabaseService(ctx) - if err != nil { - ReturnApiError(ctx, w, models.NewFromErrorWithCode(err, http.StatusInternalServerError)) - return - } + defer Recover(ctx, r, w) + orchestratorSvc := orchestrator.NewOrchestratorService(ctx) vars := mux.Vars(r) id := vars["id"] vmId := vars["vmId"] - vm, err := dbService.GetOrchestratorHostVirtualMachine(ctx, id, vmId) + vm, err := orchestratorSvc.GetHostVirtualMachine(ctx, id, vmId) if err != nil { ReturnApiError(ctx, w, models.NewFromErrorWithCode(err, 404)) return @@ -585,38 +876,17 @@ func GetOrchestratorHostVirtualMachineHandler() restapi.ControllerHandler { func DeleteOrchestratorHostVirtualMachineHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) - dbService, err := serviceprovider.GetDatabaseService(ctx) - if err != nil { - ReturnApiError(ctx, w, models.NewFromErrorWithCode(err, http.StatusInternalServerError)) - return - } + defer Recover(ctx, r, w) + orchestratorSvc := orchestrator.NewOrchestratorService(ctx) vars := mux.Vars(r) id := vars["id"] vmId := vars["vmId"] - host, err := dbService.GetOrchestratorHost(ctx, id) + err := orchestratorSvc.DeleteHostVirtualMachine(ctx, id, vmId) if err != nil { - ReturnApiError(ctx, w, models.NewFromErrorWithCode(err, 404)) - return - } - if host == nil { - ReturnApiError(ctx, w, models.ApiErrorResponse{ - Message: "Host not found", - Code: 404, - }) - return - } - if host.State != "healthy" { - ReturnApiError(ctx, w, models.ApiErrorResponse{ - Message: "Host is not healthy", - Code: 400, - }) - } - - orchestratorSvc := orchestrator.NewOrchestratorService(ctx) - if err := orchestratorSvc.DeleteHostVirtualMachine(host, vmId); err != nil { ReturnApiError(ctx, w, models.NewFromError(err)) + return } w.WriteHeader(http.StatusAccepted) @@ -639,39 +909,14 @@ func DeleteOrchestratorHostVirtualMachineHandler() restapi.ControllerHandler { func GetOrchestratorHostVirtualMachineStatusHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) - dbService, err := serviceprovider.GetDatabaseService(ctx) - if err != nil { - ReturnApiError(ctx, w, models.NewFromErrorWithCode(err, http.StatusInternalServerError)) - return - } + defer Recover(ctx, r, w) + orchestratorSvc := orchestrator.NewOrchestratorService(ctx) vars := mux.Vars(r) id := vars["id"] vmId := vars["vmId"] - host, err := dbService.GetOrchestratorHost(ctx, id) - if err != nil { - ReturnApiError(ctx, w, models.NewFromErrorWithCode(err, 404)) - return - } - - if host == nil { - ReturnApiError(ctx, w, models.ApiErrorResponse{ - Message: "Host not found", - Code: 404, - }) - return - } - if host.State != "healthy" { - ReturnApiError(ctx, w, models.ApiErrorResponse{ - Message: "Host is not healthy", - Code: 400, - }) - return - } - - orchestratorSvc := orchestrator.NewOrchestratorService(ctx) - response, err := orchestratorSvc.GetHostVirtualMachineStatus(host, vmId) + response, err := orchestratorSvc.GetHostVirtualMachineStatus(ctx, id, vmId) if err != nil { ReturnApiError(ctx, w, models.NewFromError(err)) return @@ -679,7 +924,7 @@ func GetOrchestratorHostVirtualMachineStatusHandler() restapi.ControllerHandler w.WriteHeader(http.StatusAccepted) _ = json.NewEncoder(w).Encode(response) - ctx.LogInfof("Returned successfully the orchestrator virtual machine") + ctx.LogInfof("Returned successfully the orchestrator virtual machine status") } } @@ -698,12 +943,9 @@ func GetOrchestratorHostVirtualMachineStatusHandler() restapi.ControllerHandler func RenameOrchestratorHostVirtualMachineHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) + defer Recover(ctx, r, w) var request models.RenameVirtualMachineRequest - dbService, err := serviceprovider.GetDatabaseService(ctx) - if err != nil { - ReturnApiError(ctx, w, models.NewFromErrorWithCode(err, http.StatusInternalServerError)) - return - } + orchestratorSvc := orchestrator.NewOrchestratorService(ctx) vars := mux.Vars(r) id := vars["id"] @@ -724,29 +966,7 @@ func RenameOrchestratorHostVirtualMachineHandler() restapi.ControllerHandler { return } - host, err := dbService.GetOrchestratorHost(ctx, id) - if err != nil { - ReturnApiError(ctx, w, models.NewFromErrorWithCode(err, 404)) - return - } - - if host == nil { - ReturnApiError(ctx, w, models.ApiErrorResponse{ - Message: "Host not found", - Code: 404, - }) - return - } - if host.State != "healthy" { - ReturnApiError(ctx, w, models.ApiErrorResponse{ - Message: "Host is not healthy", - Code: 400, - }) - return - } - - orchestratorSvc := orchestrator.NewOrchestratorService(ctx) - response, err := orchestratorSvc.RenameHostVirtualMachine(host, vmId, request) + response, err := orchestratorSvc.RenameHostVirtualMachine(ctx, id, vmId, request) if err != nil { ReturnApiError(ctx, w, models.NewFromError(err)) return @@ -773,12 +993,9 @@ func RenameOrchestratorHostVirtualMachineHandler() restapi.ControllerHandler { func SetOrchestratorHostVirtualMachineHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) + defer Recover(ctx, r, w) var request models.VirtualMachineConfigRequest - dbService, err := serviceprovider.GetDatabaseService(ctx) - if err != nil { - ReturnApiError(ctx, w, models.NewFromErrorWithCode(err, http.StatusInternalServerError)) - return - } + orchestratorSvc := orchestrator.NewOrchestratorService(ctx) vars := mux.Vars(r) id := vars["id"] @@ -798,29 +1015,7 @@ func SetOrchestratorHostVirtualMachineHandler() restapi.ControllerHandler { return } - host, err := dbService.GetOrchestratorHost(ctx, id) - if err != nil { - ReturnApiError(ctx, w, models.NewFromErrorWithCode(err, 404)) - return - } - - if host == nil { - ReturnApiError(ctx, w, models.ApiErrorResponse{ - Message: "Host not found", - Code: 404, - }) - return - } - if host.State != "healthy" { - ReturnApiError(ctx, w, models.ApiErrorResponse{ - Message: "Host is not healthy", - Code: 400, - }) - return - } - - orchestratorSvc := orchestrator.NewOrchestratorService(ctx) - response, err := orchestratorSvc.ConfigureHostVirtualMachine(host, vmId, request) + response, err := orchestratorSvc.ConfigureHostVirtualMachine(ctx, id, vmId, request) if err != nil { ReturnApiError(ctx, w, models.NewFromError(err)) return @@ -847,12 +1042,9 @@ func SetOrchestratorHostVirtualMachineHandler() restapi.ControllerHandler { func ExecutesOrchestratorHostVirtualMachineHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) + defer Recover(ctx, r, w) var request models.VirtualMachineExecuteCommandRequest - dbService, err := serviceprovider.GetDatabaseService(ctx) - if err != nil { - ReturnApiError(ctx, w, models.NewFromErrorWithCode(err, http.StatusInternalServerError)) - return - } + orchestratorSvc := orchestrator.NewOrchestratorService(ctx) vars := mux.Vars(r) id := vars["id"] @@ -872,29 +1064,7 @@ func ExecutesOrchestratorHostVirtualMachineHandler() restapi.ControllerHandler { return } - host, err := dbService.GetOrchestratorHost(ctx, id) - if err != nil { - ReturnApiError(ctx, w, models.NewFromErrorWithCode(err, 404)) - return - } - - if host == nil { - ReturnApiError(ctx, w, models.ApiErrorResponse{ - Message: "Host not found", - Code: 404, - }) - return - } - if host.State != "healthy" { - ReturnApiError(ctx, w, models.ApiErrorResponse{ - Message: "Host is not healthy", - Code: 400, - }) - return - } - - orchestratorSvc := orchestrator.NewOrchestratorService(ctx) - response, err := orchestratorSvc.ExecuteOnHostVirtualMachine(host, vmId, request) + response, err := orchestratorSvc.ExecuteOnHostVirtualMachine(ctx, id, vmId, request) if err != nil { ReturnApiError(ctx, w, models.NewFromError(err)) return @@ -921,12 +1091,9 @@ func ExecutesOrchestratorHostVirtualMachineHandler() restapi.ControllerHandler { func RegisterOrchestratorHostVirtualMachineHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) + defer Recover(ctx, r, w) var request models.RegisterVirtualMachineRequest - dbService, err := serviceprovider.GetDatabaseService(ctx) - if err != nil { - ReturnApiError(ctx, w, models.NewFromErrorWithCode(err, http.StatusInternalServerError)) - return - } + orchestratorSvc := orchestrator.NewOrchestratorService(ctx) vars := mux.Vars(r) id := vars["id"] @@ -944,29 +1111,7 @@ func RegisterOrchestratorHostVirtualMachineHandler() restapi.ControllerHandler { return } - host, err := dbService.GetOrchestratorHost(ctx, id) - if err != nil { - ReturnApiError(ctx, w, models.NewFromErrorWithCode(err, 404)) - return - } - - if host == nil { - ReturnApiError(ctx, w, models.ApiErrorResponse{ - Message: "Host not found", - Code: 404, - }) - return - } - if host.State != "healthy" { - ReturnApiError(ctx, w, models.ApiErrorResponse{ - Message: "Host is not healthy", - Code: 400, - }) - return - } - - orchestratorSvc := orchestrator.NewOrchestratorService(ctx) - response, err := orchestratorSvc.RegisterHostVirtualMachine(host, request) + response, err := orchestratorSvc.RegisterHostVirtualMachine(ctx, id, request) if err != nil { ReturnApiError(ctx, w, models.NewFromError(err)) return @@ -994,12 +1139,9 @@ func RegisterOrchestratorHostVirtualMachineHandler() restapi.ControllerHandler { func UnregisterOrchestratorHostVirtualMachineHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) + defer Recover(ctx, r, w) var request models.UnregisterVirtualMachineRequest - dbService, err := serviceprovider.GetDatabaseService(ctx) - if err != nil { - ReturnApiError(ctx, w, models.NewFromErrorWithCode(err, http.StatusInternalServerError)) - return - } + orchestratorSvc := orchestrator.NewOrchestratorService(ctx) vars := mux.Vars(r) id := vars["id"] @@ -1019,29 +1161,7 @@ func UnregisterOrchestratorHostVirtualMachineHandler() restapi.ControllerHandler return } - host, err := dbService.GetOrchestratorHost(ctx, id) - if err != nil { - ReturnApiError(ctx, w, models.NewFromErrorWithCode(err, 404)) - return - } - - if host == nil { - ReturnApiError(ctx, w, models.ApiErrorResponse{ - Message: "Host not found", - Code: 404, - }) - return - } - if host.State != "healthy" { - ReturnApiError(ctx, w, models.ApiErrorResponse{ - Message: "Host is not healthy", - Code: 400, - }) - return - } - - orchestratorSvc := orchestrator.NewOrchestratorService(ctx) - _, err = orchestratorSvc.UnregisterHostVirtualMachine(host, vmId, request) + _, err := orchestratorSvc.UnregisterHostVirtualMachine(ctx, id, vmId, request) if err != nil { ReturnApiError(ctx, w, models.NewFromError(err)) return @@ -1067,12 +1187,8 @@ func UnregisterOrchestratorHostVirtualMachineHandler() restapi.ControllerHandler func CreateOrchestratorHostVirtualMachineHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) + defer Recover(ctx, r, w) var request models.CreateVirtualMachineRequest - dbService, err := serviceprovider.GetDatabaseService(ctx) - if err != nil { - ReturnApiError(ctx, w, models.NewFromErrorWithCode(err, http.StatusInternalServerError)) - return - } vars := mux.Vars(r) id := vars["id"] @@ -1091,60 +1207,10 @@ func CreateOrchestratorHostVirtualMachineHandler() restapi.ControllerHandler { return } - host, err := dbService.GetOrchestratorHost(ctx, id) - if err != nil { - ReturnApiError(ctx, w, models.NewFromErrorWithCode(err, 404)) - return - } - - if host == nil { - ReturnApiError(ctx, w, models.ApiErrorResponse{ - Message: "Host not found", - Code: 404, - }) - return - } - if host.State != "healthy" { - ReturnApiError(ctx, w, models.ApiErrorResponse{ - Message: "Host is not healthy", - Code: 400, - }) - return - } - - var specs *models.CreateVirtualMachineSpecs - if request.CatalogManifest != nil && request.CatalogManifest.Specs != nil { - specs = request.CatalogManifest.Specs - } else if request.VagrantBox != nil && request.VagrantBox.Specs != nil { - specs = request.VagrantBox.Specs - } else if request.PackerTemplate != nil && request.PackerTemplate.Specs != nil { - specs = request.PackerTemplate.Specs - } else { - specs = &models.CreateVirtualMachineSpecs{ - Cpu: "1", - Memory: "2048", - } - } - - if host.Resources.TotalAvailable.LogicalCpuCount <= specs.GetCpuCount() { - ReturnApiError(ctx, w, models.ApiErrorResponse{ - Message: "Host does not have enough CPU resources", - Code: 400, - }) - return - } - if host.Resources.TotalAvailable.MemorySize <= specs.GetMemorySize() { - ReturnApiError(ctx, w, models.ApiErrorResponse{ - Message: "Host does not have enough Memory resources", - Code: 400, - }) - return - } - orchestratorSvc := orchestrator.NewOrchestratorService(ctx) - response, err := orchestratorSvc.CreateHostVirtualMachine(*host, request) + response, err := orchestratorSvc.CreateHosVirtualMachine(ctx, id, request) if err != nil { - ReturnApiError(ctx, w, models.NewFromError(err)) + ReturnApiError(ctx, w, *err) return } @@ -1168,12 +1234,8 @@ func CreateOrchestratorHostVirtualMachineHandler() restapi.ControllerHandler { func CreateOrchestratorVirtualMachineHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) + defer Recover(ctx, r, w) var request models.CreateVirtualMachineRequest - dbService, err := serviceprovider.GetDatabaseService(ctx) - if err != nil { - ReturnApiError(ctx, w, models.NewFromErrorWithCode(err, http.StatusInternalServerError)) - return - } if err := http_helper.MapRequestBody(r, &request); err != nil { ReturnApiError(ctx, w, models.ApiErrorResponse{ @@ -1190,105 +1252,10 @@ func CreateOrchestratorVirtualMachineHandler() restapi.ControllerHandler { return } - var specs *models.CreateVirtualMachineSpecs - if request.CatalogManifest != nil && request.CatalogManifest.Specs != nil { - specs = request.CatalogManifest.Specs - } else if request.VagrantBox != nil && request.VagrantBox.Specs != nil { - specs = request.VagrantBox.Specs - } else if request.PackerTemplate != nil && request.PackerTemplate.Specs != nil { - specs = request.PackerTemplate.Specs - } else { - specs = &models.CreateVirtualMachineSpecs{ - Cpu: "1", - Memory: "2048", - } - } - - hosts, err := dbService.GetOrchestratorHosts(ctx, "") + orchestratorSvc := orchestrator.NewOrchestratorService(ctx) + response, err := orchestratorSvc.CreateVirtualMachine(ctx, request) if err != nil { - ReturnApiError(ctx, w, models.NewFromErrorWithCode(err, 404)) - return - } - - var hostErr error - var response models.CreateVirtualMachineResponse - var apiError *models.ApiErrorResponse - - for _, orchestratorHost := range hosts { - if orchestratorHost.State != "healthy" { - apiError = &models.ApiErrorResponse{ - Message: "Host is not healthy", - Code: 400, - } - continue - } - if orchestratorHost.Resources == nil { - apiError = &models.ApiErrorResponse{ - Message: "Host does not have resources information", - Code: 400, - } - continue - } - if !strings.EqualFold(orchestratorHost.Architecture, request.Architecture) { - apiError = &models.ApiErrorResponse{ - Message: "Host does not have the same architecture", - Code: 400, - } - continue - } - if orchestratorHost.Resources.TotalAvailable.LogicalCpuCount > specs.GetCpuCount() && - orchestratorHost.Resources.TotalAvailable.MemorySize > specs.GetMemorySize() { - - if orchestratorHost.State != "healthy" { - apiError = &models.ApiErrorResponse{ - Message: "Host is not healthy", - Code: 400, - } - hostErr = errors.New("host is not healthy") - continue - } - - if orchestratorHost.Resources.TotalAvailable.LogicalCpuCount <= 1 { - apiError = &models.ApiErrorResponse{ - Message: "Host does not have enough CPU resources", - Code: 400, - } - hostErr = errors.New("host does not have enough CPU resources") - continue - } - if orchestratorHost.Resources.TotalAvailable.MemorySize < 2048 { - apiError = &models.ApiErrorResponse{ - Message: "Host does not have enough Memory resources", - Code: 400, - } - hostErr = errors.New("host does not have enough Memory resources") - continue - } - - orchestratorSvc := orchestrator.NewOrchestratorService(ctx) - resp, err := orchestratorSvc.CreateHostVirtualMachine(orchestratorHost, request) - if err != nil { - e := models.NewFromError(err) - apiError = &e - hostErr = err - break - } else { - response = *resp - break - } - } - } - - if hostErr == nil { - if apiError != nil { - ReturnApiError(ctx, w, *apiError) - } else { - ReturnApiError(ctx, w, models.ApiErrorResponse{ - Message: "No host available to create the virtual machine", - Code: 400, - }) - } - + ReturnApiError(ctx, w, *err) return } diff --git a/src/controllers/packer_templates.go b/src/controllers/packer_templates.go index 812cbae..0c28f4c 100644 --- a/src/controllers/packer_templates.go +++ b/src/controllers/packer_templates.go @@ -6,6 +6,7 @@ import ( "net/http" "github.com/Parallels/prl-devops-service/basecontext" + "github.com/Parallels/prl-devops-service/config" "github.com/Parallels/prl-devops-service/constants" "github.com/Parallels/prl-devops-service/mappers" "github.com/Parallels/prl-devops-service/models" @@ -17,6 +18,12 @@ import ( ) func registerPackerTemplatesHandlers(ctx basecontext.ApiContext, version string) { + config := config.Get() + + if !config.GetBoolKey(constants.ENABLE_PACKER_PLUGIN_ENV_VAR) { + ctx.LogInfof("Packer plugin is disabled, skipping packer template handlers registration") + } + ctx.LogInfof("Registering version %s packer template handlers", version) restapi.NewController(). @@ -73,6 +80,7 @@ func registerPackerTemplatesHandlers(ctx basecontext.ApiContext, version string) func GetPackerTemplatesHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) + defer Recover(ctx, r, w) dbService, err := serviceprovider.GetDatabaseService(ctx) if err != nil { ReturnApiError(ctx, w, models.NewFromErrorWithCode(err, http.StatusInternalServerError)) @@ -113,6 +121,7 @@ func GetPackerTemplatesHandler() restapi.ControllerHandler { func GetPackerTemplateHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) + defer Recover(ctx, r, w) dbService, err := serviceprovider.GetDatabaseService(ctx) if err != nil { ReturnApiError(ctx, w, models.NewFromErrorWithCode(err, http.StatusInternalServerError)) @@ -157,6 +166,7 @@ func GetPackerTemplateHandler() restapi.ControllerHandler { func CreatePackerTemplateHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) + defer Recover(ctx, r, w) var request models.CreatePackerTemplateRequest if err := http_helper.MapRequestBody(r, &request); err != nil { ReturnApiError(ctx, w, models.ApiErrorResponse{ @@ -206,6 +216,7 @@ func CreatePackerTemplateHandler() restapi.ControllerHandler { func UpdatePackerTemplateHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) + defer Recover(ctx, r, w) var request models.CreatePackerTemplateRequest if err := http_helper.MapRequestBody(r, &request); err != nil { ReturnApiError(ctx, w, models.ApiErrorResponse{ @@ -258,6 +269,7 @@ func UpdatePackerTemplateHandler() restapi.ControllerHandler { func DeletePackerTemplateHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) + defer Recover(ctx, r, w) dbService, err := serviceprovider.GetDatabaseService(ctx) if err != nil { ReturnApiError(ctx, w, models.NewFromErrorWithCode(err, http.StatusInternalServerError)) diff --git a/src/controllers/performance.go b/src/controllers/performance.go new file mode 100644 index 0000000..5325271 --- /dev/null +++ b/src/controllers/performance.go @@ -0,0 +1,75 @@ +package controllers + +import ( + "encoding/json" + "net/http" + "time" + + "github.com/Parallels/prl-devops-service/basecontext" + "github.com/Parallels/prl-devops-service/models" + "github.com/Parallels/prl-devops-service/restapi" + "github.com/Parallels/prl-devops-service/serviceprovider" + + "github.com/cjlapao/common-go/helper/http_helper" +) + +func registerPerformanceHandlers(ctx basecontext.ApiContext, version string) { + ctx.LogInfof("Registering version %s ApiKeys handlers", version) + restapi.NewController(). + WithMethod(restapi.POST). + WithVersion(version).WithPath("/performance/db"). + WithHandler(PerformDbTestHandler()). + Register() + +} + +func PerformDbTestHandler() restapi.ControllerHandler { + return func(w http.ResponseWriter, r *http.Request) { + ctx := GetBaseContext(r) + defer Recover(ctx, r, w) + + var request models.PerformanceRequest + if err := http_helper.MapRequestBody(r, &request); err != nil { + ReturnApiError(ctx, w, models.ApiErrorResponse{ + Message: "Invalid request body: " + err.Error(), + Code: http.StatusBadRequest, + }) + } + + if request.TestCount == 0 { + request.TestCount = 1 + } + + if request.ConsecutiveCalls == 0 { + request.ConsecutiveCalls = 1 + } + + if request.TimeBetweenCalls == 0 { + request.TimeBetweenCalls = 1 + } + + dbService, err := serviceprovider.GetDatabaseService(ctx) + if err != nil { + ReturnApiError(ctx, w, models.NewFromErrorWithCode(err, http.StatusInternalServerError)) + return + } + + for i := 0; i < request.TestCount; i++ { + ctx.LogInfof("This is a test log") + for j := 0; j < request.ConsecutiveCalls; j++ { + go dbService.Save(ctx) + if request.TimeBetweenConsecutiveCalls > 0 { + time.Sleep(time.Duration(request.TimeBetweenConsecutiveCalls) * time.Millisecond) + } + } + + if request.TimeBetweenCalls > 0 { + time.Sleep(time.Duration(request.TimeBetweenCalls) * time.Millisecond) + } + } + + w.WriteHeader(http.StatusOK) + _ = json.NewEncoder(w).Encode("ok") + ctx.LogInfof("Performance run successfully") + } +} diff --git a/src/controllers/roles.go b/src/controllers/roles.go index aec6ec6..6c4f44b 100644 --- a/src/controllers/roles.go +++ b/src/controllers/roles.go @@ -65,6 +65,7 @@ func registerRolesHandlers(ctx basecontext.ApiContext, version string) { func GetRolesHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) + defer Recover(ctx, r, w) dbService, err := serviceprovider.GetDatabaseService(ctx) if err != nil { ReturnApiError(ctx, w, models.NewFromErrorWithCode(err, http.StatusInternalServerError)) @@ -107,6 +108,7 @@ func GetRolesHandler() restapi.ControllerHandler { func GetRoleHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) + defer Recover(ctx, r, w) dbService, err := serviceprovider.GetDatabaseService(ctx) if err != nil { ReturnApiError(ctx, w, models.NewFromErrorWithCode(err, http.StatusInternalServerError)) @@ -144,6 +146,7 @@ func GetRoleHandler() restapi.ControllerHandler { func CreateRoleHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) + defer Recover(ctx, r, w) var request models.RoleRequest if err := http_helper.MapRequestBody(r, &request); err != nil { ReturnApiError(ctx, w, models.ApiErrorResponse{ @@ -195,6 +198,7 @@ func CreateRoleHandler() restapi.ControllerHandler { func DeleteRoleHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) + defer Recover(ctx, r, w) dbService, err := serviceprovider.GetDatabaseService(ctx) if err != nil { ReturnApiError(ctx, w, models.NewFromErrorWithCode(err, http.StatusInternalServerError)) diff --git a/src/controllers/users.go b/src/controllers/users.go index fe29377..220d880 100644 --- a/src/controllers/users.go +++ b/src/controllers/users.go @@ -120,6 +120,7 @@ func registerUsersHandlers(ctx basecontext.ApiContext, version string) { func GetUsersHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) + defer Recover(ctx, r, w) dbService, err := serviceprovider.GetDatabaseService(ctx) if err != nil { ReturnApiError(ctx, w, models.NewFromErrorWithCode(err, http.StatusInternalServerError)) @@ -162,6 +163,7 @@ func GetUsersHandler() restapi.ControllerHandler { func GetUserHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) + defer Recover(ctx, r, w) dbService, err := serviceprovider.GetDatabaseService(ctx) if err != nil { ReturnApiError(ctx, w, models.NewFromErrorWithCode(err, http.StatusInternalServerError)) @@ -199,6 +201,7 @@ func GetUserHandler() restapi.ControllerHandler { func CreateUserHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) + defer Recover(ctx, r, w) var request models.UserCreateRequest if err := http_helper.MapRequestBody(r, &request); err != nil { ReturnApiError(ctx, w, models.ApiErrorResponse{ @@ -266,6 +269,7 @@ func CreateUserHandler() restapi.ControllerHandler { func DeleteUserHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) + defer Recover(ctx, r, w) dbService, err := serviceprovider.GetDatabaseService(ctx) if err != nil { ReturnApiError(ctx, w, models.NewFromErrorWithCode(err, http.StatusInternalServerError)) @@ -300,6 +304,7 @@ func DeleteUserHandler() restapi.ControllerHandler { func UpdateUserHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) + defer Recover(ctx, r, w) var request models.UserCreateRequest if err := http_helper.MapRequestBody(r, &request); err != nil { ReturnApiError(ctx, w, models.ApiErrorResponse{ @@ -351,6 +356,7 @@ func UpdateUserHandler() restapi.ControllerHandler { func GetUserRolesHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) + defer Recover(ctx, r, w) dbService, err := serviceprovider.GetDatabaseService(ctx) if err != nil { ReturnApiError(ctx, w, models.NewFromErrorWithCode(err, http.StatusInternalServerError)) @@ -390,6 +396,7 @@ func GetUserRolesHandler() restapi.ControllerHandler { func AddRoleToUserHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) + defer Recover(ctx, r, w) var request models.RoleRequest if err := http_helper.MapRequestBody(r, &request); err != nil { ReturnApiError(ctx, w, models.ApiErrorResponse{ @@ -440,6 +447,7 @@ func AddRoleToUserHandler() restapi.ControllerHandler { func RemoveRoleFromUserHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) + defer Recover(ctx, r, w) dbService, err := serviceprovider.GetDatabaseService(ctx) if err != nil { ReturnApiError(ctx, w, models.NewFromErrorWithCode(err, http.StatusInternalServerError)) @@ -474,6 +482,7 @@ func RemoveRoleFromUserHandler() restapi.ControllerHandler { func GetUserClaimsHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) + defer Recover(ctx, r, w) dbService, err := serviceprovider.GetDatabaseService(ctx) if err != nil { ReturnApiError(ctx, w, models.NewFromErrorWithCode(err, http.StatusInternalServerError)) @@ -513,6 +522,7 @@ func GetUserClaimsHandler() restapi.ControllerHandler { func AddClaimToUserHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) + defer Recover(ctx, r, w) var request models.ClaimRequest if err := http_helper.MapRequestBody(r, &request); err != nil { ReturnApiError(ctx, w, models.ApiErrorResponse{ @@ -563,6 +573,7 @@ func AddClaimToUserHandler() restapi.ControllerHandler { func RemoveClaimFromUserHandler() restapi.ControllerHandler { return func(w http.ResponseWriter, r *http.Request) { ctx := GetBaseContext(r) + defer Recover(ctx, r, w) dbService, err := serviceprovider.GetDatabaseService(ctx) if err != nil { ReturnApiError(ctx, w, models.NewFromErrorWithCode(err, http.StatusInternalServerError)) diff --git a/src/data/api_key.go b/src/data/api_key.go index e84b6b7..2f88595 100644 --- a/src/data/api_key.go +++ b/src/data/api_key.go @@ -103,15 +103,17 @@ func (j *JsonDatabase) UpdateKey(ctx basecontext.ApiContext, key models.ApiKey) } for i, apiKey := range j.data.ApiKeys { - if apiKey.ID == key.ID { - j.data.ApiKeys[i].Revoked = key.Revoked - j.data.ApiKeys[i].RevokedAt = key.RevokedAt - j.data.ApiKeys[i].UpdatedAt = helpers.GetUtcCurrentDateTime() - if err := j.Save(ctx); err != nil { - return err - } - return nil + if apiKey.ID != key.ID { + continue } + + j.data.ApiKeys[i].Revoked = key.Revoked + j.data.ApiKeys[i].RevokedAt = key.RevokedAt + j.data.ApiKeys[i].UpdatedAt = helpers.GetUtcCurrentDateTime() + if err := j.Save(ctx); err != nil { + return err + } + return nil } return ErrApiKeyNotFound diff --git a/src/data/main.go b/src/data/main.go index 7956017..4ed4c96 100644 --- a/src/data/main.go +++ b/src/data/main.go @@ -20,8 +20,12 @@ var ( ) var memoryDatabase *JsonDatabase +var wg = &sync.WaitGroup{} +var totalSaveRequests = 0 +var mutexLock sync.Mutex type Data struct { + Schema models.DatabaseSchema `json:"schema"` Users []models.User `json:"users"` Claims []models.Claim `json:"claims"` Roles []models.Role `json:"roles"` @@ -54,8 +58,8 @@ func NewJsonDatabase(filename string) *JsonDatabase { data: Data{}, } + wg = &sync.WaitGroup{} rootContext := basecontext.NewRootBaseContext() - go memoryDatabase.ProcessSaveQueue(rootContext) _ = memoryDatabase.Load(rootContext) return memoryDatabase @@ -186,36 +190,79 @@ type saveRequest struct { } func (j *JsonDatabase) Save(ctx basecontext.ApiContext) error { - wg := &sync.WaitGroup{} - wg.Add(1) + totalSaveRequests++ ctx.LogDebugf("[Database] Enqueuing save request") - saveRequest := saveRequest{ - ctx: ctx, - wg: wg, - } - j.saveQueue = append(j.saveQueue, saveRequest) + defer func() { + if r := recover(); r != nil { + ctx.LogErrorf("[Database] Panic occurred during save: %v", r) + } + }() - j.saveProcess <- true + wg.Add(1) + go j.ProcessSaveQueue1(ctx) wg.Wait() + ctx.LogDebugf("[Database] Save request completed") return nil } -func (j *JsonDatabase) ProcessSaveQueue(ctx basecontext.ApiContext) { - for { - <-j.saveProcess - for len(j.saveQueue) > 0 { - request := j.saveQueue[0] - j.saveQueue = j.saveQueue[1:] - if err := j.processSave(ctx); err != nil { - ctx.LogErrorf("[Database] Error saving database: %v", err) - } - request.wg.Done() - } +func (j *JsonDatabase) ProcessSaveQueue1(ctx basecontext.ApiContext) { + defer wg.Done() + ctx.LogDebugf("[Database] Received for save request") + mutexLock.Lock() + if err := j.processSave(ctx); err != nil { + ctx.LogErrorf("[Database] Error saving database: %v", err) } + mutexLock.Unlock() } +// func (j *JsonDatabase) ProcessSaveQueue(ctx basecontext.ApiContext) { +// defer func() { +// if r := recover(); r != nil { +// ctx.LogErrorf("[Database] Panic occurred during save: %v", r) +// ctx.LogDebugf("[Database] Saved %v requests", totalSaveRequests) +// ctx.LogDebugf("[Database] SyncGroup count %v requests", syncGroupCount) +// } +// }() + +// for { +// ctx.LogDebugf("[Database] Waiting for save request") +// <-j.saveProcess +// ctx.LogDebugf("[Database] Received for save request") +// innerLoop: +// for { +// if len(j.saveQueue) == 0 { +// ctx.LogDebugf("[Database] No save requests in queue") +// break innerLoop +// } +// j.saveQueue = j.saveQueue[1:] +// mutexLock.Lock() +// if err := j.processSave(ctx); err != nil { +// ctx.LogErrorf("[Database] Error saving database: %v", err) +// syncGroupCount-- +// if syncGroupCount < 0 { +// fmt.Printf("here it is") +// } +// wg.Done() +// break innerLoop +// } +// mutexLock.Unlock() + +// syncGroupCount-- +// ctx.LogDebugf("[Database] SyncGroup count %v requests", syncGroupCount) +// if syncGroupCount < 0 { +// fmt.Printf("here it is") +// break innerLoop +// } + +// mutexLock.Lock() +// wg.Done() +// mutexLock.Unlock() +// } +// } +// } + func (j *JsonDatabase) processSave(ctx basecontext.ApiContext) error { j.saveMutex.Lock() @@ -224,6 +271,7 @@ func (j *JsonDatabase) processSave(ctx basecontext.ApiContext) error { backupFilename := j.filename + ".save.bak" err := helper.CopyFile(j.filename, backupFilename) if err != nil { + j.saveMutex.Unlock() ctx.LogErrorf("[Database] Error creating backup file: %v", err) return err } @@ -237,7 +285,10 @@ func (j *JsonDatabase) processSave(ctx basecontext.ApiContext) error { // Trying to open the file and waiting for it to be ready var file *os.File + openCount := 0 for { + openCount++ + ctx.LogDebugf("[Database] Trying to open file %s, attempt %v", j.filename, openCount) var fileOpenError error file, fileOpenError = os.OpenFile(j.filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o600) if fileOpenError == nil { @@ -247,18 +298,23 @@ func (j *JsonDatabase) processSave(ctx basecontext.ApiContext) error { defer file.Close() + ctx.LogDebugf("[Database] File %s opened successfully", j.filename) jsonString, err := json.MarshalIndent(j.data, "", " ") if err != nil { + ctx.LogDebugf("[Database] Error marshalling data: %v", err) j.isSaving = false j.saveMutex.Unlock() return errors.NewFromError(err) } + ctx.LogDebugf("[Database] Data marshalled successfully") if cfg.EncryptionPrivateKey() != "" { encJsonString, err := security.EncryptString(cfg.EncryptionPrivateKey(), string(jsonString)) if err != nil { + ctx.LogDebugf("[Database] Error encrypting data: %v", err) _, saveErr := file.Write(jsonString) if saveErr != nil { + ctx.LogDebugf("[Database] Error writing data: %v", saveErr) j.isSaving = false j.saveMutex.Unlock() return errors.NewFromError(saveErr) @@ -272,19 +328,23 @@ func (j *JsonDatabase) processSave(ctx basecontext.ApiContext) error { jsonString = encJsonString } + ctx.LogDebugf("[Database] Writing data to file") _, err = file.Write(jsonString) if err != nil { + ctx.LogDebugf("[Database] Error writing data: %v", err) j.isSaving = false j.saveMutex.Unlock() return err } if err := file.Close(); err != nil { + ctx.LogDebugf("[Database] Error closing file: %v", err) j.isSaving = false j.saveMutex.Unlock() return err } + ctx.LogDebugf("[Database] File %s saved successfully", j.filename) j.isSaving = false j.saveMutex.Unlock() return nil diff --git a/src/data/models/host_resources.go b/src/data/models/host_resources.go index d00bdd5..543d13d 100644 --- a/src/data/models/host_resources.go +++ b/src/data/models/host_resources.go @@ -2,6 +2,7 @@ package models type HostResourceOverviewResponseItem struct { CpuType string `json:"cpu_type,omitempty"` + CpuBrand string `json:"cpu_brand,omitempty"` Total HostResourceItem `json:"total,omitempty"` TotalAvailable HostResourceItem `json:"total_available,omitempty"` TotalInUse HostResourceItem `json:"total_in_use,omitempty"` diff --git a/src/data/models/orchestrator_host.go b/src/data/models/orchestrator_host.go index 74bcec9..fd5f6d3 100644 --- a/src/data/models/orchestrator_host.go +++ b/src/data/models/orchestrator_host.go @@ -9,6 +9,7 @@ import ( type OrchestratorHost struct { ID string `json:"id"` + Enabled bool `json:"enabled"` Host string `json:"host"` Architecture string `json:"architecture"` CpuModel string `json:"cpu_model"` @@ -37,7 +38,7 @@ func (o OrchestratorHost) GetHost() string { } schema := o.Schema if schema != "" && !strings.HasSuffix(schema, "://") { - schema = schema + "://" + schema += "://" } url, err := helpers.JoinUrl([]string{schema, o.Host, port, o.PathPrefix}) if err != nil { @@ -63,6 +64,9 @@ func (o *OrchestratorHost) Diff(source OrchestratorHost) bool { if o.Host != source.Host { return true } + if o.Enabled != source.Enabled { + return true + } if o.Architecture != source.Architecture { return true } diff --git a/src/data/models/schema.go b/src/data/models/schema.go new file mode 100644 index 0000000..592573e --- /dev/null +++ b/src/data/models/schema.go @@ -0,0 +1,5 @@ +package models + +type DatabaseSchema struct { + Version string `json:"version"` +} diff --git a/src/data/models/virtual_machine.go b/src/data/models/virtual_machine.go index 8dcb8f6..3851096 100644 --- a/src/data/models/virtual_machine.go +++ b/src/data/models/virtual_machine.go @@ -5,6 +5,7 @@ import "encoding/json" type VirtualMachine struct { ID string `json:"ID,omitempty"` HostId string `json:"host_id,omitempty"` + HostState string `json:"host_state,omitempty"` User string `json:"user,omitempty"` Host string `json:"host,omitempty"` Name string `json:"name,omitempty"` diff --git a/src/data/orchestrator.go b/src/data/orchestrator.go index 7f7853b..c865178 100644 --- a/src/data/orchestrator.go +++ b/src/data/orchestrator.go @@ -42,6 +42,38 @@ func (j *JsonDatabase) GetOrchestratorHosts(ctx basecontext.ApiContext, filter s return orderedResult, nil } +func (j *JsonDatabase) GetActiveOrchestratorHosts(ctx basecontext.ApiContext, filter string) ([]models.OrchestratorHost, error) { + if !j.IsConnected() { + return nil, ErrDatabaseNotConnected + } + + var activeHosts []models.OrchestratorHost + for _, host := range j.data.OrchestratorHosts { + if host.Enabled { + activeHosts = append(activeHosts, host) + } + } + + dbFilter, err := ParseFilter(filter) + if err != nil { + return nil, err + } + + filteredData, err := FilterByProperty(activeHosts, dbFilter) + if err != nil { + return nil, err + } + + result := GetAuthorizedRecords(ctx, filteredData...) + + orderedResult, err := OrderByProperty(result, &Order{Property: "UpdatedAt", Direction: OrderDirectionDesc}) + if err != nil { + return nil, err + } + + return orderedResult, nil +} + func (j *JsonDatabase) GetOrchestratorHost(ctx basecontext.ApiContext, idOrHost string) (*models.OrchestratorHost, error) { if !j.IsConnected() { return nil, ErrDatabaseNotConnected @@ -76,6 +108,7 @@ func (j *JsonDatabase) CreateOrchestratorHost(ctx basecontext.ApiContext, host m host.ID = helpers.GenerateId() host.CreatedAt = helpers.GetUtcCurrentDateTime() host.UpdatedAt = helpers.GetUtcCurrentDateTime() + host.Enabled = true if u, _ := j.GetOrchestratorHost(ctx, host.Host); u != nil { return nil, errors.NewWithCodef(400, "host %s already exists with ID %s", host.Host, host.ID) @@ -112,6 +145,34 @@ func (j *JsonDatabase) DeleteOrchestratorHost(ctx basecontext.ApiContext, idOrHo return ErrOrchestratorHostNotFound } +func (j *JsonDatabase) DeleteOrchestratorVirtualMachine(ctx basecontext.ApiContext, idOrHost string, vmIdOrName string) error { + if !j.IsConnected() { + return ErrDatabaseNotConnected + } + + if idOrHost == "" { + return ErrOrchestratorHostEmptyIdOrHost + } + + for _, host := range j.data.OrchestratorHosts { + if strings.EqualFold(host.ID, idOrHost) || strings.EqualFold(host.Host, idOrHost) { + for j, vm := range host.VirtualMachines { + if strings.EqualFold(vm.ID, vmIdOrName) || strings.EqualFold(vm.Name, vmIdOrName) { + host.VirtualMachines = append(host.VirtualMachines[:j], host.VirtualMachines[j+1:]...) + } + } + + if err := j.Save(ctx); err != nil { + return err + } + + return nil + } + } + + return ErrOrchestratorHostNotFound +} + func (j *JsonDatabase) UpdateOrchestratorHost(ctx basecontext.ApiContext, host *models.OrchestratorHost) (*models.OrchestratorHost, error) { if !j.IsConnected() { return nil, ErrDatabaseNotConnected @@ -128,7 +189,7 @@ func (j *JsonDatabase) UpdateOrchestratorHost(ctx basecontext.ApiContext, host * return nil, err } if host.Diff(j.data.OrchestratorHosts[index]) { - + j.data.OrchestratorHosts[index].Enabled = host.Enabled j.data.OrchestratorHosts[index].UpdatedAt = helpers.GetUtcCurrentDateTime() j.data.OrchestratorHosts[index].Host = host.Host j.data.OrchestratorHosts[index].Architecture = host.Architecture @@ -167,7 +228,7 @@ func (j *JsonDatabase) GetOrchestratorAvailableResources(ctx basecontext.ApiCont result := make(map[string]models.HostResourceItem) for _, host := range j.data.OrchestratorHosts { - if host.State == "healthy" { + if host.State == "healthy" && host.Enabled { if host.Resources != nil { if _, ok := result[host.Resources.CpuType]; !ok { result[host.Resources.CpuType] = models.HostResourceItem{} @@ -189,7 +250,7 @@ func (j *JsonDatabase) GetOrchestratorTotalResources(ctx basecontext.ApiContext) result := make(map[string]models.HostResourceItem) for _, host := range j.data.OrchestratorHosts { - if host.State == "healthy" { + if host.State == "healthy" && host.Enabled { if host.Resources != nil { if _, ok := result[host.Resources.CpuType]; !ok { result[host.Resources.CpuType] = models.HostResourceItem{} @@ -211,7 +272,7 @@ func (j *JsonDatabase) GetOrchestratorInUseResources(ctx basecontext.ApiContext) result := make(map[string]models.HostResourceItem) for _, host := range j.data.OrchestratorHosts { - if host.State == "healthy" { + if host.State == "healthy" && host.Enabled { if host.Resources != nil { if _, ok := result[host.Resources.CpuType]; !ok { result[host.Resources.CpuType] = models.HostResourceItem{} @@ -233,7 +294,7 @@ func (j *JsonDatabase) GetOrchestratorReservedResources(ctx basecontext.ApiConte result := make(map[string]models.HostResourceItem) for _, host := range j.data.OrchestratorHosts { - if host.State == "healthy" { + if host.State == "healthy" && host.Enabled { if host.Resources != nil { if _, ok := result[host.Resources.CpuType]; !ok { result[host.Resources.CpuType] = models.HostResourceItem{} diff --git a/src/data/schema.go b/src/data/schema.go new file mode 100644 index 0000000..209a17d --- /dev/null +++ b/src/data/schema.go @@ -0,0 +1,24 @@ +package data + +import ( + "github.com/Parallels/prl-devops-service/basecontext" +) + +func (j *JsonDatabase) GetSchemaVersion(ctx basecontext.ApiContext) (string, error) { + if !j.IsConnected() { + return "", ErrDatabaseNotConnected + } + + return j.data.Schema.Version, nil +} + +func (j *JsonDatabase) UpdateSchemaVersion(ctx basecontext.ApiContext, version string) error { + if !j.IsConnected() { + return ErrDatabaseNotConnected + } + + j.data.Schema.Version = version + j.Save(ctx) + + return nil +} diff --git a/src/docs/docs.go b/src/docs/docs.go index 471f855..2bba6f8 100644 --- a/src/docs/docs.go +++ b/src/docs/docs.go @@ -2540,6 +2540,64 @@ const docTemplate = `{ } } }, + "/v1/machines/{id}/clone": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + }, + { + "BearerAuth": [] + } + ], + "description": "This endpoint clones a virtual machine", + "produces": [ + "application/json" + ], + "tags": [ + "Machines" + ], + "summary": "Clones a virtual machine", + "parameters": [ + { + "type": "string", + "description": "Machine ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Machine Clone Request", + "name": "configRequest", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.VirtualMachineCloneCommandRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.VirtualMachineCloneCommandResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/models.ApiErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.OAuthErrorResponse" + } + } + } + } + }, "/v1/machines/{id}/execute": { "put": { "security": [ @@ -6249,6 +6307,28 @@ const docTemplate = `{ } } }, + "models.VirtualMachineCloneCommandRequest": { + "type": "object", + "properties": { + "clone_name": { + "type": "string" + } + } + }, + "models.VirtualMachineCloneCommandResponse": { + "type": "object", + "properties": { + "error": { + "type": "string" + }, + "id": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, "models.VirtualMachineConfigRequest": { "type": "object", "properties": { diff --git a/src/docs/swagger.json b/src/docs/swagger.json index 3b48c31..c0cf135 100644 --- a/src/docs/swagger.json +++ b/src/docs/swagger.json @@ -2533,6 +2533,64 @@ } } }, + "/v1/machines/{id}/clone": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + }, + { + "BearerAuth": [] + } + ], + "description": "This endpoint clones a virtual machine", + "produces": [ + "application/json" + ], + "tags": [ + "Machines" + ], + "summary": "Clones a virtual machine", + "parameters": [ + { + "type": "string", + "description": "Machine ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Machine Clone Request", + "name": "configRequest", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.VirtualMachineCloneCommandRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.VirtualMachineCloneCommandResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/models.ApiErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.OAuthErrorResponse" + } + } + } + } + }, "/v1/machines/{id}/execute": { "put": { "security": [ @@ -6242,6 +6300,28 @@ } } }, + "models.VirtualMachineCloneCommandRequest": { + "type": "object", + "properties": { + "clone_name": { + "type": "string" + } + } + }, + "models.VirtualMachineCloneCommandResponse": { + "type": "object", + "properties": { + "error": { + "type": "string" + }, + "id": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, "models.VirtualMachineConfigRequest": { "type": "object", "properties": { diff --git a/src/docs/swagger.yaml b/src/docs/swagger.yaml index 780a4c9..7180374 100644 --- a/src/docs/swagger.yaml +++ b/src/docs/swagger.yaml @@ -1232,6 +1232,20 @@ definitions: vertical-sync: type: string type: object + models.VirtualMachineCloneCommandRequest: + properties: + clone_name: + type: string + type: object + models.VirtualMachineCloneCommandResponse: + properties: + error: + type: string + id: + type: string + status: + type: string + type: object models.VirtualMachineConfigRequest: properties: operations: @@ -2850,6 +2864,42 @@ paths: summary: Gets a virtual machine tags: - Machines + /v1/machines/{id}/clone: + put: + description: This endpoint clones a virtual machine + parameters: + - description: Machine ID + in: path + name: id + required: true + type: string + - description: Machine Clone Request + in: body + name: configRequest + required: true + schema: + $ref: '#/definitions/models.VirtualMachineCloneCommandRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.VirtualMachineCloneCommandResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/models.ApiErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/models.OAuthErrorResponse' + security: + - ApiKeyAuth: [] + - BearerAuth: [] + summary: Clones a virtual machine + tags: + - Machines /v1/machines/{id}/execute: put: description: This endpoint executes a command on a virtual machine diff --git a/src/go.mod b/src/go.mod index dd0e97b..4101dd2 100644 --- a/src/go.mod +++ b/src/go.mod @@ -31,6 +31,7 @@ require ( github.com/Microsoft/go-winio v0.6.1 // indirect github.com/ProtonMail/go-crypto v1.0.0 // indirect github.com/andybalholm/brotli v1.1.0 // indirect + github.com/briandowns/spinner v1.23.0 // indirect github.com/cloudflare/circl v1.3.7 // indirect github.com/cyphar/filepath-securejoin v0.2.4 // indirect github.com/davecgh/go-spew v1.1.1 // indirect diff --git a/src/go.sum b/src/go.sum index 4319cc9..47aafaa 100644 --- a/src/go.sum +++ b/src/go.sum @@ -36,6 +36,8 @@ github.com/aws/aws-sdk-go v1.50.15 h1:wEMnPfEQQFaoIJwuO18zq/vtG4Ft7NxQ3r9xlEi/8z github.com/aws/aws-sdk-go v1.50.15/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk= github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M= github.com/bradleyjkemp/cupaloy/v2 v2.8.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0= +github.com/briandowns/spinner v1.23.0 h1:alDF2guRWqa/FOZZYWjlMIx2L6H0wyewPxo/CH4Pt2A= +github.com/briandowns/spinner v1.23.0/go.mod h1:rPG4gmXeN3wQV/TsAY4w8lPdIM6RX3yqeBQJSrbXjuE= github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/cjlapao/common-go v0.0.39 h1:bAAUrj2B9v0kMzbAOhzjSmiyDy+rd56r2sy7oEiQLlA= github.com/cjlapao/common-go v0.0.39/go.mod h1:M3dzazLjTjEtZJbbxoA5ZDiGCiHmpwqW9l4UWaddwOA= diff --git a/src/mappers/catalog.go b/src/mappers/catalog.go index 2e4b9d9..64565ec 100644 --- a/src/mappers/catalog.go +++ b/src/mappers/catalog.go @@ -294,6 +294,7 @@ func DtoCatalogManifestsToApi(m []data_models.CatalogManifest) []models.CatalogM func BasePullCatalogManifestResponseToApi(m catalog_models.PullCatalogManifestResponse) models.PullCatalogManifestResponse { data := models.PullCatalogManifestResponse{ ID: m.ID, + MachineID: m.MachineID, LocalPath: m.LocalPath, MachineName: m.MachineName, } diff --git a/src/mappers/orchestrator.go b/src/mappers/orchestrator.go index 79221a9..bf13462 100644 --- a/src/mappers/orchestrator.go +++ b/src/mappers/orchestrator.go @@ -10,6 +10,7 @@ import ( func DtoOrchestratorHostToApiResponse(dto data_models.OrchestratorHost) models.OrchestratorHostResponse { result := models.OrchestratorHostResponse{ ID: dto.ID, + Enabled: dto.Enabled, Host: dto.GetHost(), Architecture: dto.Architecture, CpuModel: dto.CpuModel, diff --git a/src/mappers/virtual_machine.go b/src/mappers/virtual_machine.go index e0970e1..1577f5e 100644 --- a/src/mappers/virtual_machine.go +++ b/src/mappers/virtual_machine.go @@ -58,6 +58,7 @@ func MapDtoVirtualMachineToApi(m data_models.VirtualMachine) models.ParallelsVM mapped := models.ParallelsVM{ HostId: m.HostId, Host: m.Host, + HostState: m.HostState, User: m.User, ID: m.ID, Name: m.Name, diff --git a/src/models/catalog.go b/src/models/catalog.go index 7509ef4..f48bd81 100644 --- a/src/models/catalog.go +++ b/src/models/catalog.go @@ -49,6 +49,7 @@ type CatalogManifestPackItem struct { type PullCatalogManifestResponse struct { ID string `json:"id,omitempty" yaml:"id,omitempty"` + MachineID string `json:"machine_id,omitempty" yaml:"machine_id,omitempty"` LocalPath string `json:"local_path,omitempty" yaml:"local_path,omitempty"` MachineName string `json:"machine_name,omitempty" yaml:"machine_name,omitempty"` Manifest *CatalogManifest `json:"manifest,omitempty" yaml:"manifest,omitempty"` diff --git a/src/models/orchestrator_host.go b/src/models/orchestrator_host.go index 2bca34a..0eb4132 100644 --- a/src/models/orchestrator_host.go +++ b/src/models/orchestrator_host.go @@ -79,6 +79,7 @@ func (o *OrchestratorHostRequest) Validate() error { type OrchestratorHostResponse struct { ID string `json:"id"` + Enabled bool `json:"enabled"` Host string `json:"host"` Architecture string `json:"architecture"` CpuModel string `json:"cpu_model"` diff --git a/src/models/parallels_desktop_user.go b/src/models/parallels_desktop_user.go new file mode 100644 index 0000000..b1120cb --- /dev/null +++ b/src/models/parallels_desktop_user.go @@ -0,0 +1,9 @@ +package models + +type ParallelsDesktopUsers []ParallelsDesktopUser + +type ParallelsDesktopUser struct { + Name string `json:"NAME"` + MNGSettings string `json:"MNG_SETTINGS"` + DefVMHome string `json:"DEF_VM_HOME"` +} diff --git a/src/models/performance.go b/src/models/performance.go new file mode 100644 index 0000000..6d2e827 --- /dev/null +++ b/src/models/performance.go @@ -0,0 +1,8 @@ +package models + +type PerformanceRequest struct { + TestCount int `json:"test_count"` + ConsecutiveCalls int `json:"consecutive_calls"` + TimeBetweenConsecutiveCalls int `json:"time_between_consecutive_calls"` + TimeBetweenCalls int `json:"time_between_calls"` +} diff --git a/src/models/virtual_machine_clone.go b/src/models/virtual_machine_clone.go new file mode 100644 index 0000000..c45360b --- /dev/null +++ b/src/models/virtual_machine_clone.go @@ -0,0 +1,21 @@ +package models + +import "github.com/Parallels/prl-devops-service/errors" + +type VirtualMachineCloneCommandRequest struct { + CloneName string `json:"clone_name"` +} + +func (r *VirtualMachineCloneCommandRequest) Validate() error { + if r.CloneName == "" { + return errors.NewWithCode("missing clone name", 400) + } + + return nil +} + +type VirtualMachineCloneCommandResponse struct { + Id string `json:"id,omitempty"` + Status string `json:"status,omitempty"` + Error string `json:"error,omitempty"` +} diff --git a/src/models/virtual_machine_config.go b/src/models/virtual_machine_config.go index 640a62b..6ed243e 100644 --- a/src/models/virtual_machine_config.go +++ b/src/models/virtual_machine_config.go @@ -92,9 +92,9 @@ func (r *VirtualMachineConfigRequestOperation) Validate() error { return errors.ErrConfigOperationNotSupported(r.Group, r.Operation) } case "machine": - if r.Value == "" { - return errors.ErrValueEmpty() - } + // if r.Value == "" { + // return errors.ErrValueEmpty() + // } if r.Operation != "rename" && r.Operation != "clone" && r.Operation != "archive" && diff --git a/src/models/virtual_machines.go b/src/models/virtual_machines.go index 3c9cf2e..2776c72 100644 --- a/src/models/virtual_machines.go +++ b/src/models/virtual_machines.go @@ -5,6 +5,7 @@ type ParallelsVMs []ParallelsVM type ParallelsVM struct { Host string `json:"host,omitempty"` HostId string `json:"host_id,omitempty"` + HostState string `json:"host_state,omitempty"` User string `json:"user"` ID string `json:"ID"` Name string `json:"Name"` diff --git a/src/orchestrator/common.go b/src/orchestrator/common.go index 012bdb6..358f80d 100644 --- a/src/orchestrator/common.go +++ b/src/orchestrator/common.go @@ -5,6 +5,10 @@ import ( "github.com/Parallels/prl-devops-service/serviceprovider/apiclient" ) +const ( + HealthyState = "healthy" +) + func (s *OrchestratorService) getApiClient(request models.OrchestratorHost) *apiclient.HttpClientService { apiClient := apiclient.NewHttpClient(s.ctx) if request.Authentication != nil { diff --git a/src/orchestrator/configure_host_virtual_machine.go b/src/orchestrator/configure_host_virtual_machine.go index 446bc80..b1644ed 100644 --- a/src/orchestrator/configure_host_virtual_machine.go +++ b/src/orchestrator/configure_host_virtual_machine.go @@ -1,12 +1,83 @@ package orchestrator import ( + "github.com/Parallels/prl-devops-service/basecontext" data_models "github.com/Parallels/prl-devops-service/data/models" + "github.com/Parallels/prl-devops-service/errors" "github.com/Parallels/prl-devops-service/helpers" "github.com/Parallels/prl-devops-service/models" ) -func (s *OrchestratorService) ConfigureHostVirtualMachine(host *data_models.OrchestratorHost, vmId string, request models.VirtualMachineConfigRequest) (*models.VirtualMachineConfigResponse, error) { +func (s *OrchestratorService) ConfigureVirtualMachine(ctx basecontext.ApiContext, vmId string, request models.VirtualMachineConfigRequest) (*models.VirtualMachineConfigResponse, error) { + vm, err := s.GetVirtualMachine(ctx, vmId) + if err != nil { + return nil, err + } + if vm == nil { + return nil, errors.NewWithCodef(404, "Virtual machine %s not found", vmId) + } + + host, err := s.GetHost(ctx, vm.HostId) + if err != nil { + return nil, err + } + + if host == nil { + return nil, errors.NewWithCodef(404, "Host %s not found", vm.HostId) + } + + if !host.Enabled { + return nil, errors.NewWithCodef(400, "Host %s is disabled", host.ID) + } + + if host.State != HealthyState { + return nil, errors.NewWithCodef(400, "Host %s is not healthy", host.ID) + } + + result, err := s.ConfigureHostVirtualMachine(ctx, vm.HostId, vmId, request) + if err != nil { + return nil, err + } + + return result, nil +} + +func (s *OrchestratorService) ConfigureHostVirtualMachine(ctx basecontext.ApiContext, hostId string, vmId string, request models.VirtualMachineConfigRequest) (*models.VirtualMachineConfigResponse, error) { + host, err := s.GetHost(ctx, hostId) + if err != nil { + return nil, err + } + + if host == nil { + return nil, errors.NewWithCodef(404, "Host %s not found", hostId) + } + + if !host.Enabled { + return nil, errors.NewWithCodef(400, "Host %s is disabled", hostId) + } + + if host.State != "healthy" { + return nil, errors.NewWithCodef(400, "Host %s is not healthy", hostId) + } + + vm, err := s.GetHostVirtualMachine(ctx, hostId, vmId) + if err != nil { + return nil, err + } + + if vm == nil { + return nil, errors.NewWithCodef(404, "Virtual machine %s not found on host %s", vmId, hostId) + } + + result, err := s.CallConfigureHostVirtualMachine(host, vm.ID, request) + if err != nil { + return nil, err + } + + return result, nil +} + +func (s *OrchestratorService) CallConfigureHostVirtualMachine(host *data_models.OrchestratorHost, vmId string, request models.VirtualMachineConfigRequest) (*models.VirtualMachineConfigResponse, error) { httpClient := s.getApiClient(*host) path := "/machines/" + vmId + "/set" url, err := helpers.JoinUrl([]string{host.GetHost(), path}) diff --git a/src/orchestrator/create_host_virtual_machine.go b/src/orchestrator/create_host_virtual_machine.go index 191ccea..1282ceb 100644 --- a/src/orchestrator/create_host_virtual_machine.go +++ b/src/orchestrator/create_host_virtual_machine.go @@ -1,12 +1,127 @@ package orchestrator import ( + "strings" + + "github.com/Parallels/prl-devops-service/basecontext" data_models "github.com/Parallels/prl-devops-service/data/models" "github.com/Parallels/prl-devops-service/helpers" "github.com/Parallels/prl-devops-service/models" + "github.com/Parallels/prl-devops-service/serviceprovider" ) -func (s *OrchestratorService) CreateHostVirtualMachine(host data_models.OrchestratorHost, request models.CreateVirtualMachineRequest) (*models.CreateVirtualMachineResponse, error) { +func (s *OrchestratorService) CreateVirtualMachine(ctx basecontext.ApiContext, request models.CreateVirtualMachineRequest) (*models.CreateVirtualMachineResponse, *models.ApiErrorResponse) { + var apiError *models.ApiErrorResponse + var response models.CreateVirtualMachineResponse + + dbService, err := serviceprovider.GetDatabaseService(ctx) + if err != nil { + apiError = &models.ApiErrorResponse{ + Message: "There was an error getting the database", + Code: 500, + } + return nil, apiError + } + + specs := s.getSpecsFromRequest(request) + + hosts, err := dbService.GetOrchestratorHosts(ctx, "") + if err != nil { + apiError = &models.ApiErrorResponse{ + Message: "There was an error getting the hosts from the database", + Code: 500, + } + return nil, apiError + } + + var selectedHost *data_models.OrchestratorHost + for _, orchestratorHost := range hosts { + isOk, err := s.validateHost(orchestratorHost, request.Architecture, specs) + if err != nil { + continue + } + + if isOk { + resp, err := s.CallCreateHostVirtualMachine(orchestratorHost, request) + if err != nil { + e := models.NewFromError(err) + apiError = &e + continue + } else { + response = *resp + selectedHost = &orchestratorHost + break + } + } + } + + if selectedHost == nil { + if apiError != nil { + return nil, apiError + } + + apiError = &models.ApiErrorResponse{ + Message: "No host available to create the virtual machine", + Code: 400, + } + } + + return &response, apiError +} + +func (s *OrchestratorService) CreateHosVirtualMachine(ctx basecontext.ApiContext, hostId string, request models.CreateVirtualMachineRequest) (*models.CreateVirtualMachineResponse, *models.ApiErrorResponse) { + var apiError *models.ApiErrorResponse + var response models.CreateVirtualMachineResponse + + dbService, err := serviceprovider.GetDatabaseService(ctx) + if err != nil { + apiError = &models.ApiErrorResponse{ + Message: "There was an error getting the database", + Code: 500, + } + return nil, apiError + } + + specs := s.getSpecsFromRequest(request) + + host, err := dbService.GetOrchestratorHost(ctx, hostId) + if err != nil { + apiError = &models.ApiErrorResponse{ + Message: "There was an error getting the hosts from the database", + Code: 500, + } + return nil, apiError + } + + var selectedHost *data_models.OrchestratorHost + isOk, validateErr := s.validateHost(*host, request.Architecture, specs) + if validateErr != nil { + return nil, validateErr + } + + if isOk { + resp, err := s.CallCreateHostVirtualMachine(*host, request) + if err != nil { + e := models.NewFromError(err) + apiError = &e + return nil, apiError + } else { + response = *resp + selectedHost = host + } + } + + if selectedHost == nil { + apiError = &models.ApiErrorResponse{ + Message: "No host available to create the virtual machine", + Code: 400, + } + } + + return &response, apiError +} + +func (s *OrchestratorService) CallCreateHostVirtualMachine(host data_models.OrchestratorHost, request models.CreateVirtualMachineRequest) (*models.CreateVirtualMachineResponse, error) { httpClient := s.getApiClient(host) path := "/machines" url, err := helpers.JoinUrl([]string{host.GetHost(), path}) @@ -25,3 +140,85 @@ func (s *OrchestratorService) CreateHostVirtualMachine(host data_models.Orchestr s.Refresh() return &response, nil } + +func (s *OrchestratorService) getSpecsFromRequest(request models.CreateVirtualMachineRequest) *models.CreateVirtualMachineSpecs { + var specs *models.CreateVirtualMachineSpecs + switch { + case request.CatalogManifest != nil && request.CatalogManifest.Specs != nil: + specs = request.CatalogManifest.Specs + case request.VagrantBox != nil && request.VagrantBox.Specs != nil: + specs = request.VagrantBox.Specs + case request.PackerTemplate != nil && request.PackerTemplate.Specs != nil: + specs = request.PackerTemplate.Specs + default: + specs = &models.CreateVirtualMachineSpecs{ + Cpu: "2", + Memory: "2048", + } + } + + return specs +} + +func (s *OrchestratorService) validateHost(host data_models.OrchestratorHost, architecture string, specs *models.CreateVirtualMachineSpecs) (bool, *models.ApiErrorResponse) { + var apiError *models.ApiErrorResponse + if !host.Enabled { + apiError = &models.ApiErrorResponse{ + Message: "Host is not enabled", + Code: 400, + } + return false, apiError + } + + if host.State != "healthy" { + apiError = &models.ApiErrorResponse{ + Message: "Host is not healthy", + Code: 400, + } + return false, apiError + } + + if host.Resources == nil { + apiError = &models.ApiErrorResponse{ + Message: "Host does not have resources information", + Code: 400, + } + return false, apiError + } + + if !strings.EqualFold(host.Architecture, architecture) { + apiError = &models.ApiErrorResponse{ + Message: "Host does not have the same architecture", + Code: 400, + } + + return false, apiError + } + + systemCPUThreshold := int64(1) + systemMemoryThreshold := float64(1024) + availableCpus := host.Resources.TotalAvailable.LogicalCpuCount - systemCPUThreshold + availableMemory := host.Resources.TotalAvailable.MemorySize - systemMemoryThreshold + + if availableCpus <= specs.GetCpuCount() || + availableMemory <= specs.GetMemorySize() { + if availableCpus <= specs.GetCpuCount() { + apiError = &models.ApiErrorResponse{ + Message: "Host does not have enough CPU resources", + Code: 400, + } + + return false, apiError + } + if availableMemory < specs.GetMemorySize() { + apiError = &models.ApiErrorResponse{ + Message: "Host does not have enough Memory resources", + Code: 400, + } + + return false, apiError + } + } + + return true, nil +} diff --git a/src/orchestrator/delete_host_virtual_machine.go b/src/orchestrator/delete_host_virtual_machine.go index 698b651..f5d96c1 100644 --- a/src/orchestrator/delete_host_virtual_machine.go +++ b/src/orchestrator/delete_host_virtual_machine.go @@ -1,12 +1,94 @@ package orchestrator import ( + "github.com/Parallels/prl-devops-service/basecontext" "github.com/Parallels/prl-devops-service/data/models" "github.com/Parallels/prl-devops-service/errors" "github.com/Parallels/prl-devops-service/helpers" + "github.com/Parallels/prl-devops-service/serviceprovider" ) -func (s *OrchestratorService) DeleteHostVirtualMachine(host *models.OrchestratorHost, vmId string) error { +func (s *OrchestratorService) DeleteVirtualMachine(ctx basecontext.ApiContext, vmId string) error { + vm, err := s.GetVirtualMachine(ctx, vmId) + if err != nil { + return err + } + + if vm == nil { + return errors.NewWithCodef(404, "Virtual machine %s not found", vmId) + } + + host, err := s.GetHost(ctx, vm.HostId) + if err != nil { + return err + } + + if host == nil { + return errors.NewWithCodef(404, "Host %s not found", vm.HostId) + } + + if !host.Enabled { + return errors.NewWithCodef(400, "Host %s is disabled", vm.HostId) + } + + if host.State != "healthy" { + return errors.NewWithCodef(400, "Host %s is not healthy", vm.HostId) + } + + err = s.DeleteHostVirtualMachine(ctx, vm.HostId, vmId) + if err != nil { + return err + } + + return nil +} + +func (s *OrchestratorService) DeleteHostVirtualMachine(ctx basecontext.ApiContext, hostId string, vmId string) error { + dbService, err := serviceprovider.GetDatabaseService(ctx) + if err != nil { + return err + } + + host, err := s.GetHost(ctx, hostId) + if err != nil { + return err + } + + if host == nil { + return errors.NewWithCodef(404, "Host %s not found", hostId) + } + + if !host.Enabled { + return errors.NewWithCodef(400, "Host %s is disabled", hostId) + } + + if host.State != "healthy" { + return errors.NewWithCodef(400, "Host %s is not healthy", hostId) + } + + vm, err := s.GetHostVirtualMachine(ctx, hostId, vmId) + if err != nil { + return err + } + + if vm == nil { + return errors.NewWithCodef(404, "Virtual machine %s not found on host %s", vmId, hostId) + } + + err = s.CallDeleteHostVirtualMachine(host, vmId) + if err != nil { + return err + } + + err = dbService.DeleteOrchestratorVirtualMachine(ctx, hostId, vmId) + if err != nil { + return err + } + + return nil +} + +func (s *OrchestratorService) CallDeleteHostVirtualMachine(host *models.OrchestratorHost, vmId string) error { httpClient := s.getApiClient(*host) path := "/v1/machines/" + vmId url, err := helpers.JoinUrl([]string{host.GetHost(), path}) @@ -14,7 +96,7 @@ func (s *OrchestratorService) DeleteHostVirtualMachine(host *models.Orchestrator return err } - apiResponse, err := httpClient.Get(url.String(), nil) + apiResponse, err := httpClient.Delete(url.String(), nil) if err != nil { return err } diff --git a/src/orchestrator/enable_disable_host.go b/src/orchestrator/enable_disable_host.go new file mode 100644 index 0000000..4dc91a8 --- /dev/null +++ b/src/orchestrator/enable_disable_host.go @@ -0,0 +1,54 @@ +package orchestrator + +import ( + "github.com/Parallels/prl-devops-service/basecontext" + "github.com/Parallels/prl-devops-service/data/models" + "github.com/Parallels/prl-devops-service/errors" + "github.com/Parallels/prl-devops-service/serviceprovider" +) + +func (s *OrchestratorService) EnableHost(ctx basecontext.ApiContext, hostIdOrHost string) (*models.OrchestratorHost, error) { + dbService, err := serviceprovider.GetDatabaseService(ctx) + if err != nil { + return nil, err + } + host, err := dbService.GetOrchestratorHost(ctx, hostIdOrHost) + if err != nil { + return nil, err + } + + if host == nil { + return nil, errors.NewWithCodef(404, "Host %s not found", hostIdOrHost) + } + + host.Enabled = true + updatedHost, err := dbService.UpdateOrchestratorHost(ctx, host) + if err != nil { + return nil, err + } + + return updatedHost, nil +} + +func (s *OrchestratorService) DisableHost(ctx basecontext.ApiContext, hostIdOrHost string) (*models.OrchestratorHost, error) { + dbService, err := serviceprovider.GetDatabaseService(ctx) + if err != nil { + return nil, err + } + host, err := dbService.GetOrchestratorHost(ctx, hostIdOrHost) + if err != nil { + return nil, err + } + + if host == nil { + return nil, errors.NewWithCodef(404, "Host %s not found", hostIdOrHost) + } + + host.Enabled = false + updatedHost, err := dbService.UpdateOrchestratorHost(ctx, host) + if err != nil { + return nil, err + } + + return updatedHost, nil +} diff --git a/src/orchestrator/execute_on_host_virtual_machine.go b/src/orchestrator/execute_on_host_virtual_machine.go index 3406789..4df8c40 100644 --- a/src/orchestrator/execute_on_host_virtual_machine.go +++ b/src/orchestrator/execute_on_host_virtual_machine.go @@ -1,12 +1,78 @@ package orchestrator import ( + "github.com/Parallels/prl-devops-service/basecontext" data_models "github.com/Parallels/prl-devops-service/data/models" + "github.com/Parallels/prl-devops-service/errors" "github.com/Parallels/prl-devops-service/helpers" "github.com/Parallels/prl-devops-service/models" ) -func (s *OrchestratorService) ExecuteOnHostVirtualMachine(host *data_models.OrchestratorHost, vmId string, request models.VirtualMachineExecuteCommandRequest) (*models.VirtualMachineExecuteCommandResponse, error) { +func (s *OrchestratorService) ExecuteOnVirtualMachine(ctx basecontext.ApiContext, vmId string, request models.VirtualMachineExecuteCommandRequest) (*models.VirtualMachineExecuteCommandResponse, error) { + vm, err := s.GetVirtualMachine(ctx, vmId) + if err != nil { + return nil, err + } + if vm == nil { + return nil, errors.NewWithCodef(404, "Virtual machine %s not found", vmId) + } + + host, err := s.GetHost(ctx, vm.HostId) + if err != nil { + return nil, err + } + if host == nil { + return nil, errors.NewWithCodef(404, "Host %s not found", vm.HostId) + } + + if !host.Enabled { + return nil, errors.NewWithCodef(400, "Host %s is disabled", host.Host) + } + if host.State != "healthy" { + return nil, errors.NewWithCodef(400, "Host %s is not healthy", host.Host) + } + + return s.ExecuteOnHostVirtualMachine(ctx, vm.HostId, vm.ID, request) +} + +func (s *OrchestratorService) ExecuteOnHostVirtualMachine(ctx basecontext.ApiContext, hostId string, vmId string, request models.VirtualMachineExecuteCommandRequest) (*models.VirtualMachineExecuteCommandResponse, error) { + vm, err := s.GetVirtualMachine(ctx, vmId) + if err != nil { + return nil, err + } + if vm == nil { + return nil, errors.NewWithCodef(404, "Virtual machine %s not found", vmId) + } + + host, err := s.GetHost(ctx, hostId) + if err != nil { + return nil, err + } + if host == nil { + return nil, errors.NewWithCodef(404, "Host %s not found", hostId) + } + + if !host.Enabled { + return nil, errors.NewWithCodef(400, "Host %s is disabled", host.Host) + } + if host.State != "healthy" { + return nil, errors.NewWithCodef(400, "Host %s is not healthy", host.Host) + } + + currentVmState, err := s.GetHostVirtualMachineStatus(ctx, host.ID, vm.ID) + if err != nil { + return nil, err + } + vm.State = currentVmState.Status + + if vm.State != "running" { + return nil, errors.NewWithCodef(400, "Virtual machine %s is not running", vmId) + } + + return s.CallExecuteOnHostVirtualMachine(host, vm.ID, request) +} + +func (s *OrchestratorService) CallExecuteOnHostVirtualMachine(host *data_models.OrchestratorHost, vmId string, request models.VirtualMachineExecuteCommandRequest) (*models.VirtualMachineExecuteCommandResponse, error) { httpClient := s.getApiClient(*host) path := "/machines/" + vmId + "/execute" url, err := helpers.JoinUrl([]string{host.GetHost(), path}) diff --git a/src/orchestrator/get_host_virtual_machine_status.go b/src/orchestrator/get_host_virtual_machine_status.go index f229c32..91a34b0 100644 --- a/src/orchestrator/get_host_virtual_machine_status.go +++ b/src/orchestrator/get_host_virtual_machine_status.go @@ -1,12 +1,83 @@ package orchestrator import ( + "github.com/Parallels/prl-devops-service/basecontext" data_models "github.com/Parallels/prl-devops-service/data/models" + "github.com/Parallels/prl-devops-service/errors" "github.com/Parallels/prl-devops-service/helpers" "github.com/Parallels/prl-devops-service/models" ) -func (s *OrchestratorService) GetHostVirtualMachineStatus(host *data_models.OrchestratorHost, vmId string) (*models.VirtualMachineStatusResponse, error) { +func (s *OrchestratorService) GetVirtualMachineStatus(ctx basecontext.ApiContext, vmId string) (*models.VirtualMachineStatusResponse, error) { + vm, err := s.GetVirtualMachine(ctx, vmId) + if err != nil { + return nil, err + } + if vm == nil { + return nil, errors.NewWithCodef(404, "Virtual machine %s not found", vmId) + } + + host, err := s.GetHost(ctx, vm.HostId) + if err != nil { + return nil, err + } + + if host == nil { + return nil, errors.NewWithCodef(404, "Host %s not found", vm.HostId) + } + + if !host.Enabled { + return nil, errors.NewWithCodef(400, "Host %s is disabled", host.ID) + } + + if host.State != "healthy" { + return nil, errors.NewWithCodef(400, "Host %s is not healthy", host.ID) + } + + result, err := s.GetHostVirtualMachineStatus(ctx, vm.HostId, vmId) + if err != nil { + return nil, err + } + + return result, nil +} + +func (s *OrchestratorService) GetHostVirtualMachineStatus(ctx basecontext.ApiContext, hostId string, vmId string) (*models.VirtualMachineStatusResponse, error) { + host, err := s.GetHost(ctx, hostId) + if err != nil { + return nil, err + } + + if host == nil { + return nil, errors.NewWithCodef(404, "Host %s not found", hostId) + } + + if !host.Enabled { + return nil, errors.NewWithCodef(400, "Host %s is disabled", hostId) + } + + if host.State != "healthy" { + return nil, errors.NewWithCodef(400, "Host %s is not healthy", hostId) + } + + vm, err := s.GetHostVirtualMachine(ctx, hostId, vmId) + if err != nil { + return nil, err + } + + if vm == nil { + return nil, errors.NewWithCodef(404, "Virtual machine %s not found on host %s", vmId, hostId) + } + + result, err := s.CallGetHostVirtualMachineStatus(host, vmId) + if err != nil { + return nil, err + } + + return result, nil +} + +func (s *OrchestratorService) CallGetHostVirtualMachineStatus(host *data_models.OrchestratorHost, vmId string) (*models.VirtualMachineStatusResponse, error) { httpClient := s.getApiClient(*host) path := "/machines/" + vmId + "/status" url, err := helpers.JoinUrl([]string{host.GetHost(), path}) diff --git a/src/orchestrator/get_host_virtual_machines.go b/src/orchestrator/get_host_virtual_machines.go index a6b7cf5..f0a9395 100644 --- a/src/orchestrator/get_host_virtual_machines.go +++ b/src/orchestrator/get_host_virtual_machines.go @@ -1,12 +1,78 @@ package orchestrator import ( + "strings" + + "github.com/Parallels/prl-devops-service/basecontext" "github.com/Parallels/prl-devops-service/data/models" "github.com/Parallels/prl-devops-service/errors" "github.com/Parallels/prl-devops-service/helpers" api_models "github.com/Parallels/prl-devops-service/models" + "github.com/Parallels/prl-devops-service/serviceprovider" ) +func (s *OrchestratorService) GetHostVirtualMachines(ctx basecontext.ApiContext, hostId string, filter string) ([]*models.VirtualMachine, error) { + dbService, err := serviceprovider.GetDatabaseService(ctx) + if err != nil { + return nil, err + } + + hosts, err := dbService.GetOrchestratorHosts(ctx, "") + if err != nil { + return nil, err + } + + vms, err := dbService.GetOrchestratorHostVirtualMachines(ctx, hostId, "") + if err != nil { + return nil, err + } + + result := make([]*models.VirtualMachine, 0) + + // Updating Host State for each VM + for _, vm := range vms { + hostLoop: + for _, host := range hosts { + if strings.EqualFold(vm.HostId, host.ID) { + vm.HostState = host.State + break hostLoop + } + } + + result = append(result, &vm) + } + + return result, nil +} + +func (s *OrchestratorService) GetHostVirtualMachine(ctx basecontext.ApiContext, hostId string, vmId string) (*models.VirtualMachine, error) { + dbService, err := serviceprovider.GetDatabaseService(ctx) + if err != nil { + return nil, err + } + + hosts, err := dbService.GetOrchestratorHosts(ctx, "") + if err != nil { + return nil, err + } + + vm, err := dbService.GetOrchestratorHostVirtualMachine(ctx, hostId, vmId) + if err != nil { + return nil, err + } + + // Updating Host State for each VM + + for _, host := range hosts { + if vm.HostId == host.ID { + vm.HostState = host.State + break + } + } + + return vm, nil +} + func (s *OrchestratorService) GetHostVirtualMachinesInfo(host *models.OrchestratorHost) ([]api_models.ParallelsVM, error) { httpClient := s.getApiClient(*host) path := "/v1/machines" diff --git a/src/orchestrator/get_hosts.go b/src/orchestrator/get_hosts.go new file mode 100644 index 0000000..8a1807f --- /dev/null +++ b/src/orchestrator/get_hosts.go @@ -0,0 +1,81 @@ +package orchestrator + +import ( + "sync" + "time" + + "github.com/Parallels/prl-devops-service/basecontext" + "github.com/Parallels/prl-devops-service/data/models" + "github.com/Parallels/prl-devops-service/serviceprovider" +) + +func (s *OrchestratorService) GetHosts(ctx basecontext.ApiContext, filter string) ([]*models.OrchestratorHost, error) { + dbService, err := serviceprovider.GetDatabaseService(ctx) + if err != nil { + return nil, err + } + dtoOrchestratorHosts, err := dbService.GetOrchestratorHosts(ctx, filter) + if err != nil { + return nil, err + } + + result := make([]*models.OrchestratorHost, 0) + + if len(dtoOrchestratorHosts) == 0 { + return result, nil + } + + var wg sync.WaitGroup + mutex := sync.Mutex{} + + for _, host := range dtoOrchestratorHosts { + starTime := time.Now() + wg.Add(1) + go func(host models.OrchestratorHost) { + ctx.LogDebugf("Processing Host: %v", host.Host) + defer wg.Done() + + if host.Enabled { + host.State = s.GetHostHealthCheckState(&host) + } + + mutex.Lock() + result = append(result, &host) + mutex.Unlock() + ctx.LogDebugf("Processing Host: %v - Time: %v", host.Host, time.Since(starTime)) + }(host) + } + + wg.Wait() + return result, nil +} + +func (s *OrchestratorService) GetHost(ctx basecontext.ApiContext, idOrName string) (*models.OrchestratorHost, error) { + dbService, err := serviceprovider.GetDatabaseService(ctx) + if err != nil { + return nil, err + } + host, err := dbService.GetOrchestratorHost(ctx, idOrName) + if err != nil { + return nil, err + } + + if host.Enabled { + host.State = s.GetHostHealthCheckState(host) + } + + return host, nil +} + +func (s *OrchestratorService) GetHostResources(ctx basecontext.ApiContext, idOrName string) (*models.HostResources, error) { + dbService, err := serviceprovider.GetDatabaseService(ctx) + if err != nil { + return nil, err + } + host, err := dbService.GetOrchestratorHost(ctx, idOrName) + if err != nil { + return nil, err + } + + return host.Resources, nil +} diff --git a/src/orchestrator/get_orchestrator_resources.go b/src/orchestrator/get_orchestrator_resources.go new file mode 100644 index 0000000..d276d1c --- /dev/null +++ b/src/orchestrator/get_orchestrator_resources.go @@ -0,0 +1,32 @@ +package orchestrator + +import ( + "github.com/Parallels/prl-devops-service/basecontext" + "github.com/Parallels/prl-devops-service/data/models" + "github.com/Parallels/prl-devops-service/serviceprovider" +) + +func (s *OrchestratorService) GetResources(ctx basecontext.ApiContext) ([]models.HostResourceOverviewResponseItem, error) { + dbService, err := serviceprovider.GetDatabaseService(ctx) + if err != nil { + return nil, err + } + + totalResources := dbService.GetOrchestratorTotalResources(ctx) + inUseResources := dbService.GetOrchestratorInUseResources(ctx) + availableResources := dbService.GetOrchestratorAvailableResources(ctx) + reservedResources := dbService.GetOrchestratorReservedResources(ctx) + + result := make([]models.HostResourceOverviewResponseItem, 0) + for key, value := range totalResources { + item := models.HostResourceOverviewResponseItem{} + item.Total = value + item.TotalAvailable = availableResources[key] + item.TotalInUse = inUseResources[key] + item.TotalReserved = reservedResources[key] + item.CpuType = key + result = append(result, item) + } + + return result, nil +} diff --git a/src/orchestrator/get_virtual_machines.go b/src/orchestrator/get_virtual_machines.go new file mode 100644 index 0000000..a9decb4 --- /dev/null +++ b/src/orchestrator/get_virtual_machines.go @@ -0,0 +1,78 @@ +package orchestrator + +import ( + "strings" + + "github.com/Parallels/prl-devops-service/basecontext" + "github.com/Parallels/prl-devops-service/data/models" + "github.com/Parallels/prl-devops-service/serviceprovider" +) + +func (s *OrchestratorService) GetVirtualMachines(ctx basecontext.ApiContext, filter string) ([]models.VirtualMachine, error) { + dbService, err := serviceprovider.GetDatabaseService(ctx) + if err != nil { + return nil, err + } + + hosts, err := dbService.GetOrchestratorHosts(ctx, "") + if err != nil { + return nil, err + } + + vms, err := dbService.GetOrchestratorVirtualMachines(ctx, filter) + if err != nil { + return nil, err + } + + result := make([]models.VirtualMachine, 0) + + // Updating Host State for each VM + for _, vm := range vms { + hostLoop: + for _, host := range hosts { + if vm.HostId == host.ID { + vm.HostState = host.State + break hostLoop + } + } + + result = append(result, vm) + } + + return result, nil +} + +func (s *OrchestratorService) GetVirtualMachine(ctx basecontext.ApiContext, idOrName string) (*models.VirtualMachine, error) { + dbService, err := serviceprovider.GetDatabaseService(ctx) + if err != nil { + return nil, err + } + + hosts, err := dbService.GetOrchestratorHosts(ctx, "") + if err != nil { + return nil, err + } + + vms, err := dbService.GetOrchestratorVirtualMachines(ctx, "") + if err != nil { + return nil, err + } + + var resultVm *models.VirtualMachine + for _, vm := range vms { + if strings.EqualFold(vm.ID, idOrName) || strings.EqualFold(vm.Name, idOrName) { + resultVm = &vm + break + } + } + + // Updating Host State for each VM + for _, host := range hosts { + if strings.EqualFold(resultVm.HostId, host.ID) { + resultVm.HostState = host.State + break + } + } + + return resultVm, nil +} diff --git a/src/orchestrator/main.go b/src/orchestrator/main.go index 50985bf..2617d4b 100644 --- a/src/orchestrator/main.go +++ b/src/orchestrator/main.go @@ -180,10 +180,6 @@ func (s *OrchestratorService) persistHost(host *models.OrchestratorHost) error { return nil } -func (s *OrchestratorService) GetResources() error { - return nil -} - func (s *OrchestratorService) SetHealthCheckTimeout(timeout time.Duration) { s.healthCheckTimeout = timeout } diff --git a/src/orchestrator/register_host.go b/src/orchestrator/register_host.go new file mode 100644 index 0000000..e493862 --- /dev/null +++ b/src/orchestrator/register_host.go @@ -0,0 +1,27 @@ +package orchestrator + +import ( + "github.com/Parallels/prl-devops-service/basecontext" + "github.com/Parallels/prl-devops-service/data/models" + "github.com/Parallels/prl-devops-service/serviceprovider" +) + +func (s *OrchestratorService) RegisterHost(ctx basecontext.ApiContext, host *models.OrchestratorHost) (*models.OrchestratorHost, error) { + dbService, err := serviceprovider.GetDatabaseService(ctx) + if err != nil { + return nil, err + } + + _, err = s.GetHostHardwareInfo(host) + if err != nil { + return nil, err + } + + host.Enabled = true + dbHost, err := dbService.CreateOrchestratorHost(ctx, *host) + if err != nil { + return nil, err + } + + return dbHost, nil +} diff --git a/src/orchestrator/register_host_virtual_machine.go b/src/orchestrator/register_host_virtual_machine.go index d57b388..16080e8 100644 --- a/src/orchestrator/register_host_virtual_machine.go +++ b/src/orchestrator/register_host_virtual_machine.go @@ -1,12 +1,33 @@ package orchestrator import ( + "github.com/Parallels/prl-devops-service/basecontext" data_models "github.com/Parallels/prl-devops-service/data/models" + "github.com/Parallels/prl-devops-service/errors" "github.com/Parallels/prl-devops-service/helpers" "github.com/Parallels/prl-devops-service/models" ) -func (s *OrchestratorService) RegisterHostVirtualMachine(host *data_models.OrchestratorHost, request models.RegisterVirtualMachineRequest) (*models.ParallelsVM, error) { +func (s *OrchestratorService) RegisterHostVirtualMachine(ctx basecontext.ApiContext, hostId string, request models.RegisterVirtualMachineRequest) (*models.ParallelsVM, error) { + host, err := s.GetHost(ctx, hostId) + if err != nil { + return nil, err + } + if host == nil { + return nil, errors.NewWithCodef(404, "Host %s not found", hostId) + } + + if !host.Enabled { + return nil, errors.NewWithCodef(400, "Host %s is disabled", host.Host) + } + if host.State != "healthy" { + return nil, errors.NewWithCodef(400, "Host %s is not healthy", host.Host) + } + + return s.CallRegisterHostVirtualMachine(host, request) +} + +func (s *OrchestratorService) CallRegisterHostVirtualMachine(host *data_models.OrchestratorHost, request models.RegisterVirtualMachineRequest) (*models.ParallelsVM, error) { httpClient := s.getApiClient(*host) path := "/machines/register" url, err := helpers.JoinUrl([]string{host.GetHost(), path}) diff --git a/src/orchestrator/rename_host_virtual_machine.go b/src/orchestrator/rename_host_virtual_machine.go index 1df4f42..ddd33f9 100644 --- a/src/orchestrator/rename_host_virtual_machine.go +++ b/src/orchestrator/rename_host_virtual_machine.go @@ -1,12 +1,83 @@ package orchestrator import ( + "github.com/Parallels/prl-devops-service/basecontext" data_models "github.com/Parallels/prl-devops-service/data/models" + "github.com/Parallels/prl-devops-service/errors" "github.com/Parallels/prl-devops-service/helpers" "github.com/Parallels/prl-devops-service/models" ) -func (s *OrchestratorService) RenameHostVirtualMachine(host *data_models.OrchestratorHost, vmId string, request models.RenameVirtualMachineRequest) (*models.ParallelsVM, error) { +func (s *OrchestratorService) RenameVirtualMachine(ctx basecontext.ApiContext, vmId string, request models.RenameVirtualMachineRequest) (*models.ParallelsVM, error) { + vm, err := s.GetVirtualMachine(ctx, vmId) + if err != nil { + return nil, err + } + if vm == nil { + return nil, errors.NewWithCodef(404, "Virtual machine %s not found", vmId) + } + + host, err := s.GetHost(ctx, vm.HostId) + if err != nil { + return nil, err + } + + if host == nil { + return nil, errors.NewWithCodef(404, "Host %s not found", vm.HostId) + } + + if !host.Enabled { + return nil, errors.NewWithCodef(400, "Host %s is disabled", host.ID) + } + + if host.State != "healthy" { + return nil, errors.NewWithCodef(400, "Host %s is not healthy", host.ID) + } + + result, err := s.RenameHostVirtualMachine(ctx, vm.HostId, vmId, request) + if err != nil { + return nil, err + } + + return result, nil +} + +func (s *OrchestratorService) RenameHostVirtualMachine(ctx basecontext.ApiContext, hostId string, vmId string, request models.RenameVirtualMachineRequest) (*models.ParallelsVM, error) { + host, err := s.GetHost(ctx, hostId) + if err != nil { + return nil, err + } + + if host == nil { + return nil, errors.NewWithCodef(404, "Host %s not found", hostId) + } + + if !host.Enabled { + return nil, errors.NewWithCodef(400, "Host %s is disabled", hostId) + } + + if host.State != "healthy" { + return nil, errors.NewWithCodef(400, "Host %s is not healthy", hostId) + } + + vm, err := s.GetHostVirtualMachine(ctx, hostId, vmId) + if err != nil { + return nil, err + } + + if vm == nil { + return nil, errors.NewWithCodef(404, "Virtual machine %s not found on host %s", vmId, hostId) + } + + result, err := s.CallRenameHostVirtualMachine(host, vmId, request) + if err != nil { + return nil, err + } + + return result, nil +} + +func (s *OrchestratorService) CallRenameHostVirtualMachine(host *data_models.OrchestratorHost, vmId string, request models.RenameVirtualMachineRequest) (*models.ParallelsVM, error) { httpClient := s.getApiClient(*host) path := "/machines/" + vmId + "/rename" url, err := helpers.JoinUrl([]string{host.GetHost(), path}) diff --git a/src/orchestrator/unregister_host.go b/src/orchestrator/unregister_host.go new file mode 100644 index 0000000..d75279d --- /dev/null +++ b/src/orchestrator/unregister_host.go @@ -0,0 +1,20 @@ +package orchestrator + +import ( + "github.com/Parallels/prl-devops-service/basecontext" + "github.com/Parallels/prl-devops-service/serviceprovider" +) + +func (s *OrchestratorService) UnregisterHost(ctx basecontext.ApiContext, hostId string) error { + dbService, err := serviceprovider.GetDatabaseService(ctx) + if err != nil { + return err + } + + err = dbService.DeleteOrchestratorHost(ctx, hostId) + if err != nil { + return err + } + + return nil +} diff --git a/src/orchestrator/unregister_host_virtual_machine.go b/src/orchestrator/unregister_host_virtual_machine.go index 4321135..8fa3fd8 100644 --- a/src/orchestrator/unregister_host_virtual_machine.go +++ b/src/orchestrator/unregister_host_virtual_machine.go @@ -1,12 +1,42 @@ package orchestrator import ( + "github.com/Parallels/prl-devops-service/basecontext" data_models "github.com/Parallels/prl-devops-service/data/models" + "github.com/Parallels/prl-devops-service/errors" "github.com/Parallels/prl-devops-service/helpers" "github.com/Parallels/prl-devops-service/models" ) -func (s *OrchestratorService) UnregisterHostVirtualMachine(host *data_models.OrchestratorHost, vmId string, request models.UnregisterVirtualMachineRequest) (*models.ParallelsVM, error) { +func (s *OrchestratorService) UnregisterHostVirtualMachine(ctx basecontext.ApiContext, hostId string, vmId string, request models.UnregisterVirtualMachineRequest) (*models.ParallelsVM, error) { + vm, err := s.GetVirtualMachine(ctx, vmId) + if err != nil { + return nil, err + } + + if vm == nil { + return nil, errors.NewWithCodef(404, "Virtual machine %s not found", vmId) + } + + host, err := s.GetHost(ctx, hostId) + if err != nil { + return nil, err + } + if host == nil { + return nil, errors.NewWithCodef(404, "Host %s not found", hostId) + } + + if !host.Enabled { + return nil, errors.NewWithCodef(400, "Host %s is disabled", host.Host) + } + if host.State != "healthy" { + return nil, errors.NewWithCodef(400, "Host %s is not healthy", host.Host) + } + + return s.CallUnregisterHostVirtualMachine(host, vm.ID, request) +} + +func (s *OrchestratorService) CallUnregisterHostVirtualMachine(host *data_models.OrchestratorHost, vmId string, request models.UnregisterVirtualMachineRequest) (*models.ParallelsVM, error) { httpClient := s.getApiClient(*host) path := "/machines/" + vmId + "/unregister" url, err := helpers.JoinUrl([]string{host.GetHost(), path}) diff --git a/src/restapi/http_listener.go b/src/restapi/http_listener.go index 8fb57ca..c7dd57d 100644 --- a/src/restapi/http_listener.go +++ b/src/restapi/http_listener.go @@ -14,6 +14,7 @@ import ( "github.com/Parallels/prl-devops-service/basecontext" "github.com/Parallels/prl-devops-service/common" + "github.com/Parallels/prl-devops-service/config" "github.com/Parallels/prl-devops-service/serviceprovider" _ "github.com/Parallels/prl-devops-service/docs" @@ -249,6 +250,19 @@ func (l *HttpListener) Start(serviceName string, serviceVersion string) { headersOk := handlers.AllowedHeaders([]string{"X-Requested-With", "authorization", "Authorization", "content-type"}) originsOk := handlers.AllowedOrigins([]string{"*"}) methodsOk := handlers.AllowedMethods([]string{"GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS"}) + config := config.Get() + configCorsAllowedHeaders := config.GetKey("CORS_ALLOWED_HEADERS") + if configCorsAllowedHeaders != "" { + headersOk = handlers.AllowedHeaders(strings.Split(configCorsAllowedHeaders, ",")) + } + configCorsAllowedOrigins := config.GetKey("CORS_ALLOWED_ORIGINS") + if configCorsAllowedOrigins != "" { + originsOk = handlers.AllowedOrigins(strings.Split(configCorsAllowedOrigins, ",")) + } + configCorsAllowedMethods := config.GetKey("CORS_ALLOWED_METHODS") + if configCorsAllowedMethods != "" { + methodsOk = handlers.AllowedMethods(strings.Split(configCorsAllowedMethods, ",")) + } l.Logger.Notice("Starting %v Go Rest API %v", serviceName, serviceVersion) diff --git a/src/serviceprovider/apiclient/main.go b/src/serviceprovider/apiclient/main.go index 51a6b28..d07ac3f 100644 --- a/src/serviceprovider/apiclient/main.go +++ b/src/serviceprovider/apiclient/main.go @@ -258,7 +258,7 @@ func (c *HttpClientService) RequestData(verb HttpClientServiceVerb, url string, } } - if response.Body != nil { + if response.Body != http.NoBody { body, err := io.ReadAll(response.Body) if err != nil { return &apiResponse, fmt.Errorf("error reading response body from %s, err: %v", url, err) @@ -270,7 +270,7 @@ func (c *HttpClientService) RequestData(verb HttpClientServiceVerb, url string, return &apiResponse, fmt.Errorf("error unmarshalling body from %s, err: %v ", url, err) } - c.ctx.LogDebugf("[Api Client] Response body: \n%s", string(body)) + c.ctx.LogTracef("[Api Client] Response body: \n%s", string(body)) apiResponse.Data = destination } else { var bodyData map[string]interface{} diff --git a/src/serviceprovider/parallelsdesktop/license.go b/src/serviceprovider/parallelsdesktop/license.go index 3c67d64..e1ed12e 100644 --- a/src/serviceprovider/parallelsdesktop/license.go +++ b/src/serviceprovider/parallelsdesktop/license.go @@ -7,6 +7,7 @@ import ( "github.com/Parallels/prl-devops-service/errors" "github.com/Parallels/prl-devops-service/helpers" "github.com/Parallels/prl-devops-service/models" + "github.com/cjlapao/common-go/helper" ) func (s *ParallelsService) GetLicense() (*models.ParallelsDesktopLicense, error) { @@ -63,6 +64,13 @@ func (s *ParallelsService) InstallLicense(licenseKey, username, password string) return err } + if helper.FileExists("~/parallels_password.txt") { + err := helper.DeleteFile("~/parallels_password.txt") + if err != nil { + return err + } + } + return nil } else { _, err := helpers.ExecuteWithNoOutput(installLicenseCmd) @@ -70,6 +78,13 @@ func (s *ParallelsService) InstallLicense(licenseKey, username, password string) return err } + if helper.FileExists("~/parallels_password.txt") { + err := helper.DeleteFile("~/parallels_password.txt") + if err != nil { + return err + } + } + return nil } } diff --git a/src/serviceprovider/parallelsdesktop/main.go b/src/serviceprovider/parallelsdesktop/main.go index 59b1505..86791c6 100644 --- a/src/serviceprovider/parallelsdesktop/main.go +++ b/src/serviceprovider/parallelsdesktop/main.go @@ -35,6 +35,7 @@ type ParallelsService struct { executable string serverExecutable string Info *models.ParallelsDesktopInfo + Users []*models.ParallelsDesktopUser isLicensed bool installed bool dependencies []interfaces.Service @@ -662,6 +663,61 @@ func (s *ParallelsService) GetInfo() (*models.ParallelsDesktopInfo, error) { return s.Info, nil } +func (s *ParallelsService) GetUsers(ctx basecontext.ApiContext) ([]*models.ParallelsDesktopUser, error) { + if s.Users != nil { + return s.Users, nil + } + + stdout, err := helpers.ExecuteWithNoOutput(helpers.Command{ + Command: s.serverExecutable, + Args: []string{"user", "list", "--json"}, + }) + if err != nil { + return nil, err + } + + var users []*models.ParallelsDesktopUser + err = json.Unmarshal([]byte(stdout), &users) + if err != nil { + return nil, err + } + + s.Users = users + + return s.Users, nil +} + +func (s *ParallelsService) GetUser(ctx basecontext.ApiContext, user string) (*models.ParallelsDesktopUser, error) { + if s.Users != nil || len(s.Users) == 0 { + s.GetUsers(ctx) + } + + for _, u := range s.Users { + currentName := strings.Split(u.Name, "@") + if strings.EqualFold(currentName[0], user) { + return u, nil + } + } + + return nil, errors.Newf("User %s not found", user) +} + +func (s *ParallelsService) GetUserHome(ctx basecontext.ApiContext, user string) (string, error) { + if s.Users != nil || len(s.Users) == 0 { + _, err := s.GetUsers(ctx) + if err != nil { + return "", err + } + } + + parallelsUser, err := s.GetUser(ctx, user) + if err != nil { + return "", err + } + + return parallelsUser.DefVMHome, nil +} + func (s *ParallelsService) ConfigureVm(ctx basecontext.ApiContext, id string, setOperations *models.VirtualMachineConfigRequest) error { vm, err := s.findVm(ctx, id) if err != nil { diff --git a/src/startup/main.go b/src/startup/main.go index 130a6ea..3996c1f 100644 --- a/src/startup/main.go +++ b/src/startup/main.go @@ -14,7 +14,9 @@ import ( "github.com/Parallels/prl-devops-service/security/password" "github.com/Parallels/prl-devops-service/serviceprovider" "github.com/Parallels/prl-devops-service/serviceprovider/system" + "github.com/Parallels/prl-devops-service/startup/migrations" "github.com/Parallels/prl-devops-service/telemetry" + cryptorand "github.com/cjlapao/common-go-cryptorand" ) const ( @@ -32,6 +34,8 @@ func Init(ctx basecontext.ApiContext) { func Start(ctx basecontext.ApiContext) { cfg := config.Get() + schemaMigrations := make([]migrations.Migration, 0) + schemaMigrations = append(schemaMigrations, migrations.Version0_6_0{}) system := system.SystemService{} if system.GetOperatingSystem() != "macos" { @@ -50,6 +54,12 @@ func Start(ctx basecontext.ApiContext) { panic(err) } + for _, migration := range schemaMigrations { + if err := migration.Apply(); err != nil { + ctx.LogErrorf("Error applying migration: %v", err) + } + } + if cfg.IsOrchestrator() { ctx := basecontext.NewRootBaseContext() ctx.LogInfof("Starting Orchestrator Background Service") @@ -60,13 +70,17 @@ func Start(ctx basecontext.ApiContext) { createdKey := false localhost, _ := dbService.GetOrchestratorHost(ctx, hostName) apiKey, err := dbService.GetApiKey(ctx, ORCHESTRATOR_KEY_NAME) - secret := serviceprovider.Get().HardwareSecret if err != nil { if errors.GetSystemErrorCode(err) != 404 { ctx.LogErrorf("Error getting orchestrator key: %v", err) panic(err) } } + secret, err := cryptorand.GetAlphaNumericRandomString(64) + if err != nil { + ctx.LogErrorf("Error generating secret: %v", err) + panic(err) + } if apiKey == nil { _, err := dbService.CreateApiKey(ctx, models.ApiKey{ @@ -99,7 +113,12 @@ func Start(ctx basecontext.ApiContext) { }) } else { if createdKey { - secret := serviceprovider.Get().HardwareSecret + secret, err := cryptorand.GetAlphaNumericRandomString(64) + if err != nil { + ctx.LogErrorf("Error generating secret: %v", err) + panic(err) + } + localhost.Authentication = &models.OrchestratorHostAuthentication{ ApiKey: base64.StdEncoding.EncodeToString([]byte(ORCHESTRATOR_KEY_NAME + ":" + secret)), } diff --git a/src/startup/migrations/common.go b/src/startup/migrations/common.go new file mode 100644 index 0000000..7f8ef06 --- /dev/null +++ b/src/startup/migrations/common.go @@ -0,0 +1,88 @@ +package migrations + +import ( + "errors" + "strconv" + "strings" +) + +type VersionComparisonResult int + +const ( + VersionLowerThanTarget VersionComparisonResult = -1 + VersionEqualToTarget VersionComparisonResult = 0 + VersionHigherThanTarget VersionComparisonResult = 1 +) + +func compareVersions(version, targetVersion string) (VersionComparisonResult, error) { + parts := strings.Split(version, ".") + if len(parts) > 3 { + return -1, errors.New("invalid version format") + } + if len(parts) == 1 { + parts = append(parts, "0") + } + if len(parts) == 2 { + parts = append(parts, "0") + } + + targetParts := strings.Split(targetVersion, ".") + if len(targetParts) > 3 { + return -1, errors.New("invalid version format") + } + + if len(targetParts) == 1 { + targetParts = append(targetParts, "0") + } + if len(targetParts) == 2 { + targetParts = append(targetParts, "0") + } + + major, err := strconv.Atoi(parts[0]) + if err != nil { + return -1, errors.New("invalid version format") + } + targetMajor, err := strconv.Atoi(targetParts[0]) + if err != nil { + return -1, errors.New("invalid version format") + } + + minor, err := strconv.Atoi(parts[1]) + if err != nil { + return -1, errors.New("invalid version format") + } + targetMinor, err := strconv.Atoi(targetParts[1]) + if err != nil { + return -1, errors.New("invalid version format") + } + + patch, err := strconv.Atoi(parts[2]) + if err != nil { + return -1, errors.New("invalid version format") + } + targetPatch, err := strconv.Atoi(targetParts[2]) + if err != nil { + return -1, errors.New("invalid version format") + } + + if major > targetMajor { + return 1, nil + } + if major < targetMajor { + return -1, nil + } + if minor > targetMinor { + return 1, nil + } + if minor < targetMinor { + return -1, nil + } + if patch > targetPatch { + return 1, nil + } + if patch < targetPatch { + return -1, nil + } + + return 0, nil +} diff --git a/src/startup/migrations/common_test.go b/src/startup/migrations/common_test.go new file mode 100644 index 0000000..86a6f89 --- /dev/null +++ b/src/startup/migrations/common_test.go @@ -0,0 +1,156 @@ +package migrations + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCompareVersion(t *testing.T) { + tests := []struct { + version string + targetVersion string + expected VersionComparisonResult + expectedErr error + }{ + { + version: "0.2.3", + targetVersion: "1.2.3", + expected: VersionLowerThanTarget, + expectedErr: nil, + }, + { + version: "1.2.3", + targetVersion: "1.2.3", + expected: VersionEqualToTarget, + expectedErr: nil, + }, + { + version: "1.2.3", + targetVersion: "1.2.4", + expected: VersionLowerThanTarget, + expectedErr: nil, + }, + { + version: "1.2.4", + targetVersion: "1.2.3", + expected: VersionHigherThanTarget, + expectedErr: nil, + }, + { + version: "1.2.3", + targetVersion: "1.3.0", + expected: VersionLowerThanTarget, + expectedErr: nil, + }, + { + version: "1.3.0", + targetVersion: "1.2.3", + expected: VersionHigherThanTarget, + expectedErr: nil, + }, + { + version: "1.2.3", + targetVersion: "2.0.0", + expected: VersionLowerThanTarget, + expectedErr: nil, + }, + { + version: "2.0.0", + targetVersion: "1.2.3", + expected: VersionHigherThanTarget, + expectedErr: nil, + }, + { + version: "1.2.3", + targetVersion: "1.2", + expected: VersionHigherThanTarget, + expectedErr: nil, + }, + { + version: "1.2", + targetVersion: "1.2.3", + expected: VersionLowerThanTarget, + expectedErr: nil, + }, + { + version: "1.2.3", + targetVersion: "1", + expected: VersionHigherThanTarget, + expectedErr: nil, + }, + { + version: "1", + targetVersion: "1.2.3", + expected: VersionLowerThanTarget, + expectedErr: nil, + }, + { + version: "1.2.3", + targetVersion: "1.2.3.4", + expected: VersionLowerThanTarget, + expectedErr: errors.New("invalid version format"), + }, + { + version: "1.2.3.4", + targetVersion: "1.2.3", + expected: VersionLowerThanTarget, + expectedErr: errors.New("invalid version format"), + }, + { + version: "1.2.3", + targetVersion: "1.2.3.0", + expected: VersionLowerThanTarget, + expectedErr: errors.New("invalid version format"), + }, + { + version: "1.2.3.0", + targetVersion: "1.2.3", + expected: VersionLowerThanTarget, + expectedErr: errors.New("invalid version format"), + }, + { + version: "a.b.c", + targetVersion: "c.d.e", + expected: VersionEqualToTarget, + expectedErr: errors.New("invalid version format"), + }, + { + version: "0.b.c", + targetVersion: "a.d.e", + expected: VersionEqualToTarget, + expectedErr: errors.New("invalid version format"), + }, + { + version: "0.b.c", + targetVersion: "0.d.e", + expected: VersionEqualToTarget, + expectedErr: errors.New("invalid version format"), + }, + { + version: "0.0.c", + targetVersion: "0.d.e", + expected: VersionEqualToTarget, + expectedErr: errors.New("invalid version format"), + }, + { + version: "0.0.c", + targetVersion: "0.0.e", + expected: VersionEqualToTarget, + expectedErr: errors.New("invalid version format"), + }, + { + version: "0.0.0", + targetVersion: "0.0.e", + expected: VersionEqualToTarget, + expectedErr: errors.New("invalid version format"), + }, + } + + for _, test := range tests { + result, err := compareVersions(test.version, test.targetVersion) + assert.Equalf(t, test.expectedErr, err, "expected %v, got %v for version %v and targetVersion %v", test.expectedErr, err, test.version, test.targetVersion) + assert.Equalf(t, test.expected, result, "expected %v, got %v for version %v and targetVersion %v", test.expected, result, test.version, test.targetVersion) + } +} diff --git a/src/startup/migrations/interface.go b/src/startup/migrations/interface.go new file mode 100644 index 0000000..8937866 --- /dev/null +++ b/src/startup/migrations/interface.go @@ -0,0 +1,5 @@ +package migrations + +type Migration interface { + Apply() error +} diff --git a/src/startup/migrations/service.go b/src/startup/migrations/service.go new file mode 100644 index 0000000..04ca228 --- /dev/null +++ b/src/startup/migrations/service.go @@ -0,0 +1,35 @@ +package migrations + +import ( + "github.com/Parallels/prl-devops-service/basecontext" + "github.com/Parallels/prl-devops-service/data" + "github.com/Parallels/prl-devops-service/serviceprovider" +) + +type MigrationService struct { + Context basecontext.ApiContext + DbService *data.JsonDatabase + schemaVersion string +} + +func Init() (*MigrationService, error) { + result := MigrationService{} + result.Context = basecontext.NewRootBaseContext() + result.DbService = serviceprovider.Get().JsonDatabase + err := result.DbService.Connect(result.Context) + if err != nil { + return nil, err + } + + schemaVersion, err := result.DbService.GetSchemaVersion(result.Context) + if err != nil { + return nil, err + } + if schemaVersion == "" { + schemaVersion = "0.0.0" + } + + result.schemaVersion = schemaVersion + + return &result, nil +} diff --git a/src/startup/migrations/ver_0_6_0.go b/src/startup/migrations/ver_0_6_0.go new file mode 100644 index 0000000..6a4fc66 --- /dev/null +++ b/src/startup/migrations/ver_0_6_0.go @@ -0,0 +1,46 @@ +package migrations + +import ( + "github.com/Parallels/prl-devops-service/common" +) + +type Version0_6_0 struct{} + +func (v Version0_6_0) Apply() error { + versionTarget := "0.6.0" + svc, err := Init() + if err != nil { + return err + } + + compareResult, err := compareVersions(svc.schemaVersion, versionTarget) + if err != nil { + common.Logger.Error("Error comparing versions: %s", err.Error()) + return err + } + + if compareResult == VersionEqualToTarget || compareResult == VersionHigherThanTarget { + svc.Context.LogDebugf("Schema version is already %s, skipping migration", versionTarget) + return nil + } + + svc.Context.LogInfof("Applying migration to version %s", versionTarget) + hosts, err := svc.DbService.GetOrchestratorHosts(svc.Context, "") + if err != nil { + return err + } + + for _, host := range hosts { + host.Enabled = true + _, err = svc.DbService.UpdateOrchestratorHost(svc.Context, &host) + if err != nil { + return err + } + } + + err = svc.DbService.UpdateSchemaVersion(svc.Context, versionTarget) + if err != nil { + return err + } + return nil +} diff --git a/src/startup/seeds/users.go b/src/startup/seeds/users.go index 5853770..ab90012 100644 --- a/src/startup/seeds/users.go +++ b/src/startup/seeds/users.go @@ -6,6 +6,7 @@ import ( "github.com/Parallels/prl-devops-service/constants" "github.com/Parallels/prl-devops-service/data/models" "github.com/Parallels/prl-devops-service/serviceprovider" + cryptorand "github.com/cjlapao/common-go-cryptorand" ) func SeedDefaultUsers() error { @@ -31,12 +32,17 @@ func SeedDefaultUsers() error { return err } + defaultPassword, err := cryptorand.GetAlphaNumericRandomString(32) + if err != nil { + return err + } + if _, err := db.CreateUser(ctx, models.User{ ID: serviceprovider.Get().HardwareId, Name: "Root", Username: "root", Email: "root@localhost", - Password: serviceprovider.Get().HardwareSecret, + Password: defaultPassword, Roles: []models.Role{ *suRole, },