diff --git a/skytap/client.go b/skytap/client.go index 42ec1bd..d9471f3 100644 --- a/skytap/client.go +++ b/skytap/client.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" "io" "io/ioutil" @@ -23,9 +24,18 @@ const ( headerRetryAfter = "Retry-After" defRetryAfter = 10 - defRetryCount = 30 + defRetryCount = 60 + + noRunStateCheck RunStateCheckStatus = 0 + envRunStateCheck RunStateCheckStatus = 1 + vmRunStateCheck RunStateCheckStatus = 2 + + requestNotAsExpected = "request not as expected" ) +// RunStateCheckStatus value of the run check status used to determine checking of request and response. +type RunStateCheckStatus int + // Client is a client to manage and configure the skytap cloud type Client struct { // HTTP client to be used for communicating with the SkyTap SDK @@ -79,7 +89,6 @@ type ListFilter struct { // ErrorResponse is the general purpose struct to hold error data type ErrorResponse struct { - // HTTP response that caused this error Response *http.Response @@ -92,8 +101,22 @@ type ErrorResponse struct { // RetryAfter is sometimes returned by the server RetryAfter *int - // RequiresRetry indicates whether a retry is required - RequiresRetry bool + // RateLimited informs Skytap is rate limiting + RateLimited *int +} + +type environmentVMRunState struct { + environmentID *string + vmID *string + adapterID *string + environment []EnvironmentRunstate + vm []VMRunstate + diskIdentification []DiskIdentification + runStateCheckStatus RunStateCheckStatus +} + +type responseComparator interface { + compareResponse(ctx context.Context, c *Client, v interface{}, state *environmentVMRunState) (string, bool) } // Error returns a formatted error @@ -188,40 +211,107 @@ func (c *Client) newRequest(ctx context.Context, method, path string, body inter return req, nil } -func (c *Client) do(ctx context.Context, req *http.Request, v interface{}) (*http.Response, error) { - var resp *http.Response - var err error - var makeRequest = true +func (c *Client) do(ctx context.Context, req *http.Request, v interface{}, state *environmentVMRunState, payload responseComparator) (*http.Response, error) { + if req.Method == http.MethodPost || req.Method == http.MethodPut || req.Method == http.MethodDelete { + for i := 0; i < c.retryCount; i++ { + err := c.checkResourceStateUntilSatisfied(ctx, req, state) + if err != nil { + return nil, err + } + resp, retry, err := c.requestPutPostDelete(ctx, req, state, payload, v) + if !retry || err != nil { + return resp, err + } + } + } + return c.request(ctx, req, v) +} - for i := 0; i < c.retryCount+1 && makeRequest; i++ { - resp, err = c.hc.Do(req.WithContext(ctx)) +func (c *Client) request(ctx context.Context, req *http.Request, v interface{}) (*http.Response, error) { + err := logRequest(req) + if err != nil { + return nil, err + } + resp, err := c.hc.Do(req.WithContext(ctx)) + if err != nil { + return nil, err + } + + if resp.StatusCode == http.StatusOK { + err := readResponseBody(resp, v) if err != nil { - break + return nil, err } + } else { + return resp, c.buildErrorResponse(resp) + } - err = c.checkResponse(resp) + return resp, nil +} - if err == nil { - errBody := readResponseBody(resp, v) - if errBody != nil { - break +func (c *Client) requestPutPostDelete(ctx context.Context, req *http.Request, state *environmentVMRunState, payload responseComparator, v interface{}) (*http.Response, bool, error) { + var resp *http.Response + err := logRequest(req) + if err != nil { + return nil, false, err + } + resp, err = c.hc.Do(req.WithContext(ctx)) + if err != nil { + return nil, false, err + } + + code := resp.StatusCode + + if code == http.StatusOK { + err = readResponseBody(resp, v) + if err != nil { + return nil, false, err + } + if payload != nil { + for i := 0; i < c.retryCount; i++ { + if message, ok := payload.compareResponse(ctx, c, v, state); !ok { + c.backoff("response check", fmt.Sprintf("%d", code), message, c.retryAfter) + } else { + return nil, false, nil + } } - makeRequest = false - } else if err.(*ErrorResponse).RequiresRetry { - seconds := *err.(*ErrorResponse).RetryAfter - log.Printf("[INFO] retrying after %d second(s)\n", seconds) - time.Sleep(time.Duration(seconds) * time.Second) - } else { - makeRequest = false } - errBody := resp.Body.Close() - if errBody != nil { - break + return nil, false, err + } + return c.handleError(resp, code) +} + +func (c *Client) handleError(resp *http.Response, code int) (*http.Response, bool, error) { + var errorSpecial *ErrorResponse + errorSpecial = c.buildErrorResponse(resp).(*ErrorResponse) + retryError := "" + if code == http.StatusUnprocessableEntity { + retryError = "StatusUnprocessableEntity" + if !strings.Contains(*errorSpecial.Message, "busy") { + return resp, false, errorSpecial + } + } else if code == http.StatusConflict { + retryError = "StatusConflict" + } else if code == http.StatusLocked { + retryError = "StatusLocked" + } else if code == http.StatusTooManyRequests { + retryError = "StatusTooManyRequests" + } + if retryError != "" { + seconds := c.retryAfter + if errorSpecial.RetryAfter != nil { + seconds = *errorSpecial.RetryAfter } + c.backoff("response check", fmt.Sprintf("%d", code), retryError, seconds) + return resp, true, nil } + return resp, false, errorSpecial +} - return resp, err +func (c *Client) backoff(message string, code string, codeAsString string, snooze int) { + log.Printf("[INFO] SDK %s (%s:%s). Retrying after %d second(s)\n", message, code, codeAsString, snooze) + time.Sleep(time.Duration(snooze) * time.Second) } func readResponseBody(resp *http.Response, v interface{}) error { @@ -232,10 +322,27 @@ func readResponseBody(resp *http.Response, v interface{}) error { } else { err = json.NewDecoder(resp.Body).Decode(v) } + if err != nil { + log.Printf("[ERROR] SDK response payload decoding: (%s)", err.Error()) + } + err = resp.Body.Close() } return err } +func logRequest(req *http.Request) error { + log.Printf("[DEBUG] SDK request (%s), URL (%s), agent (%s)\n", req.Method, req.URL.String(), req.UserAgent()) + if req.Body != nil { + body, err := ioutil.ReadAll(req.Body) + if err != nil { + return err + } + req.Body = ioutil.NopCloser(bytes.NewReader(body)) + log.Printf("[DEBUG] SDK request body (%s)\n", strings.TrimSpace(string(body))) + } + return nil +} + func (c *Client) setRequestListParameters(req *http.Request, params *ListParameters) error { if params == nil { params = DefaultListParameters @@ -266,43 +373,164 @@ func (c *Client) setRequestListParameters(req *http.Request, params *ListParamet return nil } -// checkResponse checks the API response for errors, and returns them if present. A response is considered an -// error if it has a status code outside the 200 range. API error responses are expected to have either no response -// body, or a JSON response body that maps to ErrorResponse. -func (c *Client) checkResponse(r *http.Response) error { - if code := r.StatusCode; code >= http.StatusOK && code <= 299 { - return nil - } - +func (c *Client) buildErrorResponse(r *http.Response) error { errorResponse := &ErrorResponse{Response: r} data, err := ioutil.ReadAll(r.Body) if err == nil && len(data) > 0 { - err := json.Unmarshal(data, errorResponse) - if err != nil { - errorResponse.Message = strToPtr(string(data)) - } + errorResponse.Message = strToPtr(string(data)) + log.Printf("[INFO] SDK response error: (%s)", *errorResponse.Message) } if requestID := r.Header.Get(headerRequestID); requestID != "" { errorResponse.RequestID = strToPtr(requestID) } - if code := r.StatusCode; code == http.StatusLocked || - code == http.StatusTooManyRequests || - code == http.StatusConflict || - (code >= http.StatusInternalServerError && code <= 599) { - if retryAfter := r.Header.Get(headerRetryAfter); retryAfter != "" { - val, err := strconv.Atoi(retryAfter) - if err == nil { - errorResponse.RetryAfter = intToPtr(val) - } else { - errorResponse.RetryAfter = intToPtr(c.retryAfter) - } + if retryAfter := r.Header.Get(headerRetryAfter); retryAfter != "" { + val, err := strconv.Atoi(retryAfter) + if err == nil { + errorResponse.RetryAfter = intToPtr(val) } else { errorResponse.RetryAfter = intToPtr(c.retryAfter) } - errorResponse.RequiresRetry = true + } else { + errorResponse.RetryAfter = intToPtr(c.retryAfter) } return errorResponse } + +func (c *Client) checkResourceStateUntilSatisfied(ctx context.Context, req *http.Request, state *environmentVMRunState) error { + runStateCheck := c.requiresChecking(state) + if runStateCheck > noRunStateCheck { + for i := 0; i < c.retryCount; i++ { + var ok bool + var err error + if runStateCheck == envRunStateCheck { + ok, err = c.getEnvironmentRunState(ctx, state.environmentID, state.environment) + } else { + ok, err = c.getVMRunState(ctx, state.environmentID, state.vmID, state.vm) + } + if err != nil || ok { + return err + } + c.backoff("pre-check loop", "", "", c.retryAfter) + } + return errors.New("timeout waiting for state") + } + return nil +} + +func (c *Client) requiresChecking(state *environmentVMRunState) RunStateCheckStatus { + if state == nil { + return noRunStateCheck + } + return state.runStateCheckStatus +} + +func (c *Client) getEnvironmentRunState(ctx context.Context, id *string, states []EnvironmentRunstate) (bool, error) { + env, err := c.Environments.Get(ctx, *id) + if err != nil { + return false, err + } + if env.Runstate == nil { + return false, errors.New("environment run state not set") + } + ok := c.containsEnvironmentRunState(env.Runstate, states) + log.Printf("[DEBUG] SDK run state of environment (%s) and require: (%s).\n", + *env.Runstate, + c.environmentsRunStatesToString(states)) + return ok, nil +} + +func (c *Client) containsEnvironmentRunState(currentState *EnvironmentRunstate, possibleStates []EnvironmentRunstate) bool { + for _, v := range possibleStates { + if v == *currentState { + return true + } + } + return false +} + +func (c *Client) environmentsRunStatesToString(possibleStates []EnvironmentRunstate) string { + var items []string + for _, v := range possibleStates { + items = append(items, string(v)) + } + return strings.Join(items, ", ") +} + +func (c *Client) getVMRunState(ctx context.Context, environmentID *string, vmID *string, states []VMRunstate) (bool, error) { + vm, err := c.VMs.Get(ctx, *environmentID, *vmID) + if err != nil { + return false, err + } + if vm.Runstate == nil { + return false, errors.New("vm run state not set") + } + ok := c.containsVMRunState(vm.Runstate, states) + log.Printf("[INFO] SDK run state of vm (%s) and require: (%s).\n", + *vm.Runstate, + c.vMRunStatesToString(states)) + return ok, nil +} + +func (c *Client) containsVMRunState(currentState *VMRunstate, possibleStates []VMRunstate) bool { + for _, v := range possibleStates { + if v == *currentState { + return true + } + } + return false +} + +func (c *Client) vMRunStatesToString(possibleStates []VMRunstate) string { + var items []string + for _, v := range possibleStates { + items = append(items, string(v)) + } + return strings.Join(items, ", ") +} + +func envRunStateNotBusy(environmentID string) *environmentVMRunState { + return &environmentVMRunState{ + environmentID: strToPtr(environmentID), + environment: []EnvironmentRunstate{ + EnvironmentRunstateRunning, + EnvironmentRunstateStopped, + EnvironmentRunstateSuspended, + EnvironmentRunstateHalted}, + runStateCheckStatus: envRunStateCheck, + } +} + +func envRunStateNotBusyWithVM(environmentID string, vmID string) *environmentVMRunState { + state := envRunStateNotBusy(environmentID) + state.vmID = strToPtr(vmID) + return state +} + +func vmRunStateNotBusy(environmentID string, vmID string) *environmentVMRunState { + return &environmentVMRunState{ + environmentID: strToPtr(environmentID), + vmID: strToPtr(vmID), + vm: []VMRunstate{ + VMRunstateStopped, + VMRunstateHalted, + VMRunstateReset, + VMRunstateRunning, + VMRunstateSuspended}, + runStateCheckStatus: vmRunStateCheck, + } +} + +func vmRequestRunStateStopped(environmentID string, vmID string) *environmentVMRunState { + state := vmRunStateNotBusy(environmentID, vmID) + state.vm = []VMRunstate{VMRunstateStopped} + return state +} + +func vmRunStateNotBusyWithAdapter(environmentID string, vmID string, adapterID string) *environmentVMRunState { + state := vmRunStateNotBusy(environmentID, vmID) + state.adapterID = strToPtr(adapterID) + return state +} diff --git a/skytap/client_test.go b/skytap/client_test.go index c5d0c02..4b46efa 100644 --- a/skytap/client_test.go +++ b/skytap/client_test.go @@ -1,8 +1,13 @@ package skytap import ( + "bytes" "context" + "encoding/json" + "fmt" "io" + "io/ioutil" + "log" "net/http" "net/http/httptest" "testing" @@ -31,15 +36,15 @@ func createClientWithUserAgent(t *testing.T, userAgent string) (*Client, *httpte settings := NewDefaultSettings(WithBaseURL(hs.URL), WithCredentialsProvider(NewAPITokenCredentials(user, token)), WithUserAgent(userAgent)) skytap, err := NewClient(settings) + assert.Nil(t, err) skytap.retryCount = testingRetryCount skytap.retryAfter = testingRetryAfter - assert.Nil(t, err) assert.NotNil(t, skytap) return skytap, hs, &handler } -func TestRetryWithFailure(t *testing.T) { +func TestGetRetryWithFailure(t *testing.T) { requestCounter := 0 skytap, hs, handler := createClient(t) @@ -53,12 +58,11 @@ func TestRetryWithFailure(t *testing.T) { _, err := skytap.Projects.Get(context.Background(), 12345) errorResponse := err.(*ErrorResponse) - assert.Nil(t, errorResponse.RetryAfter) assert.Equal(t, 1, requestCounter) assert.Equal(t, http.StatusUnauthorized, errorResponse.Response.StatusCode) } -func TestRetryWithBusy409(t *testing.T) { +func TestGetRetryWithBusy409(t *testing.T) { requestCounter := 0 skytap, hs, handler := createClient(t) skytap.retryCount = 1 @@ -75,12 +79,11 @@ func TestRetryWithBusy409(t *testing.T) { assert.Equal(t, http.StatusConflict, errorResponse.Response.StatusCode) assert.Equal(t, 2, *errorResponse.RetryAfter) - assert.True(t, errorResponse.RequiresRetry) - assert.Equal(t, 2, requestCounter) + assert.Equal(t, 1, requestCounter) assert.Equal(t, 3, testingRetryCount) } -func TestRetryWithBusy423(t *testing.T) { +func TestGetRetryWithBusy423(t *testing.T) { requestCounter := 0 skytap, hs, handler := createClient(t) skytap.retryCount = 1 @@ -97,12 +100,11 @@ func TestRetryWithBusy423(t *testing.T) { assert.Equal(t, http.StatusLocked, errorResponse.Response.StatusCode) assert.Equal(t, 2, *errorResponse.RetryAfter) - assert.True(t, errorResponse.RequiresRetry) - assert.Equal(t, 2, requestCounter) + assert.Equal(t, 1, requestCounter) assert.Equal(t, 3, testingRetryCount) } -func TestRetryWithBusy423WithBadRetryAfter(t *testing.T) { +func TestGetRetryWithBusy423WithBadRetryAfter(t *testing.T) { requestCounter := 0 skytap, hs, handler := createClient(t) defer hs.Close() @@ -118,10 +120,10 @@ func TestRetryWithBusy423WithBadRetryAfter(t *testing.T) { assert.Equal(t, testingRetryAfter, *errorResponse.RetryAfter) assert.Equal(t, http.StatusLocked, errorResponse.Response.StatusCode) - assert.Equal(t, testingRetryCount+1, requestCounter) + assert.Equal(t, 1, requestCounter) } -func TestRetryWithBusy423WithoutRetryAfter(t *testing.T) { +func TestGetRetryWithBusy423WithoutRetryAfter(t *testing.T) { requestCounter := 0 skytap, hs, handler := createClient(t) defer hs.Close() @@ -136,10 +138,10 @@ func TestRetryWithBusy423WithoutRetryAfter(t *testing.T) { assert.Equal(t, testingRetryAfter, *errorResponse.RetryAfter) assert.Equal(t, http.StatusLocked, errorResponse.Response.StatusCode) - assert.Equal(t, testingRetryCount+1, requestCounter) + assert.Equal(t, 1, requestCounter) } -func TestRetryWith429(t *testing.T) { +func TestGetRetryWith429(t *testing.T) { requestCounter := 0 skytap, hs, handler := createClient(t) defer hs.Close() @@ -154,10 +156,10 @@ func TestRetryWith429(t *testing.T) { errorResponse := err.(*ErrorResponse) assert.Equal(t, http.StatusTooManyRequests, errorResponse.Response.StatusCode) - assert.Equal(t, testingRetryCount+1, requestCounter) + assert.Equal(t, 1, requestCounter) } -func TestRetryWith50x(t *testing.T) { +func TestGetRetryWith50x(t *testing.T) { requestCounter := 0 skytap, hs, handler := createClient(t) defer hs.Close() @@ -172,27 +174,279 @@ func TestRetryWith50x(t *testing.T) { errorResponse := err.(*ErrorResponse) assert.Equal(t, http.StatusInternalServerError, errorResponse.Response.StatusCode) - assert.Equal(t, testingRetryCount+1, requestCounter) + assert.Equal(t, 1, requestCounter) +} + +func TestGetPreRequestRunstateNotExpecting(t *testing.T) { + skytap, hs, handler := createClient(t) + defer hs.Close() + responseProcessed := false + + *handler = func(rw http.ResponseWriter, req *http.Request) { + responseProcessed = true + assert.Equal(t, http.MethodGet, req.Method, "Unexpected method") + assert.Equal(t, "/v2/projects/12345", req.URL.Path, "Unexpected path") + _, err := io.WriteString(rw, `{"id": "12345", "name": "test-project", "summary": "test project"}`) + assert.NoError(t, err) + } + + _, err := skytap.Projects.Get(context.Background(), 12345) + assert.NoError(t, err) + assert.True(t, responseProcessed) +} + +func TestPutPostPreRequestRunstateNotExpecting2(t *testing.T) { + skytap, hs, handler := createClient(t) + defer hs.Close() + responseProcessed := 0 + + *handler = func(rw http.ResponseWriter, req *http.Request) { + responseProcessed++ + method := http.MethodPost + path := "/projects" + if responseProcessed >= 2 { + method = http.MethodPut + path = "/projects/12345" + } + assert.Equal(t, method, req.Method, "Unexpected method") + assert.Equal(t, path, req.URL.Path, "Unexpected path") + _, err := io.WriteString(rw, `{"id": "12345", "name": "test-project"}`) + assert.NoError(t, err) + } + + project := Project{} + _, err := skytap.Projects.Create(context.Background(), &project) + assert.NoError(t, err) + assert.Equal(t, 2, responseProcessed) } -func TestRetryWith50xResolves(t *testing.T) { +func TestPutPostPreRequestRunstate(t *testing.T) { + response := fmt.Sprintf(string(readTestFile(t, "VMResponse.json")), 456) + + skytap, hs, handler := createClient(t) + defer hs.Close() requestCounter := 0 + + *handler = func(rw http.ResponseWriter, req *http.Request) { + log.Printf("Request: (%d)\n", requestCounter) + method := http.MethodGet + path := "/v2/configurations/123/vms/456" + if requestCounter == 1 { + method = http.MethodPost + path = "/v2/configurations/123/vms/456/interfaces" + } else if requestCounter >= 2 { + method = http.MethodGet + path = "/v2/configurations/123/vms/456/interfaces/456" + } + assert.Equal(t, path, req.URL.Path, fmt.Sprintf("Bad path: %d", requestCounter)) + assert.Equal(t, method, req.Method, fmt.Sprintf("Bad method: %d", requestCounter)) + + _, err := io.WriteString(rw, response) + assert.NoError(t, err) + requestCounter++ + } + + nicType := &CreateInterfaceRequest{ + NICType: nicTypeToPtr(NICTypeE1000), + } + + _, err := skytap.Interfaces.Create(context.Background(), "123", "456", nicType) + assert.Nil(t, err) + assert.Equal(t, 5, requestCounter) +} + +func TestPutPostPreRequestRunstate2(t *testing.T) { + response := fmt.Sprintf(string(readTestFile(t, "VMResponse.json")), 456) + var vm VM + err := json.Unmarshal([]byte(response), &vm) + assert.NoError(t, err) + *vm.Runstate = VMRunstateBusy + responseBusy, err := json.Marshal(&vm) + skytap, hs, handler := createClient(t) defer hs.Close() + skytap.retryAfter = 1 + requestCounter := 0 + *handler = func(rw http.ResponseWriter, req *http.Request) { - rw.Header().Add("Retry-After", "1") - if requestCounter == 3 { - io.WriteString(rw, `{"id": "12345", "name": "test-project", "summary": "test project"}`) - } else { - rw.WriteHeader(http.StatusInternalServerError) + log.Printf("Request: (%d)\n", requestCounter) + if requestCounter == 0 { + assert.Equal(t, "/v2/configurations/123/vms/456", req.URL.Path, "Bad path") + assert.Equal(t, http.MethodGet, req.Method, "Bad method") + + _, err := io.WriteString(rw, string(responseBusy)) + assert.NoError(t, err) + } else if requestCounter == 1 { + assert.Equal(t, "/v2/configurations/123/vms/456", req.URL.Path, "Bad path") + assert.Equal(t, http.MethodGet, req.Method, "Bad method") + + _, err := io.WriteString(rw, response) + assert.NoError(t, err) + } else if requestCounter == 2 { + assert.Equal(t, "/v2/configurations/123/vms/456/interfaces", req.URL.Path, "Bad path") + assert.Equal(t, "POST", req.Method, "Bad method") + exampleInterface := fmt.Sprintf(string(readTestFile(t, "exampleCreateInterfaceResponse.json")), 456, 123) + _, err := io.WriteString(rw, exampleInterface) + assert.NoError(t, err) + } else if requestCounter == 3 { + assert.Equal(t, "/v2/configurations/123/vms/456/interfaces/nic-456", req.URL.Path, "Bad path") + assert.Equal(t, "GET", req.Method, "Bad method") + exampleInterface := fmt.Sprintf(string(readTestFile(t, "exampleCreateInterfaceResponse.json")), 456, 123) + _, err := io.WriteString(rw, exampleInterface) + assert.NoError(t, err) } requestCounter++ } - _, err := skytap.Projects.Get(context.Background(), 12345) + nicType := &CreateInterfaceRequest{ + NICType: nicTypeToPtr(NICTypeE1000), + } + + _, err = skytap.Interfaces.Create(context.Background(), "123", "456", nicType) + assert.Nil(t, err) + + assert.Equal(t, 4, requestCounter) +} + +func TestGetStatus200(t *testing.T) { + skytap, hs, handler := createClient(t) + defer hs.Close() + + *handler = func(rw http.ResponseWriter, req *http.Request) { + _, err := io.WriteString(rw, string(readTestFile(t, "exampleEnvironment.json"))) + assert.NoError(t, err) + } + + var environment Environment + path := fmt.Sprintf("%s/%s", environmentBasePath, "123") + req, err := skytap.newRequest(context.Background(), "GET", path, nil) + resp, err := skytap.request(context.Background(), req, &environment) + + assert.NoError(t, err) + assert.Equal(t, 200, resp.StatusCode) +} + +func TestPutPostDelete(t *testing.T) { + response := fmt.Sprintf(string(readTestFile(t, "VMResponse.json")), 456) + + skytap, hs, handler := createClient(t) + defer hs.Close() + + var vmResponse VM + err := json.Unmarshal([]byte(response), &vmResponse) + assert.NoError(t, err) + vmResponse.Runstate = vmRunStateToPtr(VMRunstateRunning) + bytesRunning, err := json.Marshal(&vmResponse) + assert.Nil(t, err, "Bad vm") + + requestCounter := 0 + *handler = func(rw http.ResponseWriter, req *http.Request) { + log.Printf("Request: (%d)\n", requestCounter) + if requestCounter == 0 { + + _, err := io.WriteString(rw, string(bytesRunning)) + assert.NoError(t, err) + } else if requestCounter == 1 { + vmResponse.Runstate = vmRunStateToPtr(VMRunstateStopped) + bytesStopped, err := json.Marshal(&vmResponse) + assert.Nil(t, err, "Bad vm") + + _, err = io.WriteString(rw, string(bytesStopped)) + assert.NoError(t, err) + } else if requestCounter == 2 { + _, err := io.WriteString(rw, response) + assert.NoError(t, err) + } + requestCounter++ + } + + update := &UpdateVMRequest{Runstate: vmRunStateToPtr(VMRunstateStopped)} + req, err := skytap.newRequest(context.Background(), http.MethodPut, "", update) + assert.NoError(t, err) + + var vm VM + _, err = skytap.do(context.Background(), req, &vm, vmRunStateNotBusy("123", "456"), update) + assert.NoError(t, err) + + assert.Equal(t, 3, requestCounter) +} + +func TestOutputAndHandleError(t *testing.T) { + message := `{ + "errors": [ + "IP address conflicts with another network adapter on the network" + ] + }` + + skytap, hs, _ := createClient(t) + defer hs.Close() + resp := http.Response{} + + resp.Body = ioutil.NopCloser(bytes.NewBufferString(message)) + errorSpecial := skytap.buildErrorResponse(&resp).(*ErrorResponse) + assert.Equal(t, message, *errorSpecial.Message, "Bad API method") +} + +func TestOutputAndHandle422Error(t *testing.T) { + message := `{ + "errors": [ + "Network adapter type was not a valid choice for this operating system" + ] + }` + + skytap, hs, _ := createClient(t) + defer hs.Close() + resp := http.Response{} + + resp.Body = ioutil.NopCloser(bytes.NewBufferString(message)) + _, _, err := skytap.handleError(&resp, http.StatusUnprocessableEntity) + errSpecial := err.(*ErrorResponse) + assert.Equal(t, message, *errSpecial.Message, "Bad API method") + assert.Error(t, errSpecial) +} + +func TestOutputAndHandle422Busy(t *testing.T) { + message := `{ + "errors": [ + "The machine was busy. Try again later." + ] + }` + + skytap, hs, _ := createClient(t) + defer hs.Close() + resp := http.Response{} + + resp.Body = ioutil.NopCloser(bytes.NewBufferString(message)) + _, _, err := skytap.handleError(&resp, http.StatusUnprocessableEntity) + assert.Nil(t, err) +} + +func TestMakeTimeout(t *testing.T) { + var env Environment + err := json.Unmarshal(readTestFile(t, "exampleEnvironment.json"), &env) + assert.NoError(t, err) + env.Runstate = environmentRunStateToPtr(EnvironmentRunstateBusy) + b, err := json.Marshal(&env) + assert.Nil(t, err) + + skytap, hs, handler := createClient(t) + defer hs.Close() + + requestCounter := 0 + *handler = func(rw http.ResponseWriter, req *http.Request) { + log.Printf("Request: (%d)\n", requestCounter) + _, err = io.WriteString(rw, string(b)) + assert.NoError(t, err) + requestCounter++ + } + req, err := skytap.newRequest(context.Background(), http.MethodGet, "", nil) assert.Nil(t, err) + err = skytap.checkResourceStateUntilSatisfied(context.Background(), req, envRunStateNotBusy("")) + assert.Error(t, err) + assert.Equal(t, testingRetryCount, requestCounter) + assert.Equal(t, "timeout waiting for state", err.Error()) } func TestWithUserAgent(t *testing.T) { diff --git a/skytap/convert.go b/skytap/convert.go index 41f8745..691260b 100644 --- a/skytap/convert.go +++ b/skytap/convert.go @@ -47,3 +47,8 @@ func vmRunStateToPtr(vmRunState VMRunstate) *VMRunstate { func nicTypeToPtr(nicType NICType) *NICType { return &nicType } + +// environmentRunStateToPtr returns a pointer to the passed EnvironmentRunstate. +func environmentRunStateToPtr(environmentRunstate EnvironmentRunstate) *EnvironmentRunstate { + return &environmentRunstate +} diff --git a/skytap/environment.go b/skytap/environment.go index d42cbdd..212c18e 100644 --- a/skytap/environment.go +++ b/skytap/environment.go @@ -3,6 +3,8 @@ package skytap import ( "context" "fmt" + "log" + "strings" ) // Default URL paths @@ -133,6 +135,7 @@ const ( EnvironmentRunstateStopped EnvironmentRunstate = "stopped" EnvironmentRunstateSuspended EnvironmentRunstate = "suspended" EnvironmentRunstateRunning EnvironmentRunstate = "running" + EnvironmentRunstateHalted EnvironmentRunstate = "halted" EnvironmentRunstateBusy EnvironmentRunstate = "busy" ) @@ -183,7 +186,7 @@ func (s *EnvironmentsServiceClient) List(ctx context.Context) (*EnvironmentListR } var environmentsListResponse EnvironmentListResult - _, err = s.client.do(ctx, req, &environmentsListResponse.Value) + _, err = s.client.do(ctx, req, &environmentsListResponse.Value, nil, nil) if err != nil { return nil, err } @@ -201,7 +204,7 @@ func (s *EnvironmentsServiceClient) Get(ctx context.Context, id string) (*Enviro } var environment Environment - _, err = s.client.do(ctx, req, &environment) + _, err = s.client.do(ctx, req, &environment, nil, nil) if err != nil { return nil, err } @@ -210,31 +213,39 @@ func (s *EnvironmentsServiceClient) Get(ctx context.Context, id string) (*Enviro } // Create an environment -func (s *EnvironmentsServiceClient) Create(ctx context.Context, request *CreateEnvironmentRequest) (*Environment, error) { - req, err := s.client.newRequest(ctx, "POST", environmentLegacyBasePath, request) +func (s *EnvironmentsServiceClient) Create(ctx context.Context, opts *CreateEnvironmentRequest) (*Environment, error) { + req, err := s.client.newRequest(ctx, "POST", environmentLegacyBasePath, opts) if err != nil { return nil, err } var createdEnvironment Environment - _, err = s.client.do(ctx, req, &createdEnvironment) + _, err = s.client.do(ctx, req, &createdEnvironment, nil, opts) if err != nil { return nil, err } - runstate := EnvironmentRunstateRunning + env, err := s.Get(ctx, *createdEnvironment.ID) + if err != nil { + return nil, err + } + + var runstate *EnvironmentRunstate + if *env.VMCount > 0 { + runstate = environmentRunStateToPtr(EnvironmentRunstateRunning) + } updateOpts := &UpdateEnvironmentRequest{ - Name: request.Name, - Description: request.Description, - Owner: request.Owner, - OutboundTraffic: request.OutboundTraffic, - Routable: request.Routable, - SuspendOnIdle: request.SuspendOnIdle, - SuspendAtTime: request.SuspendAtTime, - ShutdownOnIdle: request.ShutdownOnIdle, - ShutdownAtTime: request.ShutdownAtTime, - Runstate: &runstate, + Name: opts.Name, + Description: opts.Description, + Owner: opts.Owner, + OutboundTraffic: opts.OutboundTraffic, + Routable: opts.Routable, + SuspendOnIdle: opts.SuspendOnIdle, + SuspendAtTime: opts.SuspendAtTime, + ShutdownOnIdle: opts.ShutdownOnIdle, + ShutdownAtTime: opts.ShutdownAtTime, + Runstate: runstate, // we are expecting the environment to start its VMs after creation } // update environment after creation to establish the resource information. @@ -256,7 +267,7 @@ func (s *EnvironmentsServiceClient) Update(ctx context.Context, id string, updat } var environment Environment - _, err = s.client.do(ctx, req, &environment) + _, err = s.client.do(ctx, req, &environment, envRunStateNotBusy(id), updateEnvironment) if err != nil { return nil, err } @@ -272,10 +283,150 @@ func (s *EnvironmentsServiceClient) Delete(ctx context.Context, id string) error if err != nil { return err } - _, err = s.client.do(ctx, req, nil) + _, err = s.client.do(ctx, req, nil, envRunStateNotBusy(id), nil) if err != nil { return err } return nil } + +func (payload *CreateEnvironmentRequest) compareResponse(ctx context.Context, c *Client, v interface{}, state *environmentVMRunState) (string, bool) { + if envOriginal, ok := v.(*Environment); ok { + env, err := c.Environments.Get(ctx, *envOriginal.ID) + if err != nil { + return requestNotAsExpected, false + } + logEnvironmentStatus(env) + log.Printf("[DEBUG] SDK environment runstate after create (%s)\n", *env.Runstate) + if *env.Runstate != EnvironmentRunstateBusy { + return "", true + } + return "environment not ready", false + } + log.Printf("[ERROR] SDK environment comparison not possible on (%v)\n", v) + return requestNotAsExpected, false +} + +func (payload *UpdateEnvironmentRequest) compareResponse(ctx context.Context, c *Client, v interface{}, state *environmentVMRunState) (string, bool) { + if envOriginal, ok := v.(*Environment); ok { + env, err := c.Environments.Get(ctx, *envOriginal.ID) + if err != nil { + return requestNotAsExpected, false + } + logEnvironmentStatus(env) + actual := payload.buildComparison(env) + if payload.string() == actual.string() { + return "", true + } + return "environment not ready", false + } + log.Printf("[ERROR] SDK environment comparison not possible on (%v)\n", v) + return requestNotAsExpected, false +} + +func (payload *UpdateEnvironmentRequest) buildComparison(env *Environment) UpdateEnvironmentRequest { + actual := UpdateEnvironmentRequest{} + if payload.Name != nil { + actual.Name = env.Name + } + if payload.Description != nil { + actual.Description = env.Description + } + if payload.Owner != nil { + actual.Owner = env.OwnerName + } + if payload.OutboundTraffic != nil { + actual.OutboundTraffic = env.OutboundTraffic + } + if payload.Routable != nil { + actual.Routable = env.Routable + } + if payload.SuspendOnIdle != nil { + actual.SuspendOnIdle = env.SuspendOnIdle + } + if payload.SuspendAtTime != nil { + actual.SuspendAtTime = env.SuspendAtTime + } + if payload.ShutdownOnIdle != nil { + actual.ShutdownOnIdle = env.ShutdownOnIdle + } + if payload.ShutdownAtTime != nil { + actual.ShutdownAtTime = env.ShutdownAtTime + } + if payload.Runstate != nil { + actual.Runstate = env.Runstate + } + return actual +} + +func (payload *UpdateEnvironmentRequest) string() string { + name := "" + description := "" + owner := "" + outboundTraffic := "" + routable := "" + suspendOnIdle := "" + suspendAtTime := "" + shutdownOnIdle := "" + shutdownAtTime := "" + runstate := "" + + if payload.Name != nil { + name = *payload.Name + } + if payload.Description != nil { + description = *payload.Description + } + if payload.Owner != nil { + owner = *payload.Owner + } + if payload.OutboundTraffic != nil { + outboundTraffic = fmt.Sprintf("%t", *payload.OutboundTraffic) + } + if payload.Routable != nil { + routable = fmt.Sprintf("%t", *payload.Routable) + } + if payload.SuspendOnIdle != nil { + suspendOnIdle = fmt.Sprintf("%d", *payload.SuspendOnIdle) + } + if payload.SuspendAtTime != nil { + suspendAtTime = *payload.SuspendAtTime + } + if payload.ShutdownOnIdle != nil { + shutdownOnIdle = fmt.Sprintf("%d", *payload.ShutdownOnIdle) + } + if payload.ShutdownAtTime != nil { + shutdownAtTime = *payload.ShutdownAtTime + } + if payload.Runstate != nil { + runstate = string(*payload.Runstate) + } + var sb strings.Builder + sb.WriteString(name) + sb.WriteString(description) + sb.WriteString(owner) + sb.WriteString(outboundTraffic) + sb.WriteString(routable) + sb.WriteString(suspendOnIdle) + sb.WriteString(suspendAtTime) + sb.WriteString(shutdownOnIdle) + sb.WriteString(shutdownAtTime) + sb.WriteString(runstate) + log.Printf("[DEBUG] SDK environment payload (%s)\n", sb.String()) + return sb.String() +} + +func logEnvironmentStatus(env *Environment) { + if env.RateLimited != nil && *env.RateLimited { + log.Printf("[INFO] SDK environment rate limiting detected\n") + } + if len(env.Errors) > 0 { + log.Printf("[INFO] SDK environment errors detected: (%s)\n", + strings.Join(env.Errors, ", ")) + } + if len(env.ErrorDetails) > 0 { + log.Printf("[INFO] SDK environment errors detected: (%s)\n", + strings.Join(env.ErrorDetails, ", ")) + } +} diff --git a/skytap/environment_test.go b/skytap/environment_test.go index fc4e95d..fff68e0 100644 --- a/skytap/environment_test.go +++ b/skytap/environment_test.go @@ -6,370 +6,22 @@ import ( "fmt" "io" "io/ioutil" + "log" "net/http" "testing" "github.com/stretchr/testify/assert" ) -const exampleEnvironment = `{ - "id": "456", - "url": "https://cloud.skytap.com/v2/configurations/456", - "name": "No VM", - "description": "test environment", - "errors": [ - "error1" - ], - "error_details": [ - "error1 details" - ], - "runstate": "stopped", - "rate_limited": false, - "last_run": "2018/10/11 15:42:23 +0100", - "suspend_on_idle": 1, - "suspend_at_time": "2018/10/11 15:42:23 +0100", - "owner_url": "https://cloud.skytap.com/v2/users/1", - "owner_name": "Joe Bloggs", - "owner_id": "1", - "vm_count": 1, - "storage": 30720, - "network_count": 1, - "created_at": "2018/10/11 15:42:23 +0100", - "region": "US-West", - "region_backend": "skytap", - "svms": 1, - "can_save_as_template": true, - "can_copy": true, - "can_delete": true, - "can_change_state": true, - "can_share": true, - "can_edit": true, - "label_count": 1, - "label_category_count": 1, - "can_tag": true, - "tags": [ - { - "id": "43894", - "value": "tag1" - }, - { - "id": "43896", - "value": "tag2" - } - ], - "tag_list": "tag1,tag2", - "alerts": [ - { - "id":"586", - "display_type":"informational_alert", - "dismissable":true, - "message":"IBM i Technology Preview Program nominations are now open. For more information see What's New.", - "display_on_general":true, - "display_on_login":false, - "display_on_smartclient":false - } - ], - "published_service_count": 0, - "public_ip_count": 0, - "auto_suspend_description": null, - "stages": [ - { - "delay_after_finish_seconds": 300, - "index": 0, - "vm_ids": [ - "123456", - "123457" - ] - } - ], - "staged_execution": { - "action_type": "suspend", - "current_stage_delay_after_finish_seconds": 300, - "current_stage_index": 1, - "current_stage_finished_at": "2018/10/11 15:42:23 +0100", - "vm_ids": [ - "123453", - "123454" - ] - }, - "sequencing_enabled": false, - "note_count": 1, - "project_count_for_user": 0, - "project_count": 0, - "publish_set_count": 0, - "schedule_count": 0, - "vpn_count": 0, - "outbound_traffic": false, - "routable": false, - "vms": [ - { - "id": "36858580", - "name": "CentOS 7 Server x64", - "runstate": "stopped", - "rate_limited": false, - "hardware": { - "cpus": 1, - "supports_multicore": true, - "cpus_per_socket": 1, - "ram": 1024, - "svms": 1, - "guestOS": "centos-64", - "max_cpus": 12, - "min_ram": 256, - "max_ram": 262144, - "vnc_keymap": null, - "uuid": null, - "disks": [ - { - "id": "disk-19861359-37668995-scsi-0-0", - "size": 30720, - "type": "SCSI", - "controller": "0", - "lun": "0" - } - ], - "storage": 30720, - "upgradable": false, - "instance_type": null, - "time_sync_enabled": true, - "rtc_start_time": null, - "copy_paste_enabled": true, - "nested_virtualization": false, - "architecture": "x86" - }, - "error": false, - "error_details": false, - "asset_id": "1", - "hardware_version": 11, - "max_hardware_version": 11, - "interfaces": [ - { - "id": "nic-19861359-37668995-0", - "ip": "10.0.0.1", - "hostname": "centos7sx64", - "mac": "00:50:56:2B:87:F5", - "services_count": 0, - "services": [ - { - "id": "3389", - "internal_port": 3389, - "external_ip": "76.191.118.29", - "external_port": 12345 - } - ], - "public_ips_count": 0, - "public_ips": [ - { - "1.2.3.4": "5.6.7.8" - } - ], - "vm_id": "36858580", - "vm_name": "CentOS 7 Server x64", - "status": "Powered off", - "network_id": "23429874", - "network_name": "Default Network", - "network_url": "https://cloud.skytap.com/v2/configurations/456/networks/23429874", - "network_type": "automatic", - "network_subnet": "10.0.0.0/24", - "nic_type": "vmxnet3", - "secondary_ips": [ - { - "id": "10.0.2.2", - "address": "10.0.2.2" - } - ], - "public_ip_attachments": [ - { - "id": 1, - "public_ip_attachment_key": 2, - "address": "1.2.3.4", - "connect_type": 1, - "hostname": "host1", - "dns_name": "host.com", - "public_ip_key": "5.6.7.8" - } - ] - } - ], - "notes": [ - { - "id": "5377708", - "user_id": 1, - "user": { - "id": "1", - "url": "https://cloud.skytap.com/v2/users/1", - "first_name": "Joe", - "last_name": "Bloggs", - "login_name": "Joe.Bloggs@opencredo", - "email": "Joe.Bloggs@opencredo.com", - "title": "", - "deleted": false - }, - "created_at": "2018/10/11 15:27:45 +0100", - "updated_at": "2018/10/11 15:27:45 +0100", - "text": "a note" - } - ], - "labels": [ - { - "id": "43892", - "value": "test vm", - "label_category": "test multi", - "label_category_id": "7704", - "label_category_single_value": false - } - ], - "credentials": [ - { - "id": "35158632", - "text": "user/pass" - } - ], - "desktop_resizable": true, - "local_mouse_cursor": true, - "maintenance_lock_engaged": false, - "region_backend": "skytap", - "created_at": "2018/10/11 15:42:26 +0100", - "supports_suspend": true, - "can_change_object_state": true, - "containers": [ - { - "id": 1122, - "cid": "123456789abcdefghijk123456789abcdefghijk123456789abcdefghijk", - "name": "nginxtest1", - "image": "nginx:latest", - "created_at": "2016/06/16 11:58:50 -0700", - "last_run": "2016/06/16 11:58:51 -0700", - "can_change_state": true, - "can_delete": true, - "status": "running", - "privileged": false, - "vm_id": 111000, - "vm_name": "Docker VM1", - "vm_runstate": "running", - "configuration_id": 123456 - } - ], - "configuration_url": "https://cloud.skytap.com/v2/configurations/456" - } - ], - "networks": [ - { - "id": "1234567", - "url": "https://cloud.skytap.com/configurations/1111111/networks/123467", - "name": "Network 1", - "network_type": "automatic", - "subnet": "10.0.0.0/24", - "subnet_addr": "10.0.0.0", - "subnet_size": 24, - "gateway": "10.0.0.254", - "primary_nameserver": "8.8.8.8", - "secondary_nameserver": "8.8.8.9", - "region": "US-West", - "domain": "sampledomain.com", - "vpn_attachments": [ - { - "id": "111111-vpn-1234567", - "connected": false, - "network": { - "id": "1111111", - "subnet": "10.0.0.0/24", - "network_name": "Network 1", - "configuration_id": "1212121" - }, - "vpn": { - "id": "vpn-1234567", - "name": "CorpNet", - "enabled": true, - "nat_enabled": true, - "remote_subnets": "10.10.0.0/24, 10.10.1.0/24, 10.10.2.0/24, 10.10.4.0/24", - "remote_peer_ip": "199.199.199.199", - "can_reconnect": true - } - }, - { - "id": "111111-vpn-1234555", - "connected": false, - "network": { - "id": "1111111", - "subnet": "10.0.0.0/24", - "network_name": "Network 1", - "configuration_id": "1212121" - }, - "vpn": { - "id": "vpn-1234555", - "name": "Offsite DC", - "enabled": true, - "nat_enabled": true, - "remote_subnets": "10.10.0.0/24, 10.10.1.0/24, 10.10.2.0/24, 10.10.4.0/24", - "remote_peer_ip": "188.188.188.188", - "can_reconnect": true - } - } - ], - "tunnelable": false, - "tunnels": [ - { - "id": "tunnel-123456-789011", - "status": "not_busy", - "error": null, - "source_network": { - "id": "000000", - "url": "https://cloud.skytap.com/configurations/249424/networks/0000000", - "name": "Network 1", - "network_type": "automatic", - "subnet": "10.0.0.0/24", - "subnet_addr": "10.0.0.0", - "subnet_size": 24, - "gateway": "10.0.0.254", - "primary_nameserver": null, - "secondary_nameserver": null, - "region": "US-West", - "domain": "skytap.example", - "vpn_attachments": [] - }, - "target_network": { - "id": "111111", - "url": "https://cloud.skytap.com/configurations/808216/networks/111111", - "name": "Network 2", - "network_type": "automatic", - "subnet": "10.0.2.0/24", - "subnet_addr": "10.0.2.0", - "subnet_size": 24, - "gateway": "10.0.2.254", - "primary_nameserver": null, - "secondary_nameserver": null, - "region": "US-West", - "domain": "test.net", - "vpn_attachments": [] - } - } - ] - } - ], - "containers_count": 0, - "container_hosts_count": 0, - "platform_errors": [ - "platform error1" - ], - "svms_by_architecture": { - "x86": 1, - "power": 0 - }, - "all_vms_support_suspend": true, - "shutdown_on_idle": null, - "shutdown_at_time": null, - "auto_shutdown_description": "Shutting down!" -}` - func TestCreateEnvironment(t *testing.T) { skytap, hs, handler := createClient(t) defer hs.Close() - var createPhase = true + requestCounter := 0 *handler = func(rw http.ResponseWriter, req *http.Request) { - if createPhase { + log.Printf("Request: (%d)\n", requestCounter) + if requestCounter == 0 { if req.URL.Path != "/configurations" { t.Error("Bad path") } @@ -379,9 +31,27 @@ func TestCreateEnvironment(t *testing.T) { body, err := ioutil.ReadAll(req.Body) assert.Nil(t, err) assert.JSONEq(t, fmt.Sprintf(`{"template_id":%q, "project_id":%d, "description":"test environment"}`, "12345", 12345), string(body)) - io.WriteString(rw, `{"id": "456"}`) - createPhase = false - } else { + _, err = io.WriteString(rw, `{"id": "456"}`) + assert.NoError(t, err) + } else if requestCounter == 1 { + assert.Equal(t, "/v2/configurations/456", req.URL.Path, "Bad path") + assert.Equal(t, http.MethodGet, req.Method, "Bad method") + + _, err := io.WriteString(rw, string(readTestFile(t, "exampleEnvironment.json"))) + assert.NoError(t, err) + } else if requestCounter == 2 { + assert.Equal(t, "/v2/configurations/456", req.URL.Path, "Bad path") + assert.Equal(t, http.MethodGet, req.Method, "Bad method") + + _, err := io.WriteString(rw, string(readTestFile(t, "exampleEnvironment.json"))) + assert.NoError(t, err) + } else if requestCounter == 3 { + assert.Equal(t, "/v2/configurations/456", req.URL.Path, "Bad path") + assert.Equal(t, http.MethodGet, req.Method, "Bad method") + + _, err := io.WriteString(rw, string(readTestFile(t, "exampleEnvironment.json"))) + assert.NoError(t, err) + } else if requestCounter == 4 { if req.URL.Path != "/v2/configurations/456" { t.Error("Bad path") } @@ -392,8 +62,23 @@ func TestCreateEnvironment(t *testing.T) { assert.Nil(t, err) assert.JSONEq(t, `{"description": "test environment", "runstate":"running"}`, string(body)) - io.WriteString(rw, exampleEnvironment) + _, err = io.WriteString(rw, string(readTestFile(t, "exampleEnvironment.json"))) + assert.NoError(t, err) + + } else if requestCounter == 5 { + assert.Equal(t, "/v2/configurations/456", req.URL.Path, "Bad path") + assert.Equal(t, http.MethodGet, req.Method, "Bad method") + + var envRunning Environment + err := json.Unmarshal(readTestFile(t, "exampleEnvironment.json"), &envRunning) + assert.NoError(t, err) + envRunning.Runstate = environmentRunStateToPtr(EnvironmentRunstateRunning) + b, err := json.Marshal(&envRunning) + assert.Nil(t, err) + _, err = io.WriteString(rw, string(b)) + assert.NoError(t, err) } + requestCounter++ } opts := &CreateEnvironmentRequest{ @@ -408,9 +93,11 @@ func TestCreateEnvironment(t *testing.T) { var environmentExpected Environment - err = json.Unmarshal([]byte(exampleEnvironment), &environmentExpected) + err = json.Unmarshal(readTestFile(t, "exampleEnvironment.json"), &environmentExpected) assert.Equal(t, environmentExpected, *environment) + + assert.Equal(t, 6, requestCounter) } func TestReadEnvironment(t *testing.T) { @@ -424,7 +111,8 @@ func TestReadEnvironment(t *testing.T) { if req.Method != "GET" { t.Error("Bad method") } - io.WriteString(rw, exampleEnvironment) + _, err := io.WriteString(rw, string(readTestFile(t, "exampleEnvironment.json"))) + assert.NoError(t, err) } environment, err := skytap.Environments.Get(context.Background(), "456") @@ -432,7 +120,7 @@ func TestReadEnvironment(t *testing.T) { assert.Nil(t, err) var environmentExpected Environment - err = json.Unmarshal([]byte(exampleEnvironment), &environmentExpected) + err = json.Unmarshal(readTestFile(t, "exampleEnvironment.json"), &environmentExpected) assert.Equal(t, environmentExpected, *environment) } @@ -442,24 +130,48 @@ func TestUpdateEnvironment(t *testing.T) { defer hs.Close() var environment Environment - json.Unmarshal([]byte(exampleEnvironment), &environment) + err := json.Unmarshal(readTestFile(t, "exampleEnvironment.json"), &environment) + assert.NoError(t, err) *environment.Description = "updated environment" - bytes, err := json.Marshal(&environment) - assert.Nil(t, err) + requestCounter := 0 *handler = func(rw http.ResponseWriter, req *http.Request) { - if req.URL.Path != "/v2/configurations/456" { - t.Error("Bad path") - } - if req.Method != "PUT" { - t.Error("Bad method") - } - body, err := ioutil.ReadAll(req.Body) - assert.Nil(t, err) - assert.JSONEq(t, `{"description": "updated environment"}`, string(body)) + log.Printf("Request: (%d)\n", requestCounter) + if requestCounter == 0 { + assert.Equal(t, "/v2/configurations/456", req.URL.Path, "Bad path") + assert.Equal(t, http.MethodGet, req.Method, "Bad method") + + _, err := io.WriteString(rw, string(readTestFile(t, "exampleEnvironment.json"))) + assert.NoError(t, err) + } else if requestCounter == 1 { + if req.URL.Path != "/v2/configurations/456" { + t.Error("Bad path") + } + if req.Method != "PUT" { + t.Error("Bad method") + } + body, err := ioutil.ReadAll(req.Body) + assert.Nil(t, err) + assert.JSONEq(t, `{"description": "updated environment"}`, string(body)) - io.WriteString(rw, string(bytes)) + b, err := json.Marshal(&environment) + assert.Nil(t, err) + _, err = io.WriteString(rw, string(b)) + assert.NoError(t, err) + } else if requestCounter == 2 { + assert.Equal(t, "/v2/configurations/456", req.URL.Path, "Bad path") + assert.Equal(t, http.MethodGet, req.Method, "Bad method") + + var envRunning Environment + err := json.Unmarshal(readTestFile(t, "exampleEnvironment.json"), &envRunning) + assert.NoError(t, err) + envRunning.Description = strToPtr("updated environment") + b, err := json.Marshal(&envRunning) + assert.Nil(t, err) + _, err = io.WriteString(rw, string(b)) + } + requestCounter++ } opts := &UpdateEnvironmentRequest{ @@ -470,23 +182,38 @@ func TestUpdateEnvironment(t *testing.T) { assert.Nil(t, err) assert.Equal(t, environment, *environmentUpdate) + + assert.Equal(t, 3, requestCounter) } func TestDeleteEnvironment(t *testing.T) { skytap, hs, handler := createClient(t) defer hs.Close() + requestCounter := 0 + *handler = func(rw http.ResponseWriter, req *http.Request) { - if req.URL.Path != "/configurations/456" { - t.Error("Bad path") - } - if req.Method != "DELETE" { - t.Error("Bad method") + log.Printf("Request: (%d)\n", requestCounter) + if requestCounter == 0 { + assert.Equal(t, "/v2/configurations/456", req.URL.Path, "Bad path") + assert.Equal(t, http.MethodGet, req.Method, "Bad method") + + _, err := io.WriteString(rw, string(readTestFile(t, "exampleEnvironment.json"))) + assert.NoError(t, err) + } else if requestCounter == 1 { + if req.URL.Path != "/configurations/456" { + t.Error("Bad path") + } + if req.Method != "DELETE" { + t.Error("Bad method") + } } + requestCounter++ } err := skytap.Environments.Delete(context.Background(), "456") assert.Nil(t, err) + assert.Equal(t, 2, requestCounter) } func TestListEnvironments(t *testing.T) { @@ -500,7 +227,8 @@ func TestListEnvironments(t *testing.T) { if req.Method != "GET" { t.Error("Bad method") } - io.WriteString(rw, fmt.Sprintf(`[%+v]`, exampleEnvironment)) + _, err := io.WriteString(rw, fmt.Sprintf(`[%+v]`, string(readTestFile(t, "exampleEnvironment.json")))) + assert.NoError(t, err) } result, err := skytap.Environments.List(context.Background()) @@ -517,3 +245,107 @@ func TestListEnvironments(t *testing.T) { assert.True(t, found) } + +func TestCompareEnvironmentCreateTrue(t *testing.T) { + exampleEnvironment := readTestFile(t, "exampleEnvironment.json") + + var environment Environment + err := json.Unmarshal(exampleEnvironment, &environment) + assert.NoError(t, err) + opts := CreateEnvironmentRequest{ + TemplateID: strToPtr("12345"), + ProjectID: intToPtr(12345), + } + skytap, hs, handler := createClient(t) + defer hs.Close() + + *handler = func(rw http.ResponseWriter, req *http.Request) { + _, err := io.WriteString(rw, string(exampleEnvironment)) + assert.NoError(t, err) + } + + message, ok := opts.compareResponse(context.Background(), skytap, &environment, envRunStateNotBusy("123")) + assert.True(t, ok) + assert.Equal(t, "", message) +} + +func TestCompareEnvironmentCreateFalse(t *testing.T) { + exampleEnvironment := readTestFile(t, "exampleEnvironment.json") + + var environment Environment + err := json.Unmarshal(exampleEnvironment, &environment) + assert.NoError(t, err) + opts := CreateEnvironmentRequest{ + TemplateID: strToPtr("12345"), + ProjectID: intToPtr(12345), + } + + skytap, hs, handler := createClient(t) + defer hs.Close() + + *handler = func(rw http.ResponseWriter, req *http.Request) { + var envRunning Environment + err := json.Unmarshal(exampleEnvironment, &envRunning) + assert.NoError(t, err) + envRunning.Runstate = environmentRunStateToPtr(EnvironmentRunstateBusy) + b, err := json.Marshal(&envRunning) + assert.Nil(t, err) + _, err = io.WriteString(rw, string(b)) + + assert.NoError(t, err) + } + message, ok := opts.compareResponse(context.Background(), skytap, &environment, envRunStateNotBusy("123")) + assert.False(t, ok) + assert.Equal(t, "environment not ready", message) +} + +func TestCompareEnvironmentUpdateTrue(t *testing.T) { + exampleEnvironment := readTestFile(t, "exampleEnvironment.json") + + var environment Environment + err := json.Unmarshal(exampleEnvironment, &environment) + assert.NoError(t, err) + opts := UpdateEnvironmentRequest{ + Description: strToPtr(*environment.Description), + } + skytap, hs, handler := createClient(t) + defer hs.Close() + + *handler = func(rw http.ResponseWriter, req *http.Request) { + _, err := io.WriteString(rw, string(exampleEnvironment)) + assert.NoError(t, err) + } + + message, ok := opts.compareResponse(context.Background(), skytap, &environment, envRunStateNotBusy("123")) + assert.True(t, ok) + assert.Equal(t, "", message) +} + +func TestCompareEnvironmentUpdateFalse(t *testing.T) { + exampleEnvironment := readTestFile(t, "exampleEnvironment.json") + + var environment Environment + err := json.Unmarshal(exampleEnvironment, &environment) + assert.NoError(t, err) + environment.Runstate = environmentRunStateToPtr(EnvironmentRunstateBusy) + opts := UpdateEnvironmentRequest{ + Runstate: environmentRunStateToPtr(EnvironmentRunstateStopped), + } + skytap, hs, handler := createClient(t) + defer hs.Close() + + *handler = func(rw http.ResponseWriter, req *http.Request) { + var envRunning Environment + err := json.Unmarshal(exampleEnvironment, &envRunning) + assert.NoError(t, err) + envRunning.Runstate = environmentRunStateToPtr(EnvironmentRunstateRunning) + b, err := json.Marshal(&envRunning) + assert.Nil(t, err) + _, err = io.WriteString(rw, string(b)) + + assert.NoError(t, err) + } + message, ok := opts.compareResponse(context.Background(), skytap, &environment, envRunStateNotBusy("123")) + assert.False(t, ok) + assert.Equal(t, "environment not ready", message) +} diff --git a/skytap/interface.go b/skytap/interface.go index 4d148e5..3acd559 100644 --- a/skytap/interface.go +++ b/skytap/interface.go @@ -1,6 +1,10 @@ package skytap -import "context" +import ( + "context" + "fmt" + "log" +) // Default URL paths const ( @@ -150,7 +154,7 @@ func (s *InterfacesServiceClient) List(ctx context.Context, environmentID string } var interfaceListResponse InterfaceListResult - _, err = s.client.do(ctx, req, &interfaceListResponse.Value) + _, err = s.client.do(ctx, req, &interfaceListResponse.Value, nil, nil) if err != nil { return nil, err } @@ -169,7 +173,7 @@ func (s *InterfacesServiceClient) Get(ctx context.Context, environmentID string, } var networkInterface Interface - _, err = s.client.do(ctx, req, &networkInterface) + _, err = s.client.do(ctx, req, &networkInterface, nil, nil) if err != nil { return nil, err } @@ -188,7 +192,7 @@ func (s *InterfacesServiceClient) Create(ctx context.Context, environmentID stri } var createdInterface Interface - _, err = s.client.do(ctx, req, &createdInterface) + _, err = s.client.do(ctx, req, &createdInterface, vmRequestRunStateStopped(environmentID, vmID), nicType) if err != nil { return nil, err } @@ -207,7 +211,7 @@ func (s *InterfacesServiceClient) Attach(ctx context.Context, environmentID stri } var updatedInterface Interface - _, err = s.client.do(ctx, req, &updatedInterface) + _, err = s.client.do(ctx, req, &updatedInterface, vmRunStateNotBusy(environmentID, vmID), networkID) if err != nil { return nil, err } @@ -226,7 +230,7 @@ func (s *InterfacesServiceClient) Update(ctx context.Context, environmentID stri } var updatedInterface Interface - _, err = s.client.do(ctx, req, &updatedInterface) + _, err = s.client.do(ctx, req, &updatedInterface, vmRequestRunStateStopped(environmentID, vmID), opts) if err != nil { return nil, err } @@ -244,10 +248,106 @@ func (s *InterfacesServiceClient) Delete(ctx context.Context, environmentID stri return err } - _, err = s.client.do(ctx, req, nil) + _, err = s.client.do(ctx, req, nil, vmRequestRunStateStopped(environmentID, vmID), nil) if err != nil { return err } return nil } + +func (payload *CreateInterfaceRequest) compareResponse(ctx context.Context, c *Client, v interface{}, state *environmentVMRunState) (string, bool) { + if interfaceOriginal, ok := v.(*Interface); ok { + adapter, err := c.Interfaces.Get(ctx, *state.environmentID, *state.vmID, *interfaceOriginal.ID) + if err != nil { + return requestNotAsExpected, false + } + actual := CreateInterfaceRequest{ + adapter.NICType, + } + if payload.string() == actual.string() { + return "", true + } + return "network adapter not ready", false + } + log.Printf("[ERROR] SDK interface comparison not possible on (%v)\n", v) + return requestNotAsExpected, false +} + +func (payload *AttachInterfaceRequest) compareResponse(ctx context.Context, c *Client, v interface{}, state *environmentVMRunState) (string, bool) { + if interfaceOriginal, ok := v.(*Interface); ok { + adapter, err := c.Interfaces.Get(ctx, *state.environmentID, *state.vmID, *interfaceOriginal.ID) + if err != nil { + return requestNotAsExpected, false + } + actual := AttachInterfaceRequest{ + adapter.NetworkID, + } + if payload.string() == actual.string() { + return "", true + } + return "network adapter not ready", false + } + log.Printf("[ERROR] SDK interface comparison not possible on (%v)\n", v) + return requestNotAsExpected, false +} + +func (payload *UpdateInterfaceRequest) compareResponse(ctx context.Context, c *Client, v interface{}, state *environmentVMRunState) (string, bool) { + if interfaceOriginal, ok := v.(*Interface); ok { + adapter, err := c.Interfaces.Get(ctx, *state.environmentID, *state.vmID, *interfaceOriginal.ID) + if err != nil { + return requestNotAsExpected, false + } + actual := UpdateInterfaceRequest{ + adapter.IP, + adapter.Hostname, + } + if payload.string() == actual.string() { + return "", true + } + return "network adapter not ready", false + } + log.Printf("[ERROR] SDK interface comparison not possible on (%v)\n", v) + return requestNotAsExpected, false +} + +func (payload *CreateInterfaceRequest) string() string { + nicType := "" + + if payload.NICType != nil { + nicType = string(*payload.NICType) + } + s := fmt.Sprintf("%s", + nicType) + log.Printf("[DEBUG] SDK create interface payload (%s)\n", s) + return s +} + +func (payload *AttachInterfaceRequest) string() string { + networkID := "" + + if payload.NetworkID != nil { + networkID = *payload.NetworkID + } + s := fmt.Sprintf("%s", + networkID) + log.Printf("[DEBUG] SDK attach interface payload (%s)\n", s) + return s +} + +func (payload *UpdateInterfaceRequest) string() string { + ip := "" + hostname := "" + + if payload.IP != nil { + ip = string(*payload.IP) + } + if payload.Hostname != nil { + hostname = string(*payload.Hostname) + } + s := fmt.Sprintf("%s%s", + ip, + hostname) + log.Printf("[DEBUG] SDK update interface payload (%s)\n", s) + return s +} diff --git a/skytap/interface_test.go b/skytap/interface_test.go index 8587c77..fdeac1d 100644 --- a/skytap/interface_test.go +++ b/skytap/interface_test.go @@ -6,119 +6,48 @@ import ( "fmt" "io" "io/ioutil" + "log" "net/http" "testing" "github.com/stretchr/testify/assert" ) -const exampleCreateInterfaceRequest = `{ - "nic_type": "e1000" -}` - -const exampleAttachInterfaceRequest = `{ - "network_id": "23917287" -}` - -const exampleUpdateInterfaceRequest = `{ - "ip": "10.0.0.1", - "hostname": "updated-hostname" -}` - -const exampleCreateInterfaceResponse = `{ - "id": "nic-%d", - "ip": null, - "hostname": null, - "mac": "00:50:56:07:40:3F", - "services_count": 0, - "services": [], - "public_ips_count": 0, - "public_ips": [], - "vm_id": "%d", - "vm_name": "Windows Server 2016 Standard", - "status": "Powered off", - "nic_type": "e1000", - "secondary_ips": [], - "public_ip_attachments": [] -}` - -const exampleAttachInterfaceResponse = `{ - "id": "nic-20250403-38374059-4", - "ip": "192.168.0.5", - "hostname": "host-3", - "mac": "00:50:56:05:3F:84", - "services_count": 0, - "services": [], - "public_ips_count": 0, - "public_ips": [], - "vm_id": "37533321", - "vm_name": "CentOS 6 Desktop x64", - "status": "Powered off", - "network_id": "23922457", - "network_name": "tftest-network-1", - "network_url": "https://cloud.skytap.com/v2/configurations/40071754/networks/23922457", - "network_type": "automatic", - "network_subnet": "192.168.0.0/16", - "nic_type": "vmxnet3", - "secondary_ips": [], - "public_ip_attachments": [] -}` - -const exampleInterfaceListResponse = `[ - { - "id": "nic-20246343-38367563-0", - "ip": "192.168.0.1", - "hostname": "wins2016s", - "mac": "00:50:56:11:7D:D9", - "services_count": 0, - "services": [], - "public_ips_count": 0, - "public_ips": [], - "vm_id": "37527239", - "vm_name": "Windows Server 2016 Standard", - "status": "Running", - "network_id": "23917287", - "network_name": "tftest-network-1", - "network_url": "https://cloud.skytap.com/v2/configurations/40064014/networks/23917287", - "network_type": "automatic", - "network_subnet": "192.168.0.0/16", - "nic_type": "vmxnet3", - "secondary_ips": [], - "public_ip_attachments": [] - }, - { - "id": "nic-20246343-38367563-5", - "ip": null, - "hostname": null, - "mac": "00:50:56:07:40:3F", - "services_count": 0, - "services": [], - "public_ips_count": 0, - "public_ips": [], - "vm_id": "37527239", - "vm_name": "Windows Server 2016 Standard", - "status": "Running", - "nic_type": "e1000", - "secondary_ips": [], - "public_ip_attachments": [] - } -]` - func TestCreateInterface(t *testing.T) { - exampleInterface := fmt.Sprintf(exampleCreateInterfaceResponse, 456, 123) + response := fmt.Sprintf(string(readTestFile(t, "VMResponse.json")), 456) + exampleInterface := fmt.Sprintf(string(readTestFile(t, "exampleCreateInterfaceResponse.json")), 456, 123) skytap, hs, handler := createClient(t) defer hs.Close() - *handler = func(rw http.ResponseWriter, req *http.Request) { - assert.Equal(t, "/v2/configurations/123/vms/456/interfaces", req.URL.Path, "Bad path") - assert.Equal(t, "POST", req.Method, "Bad method") - - body, err := ioutil.ReadAll(req.Body) - assert.Nil(t, err, "Bad request body") - assert.JSONEq(t, exampleCreateInterfaceRequest, string(body), "Bad request body") + requestCounter := 0 - io.WriteString(rw, exampleInterface) + *handler = func(rw http.ResponseWriter, req *http.Request) { + log.Printf("Request: (%d)\n", requestCounter) + if requestCounter == 0 { + assert.Equal(t, "/v2/configurations/123/vms/456", req.URL.Path, "Bad path") + assert.Equal(t, http.MethodGet, req.Method, "Bad method") + + _, err := io.WriteString(rw, string(response)) + assert.NoError(t, err) + } else if requestCounter == 1 { + assert.Equal(t, "/v2/configurations/123/vms/456/interfaces", req.URL.Path, "Bad path") + assert.Equal(t, "POST", req.Method, "Bad method") + + body, err := ioutil.ReadAll(req.Body) + assert.Nil(t, err, "Bad request body") + assert.JSONEq(t, string(readTestFile(t, "exampleCreateInterfaceRequest.json")), string(body), "Bad request body") + + _, err = io.WriteString(rw, exampleInterface) + assert.NoError(t, err) + } else if requestCounter == 2 { + assert.Equal(t, "/v2/configurations/123/vms/456/interfaces/nic-456", req.URL.Path, "Bad path") + assert.Equal(t, http.MethodGet, req.Method, "Bad method") + + _, err := io.WriteString(rw, exampleInterface) + assert.NoError(t, err) + } + requestCounter++ } nicType := &CreateInterfaceRequest{ NICType: nicTypeToPtr(NICTypeE1000), @@ -130,23 +59,45 @@ func TestCreateInterface(t *testing.T) { var interfaceExpected Interface err = json.Unmarshal([]byte(exampleInterface), &interfaceExpected) assert.Equal(t, interfaceExpected, *networkInterface, "Bad interface") + + assert.Equal(t, 3, requestCounter) } func TestAttachInterface(t *testing.T) { - exampleInterface := fmt.Sprintf(exampleAttachInterfaceResponse) + response := fmt.Sprintf(string(readTestFile(t, "VMResponse.json")), 456) + exampleInterface := fmt.Sprintf(string(readTestFile(t, "exampleAttachInterfaceResponse.json"))) skytap, hs, handler := createClient(t) defer hs.Close() - *handler = func(rw http.ResponseWriter, req *http.Request) { - assert.Equal(t, "/v2/configurations/123/vms/456/interfaces/789", req.URL.Path, "Bad path") - assert.Equal(t, "PUT", req.Method, "Bad method") + requestCounter := 0 - body, err := ioutil.ReadAll(req.Body) - assert.Nil(t, err, "Bad request body") - assert.JSONEq(t, exampleAttachInterfaceRequest, string(body), "Bad request body") - - io.WriteString(rw, exampleInterface) + *handler = func(rw http.ResponseWriter, req *http.Request) { + log.Printf("Request: (%d)\n", requestCounter) + if requestCounter == 0 { + assert.Equal(t, "/v2/configurations/123/vms/456", req.URL.Path, "Bad path") + assert.Equal(t, http.MethodGet, req.Method, "Bad method") + + _, err := io.WriteString(rw, string(response)) + assert.NoError(t, err) + } else if requestCounter == 1 { + assert.Equal(t, "/v2/configurations/123/vms/456/interfaces/789", req.URL.Path, "Bad path") + assert.Equal(t, "PUT", req.Method, "Bad method") + + body, err := ioutil.ReadAll(req.Body) + assert.Nil(t, err, "Bad request body") + assert.JSONEq(t, string(readTestFile(t, "exampleAttachInterfaceRequest.json")), string(body), "Bad request body") + + _, err = io.WriteString(rw, exampleInterface) + assert.NoError(t, err) + } else if requestCounter == 2 { + assert.Equal(t, "/v2/configurations/123/vms/456/interfaces/nic-20250403-38374059-4", req.URL.Path, "Bad path") + assert.Equal(t, http.MethodGet, req.Method, "Bad method") + + _, err := io.WriteString(rw, exampleInterface) + assert.NoError(t, err) + } + requestCounter++ } networkID := &AttachInterfaceRequest{ NetworkID: strToPtr("23917287"), @@ -158,10 +109,12 @@ func TestAttachInterface(t *testing.T) { var interfaceExpected Interface err = json.Unmarshal([]byte(exampleInterface), &interfaceExpected) assert.Equal(t, interfaceExpected, *networkInterface, "Bad interface") + + assert.Equal(t, 3, requestCounter) } func TestReadInterface(t *testing.T) { - exampleInterface := fmt.Sprintf(exampleCreateInterfaceResponse, 456, 123) + exampleInterface := fmt.Sprintf(string(readTestFile(t, "exampleCreateInterfaceResponse.json")), 456, 123) skytap, hs, handler := createClient(t) defer hs.Close() @@ -170,7 +123,8 @@ func TestReadInterface(t *testing.T) { assert.Equal(t, "/v2/configurations/123/vms/456/interfaces/789", req.URL.Path, "Bad path") assert.Equal(t, "GET", req.Method, "Bad method") - io.WriteString(rw, exampleInterface) + _, err := io.WriteString(rw, exampleInterface) + assert.NoError(t, err) } networkInterface, err := skytap.Interfaces.Get(context.Background(), "123", "456", "789") @@ -182,29 +136,57 @@ func TestReadInterface(t *testing.T) { } func TestUpdateInterface(t *testing.T) { - exampleInterface := fmt.Sprintf(exampleAttachInterfaceResponse) + response := fmt.Sprintf(string(readTestFile(t, "VMResponse.json")), 456) + exampleInterface := fmt.Sprintf(string(readTestFile(t, "exampleUpdateInterfaceResponse.json"))) skytap, hs, handler := createClient(t) defer hs.Close() - var networkInterface Interface - json.Unmarshal([]byte(exampleInterface), &networkInterface) - networkInterface.Hostname = strToPtr("updated-hostname") - - bytes, err := json.Marshal(&networkInterface) - assert.Nil(t, err, "Bad interface") + requestCounter := 0 *handler = func(rw http.ResponseWriter, req *http.Request) { - assert.Equal(t, "/v2/configurations/123/vms/456/interfaces/789", req.URL.Path, "Bad path") - assert.Equal(t, "PUT", req.Method, "Bad method") - - body, err := ioutil.ReadAll(req.Body) - assert.Nil(t, err, "Bad request body") - assert.JSONEq(t, exampleUpdateInterfaceRequest, string(body), "Bad request body") - - io.WriteString(rw, string(bytes)) + log.Printf("Request: (%d)\n", requestCounter) + if requestCounter == 0 { + assert.Equal(t, "/v2/configurations/123/vms/456", req.URL.Path, "Bad path") + assert.Equal(t, http.MethodGet, req.Method, "Bad method") + + _, err := io.WriteString(rw, string(response)) + assert.NoError(t, err) + } else if requestCounter == 1 { + assert.Equal(t, "/v2/configurations/123/vms/456/interfaces/789", req.URL.Path, "Bad path") + assert.Equal(t, "PUT", req.Method, "Bad method") + + body, err := ioutil.ReadAll(req.Body) + assert.Nil(t, err, "Bad request body") + assert.JSONEq(t, string(readTestFile(t, "exampleUpdateInterfaceRequest.json")), string(body), "Bad request body") + + _, err = io.WriteString(rw, string(readTestFile(t, "exampleUpdateInterfaceResponse.json"))) + assert.NoError(t, err) + } else if requestCounter == 2 { + assert.Equal(t, "/v2/configurations/123/vms/456/interfaces/nic-20250403-38374059-4", req.URL.Path, "Bad path") + assert.Equal(t, http.MethodGet, req.Method, "Bad method") + + var networkInterface Interface + err := json.Unmarshal([]byte(exampleInterface), &networkInterface) + assert.NoError(t, err) + networkInterface.IP = strToPtr("10.0.0.1") + networkInterface.Hostname = strToPtr("updated-hostname") + b, err := json.Marshal(&networkInterface) + assert.NoError(t, err) + _, err = io.WriteString(rw, string(b)) + assert.NoError(t, err) + } + requestCounter++ } + var networkInterface Interface + err := json.Unmarshal([]byte(exampleInterface), &networkInterface) + assert.NoError(t, err) + networkInterface.IP = strToPtr("10.0.0.1") + networkInterface.Hostname = strToPtr("updated-hostname") + _, err = json.Marshal(&networkInterface) + assert.Nil(t, err, "Bad interface") + opts := &UpdateInterfaceRequest{ Hostname: strToPtr(*networkInterface.Hostname), IP: strToPtr("10.0.0.1"), @@ -213,19 +195,38 @@ func TestUpdateInterface(t *testing.T) { assert.Nil(t, err, "Bad API method") assert.Equal(t, networkInterface, *interfaceUpdate, "Bad interface") + + assert.Equal(t, 3, requestCounter) } func TestDeleteInterface(t *testing.T) { + response := fmt.Sprintf(string(readTestFile(t, "VMResponse.json")), 456) + skytap, hs, handler := createClient(t) defer hs.Close() + requestCounter := 0 + *handler = func(rw http.ResponseWriter, req *http.Request) { - assert.Equal(t, "/v2/configurations/123/vms/456/interfaces/789", req.URL.Path, "Bad path") - assert.Equal(t, "DELETE", req.Method, "Bad method") + log.Printf("Request: (%d)\n", requestCounter) + if requestCounter == 0 { + assert.Equal(t, "/v2/configurations/123/vms/456", req.URL.Path, "Bad path") + assert.Equal(t, http.MethodGet, req.Method, "Bad method") + + _, err := io.WriteString(rw, string(response)) + assert.NoError(t, err) + } else if requestCounter == 1 { + log.Printf("Request: (%d)\n", requestCounter) + assert.Equal(t, "/v2/configurations/123/vms/456/interfaces/789", req.URL.Path, "Bad path") + assert.Equal(t, "DELETE", req.Method, "Bad method") + } + requestCounter++ } err := skytap.Interfaces.Delete(context.Background(), "123", "456", "789") assert.Nil(t, err, "Bad API method") + + assert.Equal(t, 2, requestCounter) } func TestListInterfaces(t *testing.T) { @@ -236,7 +237,8 @@ func TestListInterfaces(t *testing.T) { assert.Equal(t, "/v2/configurations/123/vms/456/interfaces", req.URL.Path, "Bad path") assert.Equal(t, "GET", req.Method, "Bad method") - io.WriteString(rw, exampleInterfaceListResponse) + _, err := io.WriteString(rw, string(readTestFile(t, "exampleInterfaceListResponse.json"))) + assert.NoError(t, err) } result, err := skytap.Interfaces.List(context.Background(), "123", "456") @@ -251,3 +253,131 @@ func TestListInterfaces(t *testing.T) { } assert.True(t, found, "Interface not found") } + +func TestCompareInterfaceCreateTrue(t *testing.T) { + exampleInterface := fmt.Sprintf(string(readTestFile(t, "exampleCreateInterfaceResponse.json")), 456, 123) + + var adapter Interface + err := json.Unmarshal([]byte(exampleInterface), &adapter) + assert.NoError(t, err) + opts := CreateInterfaceRequest{ + NICType: nicTypeToPtr(NICTypeE1000), + } + skytap, hs, handler := createClient(t) + defer hs.Close() + + *handler = func(rw http.ResponseWriter, req *http.Request) { + _, err := io.WriteString(rw, fmt.Sprintf(exampleInterface)) + assert.NoError(t, err) + } + message, ok := opts.compareResponse(context.Background(), skytap, &adapter, vmRequestRunStateStopped("123", "456")) + assert.True(t, ok) + assert.Equal(t, "", message) +} + +func TestCompareInterfaceCreateFalse(t *testing.T) { + exampleInterface := fmt.Sprintf(string(readTestFile(t, "exampleCreateInterfaceResponse.json")), 456, 123) + + var adapter Interface + err := json.Unmarshal([]byte(exampleInterface), &adapter) + assert.NoError(t, err) + opts := CreateInterfaceRequest{ + NICType: nicTypeToPtr(NICTypeE1000E), + } + skytap, hs, handler := createClient(t) + defer hs.Close() + + *handler = func(rw http.ResponseWriter, req *http.Request) { + _, err := io.WriteString(rw, fmt.Sprintf(exampleInterface)) + assert.NoError(t, err) + } + message, ok := opts.compareResponse(context.Background(), skytap, &adapter, vmRequestRunStateStopped("123", "456")) + assert.False(t, ok) + assert.Equal(t, "network adapter not ready", message) +} + +func TestCompareInterfaceAttachTrue(t *testing.T) { + exampleInterface := fmt.Sprintf(string(readTestFile(t, "exampleAttachInterfaceResponse.json"))) + + var adapter Interface + err := json.Unmarshal([]byte(exampleInterface), &adapter) + assert.NoError(t, err) + opts := AttachInterfaceRequest{ + strToPtr("23917287"), + } + skytap, hs, handler := createClient(t) + defer hs.Close() + + *handler = func(rw http.ResponseWriter, req *http.Request) { + _, err := io.WriteString(rw, fmt.Sprintf(exampleInterface)) + assert.NoError(t, err) + } + message, ok := opts.compareResponse(context.Background(), skytap, &adapter, vmRequestRunStateStopped("123", "456")) + assert.True(t, ok) + assert.Equal(t, "", message) +} + +func TestCompareInterfaceAttachFalse(t *testing.T) { + exampleInterface := fmt.Sprintf(string(readTestFile(t, "exampleAttachInterfaceResponse.json"))) + + var adapter Interface + err := json.Unmarshal([]byte(exampleInterface), &adapter) + assert.NoError(t, err) + opts := AttachInterfaceRequest{ + strToPtr("123"), + } + skytap, hs, handler := createClient(t) + defer hs.Close() + + *handler = func(rw http.ResponseWriter, req *http.Request) { + _, err := io.WriteString(rw, fmt.Sprintf(exampleInterface)) + assert.NoError(t, err) + } + message, ok := opts.compareResponse(context.Background(), skytap, &adapter, vmRequestRunStateStopped("123", "456")) + assert.False(t, ok) + assert.Equal(t, "network adapter not ready", message) +} + +func TestCompareInterfaceUpdateTrue(t *testing.T) { + exampleInterface := fmt.Sprintf(string(readTestFile(t, "exampleUpdateInterfaceResponse.json"))) + + var adapter Interface + err := json.Unmarshal([]byte(exampleInterface), &adapter) + assert.NoError(t, err) + opts := UpdateInterfaceRequest{ + IP: strToPtr("10.0.0.1"), + Hostname: strToPtr("updated-hostname"), + } + skytap, hs, handler := createClient(t) + defer hs.Close() + + *handler = func(rw http.ResponseWriter, req *http.Request) { + _, err := io.WriteString(rw, fmt.Sprintf(exampleInterface)) + assert.NoError(t, err) + } + message, ok := opts.compareResponse(context.Background(), skytap, &adapter, vmRequestRunStateStopped("123", "456")) + assert.True(t, ok) + assert.Equal(t, "", message) +} + +func TestCompareInterfaceUpdateFalse(t *testing.T) { + exampleInterface := fmt.Sprintf(string(readTestFile(t, "exampleUpdateInterfaceResponse.json"))) + + var adapter Interface + err := json.Unmarshal([]byte(exampleInterface), &adapter) + assert.NoError(t, err) + opts := UpdateInterfaceRequest{ + IP: strToPtr("10.0.0.2"), + Hostname: strToPtr("updated-hostname"), + } + skytap, hs, handler := createClient(t) + defer hs.Close() + + *handler = func(rw http.ResponseWriter, req *http.Request) { + _, err := io.WriteString(rw, fmt.Sprintf(exampleInterface)) + assert.NoError(t, err) + } + message, ok := opts.compareResponse(context.Background(), skytap, &adapter, vmRequestRunStateStopped("123", "456")) + assert.False(t, ok) + assert.Equal(t, "network adapter not ready", message) +} diff --git a/skytap/network.go b/skytap/network.go index c3d2eb8..c41e318 100644 --- a/skytap/network.go +++ b/skytap/network.go @@ -2,6 +2,8 @@ package skytap import ( "context" + "fmt" + "log" ) // Default URL paths @@ -31,7 +33,7 @@ type Network struct { ID *string `json:"id"` URL *string `json:"url"` Name *string `json:"name"` - NetworkType *string `json:"network_type"` + NetworkType *NetworkType `json:"network_type"` Subnet *string `json:"subnet"` SubnetAddr *string `json:"subnet_addr"` SubnetSize *int `json:"subnet_size"` @@ -130,7 +132,7 @@ func (s *NetworksServiceClient) List(ctx context.Context, environmentID string) } var networkListResponse NetworkListResult - _, err = s.client.do(ctx, req, &networkListResponse.Value) + _, err = s.client.do(ctx, req, &networkListResponse.Value, nil, nil) if err != nil { return nil, err } @@ -148,7 +150,7 @@ func (s *NetworksServiceClient) Get(ctx context.Context, environmentID string, i } var network Network - _, err = s.client.do(ctx, req, &network) + _, err = s.client.do(ctx, req, &network, nil, nil) if err != nil { return nil, err } @@ -166,7 +168,7 @@ func (s *NetworksServiceClient) Create(ctx context.Context, environmentID string } var createdNetwork Network - _, err = s.client.do(ctx, req, &createdNetwork) + _, err = s.client.do(ctx, req, &createdNetwork, envRunStateNotBusy(environmentID), opts) if err != nil { return nil, err } @@ -184,7 +186,7 @@ func (s *NetworksServiceClient) Update(ctx context.Context, environmentID string } var updatedNetwork Network - _, err = s.client.do(ctx, req, &updatedNetwork) + _, err = s.client.do(ctx, req, &updatedNetwork, envRunStateNotBusy(environmentID), network) if err != nil { return nil, err } @@ -201,7 +203,7 @@ func (s *NetworksServiceClient) Delete(ctx context.Context, environmentID string return err } - _, err = s.client.do(ctx, req, nil) + _, err = s.client.do(ctx, req, nil, nil, nil) if err != nil { return err } @@ -216,3 +218,145 @@ func (s *NetworksServiceClient) buildPath(environmentID string, networkID string } return path } + +func (payload *CreateNetworkRequest) compareResponse(ctx context.Context, c *Client, v interface{}, state *environmentVMRunState) (string, bool) { + if networkOriginal, ok := v.(*Network); ok { + network, err := c.Networks.Get(ctx, *state.environmentID, *networkOriginal.ID) + if err != nil { + return requestNotAsExpected, false + } + actual := payload.buildUpdateRequestFromVM(network) + if payload.string() == actual.string() { + return "", true + } + return "network not ready", false + } + log.Printf("[ERROR] SDK network comparison not possible on (%v)\n", v) + return requestNotAsExpected, false +} + +func (payload *UpdateNetworkRequest) compareResponse(ctx context.Context, c *Client, v interface{}, state *environmentVMRunState) (string, bool) { + if networkOriginal, ok := v.(*Network); ok { + network, err := c.Networks.Get(ctx, *state.environmentID, *networkOriginal.ID) + if err != nil { + return requestNotAsExpected, false + } + actual := payload.buildUpdateRequestFromVM(network) + if payload.string() == actual.string() { + return "", true + } + return "network not ready", false + } + log.Printf("[ERROR] SDK network comparison not possible on (%v)\n", v) + return requestNotAsExpected, false +} + +func (payload *CreateNetworkRequest) buildUpdateRequestFromVM(network *Network) CreateNetworkRequest { + actual := CreateNetworkRequest{} + if payload.Name != nil { + actual.Name = network.Name + } + if payload.NetworkType != nil { + actual.NetworkType = network.NetworkType + } + if payload.Subnet != nil { + actual.Subnet = network.Subnet + } + if payload.Domain != nil { + actual.Domain = network.Domain + } + if payload.Gateway != nil { + actual.Gateway = network.Gateway + } + if payload.Tunnelable != nil { + actual.Tunnelable = network.Tunnelable + } + return actual +} + +func (payload *UpdateNetworkRequest) buildUpdateRequestFromVM(network *Network) UpdateNetworkRequest { + actual := UpdateNetworkRequest{} + if payload.Name != nil { + actual.Name = network.Name + } + if payload.Subnet != nil { + actual.Subnet = network.Subnet + } + if payload.Domain != nil { + actual.Domain = network.Domain + } + if payload.Gateway != nil { + actual.Gateway = network.Gateway + } + if payload.Tunnelable != nil { + actual.Tunnelable = network.Tunnelable + } + return actual +} + +func (payload *CreateNetworkRequest) string() string { + name := "" + networkType := "" + subnet := "" + domain := "" + gateway := "" + tunnelable := "" + + if payload.Name != nil { + name = *payload.Name + } + if payload.NetworkType != nil { + networkType = string(*payload.NetworkType) + } + if payload.Subnet != nil { + subnet = *payload.Subnet + } + if payload.Domain != nil { + domain = *payload.Domain + } + if payload.Gateway != nil { + gateway = *payload.Gateway + } + if payload.Tunnelable != nil { + tunnelable = fmt.Sprintf("%t", *payload.Tunnelable) + } + return fmt.Sprintf("%s%s%s%s%s%s", + name, + networkType, + subnet, + domain, + gateway, + tunnelable) +} + +func (payload *UpdateNetworkRequest) string() string { + name := "" + subnet := "" + domain := "" + gateway := "" + tunnelable := "" + + if payload.Name != nil { + name = *payload.Name + } + if payload.Subnet != nil { + subnet = *payload.Subnet + } + if payload.Domain != nil { + domain = *payload.Domain + } + if payload.Gateway != nil { + gateway = *payload.Gateway + } + if payload.Tunnelable != nil { + tunnelable = fmt.Sprintf("%t", *payload.Tunnelable) + } + s := fmt.Sprintf("%s%s%s%s%s", + name, + subnet, + domain, + gateway, + tunnelable) + log.Printf("[DEBUG] SDK network payload (%s)\n", s) + return s +} diff --git a/skytap/network_test.go b/skytap/network_test.go index dad84e3..e000f39 100644 --- a/skytap/network_test.go +++ b/skytap/network_test.go @@ -6,53 +6,47 @@ import ( "fmt" "io" "io/ioutil" + "log" "net/http" "testing" "github.com/stretchr/testify/assert" ) -const exampleNetworkRequest = `{"name": - "test network", - "network_type": "automatic", - "subnet": "10.0.2.0/24", - "domain": "sampledomain.com", - "gateway": "10.0.2.254", - "tunnelable": true -}` - -const exampleNetworkResponse = `{"id": "%d", - "url": "https://cloud.skytap.com/v2/configurations/%d/networks/%d", - "name": "test network", - "network_type": "automatic", - "subnet": "10.0.2.0/24", - "subnet_addr": "10.0.2.0", - "subnet_size": 24, - "gateway": "10.0.2.254", - "primary_nameserver": null, - "secondary_nameserver": null, - "region": "US-West", - "domain": "sampledomain.com", - "vpn_attachments": [], - "tunnelable": true, - "tunnels": [] -}` - func TestCreateNetwork(t *testing.T) { - exampleNetwork := fmt.Sprintf(exampleNetworkResponse, 456, 123, 456) + exampleNetwork := fmt.Sprintf(string(readTestFile(t, "exampleNetworkResponse.json")), 456, 123, 456) skytap, hs, handler := createClient(t) defer hs.Close() - *handler = func(rw http.ResponseWriter, req *http.Request) { - assert.Equal(t, "/v2/configurations/123/networks", req.URL.Path, "Bad path") - assert.Equal(t, "POST", req.Method, "Bad method") + requestCounter := 0 - body, err := ioutil.ReadAll(req.Body) - assert.Nil(t, err, "Bad request body") - assert.JSONEq(t, exampleNetworkRequest, string(body), "Bad request body") - - io.WriteString(rw, exampleNetwork) + *handler = func(rw http.ResponseWriter, req *http.Request) { + log.Printf("Request: (%d)\n", requestCounter) + if requestCounter == 0 { + assert.Equal(t, "/v2/configurations/123", req.URL.Path, "Bad path") + assert.Equal(t, http.MethodGet, req.Method, "Bad method") + + _, err := io.WriteString(rw, string(readTestFile(t, "exampleEnvironment.json"))) + assert.NoError(t, err) + } else if requestCounter == 1 { + assert.Equal(t, "/v2/configurations/123/networks", req.URL.Path, "Bad path") + assert.Equal(t, "POST", req.Method, "Bad method") + + body, err := ioutil.ReadAll(req.Body) + assert.Nil(t, err, "Bad request body") + assert.JSONEq(t, string(readTestFile(t, "exampleNetworkRequest.json")), string(body), "Bad request body") + + _, err = io.WriteString(rw, exampleNetwork) + assert.NoError(t, err) + } else if requestCounter == 2 { + assert.Equal(t, "/v2/configurations/123/networks/456", req.URL.Path, "Bad path") + assert.Equal(t, http.MethodGet, req.Method, "Bad method") + + _, err := io.WriteString(rw, exampleNetwork) + assert.NoError(t, err) + } + requestCounter++ } opts := &CreateNetworkRequest{ Name: strToPtr("test network"), @@ -69,10 +63,12 @@ func TestCreateNetwork(t *testing.T) { var networkExpected Network err = json.Unmarshal([]byte(exampleNetwork), &networkExpected) assert.Equal(t, networkExpected, *network, "Bad network") + + assert.Equal(t, 3, requestCounter) } func TestReadNetwork(t *testing.T) { - exampleNetwork := fmt.Sprintf(exampleNetworkResponse, 456, 123, 456) + exampleNetwork := fmt.Sprintf(string(readTestFile(t, "exampleNetworkResponse.json")), 456, 123, 456) skytap, hs, handler := createClient(t) defer hs.Close() @@ -81,7 +77,8 @@ func TestReadNetwork(t *testing.T) { assert.Equal(t, "/v2/configurations/123/networks/456", req.URL.Path, "Bad path") assert.Equal(t, "GET", req.Method, "Bad method") - io.WriteString(rw, exampleNetwork) + _, err := io.WriteString(rw, exampleNetwork) + assert.NoError(t, err) } network, err := skytap.Networks.Get(context.Background(), "123", "456") @@ -93,27 +90,46 @@ func TestReadNetwork(t *testing.T) { } func TestUpdateNetwork(t *testing.T) { - exampleNetwork := fmt.Sprintf(exampleNetworkResponse, 456, 123, 456) - - skytap, hs, handler := createClient(t) - defer hs.Close() + exampleNetwork := fmt.Sprintf(string(readTestFile(t, "exampleNetworkResponse.json")), 456, 123, 456) var network Network - json.Unmarshal([]byte(exampleNetwork), &network) + err := json.Unmarshal([]byte(exampleNetwork), &network) + assert.NoError(t, err) *network.Name = "updated network" - - bytes, err := json.Marshal(&network) + b, err := json.Marshal(&network) assert.Nil(t, err, "Bad network") - *handler = func(rw http.ResponseWriter, req *http.Request) { - assert.Equal(t, "/v2/configurations/123/networks/456", req.URL.Path, "Bad path") - assert.Equal(t, "PUT", req.Method, "Bad method") + skytap, hs, handler := createClient(t) + defer hs.Close() - body, err := ioutil.ReadAll(req.Body) - assert.Nil(t, err, "Bad request body") - assert.JSONEq(t, `{"name": "updated network"}`, string(body), "Bad request body") + requestCounter := 0 - io.WriteString(rw, string(bytes)) + *handler = func(rw http.ResponseWriter, req *http.Request) { + log.Printf("Request: (%d)\n", requestCounter) + if requestCounter == 0 { + assert.Equal(t, "/v2/configurations/123", req.URL.Path, "Bad path") + assert.Equal(t, http.MethodGet, req.Method, "Bad method") + + _, err := io.WriteString(rw, string(readTestFile(t, "exampleEnvironment.json"))) + assert.NoError(t, err) + } else if requestCounter == 1 { + assert.Equal(t, "/v2/configurations/123/networks/456", req.URL.Path, "Bad path") + assert.Equal(t, "PUT", req.Method, "Bad method") + + body, err := ioutil.ReadAll(req.Body) + assert.Nil(t, err, "Bad request body") + assert.JSONEq(t, `{"name": "updated network"}`, string(body), "Bad request body") + + _, err = io.WriteString(rw, string(b)) + assert.NoError(t, err) + } else if requestCounter == 2 { + assert.Equal(t, "/v2/configurations/123/networks/456", req.URL.Path, "Bad path") + assert.Equal(t, http.MethodGet, req.Method, "Bad method") + + _, err := io.WriteString(rw, string(b)) + assert.NoError(t, err) + } + requestCounter++ } opts := &UpdateNetworkRequest{ @@ -123,23 +139,31 @@ func TestUpdateNetwork(t *testing.T) { assert.Nil(t, err, "Bad API method") assert.Equal(t, network, *networkUpdate, "Bad network") + + assert.Equal(t, 3, requestCounter) } func TestDeleteNetwork(t *testing.T) { skytap, hs, handler := createClient(t) defer hs.Close() + requestCounter := 0 + *handler = func(rw http.ResponseWriter, req *http.Request) { + log.Printf("Request: (%d)\n", requestCounter) assert.Equal(t, "/v2/configurations/123/networks/456", req.URL.Path, "Bad path") assert.Equal(t, "DELETE", req.Method, "Bad method") + requestCounter++ } err := skytap.Networks.Delete(context.Background(), "123", "456") assert.Nil(t, err, "Bad API method") + + assert.Equal(t, 1, requestCounter) } func TestListNetworks(t *testing.T) { - exampleNetwork := fmt.Sprintf(exampleNetworkResponse, 456, 123, 456) + exampleNetwork := fmt.Sprintf(string(readTestFile(t, "exampleNetworkResponse.json")), 456, 123, 456) skytap, hs, handler := createClient(t) defer hs.Close() @@ -148,7 +172,8 @@ func TestListNetworks(t *testing.T) { assert.Equal(t, "/v2/configurations/123/networks", req.URL.Path, "Bad path") assert.Equal(t, "GET", req.Method, "Bad method") - io.WriteString(rw, fmt.Sprintf(`[%+v]`, exampleNetwork)) + _, err := io.WriteString(rw, fmt.Sprintf(`[%+v]`, exampleNetwork)) + assert.NoError(t, err) } result, err := skytap.Networks.List(context.Background(), "123") @@ -163,3 +188,96 @@ func TestListNetworks(t *testing.T) { } assert.True(t, found, "Network not found") } + +func TestCompareNetworkCreateTrue(t *testing.T) { + exampleNetwork := fmt.Sprintf(string(readTestFile(t, "exampleNetworkResponse.json")), 456, 123, 456) + + var network Network + err := json.Unmarshal([]byte(exampleNetwork), &network) + assert.NoError(t, err) + opts := CreateNetworkRequest{ + Name: strToPtr("test network"), + Subnet: strToPtr("10.0.2.0/24"), + Gateway: strToPtr("10.0.2.254"), + Tunnelable: boolToPtr(true), + Domain: strToPtr("sampledomain.com"), + NetworkType: networkTypeToPtr(NetworkTypeAutomatic), + } + skytap, hs, handler := createClient(t) + defer hs.Close() + + *handler = func(rw http.ResponseWriter, req *http.Request) { + _, err := io.WriteString(rw, exampleNetwork) + assert.NoError(t, err) + } + message, ok := opts.compareResponse(context.Background(), skytap, &network, envRunStateNotBusy("123")) + assert.True(t, ok) + assert.Equal(t, "", message) +} + +func TestCompareNetworkCreateFalse(t *testing.T) { + exampleNetwork := fmt.Sprintf(string(readTestFile(t, "exampleNetworkResponse.json")), 456, 123, 456) + + var network Network + err := json.Unmarshal([]byte(exampleNetwork), &network) + assert.NoError(t, err) + opts := CreateNetworkRequest{ + Name: strToPtr("test network2"), + } + skytap, hs, handler := createClient(t) + defer hs.Close() + + *handler = func(rw http.ResponseWriter, req *http.Request) { + _, err := io.WriteString(rw, exampleNetwork) + assert.NoError(t, err) + } + message, ok := opts.compareResponse(context.Background(), skytap, &network, envRunStateNotBusy("123")) + assert.False(t, ok) + assert.Equal(t, "network not ready", message) +} + +func TestCompareNetworkUpdateTrue(t *testing.T) { + exampleNetwork := fmt.Sprintf(string(readTestFile(t, "exampleNetworkResponse.json")), 456, 123, 456) + + var network Network + err := json.Unmarshal([]byte(exampleNetwork), &network) + assert.NoError(t, err) + opts := UpdateNetworkRequest{ + Name: strToPtr("test network"), + Subnet: strToPtr("10.0.2.0/24"), + Gateway: strToPtr("10.0.2.254"), + Tunnelable: boolToPtr(true), + Domain: strToPtr("sampledomain.com"), + } + skytap, hs, handler := createClient(t) + defer hs.Close() + + *handler = func(rw http.ResponseWriter, req *http.Request) { + _, err := io.WriteString(rw, exampleNetwork) + assert.NoError(t, err) + } + message, ok := opts.compareResponse(context.Background(), skytap, &network, envRunStateNotBusy("123")) + assert.True(t, ok) + assert.Equal(t, "", message) +} + +func TestCompareNetworkUpdateFalse(t *testing.T) { + exampleNetwork := fmt.Sprintf(string(readTestFile(t, "exampleNetworkResponse.json")), 456, 123, 456) + + var network Network + err := json.Unmarshal([]byte(exampleNetwork), &network) + assert.NoError(t, err) + opts := UpdateNetworkRequest{ + Name: strToPtr("test network2"), + } + skytap, hs, handler := createClient(t) + defer hs.Close() + + *handler = func(rw http.ResponseWriter, req *http.Request) { + _, err := io.WriteString(rw, exampleNetwork) + assert.NoError(t, err) + } + message, ok := opts.compareResponse(context.Background(), skytap, &network, envRunStateNotBusy("123")) + assert.False(t, ok) + assert.Equal(t, "network not ready", message) +} diff --git a/skytap/project.go b/skytap/project.go index 2741f81..63c2ac9 100644 --- a/skytap/project.go +++ b/skytap/project.go @@ -2,6 +2,7 @@ package skytap import ( "context" + "errors" "fmt" ) @@ -63,7 +64,7 @@ func (s *ProjectsServiceClient) List(ctx context.Context) (*ProjectListResult, e } var projectListResponse ProjectListResult - _, err = s.client.do(ctx, req, &projectListResponse.Value) + _, err = s.client.do(ctx, req, &projectListResponse.Value, nil, nil) if err != nil { return nil, err } @@ -81,7 +82,7 @@ func (s *ProjectsServiceClient) Get(ctx context.Context, id int) (*Project, erro } var project Project - _, err = s.client.do(ctx, req, &project) + _, err = s.client.do(ctx, req, &project, nil, nil) if err != nil { return nil, err } @@ -97,13 +98,16 @@ func (s *ProjectsServiceClient) Create(ctx context.Context, project *Project) (* } var createdProject Project - _, err = s.client.do(ctx, req, &createdProject) + _, err = s.client.do(ctx, req, &createdProject, nil, nil) if err != nil { return nil, err } createdProject.Summary = project.Summary + if createdProject.ID == nil { + return nil, errors.New("missing project ID") + } // update project after creation to establish the resource information. updatedProject, err := s.Update(ctx, *createdProject.ID, &createdProject) if err != nil { @@ -123,7 +127,7 @@ func (s *ProjectsServiceClient) Update(ctx context.Context, id int, project *Pro } var updatedProject Project - _, err = s.client.do(ctx, req, &updatedProject) + _, err = s.client.do(ctx, req, &updatedProject, nil, nil) if err != nil { return nil, err } @@ -140,7 +144,7 @@ func (s *ProjectsServiceClient) Delete(ctx context.Context, id int) error { return err } - _, err = s.client.do(ctx, req, nil) + _, err = s.client.do(ctx, req, nil, nil, nil) if err != nil { return err } diff --git a/skytap/published_service.go b/skytap/published_service.go index d11a8b0..b20e671 100644 --- a/skytap/published_service.go +++ b/skytap/published_service.go @@ -1,6 +1,10 @@ package skytap -import "context" +import ( + "context" + "fmt" + "log" +) // Default URL paths const ( @@ -58,7 +62,6 @@ type PublishedServicesService interface { List(ctx context.Context, environmentID string, vmID string, nicID string) (*PublishedServiceListResult, error) Get(ctx context.Context, environmentID string, vmID string, nicID string, id string) (*PublishedService, error) Create(ctx context.Context, environmentID string, vmID string, nicID string, internalPort *CreatePublishedServiceRequest) (*PublishedService, error) - Update(ctx context.Context, environmentID string, vmID string, nicID string, id string, internalPort *UpdatePublishedServiceRequest) (*PublishedService, error) Delete(ctx context.Context, environmentID string, vmID string, nicID string, id string) error } @@ -81,11 +84,6 @@ type CreatePublishedServiceRequest struct { InternalPort *int `json:"internal_port"` } -// UpdatePublishedServiceRequest describes the update the publishedService data -type UpdatePublishedServiceRequest struct { - CreatePublishedServiceRequest -} - // PublishedServiceListResult is the listing request specific struct type PublishedServiceListResult struct { Value []PublishedService @@ -107,7 +105,7 @@ func (s *PublishedServicesServiceClient) List(ctx context.Context, environmentID } var serviceListResponse PublishedServiceListResult - _, err = s.client.do(ctx, req, &serviceListResponse.Value) + _, err = s.client.do(ctx, req, &serviceListResponse.Value, nil, nil) if err != nil { return nil, err } @@ -126,7 +124,7 @@ func (s *PublishedServicesServiceClient) Get(ctx context.Context, environmentID } var service PublishedService - _, err = s.client.do(ctx, req, &service) + _, err = s.client.do(ctx, req, &service, nil, nil) if err != nil { return nil, err } @@ -145,7 +143,7 @@ func (s *PublishedServicesServiceClient) Create(ctx context.Context, environment } var createdService PublishedService - _, err = s.client.do(ctx, req, &createdService) + _, err = s.client.do(ctx, req, &createdService, vmRunStateNotBusyWithAdapter(environmentID, vmID, nicID), internalPort) if err != nil { return nil, err } @@ -153,15 +151,6 @@ func (s *PublishedServicesServiceClient) Create(ctx context.Context, environment return &createdService, nil } -// Update a publishedService -func (s *PublishedServicesServiceClient) Update(ctx context.Context, environmentID string, vmID string, nicID string, id string, internalPort *UpdatePublishedServiceRequest) (*PublishedService, error) { - err := s.Delete(ctx, environmentID, vmID, nicID, id) - if err != nil { - return nil, err - } - return s.Create(ctx, environmentID, vmID, nicID, &internalPort.CreatePublishedServiceRequest) -} - // Delete a publishedService func (s *PublishedServicesServiceClient) Delete(ctx context.Context, environmentID string, vmID string, nicID string, id string) error { var builder publishedServicePathBuilderImpl @@ -172,10 +161,40 @@ func (s *PublishedServicesServiceClient) Delete(ctx context.Context, environment return err } - _, err = s.client.do(ctx, req, nil) + _, err = s.client.do(ctx, req, nil, nil, nil) if err != nil { return err } return nil } + +func (payload *CreatePublishedServiceRequest) compareResponse(ctx context.Context, c *Client, v interface{}, state *environmentVMRunState) (string, bool) { + if serviceOriginal, ok := v.(*PublishedService); ok { + publishedService, err := c.PublishedServices.Get(ctx, *state.environmentID, *state.vmID, *state.adapterID, *serviceOriginal.ID) + if err != nil { + return requestNotAsExpected, false + } + actual := CreatePublishedServiceRequest{ + publishedService.InternalPort, + } + if payload.string() == actual.string() { + return "", true + } + return "published service not ready", false + } + log.Printf("[ERROR] SDK published service comparison not possible on (%v)\n", v) + return requestNotAsExpected, false +} + +func (payload *CreatePublishedServiceRequest) string() string { + internalPort := "" + + if payload.InternalPort != nil { + internalPort = fmt.Sprintf("%d", *payload.InternalPort) + } + s := fmt.Sprintf("%s", + internalPort) + log.Printf("[DEBUG] SDK create published service payload (%s)\n", s) + return s +} diff --git a/skytap/published_service_test.go b/skytap/published_service_test.go index 638f680..1924a8e 100644 --- a/skytap/published_service_test.go +++ b/skytap/published_service_test.go @@ -6,55 +6,50 @@ import ( "fmt" "io" "io/ioutil" + "log" "net/http" "testing" "github.com/stretchr/testify/assert" ) -const examplePublishedServiceRequest = `{ - "internal_port": %d -}` - -const examplePublishedServiceResponse = `{ - "id": "%d", - "internal_port": %d, - "external_ip": "services-uswest.skytap.com", - "external_port": 26160 -}` - -const examplePublishedServiceListResponse = `[ - { - "id": "8080", - "internal_port": 8080, - "external_ip": "services-uswest.skytap.com", - "external_port": 26160 - }, - { - "id": "8081", - "internal_port": 8081, - "external_ip": "services-uswest.skytap.com", - "external_port": 17785 - } -]` - func TestCreateService(t *testing.T) { + response := fmt.Sprintf(string(readTestFile(t, "VMResponse.json")), 456) + port := 8080 - exampleService := fmt.Sprintf(examplePublishedServiceResponse, port, port) + exampleService := fmt.Sprintf(string(readTestFile(t, "examplePublishedServiceResponse.json")), port, port) skytap, hs, handler := createClient(t) defer hs.Close() + requestCounter := 0 + *handler = func(rw http.ResponseWriter, req *http.Request) { - assert.Equal(t, "/v2/configurations/123/vms/456/interfaces/789/services", req.URL.Path, "Bad path") - assert.Equal(t, "POST", req.Method, "Bad method") + log.Printf("Request: (%d)\n", requestCounter) + if requestCounter == 0 { + assert.Equal(t, "/v2/configurations/123/vms/456", req.URL.Path, "Bad path") + assert.Equal(t, http.MethodGet, req.Method, "Bad method") - body, err := ioutil.ReadAll(req.Body) - assert.Nil(t, err, "Bad request body") - assert.JSONEq(t, fmt.Sprintf(examplePublishedServiceRequest, port), string(body), "Bad request body") + _, err := io.WriteString(rw, response) + assert.NoError(t, err) + } else if requestCounter == 1 { + assert.Equal(t, "/v2/configurations/123/vms/456/interfaces/789/services", req.URL.Path, "Bad path") + assert.Equal(t, "POST", req.Method, "Bad method") - _, err = io.WriteString(rw, exampleService) - assert.NoError(t, err) + body, err := ioutil.ReadAll(req.Body) + assert.Nil(t, err, "Bad request body") + assert.JSONEq(t, fmt.Sprintf(string(readTestFile(t, "examplePublishedServiceRequest.json")), port), string(body), "Bad request body") + + _, err = io.WriteString(rw, exampleService) + assert.NoError(t, err) + } else if requestCounter == 2 { + assert.Equal(t, "/v2/configurations/123/vms/456/interfaces/789/services/8080", req.URL.Path, "Bad path") + assert.Equal(t, http.MethodGet, req.Method, "Bad method") + + _, err := io.WriteString(rw, exampleService) + assert.NoError(t, err) + } + requestCounter++ } internalPort := &CreatePublishedServiceRequest{ InternalPort: intToPtr(port), @@ -66,20 +61,26 @@ func TestCreateService(t *testing.T) { var serviceExpected PublishedService err = json.Unmarshal([]byte(exampleService), &serviceExpected) assert.Equal(t, serviceExpected, *service, "Bad publishedService") + + assert.Equal(t, 3, requestCounter) } func TestReadService(t *testing.T) { - exampleService := fmt.Sprintf(examplePublishedServiceResponse, 8080, 8080) + exampleService := fmt.Sprintf(string(readTestFile(t, "examplePublishedServiceResponse.json")), 8080, 8080) skytap, hs, handler := createClient(t) defer hs.Close() + requestCounter := 0 + *handler = func(rw http.ResponseWriter, req *http.Request) { + log.Printf("Request: (%d)\n", requestCounter) assert.Equal(t, "/v2/configurations/123/vms/456/interfaces/789/services/abc", req.URL.Path, "Bad path") assert.Equal(t, "GET", req.Method, "Bad method") _, err := io.WriteString(rw, exampleService) assert.NoError(t, err) + requestCounter++ } service, err := skytap.PublishedServices.Get(context.Background(), "123", "456", "789", "abc") @@ -88,73 +89,43 @@ func TestReadService(t *testing.T) { var serviceExpected PublishedService err = json.Unmarshal([]byte(exampleService), &serviceExpected) assert.Equal(t, serviceExpected, *service, "Bad Interface") -} - -func TestUpdateService(t *testing.T) { - port := 8081 - exampleService := fmt.Sprintf(examplePublishedServiceResponse, port, port) - - skytap, hs, handler := createClient(t) - defer hs.Close() - - var service PublishedService - err := json.Unmarshal([]byte(exampleService), &service) - assert.NoError(t, err) - - var deletePhase = true - - *handler = func(rw http.ResponseWriter, req *http.Request) { - if deletePhase { - assert.Equal(t, "/v2/configurations/123/vms/456/interfaces/789/services/abc", req.URL.Path, "Bad path") - assert.Equal(t, "DELETE", req.Method, "Bad method") - deletePhase = false - } else { - assert.Equal(t, "/v2/configurations/123/vms/456/interfaces/789/services", req.URL.Path, "Bad path") - assert.Equal(t, "POST", req.Method, "Bad method") - - body, err := ioutil.ReadAll(req.Body) - assert.Nil(t, err, "Bad request body") - assert.JSONEq(t, fmt.Sprintf(examplePublishedServiceRequest, port), string(body), "Bad request body") - - _, err = io.WriteString(rw, exampleService) - assert.NoError(t, err) - } - } - opts := &UpdatePublishedServiceRequest{ - CreatePublishedServiceRequest{ - InternalPort: intToPtr(port), - }, - } - serviceUpdate, err := skytap.PublishedServices.Update(context.Background(), "123", "456", "789", "abc", opts) - assert.Nil(t, err, "Bad API method") - - assert.Equal(t, service, *serviceUpdate, "Bad publishedService") + assert.Equal(t, 1, requestCounter) } func TestDeleteService(t *testing.T) { skytap, hs, handler := createClient(t) defer hs.Close() + requestCounter := 0 + *handler = func(rw http.ResponseWriter, req *http.Request) { + log.Printf("Request: (%d)\n", requestCounter) assert.Equal(t, "/v2/configurations/123/vms/456/interfaces/789/services/abc", req.URL.Path, "Bad path") assert.Equal(t, "DELETE", req.Method, "Bad method") + requestCounter++ } err := skytap.PublishedServices.Delete(context.Background(), "123", "456", "789", "abc") assert.Nil(t, err, "Bad API method") + + assert.Equal(t, 1, requestCounter) } func TestListServices(t *testing.T) { skytap, hs, handler := createClient(t) defer hs.Close() + requestCounter := 0 + *handler = func(rw http.ResponseWriter, req *http.Request) { + log.Printf("Request: (%d)\n", requestCounter) assert.Equal(t, "/v2/configurations/123/vms/456/interfaces/789/services", req.URL.Path, "Bad path") assert.Equal(t, "GET", req.Method, "Bad method") - _, err := io.WriteString(rw, examplePublishedServiceListResponse) + _, err := io.WriteString(rw, string(readTestFile(t, "examplePublishedServiceListResponse.json"))) assert.NoError(t, err) + requestCounter++ } result, err := skytap.PublishedServices.List(context.Background(), "123", "456", "789") @@ -168,4 +139,53 @@ func TestListServices(t *testing.T) { } } assert.True(t, found, "PublishedService not found") + + assert.Equal(t, 1, requestCounter) +} + +func TestComparePublishedServiceCreateTrue(t *testing.T) { + examplePublishedService := fmt.Sprintf(string(readTestFile(t, "examplePublishedServiceResponse.json")), 789, 8080) + + var service PublishedService + err := json.Unmarshal([]byte(examplePublishedService), &service) + assert.NoError(t, err) + opts := CreatePublishedServiceRequest{ + InternalPort: intToPtr(8080), + } + skytap, hs, handler := createClient(t) + defer hs.Close() + + *handler = func(rw http.ResponseWriter, req *http.Request) { + _, err := io.WriteString(rw, string(examplePublishedService)) + assert.NoError(t, err) + } + state := vmRunStateNotBusy("123", "456") + state.adapterID = strToPtr("789") + message, ok := opts.compareResponse(context.Background(), skytap, &service, state) + assert.True(t, ok) + assert.Equal(t, "", message) +} + +func TestComparePublishedServiceCreateFalse(t *testing.T) { + examplePublishedService := fmt.Sprintf(string(readTestFile(t, "examplePublishedServiceResponse.json")), 789, 8080) + + var service PublishedService + err := json.Unmarshal([]byte(examplePublishedService), &service) + assert.NoError(t, err) + opts := CreatePublishedServiceRequest{ + InternalPort: intToPtr(8081), + } + skytap, hs, handler := createClient(t) + defer hs.Close() + + *handler = func(rw http.ResponseWriter, req *http.Request) { + response := fmt.Sprintf(string(readTestFile(t, "examplePublishedServiceRequest.json")), 8080) + _, err := io.WriteString(rw, string(response)) + assert.NoError(t, err) + } + state := vmRunStateNotBusy("123", "456") + state.adapterID = strToPtr("789") + message, ok := opts.compareResponse(context.Background(), skytap, &service, state) + assert.False(t, ok) + assert.Equal(t, "published service not ready", message) } diff --git a/skytap/template.go b/skytap/template.go index 2d3b477..1768a90 100644 --- a/skytap/template.go +++ b/skytap/template.go @@ -73,7 +73,7 @@ func (s *TemplatesServiceClient) List(ctx context.Context) (*TemplateListResult, } var templatesListResponse TemplateListResult - _, err = s.client.do(ctx, req, &templatesListResponse.Value) + _, err = s.client.do(ctx, req, &templatesListResponse.Value, nil, nil) if err != nil { return nil, err } @@ -91,7 +91,7 @@ func (s *TemplatesServiceClient) Get(ctx context.Context, id string) (*Template, } var template Template - _, err = s.client.do(ctx, req, &template) + _, err = s.client.do(ctx, req, &template, nil, nil) if err != nil { return nil, err } diff --git a/skytap/testdata/createVMResponse.json b/skytap/testdata/createVMResponse.json index 1e6e79a..c4b4a77 100644 --- a/skytap/testdata/createVMResponse.json +++ b/skytap/testdata/createVMResponse.json @@ -2,7 +2,7 @@ "id": "%d", "url": "https://cloud.skytap.com/configurations/%d", "name": "base", - "error": "", + "error": false, "runstate": "busy", "rate_limited": false, "description": "used for basic understanding", diff --git a/skytap/testdata/exampleAttachInterfaceRequest.json b/skytap/testdata/exampleAttachInterfaceRequest.json new file mode 100644 index 0000000..50a7d44 --- /dev/null +++ b/skytap/testdata/exampleAttachInterfaceRequest.json @@ -0,0 +1,3 @@ +{ + "network_id": "23917287" +} \ No newline at end of file diff --git a/skytap/testdata/exampleAttachInterfaceResponse.json b/skytap/testdata/exampleAttachInterfaceResponse.json new file mode 100644 index 0000000..ed281a8 --- /dev/null +++ b/skytap/testdata/exampleAttachInterfaceResponse.json @@ -0,0 +1,21 @@ +{ + "id": "nic-20250403-38374059-4", + "ip": "192.168.0.5", + "hostname": "host-3", + "mac": "00:50:56:05:3F:84", + "services_count": 0, + "services": [], + "public_ips_count": 0, + "public_ips": [], + "vm_id": "37533321", + "vm_name": "CentOS 6 Desktop x64", + "status": "Powered off", + "network_id": "23917287", + "network_name": "tftest-network-1", + "network_url": "https://cloud.skytap.com/v2/configurations/40071754/networks/23922457", + "network_type": "automatic", + "network_subnet": "192.168.0.0/16", + "nic_type": "vmxnet3", + "secondary_ips": [], + "public_ip_attachments": [] +} \ No newline at end of file diff --git a/skytap/testdata/exampleCreateInterfaceRequest.json b/skytap/testdata/exampleCreateInterfaceRequest.json new file mode 100644 index 0000000..e47e8fe --- /dev/null +++ b/skytap/testdata/exampleCreateInterfaceRequest.json @@ -0,0 +1,3 @@ +{ + "nic_type": "e1000" +} \ No newline at end of file diff --git a/skytap/testdata/exampleCreateInterfaceResponse.json b/skytap/testdata/exampleCreateInterfaceResponse.json new file mode 100644 index 0000000..ae1ecc1 --- /dev/null +++ b/skytap/testdata/exampleCreateInterfaceResponse.json @@ -0,0 +1,16 @@ +{ + "id": "nic-%d", + "ip": null, + "hostname": null, + "mac": "00:50:56:07:40:3F", + "services_count": 0, + "services": [], + "public_ips_count": 0, + "public_ips": [], + "vm_id": "%d", + "vm_name": "Windows Server 2016 Standard", + "status": "Powered off", + "nic_type": "e1000", + "secondary_ips": [], + "public_ip_attachments": [] +} \ No newline at end of file diff --git a/skytap/testdata/exampleEnvironment.json b/skytap/testdata/exampleEnvironment.json new file mode 100644 index 0000000..845457c --- /dev/null +++ b/skytap/testdata/exampleEnvironment.json @@ -0,0 +1,349 @@ +{ + "id": "456", + "url": "https://cloud.skytap.com/v2/configurations/456", + "name": "No VM", + "description": "test environment", + "errors": [ + "error1" + ], + "error_details": [ + "error1 details" + ], + "runstate": "stopped", + "rate_limited": false, + "last_run": "2018/10/11 15:42:23 +0100", + "suspend_on_idle": 1, + "suspend_at_time": "2018/10/11 15:42:23 +0100", + "owner_url": "https://cloud.skytap.com/v2/users/1", + "owner_name": "Joe Bloggs", + "owner_id": "1", + "vm_count": 1, + "storage": 30720, + "network_count": 1, + "created_at": "2018/10/11 15:42:23 +0100", + "region": "US-West", + "region_backend": "skytap", + "svms": 1, + "can_save_as_template": true, + "can_copy": true, + "can_delete": true, + "can_change_state": true, + "can_share": true, + "can_edit": true, + "label_count": 1, + "label_category_count": 1, + "can_tag": true, + "tags": [ + { + "id": "43894", + "value": "tag1" + }, + { + "id": "43896", + "value": "tag2" + } + ], + "tag_list": "tag1,tag2", + "alerts": [ + { + "id":"586", + "display_type":"informational_alert", + "dismissable":true, + "message":"IBM i Technology Preview Program nominations are now open. For more information see What's New.", + "display_on_general":true, + "display_on_login":false, + "display_on_smartclient":false + } + ], + "published_service_count": 0, + "public_ip_count": 0, + "auto_suspend_description": null, + "stages": [ + { + "delay_after_finish_seconds": 300, + "index": 0, + "vm_ids": [ + "123456", + "123457" + ] + } + ], + "staged_execution": { + "action_type": "suspend", + "current_stage_delay_after_finish_seconds": 300, + "current_stage_index": 1, + "current_stage_finished_at": "2018/10/11 15:42:23 +0100", + "vm_ids": [ + "123453", + "123454" + ] + }, + "sequencing_enabled": false, + "note_count": 1, + "project_count_for_user": 0, + "project_count": 0, + "publish_set_count": 0, + "schedule_count": 0, + "vpn_count": 0, + "outbound_traffic": false, + "routable": false, + "vms": [ + { + "id": "36858580", + "name": "CentOS 7 Server x64", + "runstate": "stopped", + "rate_limited": false, + "hardware": { + "cpus": 1, + "supports_multicore": true, + "cpus_per_socket": 1, + "ram": 1024, + "svms": 1, + "guestOS": "centos-64", + "max_cpus": 12, + "min_ram": 256, + "max_ram": 262144, + "vnc_keymap": null, + "uuid": null, + "disks": [ + { + "id": "disk-19861359-37668995-scsi-0-0", + "size": 30720, + "type": "SCSI", + "controller": "0", + "lun": "0" + } + ], + "storage": 30720, + "upgradable": false, + "instance_type": null, + "time_sync_enabled": true, + "rtc_start_time": null, + "copy_paste_enabled": true, + "nested_virtualization": false, + "architecture": "x86" + }, + "error": false, + "error_details": false, + "asset_id": "1", + "hardware_version": 11, + "max_hardware_version": 11, + "interfaces": [ + { + "id": "nic-19861359-37668995-0", + "ip": "10.0.0.1", + "hostname": "centos7sx64", + "mac": "00:50:56:2B:87:F5", + "services_count": 0, + "services": [ + { + "id": "3389", + "internal_port": 3389, + "external_ip": "76.191.118.29", + "external_port": 12345 + } + ], + "public_ips_count": 0, + "public_ips": [ + { + "1.2.3.4": "5.6.7.8" + } + ], + "vm_id": "36858580", + "vm_name": "CentOS 7 Server x64", + "status": "Powered off", + "network_id": "23429874", + "network_name": "Default Network", + "network_url": "https://cloud.skytap.com/v2/configurations/456/networks/23429874", + "network_type": "automatic", + "network_subnet": "10.0.0.0/24", + "nic_type": "vmxnet3", + "secondary_ips": [ + { + "id": "10.0.2.2", + "address": "10.0.2.2" + } + ], + "public_ip_attachments": [ + { + "id": 1, + "public_ip_attachment_key": 2, + "address": "1.2.3.4", + "connect_type": 1, + "hostname": "host1", + "dns_name": "host.com", + "public_ip_key": "5.6.7.8" + } + ] + } + ], + "notes": [ + { + "id": "5377708", + "user_id": 1, + "user": { + "id": "1", + "url": "https://cloud.skytap.com/v2/users/1", + "first_name": "Joe", + "last_name": "Bloggs", + "login_name": "Joe.Bloggs@opencredo", + "email": "Joe.Bloggs@opencredo.com", + "title": "", + "deleted": false + }, + "created_at": "2018/10/11 15:27:45 +0100", + "updated_at": "2018/10/11 15:27:45 +0100", + "text": "a note" + } + ], + "labels": [ + { + "id": "43892", + "value": "test vm", + "label_category": "test multi", + "label_category_id": "7704", + "label_category_single_value": false + } + ], + "credentials": [ + { + "id": "35158632", + "text": "user/pass" + } + ], + "desktop_resizable": true, + "local_mouse_cursor": true, + "maintenance_lock_engaged": false, + "region_backend": "skytap", + "created_at": "2018/10/11 15:42:26 +0100", + "supports_suspend": true, + "can_change_object_state": true, + "containers": [ + { + "id": 1122, + "cid": "123456789abcdefghijk123456789abcdefghijk123456789abcdefghijk", + "name": "nginxtest1", + "image": "nginx:latest", + "created_at": "2016/06/16 11:58:50 -0700", + "last_run": "2016/06/16 11:58:51 -0700", + "can_change_state": true, + "can_delete": true, + "status": "running", + "privileged": false, + "vm_id": 111000, + "vm_name": "Docker VM1", + "vm_runstate": "running", + "configuration_id": 123456 + } + ], + "configuration_url": "https://cloud.skytap.com/v2/configurations/456" + } + ], + "networks": [ + { + "id": "1234567", + "url": "https://cloud.skytap.com/configurations/1111111/networks/123467", + "name": "Network 1", + "network_type": "automatic", + "subnet": "10.0.0.0/24", + "subnet_addr": "10.0.0.0", + "subnet_size": 24, + "gateway": "10.0.0.254", + "primary_nameserver": "8.8.8.8", + "secondary_nameserver": "8.8.8.9", + "region": "US-West", + "domain": "sampledomain.com", + "vpn_attachments": [ + { + "id": "111111-vpn-1234567", + "connected": false, + "network": { + "id": "1111111", + "subnet": "10.0.0.0/24", + "network_name": "Network 1", + "configuration_id": "1212121" + }, + "vpn": { + "id": "vpn-1234567", + "name": "CorpNet", + "enabled": true, + "nat_enabled": true, + "remote_subnets": "10.10.0.0/24, 10.10.1.0/24, 10.10.2.0/24, 10.10.4.0/24", + "remote_peer_ip": "199.199.199.199", + "can_reconnect": true + } + }, + { + "id": "111111-vpn-1234555", + "connected": false, + "network": { + "id": "1111111", + "subnet": "10.0.0.0/24", + "network_name": "Network 1", + "configuration_id": "1212121" + }, + "vpn": { + "id": "vpn-1234555", + "name": "Offsite DC", + "enabled": true, + "nat_enabled": true, + "remote_subnets": "10.10.0.0/24, 10.10.1.0/24, 10.10.2.0/24, 10.10.4.0/24", + "remote_peer_ip": "188.188.188.188", + "can_reconnect": true + } + } + ], + "tunnelable": false, + "tunnels": [ + { + "id": "tunnel-123456-789011", + "status": "not_busy", + "error": null, + "source_network": { + "id": "000000", + "url": "https://cloud.skytap.com/configurations/249424/networks/0000000", + "name": "Network 1", + "network_type": "automatic", + "subnet": "10.0.0.0/24", + "subnet_addr": "10.0.0.0", + "subnet_size": 24, + "gateway": "10.0.0.254", + "primary_nameserver": null, + "secondary_nameserver": null, + "region": "US-West", + "domain": "skytap.example", + "vpn_attachments": [] + }, + "target_network": { + "id": "111111", + "url": "https://cloud.skytap.com/configurations/808216/networks/111111", + "name": "Network 2", + "network_type": "automatic", + "subnet": "10.0.2.0/24", + "subnet_addr": "10.0.2.0", + "subnet_size": 24, + "gateway": "10.0.2.254", + "primary_nameserver": null, + "secondary_nameserver": null, + "region": "US-West", + "domain": "test.net", + "vpn_attachments": [] + } + } + ] + } + ], + "containers_count": 0, + "container_hosts_count": 0, + "platform_errors": [ + "platform error1" + ], + "svms_by_architecture": { + "x86": 1, + "power": 0 + }, + "all_vms_support_suspend": true, + "shutdown_on_idle": null, + "shutdown_at_time": null, + "auto_shutdown_description": "Shutting down!" +} \ No newline at end of file diff --git a/skytap/testdata/exampleInterfaceListResponse.json b/skytap/testdata/exampleInterfaceListResponse.json new file mode 100644 index 0000000..880752c --- /dev/null +++ b/skytap/testdata/exampleInterfaceListResponse.json @@ -0,0 +1,39 @@ +[ + { + "id": "nic-20246343-38367563-0", + "ip": "192.168.0.1", + "hostname": "wins2016s", + "mac": "00:50:56:11:7D:D9", + "services_count": 0, + "services": [], + "public_ips_count": 0, + "public_ips": [], + "vm_id": "37527239", + "vm_name": "Windows Server 2016 Standard", + "status": "Running", + "network_id": "23917287", + "network_name": "tftest-network-1", + "network_url": "https://cloud.skytap.com/v2/configurations/40064014/networks/23917287", + "network_type": "automatic", + "network_subnet": "192.168.0.0/16", + "nic_type": "vmxnet3", + "secondary_ips": [], + "public_ip_attachments": [] + }, + { + "id": "nic-20246343-38367563-5", + "ip": null, + "hostname": null, + "mac": "00:50:56:07:40:3F", + "services_count": 0, + "services": [], + "public_ips_count": 0, + "public_ips": [], + "vm_id": "37527239", + "vm_name": "Windows Server 2016 Standard", + "status": "Running", + "nic_type": "e1000", + "secondary_ips": [], + "public_ip_attachments": [] + } +] \ No newline at end of file diff --git a/skytap/testdata/exampleNetworkRequest.json b/skytap/testdata/exampleNetworkRequest.json new file mode 100644 index 0000000..5e59d92 --- /dev/null +++ b/skytap/testdata/exampleNetworkRequest.json @@ -0,0 +1,8 @@ +{ + "name":"test network", + "network_type": "automatic", + "subnet": "10.0.2.0/24", + "domain": "sampledomain.com", + "gateway": "10.0.2.254", + "tunnelable": true +} \ No newline at end of file diff --git a/skytap/testdata/exampleNetworkResponse.json b/skytap/testdata/exampleNetworkResponse.json new file mode 100644 index 0000000..74ab822 --- /dev/null +++ b/skytap/testdata/exampleNetworkResponse.json @@ -0,0 +1,17 @@ +{ + "id": "%d", + "url": "https://cloud.skytap.com/v2/configurations/%d/networks/%d", + "name": "test network", + "network_type": "automatic", + "subnet": "10.0.2.0/24", + "subnet_addr": "10.0.2.0", + "subnet_size": 24, + "gateway": "10.0.2.254", + "primary_nameserver": null, + "secondary_nameserver": null, + "region": "US-West", + "domain": "sampledomain.com", + "vpn_attachments": [], + "tunnelable": true, + "tunnels": [] +} \ No newline at end of file diff --git a/skytap/testdata/examplePublishedServiceListResponse.json b/skytap/testdata/examplePublishedServiceListResponse.json new file mode 100644 index 0000000..239179d --- /dev/null +++ b/skytap/testdata/examplePublishedServiceListResponse.json @@ -0,0 +1,14 @@ +[ + { + "id": "8080", + "internal_port": 8080, + "external_ip": "services-uswest.skytap.com", + "external_port": 26160 + }, + { + "id": "8081", + "internal_port": 8081, + "external_ip": "services-uswest.skytap.com", + "external_port": 17785 + } +] \ No newline at end of file diff --git a/skytap/testdata/examplePublishedServiceRequest.json b/skytap/testdata/examplePublishedServiceRequest.json new file mode 100644 index 0000000..cbef3b9 --- /dev/null +++ b/skytap/testdata/examplePublishedServiceRequest.json @@ -0,0 +1,3 @@ +{ + "internal_port": %d +} \ No newline at end of file diff --git a/skytap/testdata/examplePublishedServiceResponse.json b/skytap/testdata/examplePublishedServiceResponse.json new file mode 100644 index 0000000..7a8bf13 --- /dev/null +++ b/skytap/testdata/examplePublishedServiceResponse.json @@ -0,0 +1,6 @@ +{ + "id": "%d", + "internal_port": %d, + "external_ip": "services-uswest.skytap.com", + "external_port": 26160 +} \ No newline at end of file diff --git a/skytap/testdata/exampleUpdateInterfaceRequest.json b/skytap/testdata/exampleUpdateInterfaceRequest.json new file mode 100644 index 0000000..d136c4d --- /dev/null +++ b/skytap/testdata/exampleUpdateInterfaceRequest.json @@ -0,0 +1,4 @@ +{ + "ip": "10.0.0.1", + "hostname": "updated-hostname" +} \ No newline at end of file diff --git a/skytap/testdata/exampleUpdateInterfaceResponse.json b/skytap/testdata/exampleUpdateInterfaceResponse.json new file mode 100644 index 0000000..d3386fb --- /dev/null +++ b/skytap/testdata/exampleUpdateInterfaceResponse.json @@ -0,0 +1,21 @@ +{ + "id": "nic-20250403-38374059-4", + "ip": "10.0.0.1", + "hostname": "updated-hostname", + "mac": "00:50:56:05:3F:84", + "services_count": 0, + "services": [], + "public_ips_count": 0, + "public_ips": [], + "vm_id": "37533321", + "vm_name": "CentOS 6 Desktop x64", + "status": "Powered off", + "network_id": "23922457", + "network_name": "tftest-network-1", + "network_url": "https://cloud.skytap.com/v2/configurations/40071754/networks/23922457", + "network_type": "automatic", + "network_subnet": "192.168.0.0/16", + "nic_type": "vmxnet3", + "secondary_ips": [], + "public_ip_attachments": [] +} \ No newline at end of file diff --git a/skytap/vm.go b/skytap/vm.go index 85e0ccf..b5eef04 100644 --- a/skytap/vm.go +++ b/skytap/vm.go @@ -4,8 +4,8 @@ import ( "context" "fmt" "log" - "net/http" "sort" + "strings" "time" ) @@ -39,8 +39,6 @@ type VM struct { Runstate *VMRunstate `json:"runstate"` RateLimited *bool `json:"rate_limited"` Hardware *Hardware `json:"hardware"` - Error *bool `json:"error"` - ErrorDetails *bool `json:"error_details"` AssetID *string `json:"asset_id"` HardwareVersion *int `json:"hardware_version"` MaxHardwareVersion *int `json:"max_hardware_version"` @@ -239,7 +237,7 @@ func (s *VMsServiceClient) List(ctx context.Context, environmentID string) (*VML } var vmListResponse VMListResult - _, err = s.client.do(ctx, req, &vmListResponse.Value) + _, err = s.client.do(ctx, req, &vmListResponse.Value, nil, nil) if err != nil { return nil, err } @@ -257,7 +255,7 @@ func (s *VMsServiceClient) Get(ctx context.Context, environmentID string, id str } var vm VM - _, err = s.client.do(ctx, req, &vm) + _, err = s.client.do(ctx, req, &vm, nil, nil) if err != nil { return nil, err } @@ -273,34 +271,17 @@ func (s *VMsServiceClient) Create(ctx context.Context, environmentID string, opt TemplateID: opts.TemplateID, VMID: []string{opts.VMID}, } - req, err := s.client.newRequest(ctx, "PUT", path, apiOpts) + + var environment Environment + _, err = s.client.do(ctx, req, &environment, envRunStateNotBusy(environmentID), opts) if err != nil { return nil, err } - // Retry to work around 422 errors on creating a vm. - var createdEnvironment Environment - var makeRequest = true - for i := 0; i < s.client.retryCount+1 && makeRequest; i++ { - _, err = s.client.do(ctx, req, &createdEnvironment) - if err == nil { - log.Printf("[INFO] VM created\n") - makeRequest = false - } else { - errorResponse := err.(*ErrorResponse) - if http.StatusUnprocessableEntity == errorResponse.Response.StatusCode { - log.Printf("[INFO] 422 error received: waiting for %d second(s)\n", s.client.retryAfter) - time.Sleep(time.Duration(s.client.retryAfter) * time.Second) - } else { - return nil, err - } - } - } - // The create method returns an environment. The ID of the VM is not specified. // It is necessary to retrieve the most recently created vm. - createdVM, err := mostRecentVM(&createdEnvironment) + createdVM, err := mostRecentVM(&environment) if err != nil { return nil, err } @@ -327,7 +308,7 @@ func (s *VMsServiceClient) Delete(ctx context.Context, environmentID string, id return err } - _, err = s.client.do(ctx, req, nil) + _, err = s.client.do(ctx, req, nil, envRunStateNotBusyWithVM(environmentID, id), nil) if err != nil { return err } @@ -357,94 +338,89 @@ func (s *VMsServiceClient) updateHardware(ctx context.Context, environmentID str diskIdentification := opts.Hardware.UpdateDisks.DiskIdentification opts.Hardware.UpdateDisks.DiskIdentification = nil - currentVM, err := s.Get(ctx, environmentID, id) + vm, err := s.Get(ctx, environmentID, id) if err != nil { return nil, err } // if started stop - runstate := currentVM.Runstate - if *runstate == VMRunstateRunning { + runstate := *vm.Runstate + if runstate == VMRunstateRunning { _, err = s.changeRunstate(ctx, environmentID, id, &UpdateVMRequest{Runstate: vmRunStateToPtr(VMRunstateStopped)}) if err != nil { return nil, err } - err = s.waitForRunstate(&ctx, environmentID, id, VMRunstateStopped) - if err != nil { - return nil, err - } } - removes := buildRemoveList(currentVM, diskIdentification) - updates := buildUpdateList(currentVM, diskIdentification) - addOSDiskResize(osDiskSize, currentVM, updates) + removes := buildRemoveList(vm, diskIdentification) + updates := buildUpdateList(vm, diskIdentification) + addOSDiskResize(osDiskSize, vm, updates) if len(updates) > 0 { opts.Hardware.UpdateDisks.ExistingDisks = updates } else if len(opts.Hardware.UpdateDisks.NewDisks) == 0 { opts.Hardware.UpdateDisks = nil } - requestCreate, err := s.client.newRequest(ctx, "PUT", path, opts) - if err != nil { - return nil, err - } - - var updatedVM *VM - _, err = s.client.do(ctx, requestCreate, updatedVM) - if err != nil { - return nil, err - } + state := vmRequestRunStateStopped(environmentID, id) + state.diskIdentification = diskIdentification + if opts.Hardware.UpdateDisks != nil || opts.Hardware.RAM != nil || opts.Hardware.CPUs != nil || opts.Name != nil { + requestCreate, err := s.client.newRequest(ctx, "PUT", path, opts) + if err != nil { + return nil, err + } + _, err = s.client.do(ctx, requestCreate, &vm, state, opts) + if err != nil { + return nil, err + } - // wait until not busy - err = s.waitForRunstate(&ctx, environmentID, id, VMRunstateStopped) - if err != nil { - return nil, err - } - updatedVM, err = s.Get(ctx, environmentID, id) - if err != nil { - return nil, err + vm, err = s.Get(ctx, environmentID, id) + if err != nil { + return nil, err + } } + matchUpExistingDisks(vm, diskIdentification, removes) + matchUpNewDisks(vm, diskIdentification, removes) - matchUpExistingDisks(updatedVM, diskIdentification, removes) - matchUpNewDisks(updatedVM, diskIdentification, removes) - - disks := updatedVM.Hardware.Disks + disks := vm.Hardware.Disks if len(removes) > 0 { // delete phase opts.Hardware.CPUs = nil opts.Hardware.RAM = nil + if opts.Hardware.UpdateDisks == nil { + opts.Hardware.UpdateDisks = &UpdateDisks{} + } opts.Hardware.UpdateDisks.NewDisks = nil opts.Hardware.UpdateDisks.ExistingDisks = removes requestDelete, err := s.client.newRequest(ctx, "PUT", path, opts) if err != nil { return nil, err } - _, err = s.client.do(ctx, requestDelete, &updatedVM) + _, err = s.client.do(ctx, requestDelete, &vm, state, opts) if err != nil { return nil, err } - err = s.waitForRunstate(&ctx, environmentID, id, VMRunstateStopped) - if err != nil { - return nil, err - } - updatedVM, err = s.Get(ctx, environmentID, id) + vm, err = s.Get(ctx, environmentID, id) if err != nil { return nil, err } - // update new list of disks - updateFinalDiskList(updatedVM, disks) + updateFinalDiskList(vm, disks) } // if stopped start - if *runstate == VMRunstateRunning { + if runstate == VMRunstateRunning { _, err = s.changeRunstate(ctx, environmentID, id, &UpdateVMRequest{Runstate: vmRunStateToPtr(VMRunstateRunning)}) if err != nil { return nil, err } + vm, err = s.Get(ctx, environmentID, id) + if err != nil { + return nil, err + } + updateFinalDiskList(vm, disks) } - return updatedVM, nil + return vm, nil } func (s *VMsServiceClient) changeRunstate(ctx context.Context, environmentID string, id string, opts *UpdateVMRequest) (*VM, error) { @@ -458,7 +434,7 @@ func (s *VMsServiceClient) changeRunstate(ctx context.Context, environmentID str } var updatedVM VM - _, err = s.client.do(ctx, requestCreate, &updatedVM) + _, err = s.client.do(ctx, requestCreate, &updatedVM, envRunStateNotBusyWithVM(environmentID, id), opts) if err != nil { return nil, err } @@ -500,30 +476,6 @@ func matchUpNewDisks(vm *VM, identifications []DiskIdentification, ignored map[s } } -// wait for runstate -func (s *VMsServiceClient) waitForRunstate(ctx *context.Context, environmentID string, id string, runstate VMRunstate) error { - log.Printf("[INFO] waiting for runstate (%s)\n", string(runstate)) - var makeRequest = true - var err error - for i := 0; i < s.client.retryCount+1 && makeRequest; i++ { - vm, err := s.Get(*ctx, environmentID, id) - if err != nil { - break - } - - makeRequest = *vm.Runstate != runstate - - if makeRequest { - seconds := s.client.retryAfter - log.Printf("[INFO] waiting for %d second(s)\n", seconds) - time.Sleep(time.Duration(seconds) * time.Second) - } else { - log.Printf("[INFO] runstate is now (%s)\n", string(runstate)) - } - } - return err -} - func (s *VMsServiceClient) buildPath(legacy bool, environmentID string, vmID string) string { var path string if legacy { @@ -596,10 +548,237 @@ func buildUpdateList(vm *VM, diskIDs []DiskIdentification) map[string]ExistingDi } func addOSDiskResize(osDiskSize *int, vm *VM, updates map[string]ExistingDisk) { - if osDiskSize != nil { + if osDiskSize != nil && (*vm.Hardware.Disks[0].Size) < *osDiskSize { updates[*vm.Hardware.Disks[0].ID] = ExistingDisk{ ID: vm.Hardware.Disks[0].ID, Size: osDiskSize, } } } + +func (payload *CreateVMRequest) compareResponse(ctx context.Context, c *Client, v interface{}, state *environmentVMRunState) (string, bool) { + env, err := c.Environments.Get(ctx, *state.environmentID) + if err != nil { + return requestNotAsExpected, false + } + logEnvironmentStatus(env) + if *env.Runstate != EnvironmentRunstateBusy { + return "", true + } + return "VM environment not ready", false +} + +func (payload *UpdateVMRequest) compareResponse(ctx context.Context, c *Client, v interface{}, state *environmentVMRunState) (string, bool) { + vm, err := c.VMs.Get(ctx, *state.environmentID, *state.vmID) + if err != nil { + return requestNotAsExpected, false + } + logVMStatus(vm) + if payload.Runstate != nil && payload.Hardware == nil { + if *payload.Runstate == *vm.Runstate { + return "", true + } + return "VM not ready", false + } + actual := payload.buildComparison(vm, state.diskIdentification) + if payload.string() == actual.string() { + return "", true + } + return "VM not ready", false +} + +func (payload *UpdateVMRequest) buildComparison(vm *VM, diskIdentification []DiskIdentification) *UpdateVMRequest { + update := &UpdateVMRequest{} + + if payload.Name != nil { + update.Name = vm.Name + } + if payload.Runstate != nil { + update.Runstate = vm.Runstate + } + if payload.Hardware != nil { + update.Hardware = &UpdateHardware{} + if payload.Hardware.CPUs != nil { + update.Hardware.CPUs = vm.Hardware.CPUs + } + if payload.Hardware.RAM != nil { + update.Hardware.RAM = vm.Hardware.RAM + } + if payload.Hardware.UpdateDisks != nil { + update.Hardware.UpdateDisks = payload.buildDiskStructure(vm, diskIdentification) + } + } + if payload.Hardware.CPUs == nil && payload.Hardware.RAM == nil && payload.Hardware.UpdateDisks == nil { + payload.Hardware = nil + } + return update +} + +func (payload *UpdateVMRequest) buildDiskStructure(vm *VM, diskIdentification []DiskIdentification) *UpdateDisks { + if diskIdentification == nil { + log.Println("[ERROR] SDK cannot compare disks because the disk identification structure is empty.") + return nil + } + if vm.Hardware.Disks == nil { + return nil + } + existing := payload.buildVMExistingDisks(vm.Hardware.Disks) + newDisks := payload.buildVMNewDisks(vm.Hardware.Disks, diskIdentification) + + update := &UpdateDisks{} + if existing != nil { + update.ExistingDisks = existing + } + if newDisks != nil { + update.NewDisks = newDisks + } + if update.ExistingDisks == nil && update.NewDisks == nil { + return nil + } + return update +} + +func (payload *UpdateVMRequest) buildVMExistingDisks(disks []Disk) map[string]ExistingDisk { + var existingDiskPayload map[string]ExistingDisk + if payload.Hardware != nil && + payload.Hardware.UpdateDisks != nil && + payload.Hardware.UpdateDisks.ExistingDisks != nil { + existingDiskPayload = payload.Hardware.UpdateDisks.ExistingDisks + } + existingDisks := make(map[string]ExistingDisk) + disks = disks[0:] + for _, disk := range disks { + if _, ok := existingDiskPayload[*disk.ID]; ok { + existingDisks[*disk.ID] = ExistingDisk{ + ID: disk.ID, + Size: disk.Size, + } + } + } + for id, disk := range existingDiskPayload { + if disk.Size == nil { + existingDisks[id] = disk + } + } + if len(existingDisks) == 0 { + return nil + } + return existingDisks +} + +func (payload *UpdateVMRequest) buildVMNewDisks(disks []Disk, identification []DiskIdentification) []int { + if payload.Hardware == nil || + payload.Hardware.UpdateDisks == nil || + payload.Hardware.UpdateDisks.NewDisks == nil { + return nil + } + newDisks := make([]int, 0) + disks = disks[1:] + for _, disk := range disks { + if identification == nil { + newDisks = append(newDisks, *disk.Size) + } else { + found := false + for _, diskID := range identification { + if diskID.ID != nil && *diskID.ID == *disk.ID { + found = true + break + } + } + if !found { + newDisks = append(newDisks, *disk.Size) + } + } + } + if len(newDisks) == 0 { + return nil + } + sort.Ints(payload.Hardware.UpdateDisks.NewDisks) + sort.Ints(newDisks) + return newDisks +} + +func (payload *UpdateVMRequest) string() string { + name := "" + runState := "" + hardware := "" + + if payload.Name != nil { + name = *payload.Name + } + if payload.Runstate != nil { + runState = string(*payload.Runstate) + } + if payload.Hardware != nil { + hardware = payload.Hardware.string() + } + s := fmt.Sprintf("%s%s%s", + name, + runState, + hardware) + log.Printf("[DEBUG] SDK update vm payload: %s", s) + return s +} + +func (payload *UpdateHardware) string() string { + cpus := "" + ram := "" + updateDisks := "" + + if payload.CPUs != nil { + cpus = fmt.Sprintf("%d", *payload.CPUs) + } + if payload.RAM != nil { + ram = fmt.Sprintf("%d", *payload.RAM) + } + if payload.UpdateDisks != nil { + updateDisks = payload.UpdateDisks.string() + } + return fmt.Sprintf("%s%s%s", + cpus, + ram, + updateDisks) +} + +func (payload *UpdateDisks) string() string { + osSize := "" + disksExisting := "" + disksNew := "" + + if payload.OSSize != nil { + osSize = fmt.Sprintf("%d", *payload.OSSize) + } + if payload.ExistingDisks != nil { + disks := make([]string, 0) + for _, disk := range payload.ExistingDisks { + id := "" + size := "" + if disk.ID != nil { + id = *disk.ID + } + if disk.Size != nil { + size = fmt.Sprintf("%d", *disk.Size) + } + disks = append(disks, fmt.Sprintf("%s:%s", id, size)) + } + sort.Strings(disks) + disksExisting = strings.Join(disks, ",") + } + if payload.NewDisks != nil { + disks := make([]string, 0) + for _, disk := range payload.NewDisks { + disks = append(disks, fmt.Sprintf("%d", disk)) + } + disksNew = strings.Join(disks, ",") + } + return fmt.Sprintf("%s%s%s", + osSize, + disksExisting, + disksNew) +} + +func logVMStatus(vm *VM) { + if vm.RateLimited != nil && *vm.RateLimited { + log.Printf("[INFO] SDK VM rate limiting detected\n") + } +} diff --git a/skytap/vm_test.go b/skytap/vm_test.go index 5beb440..46ee6c3 100644 --- a/skytap/vm_test.go +++ b/skytap/vm_test.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "io/ioutil" + "log" "net/http" "path/filepath" "testing" @@ -25,57 +26,38 @@ func TestCreateVM(t *testing.T) { skytap, hs, handler := createClient(t) defer hs.Close() - *handler = func(rw http.ResponseWriter, req *http.Request) { - assert.Equal(t, "/configurations/123", req.URL.Path, "Bad path") - assert.Equal(t, "PUT", req.Method, "Bad method") - - body, err := ioutil.ReadAll(req.Body) - assert.Nil(t, err, "Bad request body") - assert.JSONEq(t, request, string(body), "Bad request body") - - _, err = io.WriteString(rw, response) - assert.NoError(t, err) - } - opts := &CreateVMRequest{ - TemplateID: "42", - VMID: "43", - } - - createdVM, err := skytap.VMs.Create(context.Background(), "123", opts) - assert.Nil(t, err, "Bad API method") - - var environment Environment - err = json.Unmarshal([]byte(response), &environment) - assert.NoError(t, err) - assert.Equal(t, environment.VMs[1], *createdVM, "Bad VM") -} - -func TestCreateVM422(t *testing.T) { - request := fmt.Sprintf(`{ - "template_id": "%d", - "vm_ids": [ - "%d" - ] - }`, 42, 43) - response := fmt.Sprintf(string(readTestFile(t, "createVMResponse.json")), 123, 123, 456) requestCounter := 0 - skytap, hs, handler := createClient(t) - defer hs.Close() - *handler = func(rw http.ResponseWriter, req *http.Request) { - assert.Equal(t, "/configurations/123", req.URL.Path, "Bad path") - assert.Equal(t, "PUT", req.Method, "Bad method") + log.Printf("Request: (%d)\n", requestCounter) + if requestCounter == 0 { + assert.Equal(t, "/v2/configurations/123", req.URL.Path, "Bad path") + assert.Equal(t, http.MethodGet, req.Method, "Bad method") - body, err := ioutil.ReadAll(req.Body) - assert.Nil(t, err, "Bad request body") - assert.JSONEq(t, request, string(body), "Bad request body") + _, err := io.WriteString(rw, string(readTestFile(t, "exampleEnvironment.json"))) + assert.NoError(t, err) + } else if requestCounter == 1 { + assert.Equal(t, "/configurations/123", req.URL.Path, "Bad path") + assert.Equal(t, http.MethodPut, req.Method, "Bad method") + + body, err := ioutil.ReadAll(req.Body) + assert.Nil(t, err, "Bad request body") + assert.JSONEq(t, request, string(body), "Bad request body") - if requestCounter == 0 { - rw.WriteHeader(http.StatusUnprocessableEntity) - } else { _, err = io.WriteString(rw, response) assert.NoError(t, err) + } else if requestCounter == 2 { + assert.Equal(t, "/v2/configurations/123", req.URL.Path, "Bad path") + assert.Equal(t, http.MethodGet, req.Method, "Bad method") + + _, err := io.WriteString(rw, string(readTestFile(t, "exampleEnvironment.json"))) + assert.NoError(t, err) + } else if requestCounter == 3 { + assert.Equal(t, "/v2/configurations/123", req.URL.Path, "Bad path") + assert.Equal(t, http.MethodPut, req.Method, "Bad method") + + _, err := io.WriteString(rw, string(readTestFile(t, "exampleEnvironment.json"))) + assert.NoError(t, err) } requestCounter++ } @@ -92,7 +74,7 @@ func TestCreateVM422(t *testing.T) { assert.NoError(t, err) assert.Equal(t, environment.VMs[1], *createdVM, "Bad VM") - assert.Equal(t, 2, requestCounter) + assert.Equal(t, 3, requestCounter) } func TestCreateVMError(t *testing.T) { @@ -102,20 +84,28 @@ func TestCreateVMError(t *testing.T) { "%d" ] }`, 42, 43) - requestCounter := 0 - skytap, hs, handler := createClient(t) defer hs.Close() + requestCounter := 0 *handler = func(rw http.ResponseWriter, req *http.Request) { - assert.Equal(t, "/configurations/123", req.URL.Path, "Bad path") - assert.Equal(t, "PUT", req.Method, "Bad method") + log.Printf("Request: (%d)\n", requestCounter) + if requestCounter == 0 { + assert.Equal(t, "/v2/configurations/123", req.URL.Path, "Bad path") + assert.Equal(t, http.MethodGet, req.Method, "Bad method") + + _, err := io.WriteString(rw, string(readTestFile(t, "exampleEnvironment.json"))) + assert.NoError(t, err) + } else if requestCounter == 1 { + assert.Equal(t, "/configurations/123", req.URL.Path, "Bad path") + assert.Equal(t, "PUT", req.Method, "Bad method") - body, err := ioutil.ReadAll(req.Body) - assert.Nil(t, err, "Bad request body") - assert.JSONEq(t, request, string(body), "Bad request body") + body, err := ioutil.ReadAll(req.Body) + assert.Nil(t, err, "Bad request body") + assert.JSONEq(t, request, string(body), "Bad request body") - rw.WriteHeader(401) + rw.WriteHeader(401) + } requestCounter++ } opts := &CreateVMRequest{ @@ -127,9 +117,9 @@ func TestCreateVMError(t *testing.T) { assert.Nil(t, createdVM, "Bad API method") errorResponse := err.(*ErrorResponse) - assert.Nil(t, errorResponse.RetryAfter) - assert.Equal(t, 1, requestCounter) assert.Equal(t, http.StatusUnauthorized, errorResponse.Response.StatusCode) + + assert.Equal(t, 2, requestCounter) } func TestReadVM(t *testing.T) { @@ -138,12 +128,16 @@ func TestReadVM(t *testing.T) { skytap, hs, handler := createClient(t) defer hs.Close() + requestCounter := 0 + *handler = func(rw http.ResponseWriter, req *http.Request) { + log.Printf("Request: (%d)\n", requestCounter) assert.Equal(t, "/v2/configurations/123/vms/456", req.URL.Path, "Bad path") assert.Equal(t, "GET", req.Method, "Bad method") _, err := io.WriteString(rw, response) assert.NoError(t, err) + requestCounter++ } vm, err := skytap.VMs.Get(context.Background(), "123", "456") @@ -152,10 +146,11 @@ func TestReadVM(t *testing.T) { var vmExpected VM err = json.Unmarshal([]byte(response), &vmExpected) assert.Equal(t, vmExpected, *vm, "Bad VM") + + assert.Equal(t, 1, requestCounter) } -// Not testing stopping and starting. -func TestUpdateVM(t *testing.T) { +func TestUpdateVMModifyDisks(t *testing.T) { response := fmt.Sprintf(string(readTestFile(t, "VMResponse.json")), 456) skytap, hs, handler := createClient(t) @@ -164,143 +159,184 @@ func TestUpdateVM(t *testing.T) { var vmCurrent VM err := json.Unmarshal([]byte(response), &vmCurrent) assert.NoError(t, err) - vmCurrent.Hardware.Disks = vmCurrent.Hardware.Disks[0:3] bytesCurrent, err := json.Marshal(&vmCurrent) assert.Nil(t, err, "Bad vm") - var vmDisksAdded VM - err = json.Unmarshal([]byte(response), &vmDisksAdded) + var vmOSSizeDiskAmendedDiskAdded VM + err = json.Unmarshal([]byte(response), &vmOSSizeDiskAmendedDiskAdded) assert.NoError(t, err) - vmDisksAdded.Hardware.Disks = vmDisksAdded.Hardware.Disks[0:3] - vmDisksAdded.Hardware.Disks[0].Size = intToPtr(10000) - vmDisksAdded.Hardware.Disks = append(vmDisksAdded.Hardware.Disks, *createDisk("disk-20142867-38186761-scsi-0-3", nil)) - bytesDisksAdded, err := json.Marshal(&vmDisksAdded) - assert.Nil(t, err, "Bad vm") - - var vmNew VM - err = json.Unmarshal([]byte(response), &vmNew) - vmNew.Hardware.Disks = vmNew.Hardware.Disks[0:2] - vmNew.Hardware.Disks[1].Name = strToPtr("1") - vmNew.Hardware.Disks = append(vmNew.Hardware.Disks, *createDisk("disk-20142867-38186761-scsi-0-3", strToPtr("3"))) - bytesNew, err := json.Marshal(&vmNew) + vmOSSizeDiskAmendedDiskAdded.Hardware.Disks[0].Size = intToPtr(51201) + vmOSSizeDiskAmendedDiskAdded.Hardware.Disks[1].Size = intToPtr(51202) + vmOSSizeDiskAmendedDiskAdded.Hardware.Disks = append(vmOSSizeDiskAmendedDiskAdded.Hardware.Disks, *createDisk("disk-20142867-38186761-scsi-0-4", nil)) + bytesDisksModified, err := json.Marshal(&vmOSSizeDiskAmendedDiskAdded) assert.Nil(t, err, "Bad vm") - first := true - second := true - third := true - fourth := true - fifth := true - sixth := true - seventh := true + requestCounter := 0 *handler = func(rw http.ResponseWriter, req *http.Request) { - if first { + log.Printf("Request: (%d)\n", requestCounter) + if requestCounter == 0 { assert.Equal(t, "/v2/configurations/123/vms/456", req.URL.Path, "Bad path") assert.Equal(t, "GET", req.Method, "Bad method") _, err := io.WriteString(rw, string(bytesCurrent)) assert.NoError(t, err) - first = false - } else if second { + } else if requestCounter == 1 { + assert.Equal(t, "/v2/configurations/123/vms/456", req.URL.Path, "Bad path") + assert.Equal(t, "GET", req.Method, "Bad method") + + _, err := io.WriteString(rw, string(bytesCurrent)) + assert.NoError(t, err) + } else if requestCounter == 2 { // add the hardware changes assert.Equal(t, "/v2/configurations/123/vms/456", req.URL.Path, "Bad path") assert.Equal(t, "PUT", req.Method, "Bad method") body, err := ioutil.ReadAll(req.Body) assert.Nil(t, err, "Bad request body") - assert.JSONEq(t, `{"name": "updated vm", "hardware" : {"cpus": 12, "ram": 8192, "disks": {"new": [51200], "existing": {"disk-20142867-38186761-scsi-0-0" : {"id":"disk-20142867-38186761-scsi-0-0", "size": 10000}}}}}`, string(body), "Bad request body") + assert.JSONEq(t, ` + { + "name": "test vm", "runstate":"stopped", + "hardware" :{ + "disks": { + "new": [51200], + "existing": { + "disk-20142867-38186761-scsi-0-0" : {"id":"disk-20142867-38186761-scsi-0-0", "size": 51201}, + "disk-20142867-38186761-scsi-0-1" : {"id":"disk-20142867-38186761-scsi-0-1", "size": 51202} + } + } + } + }`, string(body), "Bad request body") + + _, err = io.WriteString(rw, string(bytesDisksModified)) + assert.NoError(t, err) + } else if requestCounter == 3 { + assert.Equal(t, "/v2/configurations/123/vms/456", req.URL.Path, "Bad path") + assert.Equal(t, "GET", req.Method, "Bad method") - _, err = io.WriteString(rw, string(bytesCurrent)) + _, err := io.WriteString(rw, string(bytesDisksModified)) assert.NoError(t, err) - second = false - } else if third { - // wait until not busy + } else if requestCounter == 4 { + assert.Equal(t, "/v2/configurations/123/vms/456", req.URL.Path, "Bad path") + assert.Equal(t, "GET", req.Method, "Bad method") + + _, err := io.WriteString(rw, string(bytesDisksModified)) + assert.NoError(t, err) + } + requestCounter++ + } + + opts := createVMUpdateStructure() + opts.Hardware.RAM = nil + opts.Hardware.CPUs = nil + vmUpdate, err := skytap.VMs.Update(context.Background(), "123", "456", opts) + assert.Nil(t, err, "Bad API method") + + assert.Equal(t, 5, requestCounter) + + vmOSSizeDiskAmendedDiskAdded.Hardware.Disks[1].Name = strToPtr("1") + vmOSSizeDiskAmendedDiskAdded.Hardware.Disks[2].Name = strToPtr("2") + vmOSSizeDiskAmendedDiskAdded.Hardware.Disks[3].Name = strToPtr("3") + vmOSSizeDiskAmendedDiskAdded.Hardware.Disks[4].Name = strToPtr("4") + assert.Equal(t, vmOSSizeDiskAmendedDiskAdded, *vmUpdate, "Bad vm") + + disks := vmUpdate.Hardware.Disks + assert.Equal(t, 5, len(disks), "disk length") + + assert.Nil(t, disks[0].Name, "os") +} + +func TestUpdateVMDeleteDisk(t *testing.T) { + response := fmt.Sprintf(string(readTestFile(t, "VMResponse.json")), 456) + + skytap, hs, handler := createClient(t) + defer hs.Close() + + var vmCurrent VM + err := json.Unmarshal([]byte(response), &vmCurrent) + assert.NoError(t, err) + bytesCurrent, err := json.Marshal(&vmCurrent) + assert.Nil(t, err, "Bad vm") + + var vmDisksRemoved VM + err = json.Unmarshal([]byte(response), &vmDisksRemoved) + vmDisksRemoved.Hardware.Disks = vmDisksRemoved.Hardware.Disks[0:2] + bytesDisksModified, err := json.Marshal(&vmDisksRemoved) + assert.Nil(t, err, "Bad vm") + + requestCounter := 0 + *handler = func(rw http.ResponseWriter, req *http.Request) { + log.Printf("Request: (%d)\n", requestCounter) + if requestCounter == 0 { assert.Equal(t, "/v2/configurations/123/vms/456", req.URL.Path, "Bad path") assert.Equal(t, "GET", req.Method, "Bad method") _, err := io.WriteString(rw, string(bytesCurrent)) assert.NoError(t, err) - third = false - } else if fourth { - // the last retry - gives the expected count (6 currently) + } else if requestCounter == 1 { assert.Equal(t, "/v2/configurations/123/vms/456", req.URL.Path, "Bad path") assert.Equal(t, "GET", req.Method, "Bad method") - _, err := io.WriteString(rw, string(bytesDisksAdded)) + _, err := io.WriteString(rw, string(bytesCurrent)) assert.NoError(t, err) - fourth = false - } else if fifth { + } else if requestCounter == 2 { // delete the disks assert.Equal(t, "/v2/configurations/123/vms/456", req.URL.Path, "Bad path") assert.Equal(t, "PUT", req.Method, "Bad method") body, err := ioutil.ReadAll(req.Body) assert.Nil(t, err, "Bad request body") - assert.JSONEq(t, `{"name": "updated vm", "hardware" : {"disks": {"existing": {"disk-20142867-38186761-scsi-0-2" : {"id":"disk-20142867-38186761-scsi-0-2", "size": null}}}}}`, string(body), "Bad request body") - - _, err = io.WriteString(rw, string(bytesDisksAdded)) + assert.JSONEq(t, ` + { + "hardware" : { + "disks": { + "existing": { + "disk-20142867-38186761-scsi-0-2" : {"id":"disk-20142867-38186761-scsi-0-2", "size": null}, + "disk-20142867-38186761-scsi-0-3" : {"id":"disk-20142867-38186761-scsi-0-3", "size": null} + } + } + } + }`, string(body), "Bad request body") + + _, err = io.WriteString(rw, string(bytesDisksModified)) assert.NoError(t, err) - fifth = false - } else if sixth { - // wait until not busy + } else if requestCounter == 3 { assert.Equal(t, "/v2/configurations/123/vms/456", req.URL.Path, "Bad path") assert.Equal(t, "GET", req.Method, "Bad method") - _, err := io.WriteString(rw, string(bytesDisksAdded)) + _, err := io.WriteString(rw, string(bytesDisksModified)) assert.NoError(t, err) - sixth = false - } else if seventh { - // the last retry - gives the expected count (4 currently) + } else if requestCounter == 4 { assert.Equal(t, "/v2/configurations/123/vms/456", req.URL.Path, "Bad path") assert.Equal(t, "GET", req.Method, "Bad method") - _, err := io.WriteString(rw, string(bytesNew)) + _, err := io.WriteString(rw, string(bytesDisksModified)) assert.NoError(t, err) - fourth = false - } else { - seventh = false } + requestCounter++ } - diskIdentification := make([]DiskIdentification, 2) - diskIdentification[0] = DiskIdentification{ID: strToPtr("disk-20142867-38186761-scsi-0-1"), - Size: intToPtr(51200), - Name: strToPtr("1")} - diskIdentification[1] = DiskIdentification{ID: nil, - Size: intToPtr(51200), - Name: strToPtr("3")} - - opts := &UpdateVMRequest{ - Name: strToPtr("updated vm"), - Hardware: &UpdateHardware{ - CPUs: intToPtr(12), - RAM: intToPtr(8192), - UpdateDisks: &UpdateDisks{ - NewDisks: []int{51200}, - DiskIdentification: diskIdentification, - OSSize: intToPtr(10000), - }, - }, - } + opts := createVMUpdateStructure() + opts.Name = nil + opts.Runstate = nil + opts.Hardware.RAM = nil + opts.Hardware.CPUs = nil + opts.Hardware.UpdateDisks.NewDisks = nil + opts.Hardware.UpdateDisks.OSSize = nil + opts.Hardware.UpdateDisks.DiskIdentification = opts.Hardware.UpdateDisks.DiskIdentification[0:1] + opts.Hardware.UpdateDisks.DiskIdentification[0].Size = intToPtr(51200) + delete(opts.Hardware.UpdateDisks.ExistingDisks, "disk-20142867-38186761-scsi-0-1") + delete(opts.Hardware.UpdateDisks.ExistingDisks, "disk-20142867-38186761-scsi-0-2") + delete(opts.Hardware.UpdateDisks.ExistingDisks, "disk-20142867-38186761-scsi-0-3") vmUpdate, err := skytap.VMs.Update(context.Background(), "123", "456", opts) assert.Nil(t, err, "Bad API method") - assert.False(t, first) - assert.False(t, second) - assert.False(t, third) - assert.False(t, fourth) - assert.False(t, fifth) - assert.False(t, sixth) - assert.True(t, seventh) + assert.Equal(t, 5, requestCounter) - assert.Equal(t, vmNew, *vmUpdate, "Bad vm") + vmDisksRemoved.Hardware.Disks[1].Name = strToPtr("1") + assert.Equal(t, vmDisksRemoved, *vmUpdate, "Bad vm") disks := vmUpdate.Hardware.Disks - assert.Equal(t, 3, len(disks), "disk length") - - assert.Nil(t, disks[0].Name, "os") - assert.Equal(t, "1", *disks[1].Name, "disk name") - assert.Equal(t, "3", *disks[2].Name, "disk name") - + assert.Equal(t, 2, len(disks), "disk length") } // Updating cpu and ram is possible on their own @@ -311,53 +347,42 @@ func TestUpdateCPURAMVM(t *testing.T) { skytap, hs, handler := createClient(t) defer hs.Close() - var vmRunning VM - err := json.Unmarshal([]byte(response), &vmRunning) + var vmExisting VM + err := json.Unmarshal([]byte(response), &vmExisting) assert.NoError(t, err) - vmRunning.Runstate = vmRunStateToPtr(VMRunstateRunning) - bytesRunning, err := json.Marshal(&vmRunning) + vmExisting.Runstate = vmRunStateToPtr(VMRunstateRunning) + bytesExisting, err := json.Marshal(&vmExisting) assert.Nil(t, err, "Bad vm") - var vm VM - err = json.Unmarshal([]byte(response), &vm) + var vmUpdated VM + err = json.Unmarshal([]byte(response), &vmUpdated) assert.NoError(t, err) - *vm.Name = "updated vm" - *vm.Hardware.CPUs = 12 - *vm.Hardware.RAM = 8192 - bytes, err := json.Marshal(&vm) + *vmUpdated.Name = "updated vm" + *vmUpdated.Hardware.CPUs = 6 + *vmUpdated.Hardware.RAM = 4096 + vmUpdated.Runstate = vmRunStateToPtr(VMRunstateStopped) + bytes, err := json.Marshal(&vmUpdated) assert.Nil(t, err, "Bad vm") - var vmEnriched VM - err = json.Unmarshal([]byte(response), &vmEnriched) - assert.NoError(t, err) - *vmEnriched.Name = "updated vm" - *vmEnriched.Hardware.CPUs = 12 - *vmEnriched.Hardware.RAM = 8192 - vmEnriched.Hardware.Disks[1].Name = strToPtr("1") - vmEnriched.Hardware.Disks[2].Name = strToPtr("2") - vmEnriched.Hardware.Disks[3].Name = strToPtr("3") - vmEnriched.Runstate = vmRunStateToPtr(VMRunstateRunning) - bytesEnriched, err := json.Marshal(&vmEnriched) - assert.Nil(t, err, "Bad vm") + vmUpdated.Runstate = vmRunStateToPtr(VMRunstateRunning) + bytesRunning, err := json.Marshal(&vmUpdated) - first := true - second := true - third := true - fourth := true - fifth := true - sixth := true - seventh := true - eighth := true + requestCounter := 0 *handler = func(rw http.ResponseWriter, req *http.Request) { - if first { - // get vm + log.Printf("Request: (%d)\n", requestCounter) + if requestCounter == 0 { assert.Equal(t, "/v2/configurations/123/vms/456", req.URL.Path, "Bad path") assert.Equal(t, "GET", req.Method, "Bad method") - _, err := io.WriteString(rw, string(bytesRunning)) + _, err := io.WriteString(rw, string(bytesExisting)) + assert.NoError(t, err) + } else if requestCounter == 1 { + assert.Equal(t, "/v2/configurations/123", req.URL.Path, "Bad path") + assert.Equal(t, "GET", req.Method, "Bad method") + + _, err := io.WriteString(rw, string(readTestFile(t, "exampleEnvironment.json"))) assert.NoError(t, err) - first = false - } else if second { + } else if requestCounter == 2 { // turn to stopped assert.Equal(t, "/v2/configurations/123/vms/456", req.URL.Path, "Bad path") assert.Equal(t, "PUT", req.Method, "Bad method") @@ -368,45 +393,49 @@ func TestUpdateCPURAMVM(t *testing.T) { _, err = io.WriteString(rw, response) assert.NoError(t, err) - second = false - } else if third { - // get vm to confirm it is stopped + } else if requestCounter == 3 { // confirm vm in correct state, i.e. not busy assert.Equal(t, "/v2/configurations/123/vms/456", req.URL.Path, "Bad path") - assert.Equal(t, "GET", req.Method, "Bad method") + assert.Equal(t, http.MethodGet, req.Method, "Bad method") + + _, err := io.WriteString(rw, response) + assert.NoError(t, err) + } else if requestCounter == 4 { // confirm vm in correct state, i.e. not busy + assert.Equal(t, "/v2/configurations/123/vms/456", req.URL.Path, "Bad path") + assert.Equal(t, http.MethodGet, req.Method, "Bad method") _, err := io.WriteString(rw, response) assert.NoError(t, err) - third = false - } else if fourth { + } else if requestCounter == 5 { // update assert.Equal(t, "/v2/configurations/123/vms/456", req.URL.Path, "Bad path") assert.Equal(t, "PUT", req.Method, "Bad method") body, err := ioutil.ReadAll(req.Body) assert.Nil(t, err, "Bad request body") - assert.JSONEq(t, `{"name": "updated vm", "hardware" : {"cpus": 12, "ram": 8192}}`, string(body), "Bad request body") + assert.JSONEq(t, `{"name": "updated vm", "hardware" : {"cpus": 6, "ram": 4096}}`, string(body), "Bad request body") _, err = io.WriteString(rw, string(bytes)) assert.NoError(t, err) - fourth = false - } else if fifth { - // wait until not busy + } else if requestCounter == 6 { assert.Equal(t, "/v2/configurations/123/vms/456", req.URL.Path, "Bad path") assert.Equal(t, "GET", req.Method, "Bad method") _, err := io.WriteString(rw, string(bytes)) assert.NoError(t, err) - fifth = false - } else if sixth { - // get updated vm + } else if requestCounter == 7 { assert.Equal(t, "/v2/configurations/123/vms/456", req.URL.Path, "Bad path") assert.Equal(t, "GET", req.Method, "Bad method") - _, err := io.WriteString(rw, string(bytesEnriched)) + _, err := io.WriteString(rw, string(bytes)) assert.NoError(t, err) - sixth = false - } else if seventh { - // switch back to running + } else if requestCounter == 8 { + assert.Equal(t, "/v2/configurations/123", req.URL.Path, "Bad path") + assert.Equal(t, "GET", req.Method, "Bad method") + + _, err := io.WriteString(rw, string(readTestFile(t, "exampleEnvironment.json"))) + assert.NoError(t, err) + } else if requestCounter == 9 { + // turn to running assert.Equal(t, "/v2/configurations/123/vms/456", req.URL.Path, "Bad path") assert.Equal(t, "PUT", req.Method, "Bad method") @@ -414,45 +443,43 @@ func TestUpdateCPURAMVM(t *testing.T) { assert.Nil(t, err, "Bad request body") assert.JSONEq(t, `{"runstate":"running"}`, string(body), "Bad request body") + _, err = io.WriteString(rw, string(bytes)) + assert.NoError(t, err) + } else if requestCounter == 10 { + assert.Equal(t, "/v2/configurations/123/vms/456", req.URL.Path, "Bad path") + assert.Equal(t, "GET", req.Method, "Bad method") + _, err = io.WriteString(rw, string(bytesRunning)) assert.NoError(t, err) - seventh = false - } else { - // dont bother waiting for vm to be running - eighth = false - } - } + } else if requestCounter == 11 { + assert.Equal(t, "/v2/configurations/123/vms/456", req.URL.Path, "Bad path") + assert.Equal(t, "GET", req.Method, "Bad method") - diskIDs := []DiskIdentification{ - {strToPtr("disk-20142867-38186761-scsi-0-1"), intToPtr(51200), strToPtr("1")}, - {strToPtr("disk-20142867-38186761-scsi-0-2"), intToPtr(51200), strToPtr("2")}, - {strToPtr("disk-20142867-38186761-scsi-0-3"), intToPtr(51200), strToPtr("3")}, + _, err = io.WriteString(rw, string(bytesRunning)) + assert.NoError(t, err) + } + requestCounter++ } - opts := &UpdateVMRequest{ - Name: strToPtr(*vm.Name), - Hardware: &UpdateHardware{ - CPUs: intToPtr(*vm.Hardware.CPUs), - RAM: intToPtr(*vm.Hardware.RAM), - UpdateDisks: &UpdateDisks{ - DiskIdentification: diskIDs, - }, - }, - } - vmUpdate, err := skytap.VMs.Update(context.Background(), "123", "456", opts) + opts := createVMUpdateStructure() + opts.Name = strToPtr("updated vm") + opts.Runstate = nil + opts.Hardware.RAM = intToPtr(4096) + opts.Hardware.CPUs = intToPtr(6) + opts.Hardware.UpdateDisks.NewDisks = nil + opts.Hardware.UpdateDisks.ExistingDisks = nil + opts.Hardware.UpdateDisks.OSSize = nil + opts.Hardware.UpdateDisks.DiskIdentification[0].Size = intToPtr(51200) + vm, err := skytap.VMs.Update(context.Background(), "123", "456", opts) assert.Nil(t, err, "Bad API method") - assert.False(t, first) - assert.False(t, second) - assert.False(t, third) - assert.False(t, fourth) - assert.False(t, fifth) - assert.False(t, sixth) - assert.False(t, seventh) - assert.True(t, eighth) + assert.Equal(t, 12, requestCounter) - assert.Equal(t, vmEnriched, *vmUpdate, "Bad vm") - assert.Equal(t, VMRunstateRunning, *vmUpdate.Runstate, "running") + vmUpdated.Hardware.Disks[1].Name = strToPtr("1") + vmUpdated.Hardware.Disks[2].Name = strToPtr("2") + vmUpdated.Hardware.Disks[3].Name = strToPtr("3") + assert.Equal(t, vmUpdated, *vm, "Bad vm") + assert.Equal(t, VMRunstateRunning, *vm.Runstate, "running") } // Updating runstate can only be done on its own @@ -470,16 +497,34 @@ func TestUpdateRunstateVM(t *testing.T) { bytes, err := json.Marshal(&vm) assert.Nil(t, err, "Bad vm") + requestCounter := 0 + *handler = func(rw http.ResponseWriter, req *http.Request) { - assert.Equal(t, "/v2/configurations/123/vms/456", req.URL.Path, "Bad path") - assert.Equal(t, "PUT", req.Method, "Bad method") + log.Printf("Request: (%d)\n", requestCounter) + if requestCounter == 0 { + assert.Equal(t, "/v2/configurations/123", req.URL.Path, "Bad path") + assert.Equal(t, "GET", req.Method, "Bad method") - body, err := ioutil.ReadAll(req.Body) - assert.Nil(t, err, "Bad request body") - assert.JSONEq(t, `{"runstate":"running"}`, string(body), "Bad request body") + _, err := io.WriteString(rw, string(readTestFile(t, "exampleEnvironment.json"))) + assert.NoError(t, err) + } else if requestCounter == 1 { + assert.Equal(t, "/v2/configurations/123/vms/456", req.URL.Path, "Bad path") + assert.Equal(t, "PUT", req.Method, "Bad method") - _, err = io.WriteString(rw, string(bytes)) - assert.NoError(t, err) + body, err := ioutil.ReadAll(req.Body) + assert.Nil(t, err, "Bad request body") + assert.JSONEq(t, `{"runstate":"running"}`, string(body), "Bad request body") + + _, err = io.WriteString(rw, string(bytes)) + assert.NoError(t, err) + } else if requestCounter == 2 { + assert.Equal(t, "/v2/configurations/123/vms/456", req.URL.Path, "Bad path") + assert.Equal(t, "GET", req.Method, "Bad method") + + _, err = io.WriteString(rw, string(bytes)) + assert.NoError(t, err) + } + requestCounter++ } opts := &UpdateVMRequest{ @@ -490,19 +535,35 @@ func TestUpdateRunstateVM(t *testing.T) { assert.Equal(t, vm, *vmUpdate, "Bad vm") assert.Equal(t, VMRunstateRunning, *vmUpdate.Runstate, "running") + + assert.Equal(t, 3, requestCounter) } func TestDeleteVM(t *testing.T) { skytap, hs, handler := createClient(t) defer hs.Close() + requestCounter := 0 + *handler = func(rw http.ResponseWriter, req *http.Request) { - assert.Equal(t, "/v2/configurations/123/vms/456", req.URL.Path, "Bad path") - assert.Equal(t, "DELETE", req.Method, "Bad method") + log.Printf("Request: (%d)\n", requestCounter) + if requestCounter == 0 { + assert.Equal(t, "/v2/configurations/123", req.URL.Path, "Bad path") + assert.Equal(t, http.MethodGet, req.Method, "Bad method") + + _, err := io.WriteString(rw, string(readTestFile(t, "exampleEnvironment.json"))) + assert.NoError(t, err) + } else if requestCounter == 1 { + assert.Equal(t, "/v2/configurations/123/vms/456", req.URL.Path, "Bad path") + assert.Equal(t, "DELETE", req.Method, "Bad method") + } + requestCounter++ } err := skytap.VMs.Delete(context.Background(), "123", "456") assert.Nil(t, err, "Bad API method") + + assert.Equal(t, 2, requestCounter) } func TestListVMs(t *testing.T) { @@ -511,12 +572,16 @@ func TestListVMs(t *testing.T) { skytap, hs, handler := createClient(t) defer hs.Close() + requestCounter := 0 + *handler = func(rw http.ResponseWriter, req *http.Request) { + log.Printf("Request: (%d)\n", requestCounter) assert.Equal(t, "/v2/configurations/123/vms", req.URL.Path, "Bad path") assert.Equal(t, "GET", req.Method, "Bad method") _, err := io.WriteString(rw, fmt.Sprintf(`[%+v]`, response)) assert.NoError(t, err) + requestCounter++ } result, err := skytap.VMs.List(context.Background(), "123") @@ -530,6 +595,8 @@ func TestListVMs(t *testing.T) { } } assert.True(t, found, "VM not found") + + assert.Equal(t, 1, requestCounter) } func readTestFile(t *testing.T, name string) []byte { @@ -705,8 +772,263 @@ func TestOSDiskResize(t *testing.T) { addOSDiskResize(nil, &vm, updates) assert.Equal(t, 1, len(updates)) - addOSDiskResize(intToPtr(10000), &vm, updates) + addOSDiskResize(intToPtr(51203), &vm, updates) assert.Equal(t, 2, len(updates)) assert.Equal(t, ExistingDisk{ID: strToPtr("disk-20142867-38186761-scsi-0-0"), - Size: intToPtr(10000)}, updates["disk-20142867-38186761-scsi-0-0"]) + Size: intToPtr(51203)}, updates["disk-20142867-38186761-scsi-0-0"]) +} + +func TestCompareVMCreateTrue(t *testing.T) { + exampleVM := fmt.Sprintf(string(readTestFile(t, "createVMResponse.json")), 123, 123, 456) + + var env Environment + err := json.Unmarshal([]byte(exampleVM), &env) + env.Runstate = environmentRunStateToPtr(EnvironmentRunstateStopped) + assert.NoError(t, err) + opts := CreateVMRequest{ + TemplateID: "42", + VMID: "43", + } + skytap, hs, handler := createClient(t) + defer hs.Close() + + *handler = func(rw http.ResponseWriter, req *http.Request) { + bytes, err := json.Marshal(&env) + assert.Nil(t, err, "Bad vm") + _, err = io.WriteString(rw, string(bytes)) + assert.NoError(t, err) + } + state := vmRunStateNotBusy("123", "456") + state.diskIdentification = buildDiskidentification() + message, ok := opts.compareResponse(context.Background(), skytap, &env, state) + assert.True(t, ok) + assert.Equal(t, "", message) +} + +func TestCompareVMCreateFalse(t *testing.T) { + exampleVM := fmt.Sprintf(string(readTestFile(t, "createVMResponse.json")), 123, 123, 456) + + var env Environment + err := json.Unmarshal([]byte(exampleVM), &env) + assert.NoError(t, err) + opts := CreateVMRequest{ + TemplateID: "42", + VMID: "43", + } + skytap, hs, handler := createClient(t) + defer hs.Close() + + *handler = func(rw http.ResponseWriter, req *http.Request) { + _, err := io.WriteString(rw, exampleVM) + assert.NoError(t, err) + } + state := vmRunStateNotBusy("123", "456") + state.diskIdentification = buildDiskidentification() + message, ok := opts.compareResponse(context.Background(), skytap, &env, state) + assert.False(t, ok) + assert.Equal(t, "VM environment not ready", message) +} + +func TestCompareVMUpdateRunStateTrue(t *testing.T) { + exampleVM := fmt.Sprintf(string(readTestFile(t, "VMResponse.json")), 456) + + var vm VM + err := json.Unmarshal([]byte(exampleVM), &vm) + assert.NoError(t, err) + opts := &UpdateVMRequest{ + Runstate: vmRunStateToPtr(VMRunstateStopped), + } + skytap, hs, handler := createClient(t) + defer hs.Close() + + *handler = func(rw http.ResponseWriter, req *http.Request) { + _, err := io.WriteString(rw, exampleVM) + assert.NoError(t, err) + } + state := vmRunStateNotBusy("123", "456") + state.diskIdentification = buildDiskidentification() + message, ok := opts.compareResponse(context.Background(), skytap, &vm, state) + assert.True(t, ok) + assert.Equal(t, "", message) +} + +func TestCompareVMUpdateRunStateFalse(t *testing.T) { + exampleVM := fmt.Sprintf(string(readTestFile(t, "VMResponse.json")), 456) + + var vm VM + err := json.Unmarshal([]byte(exampleVM), &vm) + vm.Runstate = vmRunStateToPtr(VMRunstateBusy) + assert.NoError(t, err) + opts := &UpdateVMRequest{ + Runstate: vmRunStateToPtr(VMRunstateStopped), + } + skytap, hs, handler := createClient(t) + defer hs.Close() + + *handler = func(rw http.ResponseWriter, req *http.Request) { + bytes, err := json.Marshal(&vm) + assert.Nil(t, err, "Bad vm") + _, err = io.WriteString(rw, string(bytes)) + assert.NoError(t, err) + } + state := vmRunStateNotBusy("123", "456") + state.diskIdentification = buildDiskidentification() + message, ok := opts.compareResponse(context.Background(), skytap, &vm, state) + assert.False(t, ok) + assert.Equal(t, "VM not ready", message) +} + +func TestCompareVMUpdateTrue(t *testing.T) { + exampleVM := fmt.Sprintf(string(readTestFile(t, "VMResponse.json")), 456) + + var vm VM + err := json.Unmarshal([]byte(exampleVM), &vm) + assert.NoError(t, err) + vm.Hardware.Disks = append(vm.Hardware.Disks, Disk{ + ID: strToPtr("disk-20142867-38186761-scsi-0-4"), + Size: intToPtr(51200), + }) + vm.Hardware.Disks[0].Size = intToPtr(51200) + + existingDisks := make(map[string]ExistingDisk) + existingDisks["disk-20142867-38186761-scsi-0-1"] = ExistingDisk{ + ID: strToPtr("disk-20142867-38186761-scsi-0-1"), + Size: intToPtr(51200), + } + existingDisks["disk-20142867-38186761-scsi-0-2"] = ExistingDisk{ + ID: strToPtr("disk-20142867-38186761-scsi-0-2"), + Size: intToPtr(51200), + } + existingDisks["disk-20142867-38186761-scsi-0-3"] = ExistingDisk{ + ID: strToPtr("disk-20142867-38186761-scsi-0-3"), + Size: intToPtr(51200), + } + opts := &UpdateVMRequest{ + Name: strToPtr("test vm"), + Runstate: vmRunStateToPtr(VMRunstateStopped), + Hardware: &UpdateHardware{ + CPUs: intToPtr(12), + RAM: intToPtr(8192), + UpdateDisks: &UpdateDisks{ + NewDisks: []int{51200}, + ExistingDisks: existingDisks, + }, + }, + } + + skytap, hs, handler := createClient(t) + defer hs.Close() + + *handler = func(rw http.ResponseWriter, req *http.Request) { + bytes, err := json.Marshal(&vm) + assert.Nil(t, err, "Bad vm") + _, err = io.WriteString(rw, string(bytes)) + assert.NoError(t, err) + } + state := vmRunStateNotBusy("123", "456") + state.diskIdentification = buildDiskidentification() + message, ok := opts.compareResponse(context.Background(), skytap, &vm, state) + assert.True(t, ok) + assert.Equal(t, "", message) +} + +func TestCompareVMUpdateFalse(t *testing.T) { + exampleVM := fmt.Sprintf(string(readTestFile(t, "VMResponse.json")), 456) + + var vm VM + vm.Runstate = vmRunStateToPtr(VMRunstateBusy) + err := json.Unmarshal([]byte(exampleVM), &vm) + assert.NoError(t, err) + opts := createVMUpdateStructure() + skytap, hs, handler := createClient(t) + defer hs.Close() + + *handler = func(rw http.ResponseWriter, req *http.Request) { + bytes, err := json.Marshal(&vm) + assert.Nil(t, err, "Bad vm") + _, err = io.WriteString(rw, string(bytes)) + assert.NoError(t, err) + } + state := vmRunStateNotBusy("123", "456") + state.diskIdentification = buildDiskidentification() + message, ok := opts.compareResponse(context.Background(), skytap, &vm, state) + assert.False(t, ok) + assert.Equal(t, "VM not ready", message) +} + +func TestCompareDiskStructureNoDisks(t *testing.T) { + exampleEnvironment := fmt.Sprintf(string(readTestFile(t, "createVMResponse.json")), 123, 123, 456) + exampleVM := fmt.Sprintf(string(readTestFile(t, "VMResponse.json")), 456) + + var env Environment + err := json.Unmarshal([]byte(exampleEnvironment), &env) + assert.NoError(t, err) + vm := env.VMs[1] + vm.Hardware.Disks = nil + opts := createVMUpdateStructure() + skytap, hs, handler := createClient(t) + defer hs.Close() + + *handler = func(rw http.ResponseWriter, req *http.Request) { + _, err := io.WriteString(rw, exampleVM) + assert.NoError(t, err) + } + state := vmRunStateNotBusy("123", "456") + state.diskIdentification = buildDiskidentification() + message, ok := opts.compareResponse(context.Background(), skytap, &vm, state) + assert.False(t, ok) + assert.Equal(t, "VM not ready", message) +} + +func createVMUpdateStructure() *UpdateVMRequest { + diskIdentification := buildDiskidentification() + diskIdentification[0] = DiskIdentification{ID: strToPtr("disk-20142867-38186761-scsi-0-1"), + Size: intToPtr(51202), + Name: strToPtr("1")} + + existingDisks := make(map[string]ExistingDisk) + existingDisks["disk-20142867-38186761-scsi-0-1"] = ExistingDisk{ + ID: strToPtr("disk-20142867-38186761-scsi-0-1"), + Size: intToPtr(51200), + } + existingDisks["disk-20142867-38186761-scsi-0-2"] = ExistingDisk{ + ID: strToPtr("disk-20142867-38186761-scsi-0-2"), + Size: intToPtr(51200), + } + existingDisks["disk-20142867-38186761-scsi-0-3"] = ExistingDisk{ + ID: strToPtr("disk-20142867-38186761-scsi-0-3"), + Size: intToPtr(51200), + } + opts := &UpdateVMRequest{ + Name: strToPtr("test vm"), + Runstate: vmRunStateToPtr(VMRunstateStopped), + Hardware: &UpdateHardware{ + CPUs: intToPtr(12), + RAM: intToPtr(8192), + UpdateDisks: &UpdateDisks{ + NewDisks: []int{51200}, + ExistingDisks: existingDisks, + DiskIdentification: diskIdentification, + OSSize: intToPtr(51201), + }, + }, + } + return opts +} + +func buildDiskidentification() []DiskIdentification { + diskIdentification := make([]DiskIdentification, 4) + diskIdentification[0] = DiskIdentification{ID: strToPtr("disk-20142867-38186761-scsi-0-1"), + Size: intToPtr(51200), + Name: strToPtr("1")} + diskIdentification[1] = DiskIdentification{ID: strToPtr("disk-20142867-38186761-scsi-0-2"), + Size: intToPtr(51200), + Name: strToPtr("2")} + diskIdentification[2] = DiskIdentification{ID: strToPtr("disk-20142867-38186761-scsi-0-3"), + Size: intToPtr(51200), + Name: strToPtr("3")} + diskIdentification[3] = DiskIdentification{ID: nil, + Size: intToPtr(51200), + Name: strToPtr("4")} + return diskIdentification }