diff --git a/.changes/v2.21.0/587-features.md b/.changes/v2.21.0/587-features.md new file mode 100644 index 000000000..c8ea7650f --- /dev/null +++ b/.changes/v2.21.0/587-features.md @@ -0,0 +1,3 @@ +* Added types and methods `DistributedFirewallRule`, `VdcGroup.CreateDistributedFirewallRule`, + `DistributedFirewallRule.Update`, `.DistributedFirewallRuleDelete` to manage NSX-T Distributed + Firewall Rules one by one (opposed to managing all at once using `DistributedFirewall`) [GH-587] diff --git a/govcd/nsxt_distributed_firewall.go b/govcd/nsxt_distributed_firewall.go index 731b1bed0..1d985c9a3 100644 --- a/govcd/nsxt_distributed_firewall.go +++ b/govcd/nsxt_distributed_firewall.go @@ -1,10 +1,13 @@ package govcd import ( + "encoding/json" "errors" "fmt" + "strings" "github.com/vmware/go-vcloud-director/v2/types/v56" + "github.com/vmware/go-vcloud-director/v2/util" ) // DistributedFirewall contains a types.DistributedFirewallRules which handles Distributed Firewall @@ -15,6 +18,13 @@ type DistributedFirewall struct { VdcGroup *VdcGroup } +// DistributedFirewallRule is a representation of a single rule +type DistributedFirewallRule struct { + Rule *types.DistributedFirewallRule + client *Client + VdcGroup *VdcGroup +} + // GetDistributedFirewall retrieves Distributed Firewall in a VDC Group which contains all rules // // Note. This function works only with `default` policy as this was the only supported when this @@ -99,3 +109,330 @@ func (firewall *DistributedFirewall) DeleteAllRules() error { return firewall.VdcGroup.DeleteAllDistributedFirewallRules() } + +// GetDistributedFirewallRuleById retrieves single Distributed Firewall Rule by ID +func (vdcGroup *VdcGroup) GetDistributedFirewallRuleById(id string) (*DistributedFirewallRule, error) { + if id == "" { + return nil, fmt.Errorf("id must be specified") + } + + client := vdcGroup.client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVdcGroupsDfwRules + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + // "default" policy is hardcoded because there is no other policy supported + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, vdcGroup.VdcGroup.Id, types.DistributedFirewallPolicyDefault), "/", id) + if err != nil { + return nil, err + } + + returnObject := &DistributedFirewallRule{ + Rule: &types.DistributedFirewallRule{}, + client: client, + VdcGroup: vdcGroup, + } + + err = client.OpenApiGetItem(apiVersion, urlRef, nil, returnObject.Rule, nil) + if err != nil { + return nil, fmt.Errorf("error retrieving Distributed Firewall rule: %s", err) + } + + return returnObject, nil +} + +// GetDistributedFirewallRuleByName retrieves single firewall rule by name +func (vdcGroup *VdcGroup) GetDistributedFirewallRuleByName(name string) (*DistributedFirewallRule, error) { + if name == "" { + return nil, fmt.Errorf("name must be specified") + } + + dfw, err := vdcGroup.GetDistributedFirewall() + if err != nil { + return nil, fmt.Errorf("error returning distributed firewall rules: %s", err) + } + + var filteredByName []*types.DistributedFirewallRule + for _, rule := range dfw.DistributedFirewallRuleContainer.Values { + if rule.Name == name { + filteredByName = append(filteredByName, rule) + } + } + + oneByName, err := oneOrError("name", name, filteredByName) + if err != nil { + return nil, err + } + + return vdcGroup.GetDistributedFirewallRuleById(oneByName.ID) +} + +// CreateDistributedFirewallRule is a non-thread safe wrapper around +// "vdcGroups/%s/dfwPolicies/%s/rules" endpoint which handles all distributed firewall (DFW) rules +// at once. While there is no real endpoint to create single firewall rule, it is a requirements for +// some cases (e.g. using in Terraform) +// The code works by doing the following steps: +// +// 1. Getting all Distributed Firewall Rules and storing them in private intermediate +// type`distributedFirewallRulesRaw` which holds a []json.RawMessage (text) instead of exact types. +// This will prevent altering existing rules in any way (for example if a new field appears in +// schema in future VCD versions) +// +// 2. Converting the given `rule` into json.RawMessage so that it is provided in the same format as +// other already retrieved rules +// +// 3. Creating a new structure of []json.RawMessage which puts the new rule into one of places: +// 3.1. to the end of []json.RawMessage - bottom of the list +// 3.2. if `optionalAboveRuleId` argument is specified - identifying the position and placing new +// rule above it +// 4. Perform a PUT (update) call to the "vdcGroups/%s/dfwPolicies/%s/rules" endpoint using the +// newly constructed payload +// +// Note. Running this function concurrently will corrupt firewall rules as it uses an endpoint that +// manages all rules ("vdcGroups/%s/dfwPolicies/%s/rules") +func (vdcGroup *VdcGroup) CreateDistributedFirewallRule(optionalAboveRuleId string, rule *types.DistributedFirewallRule) (*DistributedFirewall, *DistributedFirewallRule, error) { + client := vdcGroup.client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVdcGroupsDfwRules + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, nil, err + } + + // "default" policy is hardcoded because there is no other policy supported + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, vdcGroup.VdcGroup.Id, types.DistributedFirewallPolicyDefault)) + if err != nil { + return nil, nil, err + } + + // 1. Getting all Distributed Firewall Rules and storing them in private intermediate + // type`distributedFirewallRulesRaw` which holds a []json.RawMessage (text) instead of exact types. + // This will prevent altering existing rules in any way (for example if a new field appears in + // schema in future VCD versions) + rawJsonExistingFirewallRules := &distributedFirewallRulesRaw{} + err = client.OpenApiGetItem(apiVersion, urlRef, nil, rawJsonExistingFirewallRules, nil) + if err != nil { + return nil, nil, fmt.Errorf("error retrieving Distributed Firewall rules in raw format: %s", err) + } + + // 2. Converting the give `rule` (*types.DistributedFirewallRule) into json.RawMessage so that + // it is provided in the same format as other already retrieved rules + newRuleRawJson, err := firewallRuleToRawJson(rule) + if err != nil { + return nil, nil, err + } + + // dfwRuleUpdatePayload will contain complete request for Distributed Firewall Rule Update + // operation. Its content will be decided based on whether 'optionalAboveRuleId' parameter was + // specified or not. + var dfwRuleUpdatePayload []json.RawMessage + // newRuleSlicePosition will contain slice index to where new firewall rule will be put + var newRuleSlicePosition int + + // 3. Creating a new structure of []json.RawMessage which puts the new rule into one of places: + switch { + // 3.1. to the end of []json.RawMessage - bottom of the list (optionalAboveRuleId is empty) + case optionalAboveRuleId == "": + rawJsonExistingFirewallRules.Values = append(rawJsonExistingFirewallRules.Values, newRuleRawJson) + dfwRuleUpdatePayload = rawJsonExistingFirewallRules.Values + newRuleSlicePosition = len(dfwRuleUpdatePayload) - 1 // -1 to match for slice index + + // 3.2. if `optionalAboveRuleId` argument is specified - identifying the position and placing new + // rule above it + case optionalAboveRuleId != "": + // 3.2.1 Convert '[]json.Rawmessage' to 'types.DistributedFirewallRules' + dfwRules, err := convertRawJsonToFirewallRules(rawJsonExistingFirewallRules) + if err != nil { + return nil, nil, err + } + // 3.2.2 Find index for specified 'optionalAboveRuleId' rule + newFwRuleSliceIndex, err := getFirewallRuleIndexById(dfwRules, optionalAboveRuleId) + if err != nil { + return nil, nil, err + } + + // 3.2.3 Compose new update (PUT) payload with all firewall rules and inject + // 'newRuleRawJson' into position 'newFwRuleSliceIndex' and shift other rules to the bottom + dfwRuleUpdatePayload, err = composeUpdatePayloadWithNewRulePosition(newFwRuleSliceIndex, rawJsonExistingFirewallRules, newRuleRawJson) + if err != nil { + return nil, nil, fmt.Errorf("error creating update payload with optionalAboveRuleId '%s' :%s", optionalAboveRuleId, err) + } + } + // 4. Perform a PUT (update) call to the "vdcGroups/%s/dfwPolicies/%s/rules" endpoint using the + // newly constructed payload + updateRequestPayload := &distributedFirewallRulesRaw{ + Values: dfwRuleUpdatePayload, + } + + returnAllFirewallRules := &DistributedFirewall{ + DistributedFirewallRuleContainer: &types.DistributedFirewallRules{}, + client: client, + VdcGroup: vdcGroup, + } + + err = client.OpenApiPutItem(apiVersion, urlRef, nil, updateRequestPayload, returnAllFirewallRules.DistributedFirewallRuleContainer, nil) + if err != nil { + return nil, nil, fmt.Errorf("error updating Distributed Firewall rules: %s", err) + } + + // Create an entity for single firewall rule (which can be updated and deleted using their own endpoints) + returnObjectSingleRule := &DistributedFirewallRule{ + Rule: returnAllFirewallRules.DistributedFirewallRuleContainer.Values[newRuleSlicePosition], + client: client, + VdcGroup: vdcGroup, + } + + return returnAllFirewallRules, returnObjectSingleRule, nil +} + +// Update a single Distributed Firewall Rule +func (dfwRule *DistributedFirewallRule) Update(rule *types.DistributedFirewallRule) (*DistributedFirewallRule, error) { + if dfwRule.Rule.ID == "" { + return nil, fmt.Errorf("cannot update NSX-T Distribute Firewall Rule without ID") + } + + client := dfwRule.client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVdcGroupsDfwRules + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + // "default" policy is hardcoded because there is no other policy supported + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, dfwRule.VdcGroup.VdcGroup.Id, types.DistributedFirewallPolicyDefault), "/", dfwRule.Rule.ID) + if err != nil { + return nil, err + } + + returnObjectSingleRule := &DistributedFirewallRule{ + Rule: &types.DistributedFirewallRule{}, + client: client, + VdcGroup: dfwRule.VdcGroup, + } + + rule.ID = dfwRule.Rule.ID + err = client.OpenApiPutItem(apiVersion, urlRef, nil, rule, returnObjectSingleRule.Rule, nil) + if err != nil { + return nil, fmt.Errorf("error updating Distributed Firewall rules: %s", err) + } + + return returnObjectSingleRule, nil +} + +// Delete a single Distributed Firewall Rule +func (dfwRule *DistributedFirewallRule) Delete() error { + if dfwRule.Rule.ID == "" { + return fmt.Errorf("cannot delete NSX-T Distribute Firewall Rule without ID") + } + + client := dfwRule.client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVdcGroupsDfwRules + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return err + } + + // "default" policy is hardcoded because there is no other policy supported + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, dfwRule.VdcGroup.VdcGroup.Id, types.DistributedFirewallPolicyDefault), "/", dfwRule.Rule.ID) + if err != nil { + return err + } + + err = client.OpenApiDeleteItem(apiVersion, urlRef, nil, nil) + if err != nil { + return fmt.Errorf("error deleting NSX-T Distribute Firewall Rule with ID '%s': %s", dfwRule.Rule.ID, err) + } + + return nil +} + +// getFirewallRuleIndexById searches for 'firewallRuleId' going through a list of available firewall +// rules and returns its index or error if the firewall rule is not found +func getFirewallRuleIndexById(dfwRules *types.DistributedFirewallRules, firewallRuleId string) (int, error) { + util.Logger.Printf("[DEBUG] CreateDistributedFirewallRule 'optionalAboveRuleId=%s'. Searching within '%d' items", + firewallRuleId, len(dfwRules.Values)) + var fwRuleSliceIndex *int + for index := range dfwRules.Values { + if dfwRules.Values[index].ID == firewallRuleId { + // using function `addrOf` to get copy of `index` value as taking a direct address + // of `&index` will shift before it is used in later code due to how Go range works + fwRuleSliceIndex = addrOf(index) + util.Logger.Printf("[DEBUG] CreateDistributedFirewallRule found existing Firewall Rule with ID '%s' at position '%d'", + firewallRuleId, index) + continue + } + } + + if fwRuleSliceIndex == nil { + return 0, fmt.Errorf("specified above rule ID '%s' does not exist in current Distributed Firewall Rule list", firewallRuleId) + } + + return *fwRuleSliceIndex, nil +} + +// firewallRuleToRawJson Marshal a single `types.DistributedFirewallRule` into `json.RawMessage` +// representation +func firewallRuleToRawJson(rule *types.DistributedFirewallRule) (json.RawMessage, error) { + ruleByteSlice, err := json.Marshal(rule) + if err != nil { + return nil, fmt.Errorf("error marshalling 'rule': %s", err) + } + ruleJsonMessage := json.RawMessage(string(ruleByteSlice)) + return ruleJsonMessage, nil +} + +// convertRawJsonToFirewallRules converts []json.RawMessage to +// types.DistributedFirewallRules.Values so that entries can be filtered by ID or other fields. +// Note. Slice order remains the same +func convertRawJsonToFirewallRules(rawBodyStructure *distributedFirewallRulesRaw) (*types.DistributedFirewallRules, error) { + var rawJsonBodies []string + for _, singleObject := range rawBodyStructure.Values { + rawJsonBodies = append(rawJsonBodies, string(singleObject)) + } + // rawJsonBodies contains a slice of all response objects and they must be formatted as a JSON slice (wrapped + // into `[]`, separated with semicolons) so that unmarshalling to specified `outType` works in one go + allResponses := `[` + strings.Join(rawJsonBodies, ",") + `]` + + // Convert the retrieved []json.RawMessage to *types.DistributedFirewallRules.Values so that IDs can be searched for + // Note. The main goal here is to have 2 slices - one with []json.RawMessage and other + // []*DistributedFirewallRule. One can look for IDs and capture firewall rule index + dfwRules := &types.DistributedFirewallRules{} + // Unmarshal all accumulated responses into `dfwRules` + if err := json.Unmarshal([]byte(allResponses), &dfwRules.Values); err != nil { + return nil, fmt.Errorf("error decoding values into type types.DistributedFirewallRules: %s", err) + } + + return dfwRules, nil +} + +// composeUpdatePayloadWithNewRulePosition takes a slice of existing firewall rules and injects new +// firewall rule at a given position `newRuleSlicePosition` +func composeUpdatePayloadWithNewRulePosition(newRuleSlicePosition int, rawBodyStructure *distributedFirewallRulesRaw, newRuleJsonMessage json.RawMessage) ([]json.RawMessage, error) { + // Create a new slice with additional capacity of 1 to add new firewall rule into existing list + newFwRuleSlice := make([]json.RawMessage, len(rawBodyStructure.Values)+1) + util.Logger.Printf("[DEBUG] CreateDistributedFirewallRule new container slice of size '%d' with previous element count '%d'", len(newFwRuleSlice), len(rawBodyStructure.Values)) + // if newRulePosition is not 0 (at the top), then previous rules need to be copied to the beginning of new slice + if newRuleSlicePosition != 0 { + util.Logger.Printf("[DEBUG] CreateDistributedFirewallRule copying first '%d' slice [:%d]", newRuleSlicePosition, newRuleSlicePosition) + copy(newFwRuleSlice[:newRuleSlicePosition], rawBodyStructure.Values[:newRuleSlicePosition]) + } + + // Insert the new element at specified index + util.Logger.Printf("[DEBUG] CreateDistributedFirewallRule inserting new element into position %d", newRuleSlicePosition) + newFwRuleSlice[newRuleSlicePosition] = newRuleJsonMessage + + // Copy the remaining elements after new rule + copy(newFwRuleSlice[newRuleSlicePosition+1:], rawBodyStructure.Values[newRuleSlicePosition:]) + util.Logger.Printf("[DEBUG] CreateDistributedFirewallRule copying remaining items '%d'", newRuleSlicePosition) + + return newFwRuleSlice, nil +} + +// distributedFirewallRulesRaw is a copy of `types.DistributedFirewallRules` so that values can be +// unmarshalled into json.RawMessage (as strings) instead of exact types `DistributedFirewallRule` +// It has Public field Values so that marshalling can work, but is not exported itself as it is only +// an intermediate type used in `VdcGroup.CreateDistributedFirewallRule` +type distributedFirewallRulesRaw struct { + Values []json.RawMessage `json:"values"` +} diff --git a/govcd/nsxt_distributed_firewall_test.go b/govcd/nsxt_distributed_firewall_test.go index ec2fdb6a7..f86cdf9dc 100644 --- a/govcd/nsxt_distributed_firewall_test.go +++ b/govcd/nsxt_distributed_firewall_test.go @@ -283,3 +283,150 @@ func dumpDistributedFirewallRulesToScreen(rules []*types.DistributedFirewallRule util.Logger.Printf("Error while dumping Distributed Firewall rules to screen: %s", err) } } + +// Test_NsxtDistributedFirewallRule tests the capability of managing Firewall Rules one by one using +// `DistributedFirewallRule` type. +func (vcd *TestVCD) Test_NsxtDistributedFirewallRule(check *C) { + if vcd.skipAdminTests { + check.Skip(fmt.Sprintf(TestRequiresSysAdminPrivileges, check.TestName())) + } + skipNoNsxtConfiguration(vcd, check) + skipOpenApiEndpointTest(vcd, check, types.OpenApiPathVersion1_0_0+types.OpenApiEndpointEdgeGateways) + + adminOrg, err := vcd.client.GetAdminOrgByName(vcd.config.VCD.Org) + check.Assert(adminOrg, NotNil) + check.Assert(err, IsNil) + + nsxtExternalNetwork, err := GetExternalNetworkV2ByName(vcd.client, vcd.config.VCD.Nsxt.ExternalNetwork) + check.Assert(nsxtExternalNetwork, NotNil) + check.Assert(err, IsNil) + + vdc, vdcGroup := test_CreateVdcGroup(check, adminOrg, vcd) + check.Assert(vdc, NotNil) + check.Assert(vdcGroup, NotNil) + + defer func() { + // Cleanup + err = vdcGroup.Delete() + check.Assert(err, IsNil) + err = vdc.DeleteWait(true, true) + check.Assert(err, IsNil) + }() + + fmt.Println("# Running Distributed Firewall tests for single Rule") + test_NsxtDistributedFirewallRule(vcd, check, vdcGroup.VdcGroup.Id, vcd.client, vdc) +} + +func test_NsxtDistributedFirewallRule(vcd *TestVCD, check *C, vdcGroupId string, vcdClient *VCDClient, vdc *Vdc) { + adminOrg, err := vcdClient.GetAdminOrgByName(vcd.config.VCD.Org) + check.Assert(adminOrg, NotNil) + check.Assert(err, IsNil) + + vdcGroup, err := adminOrg.GetVdcGroupById(vdcGroupId) + check.Assert(err, IsNil) + + _, err = vdcGroup.ActivateDfw() + check.Assert(err, IsNil) + + // Prep firewall rule sample to operate with + randomizedFwRuleDefs, ipSet, secGroup := createDistributedFirewallDefinitions(check, vcd, vdcGroup.VdcGroup.Id, vcdClient, vdc) + // defer cleanup function in case something goes wrong + defer func() { + dfw, err := vdcGroup.GetDistributedFirewall() + check.Assert(err, IsNil) + err = dfw.DeleteAllRules() + check.Assert(err, IsNil) + _, err = vdcGroup.DisableDefaultPolicy() + check.Assert(err, IsNil) + err = ipSet.Delete() + check.Assert(err, IsNil) + err = secGroup.Delete() + check.Assert(err, IsNil) + }() + + randomizedFwRuleSubSet := randomizedFwRuleDefs[0:5] // taking only first 5 rules to limit time of testing + + // removing default firewall rule which is created by VCD when vdcGroup.ActivateDfw() is executed + err = vdcGroup.DeleteAllDistributedFirewallRules() + check.Assert(err, IsNil) + + // Adding firewal rules one by one and checking that each of them is + testDistributedFirewallRuleSequence(vcd, check, randomizedFwRuleSubSet, vdcGroup, false) + testDistributedFirewallRuleSequence(vcd, check, randomizedFwRuleSubSet, vdcGroup, true) +} + +// testDistributedFirewallRuleSequence tests the following: +// * create firewall rules one one by one +// * check that the order of firewall rules is the same as requested (or exactly reverse if +// reverseOrder=true) +// * check that all IDs of created firewall rules persisted during further updates (means that no +// firewall rules were recreated during addition of new ones) +func testDistributedFirewallRuleSequence(vcd *TestVCD, check *C, randomizedFwRuleSubSet []*types.DistributedFirewallRule, vdcGroup *VdcGroup, reverseOrder bool) { + createdIdsFound := make(map[string]bool) + fmt.Printf("# Creating '%d' rules one by one (reverseOrder: %t)\n", len(randomizedFwRuleSubSet), reverseOrder) + previousRuleId := "" + for _, rule := range randomizedFwRuleSubSet { + if testVerbose { + fmt.Printf("%s\t%s\t%s\t%t\t%s\t%t\t%d\t%d\t%d\t%d\n", rule.Name, rule.Direction, rule.IpProtocol, + rule.Enabled, rule.Action, rule.Logging, len(rule.SourceFirewallGroups), len(rule.DestinationFirewallGroups), len(rule.ApplicationPortProfiles), len(rule.NetworkContextProfiles)) + } + + completeDfw, singleCreatedFwRule, err := vdcGroup.CreateDistributedFirewallRule(previousRuleId, rule) + check.Assert(err, IsNil) + check.Assert(completeDfw, NotNil) + check.Assert(singleCreatedFwRule, NotNil) + createdIdsFound[singleCreatedFwRule.Rule.ID] = false + + // caching ID to use as previous rule in case + if reverseOrder { + previousRuleId = singleCreatedFwRule.Rule.ID + } + } + fmt.Printf("# Done creating '%d' rules one by one (reverseOrder: %t)\n", len(randomizedFwRuleSubSet), reverseOrder) + + // Retrieve all firewall rules and check that order matches + allRules, err := vdcGroup.GetDistributedFirewall() + check.Assert(err, IsNil) + check.Assert(len(allRules.DistributedFirewallRuleContainer.Values), Equals, len(randomizedFwRuleSubSet)) + + // check that rule order is exactly as expected (either reverse of randomizedFwRuleSubSet or exactly the same based on reverseOrder parameter) + if reverseOrder { + for ruleIndex, rule := range allRules.DistributedFirewallRuleContainer.Values { + reverseRuleIndex := len(randomizedFwRuleSubSet) - ruleIndex - 1 + check.Assert(rule.Name, Equals, randomizedFwRuleSubSet[reverseRuleIndex].Name) + createdIdsFound[rule.ID] = true + } + } else { + for ruleIndex, rule := range allRules.DistributedFirewallRuleContainer.Values { + check.Assert(rule.Name, Equals, randomizedFwRuleSubSet[ruleIndex].Name) + createdIdsFound[rule.ID] = true + } + } + + // Check that all created IDs are in the final output (none of the firewall rules were recreated) + for _, value := range createdIdsFound { + check.Assert(value, Equals, true) + } + + // Perform Update + ruleById, err := vdcGroup.GetDistributedFirewallRuleById(allRules.DistributedFirewallRuleContainer.Values[0].ID) + check.Assert(err, IsNil) + + updatedRuleName := check.TestName() + "-updated" + ruleById.Rule.Name = updatedRuleName + updatedRule, err := ruleById.Update(ruleById.Rule) + check.Assert(err, IsNil) + check.Assert(updatedRule.Rule.Name, Equals, updatedRuleName) + + // Delete + err = updatedRule.Delete() + check.Assert(err, IsNil) + + notFoundById, err := vdcGroup.GetDistributedFirewallRuleById(updatedRule.Rule.ID) + check.Assert(err, NotNil) + check.Assert(notFoundById, IsNil) + + // Clean up created firewall rules for next phase + err = vdcGroup.DeleteAllDistributedFirewallRules() + check.Assert(err, IsNil) +}