github.com/vmware/go-vcloud-director/v2@v2.24.0/CODING_GUIDELINES.md (about) 1 # Coding guidelines 2 3 4 ## Principles 5 6 The functions, entities, and methods in this library have the wide goal of providing access to vCD functionality 7 using Go clients. 8 A more focused goal is to support the [Terraform Provider for vCD](https://github.com/terraform-providers/terraform-provider-vcd). 9 When in doubt about the direction of development, we should facilitate the path towards making the code usable and maintainable 10 in the above project. 11 12 13 ## Create new entities 14 15 A new entity must have its type defined in `types/56/types.go`. If the type is not already there, it should be 16 added using the [vCD API](https://code.vmware.com/apis/72/vcloud-director), and possibly reusing components already defined 17 in `types.go`. 18 19 The new entity should have a structure in `entity.go` as 20 21 ```go 22 type Entity struct { 23 Entity *types.Entity 24 client *VCDClient 25 // Optional, in some cases: Parent *Parent 26 } 27 ``` 28 29 The entity should have at least the following: 30 31 ``` 32 (parent *Parent) CreateEntityAsync(input *types.Entity) (Task, error) 33 (parent *Parent) CreateEntity(input *types.Entity) (*Entity, error) 34 ``` 35 36 The second form will invoke the `*Async` method, run task.WaitCompletion(), and then retrieving the new entity 37 from the parent and returning it. 38 39 If the API does not provide a task, the second method will be sufficient. 40 41 If the structure is exceedingly complex, we can use two approaches: 42 43 1. if the parameters needed to create the entity are less than 4, we can pass them as argument 44 45 ```go 46 (parent *Parent) CreateEntityAsync(field1, field2 string, field3 bool) (Task, error) 47 ``` 48 49 2. If there are too many parameters to pass, we can create a simplified structure: 50 51 ```go 52 type EntityInput struct { 53 field1 string 54 field2 string 55 field3 bool 56 field4 bool 57 field5 int 58 field6 string 59 field7 []string 60 } 61 62 (parent *Parent) CreateEntityAsync(simple EntityInput) (Task, error) 63 ``` 64 65 The latter approach should be preferred when the simplified structure would be a one-to-one match with the corresponding 66 resource in Terraform. 67 68 ## Calling the API 69 70 Calls to the vCD API should not be sent directly, but using one of the following functions from `api.go: 71 72 ```go 73 // Helper function creates request, runs it, check responses and parses out interface from response. 74 // pathURL - request URL 75 // requestType - HTTP method type 76 // contentType - value to set for "Content-Type" 77 // errorMessage - error message to return when error happens 78 // payload - XML struct which will be marshalled and added as body/payload 79 // out - structure to be used for unmarshalling xml 80 // E.g. unmarshalledAdminOrg := &types.AdminOrg{} 81 // client.ExecuteRequest(adminOrg.AdminOrg.HREF, http.MethodGet, "", "error refreshing organization: %s", nil, unmarshalledAdminOrg) 82 func (client *Client) ExecuteRequest(pathURL, requestType, contentType, errorMessage string, payload, out interface{}) (*http.Response, error) 83 ``` 84 85 ```go 86 // Helper function creates request, runs it, checks response and parses task from response. 87 // pathURL - request URL 88 // requestType - HTTP method type 89 // contentType - value to set for "Content-Type" 90 // errorMessage - error message to return when error happens 91 // payload - XML struct which will be marshalled and added as body/payload 92 // E.g. client.ExecuteTaskRequest(updateDiskLink.HREF, http.MethodPut, updateDiskLink.Type, "error updating disk: %s", xmlPayload) 93 func (client *Client) ExecuteTaskRequest(pathURL, requestType, contentType, errorMessage string, payload interface{}) (Task, error) 94 ``` 95 96 ```go 97 // Helper function creates request, runs it, checks response and do not expect any values from it. 98 // pathURL - request URL 99 // requestType - HTTP method type 100 // contentType - value to set for "Content-Type" 101 // errorMessage - error message to return when error happens 102 // payload - XML struct which will be marshalled and added as body/payload 103 // E.g. client.ExecuteRequestWithoutResponse(catalogItemHREF.String(), http.MethodDelete, "", "error deleting Catalog item: %s", nil) 104 func (client *Client) ExecuteRequestWithoutResponse(pathURL, requestType, contentType, errorMessage string, payload interface{}) error 105 ``` 106 107 ```go 108 // ExecuteRequestWithCustomError sends the request and checks for 2xx response. If the returned status code 109 // was not as expected - the returned error will be unmarshaled to `errType` which implements Go's standard `error` 110 // interface. 111 func (client *Client) ExecuteRequestWithCustomError(pathURL, requestType, contentType, errorMessage string, 112 payload interface{}, errType error) (*http.Response, error) 113 ``` 114 115 In addition to saving code and time by reducing the boilerplate, these functions also trigger debugging calls that make the code 116 easier to monitor. 117 Using any of the above calls will result in the standard log i 118 (See [LOGGING.md](https://github.com/vmware/go-vcloud-director/blob/main/util/LOGGING.md)) recording all the requests and responses 119 on demand, and also triggering debug output for specific calls (see `enableDebugShowRequest` and `enableDebugShowResponse` 120 and the corresponding `disable*` in `api.go`). 121 122 123 ## Implementing search methods 124 125 Each entity should have the following methods: 126 127 ``` 128 // OPTIONAL 129 (parent *Parent) GetEntityByHref(href string) (*Entity, error) 130 131 // ALWAYS 132 (parent *Parent) GetEntityByName(name string) (*Entity, error) 133 (parent *Parent) GetEntityById(id string) (*Entity, error) 134 (parent *Parent) GetEntityByNameOrId(identifier string) (*Entity, error) 135 ``` 136 137 For example, the parent for `Vdc` is `Org`, the parent for `EdgeGateway` is `Vdc`. 138 If the entity is at the top level (such as `Org`, `ExternalNetwork`), the parent is `VCDClient`. 139 140 These methods return a pointer to the entity's structure and a nil error when the search was successful, 141 a nil pointer and an error in every other case. 142 When the method can establish that the entity was not found because it did not appear in the 143 parent's list of entities, the method will return `ErrorEntityNotFound`. 144 In no cases we return a nil error when the method fails to find the entity. 145 The "ALWAYS" methods can optionally add a Boolean `refresh` argument, signifying that the parent should be refreshed 146 prior to attempting a search. 147 148 Note: We are in the process of replacing methods that don't adhere to the above principles (for example, return a 149 structure instead of a pointer, return a nil error on not-found, etc). 150 151 ## Implementing functions to support different vCD API versions 152 153 Functions dealing with different versions should use a matrix structure to identify which calls to run according to the 154 highest API version supported by vCD. An example can be found in adminvdc.go. 155 156 Note: use this pattern for adding new vCD functionality, which is not available in the earliest API version supported 157 by the code base (as indicated by `Client.APIVersion`). 158 159 ``` 160 type vdcVersionedFunc struct { 161 SupportedVersion string 162 CreateVdc func(adminOrg *AdminOrg, vdcConfiguration *types.VdcConfiguration) (*Vdc, error) 163 CreateVdcAsync func(adminOrg *AdminOrg, vdcConfiguration *types.VdcConfiguration) (Task, error) 164 UpdateVdc func(adminVdc *AdminVdc) (*AdminVdc, error) 165 UpdateVdcAsync func(adminVdc *AdminVdc) (Task, error) 166 } 167 168 var vdcVersionedFuncsV95 = vdcVersionedFuncs{ 169 SupportedVersion: "31.0", 170 CreateVdc: createVdc, 171 CreateVdcAsync: createVdcAsync, 172 UpdateVdc: updateVdc, 173 UpdateVdcAsync: updateVdcAsync, 174 } 175 176 var vdcVersionedFuncsV97 = vdcVersionedFuncs{ 177 SupportedVersion: "32.0", 178 CreateVdc: createVdcV97, 179 CreateVdcAsync: createVdcAsyncV97, 180 UpdateVdc: updateVdcV97, 181 UpdateVdcAsync: updateVdcAsyncV97, 182 } 183 184 var vdcVersionedFuncsByVcdVersion = map[string]vdcVersionedFuncs{ 185 "vdc9.5": vdcVersionedFuncsV95, 186 "vdc9.7": vdcVersionedFuncsV97, 187 "vdc10.0": vdcVersionedFuncsV97 188 } 189 190 func (adminOrg *AdminOrg) CreateOrgVdc(vdcConfiguration *types.VdcConfiguration) (*Vdc, error) { 191 apiVersion, err := adminOrg.client.MaxSupportedVersion() 192 if err != nil { 193 return nil, err 194 } 195 vdcFunctions, ok := vdcVersionedFuncsByVcdVersion["vdc"+apiVersionToVcdVersion[apiVersion]] 196 if !ok { 197 return nil, fmt.Errorf("no entity type found %s", "vdc"+apiVersion) 198 } 199 if vdcFunctions.CreateVdc == nil { 200 return nil, fmt.Errorf("function CreateVdc is not defined for %s", "vdc"+apiVersion) 201 } 202 util.Logger.Printf("[DEBUG] CreateOrgVdc call function for version %s", vdcFunctions.SupportedVersion) 203 return vdcFunctions.CreateVdc(adminOrg, vdcConfiguration) 204 } 205 ``` 206 207 ## Query engine 208 209 The query engine is a search engine that is based on queries (see `query.go`) with additional filters. 210 211 The query runs through the function `client.SearchByFilter` (`filter_engine.go`), which requires a `queryType` (string), 212 and a set of criteria (`*FilterDef`). 213 214 We can search by one of the types handled by `queryFieldsOnDemand` (`query_metadata.go`), such as 215 216 ```go 217 const ( 218 QtVappTemplate = "vappTemplate" // vApp template 219 QtAdminVappTemplate = "adminVAppTemplate" // vApp template as admin 220 QtEdgeGateway = "edgeGateway" // edge gateway 221 QtOrgVdcNetwork = "orgVdcNetwork" // Org VDC network 222 QtAdminCatalog = "adminCatalog" // catalog 223 QtCatalogItem = "catalogItem" // catalog item 224 QtAdminCatalogItem = "adminCatalogItem" // catalog item as admin 225 QtAdminMedia = "adminMedia" // media item as admin 226 QtMedia = "media" // media item 227 ) 228 ``` 229 There are two reasons for this limitation: 230 231 * If we want to include metadata, we need to add the metadata fields to the list of fields we want the query to fetch. 232 * Unfortunately, not all fields defined in the corresponding type is accepted by the `fields` parameter in a query. 233 The fields returned by `queryFieldsOnDemand` are the one that have been proven to be accepted. 234 235 236 The `FilterDef` type is defined as follows (`filter_utils.go`) 237 ```go 238 type FilterDef struct { 239 // A collection of filters (with keys from SupportedFilters) 240 Filters map[string]string 241 242 // A list of metadata filters 243 Metadata []MetadataDef 244 245 // If true, the query will include metadata fields and search for exact values. 246 // Otherwise, the engine will collect metadata fields and search by regexp 247 UseMetadataApiFilter bool 248 } 249 ``` 250 251 A `FilterDef` may contain several filters, such as: 252 253 ```go 254 criteria := &govcd.FilterDef{ 255 Filters: { 256 "name": "^Centos", 257 "date": "> 2020-02-02", 258 "latest": "true", 259 }, 260 Metadata: { 261 { 262 Key: "dept", 263 Type: "STRING", 264 Value: "ST\\w+", 265 IsSystem: false, 266 }, 267 }, 268 UseMetadataApiFilter: false, 269 } 270 ``` 271 272 The set of criteria above will find an item with name starting with "Centos", created after February 2nd, 2020, with 273 a metadata key "dept" associated with a value starting with "ST". If more than one item is found, the engine will return 274 the newest one (because of `"latest": "true"`) 275 The argument `UseMetadataApiFilter`, when true, instructs the engine to run the search with metadata values. Meaning that 276 the query will contain a clause `filter=metadata:KeyName==TYPE:Value`. If `IsSystem` is true, the clause will become 277 `filter=metadata@SYSTEM:KeyName==TYPE:Value`. This search can't evaluate regular expressions, because it goes directly 278 to vCD. 279 280 An example of `SYSTEM` metadata values is the set of annotations that the vCD adds to a vApp template when we save a 281 vApp to a catalog. 282 283 ``` 284 "metadata" = { 285 "vapp.origin.id" = "deadbeef-2913-4ed7-b943-79a91620fd52" // vApp ID 286 "vapp.origin.name" = "my_vapp_name" 287 "vapp.origin.type" = "com.vmware.vcloud.entity.vapp" 288 } 289 ``` 290 291 The engine returns a list of `QueryItem`, and interface that defines several methods used to help evaluate the search 292 conditions. 293 294 ### How to use the query engine 295 296 Here is an example of how to retrieve a media item. 297 The criteria ask for the newest item created after the 2nd of February 2020, containing a metadata field named "abc", 298 with a non-empty value. 299 300 ```go 301 criteria := &govcd.FilterDef{ 302 Filters: map[string]string{ 303 "date":"> 2020-02-02", 304 "latest": "true", 305 }, 306 Metadata: []govcd.MetadataDef{ 307 { 308 Key: "abc", 309 Type: "STRING", 310 Value: "\\S+", 311 IsSystem: false, 312 }, 313 }, 314 UseMetadataApiFilter: false, 315 } 316 queryType := govcd.QtMedia 317 if vcdClient.Client.IsSysAdmin { 318 queryType = govcd.QtAdminMedia 319 } 320 queryItems, explanation, err := vcdClient.Client.SearchByFilter(queryType, criteria) 321 if err != nil { 322 return err 323 } 324 if len(queryItems) == 0 { 325 return fmt.Errorf("no media found with given criteria (%s)", explanation) 326 } 327 if len(queryItems) > 1 { 328 // deal with several items 329 var itemNames = make([]string, len(queryItems)) 330 for i, item := range queryItems { 331 itemNames[i] = item.GetName() 332 } 333 return fmt.Errorf("more than one media item found by given criteria: %v", itemNames) 334 } 335 // retrieve the full entity for the item found 336 media, err = catalog.GetMediaByHref(queryItems[0].GetHref()) 337 ``` 338 339 The `explanation` returned by `SearchByFilter` contains the details of the criteria as they were understood by the 340 engine, and the detail of how each comparison with other items was evaluated. This is useful to create meaningful error 341 messages. 342 343 ### Supporting a new type in the query engine 344 345 To add a type to the search engine, we need the following: 346 347 1. Add the type to `types.QueryResultRecordsType` (`types.go`), or, if the type exists, make sure it includes `Metadata` 348 2. Add the list of supported fields to `queryFieldsOnDemand` (`query_metadata.go`) 349 3. Implement the interface `QueryItem` (`filter_interface.go`), which requires a type localization (such as 350 `type QueryMedia types.MediaRecordType`) 351 4. Add a clause to `resultToQueryItems` (`filter_interface.go`) 352 353 ## Data inspection checkpoints 354 355 Logs should not be cluttered with excessive detail. 356 However, sometimes we need to provide such detail when hunting for bugs. 357 358 We can introduce data inspection points, regulated by the environment variable `GOVCD_INSPECT`, which uses a convenient 359 code to activate the inspection at different points. 360 361 For example, we can mark the inspection points in the query engine with labels "QE1", "QE2", etc., in the network creation 362 they will be "NET1", "NET2", etc, and then activate them using 363 `GOVCD_INSPECT=QE2,NET1`. 364 365 In the code, we use the function `dataInspectionRequested(code)` that will check whether the environment variable contains 366 the given code. 367 368 ## Tenant Context 369 370 Tenant context is a mechanism in the VCD API to run calls as a tenant when connected as a system administrator. 371 It is used, for example, in the UI, to start a session as tenant administrator without having credentials for such a user, 372 or even when there is no such user yet. 373 The context change works by adding a header to the API call, containing these fields: 374 375 ``` 376 X-Vmware-Vcloud-Tenant-Context: [604cf889-b01e-408b-95ae-67b02a0ecf33] 377 X-Vmware-Vcloud-Auth-Context: [org-name] 378 ``` 379 380 The field `X-Vmware-Vcloud-Tenant-Context` contains the bare ID of the organization (it's just the UUID, without the 381 prefix `urn:vcloud:org:`). 382 The field `X-Vmware-Vcloud-Auth-Context` contains the organization name. 383 384 ### tenant context: data availability 385 386 From the SDK standpoint, finding the data needed to put together the tenant context is relatively easy when the originator 387 of the API call is the organization itself (such as `org.GetSomeEntityByName`). 388 When we deal with objects down the hierarchy, however, things are more difficult. Running a call from a VDC means that 389 we need to retrieve the parent organization, and extract ID and name. The ID is available through the `Link` structure 390 of the VDC, but for the name we need to retrieve the organization itself. 391 392 The approach taken in the SDK is to save the tenant context (or a pointer to the parent) in the object that we have just 393 created. For example, when we create a VDC, we save the organization as a pointer in the `parent` field, and the organization 394 itself has a field `TenantContext` with the needed information. 395 396 Here are the types that are needed for tenant context manipulation 397 ```go 398 399 // tenant_context.go 400 type TenantContext struct { 401 OrgId string // The bare ID (without prefix) of an organization 402 OrgName string // The organization name 403 } 404 405 // tenant_context.go 406 type organization interface { 407 orgId() string 408 orgName() string 409 tenantContext() (*TenantContext, error) 410 fullObject() interface{} 411 } 412 413 // org.go 414 type Org struct { 415 Org *types.Org 416 client *Client 417 TenantContext *TenantContext 418 } 419 420 // adminorg.go 421 type AdminOrg struct { 422 AdminOrg *types.AdminOrg 423 client *Client 424 TenantContext *TenantContext 425 } 426 427 // vdc.go 428 type Vdc struct { 429 Vdc *types.Vdc 430 client *Client 431 parent organization 432 } 433 ``` 434 435 The `organization` type is an abstraction to include both `Org` and `AdminOrg`. Thus, the VDC object has a pointer to its 436 parent that is only needed to get the tenant context quickly. 437 438 Each object has a way to get the tenant context by means of a `entity.getTenantContext()`. The information 439 trickles down from the hierarchy: 440 441 * a VDC gets the tenant context directly from its `parent` field, which has a method `tenantContext()` 442 * similarly, a Catalog has a `parent` field with the same functionality. 443 * a vApp will get the tenant context by first retrieving its parent (`vapp.getParentVdc()`) and then asking the parent 444 for the tenant context. 445 446 ### tenant context: usage 447 448 Once we have the tenant context, we need to pass the information along to the HTTP request that builds the request header, 449 so that our API call will run in the desired context. 450 451 The basic OpenAPI methods (`Client.OpenApiDeleteItem`, `Client.OpenApiGetAllItems`, `Client.OpenApiGetItem`, 452 `Client.OpenApiPostItem`, `Client.OpenApiPutItem`, `Client.OpenApiPutItemAsync`, `Client.OpenApiPutItemSync`) all include 453 a parameter `additionalHeader map[string]string` containing the information needed to build the tenant context header elements. 454 455 Inside the function where we want to use tenant context, we do these two steps: 456 457 1. retrieve the tenant context 458 2. add the additional header to the API call. 459 460 For example: 461 462 ```go 463 func (adminOrg *AdminOrg) GetAllRoles(queryParameters url.Values) ([]*Role, error) { 464 tenantContext, err := adminOrg.getTenantContext() 465 if err != nil { 466 return nil, err 467 } 468 return getAllRoles(adminOrg.client, queryParameters, getTenantContextHeader(tenantContext)) 469 } 470 ``` 471 The function `getTenantContextHeader` takes a tenant context and returns a map of strings containing the right header 472 keys. In the example above, the header is passed to `getAllRoles`, which in turn calls `Client.OpenApiGetAllItems`, 473 which passes the additional header until it reaches `newOpenApiRequest`, where the tenent context data is inserted in 474 the request header. 475 476 When the tenant context is not needed (system administration calls), we just pass `nil` as `additionalHeader`. 477 478 ## Generic CRUD functions for OpenAPI entity implementation 479 480 Generic CRUD functions are used to minimize boilerplate for entity implementation in the SDK. They 481 might not always be the way to go when there are very specific operation needs as it is not worth 482 having a generic function for single use case. In such cases, low level API client function set, 483 that is located in `openapi.go` can help to perform such operations. 484 485 ### Terminology 486 487 #### inner vs outer types 488 489 For the context of generic CRUD function implementation (mainly in files 490 `govcd/openapi_generic_outer_entities.go`, `govcd/openapi_generic_inner_entities.go`), such terms 491 are commonly used: 492 493 * `inner` type is the type that is responsible for marshaling/unmarshaling API 494 request payload and is usually inside `types` package. (e.g. `types.IpSpace`, 495 `types.NsxtAlbPoolMember`, etc.) 496 * `outer` (type) - this is the type that wraps `inner` type and possibly any other entities that are 497 required to perform operations for a particular VCD entity. It will almost always include some 498 reference to client (`VCDClient` or `Client`), which is required to perform API operations. It may 499 contain additional fields. 500 501 Here are the entities mapped in the example below: 502 503 * `DistributedFirewall` is the **`outer`** type 504 * `types.DistributedFirewallRules` is the **`inner`** type (specified in 505 `DistributedFirewall.DistributedFirewallRuleContainer` field) 506 * `client` field contains the client that is required for perfoming API operations 507 * `VdcGroup` field contains additional data (VDC Group reference) that is required for 508 implementation of this particular entity 509 510 ```go 511 type DistributedFirewall struct { 512 DistributedFirewallRuleContainer *types.DistributedFirewallRules 513 client *Client 514 VdcGroup *VdcGroup 515 } 516 ``` 517 518 #### crudConfig 519 520 A special type `govcd.crudConfig` is used for passing configuration to both - `inner` and `outer` 521 generic CRUD functions. It also has an internal `validate()` method, which is called upon execution 522 of any `inner` and `outer` CRUD functions. 523 524 See documentation of `govcd.crudConfig` for the options it provides. 525 526 ### Use cases 527 528 The main consideration when to use which functions depends on whether one is dealing with `inner` 529 types or `outer` types. Both types can be used for quicker development. 530 531 Usually, `outer` type is used for a full featured entity (e.g. `IpSpace`, `NsxtEdgeGateway`), while 532 `inner` suits cases where one needs to perform operations on an already existing or a read-only 533 entity. 534 535 **Hint:** return value of your entity method will always hint whether it is `inner` or `outer` one: 536 537 `inner` type function signature example (returns `*types.VdcNetworkProfile`): 538 539 ``` 540 func (adminVdc *AdminVdc) UpdateVdcNetworkProfile(vdcNetworkProfileConfig *types.VdcNetworkProfile) (*types.VdcNetworkProfile, error) { 541 ``` 542 543 `outer` type function signature example (returns `*IpSpace`): 544 545 ``` 546 func (vcdClient *VCDClient) CreateIpSpace(ipSpaceConfig *types.IpSpace) (*IpSpace, error) { 547 ``` 548 549 #### inner CRUD functions 550 551 The entities that match below criteria are usually going to use `inner` crud functions: 552 * API property manipulation with separate API endpoints for an already existing entity (e.g. VDC 553 Network Profiles `Vdc.UpdateVdcNetworkProfile`) 554 * Read only entities (e.g. NSX-T Segment Profiles `VCDClient.GetAllIpDiscoveryProfiles`) 555 556 Inner types are more simple as they can be directly used without any additional overhead. There are 557 7 functions that can be used: 558 559 * `createInnerEntity` 560 * `updateInnerEntity` 561 * `updateInnerEntityWithHeaders` 562 * `getInnerEntity` 563 * `getInnerEntityWithHeaders` 564 * `deleteEntityById` 565 * `getAllInnerEntities` 566 567 Existing examples of the implementation are: 568 569 * `Vdc.GetVdcNetworkProfile` 570 * `Vdc.UpdateVdcNetworkProfile` 571 * `Vdc.DeleteVdcNetworkProfile` 572 * `VCDClient.GetAllIpDiscoveryProfiles` 573 574 #### outer CRUD functions 575 576 The entities, that implement complete management of a VCD entity will usually rely on `outer` CRUD 577 functions. Any `outer` type *must* implement `wrap` method (example signature provided below). It is 578 required to satisfy generic interface constraint (so that generic functions are able to wrap `inner` 579 type into `outer` type) 580 581 ```go 582 func (o OuterEntity) wrap(inner *InnerEntity) *OuterEntity { 583 o.OuterEntity = inner 584 return &o 585 } 586 ``` 587 There are 5 functions for handling CRU(D). 588 * `createOuterEntity` 589 * `updateOuterEntity` 590 * `getOuterEntity` 591 * `getOuterEntityWithHeaders` 592 * `getAllOuterEntities` 593 594 *Note*: `D` (deletion) in `CRUD` is a simple operation that does not additionally handle data and 595 `deleteEntityById` is sufficient. 596 597 Existing examples of the implementation are: 598 * `IpSpace` 599 * `IpSpaceUplink` 600 * `DistributedFirewall` 601 * `DistributedFirewallRule` 602 * `NsxtSegmentProfileTemplate` 603 * `DefinedEntityType` 604 * `DefinedInterface` 605 * `DefinedEntity` 606 607 ## Testing 608 609 Every feature in the library must include testing. See [TESTING.md](https://github.com/vmware/go-vcloud-director/blob/main/TESTING.md) for more info.