diff --git a/lib/apiservers/service/restapi/configure_vic_machine.go b/lib/apiservers/service/restapi/configure_vic_machine.go index 9eea16fe74..5a6d3de36b 100644 --- a/lib/apiservers/service/restapi/configure_vic_machine.go +++ b/lib/apiservers/service/restapi/configure_vic_machine.go @@ -80,9 +80,15 @@ func configureAPI(api *operations.VicMachineAPI) http.Handler { // GET /container/target/{target}/vch/{vch-id}/certificate api.GetTargetTargetVchVchIDCertificateHandler = &handlers.VCHCertGet{} + // GET /container/target/{target}/vch/{vch-id}/log + api.GetTargetTargetVchVchIDLogHandler = &handlers.VCHLogGet{} + // GET /container/target/{target}/datacenter/{datacenter}/vch/{vch-id}/certificate api.GetTargetTargetDatacenterDatacenterVchVchIDCertificateHandler = &handlers.VCHDatacenterCertGet{} + // GET /container/target/{target}/datacenter/{datacenter}/vch/{vch-id}/log + api.GetTargetTargetDatacenterDatacenterVchVchIDLogHandler = &handlers.VCHDatacenterLogGet{} + // PUT /container/target/{target}/vch/{vch-id} api.PutTargetTargetVchVchIDHandler = operations.PutTargetTargetVchVchIDHandlerFunc(func(params operations.PutTargetTargetVchVchIDParams, principal interface{}) middleware.Responder { return middleware.NotImplemented("operation .PutTargetTargetVchVchID has not yet been implemented") diff --git a/lib/apiservers/service/restapi/handlers/common.go b/lib/apiservers/service/restapi/handlers/common.go index e061c1f13d..915cb57369 100644 --- a/lib/apiservers/service/restapi/handlers/common.go +++ b/lib/apiservers/service/restapi/handlers/common.go @@ -17,6 +17,7 @@ package handlers import ( "context" "fmt" + "net/http" "net/url" "github.com/vmware/govmomi/object" @@ -48,14 +49,14 @@ func buildData(ctx context.Context, url url.URL, user string, pass string, thumb if datacenter != nil { validator, err := validateTarget(ctx, &d) if err != nil { - return nil, util.WrapError(500, err) + return nil, util.WrapError(http.StatusInternalServerError, err) } datacenterManagedObjectReference := types.ManagedObjectReference{Type: "Datacenter", Value: *datacenter} datacenterObject, err := validator.Session.Finder.ObjectReference(ctx, datacenterManagedObjectReference) if err != nil { - return nil, util.WrapError(404, err) + return nil, util.WrapError(http.StatusNotFound, err) } d.Target.URL.Path = datacenterObject.(*object.Datacenter).InventoryPath @@ -130,18 +131,18 @@ func getVCHConfig(op trace.Operation, d *data.Data) (*config.VirtualContainerHos // TODO(jzt): abstract some of this boilerplate into helpers validator, err := validateTarget(op.Context, d) if err != nil { - return nil, util.WrapError(400, err) + return nil, util.WrapError(http.StatusBadRequest, err) } executor := management.NewDispatcher(validator.Context, validator.Session, nil, false) vch, err := executor.NewVCHFromID(d.ID) if err != nil { - return nil, util.NewError(500, fmt.Sprintf("failed to create VCH %s: %s", d.ID, err)) + return nil, util.NewError(http.StatusNotFound, fmt.Sprintf("Unable to find VCH %s: %s", d.ID, err)) } err = validate.SetDataFromVM(validator.Context, validator.Session.Finder, vch, d) if err != nil { - return nil, util.NewError(500, fmt.Sprintf("Failed to load VCH data: %s", err)) + return nil, util.NewError(http.StatusInternalServerError, fmt.Sprintf("Failed to load VCH data: %s", err)) } vchConfig, err := executor.GetNoSecretVCHConfig(vch) diff --git a/lib/apiservers/service/restapi/handlers/vch_log_get.go b/lib/apiservers/service/restapi/handlers/vch_log_get.go new file mode 100644 index 0000000000..9d76579dfd --- /dev/null +++ b/lib/apiservers/service/restapi/handlers/vch_log_get.go @@ -0,0 +1,200 @@ +// Copyright 2017 VMware, Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package handlers + +import ( + "bytes" + "context" + "fmt" + "net/http" + "net/url" + "sort" + "strings" + + "github.com/go-openapi/runtime/middleware" + + "github.com/vmware/vic/lib/apiservers/service/models" + "github.com/vmware/vic/lib/apiservers/service/restapi/handlers/util" + "github.com/vmware/vic/lib/apiservers/service/restapi/operations" + "github.com/vmware/vic/lib/install/data" + "github.com/vmware/vic/lib/install/management" + "github.com/vmware/vic/lib/install/validate" + "github.com/vmware/vic/pkg/trace" + "github.com/vmware/vic/pkg/vsphere/datastore" +) + +const ( + logFilePrefix = "vic-machine" // logFilePrefix is the prefix for file names of all vic-machine log files + logFileSuffix = ".log" // logFileSuffix is the suffix for file names of all vic-machine log files +) + +// VCHLogGet is the handler for getting the log messages for a VCH +type VCHLogGet struct { +} + +// VCHDatacenterLogGet is the handler for getting the log messages for a VCH within a Datacenter +type VCHDatacenterLogGet struct { +} + +func (h *VCHLogGet) Handle(params operations.GetTargetTargetVchVchIDLogParams, principal interface{}) middleware.Responder { + d, err := buildData(params.HTTPRequest.Context(), + url.URL{Host: params.Target}, + principal.(Credentials).user, + principal.(Credentials).pass, + params.Thumbprint, + nil, + nil) + if err != nil { + return operations.NewGetTargetTargetVchVchIDLogDefault(util.StatusCode(err)).WithPayload(&models.Error{Message: err.Error()}) + } + + d.ID = params.VchID + op := trace.NewOperation(params.HTTPRequest.Context(), "vch: %s", params.VchID) + + helper, err := getDatastoreHelper(op.Context, d) + if err != nil { + return operations.NewGetTargetTargetVchVchIDLogDefault(util.StatusCode(err)).WithPayload(&models.Error{Message: err.Error()}) + } + + logFilePaths, err := getAllLogFilePaths(op.Context, helper) + if err != nil { + return operations.NewGetTargetTargetVchVchIDLogDefault(util.StatusCode(err)).WithPayload(&models.Error{Message: err.Error()}) + } + + output, err := getContentFromLogFiles(op.Context, helper, logFilePaths) + if err != nil { + return operations.NewGetTargetTargetVchVchIDLogDefault(util.StatusCode(err)).WithPayload(&models.Error{Message: err.Error()}) + } + + return operations.NewGetTargetTargetVchVchIDLogOK().WithPayload(output) +} + +func (h *VCHDatacenterLogGet) Handle(params operations.GetTargetTargetDatacenterDatacenterVchVchIDLogParams, principal interface{}) middleware.Responder { + d, err := buildData(params.HTTPRequest.Context(), + url.URL{Host: params.Target}, + principal.(Credentials).user, + principal.(Credentials).pass, + params.Thumbprint, + ¶ms.Datacenter, + nil) + if err != nil { + return operations.NewGetTargetTargetDatacenterDatacenterVchVchIDLogDefault(util.StatusCode(err)).WithPayload(&models.Error{Message: err.Error()}) + } + + d.ID = params.VchID + op := trace.NewOperation(params.HTTPRequest.Context(), "vch: %s", params.VchID) + + helper, err := getDatastoreHelper(op.Context, d) + if err != nil { + return operations.NewGetTargetTargetDatacenterDatacenterVchVchIDLogDefault(util.StatusCode(err)).WithPayload(&models.Error{Message: err.Error()}) + } + + logFilePaths, err := getAllLogFilePaths(op.Context, helper) + if err != nil { + return operations.NewGetTargetTargetDatacenterDatacenterVchVchIDLogDefault(util.StatusCode(err)).WithPayload(&models.Error{Message: err.Error()}) + } + + output, err := getContentFromLogFiles(op.Context, helper, logFilePaths) + if err != nil { + return operations.NewGetTargetTargetDatacenterDatacenterVchVchIDLogDefault(util.StatusCode(err)).WithPayload(&models.Error{Message: err.Error()}) + } + + return operations.NewGetTargetTargetDatacenterDatacenterVchVchIDLogOK().WithPayload(output) +} + +// getDatastoreHelper validates the VCH and returns the datastore helper for the VCH. It errors when validation fails or when datastore is not ready +func getDatastoreHelper(ctx context.Context, d *data.Data) (*datastore.Helper, error) { + // TODO (angiew): abstract some of the boilerplate into helpers in common.go + validator, err := validateTarget(ctx, d) + if err != nil { + return nil, util.WrapError(http.StatusBadRequest, err) + } + + executor := management.NewDispatcher(validator.Context, validator.Session, nil, false) + vch, err := executor.NewVCHFromID(d.ID) + if err != nil { + return nil, util.NewError(http.StatusNotFound, fmt.Sprintf("Unable to find VCH %s: %s", d.ID, err)) + } + + if err := validate.SetDataFromVM(validator.Context, validator.Session.Finder, vch, d); err != nil { + return nil, util.NewError(http.StatusInternalServerError, fmt.Sprintf("Failed to load VCH data: %s", err)) + } + + // Get VCH configuration + vchConfig, err := executor.GetNoSecretVCHConfig(vch) + if err != nil { + return nil, fmt.Errorf("Unable to retrieve VCH information: %s", err) + } + + // Relative path of datastore folder + vmPath := vchConfig.ImageStores[0].Path + + // Get VCH datastore object + ds, err := validator.Session.Finder.Datastore(validator.Context, vchConfig.ImageStores[0].Host) + if err != nil { + return nil, util.NewError(http.StatusNotFound, fmt.Sprintf("Datastore folder not found for VCH %s: %s", d.ID, err)) + } + + // Create a new datastore helper for file finding + helper, err := datastore.NewHelper(ctx, validator.Session, ds, vmPath) + if err != nil { + return nil, fmt.Errorf("Unable to get datastore helper: %s", err) + } + + return helper, nil +} + +// getAllLogFilePaths returns a list of all log file paths under datastore folder, errors out when no log file found +func getAllLogFilePaths(ctx context.Context, helper *datastore.Helper) ([]string, error) { + res, err := helper.Ls(ctx, "") + if err != nil { + return nil, fmt.Errorf("Unable to list all files under datastore: %s", err) + } + + var paths []string + for _, f := range res.File { + path := f.GetFileInfo().Path + if strings.HasPrefix(path, logFilePrefix) && strings.HasSuffix(path, logFileSuffix) { + paths = append(paths, path) + } + } + + if len(paths) == 0 { + return nil, util.NewError(http.StatusNotFound, "No log file available in datastore folder") + } + + return paths, nil +} + +// getContentFromLogFile downloads all log files in the list, concatenates the content of each log file and outputs a string of contents +func getContentFromLogFiles(ctx context.Context, helper *datastore.Helper, paths []string) (string, error) { + var buffer bytes.Buffer + + // sort log files based on timestamp + sort.Strings(paths) + + for _, p := range paths { + reader, err := helper.Download(ctx, p) + if err != nil { + return "", fmt.Errorf("Unable to download log file %s: %s", p, err) + } + + if _, err := buffer.ReadFrom(reader); err != nil { + return "", fmt.Errorf("Error reading from log file %s: %s", p, err) + } + } + + return string(buffer.Bytes()), nil +} diff --git a/lib/apiservers/service/swagger.json b/lib/apiservers/service/swagger.json index 6789458301..2f9c3e7cff 100644 --- a/lib/apiservers/service/swagger.json +++ b/lib/apiservers/service/swagger.json @@ -194,6 +194,21 @@ } } }, + "/target/{target}/vch/{vchId}/log": { + "get": { + "summary": "Access log messages for a VCH", + "description": "Making a `GET` request on /log under a VCH resource will return the log messages for vic-machine processes on a VCH", + "parameters": [ + { "$ref": "#/parameters/target" }, + { "$ref": "#/parameters/vch-id" }, + { "$ref": "#/parameters/thumbprint" } + ], + "responses": { + "200": { "$ref": "#/responses/vch-log" }, + "default": { "$ref": "#/responses/error" } + } + } + }, "/target/{target}/datacenter/{datacenter}": { "get": { "summary": "Show information about the specified vSphere resources", @@ -353,6 +368,22 @@ "default": { "$ref": "#/responses/error" } } } + }, + "/target/{target}/datacenter/{datacenter}/vch/{vchId}/log": { + "get": { + "summary": "Access log messages for a VCH in a particular datacenter", + "description": "Making a `GET` request on /log under a VCH resource will return the log messages for vic-machine processes on a VCH.", + "parameters": [ + { "$ref": "#/parameters/target" }, + { "$ref": "#/parameters/datacenter" }, + { "$ref": "#/parameters/vch-id" }, + { "$ref": "#/parameters/thumbprint" } + ], + "responses": { + "200": { "$ref": "#/responses/vch-log" }, + "default": { "$ref": "#/responses/error" } + } + } } }, "definitions": { @@ -897,6 +928,12 @@ "description": "A VCH host certificate", "schema": { "$ref": "#/definitions/PEM" } }, + "vch-log": { + "description": "Log messages for a VCH", + "schema": { + "type": "string" + } + }, "vsphere-task": { "description": "A vSphere task", "schema": { diff --git a/lib/install/management/create.go b/lib/install/management/create.go index 745d25eeb2..ccdbac7bee 100644 --- a/lib/install/management/create.go +++ b/lib/install/management/create.go @@ -64,11 +64,11 @@ func (d *Dispatcher) CreateVCH(conf *config.VirtualContainerHostConfigSpec, sett // send the signal to VCH logger to indicate VCH datastore path is ready datastoreReadySignal := vchlog.DatastoreReadySignal{ - Datastore: d.session.Datastore, - LogFileName: "vic-machine-create", - Operation: trace.NewOperation(d.ctx, "vic-machine create"), - VMPathName: d.vmPathName, - Timestamp: time.Now().UTC().Format(timeFormat), + Datastore: d.session.Datastore, + Name: "create", + Operation: trace.NewOperation(d.ctx, "create"), + VMPathName: d.vmPathName, + Timestamp: time.Now().UTC().Format(timeFormat), } vchlog.Signal(datastoreReadySignal) diff --git a/lib/install/vchlog/vchlogger.go b/lib/install/vchlog/vchlogger.go index 80a0cff674..afa0e2f778 100644 --- a/lib/install/vchlog/vchlogger.go +++ b/lib/install/vchlog/vchlogger.go @@ -23,15 +23,16 @@ import ( // DatastoreReadySignal serves as a signal struct indicating datastore folder path is available // Datastore: the govmomi datastore object +// Name: the name of the vic-machine process that sends the signal (e.g. "create", "inspect") // LogFileName: the filename of the destination path on datastore // Context: the caller context when sending the signal // VMPathName: the datastore path type DatastoreReadySignal struct { - Datastore *object.Datastore - LogFileName string - Operation trace.Operation - VMPathName string - Timestamp string + Datastore *object.Datastore + Name string + Operation trace.Operation + VMPathName string + Timestamp string } // pipe: the streaming readwriter pipe to hold log messages @@ -50,7 +51,7 @@ func Init() { func Run() { sig := <-signalChan // suffix the log file name with caller operation ID and timestamp - logFileName := sig.LogFileName + "_time_" + sig.Timestamp + "_op_" + sig.Operation.ID() + logFileName := "vic-machine" + "_" + sig.Timestamp + "_" + sig.Name + "_" + sig.Operation.ID() + ".log" sig.Datastore.Upload(sig.Operation.Context, pipe, path.Join(sig.VMPathName, logFileName), nil) } diff --git a/tests/test-cases/Group23-VIC-Machine-Service/23-05-VCH-Logs.md b/tests/test-cases/Group23-VIC-Machine-Service/23-05-VCH-Logs.md new file mode 100644 index 0000000000..1a96975652 --- /dev/null +++ b/tests/test-cases/Group23-VIC-Machine-Service/23-05-VCH-Logs.md @@ -0,0 +1,26 @@ +Test 23-05 - VCH Logs +======= + +# Purpose: +To verify vic-machine-server can provide logs for a VCH host when available + +# References: +[1 - VIC Machine Service API Design Doc - VCH Certificate](../../../doc/design/vic-machine/service.md) + +# Environment: +This test requires that a vSphere server is running and available, where VCH can be deployed. + +# Test Steps: +1. Deloy a VCH into the test environment +2. Verify that the creation log is available after the VCH is created using the vic-machine-service +3. Verify that the creation log is available for its particular datacenter using the vic-machine-service +4. Delete the log file from VCH datastore folder +5. Verify that creation log is unavailable (404) using the vic-machine service +6. Verify that creation log is unavailable (404) for its particular datacenter using the vic-machine-service + +# Expected Outcome: +* Step 2-3 should succeed and output should contain log message that the creation is completed successfully +* Step 5-6 should error with a 404 (not found) as no log file exists + +# Possible Problems: +None diff --git a/tests/test-cases/Group23-VIC-Machine-Service/23-05-VCH-Logs.robot b/tests/test-cases/Group23-VIC-Machine-Service/23-05-VCH-Logs.robot new file mode 100644 index 0000000000..11a6f2e30b --- /dev/null +++ b/tests/test-cases/Group23-VIC-Machine-Service/23-05-VCH-Logs.robot @@ -0,0 +1,71 @@ +# Copyright 2017 VMware, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License + +*** Settings *** +Documentation Test 23-05 - VCH Logs +Resource ../../resources/Util.robot +Suite Setup Start VIC Machine Server +Suite Teardown Terminate All Processes kill=True +Test Setup Install VIC Appliance To Test Server +Test Teardown Cleanup VIC Appliance On Test Server +Default Tags + +*** Keywords *** +Start VIC Machine Server + Start Process ./bin/vic-machine-server --port 31337 --scheme http shell=True cwd=/go/src/github.com/vmware/vic + Sleep 1s for service to start + +Curl No Datacenter + [Arguments] ${vch-id} ${auth} + ${rc} ${output}= Run And Return Rc And Output curl -s -w "\%{http_code}\n" -X GET "http://127.0.0.1:31337/container/target/%{TEST_URL}/vch/${vch-id}/log?thumbprint=%{TEST_THUMBPRINT}" -H "authorization: Basic ${auth}" + [Return] ${rc} ${output} + +Curl Datacenter + [Arguments] ${vch-id} ${auth} + ${dcID}= Get Datacenter ID + ${rc} ${output}= Run And Return Rc And Output curl -s -w "\%{http_code}\n" -X GET "http://127.0.0.1:31337/container/target/%{TEST_URL}/datacenter/${dcID}/vch/${vch-id}/log?thumbprint=%{TEST_THUMBPRINT}" -H "authorization: Basic ${auth}" + [Return] ${rc} ${output} + +Delete Log File From VCH Datastore + ${filename}= Run GOVC_DATASTORE=%{TEST_DATASTORE} govc datastore.ls %{VCH-NAME} | grep vic-machine_ + Should Not Be Empty ${filename} + ${output}= Run govc datastore.rm "%{VCH-NAME}/${filename}" + ${filename}= Run GOVC_DATASTORE=%{TEST_DATASTORE} govc datastore.ls %{VCH-NAME} | grep vic-machine_ + Should Be Empty ${filename} + + +*** Test Cases *** +Get VCH Creation Log succeeds after installation completes + ${id}= Get VCH ID %{VCH-NAME} + ${auth}= Evaluate base64.b64encode("%{TEST_USERNAME}:%{TEST_PASSWORD}") modules=base64 + ${rc} ${output}= Curl No Datacenter ${id} ${auth} + Should Be Equal As Integers ${rc} 0 + Should Contain ${output} Installer completed successfully + ${status}= Get Line ${output} -1 + Should Be Equal As Integers 200 ${status} + ${rc} ${outputDC}= Curl Datacenter ${id} ${auth} + Should Be Equal As Integers ${rc} 0 + Should Be Equal ${output} ${outputDC} + +Get VCH Creation log errors with 404 after log file is deleted + ${id}= Get VCH ID %{VCH-NAME} + ${auth}= Evaluate base64.b64encode("%{TEST_USERNAME}:%{TEST_PASSWORD}") modules=base64 + Delete Log File From VCH Datastore + ${rc} ${output}= Curl No Datacenter ${id} ${auth} + Should Be Equal As Integers ${rc} 0 + ${status}= Get Line ${output} -1 + Should Be Equal As Integers 404 ${status} + ${rc} ${outputDC}= Curl Datacenter ${id} ${auth} + Should Be Equal As Integers ${rc} 0 + Should Be Equal ${output} ${outputDC} diff --git a/tests/test-cases/Group6-VIC-Machine/6-04-Create-Basic.robot b/tests/test-cases/Group6-VIC-Machine/6-04-Create-Basic.robot index fb044de194..3582fda8b3 100644 --- a/tests/test-cases/Group6-VIC-Machine/6-04-Create-Basic.robot +++ b/tests/test-cases/Group6-VIC-Machine/6-04-Create-Basic.robot @@ -262,7 +262,7 @@ Creation log file uploaded to datastore ${output}= Run bin/vic-machine-linux create --name=%{VCH-NAME} --target=%{TEST_URL} --thumbprint=%{TEST_THUMBPRINT} --user=%{TEST_USERNAME} --image-store=%{TEST_DATASTORE} --appliance-iso=bin/appliance.iso --bootstrap-iso=bin/bootstrap.iso --password=%{TEST_PASSWORD} --force=true --bridge-network=%{BRIDGE_NETWORK} --public-network=%{PUBLIC_NETWORK} --compute-resource=%{TEST_RESOURCE} --timeout %{TEST_TIMEOUT} ${vicmachinetls} --insecure-registry harbor.ci.drone.local - ${filename}= Run GOVC_DATASTORE=%{TEST_DATASTORE} govc datastore.ls %{VCH-NAME} | grep vic-machine-create + ${filename}= Run GOVC_DATASTORE=%{TEST_DATASTORE} govc datastore.ls %{VCH-NAME} | grep vic-machine_ Should Not Be Empty ${filename} ${output}= Run govc datastore.tail -n 1 "%{VCH-NAME}/${filename}" Should Contain ${output} Installer completed successfully