From 69317f6b584900907fef39b7a2f16e0afe87a09c Mon Sep 17 00:00:00 2001 From: Adam Barreiro Date: Fri, 14 Jul 2023 11:56:32 +0200 Subject: [PATCH] Add Behavior support to Defined Interfaces and Defined Entity Types (#584) Signed-off-by: abarreiro --- .changes/v2.21.0/584-features.md | 9 ++ govcd/defined_entity.go | 206 +++++++++++++++++++++++++++++++ govcd/defined_entity_test.go | 203 ++++++++++++++++++++++++++++++ govcd/defined_interface.go | 147 ++++++++++++++++++++++ govcd/defined_interface_test.go | 98 +++++++++++++++ govcd/openapi_endpoints.go | 4 + types/v56/constants.go | 4 + types/v56/openapi.go | 21 ++++ 8 files changed, 692 insertions(+) create mode 100644 .changes/v2.21.0/584-features.md diff --git a/.changes/v2.21.0/584-features.md b/.changes/v2.21.0/584-features.md new file mode 100644 index 000000000..45f035ff7 --- /dev/null +++ b/.changes/v2.21.0/584-features.md @@ -0,0 +1,9 @@ +* Added RDE Defined Interface Behaviors support with methods `DefinedInterface.AddBehavior`, `DefinedInterface.GetAllBehaviors`, + `DefinedInterface.GetBehaviorById` `DefinedInterface.GetBehaviorByName`, `DefinedInterface.UpdateBehavior` and + `DefinedInterface.DeleteBehavior` [GH-584] +* Added RDE Defined Entity Type Behaviors support with methods `DefinedEntityType.GetAllBehaviors`, + `DefinedEntityType.GetBehaviorById` `DefinedEntityType.GetBehaviorByName`, `DefinedEntityType.UpdateBehaviorOverride` and + `DefinedEntityType.DeleteBehaviorOverride` [GH-584] +* Added RDE Defined Entity Type Behavior Access Controls support with methods `DefinedEntityType.GetAllBehaviorsAccessControls` and + `DefinedEntityType.SetBehaviorAccessControls` [GH-584] +* Added method to invoke Behaviors on Defined Entities `DefinedEntity.InvokeBehavior` and `DefinedEntity.InvokeBehaviorAndMarshal` [GH-584] diff --git a/govcd/defined_entity.go b/govcd/defined_entity.go index 3da632d05..5022cc1a0 100644 --- a/govcd/defined_entity.go +++ b/govcd/defined_entity.go @@ -5,6 +5,7 @@ package govcd import ( + "encoding/json" "fmt" "github.com/vmware/go-vcloud-director/v2/types/v56" "net/url" @@ -204,6 +205,163 @@ func (rdeType *DefinedEntityType) Delete() error { return nil } +// GetAllBehaviors retrieves all the Behaviors of the receiver RDE Type. +func (rdeType *DefinedEntityType) GetAllBehaviors(queryParameters url.Values) ([]*types.Behavior, error) { + if rdeType.DefinedEntityType.ID == "" { + return nil, fmt.Errorf("ID of the receiver Defined Entity Type is empty") + } + return getAllBehaviors(rdeType.client, rdeType.DefinedEntityType.ID, types.OpenApiEndpointRdeTypeBehaviors, queryParameters) +} + +// GetBehaviorById retrieves a unique Behavior that belongs to the receiver RDE Type and is determined by the +// input ID. The ID can be a RDE Interface Behavior ID or a RDE Type overridden Behavior ID. +func (rdeType *DefinedEntityType) GetBehaviorById(id string) (*types.Behavior, error) { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeTypeBehaviors + apiVersion, err := rdeType.client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := rdeType.client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, rdeType.DefinedEntityType.ID), id) + if err != nil { + return nil, err + } + + response := types.Behavior{} + err = rdeType.client.OpenApiGetItem(apiVersion, urlRef, nil, &response, nil) + if err != nil { + return nil, err + } + + return &response, nil +} + +// GetBehaviorByName retrieves a unique Behavior that belongs to the receiver RDE Type and is named after +// the input. +func (rdeType *DefinedEntityType) GetBehaviorByName(name string) (*types.Behavior, error) { + behaviors, err := rdeType.GetAllBehaviors(nil) + if err != nil { + return nil, fmt.Errorf("could not get the Behaviors of the Defined Entity Type with ID '%s': %s", rdeType.DefinedEntityType.ID, err) + } + for _, b := range behaviors { + if b.Name == name { + return b, nil + } + } + return nil, fmt.Errorf("could not find any Behavior with name '%s' in Defined Entity Type with ID '%s': %s", name, rdeType.DefinedEntityType.ID, ErrorEntityNotFound) +} + +// UpdateBehaviorOverride overrides an Interface Behavior. Only Behavior description and execution can be overridden. +// It returns the new Behavior, result of the override (with a new ID). +func (rdeType *DefinedEntityType) UpdateBehaviorOverride(behavior types.Behavior) (*types.Behavior, error) { + if rdeType.DefinedEntityType.ID == "" { + return nil, fmt.Errorf("ID of the receiver Defined Entity Type is empty") + } + if behavior.ID == "" { + return nil, fmt.Errorf("ID of the Behavior to override is empty") + } + + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeTypeBehaviors + apiVersion, err := rdeType.client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := rdeType.client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, rdeType.DefinedEntityType.ID), behavior.ID) + if err != nil { + return nil, err + } + response := types.Behavior{} + err = rdeType.client.OpenApiPutItem(apiVersion, urlRef, nil, behavior, &response, nil) + if err != nil { + return nil, err + } + + return &response, nil +} + +// DeleteBehaviorOverride removes a Behavior specified by its ID from the receiver Defined Entity Type. +// The ID can be the Interface Behavior ID or the Type Behavior ID (the overridden one). +func (rdeType *DefinedEntityType) DeleteBehaviorOverride(behaviorId string) error { + if rdeType.DefinedEntityType.ID == "" { + return fmt.Errorf("ID of the receiver Defined Entity Type is empty") + } + + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeTypeBehaviors + apiVersion, err := rdeType.client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return err + } + + urlRef, err := rdeType.client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, rdeType.DefinedEntityType.ID), behaviorId) + if err != nil { + return err + } + err = rdeType.client.OpenApiDeleteItem(apiVersion, urlRef, nil, nil) + if err != nil { + return err + } + + return nil +} + +// SetBehaviorAccessControls sets the given slice of BehaviorAccess to the receiver Defined Entity Type. +func (det *DefinedEntityType) SetBehaviorAccessControls(acls []*types.BehaviorAccess) error { + if det.DefinedEntityType.ID == "" { + return fmt.Errorf("ID of the receiver Defined Entity Type is empty") + } + + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeTypeBehaviorAccessControls + apiVersion, err := det.client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return err + } + + urlRef, err := det.client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, det.DefinedEntityType.ID)) + if err != nil { + return err + } + + // Wrap it in OpenAPI pages, this endpoint requires it + rawMessage, err := json.Marshal(acls) + if err != nil { + return fmt.Errorf("error setting Access controls in payload: %s", err) + } + payload := types.OpenApiPages{ + Values: rawMessage, + } + + err = det.client.OpenApiPutItem(apiVersion, urlRef, nil, payload, nil, nil) + if err != nil { + return err + } + + return nil +} + +// GetAllBehaviorsAccessControls gets all the Behaviors Access Controls from the receiver DefinedEntityType. +// Query parameters can be supplied to modify pagination. +func (det *DefinedEntityType) GetAllBehaviorsAccessControls(queryParameters url.Values) ([]*types.BehaviorAccess, error) { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeTypeBehaviorAccessControls + apiVersion, err := det.client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := det.client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, det.DefinedEntityType.ID)) + if err != nil { + return nil, err + } + + typeResponses := []*types.BehaviorAccess{{}} + err = det.client.OpenApiGetAllItems(apiVersion, urlRef, queryParameters, &typeResponses, nil) + if err != nil { + return nil, err + } + + return typeResponses, nil +} + // GetAllRdes gets all the RDE instances of the given vendor, nss and version. func (vcdClient *VCDClient) GetAllRdes(vendor, nss, version string, queryParameters url.Values) ([]*DefinedEntity, error) { return getAllRdes(&vcdClient.Client, vendor, nss, version, queryParameters) @@ -498,3 +656,51 @@ func (rde *DefinedEntity) Delete() error { rde.Etag = "" return nil } + +// InvokeBehavior calls a Behavior identified by the given ID with the given execution parameters. +// Returns the invocation result as a raw string. +func (rde *DefinedEntity) InvokeBehavior(behaviorId string, invocation types.BehaviorInvocation) (string, error) { + client := rde.client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeEntitiesBehaviorsInvocations + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return "", err + } + + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, rde.DefinedEntity.ID, behaviorId)) + if err != nil { + return "", err + } + + task, err := client.OpenApiPostItemAsync(apiVersion, urlRef, nil, invocation) + if err != nil { + return "", err + } + + err = task.WaitTaskCompletion() + if err != nil { + return "", err + } + + if task.Task.Result == nil { + return "", fmt.Errorf("the Task '%s' returned an empty Result content", task.Task.ID) + } + + return task.Task.Result.ResultContent.Text, nil +} + +// InvokeBehaviorAndMarshal calls a Behavior identified by the given ID with the given execution parameters. +// Returns the invocation result marshaled with the input object. +func (rde *DefinedEntity) InvokeBehaviorAndMarshal(behaviorId string, invocation types.BehaviorInvocation, output interface{}) error { + result, err := rde.InvokeBehavior(behaviorId, invocation) + if err != nil { + return err + } + + err = json.Unmarshal([]byte(result), &output) + if err != nil { + return fmt.Errorf("error marshaling the invocation result '%s': %s", result, err) + } + + return nil +} diff --git a/govcd/defined_entity_test.go b/govcd/defined_entity_test.go index c20e2d8b0..cad3930c1 100644 --- a/govcd/defined_entity_test.go +++ b/govcd/defined_entity_test.go @@ -357,3 +357,206 @@ func loadRdeTypeSchemaFromTestResources() (map[string]interface{}, error) { return unmarshaledJson, nil } + +// Test_RdeTypeBehavior tests the CRUD methods of RDE Types to create Behaviors, as a System administrator and tenant user. +// This test can be run with GOVCD_SKIP_VAPP_CREATION option enabled. +func (vcd *TestVCD) Test_RdeTypeBehavior(check *C) { + if vcd.skipAdminTests { + check.Skip(fmt.Sprintf(TestRequiresSysAdminPrivileges, check.TestName())) + } + skipOpenApiEndpointTest(vcd, check, types.OpenApiPathVersion1_0_0+types.OpenApiEndpointRdeTypeBehaviors) + + // Create a new RDE Type from scratch + unmarshaledRdeTypeSchema, err := loadRdeTypeSchemaFromTestResources() + check.Assert(err, IsNil) + check.Assert(true, Equals, len(unmarshaledRdeTypeSchema) > 0) + sanizitedTestName := strings.NewReplacer("_", "", ".", "").Replace(check.TestName()) + rdeType, err := vcd.client.CreateRdeType(&types.DefinedEntityType{ + Name: sanizitedTestName, + Description: "Created by " + check.TestName(), + Nss: "nss", + Version: "1.0.0", + Vendor: "vmware", + Schema: unmarshaledRdeTypeSchema, + Interfaces: []string{"urn:vcloud:interface:vmware:k8s:1.0.0"}, + }) + check.Assert(err, IsNil) + AddToCleanupListOpenApi(rdeType.DefinedEntityType.ID, check.TestName(), types.OpenApiPathVersion1_0_0+types.OpenApiEndpointRdeEntityTypes+rdeType.DefinedEntityType.ID) + defer func() { + err := rdeType.Delete() + check.Assert(err, IsNil) + }() + + // Get all the behaviors of the RDE Type. As it referenced the K8s Interface, it inherits its Behaviors, so it + // has one Behavior without anyone creating it. + originalBehaviors, err := rdeType.GetAllBehaviors(nil) + check.Assert(err, IsNil) + check.Assert(len(originalBehaviors), Equals, 1) + check.Assert(originalBehaviors[0].Name, Equals, "createKubeConfig") + check.Assert(originalBehaviors[0].Description, Equals, "Creates and returns a kubeconfig") + check.Assert(len(originalBehaviors[0].Execution), Equals, 2) + check.Assert(originalBehaviors[0].Execution["id"], Equals, "CreateKubeConfigActivity") + check.Assert(originalBehaviors[0].Execution["type"], Equals, "Activity") + + // Error getting non-existing Behaviors + _, err = rdeType.GetBehaviorById("urn:vcloud:behavior-type:notexist:notexist:notexist:9.9.9") + check.Assert(err, NotNil) + check.Assert(strings.Contains(err.Error(), "RDE_INVALID_BEHAVIOR_SCOPE"), Equals, true) + + _, err = rdeType.GetBehaviorByName("DoesNotExist") + check.Assert(err, NotNil) + check.Assert(strings.Contains(err.Error(), ErrorEntityNotFound.Error()), Equals, true) + + // Getting behaviors correctly + originalBehavior, err := rdeType.GetBehaviorById(originalBehaviors[0].ID) + check.Assert(err, IsNil) + check.Assert(originalBehavior, NotNil) + check.Assert(originalBehavior.Name, Equals, originalBehaviors[0].Name) + check.Assert(originalBehavior.Description, Equals, originalBehaviors[0].Description) + check.Assert(originalBehavior.Execution, DeepEquals, originalBehaviors[0].Execution) + + behavior2, err := rdeType.GetBehaviorByName(originalBehaviors[0].Name) + check.Assert(err, IsNil) + check.Assert(originalBehavior, NotNil) + check.Assert(originalBehavior, DeepEquals, behavior2) + + // Override the behavior + rdeTypeBehavior, err := rdeType.UpdateBehaviorOverride(types.Behavior{ + ID: originalBehavior.ID, + Description: originalBehavior.Description + "Overridden", + Execution: map[string]interface{}{ + "id": originalBehavior.Execution["id"].(string) + "Overridden", + "type": "noop", + }, + Name: "WillNotBeOverridden", + }) + check.Assert(err, IsNil) + check.Assert(rdeTypeBehavior.ID, Not(Equals), originalBehavior.ID) // Now that it is overridden, ID changes to behavior-type + check.Assert(rdeTypeBehavior.Ref, Equals, originalBehavior.ID) + check.Assert(rdeTypeBehavior.Description, Equals, originalBehavior.Description+"Overridden") + check.Assert(rdeTypeBehavior.Execution["id"], Equals, originalBehavior.Execution["id"].(string)+"Overridden") + check.Assert(rdeTypeBehavior.Execution["type"], Equals, "noop") + check.Assert(rdeTypeBehavior.Name, Equals, originalBehavior.Name) // Name can't be overridden + + // Check that it can be retrieved with new Behavior ID (generated by the override) and old one + retrOverridden, err := rdeType.GetBehaviorById(rdeTypeBehavior.ID) + check.Assert(err, IsNil) + check.Assert(retrOverridden, DeepEquals, rdeTypeBehavior) + + retrOverridden, err = rdeType.GetBehaviorById(rdeTypeBehavior.Ref) // Ref is the Interface Behavior ID + check.Assert(err, IsNil) + check.Assert(retrOverridden, DeepEquals, rdeTypeBehavior) + + testRdeTypeAccessControls(check, rdeType, retrOverridden) + + // We test Behavior invocation. Note that we invoke the overridden Behavior + // instead of the one from the Interface as the overridden is a dummy No-op that will finish OK all the time, + // as opposed as the original Activity. + testRdeBehaviorInvocation(check, rdeType, retrOverridden) + + // Delete the Behavior with original RDE Interface Behavior ID. It doesn't care if we use the original or the overridden ID, + // it is smart enough to delete the Behavior from the receiver Type. + err = rdeType.DeleteBehaviorOverride(originalBehavior.ID) + check.Assert(err, IsNil) + + // Check that the deletion was done correctly + originalBehaviors, err = rdeType.GetAllBehaviors(nil) + check.Assert(err, IsNil) + check.Assert(len(originalBehaviors), Equals, 1) // We still have 1: The original RDE Interface Behavior + check.Assert(originalBehaviors[0].ID, Not(Equals), retrOverridden.ID) // The ID should not be the overridden one as we deleted it + check.Assert(originalBehaviors[0].ID, Equals, originalBehavior.ID) // The ID should not be the overridden one as we deleted it +} + +// testRdeBehaviorInvocation tests that a Behavior can be invoked in a RDE of a given Type. +func testRdeBehaviorInvocation(check *C, rdeType *DefinedEntityType, behavior *types.Behavior) { + rdeEntityJson := []byte(` + { + "bar": "stringValue1", + "prop2": { + "subprop1": "stringValue2", + "subprop2": [ + "stringValue3", + "stringValue4" + ] + }, + "foo": { + "key": "stringValue5" + } + }`) + var unmarshaledRdeEntityJson map[string]interface{} + err := json.Unmarshal(rdeEntityJson, &unmarshaledRdeEntityJson) + check.Assert(err, IsNil) + + rde, err := rdeType.CreateRde(types.DefinedEntity{ + Name: check.TestName(), + Entity: unmarshaledRdeEntityJson, + }, nil) + check.Assert(err, IsNil) + + // RDE needs to be Resolved to ve invoked or deleted + err = rde.Resolve() + check.Assert(err, IsNil) + AddToCleanupListOpenApi(rde.DefinedEntity.ID, check.TestName(), types.OpenApiPathVersion1_0_0+types.OpenApiEndpointRdeEntities+rde.DefinedEntity.ID) + defer func() { + err := rde.Delete() + check.Assert(err, IsNil) + }() + + // We need to grant access to the Behavior to invoke it + err = rdeType.SetBehaviorAccessControls([]*types.BehaviorAccess{ + { + AccessLevelId: "urn:vcloud:accessLevel:FullControl", + BehaviorId: behavior.Ref, + }, + }) + check.Assert(err, IsNil) + + var invocation map[string]interface{} + err = rde.InvokeBehaviorAndMarshal(behavior.ID, types.BehaviorInvocation{ + Arguments: map[string]interface{}{}, + Metadata: map[string]interface{}{}, + }, &invocation) + check.Assert(err, IsNil) + check.Assert(len(invocation) > 0, Equals, true) + check.Assert(invocation["entityId"], Equals, rde.DefinedEntity.ID) +} + +func testRdeTypeAccessControls(check *C, rdeType *DefinedEntityType, behavior *types.Behavior) { + allAccCtrl, err := rdeType.GetAllBehaviorsAccessControls(nil) + check.Assert(err, IsNil) + check.Assert(len(allAccCtrl), Equals, 0) + + // Add the behavior access controls + behaviorAccess := &types.BehaviorAccess{ + AccessLevelId: "urn:vcloud:accessLevel:ReadOnly", + BehaviorId: behavior.ID, + } + err = rdeType.SetBehaviorAccessControls([]*types.BehaviorAccess{behaviorAccess}) + check.Assert(err, IsNil) + + allAccCtrl, err = rdeType.GetAllBehaviorsAccessControls(nil) + check.Assert(err, IsNil) + check.Assert(len(allAccCtrl), Equals, 1) + check.Assert(*allAccCtrl[0], DeepEquals, *behaviorAccess) + + // Update the behavior access controls + behaviorAccess = &types.BehaviorAccess{ + AccessLevelId: "urn:vcloud:accessLevel:ReadWrite", + BehaviorId: behavior.ID, + } + err = rdeType.SetBehaviorAccessControls([]*types.BehaviorAccess{behaviorAccess}) + check.Assert(err, IsNil) + + allAccCtrl, err = rdeType.GetAllBehaviorsAccessControls(nil) + check.Assert(err, IsNil) + check.Assert(len(allAccCtrl), Equals, 1) + check.Assert(*allAccCtrl[0], DeepEquals, *behaviorAccess) + + // Delete the behavior access controls + err = rdeType.SetBehaviorAccessControls([]*types.BehaviorAccess{}) + check.Assert(err, IsNil) + + allAccCtrl, err = rdeType.GetAllBehaviorsAccessControls(nil) + check.Assert(err, IsNil) + check.Assert(len(allAccCtrl), Equals, 0) +} diff --git a/govcd/defined_interface.go b/govcd/defined_interface.go index c8a02e1e9..873aa6b7e 100644 --- a/govcd/defined_interface.go +++ b/govcd/defined_interface.go @@ -192,6 +192,153 @@ func (di *DefinedInterface) Delete() error { return nil } +// AddBehavior adds a new Behavior to the receiver DefinedInterface. +// Only allowed if the Interface is not in use. +func (di *DefinedInterface) AddBehavior(behavior types.Behavior) (*types.Behavior, error) { + if di.DefinedInterface.ID == "" { + return nil, fmt.Errorf("ID of the receiver Defined Interface is empty") + } + + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeInterfaceBehaviors + apiVersion, err := di.client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := di.client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, di.DefinedInterface.ID)) + if err != nil { + return nil, err + } + + result := &types.Behavior{} + err = di.client.OpenApiPostItem(apiVersion, urlRef, nil, behavior, result, nil) + if err != nil { + return nil, err + } + + return result, nil +} + +// GetAllBehaviors retrieves all the Behaviors of the receiver Defined Interface. +func (di *DefinedInterface) GetAllBehaviors(queryParameters url.Values) ([]*types.Behavior, error) { + if di.DefinedInterface.ID == "" { + return nil, fmt.Errorf("ID of the receiver Defined Interface is empty") + } + return getAllBehaviors(di.client, di.DefinedInterface.ID, types.OpenApiEndpointRdeInterfaceBehaviors, queryParameters) +} + +// getAllBehaviors gets all the Behaviors from the object referenced by the input Object ID with the given OpenAPI endpoint. +func getAllBehaviors(client *Client, objectId, openApiEndpoint string, queryParameters url.Values) ([]*types.Behavior, error) { + endpoint := types.OpenApiPathVersion1_0_0 + openApiEndpoint + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, objectId)) + if err != nil { + return nil, err + } + + typeResponses := []*types.Behavior{{}} + err = client.OpenApiGetAllItems(apiVersion, urlRef, queryParameters, &typeResponses, nil) + if err != nil { + return nil, err + } + + return typeResponses, nil +} + +// GetBehaviorById retrieves a unique Behavior that belongs to the receiver Defined Interface and is determined by the +// input ID. +func (di *DefinedInterface) GetBehaviorById(id string) (*types.Behavior, error) { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeInterfaceBehaviors + apiVersion, err := di.client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := di.client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, di.DefinedInterface.ID), id) + if err != nil { + return nil, err + } + + response := types.Behavior{} + err = di.client.OpenApiGetItem(apiVersion, urlRef, nil, &response, nil) + if err != nil { + return nil, err + } + + return &response, nil +} + +// GetBehaviorByName retrieves a unique Behavior that belongs to the receiver Defined Interface and is named after +// the input. +func (di *DefinedInterface) GetBehaviorByName(name string) (*types.Behavior, error) { + behaviors, err := di.GetAllBehaviors(nil) + if err != nil { + return nil, fmt.Errorf("could not get the Behaviors of the Defined Interface with ID '%s': %s", di.DefinedInterface.ID, err) + } + for _, b := range behaviors { + if b.Name == name { + return b, nil + } + } + return nil, fmt.Errorf("could not find any Behavior with name '%s' in Defined Interface with ID '%s': %s", name, di.DefinedInterface.ID, ErrorEntityNotFound) +} + +// UpdateBehavior updates a Behavior specified by the input. +func (di *DefinedInterface) UpdateBehavior(behavior types.Behavior) (*types.Behavior, error) { + if di.DefinedInterface.ID == "" { + return nil, fmt.Errorf("ID of the receiver Defined Interface is empty") + } + if behavior.ID == "" { + return nil, fmt.Errorf("ID of the Behavior to update is empty") + } + + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeInterfaceBehaviors + apiVersion, err := di.client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := di.client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, di.DefinedInterface.ID), behavior.ID) + if err != nil { + return nil, err + } + response := types.Behavior{} + err = di.client.OpenApiPutItem(apiVersion, urlRef, nil, behavior, &response, nil) + if err != nil { + return nil, err + } + + return &response, nil +} + +// DeleteBehavior removes a Behavior specified by its ID from the receiver Defined Interface. +func (di *DefinedInterface) DeleteBehavior(behaviorId string) error { + if di.DefinedInterface.ID == "" { + return fmt.Errorf("ID of the receiver Defined Interface is empty") + } + + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeInterfaceBehaviors + apiVersion, err := di.client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return err + } + + urlRef, err := di.client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, di.DefinedInterface.ID), behaviorId) + if err != nil { + return err + } + err = di.client.OpenApiDeleteItem(apiVersion, urlRef, nil, nil) + if err != nil { + return err + } + + return nil +} + // amendRdeApiError fixes a wrong type of error returned by VCD API <= v36.0 on GET operations // when the defined interface does not exist. func amendRdeApiError(client *Client, err error) error { diff --git a/govcd/defined_interface_test.go b/govcd/defined_interface_test.go index 928f7e96f..02c9df6e3 100644 --- a/govcd/defined_interface_test.go +++ b/govcd/defined_interface_test.go @@ -124,3 +124,101 @@ func (vcd *TestVCD) Test_DefinedInterface(check *C) { check.Assert(err, NotNil) check.Assert(strings.Contains(err.Error(), ErrorEntityNotFound.Error()), Equals, true) } + +// Test_DefinedInterfaceBehavior tests the CRUD methods of Defined Interfaces to create Behaviors. +// This test can be run with GOVCD_SKIP_VAPP_CREATION option enabled. +func (vcd *TestVCD) Test_DefinedInterfaceBehavior(check *C) { + if vcd.skipAdminTests { + check.Skip(fmt.Sprintf(TestRequiresSysAdminPrivileges, check.TestName())) + } + skipOpenApiEndpointTest(vcd, check, types.OpenApiPathVersion1_0_0+types.OpenApiEndpointRdeInterfaceBehaviors) + + // Create a new Defined Interface with dummy values, so we can test behaviors on it + sanizitedTestName := strings.NewReplacer("_", "", ".", "").Replace(check.TestName()) + di, err := vcd.client.CreateDefinedInterface(&types.DefinedInterface{ + Name: sanizitedTestName, + Nss: "nss", + Version: "1.0.0", + Vendor: "vmware", + }) + check.Assert(err, IsNil) + AddToCleanupListOpenApi(di.DefinedInterface.ID, check.TestName(), types.OpenApiPathVersion1_0_0+types.OpenApiEndpointRdeInterfaces+di.DefinedInterface.ID) + defer func() { + err := di.Delete() + check.Assert(err, IsNil) + }() + + // Create a new Behavior payload with an Activity type. + behaviorPayload := types.Behavior{ + Name: sanizitedTestName, + Description: "Generated by " + check.TestName(), + Execution: map[string]interface{}{ + "id": "TestActivity", + "type": "Activity", + }, + } + behavior, err := di.AddBehavior(behaviorPayload) + check.Assert(err, IsNil) + check.Assert(behavior.Name, Equals, behaviorPayload.Name) + check.Assert(behavior.Description, Equals, behaviorPayload.Description) + check.Assert(behavior.Ref, Equals, fmt.Sprintf("urn:vcloud:behavior-interface:%s:%s:%s:%s", behaviorPayload.Name, di.DefinedInterface.Vendor, di.DefinedInterface.Nss, di.DefinedInterface.Version)) + check.Assert(behavior.ID, Equals, behavior.Ref) + + // Try to add the same behavior again. + _, err = di.AddBehavior(behaviorPayload) + check.Assert(err, NotNil) + check.Assert(strings.Contains(err.Error(), "RDE_BEHAVIOR_ALREADY_EXISTS"), Equals, true) + + // We check that the Behaviors can be retrieved + allBehaviors, err := di.GetAllBehaviors(nil) + check.Assert(err, IsNil) + check.Assert(1, Equals, len(allBehaviors)) + check.Assert(allBehaviors[0], DeepEquals, behavior) + + // Error getting non-existing Behaviors + _, err = di.GetBehaviorById("urn:vcloud:behavior-interface:notexist:notexist:notexist:9.9.9") + check.Assert(err, NotNil) + check.Assert(strings.Contains(err.Error(), ErrorEntityNotFound.Error()), Equals, true) + + _, err = di.GetBehaviorByName("DoesNotExist") + check.Assert(err, NotNil) + check.Assert(strings.Contains(err.Error(), ErrorEntityNotFound.Error()), Equals, true) + + // Getting behaviors correctly + retrievedBehavior, err := di.GetBehaviorById(behavior.ID) + check.Assert(err, IsNil) + check.Assert(retrievedBehavior, NotNil) + check.Assert(retrievedBehavior.Name, Equals, behavior.Name) + check.Assert(retrievedBehavior.Description, Equals, behavior.Description) + check.Assert(retrievedBehavior.Execution, DeepEquals, behavior.Execution) + + retrievedBehavior2, err := di.GetBehaviorByName(behavior.Name) + check.Assert(err, IsNil) + check.Assert(retrievedBehavior, NotNil) + check.Assert(retrievedBehavior, DeepEquals, retrievedBehavior2) + + updatePayload := types.Behavior{ + Description: "Updated description", + Execution: map[string]interface{}{ + "id": "TestActivityUpdated", + "type": "Activity", + }, + Ref: "notGoingToUpdate1", + Name: "notGoingToUpdate2", + } + _, err = di.UpdateBehavior(updatePayload) + check.Assert(err, NotNil) + check.Assert(err.Error(), Equals, "ID of the Behavior to update is empty") + + updatePayload.ID = retrievedBehavior.ID + updatedBehavior, err := di.UpdateBehavior(updatePayload) + check.Assert(err, IsNil) + check.Assert(updatedBehavior.ID, Equals, retrievedBehavior.ID) + check.Assert(updatedBehavior.Ref, Equals, retrievedBehavior.Ref) // This cannot be updated + check.Assert(updatedBehavior.Name, Equals, retrievedBehavior.Name) // This cannot be updated + check.Assert(updatedBehavior.Execution, DeepEquals, updatePayload.Execution) + check.Assert(updatedBehavior.Description, Equals, updatePayload.Description) + + err = di.DeleteBehavior(behavior.ID) + check.Assert(err, IsNil) +} diff --git a/govcd/openapi_endpoints.go b/govcd/openapi_endpoints.go index 39005bbd8..2009d66b6 100644 --- a/govcd/openapi_endpoints.go +++ b/govcd/openapi_endpoints.go @@ -61,10 +61,14 @@ var endpointMinApiVersions = map[string]string{ types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNsxtRouteAdvertisement: "34.0", // VCD 10.1+ types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointLogicalVmGroups: "35.0", // VCD 10.2+ types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeInterfaces: "35.0", // VCD 10.2+ + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeInterfaceBehaviors: "35.0", // VCD 10.2+ types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeEntityTypes: "35.0", // VCD 10.2+ + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeTypeBehaviors: "35.0", // VCD 10.2+ + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeTypeBehaviorAccessControls: "35.0", // VCD 10.2+ types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeEntities: "35.0", // VCD 10.2+ types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeEntitiesTypes: "35.0", // VCD 10.2+ types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeEntitiesResolve: "35.0", // VCD 10.2+ + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeEntitiesBehaviorsInvocations: "35.0", // VCD 10.2+ // IP Spaces types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSpaces: "37.1", // VCD 10.4.1+ diff --git a/types/v56/constants.go b/types/v56/constants.go index e8304cd43..60aab7d2c 100644 --- a/types/v56/constants.go +++ b/types/v56/constants.go @@ -408,10 +408,14 @@ const ( OpenApiEndpointEdgeBgpConfigPrefixLists = "edgeGateways/%s/routing/bgp/prefixLists/" // '%s' is NSX-T Edge Gateway ID OpenApiEndpointEdgeBgpConfig = "edgeGateways/%s/routing/bgp" // '%s' is NSX-T Edge Gateway ID OpenApiEndpointRdeInterfaces = "interfaces/" + OpenApiEndpointRdeInterfaceBehaviors = "interfaces/%s/behaviors/" OpenApiEndpointRdeEntityTypes = "entityTypes/" + OpenApiEndpointRdeTypeBehaviors = "entityTypes/%s/behaviors/" + OpenApiEndpointRdeTypeBehaviorAccessControls = "entityTypes/%s/behaviorAccessControls" OpenApiEndpointRdeEntities = "entities/" OpenApiEndpointRdeEntitiesTypes = "entities/types/" OpenApiEndpointRdeEntitiesResolve = "entities/%s/resolve" + OpenApiEndpointRdeEntitiesBehaviorsInvocations = "entities/%s/behaviors/%s/invocations" OpenApiEndpointVirtualCenters = "virtualCenters" OpenApiEndpointResourcePools = "virtualCenters/%s/resourcePools/browse" // '%s' is vCenter ID OpenApiEndpointResourcePoolsBrowseAll = "virtualCenters/%s/resourcePools/browseAll" // '%s' is vCenter ID diff --git a/types/v56/openapi.go b/types/v56/openapi.go index 6d824e39f..666ff8b16 100644 --- a/types/v56/openapi.go +++ b/types/v56/openapi.go @@ -464,6 +464,27 @@ type DefinedInterface struct { IsReadOnly bool `json:"readonly,omitempty"` // True if the entity type cannot be modified } +// Behavior defines a concept similar to a "procedure" that lives inside Defined Interfaces or Defined Entity Types as overrides. +type Behavior struct { + ID string `json:"id,omitempty"` // The Behavior ID is generated and is an output-only property + Description string `json:"description,omitempty"` // A description specifying the contract of the Behavior + Execution map[string]interface{} `json:"execution,omitempty"` // The Behavior execution mechanism. Can be defined both in an Interface and in a Defined Entity Type as an override + Ref string `json:"ref,omitempty"` // The Behavior invocation reference to be used for polymorphic behavior invocations. It is generated and is an output-only property + Name string `json:"name,omitempty"` +} + +// BehaviorAccess defines the access control configuration of a Behavior. +type BehaviorAccess struct { + AccessLevelId string `json:"accessLevelId,omitempty"` // The ID of an AccessLevel + BehaviorId string `json:"behaviorId,omitempty"` // The ID of the Behavior. It can be both a behavior-interface or an overridden behavior-type ID +} + +// BehaviorInvocation is an invocation of a Behavior on a Defined Entity instance. Currently, the Behavior interfaces are key-value maps specified in the Behavior description. +type BehaviorInvocation struct { + Arguments interface{} `json:"arguments,omitempty"` + Metadata interface{} `json:"metadata,omitempty"` +} + // DefinedEntityType describes what a Defined Entity Type should look like. type DefinedEntityType struct { ID string `json:"id,omitempty"` // The id of the defined entity type in URN format