Skip to content

Commit

Permalink
Add support for standalone Distributed Firewall Rule management (#587)
Browse files Browse the repository at this point in the history
  • Loading branch information
Didainius authored Jul 14, 2023
1 parent c7f2584 commit a329d75
Show file tree
Hide file tree
Showing 3 changed files with 487 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .changes/v2.21.0/587-features.md
Original file line number Diff line number Diff line change
@@ -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]
337 changes: 337 additions & 0 deletions govcd/nsxt_distributed_firewall.go
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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"`
}
Loading

0 comments on commit a329d75

Please sign in to comment.