From aea05c46aa2c2b3b04bb2a927a8649d227f4d8e2 Mon Sep 17 00:00:00 2001 From: Giuseppe Maxia Date: Tue, 14 Mar 2023 09:38:54 +0100 Subject: [PATCH] Add distributed firewall for NSX-V VDC (#521) * Add functions to enable/disable a NSX-V distributed firewall * Add NSX-V distributed firewall and service types specifications * Add services and configuration retrieval functions * Add NSX-V distributed firewall update function * Implement IsNsxv for AdminVdc * Add test for AdminVdc.IsNsxv * Add test for NSX-V distributed firewall update * Add CHANGELOG entry Signed-off-by: Giuseppe Maxia --- .changes/v2.20.0/521-features.md | 5 + govcd/adminvdc.go | 8 + govcd/api.go | 15 + govcd/api_vcd_test.go | 35 ++ govcd/nsxv_distributed_firewall.go | 442 ++++++++++++++++++++++++ govcd/nsxv_distributed_firewall_test.go | 320 +++++++++++++++++ govcd/vdc_test.go | 16 + types/v56/constants.go | 51 +++ types/v56/network.go | 192 ++++++++++ 9 files changed, 1084 insertions(+) create mode 100644 .changes/v2.20.0/521-features.md create mode 100644 govcd/nsxv_distributed_firewall.go create mode 100644 govcd/nsxv_distributed_firewall_test.go create mode 100644 types/v56/network.go diff --git a/.changes/v2.20.0/521-features.md b/.changes/v2.20.0/521-features.md new file mode 100644 index 000000000..605e408ab --- /dev/null +++ b/.changes/v2.20.0/521-features.md @@ -0,0 +1,5 @@ +* Added method `AdminVdc.IsNsxv` to detect whether an Admin VDC is NSX-V [GH-521] +* Added function `NewNsxvDistributedFirewall` to create a new NSX-V distributed firewall [GH-521] +* Added `NsxvDistributedFirewall` methods `GetConfiguration`, `IsEnabled`, `Enable`, `Disable`, `UpdateConfiguration`, `Refresh` to handle CRUD operations with NSX-V distributed firewalls [GH-521] +* Added `NsxvDistributedFirewall` methods `GetServices`, `GetServiceGroups`, `GetServiceById`, `GetServiceByName`, `GetServiceGroupById`, `GetServiceGroupByName` to retrieve specific services or service groups [GH-521] +* Added `NsxvDistributedFirewall` methods `GetServicesByRegex` and `GetServiceGroupsByRegex` to search services or service groups by regular expression [GH-521] diff --git a/govcd/adminvdc.go b/govcd/adminvdc.go index b91ec9cc6..f79ed4f0e 100644 --- a/govcd/adminvdc.go +++ b/govcd/adminvdc.go @@ -596,3 +596,11 @@ func (adminVdc *AdminVdc) GetDefaultStorageProfileReference() (*types.Reference, } return nil, fmt.Errorf("no default storage profile found for VDC %s", adminVdc.AdminVdc.Name) } + +// IsNsxv is a convenience function to check if the Admin VDC is backed by NSX-V Provider VDC +func (adminVdc *AdminVdc) IsNsxv() bool { + vdc := NewVdc(adminVdc.client) + vdc.Vdc = &adminVdc.AdminVdc.Vdc + vdc.parent = adminVdc.parent + return vdc.IsNsxv() +} diff --git a/govcd/api.go b/govcd/api.go index 3224717ce..fd347ab8f 100644 --- a/govcd/api.go +++ b/govcd/api.go @@ -871,6 +871,21 @@ func (client *Client) TestConnectionWithDefaults(subscriptionURL string) (bool, return true, nil } +// buildUrl uses the Client base URL to create a customised URL +func (client *Client) buildUrl(elements ...string) (string, error) { + baseUrl := client.VCDHREF.String() + if !IsValidUrl(baseUrl) { + return "", fmt.Errorf("incorrect URL %s", client.VCDHREF.String()) + } + if strings.HasSuffix(baseUrl, "/") { + baseUrl = strings.TrimRight(baseUrl, "/") + } + if strings.HasSuffix(baseUrl, "/api") { + baseUrl = strings.TrimRight(baseUrl, "/api") + } + return url.JoinPath(baseUrl, elements...) +} + // --------------------------------------------------------------------- // The following functions are needed to avoid strict Coverity warnings // --------------------------------------------------------------------- diff --git a/govcd/api_vcd_test.go b/govcd/api_vcd_test.go index f4d10780d..7bd0fef9c 100644 --- a/govcd/api_vcd_test.go +++ b/govcd/api_vcd_test.go @@ -1157,6 +1157,38 @@ func (vcd *TestVCD) removeLeftoverEntities(entity CleanupEntity) { vcd.infoCleanup(notDeletedMsg, entity.EntityType, entity.Name, err) } return + case "nsxv_dfw": + if entity.Parent == "" { + vcd.infoCleanup("removeLeftoverEntries: [ERROR] No ORG provided for VDC '%s'\n", entity.Name) + return + } + org, err := vcd.client.GetAdminOrgByName(entity.Parent) + if err != nil { + vcd.infoCleanup(notFoundMsg, "org", entity.Parent) + return + } + vdc, err := org.GetVDCByName(entity.Name, false) + if vdc == nil || err != nil { + vcd.infoCleanup(notFoundMsg, "vdc", entity.Name) + return + } + dfw := NewNsxvDistributedFirewall(vdc.client, vdc.Vdc.ID) + enabled, err := dfw.IsEnabled() + if err != nil { + vcd.infoCleanup("removeLeftoverEntries: [ERROR] checking distributed firewall from VCD '%s': %s", entity.Name, err) + return + } + if !enabled { + vcd.infoCleanup(notFoundMsg, entity.EntityType, entity.Name) + return + } + err = dfw.Disable() + if err == nil { + vcd.infoCleanup(removedMsg, entity.EntityType, entity.Name, entity.CreatedBy) + } else { + vcd.infoCleanup("removeLeftoverEntries: [ERROR] removing distributed firewall from VCD '%s': %s", entity.Name, err) + return + } case "standaloneVm": vm, err := vcd.org.QueryVmById(entity.Name) // The VM ID must be passed as Name if IsNotFound(err) { @@ -1622,6 +1654,9 @@ func (vcd *TestVCD) TearDownSuite(check *C) { // Tests getloginurl with the endpoint given // in the config file. func TestClient_getloginurl(t *testing.T) { + if os.Getenv("GOVCD_API_VERSION") != "" { + t.Skip("custom API version is being used") + } config, err := GetConfigStruct() if err != nil { t.Fatalf("err: %s", err) diff --git a/govcd/nsxv_distributed_firewall.go b/govcd/nsxv_distributed_firewall.go new file mode 100644 index 000000000..194a5558d --- /dev/null +++ b/govcd/nsxv_distributed_firewall.go @@ -0,0 +1,442 @@ +/* + * Copyright 2023 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "fmt" + "net/http" + "net/url" + "regexp" + "strings" + + "github.com/vmware/go-vcloud-director/v2/types/v56" +) + +// NsxvDistributedFirewall defines a distributed firewall for a NSX-V VDC +type NsxvDistributedFirewall struct { + VdcId string // The ID of the VDC + Configuration *types.FirewallConfiguration // The latest firewall configuration + Etag string + enabled bool // internal flag that signifies whether the firewall is enabled + client *Client // internal usage client + + Services []types.Application // The list of services for this VDC + ServiceGroups []types.ApplicationGroup // The list of service groups for this VDC +} + +// NewNsxvDistributedFirewall creates a new NsxvDistributedFirewall +func NewNsxvDistributedFirewall(client *Client, vdcId string) *NsxvDistributedFirewall { + return &NsxvDistributedFirewall{ + client: client, + VdcId: extractUuid(vdcId), + } +} + +// GetConfiguration retrieves the configuration of a distributed firewall +func (dfw *NsxvDistributedFirewall) GetConfiguration() (*types.FirewallConfiguration, error) { + // Explicitly retrieving only the Layer 3 rules, as we don't need to deal with layer 2 + initialUrl, err := dfw.client.buildUrl("network", "firewall", "globalroot-0", "config", "layer3sections", dfw.VdcId) + if err != nil { + return nil, err + } + + requestUrl, err := url.ParseRequestURI(initialUrl) + if err != nil { + return nil, err + } + + req := dfw.client.NewRequest(nil, http.MethodGet, *requestUrl, nil) + + resp, err := checkResp(dfw.client.Http.Do(req)) + if err != nil { + return nil, err + } + var config types.FirewallConfiguration + + var firewallSection types.FirewallSection + err = decodeBody(types.BodyTypeXML, resp, &firewallSection) + if err != nil { + return nil, err + } + dfw.Etag = resp.Header.Get("etag") + // The ETag header is needed for further operations. Rules insertion and update need to have a + // header "If-Match" with the contents of the ETag from a previous read. + // The same data can be found in the "GenerationNumber" within the section to update. + // The value of the ETag changes at every GET + if dfw.Etag == "" && firewallSection.GenerationNumber != "" { + dfw.Etag = firewallSection.GenerationNumber + } + config.Layer3Sections = &types.Layer3Sections{Section: &firewallSection} + dfw.Configuration = &config + dfw.Configuration.Layer3Sections = config.Layer3Sections + dfw.enabled = true + return &config, nil +} + +// IsEnabled returns true when the distributed firewall is enabled +func (dfw *NsxvDistributedFirewall) IsEnabled() (bool, error) { + if dfw.VdcId == "" { + return false, fmt.Errorf("no VDC set for this NsxvDistributedFirewall") + } + + conf, err := dfw.GetConfiguration() + if err != nil { + return false, nil + } + if dfw.client.APIVersion == "36.0" { + return conf != nil, nil + } + return true, nil +} + +// Enable makes the distributed firewall available +// It fails with a non-NSX-V VDC +func (dfw *NsxvDistributedFirewall) Enable() error { + dfw.enabled = false + if dfw.VdcId == "" { + return fmt.Errorf("no AdminVdc set for this NsxvDistributedFirewall") + } + initialUrl, err := dfw.client.buildUrl("network", "firewall", "vdc", extractUuid(dfw.VdcId)) + if err != nil { + return err + } + + requestUrl, err := url.ParseRequestURI(initialUrl) + if err != nil { + return err + } + + req := dfw.client.NewRequest(nil, http.MethodPost, *requestUrl, nil) + + resp, err := checkResp(dfw.client.Http.Do(req)) + if err != nil { + return err + } + if resp != nil && resp.StatusCode != http.StatusCreated { + return fmt.Errorf("[enable DistributedFirewall] expected status code %d - received %d", http.StatusCreated, resp.StatusCode) + } + dfw.enabled = true + return nil +} + +// Disable removes the availability of a distributed firewall +// WARNING: it also removes all rules +func (dfw *NsxvDistributedFirewall) Disable() error { + if dfw.VdcId == "" { + return fmt.Errorf("no AdminVdc set for this NsxvDistributedFirewall") + } + initialUrl, err := dfw.client.buildUrl("network", "firewall", "vdc", extractUuid(dfw.VdcId)) + if err != nil { + return err + } + + requestUrl, err := url.ParseRequestURI(initialUrl) + if err != nil { + return err + } + + req := dfw.client.NewRequest(nil, http.MethodDelete, *requestUrl, nil) + + resp, err := checkResp(dfw.client.Http.Do(req)) + if err != nil { + // VCD 10.3.x sometimes returns an error even though the removal succeeds + if dfw.client.APIVersion == "36.0" { + conf, _ := dfw.GetConfiguration() + if conf == nil { + return nil + } + } + return fmt.Errorf("error deleting Distributed firewall: %s", err) + + } + if resp != nil && resp.StatusCode != http.StatusNoContent { + return fmt.Errorf("[disable DistributedFirewall] expected status code %d - received %d", http.StatusNoContent, resp.StatusCode) + } + dfw.Configuration = nil + dfw.Services = nil + dfw.ServiceGroups = nil + dfw.enabled = false + return nil +} + +// UpdateConfiguration will either create a new set of rules or update existing ones. +// If the firewall already contains rules, they are overwritten by the ones passed as parameters +func (dfw *NsxvDistributedFirewall) UpdateConfiguration(rules []types.NsxvDistributedFirewallRule) (*types.FirewallConfiguration, error) { + + oldConf, err := dfw.GetConfiguration() + if err != nil { + return nil, err + } + if dfw.Etag == "" { + return nil, fmt.Errorf("error getting ETag from distributed firewall") + } + initialUrl, err := dfw.client.buildUrl("network", "firewall", "globalroot-0", "config", "layer3sections", dfw.VdcId) + if err != nil { + return nil, err + } + + requestUrl, err := url.ParseRequestURI(initialUrl) + if err != nil { + return nil, err + } + + var errorList []string + for i := 0; i < len(rules); i++ { + rules[i].SectionID = oldConf.Layer3Sections.Section.ID + if rules[i].Direction == "" { + errorList = append(errorList, fmt.Sprintf("missing Direction in rule n. %d ", i+1)) + } + if rules[i].PacketType == "" { + errorList = append(errorList, fmt.Sprintf("missing Packet Type in rule n. %d ", i+1)) + } + if rules[i].Action == "" { + errorList = append(errorList, fmt.Sprintf("missing Action in rule n. %d ", i+1)) + } + } + if len(errorList) > 0 { + return nil, fmt.Errorf("missing required elements from rules: %s", strings.Join(errorList, "; ")) + } + + ruleSet := types.FirewallSection{ + ID: oldConf.Layer3Sections.Section.ID, + GenerationNumber: strings.Trim(dfw.Etag, `"`), + Name: dfw.VdcId, + Rule: rules, + } + + var newRuleset types.FirewallSection + + dfw.client.SetCustomHeader(map[string]string{ + "If-Match": strings.Trim(oldConf.Layer3Sections.Section.GenerationNumber, `"`), + }) + defer dfw.client.RemoveCustomHeader() + + contentType := fmt.Sprintf("application/*+xml;version=%s", dfw.client.APIVersion) + + resp, err := dfw.client.ExecuteRequest(requestUrl.String(), http.MethodPut, contentType, + "error updating NSX-V distributed firewall: %s", ruleSet, &newRuleset) + + if err != nil { + return nil, err + } + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("[update DistributedFirewall] expected status code %d - received %d", http.StatusOK, resp.StatusCode) + } + return dfw.GetConfiguration() +} + +// GetServices retrieves the list of services for the current VCD +// If `refresh` = false and the services were already retrieved in a previous operation, +// then it returns the internal values instead of fetching new ones +func (dfw *NsxvDistributedFirewall) GetServices(refresh bool) ([]types.Application, error) { + if dfw.Services != nil && !refresh { + return dfw.Services, nil + } + if dfw.VdcId == "" { + return nil, fmt.Errorf("no AdminVdc set for this NsxvDistributedFirewall") + } + initialUrl, err := dfw.client.buildUrl("network", "services", "application", "scope", extractUuid(dfw.VdcId)) + if err != nil { + return nil, err + } + + requestUrl, err := url.ParseRequestURI(initialUrl) + if err != nil { + return nil, err + } + + req := dfw.client.NewRequest(nil, http.MethodGet, *requestUrl, nil) + + resp, err := checkResp(dfw.client.Http.Do(req)) + if err != nil { + return nil, err + } + if resp != nil && resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("error fetching the services: %s", resp.Status) + } + var applicationList types.ApplicationList + err = decodeBody(types.BodyTypeXML, resp, &applicationList) + if err != nil { + return nil, err + } + dfw.Services = applicationList.Application + return applicationList.Application, nil +} + +// GetServiceGroups retrieves the list of services for the current VDC +// If `refresh` = false and the services were already retrieved in a previous operation, +// then it returns the internal values instead of fetching new ones +func (dfw *NsxvDistributedFirewall) GetServiceGroups(refresh bool) ([]types.ApplicationGroup, error) { + if dfw.ServiceGroups != nil && !refresh { + return dfw.ServiceGroups, nil + } + if dfw.VdcId == "" { + return nil, fmt.Errorf("no AdminVdc set for this NsxvDistributedFirewall") + } + initialUrl, err := dfw.client.buildUrl("network", "services", "applicationgroup", "scope", extractUuid(dfw.VdcId)) + if err != nil { + return nil, err + } + + requestUrl, err := url.ParseRequestURI(initialUrl) + if err != nil { + return nil, err + } + + req := dfw.client.NewRequest(nil, http.MethodGet, *requestUrl, nil) + + resp, err := checkResp(dfw.client.Http.Do(req)) + if err != nil { + return nil, err + } + if resp != nil && resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("error fetching the service groups: %s", resp.Status) + } + var applicationGroupList types.ApplicationGroupList + err = decodeBody(types.BodyTypeXML, resp, &applicationGroupList) + if err != nil { + return nil, err + } + dfw.ServiceGroups = applicationGroupList.ApplicationGroup + return applicationGroupList.ApplicationGroup, nil +} + +// Refresh retrieves fresh values for the distributed firewall rules, services, and service groups +func (dfw *NsxvDistributedFirewall) Refresh() error { + if dfw.VdcId == "" { + return fmt.Errorf("no AdminVdc set for this NsxvDistributedFirewall") + } + + _, err := dfw.GetServices(true) + if err != nil { + return err + } + + _, err = dfw.GetServiceGroups(true) + if err != nil { + return err + } + + _, err = dfw.GetConfiguration() + return err +} + +// GetServiceById retrieves a single service, identified by its ID, for the current VDC +// If the list of services was already retrieved, it uses it, otherwise fetches new ones. +// Returns ErrorEntityNotFound when the requested services was not found +func (dfw *NsxvDistributedFirewall) GetServiceById(serviceId string) (*types.Application, error) { + services, err := dfw.GetServices(false) + if err != nil { + return nil, err + } + for _, app := range services { + if app.ObjectID == serviceId { + return &app, nil + } + } + return nil, ErrorEntityNotFound +} + +// GetServiceByName retrieves a single service, identified by its name, for the current VDC +// If the list of services was already retrieved, it uses it, otherwise fetches new ones. +// Returns ErrorEntityNotFound when the requested service was not found +func (dfw *NsxvDistributedFirewall) GetServiceByName(serviceName string) (*types.Application, error) { + services, err := dfw.GetServices(false) + if err != nil { + return nil, err + } + var foundService types.Application + for _, app := range services { + if app.Name == serviceName { + if foundService.ObjectID != "" { + return nil, fmt.Errorf("more than one service found with name '%s'", serviceName) + } + foundService = app + } + } + if foundService.ObjectID == "" { + return nil, ErrorEntityNotFound + } + return &foundService, nil +} + +// GetServicesByRegex returns a list of services with their names matching the given regular expression +// It may return an empty list (without error) +func (dfw *NsxvDistributedFirewall) GetServicesByRegex(expression string) ([]types.Application, error) { + services, err := dfw.GetServices(false) + if err != nil { + return nil, err + } + searchRegex, err := regexp.Compile(expression) + if err != nil { + return nil, fmt.Errorf("[GetServicesByRegex] error validating regular expression '%s': %s", expression, err) + } + var found []types.Application + for _, app := range services { + if searchRegex.MatchString(app.Name) { + found = append(found, app) + } + } + return found, nil +} + +// GetServiceGroupById retrieves a single service group, identified by its ID, for the current VDC +// If the list of service groups was already retrieved, it uses it, otherwise fetches new ones. +// Returns ErrorEntityNotFound when the requested service group was not found +func (dfw *NsxvDistributedFirewall) GetServiceGroupById(serviceGroupId string) (*types.ApplicationGroup, error) { + serviceGroups, err := dfw.GetServiceGroups(false) + if err != nil { + return nil, err + } + for _, appGroup := range serviceGroups { + if appGroup.ObjectID == serviceGroupId { + return &appGroup, nil + } + } + return nil, ErrorEntityNotFound +} + +// GetServiceGroupByName retrieves a single service group, identified by its name, for the current VDC +// If the list of service groups was already retrieved, it uses it, otherwise fetches new ones. +// Returns ErrorEntityNotFound when the requested service group was not found +func (dfw *NsxvDistributedFirewall) GetServiceGroupByName(serviceGroupName string) (*types.ApplicationGroup, error) { + serviceGroups, err := dfw.GetServiceGroups(false) + if err != nil { + return nil, err + } + var foundAppGroup types.ApplicationGroup + for _, appGroup := range serviceGroups { + if appGroup.Name == serviceGroupName { + if foundAppGroup.ObjectID != "" { + return nil, fmt.Errorf("more than one service group found with name %s", serviceGroupName) + } + foundAppGroup = appGroup + } + } + if foundAppGroup.ObjectID == "" { + return nil, ErrorEntityNotFound + } + return &foundAppGroup, nil +} + +// GetServiceGroupsByRegex returns a list of services with their names matching the given regular expression +// It may return an empty list (without error) +func (dfw *NsxvDistributedFirewall) GetServiceGroupsByRegex(expression string) ([]types.ApplicationGroup, error) { + serviceGroups, err := dfw.GetServiceGroups(false) + if err != nil { + return nil, err + } + searchRegex, err := regexp.Compile(expression) + if err != nil { + return nil, fmt.Errorf("[GetServiceGroupsByRegex] error validating regular expression '%s': %s", expression, err) + } + var found []types.ApplicationGroup + for _, appGroup := range serviceGroups { + if searchRegex.MatchString(appGroup.Name) { + found = append(found, appGroup) + } + } + return found, nil +} diff --git a/govcd/nsxv_distributed_firewall_test.go b/govcd/nsxv_distributed_firewall_test.go new file mode 100644 index 000000000..40d9a4150 --- /dev/null +++ b/govcd/nsxv_distributed_firewall_test.go @@ -0,0 +1,320 @@ +//go:build functional || network || ALL + +/* + * Copyright 2023 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ +package govcd + +import ( + "fmt" + "github.com/kr/pretty" + "github.com/vmware/go-vcloud-director/v2/types/v56" + . "gopkg.in/check.v1" + "strings" +) + +func (vcd *TestVCD) Test_NsxvDistributedFirewall(check *C) { + fmt.Printf("Running: %s\n", check.TestName()) + + org, err := vcd.client.GetAdminOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + check.Assert(org, NotNil) + + if vcd.config.VCD.Nsxt.Vdc != "" { + if testVerbose { + fmt.Println("Testing attempted access to NSX-T VDC") + } + // Retrieve a NSX-T VDC + nsxtVdc, err := org.GetAdminVDCByName(vcd.config.VCD.Nsxt.Vdc, false) + check.Assert(err, IsNil) + + notWorkingDfw := NewNsxvDistributedFirewall(nsxtVdc.client, nsxtVdc.AdminVdc.ID) + check.Assert(notWorkingDfw, NotNil) + + isEnabled, err := notWorkingDfw.IsEnabled() + check.Assert(err, IsNil) + check.Assert(isEnabled, Equals, false) + + err = notWorkingDfw.Enable() + // NSX-T VDCs don't support NSX-V distributed firewalls. We expect an error here. + check.Assert(err, NotNil) + check.Assert(strings.Contains(err.Error(), "Forbidden"), Equals, true) + if testVerbose { + fmt.Printf("notWorkingDfw: %s\n", err) + } + config, err := notWorkingDfw.GetConfiguration() + // Also this operation should fail + check.Assert(err, NotNil) + check.Assert(config, IsNil) + } + + // Retrieve a NSX-V VDC + vdc, err := org.GetAdminVDCByName(vcd.config.VCD.Vdc, false) + check.Assert(err, IsNil) + check.Assert(vdc, NotNil) + + dfw := NewNsxvDistributedFirewall(vdc.client, vdc.AdminVdc.ID) + check.Assert(dfw, NotNil) + + // dfw.Enable is an idempotent operation. It can be repeated on an already enabled DFW + // without errors. + err = dfw.Enable() + check.Assert(err, IsNil) + + enabled, err := dfw.IsEnabled() + check.Assert(err, IsNil) + check.Assert(enabled, Equals, true) + + config, err := dfw.GetConfiguration() + check.Assert(err, IsNil) + check.Assert(config, NotNil) + if testVerbose { + fmt.Printf("%# v\n", pretty.Formatter(config)) + } + + // Repeat enable operation + err = dfw.Enable() + check.Assert(err, IsNil) + + enabled, err = dfw.IsEnabled() + check.Assert(err, IsNil) + check.Assert(enabled, Equals, true) + + err = dfw.Disable() + check.Assert(err, IsNil) + enabled, err = dfw.IsEnabled() + check.Assert(err, IsNil) + check.Assert(enabled, Equals, false) + + // Also dfw.Disable is idempotent + err = dfw.Disable() + check.Assert(err, IsNil) + + enabled, err = dfw.IsEnabled() + check.Assert(err, IsNil) + check.Assert(enabled, Equals, false) +} + +func (vcd *TestVCD) Test_NsxvDistributedFirewallUpdate(check *C) { + fmt.Printf("Running: %s\n", check.TestName()) + + org, err := vcd.client.GetAdminOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + check.Assert(org, NotNil) + + // Retrieve a NSX-V VDC + adminVdc, err := org.GetAdminVDCByName(vcd.config.VCD.Vdc, false) + check.Assert(err, IsNil) + check.Assert(adminVdc, NotNil) + vdc, err := org.GetVDCByName(vcd.config.VCD.Vdc, false) + check.Assert(err, IsNil) + + dfw := NewNsxvDistributedFirewall(adminVdc.client, adminVdc.AdminVdc.ID) + check.Assert(dfw, NotNil) + enabled, err := dfw.IsEnabled() + check.Assert(err, IsNil) + // + if enabled { + check.Skip(fmt.Sprintf("VDC %s already contains a distributed firewall - skipping", vcd.config.VCD.Vdc)) + } + + vms, err := vdc.QueryVmList(types.VmQueryFilterOnlyDeployed) + check.Assert(err, IsNil) + + sampleDestination := &types.Destinations{} + if len(vms) > 0 { + sampleDestination.Destination = []types.Destination{ + { + Name: vms[0].Name, + Value: extractUuid(vms[0].HREF), + Type: types.DFWElementVirtualMachine, + IsValid: true, + }, + } + } + err = dfw.Enable() + check.Assert(err, IsNil) + + dnsService, err := dfw.GetServiceByName("DNS") + check.Assert(err, IsNil) + integrationServiceGroup, err := dfw.GetServiceGroupByName("MSSQL Integration Services") + check.Assert(err, IsNil) + + network, err := vdc.GetOrgVdcNetworkByName(vcd.config.VCD.Network.Net1, false) + check.Assert(err, IsNil) + AddToCleanupList(vcd.config.VCD.Vdc, "nsxv_dfw", vcd.config.VCD.Org, check.TestName()) + rules := []types.NsxvDistributedFirewallRule{ + { + Name: "first", + Action: types.DFWActionDeny, + AppliedToList: &types.AppliedToList{ + AppliedTo: []types.AppliedTo{ + { + Name: adminVdc.AdminVdc.Name, + Value: adminVdc.AdminVdc.ID, + Type: "VDC", + IsValid: true, + }, + }, + }, + Direction: types.DFWDirectionInout, + PacketType: types.DFWPacketAny, + }, + { + Name: "second", + AppliedToList: &types.AppliedToList{}, + SectionID: nil, + Sources: nil, + Destinations: nil, + Services: nil, + Direction: types.DFWDirectionIn, + PacketType: types.DFWPacketAny, + Action: types.DFWActionAllow, + }, + { + Name: "third", + Action: types.DFWActionAllow, + AppliedToList: &types.AppliedToList{}, + Sources: &types.Sources{ + Source: []types.Source{ + // Anonymous source + { + Name: "10.10.10.1", + Value: "10.10.10.1", + Type: types.DFWElementIpv4, + }, + // Named source + { + Name: network.OrgVDCNetwork.Name, + Value: extractUuid(network.OrgVDCNetwork.ID), + Type: types.DFWElementNetwork, + IsValid: true, + }, + }, + }, + Destinations: sampleDestination, + Services: &types.Services{ + Service: []types.Service{ + // Anonymous service + { + IsValid: true, + SourcePort: takeStringPointer("1000"), + DestinationPort: takeStringPointer("1200"), + Protocol: takeIntAddress(types.NsxvProtocolCodes[types.DFWProtocolTcp]), + ProtocolName: takeStringPointer(types.DFWProtocolTcp), + }, + // Named service + { + IsValid: true, + Name: dnsService.Name, + Value: dnsService.ObjectID, + Type: types.DFWServiceTypeApplication, + }, + // Named service group + { + IsValid: true, + Name: integrationServiceGroup.Name, + Value: integrationServiceGroup.ObjectID, + Type: types.DFWServiceTypeApplicationGroup, + }, + }, + }, + Direction: types.DFWDirectionIn, + PacketType: types.DFWPacketIpv4, + }, + } + + updatedRules, err := dfw.UpdateConfiguration(rules) + check.Assert(err, IsNil) + check.Assert(updatedRules, NotNil) + + err = dfw.Disable() + check.Assert(err, IsNil) + +} + +func (vcd *TestVCD) Test_NsxvServices(check *C) { + fmt.Printf("Running: %s\n", check.TestName()) + + org, err := vcd.client.GetAdminOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + check.Assert(org, NotNil) + + // Retrieve a NSX-V VDC + vdc, err := org.GetAdminVDCByName(vcd.config.VCD.Vdc, false) + check.Assert(err, IsNil) + check.Assert(vdc, NotNil) + + dfw := NewNsxvDistributedFirewall(vdc.client, vdc.AdminVdc.ID) + check.Assert(dfw, NotNil) + + services, err := dfw.GetServices(false) + check.Assert(err, IsNil) + check.Assert(services, NotNil) + check.Assert(len(services) > 0, Equals, true) + + if testVerbose { + fmt.Printf("services: %d\n", len(services)) + fmt.Printf("%# v\n", pretty.Formatter(services[0])) + } + + serviceName := "PostgreSQL" + serviceByName, err := dfw.GetServiceByName(serviceName) + + check.Assert(err, IsNil) + check.Assert(serviceByName, NotNil) + check.Assert(serviceByName.Name, Equals, serviceName) + + serviceById, err := dfw.GetServiceById(serviceByName.ObjectID) + check.Assert(err, IsNil) + check.Assert(serviceById.Name, Equals, serviceName) + + searchRegex := "M.SQL" // Finds, among others, names containing "MySQL" or "MSSQL" + servicesByRegex, err := dfw.GetServicesByRegex(searchRegex) + check.Assert(err, IsNil) + check.Assert(len(servicesByRegex) > 1, Equals, true) + + searchRegex = "." // Finds all services + servicesByRegex, err = dfw.GetServicesByRegex(searchRegex) + check.Assert(err, IsNil) + check.Assert(len(servicesByRegex), Equals, len(services)) + + searchRegex = "^####--no-such-service--####$" // Finds no services + servicesByRegex, err = dfw.GetServicesByRegex(searchRegex) + check.Assert(err, IsNil) + check.Assert(len(servicesByRegex), Equals, 0) + + serviceGroups, err := dfw.GetServiceGroups(false) + check.Assert(err, IsNil) + check.Assert(serviceGroups, NotNil) + check.Assert(len(serviceGroups) > 0, Equals, true) + if testVerbose { + fmt.Printf("service groups: %d\n", len(serviceGroups)) + fmt.Printf("%# v\n", pretty.Formatter(serviceGroups[0])) + } + serviceGroupName := "Orchestrator" + serviceGroupByName, err := dfw.GetServiceGroupByName(serviceGroupName) + check.Assert(err, IsNil) + check.Assert(serviceGroupByName, NotNil) + check.Assert(serviceGroupByName.Name, Equals, serviceGroupName) + + serviceGroupById, err := dfw.GetServiceGroupById(serviceGroupByName.ObjectID) + check.Assert(err, IsNil) + check.Assert(serviceGroupById, NotNil) + check.Assert(serviceGroupById.Name, Equals, serviceGroupName) + + searchRegex = "Oracle" + serviceGroupsByRegex, err := dfw.GetServiceGroupsByRegex(searchRegex) + check.Assert(err, IsNil) + check.Assert(len(serviceGroupsByRegex) > 1, Equals, true) + + searchRegex = "." + serviceGroupsByRegex, err = dfw.GetServiceGroupsByRegex(searchRegex) + check.Assert(err, IsNil) + check.Assert(len(serviceGroupsByRegex), Equals, len(serviceGroups)) + + searchRegex = "^####--no-such-service-group--####$" + serviceGroupsByRegex, err = dfw.GetServiceGroupsByRegex(searchRegex) + check.Assert(err, IsNil) + check.Assert(len(serviceGroupsByRegex), Equals, 0) +} diff --git a/govcd/vdc_test.go b/govcd/vdc_test.go index 429d3a4c9..904684987 100644 --- a/govcd/vdc_test.go +++ b/govcd/vdc_test.go @@ -467,10 +467,26 @@ func (vcd *TestVCD) TestGetVdcCapabilities(check *C) { func (vcd *TestVCD) TestVdcIsNsxt(check *C) { skipNoNsxtConfiguration(vcd, check) check.Assert(vcd.nsxtVdc.IsNsxt(), Equals, true) + if vcd.vdc != nil { + check.Assert(vcd.vdc.IsNsxt(), Equals, false) + } } func (vcd *TestVCD) TestVdcIsNsxv(check *C) { check.Assert(vcd.vdc.IsNsxv(), Equals, true) + // retrieve the same VDC as AdminVdc, to test the corresponding function + adminOrg, err := vcd.client.GetAdminOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + adminVdc, err := adminOrg.GetVDCByName(vcd.vdc.Vdc.Name, false) + check.Assert(err, IsNil) + check.Assert(adminVdc.IsNsxv(), Equals, true) + // if NSX-T is configured, we also check a NSX-T VDC + if vcd.nsxtVdc != nil { + check.Assert(vcd.nsxtVdc.IsNsxv(), Equals, false) + nsxtAdminVdc, err := adminOrg.GetAdminVDCByName(vcd.config.VCD.Nsxt.Vdc, false) + check.Assert(err, IsNil) + check.Assert(nsxtAdminVdc.IsNsxv(), Equals, false) + } } func (vcd *TestVCD) TestCreateRawVapp(check *C) { diff --git a/types/v56/constants.go b/types/v56/constants.go index d21826e07..47b23ed21 100644 --- a/types/v56/constants.go +++ b/types/v56/constants.go @@ -529,3 +529,54 @@ const ( // DistributedFirewallPolicyDefault is a constant for "default" Distributed Firewall Policy DistributedFirewallPolicyDefault = "default" ) + +// NSX-V distributed firewall + +// Protocols +const ( + DFWProtocolTcp = "TCP" + DFWProtocolUdp = "UDP" + DFWProtocolIcmp = "ICMP" +) + +// Action types +const ( + DFWActionAllow = "allow" + DFWActionDeny = "deny" +) + +// Directions +const ( + DFWDirectionIn = "in" + DFWDirectionOut = "out" + DFWDirectionInout = "inout" +) + +// Types of packet +const ( + DFWPacketAny = "any" + DFWPacketIpv4 = "ipv4" + DFWPacketIpv6 = "ipv6" +) + +// Elements of Source, Destination, and Applies-To +const ( + DFWElementVdc = "VDC" + DFWElementVirtualMachine = "VirtualMachine" + DFWElementNetwork = "Network" + DFWElementEdge = "Edge" + DFWElementIpSet = "IPSet" + DFWElementIpv4 = "Ipv4Address" +) + +// Types of service +const ( + DFWServiceTypeApplication = "Application" + DFWServiceTypeApplicationGroup = "ApplicationGroup" +) + +var NsxvProtocolCodes = map[string]int{ + DFWProtocolTcp: 6, + DFWProtocolUdp: 17, + DFWProtocolIcmp: 1, +} diff --git a/types/v56/network.go b/types/v56/network.go new file mode 100644 index 000000000..073dccb91 --- /dev/null +++ b/types/v56/network.go @@ -0,0 +1,192 @@ +package types + +import "encoding/xml" + +// NOTE: The types in this file were created using goxmlstruct +// (https://github.com/twpayne/go-xmlstruct) + +// ----------------------------------- +// type FirewallConfiguration +// ----------------------------------- + +type FirewallConfiguration struct { + ContextID string `xml:"contextId"` + Layer3Sections *Layer3Sections `xml:"layer3Sections"` + Layer2Sections *Layer2Sections `xml:"layer2Sections"` +} + +type Layer2Sections struct { + Section *FirewallSection `xml:"section"` +} + +type Layer3Sections struct { + Section *FirewallSection `xml:"section"` +} + +type FirewallSection struct { + XMLName xml.Name `xml:"section"` + GenerationNumber string `xml:"generationNumber,attr"` + ID *int `xml:"id,attr"` + Name string `xml:"name,attr"` + Stateless bool `xml:"stateless,attr"` + TcpStrict bool `xml:"tcpStrict,attr"` + Timestamp int `xml:"timestamp,attr"` + Type string `xml:"type,attr"` + UseSid bool `xml:"useSid,attr"` + Rule []NsxvDistributedFirewallRule `xml:"rule"` +} + +type NsxvDistributedFirewallRule struct { + Disabled bool `xml:"disabled,attr"` + ID *int `xml:"id,attr"` + Logged bool `xml:"logged,attr"` + Name string `xml:"name"` + Action string `xml:"action"` // allow, deny + AppliedToList *AppliedToList `xml:"appliedToList"` + SectionID *int `xml:"sectionId"` + Sources *Sources `xml:"sources"` + Destinations *Destinations `xml:"destinations"` + Services *Services `xml:"services"` + Direction string `xml:"direction"` // in, out, inout + PacketType string `xml:"packetType"` + Tag string `xml:"tag"` +} + +type AppliedToList struct { + AppliedTo []AppliedTo `xml:"appliedTo"` +} + +type AppliedTo struct { + Name string `xml:"name"` + Value string `xml:"value"` + Type string `xml:"type"` + IsValid bool `xml:"isValid"` +} + +type Sources struct { + Excluded bool `xml:"excluded,attr"` + Source []Source `xml:"source"` +} + +type Source struct { + Name string `xml:"name"` + Value string `xml:"value"` + Type string `xml:"type"` + IsValid bool `xml:"isValid"` +} + +type Destinations struct { + Excluded bool `xml:"excluded,attr"` + Destination []Destination `xml:"destination"` +} + +type Destination struct { + Name string `xml:"name"` + Value string `xml:"value"` + Type string `xml:"type"` + IsValid bool `xml:"isValid"` +} + +type Services struct { + Service []Service `xml:"service"` +} + +type Service struct { + IsValid bool `xml:"isValid"` + SourcePort *string `xml:"sourcePort"` + DestinationPort *string `xml:"destinationPort"` + Protocol *int `xml:"protocol"` + ProtocolName *string `xml:"protocolName"` + Name string `xml:"name,omitempty"` + Value string `xml:"value,omitempty"` + Type string `xml:"type,omitempty"` +} + +// ----------------------------------- +// type Service +// ----------------------------------- + +type ApplicationList struct { + Application []Application `xml:"application"` +} + +type Application struct { + ObjectID string `xml:"objectId"` + ObjectTypeName string `xml:"objectTypeName"` + VsmUuid string `xml:"vsmUuid"` + NodeID string `xml:"nodeId"` + Revision string `xml:"revision"` + Type ApplicationType `xml:"type"` + Name string `xml:"name"` + Scope Scope `xml:"scope"` + ClientHandle struct{} `xml:"clientHandle"` + ExtendedAttributes struct{} `xml:"extendedAttributes"` + IsUniversal bool `xml:"isUniversal"` + UniversalRevision string `xml:"universalRevision"` + IsTemporal bool `xml:"isTemporal"` + InheritanceAllowed bool `xml:"inheritanceAllowed"` + Element Element `xml:"element"` + Layer string `xml:"layer"` + IsReadOnly bool `xml:"isReadOnly"` + Description *string `xml:"description"` +} + +type ApplicationType struct { + TypeName string `xml:"typeName"` +} + +type Scope struct { + ID string `xml:"id"` + ObjectTypeName string `xml:"objectTypeName"` + Name string `xml:"name"` +} + +type Element struct { + ApplicationProtocol *string `xml:"applicationProtocol"` + Value *string `xml:"value"` + SourcePort *string `xml:"sourcePort"` + AppGuidName *string `xml:"appGuidName"` +} + +// ----------------------------------- +// type ServiceGroup +// ----------------------------------- + +type ApplicationGroupList struct { + ApplicationGroup []ApplicationGroup `xml:"applicationGroup"` +} + +type ApplicationGroup struct { + ObjectID string `xml:"objectId"` + ObjectTypeName string `xml:"objectTypeName"` + VsmUuid string `xml:"vsmUuid"` + NodeID string `xml:"nodeId"` + Revision string `xml:"revision"` + Type ApplicationType `xml:"type"` + Name string `xml:"name"` + Scope Scope `xml:"scope"` + ClientHandle struct{} `xml:"clientHandle"` + ExtendedAttributes struct{} `xml:"extendedAttributes"` + IsUniversal bool `xml:"isUniversal"` + UniversalRevision string `xml:"universalRevision"` + IsTemporal bool `xml:"isTemporal"` + InheritanceAllowed bool `xml:"inheritanceAllowed"` + IsReadOnly bool `xml:"isReadOnly"` + Member []Member `xml:"member"` +} + +type Member struct { + ObjectID string `xml:"objectId"` + ObjectTypeName string `xml:"objectTypeName"` + VsmUuid string `xml:"vsmUuid"` + NodeID string `xml:"nodeId"` + Revision string `xml:"revision"` + Type ApplicationType `xml:"type"` + Name string `xml:"name"` + Scope Scope `xml:"scope"` + ClientHandle struct{} `xml:"clientHandle"` + ExtendedAttributes struct{} `xml:"extendedAttributes"` + IsUniversal bool `xml:"isUniversal"` + UniversalRevision bool `xml:"universalRevision"` + IsTemporal bool `xml:"isTemporal"` +}