0
0
mirror of https://github.com/bpg/terraform-provider-proxmox.git synced 2025-06-30 02:31:10 +00:00

fix(firewall): fw controls bugfixes (#287)

fix(firewall): fw controls bugfixes

- fix alias examples in docs
- docs & examples improvements
- add support for security group insertion

Release-As: 0.17.0-rc2
This commit is contained in:
Pavel Boldyrev 2023-04-03 20:42:35 -04:00 committed by GitHub
parent fc08e19f86
commit 1bfc29e2cc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 197 additions and 90 deletions

View File

@ -65,9 +65,10 @@ resource "proxmox_virtual_environment_cluster_firewall_security_group" "webserve
rules can use arbitrary strings. rules can use arbitrary strings.
- `log` - (Optional) Log level for this rule (`emerg`, `alert`, `crit`, - `log` - (Optional) Log level for this rule (`emerg`, `alert`, `crit`,
`err`, `warning`, `notice`, `info`, `debug`, `nolog`). `err`, `warning`, `notice`, `info`, `debug`, `nolog`).
- `macro`- (Optional) Macro name. Use predefined standard macro. - `macro`- (Optional) Macro name. Use predefined standard macro
from https://pve.proxmox.com/pve-docs/pve-admin-guide.html#_firewall_macro_definitions
- `proto` - (Optional) Restrict packet protocol. You can use protocol names - `proto` - (Optional) Restrict packet protocol. You can use protocol names
or simple numbers (0-255), as defined in '/etc/protocols'. as defined in '/etc/protocols'.
- `source` - (Optional) Restrict packet source address. This can refer - `source` - (Optional) Restrict packet source address. This can refer
to a single IP address, an IP set ('+ipsetname') or an IP alias to a single IP address, an IP set ('+ipsetname') or an IP alias
definition. You can also specify an address range like definition. You can also specify an address range like
@ -82,6 +83,6 @@ resource "proxmox_virtual_environment_cluster_firewall_security_group" "webserve
## Attribute Reference ## Attribute Reference
- `rule` - `rule`
- `pos` - Position of the rule in the list. - `pos` - Position of the rule in the list.
There are no attribute references available for this resource. There are no attribute references available for this resource.

View File

@ -1,13 +1,13 @@
--- ---
layout: page layout: page
title: proxmox_virtual_environment_cluster_firewall_alias title: proxmox_virtual_environment_firewall_alias
permalink: /resources/virtual_environment_cluster_firewall_alias permalink: /resources/virtual_environment_firewall_alias
nav_order: 7 nav_order: 7
parent: Resources parent: Resources
subcategory: Virtual Environment subcategory: Virtual Environment
--- ---
# Resource: proxmox_virtual_environment_cluster_firewall_alias # Resource: proxmox_virtual_environment_firewall_alias
Aliases are used to see what devices or group of devices are affected by a rule. Aliases are used to see what devices or group of devices are affected by a rule.
We can create aliases to identify an IP address or a network. Aliases can be We can create aliases to identify an IP address or a network. Aliases can be
@ -16,7 +16,7 @@ created on the cluster level, on VM / Container level.
## Example Usage ## Example Usage
```terraform ```terraform
resource "proxmox_virtual_environment_cluster_firewall_alias" "local_network" { resource "proxmox_virtual_environment_firewall_alias" "local_network" {
depends_on = [proxmox_virtual_environment_vm.example] depends_on = [proxmox_virtual_environment_vm.example]
node_name = proxmox_virtual_environment_vm.example.node_name node_name = proxmox_virtual_environment_vm.example.node_name
@ -27,7 +27,7 @@ resource "proxmox_virtual_environment_cluster_firewall_alias" "local_network" {
comment = "Managed by Terraform" comment = "Managed by Terraform"
} }
resource "proxmox_virtual_environment_cluster_firewall_alias" "ubuntu_vm" { resource "proxmox_virtual_environment_firewall_alias" "ubuntu_vm" {
name = "ubuntu" name = "ubuntu"
cidr = "192.168.0.1" cidr = "192.168.0.1"
comment = "Managed by Terraform" comment = "Managed by Terraform"

View File

@ -15,6 +15,11 @@ Manages firewall options on VM / Container level.
```terraform ```terraform
resource "proxmox_virtual_environment_firewall_options" "example" { resource "proxmox_virtual_environment_firewall_options" "example" {
depends_on = [proxmox_virtual_environment_vm.example]
node_name = proxmox_virtual_environment_vm.example.node_name
vm_id = proxmox_virtual_environment_vm.example.vm_id
enabled = false enabled = false
dhcp = true dhcp = true

View File

@ -18,11 +18,14 @@ level, on VM / Container level.
```terraform ```terraform
resource "proxmox_virtual_environment_firewall_rules" "inbound" { resource "proxmox_virtual_environment_firewall_rules" "inbound" {
depends_on = [proxmox_virtual_environment_vm.example] depends_on = [
proxmox_virtual_environment_vm.example,
proxmox_virtual_environment_cluster_firewall_security_group.example,
]
node_name = proxmox_virtual_environment_vm.example.node_name node_name = proxmox_virtual_environment_vm.example.node_name
vm_id = proxmox_virtual_environment_vm.example.vm_id vm_id = proxmox_virtual_environment_vm.example.vm_id
rule { rule {
type = "in" type = "in"
action = "ACCEPT" action = "ACCEPT"
@ -42,47 +45,66 @@ resource "proxmox_virtual_environment_firewall_rules" "inbound" {
proto = "tcp" proto = "tcp"
log = "info" log = "info"
} }
rule {
security_group = proxmox_virtual_environment_cluster_firewall_security_group.example.name
comment = "From security group"
iface = "net0"
}
} }
``` ```
## Argument Reference ## Argument Reference
- `node_name` - (Optional) Node name. Leave empty for cluster level aliases. - `node_name` - (Optional) Node name. Leave empty for cluster level rules.
- `vm_id` - (Optional) VM ID. Leave empty for cluster level aliases. - `vm_id` - (Optional) VM ID. Leave empty for cluster level rules.
- `container_id` - (Optional) Container ID. Leave empty for cluster level aliases. - `container_id` - (Optional) Container ID. Leave empty for cluster level
rules.
- `rule` - (Optional) Firewall rule block (multiple blocks supported). - `rule` - (Optional) Firewall rule block (multiple blocks supported).
- `action` - (Required) Rule action (`ACCEPT`, `DROP`, `REJECT`). The provider supports two types of the `rule` blocks:
- `type` - (Required) Rule type (`in`, `out`). - a rule definition block, which includes the following arguments:
- `comment` - (Optional) Rule comment. - `action` - (Required) Rule action (`ACCEPT`, `DROP`, `REJECT`).
- `dest` - (Optional) Restrict packet destination address. This can refer to - `type` - (Required) Rule type (`in`, `out`).
a single IP address, an IP set ('+ipsetname') or an IP alias definition. - `comment` - (Optional) Rule comment.
You can also specify an address range like `20.34.101.207-201.3.9.99`, or - `dest` - (Optional) Restrict packet destination address. This can
a list of IP addresses and networks (entries are separated by comma). refer to a single IP address, an IP set ('+ipsetname') or an IP alias
Please do not mix IPv4 and IPv6 addresses inside such lists. definition. You can also specify an address range
- `dport` - (Optional) Restrict TCP/UDP destination port. You can use like `20.34.101.207-201.3.9.99`, or a list of IP addresses and
service names or simple numbers (0-65535), as defined in '/etc/services'. networks (entries are separated by comma). Please do not mix IPv4 and
Port ranges can be specified with '\d+:\d+', for example `80:85`, and IPv6 addresses inside such lists.
you can use comma separated list to match several ports or ranges. - `dport` - (Optional) Restrict TCP/UDP destination port. You can use
- `enable` - (Optional) Enable this rule. Defaults to `true`. service names or simple numbers (0-65535), as defined
- `iface` - (Optional) Network interface name. You have to use network in `/etc/services`. Port ranges can be specified with '\d+:\d+', for
configuration key names for VMs and containers ('net\d+'). Host related example `80:85`, and you can use comma separated list to match several
rules can use arbitrary strings. ports or ranges.
- `log` - (Optional) Log level for this rule (`emerg`, `alert`, `crit`, - `enabled` - (Optional) Enable this rule. Defaults to `true`.
`err`, `warning`, `notice`, `info`, `debug`, `nolog`). - `iface` - (Optional) Network interface name. You have to use network
- `macro`- (Optional) Macro name. Use predefined standard macro. configuration key names for VMs and containers ('net\d+'). Host
- `proto` - (Optional) Restrict packet protocol. You can use protocol names related rules can use arbitrary strings.
or simple numbers (0-255), as defined in '/etc/protocols'. - `log` - (Optional) Log level for this rule (`emerg`, `alert`, `crit`,
- `source` - (Optional) Restrict packet source address. This can refer `err`, `warning`, `notice`, `info`, `debug`, `nolog`).
to a single IP address, an IP set ('+ipsetname') or an IP alias - `macro`- (Optional) Macro name. Use predefined standard macro
definition. You can also specify an address range like from https://pve.proxmox.com/pve-docs/pve-admin-guide.html#_firewall_macro_definitions
`20.34.101.207-201.3.9.99`, or a list of IP addresses and networks ( - `proto` - (Optional) Restrict packet protocol. You can use protocol
entries names as defined in '/etc/protocols'.
are separated by comma). Please do not mix IPv4 and IPv6 addresses inside - `source` - (Optional) Restrict packet source address. This can refer
such lists. to a single IP address, an IP set ('+ipsetname') or an IP alias
- `sport` - (Optional) Restrict TCP/UDP source port. You can use definition. You can also specify an address range
service names or simple numbers (0-65535), as defined in '/etc/services'. like `20.34.101.207-201.3.9.99`, or a list of IP addresses and
Port ranges can be specified with '\d+:\d+', for example `80:85`, and networks (entries are separated by comma). Please do not mix IPv4 and
you can use comma separated list to match several ports or ranges. IPv6 addresses inside such lists.
- `sport` - (Optional) Restrict TCP/UDP source port. You can use
service names or simple numbers (0-65535), as defined
in `/etc/services`. Port ranges can be specified with '\d+:\d+', for
example `80:85`, and you can use comma separated list to match several
ports or ranges.
- a security group insertion block, which includes the following arguments:
- `comment` - (Optional) Rule comment.
- `enabled` - (Optional) Enable this rule. Defaults to `true`.
- `iface` - (Optional) Network interface name. You have to use network
configuration key names for VMs and containers ('net\d+'). Host
related rules can use arbitrary strings.
- `security_group` - (Required) Security group name.
## Attribute Reference ## Attribute Reference

View File

@ -15,7 +15,7 @@ resource "proxmox_virtual_environment_cluster_firewall_security_group" "example"
rule { rule {
type = "in" type = "in"
action = "DROP" action = "DROP"
comment = "Allow SSH" comment = "Drop SSH"
dest = "192.168.1.5" dest = "192.168.1.5"
dport = "22" dport = "22"
proto = "udp" proto = "udp"

View File

@ -12,7 +12,7 @@ resource "proxmox_virtual_environment_firewall_rules" "cluster_rules" {
rule { rule {
type = "out" type = "out"
action = "DROP" action = "DROP"
comment = "Allow SSH" comment = "Drop SSH"
dest = "192.168.0.5" dest = "192.168.0.5"
dport = "22" dport = "22"
proto = "tcp" proto = "tcp"
@ -20,16 +20,26 @@ resource "proxmox_virtual_environment_firewall_rules" "cluster_rules" {
} }
resource "proxmox_virtual_environment_firewall_rules" "vm_rules" { resource "proxmox_virtual_environment_firewall_rules" "vm_rules" {
depends_on = [proxmox_virtual_environment_vm.example] depends_on = [
proxmox_virtual_environment_vm.example,
proxmox_virtual_environment_cluster_firewall_security_group.example,
]
node_name = proxmox_virtual_environment_vm.example.node_name node_name = proxmox_virtual_environment_vm.example.node_name
vm_id = proxmox_virtual_environment_vm.example.vm_id vm_id = proxmox_virtual_environment_vm.example.vm_id
rule {
security_group = proxmox_virtual_environment_cluster_firewall_security_group.example.name
enabled = true
comment = "From XXX"
iface = "net0"
}
rule { rule {
type = "in" type = "in"
action = "ACCEPT" action = "ACCEPT"
comment = "Allow FTP" comment = "Allow FTP"
dest = "192.168.1.5" dest = "192.168.1.15"
dport = "21" dport = "21"
proto = "tcp" proto = "tcp"
log = "info" log = "info"
@ -38,8 +48,8 @@ resource "proxmox_virtual_environment_firewall_rules" "vm_rules" {
rule { rule {
type = "out" type = "out"
action = "DROP" action = "DROP"
comment = "Allow SSH" comment = "Drop SSH"
dest = "192.168.1.5" dest = "192.168.1.15"
dport = "22" dport = "22"
proto = "tcp" proto = "tcp"
} }
@ -64,7 +74,7 @@ resource "proxmox_virtual_environment_firewall_rules" "container_rules" {
rule { rule {
type = "out" type = "out"
action = "DROP" action = "DROP"
comment = "Allow SSH" comment = "Drop SSH"
dest = "192.168.2.5" dest = "192.168.2.5"
dport = "22" dport = "22"
proto = "tcp" proto = "tcp"

View File

@ -22,24 +22,27 @@ import (
) )
const ( const (
dvRuleComment = "" dvSecurityGroup = ""
dvRuleDPort = "" dvRuleComment = ""
dvRuleDest = "" dvRuleDPort = ""
dvRuleEnable = true dvRuleDest = ""
dvRuleIface = "" dvRuleEnabled = true
dvRuleLog = "nolog" dvRuleIface = ""
dvRuleMacro = "" dvRuleLog = ""
dvRuleProto = "" dvRuleMacro = ""
dvRuleSPort = "" dvRuleProto = ""
dvRuleSource = "" dvRuleSPort = ""
dvRuleSource = ""
MkRule = "rule" MkRule = "rule"
mkSecurityGroup = "security_group"
mkRuleAction = "action" mkRuleAction = "action"
mkRuleComment = "comment" mkRuleComment = "comment"
mkRuleDPort = "dport" mkRuleDPort = "dport"
mkRuleDest = "dest" mkRuleDest = "dest"
mkRuleEnable = "enable" mkRuleEnabled = "enabled"
mkRuleIFace = "iface" mkRuleIFace = "iface"
mkRuleLog = "log" mkRuleLog = "log"
mkRuleMacro = "macro" mkRuleMacro = "macro"
@ -57,16 +60,23 @@ func Rules() *schema.Resource {
Description: "Rules position", Description: "Rules position",
Computed: true, Computed: true,
}, },
mkSecurityGroup: {
Type: schema.TypeString,
Description: "Security group name",
Optional: true,
ForceNew: true,
Default: dvSecurityGroup,
},
mkRuleAction: { mkRuleAction: {
Type: schema.TypeString, Type: schema.TypeString,
Description: "Rules action ('ACCEPT', 'DROP', 'REJECT')", Description: "Rules action ('ACCEPT', 'DROP', 'REJECT')",
Required: true, Optional: true,
ValidateDiagFunc: validator.FirewallPolicy(), ValidateDiagFunc: validator.FirewallPolicy(),
}, },
mkRuleType: { mkRuleType: {
Type: schema.TypeString, Type: schema.TypeString,
Description: "Rules type ('in', 'out')", Description: "Rules type ('in', 'out')",
Required: true, Optional: true,
ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{"in", "out"}, true)), ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{"in", "out"}, true)),
}, },
mkRuleComment: { mkRuleComment: {
@ -92,18 +102,19 @@ func Rules() *schema.Resource {
Optional: true, Optional: true,
Default: dvRuleDPort, Default: dvRuleDPort,
}, },
mkRuleEnable: { mkRuleEnabled: {
Type: schema.TypeBool, Type: schema.TypeBool,
Description: "Enable rule", Description: "Enable rule",
Optional: true, Optional: true,
Default: dvRuleEnable, Default: dvRuleEnabled,
}, },
mkRuleIFace: { mkRuleIFace: {
Type: schema.TypeString, Type: schema.TypeString,
Description: "Network interface name. You have to use network configuration key names for VMs" + Description: "Network interface name. You have to use network configuration key names for VMs" +
" and containers ('net\\d+'). Host related rules can use arbitrary strings.", " and containers ('net\\d+'). Host related rules can use arbitrary strings.",
Optional: true, Optional: true,
Default: dvRuleIface, Default: dvRuleIface,
ValidateDiagFunc: validator.FirewallIFace(),
}, },
mkRuleLog: { mkRuleLog: {
Type: schema.TypeString, Type: schema.TypeString,
@ -171,11 +182,29 @@ func RulesCreate(ctx context.Context, api firewall.Rule, d *schema.ResourceData)
rules := d.Get(MkRule).([]interface{}) rules := d.Get(MkRule).([]interface{})
for i := len(rules) - 1; i >= 0; i-- { for i := len(rules) - 1; i >= 0; i-- {
rule := rules[i].(map[string]interface{}) rule := rules[i].(map[string]interface{})
var ruleBody firewall.RuleCreateRequestBody
ruleBody := firewall.RuleCreateRequestBody{ sg := rule[mkSecurityGroup].(string)
BaseRule: *mapToBaseRule(rule), if sg != "" {
Action: rule[mkRuleAction].(string), // this is a special case of security group insertion
Type: rule[mkRuleType].(string), ruleBody = firewall.RuleCreateRequestBody{
Action: sg,
Type: "group",
BaseRule: *mapToSecurityGroupBaseRule(rule),
}
} else {
a := rule[mkRuleAction].(string)
t := rule[mkRuleType].(string)
if a == "" || t == "" {
diags = append(diags, diag.Errorf("Either '%s' OR both '%s' and '%s' must be defined for the rule #%d",
mkSecurityGroup, mkRuleAction, mkRuleType, i)...)
continue
}
ruleBody = firewall.RuleCreateRequestBody{
Action: a,
Type: t,
BaseRule: *mapToBaseRule(rule),
}
} }
err := api.CreateRule(ctx, &ruleBody) err := api.CreateRule(ctx, &ruleBody)
@ -206,12 +235,18 @@ func RulesRead(ctx context.Context, api firewall.Rule, d *schema.ResourceData) d
return fmt.Errorf("error reading rule %d : %w", pos, err) return fmt.Errorf("error reading rule %d : %w", pos, err)
} }
baseRuleToMap(&rule.BaseRule, ruleMap)
// pos in the map should be int! // pos in the map should be int!
ruleMap[mkRulePos] = pos ruleMap[mkRulePos] = pos
ruleMap[mkRuleAction] = rule.Action
ruleMap[mkRuleType] = rule.Type if rule.Type == "group" {
// this is a special case of security group insertion
ruleMap[mkSecurityGroup] = rule.Action
securityGroupBaseRuleToMap(&rule.BaseRule, ruleMap)
} else {
ruleMap[mkRuleAction] = rule.Action
ruleMap[mkRuleType] = rule.Type
baseRuleToMap(&rule.BaseRule, ruleMap)
}
return nil return nil
} }
@ -324,11 +359,9 @@ func mapToBaseRule(rule map[string]interface{}) *firewall.BaseRule {
if dport != "" { if dport != "" {
baseRule.DPort = &dport baseRule.DPort = &dport
} }
enable := rule[mkRuleEnable].(bool) enableBool := types.CustomBool(rule[mkRuleEnabled].(bool))
if enable { baseRule.Enable = &enableBool
enableBool := types.CustomBool(true)
baseRule.Enable = &enableBool
}
iface := rule[mkRuleIFace].(string) iface := rule[mkRuleIFace].(string)
if iface != "" { if iface != "" {
baseRule.IFace = &iface baseRule.IFace = &iface
@ -356,6 +389,24 @@ func mapToBaseRule(rule map[string]interface{}) *firewall.BaseRule {
return baseRule return baseRule
} }
func mapToSecurityGroupBaseRule(rule map[string]interface{}) *firewall.BaseRule {
baseRule := &firewall.BaseRule{}
comment := rule[mkRuleComment].(string)
if comment != "" {
baseRule.Comment = &comment
}
enableBool := types.CustomBool(rule[mkRuleEnabled].(bool))
baseRule.Enable = &enableBool
iface := rule[mkRuleIFace].(string)
if iface != "" {
baseRule.IFace = &iface
}
return baseRule
}
func baseRuleToMap(baseRule *firewall.BaseRule, rule map[string]interface{}) { func baseRuleToMap(baseRule *firewall.BaseRule, rule map[string]interface{}) {
if baseRule.Comment != nil { if baseRule.Comment != nil {
@ -368,7 +419,7 @@ func baseRuleToMap(baseRule *firewall.BaseRule, rule map[string]interface{}) {
rule[mkRuleDPort] = *baseRule.DPort rule[mkRuleDPort] = *baseRule.DPort
} }
if baseRule.Enable != nil { if baseRule.Enable != nil {
rule[mkRuleEnable] = *baseRule.Enable rule[mkRuleEnabled] = *baseRule.Enable
} }
if baseRule.IFace != nil { if baseRule.IFace != nil {
rule[mkRuleIFace] = *baseRule.IFace rule[mkRuleIFace] = *baseRule.IFace
@ -389,6 +440,17 @@ func baseRuleToMap(baseRule *firewall.BaseRule, rule map[string]interface{}) {
rule[mkRuleSPort] = *baseRule.SPort rule[mkRuleSPort] = *baseRule.SPort
} }
} }
func securityGroupBaseRuleToMap(baseRule *firewall.BaseRule, rule map[string]interface{}) {
if baseRule.Comment != nil {
rule[mkRuleComment] = *baseRule.Comment
}
if baseRule.Enable != nil {
rule[mkRuleEnabled] = *baseRule.Enable
}
if baseRule.IFace != nil {
rule[mkRuleIFace] = *baseRule.IFace
}
}
func invokeRuleAPI( func invokeRuleAPI(
f func(context.Context, firewall.Rule, *schema.ResourceData) diag.Diagnostics, f func(context.Context, firewall.Rule, *schema.ResourceData) diag.Diagnostics,

View File

@ -37,16 +37,14 @@ func TestRuleSchema(t *testing.T) {
ruleSchema := structure.AssertNestedSchemaExistence(t, rulesSchema, MkRule).Schema ruleSchema := structure.AssertNestedSchemaExistence(t, rulesSchema, MkRule).Schema
structure.AssertRequiredArguments(t, ruleSchema, []string{ structure.AssertOptionalArguments(t, ruleSchema, []string{
mkSecurityGroup,
mkRuleAction, mkRuleAction,
mkRuleType, mkRuleType,
})
structure.AssertOptionalArguments(t, ruleSchema, []string{
mkRuleComment, mkRuleComment,
mkRuleDest, mkRuleDest,
mkRuleDPort, mkRuleDPort,
mkRuleEnable, mkRuleEnabled,
mkRuleIFace, mkRuleIFace,
mkRuleLog, mkRuleLog,
mkRuleMacro, mkRuleMacro,
@ -62,7 +60,7 @@ func TestRuleSchema(t *testing.T) {
mkRuleComment: schema.TypeString, mkRuleComment: schema.TypeString,
mkRuleDest: schema.TypeString, mkRuleDest: schema.TypeString,
mkRuleDPort: schema.TypeString, mkRuleDPort: schema.TypeString,
mkRuleEnable: schema.TypeBool, mkRuleEnabled: schema.TypeBool,
mkRuleIFace: schema.TypeString, mkRuleIFace: schema.TypeString,
mkRuleLog: schema.TypeString, mkRuleLog: schema.TypeString,
mkRuleMacro: schema.TypeString, mkRuleMacro: schema.TypeString,

View File

@ -13,7 +13,10 @@ import (
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
) )
var rateExpression = regexp.MustCompile(`[1-9][0-9]*/(second|minute|hour|day)`) var (
rateExpression = regexp.MustCompile(`[1-9][0-9]*/(second|minute|hour|day)`)
ifaceExpression = regexp.MustCompile(`net\d+`)
)
func FirewallRate() schema.SchemaValidateDiagFunc { func FirewallRate() schema.SchemaValidateDiagFunc {
return validation.ToDiagFunc(validation.StringMatch( return validation.ToDiagFunc(validation.StringMatch(
@ -21,6 +24,12 @@ func FirewallRate() schema.SchemaValidateDiagFunc {
"Must be a valid rate expression, e.g. '1/second'", "Must be a valid rate expression, e.g. '1/second'",
)) ))
} }
func FirewallIFace() schema.SchemaValidateDiagFunc {
return validation.ToDiagFunc(validation.StringMatch(
ifaceExpression,
"Must be a valid VM/Container iface key, e.g. 'net0'",
))
}
func FirewallPolicy() schema.SchemaValidateDiagFunc { func FirewallPolicy() schema.SchemaValidateDiagFunc {
return validation.ToDiagFunc(validation.StringInSlice( return validation.ToDiagFunc(validation.StringInSlice(