opentofu/builtin/providers/cloudstack/resource_cloudstack_security_group_rule.go
Sander van Harmelen 1619a8138f provider/cloudstack: enhance security groups and rules (#9645)
* govendor: update go-cloudstack dependency

* Separate security groups and rules

This commit separates the creation and management of security groups and security group rules.

It extends the `icmp` options so you can supply `icmp_type` and `icmp_code` to enbale more specific configs.

And it adds lifecycle management of security group rules, so that security groups do not have to be recreated when rules are added or removed.

This is particulary helpful since the `cloudstack_instance` cannot update a security group without having to recreate the instance.

In CloudStack >= 4.9.0 it is possible to update security groups of existing instances, but as that is just added to the latest version it seems a bit too soon to start using this (causing backwards incompatibility issues for people or service providers running older versions).

* Add and update documentation

* Add acceptance tests
2016-10-27 11:10:15 +02:00

632 lines
16 KiB
Go

package cloudstack
import (
"fmt"
"log"
"strconv"
"strings"
"sync"
"time"
multierror "github.com/hashicorp/go-multierror"
"github.com/hashicorp/terraform/helper/schema"
"github.com/xanzy/go-cloudstack/cloudstack"
)
type authorizeSecurityGroupParams interface {
SetCidrlist([]string)
SetIcmptype(int)
SetIcmpcode(int)
SetStartport(int)
SetEndport(int)
SetProtocol(string)
SetSecuritygroupid(string)
SetUsersecuritygrouplist(map[string]string)
}
func resourceCloudStackSecurityGroupRule() *schema.Resource {
return &schema.Resource{
Create: resourceCloudStackSecurityGroupRuleCreate,
Read: resourceCloudStackSecurityGroupRuleRead,
Update: resourceCloudStackSecurityGroupRuleUpdate,
Delete: resourceCloudStackSecurityGroupRuleDelete,
Schema: map[string]*schema.Schema{
"security_group_id": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"rule": &schema.Schema{
Type: schema.TypeSet,
Required: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"cidr_list": &schema.Schema{
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
Set: schema.HashString,
},
"protocol": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"icmp_type": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
Computed: true,
},
"icmp_code": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
Computed: true,
},
"ports": &schema.Schema{
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
Set: schema.HashString,
},
"traffic_type": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Default: "ingress",
},
"user_security_group_list": &schema.Schema{
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
Set: schema.HashString,
},
"uuids": &schema.Schema{
Type: schema.TypeMap,
Computed: true,
},
},
},
},
"project": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
"parallelism": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
Default: 2,
},
},
}
}
func resourceCloudStackSecurityGroupRuleCreate(d *schema.ResourceData, meta interface{}) error {
// We need to set this upfront in order to be able to save a partial state
d.SetId(d.Get("security_group_id").(string))
// Create all rules that are configured
if nrs := d.Get("rule").(*schema.Set); nrs.Len() > 0 {
// Create an empty rule set to hold all newly created rules
rules := resourceCloudStackSecurityGroupRule().Schema["rule"].ZeroValue().(*schema.Set)
err := createSecurityGroupRules(d, meta, rules, nrs)
// We need to update this first to preserve the correct state
d.Set("rule", rules)
if err != nil {
return err
}
}
return resourceCloudStackSecurityGroupRuleRead(d, meta)
}
func createSecurityGroupRules(d *schema.ResourceData, meta interface{}, rules *schema.Set, nrs *schema.Set) error {
cs := meta.(*cloudstack.CloudStackClient)
var errs *multierror.Error
var wg sync.WaitGroup
wg.Add(nrs.Len())
sem := make(chan struct{}, d.Get("parallelism").(int))
for _, rule := range nrs.List() {
// Put in a tiny sleep here to avoid DoS'ing the API
time.Sleep(500 * time.Millisecond)
go func(rule map[string]interface{}) {
defer wg.Done()
sem <- struct{}{}
// Make sure all required parameters are there
if err := verifySecurityGroupRuleParams(d, rule); err != nil {
errs = multierror.Append(errs, err)
return
}
var p authorizeSecurityGroupParams
if cidrList, ok := rule["cidr_list"].(*schema.Set); ok && cidrList.Len() > 0 {
for _, cidr := range cidrList.List() {
// Create a new parameter struct
switch rule["traffic_type"].(string) {
case "ingress":
p = cs.SecurityGroup.NewAuthorizeSecurityGroupIngressParams()
case "egress":
p = cs.SecurityGroup.NewAuthorizeSecurityGroupEgressParams()
}
p.SetSecuritygroupid(d.Id())
p.SetCidrlist([]string{cidr.(string)})
// Create a single rule
err := createSecurityGroupRule(d, meta, rule, p, cidr.(string))
if err != nil {
errs = multierror.Append(errs, err)
}
}
}
if usgList, ok := rule["user_security_group_list"].(*schema.Set); ok && usgList.Len() > 0 {
for _, usg := range usgList.List() {
sg, _, err := cs.SecurityGroup.GetSecurityGroupByName(
usg.(string),
cloudstack.WithProject(d.Get("project").(string)),
)
if err != nil {
errs = multierror.Append(errs, err)
continue
}
// Create a new parameter struct
switch rule["traffic_type"].(string) {
case "ingress":
p = cs.SecurityGroup.NewAuthorizeSecurityGroupIngressParams()
case "egress":
p = cs.SecurityGroup.NewAuthorizeSecurityGroupEgressParams()
}
p.SetSecuritygroupid(d.Id())
p.SetUsersecuritygrouplist(map[string]string{sg.Account: usg.(string)})
// Create a single rule
err = createSecurityGroupRule(d, meta, rule, p, usg.(string))
if err != nil {
errs = multierror.Append(errs, err)
}
}
}
// If we have at least one UUID, we need to save the rule
if len(rule["uuids"].(map[string]interface{})) > 0 {
rules.Add(rule)
}
<-sem
}(rule.(map[string]interface{}))
}
wg.Wait()
return errs.ErrorOrNil()
}
func createSecurityGroupRule(d *schema.ResourceData, meta interface{}, rule map[string]interface{}, p authorizeSecurityGroupParams, uuid string) error {
cs := meta.(*cloudstack.CloudStackClient)
uuids := rule["uuids"].(map[string]interface{})
// Set the protocol
p.SetProtocol(rule["protocol"].(string))
// If the protocol is ICMP set the needed ICMP parameters
if rule["protocol"].(string) == "icmp" {
p.SetIcmptype(rule["icmp_type"].(int))
p.SetIcmpcode(rule["icmp_code"].(int))
ruleID, err := createIngressOrEgressRule(cs, p)
if err != nil {
return err
}
uuids[uuid+"icmp"] = ruleID
rule["uuids"] = uuids
}
// If protocol is TCP or UDP, loop through all ports
if rule["protocol"].(string) == "tcp" || rule["protocol"].(string) == "udp" {
if ps := rule["ports"].(*schema.Set); ps.Len() > 0 {
// Create an empty schema.Set to hold all processed ports
ports := &schema.Set{F: schema.HashString}
for _, port := range ps.List() {
if _, ok := uuids[uuid+port.(string)]; ok {
ports.Add(port)
rule["ports"] = ports
continue
}
m := splitPorts.FindStringSubmatch(port.(string))
startPort, err := strconv.Atoi(m[1])
if err != nil {
return err
}
endPort := startPort
if m[2] != "" {
endPort, err = strconv.Atoi(m[2])
if err != nil {
return err
}
}
p.SetStartport(startPort)
p.SetEndport(endPort)
ruleID, err := createIngressOrEgressRule(cs, p)
if err != nil {
return err
}
ports.Add(port)
rule["ports"] = ports
uuids[uuid+port.(string)] = ruleID
rule["uuids"] = uuids
}
}
}
return nil
}
func createIngressOrEgressRule(cs *cloudstack.CloudStackClient, p authorizeSecurityGroupParams) (string, error) {
switch p := p.(type) {
case *cloudstack.AuthorizeSecurityGroupIngressParams:
r, err := cs.SecurityGroup.AuthorizeSecurityGroupIngress(p)
if err != nil {
return "", err
}
return r.Ruleid, nil
case *cloudstack.AuthorizeSecurityGroupEgressParams:
r, err := cs.SecurityGroup.AuthorizeSecurityGroupEgress(p)
if err != nil {
return "", err
}
return r.Ruleid, nil
default:
return "", fmt.Errorf("Unknown authorize security group rule type: %v", p)
}
}
func resourceCloudStackSecurityGroupRuleRead(d *schema.ResourceData, meta interface{}) error {
cs := meta.(*cloudstack.CloudStackClient)
// Get the security group details
sg, count, err := cs.SecurityGroup.GetSecurityGroupByID(
d.Id(),
cloudstack.WithProject(d.Get("project").(string)),
)
if err != nil {
if count == 0 {
log.Printf("[DEBUG] Security group %s does not longer exist", d.Get("name").(string))
d.SetId("")
return nil
}
return err
}
// Make a map of all the rule indexes so we can easily find a rule
sgRules := append(sg.Ingressrule, sg.Egressrule...)
ruleIndex := make(map[string]int, len(sgRules))
for idx, r := range sgRules {
ruleIndex[r.Ruleid] = idx
}
// Create an empty schema.Set to hold all rules
rules := resourceCloudStackSecurityGroupRule().Schema["rule"].ZeroValue().(*schema.Set)
// Read all rules that are configured
if rs := d.Get("rule").(*schema.Set); rs.Len() > 0 {
for _, rule := range rs.List() {
rule := rule.(map[string]interface{})
// First get any existing values
cidrList, cidrListOK := rule["cidr_list"].(*schema.Set)
usgList, usgListOk := rule["user_security_group_list"].(*schema.Set)
// Then reset the values to a new empty set
rule["cidr_list"] = &schema.Set{F: schema.HashString}
rule["user_security_group_list"] = &schema.Set{F: schema.HashString}
if cidrListOK && cidrList.Len() > 0 {
for _, cidr := range cidrList.List() {
readSecurityGroupRule(sg, ruleIndex, rule, cidr.(string))
}
}
if usgListOk && usgList.Len() > 0 {
for _, usg := range usgList.List() {
readSecurityGroupRule(sg, ruleIndex, rule, usg.(string))
}
}
rules.Add(rule)
}
}
return nil
}
func readSecurityGroupRule(sg *cloudstack.SecurityGroup, ruleIndex map[string]int, rule map[string]interface{}, uuid string) {
uuids := rule["uuids"].(map[string]interface{})
sgRules := append(sg.Ingressrule, sg.Egressrule...)
if rule["protocol"].(string) == "icmp" {
id, ok := uuids[uuid+"icmp"]
if !ok {
return
}
// Get the rule
idx, ok := ruleIndex[id.(string)]
if !ok {
delete(uuids, uuid+"icmp")
return
}
r := sgRules[idx]
// Update the values
if r.Cidr != "" {
rule["cidr_list"].(*schema.Set).Add(r.Cidr)
}
if r.Securitygroupname != "" {
rule["user_security_group_list"].(*schema.Set).Add(r.Securitygroupname)
}
rule["protocol"] = r.Protocol
rule["icmp_type"] = r.Icmptype
rule["icmp_code"] = r.Icmpcode
}
// If protocol is tcp or udp, loop through all ports
if rule["protocol"].(string) == "tcp" || rule["protocol"].(string) == "udp" {
if ps := rule["ports"].(*schema.Set); ps.Len() > 0 {
// Create an empty schema.Set to hold all ports
ports := &schema.Set{F: schema.HashString}
// Loop through all ports and retrieve their info
for _, port := range ps.List() {
id, ok := uuids[uuid+port.(string)]
if !ok {
continue
}
// Get the rule
idx, ok := ruleIndex[id.(string)]
if !ok {
delete(uuids, uuid+port.(string))
continue
}
r := sgRules[idx]
// Create a set with all CIDR's
cidrs := &schema.Set{F: schema.HashString}
for _, cidr := range strings.Split(r.Cidr, ",") {
cidrs.Add(cidr)
}
// Update the values
rule["protocol"] = r.Protocol
ports.Add(port)
}
// If there is at least one port found, add this rule to the rules set
if ports.Len() > 0 {
rule["ports"] = ports
}
}
}
}
func resourceCloudStackSecurityGroupRuleUpdate(d *schema.ResourceData, meta interface{}) error {
// Check if the rule set as a whole has changed
if d.HasChange("rule") {
o, n := d.GetChange("rule")
ors := o.(*schema.Set).Difference(n.(*schema.Set))
nrs := n.(*schema.Set).Difference(o.(*schema.Set))
// We need to start with a rule set containing all the rules we
// already have and want to keep. Any rules that are not deleted
// correctly and any newly created rules, will be added to this
// set to make sure we end up in a consistent state
rules := o.(*schema.Set).Intersection(n.(*schema.Set))
// First loop through all the old rules destroy them
if ors.Len() > 0 {
err := deleteSecurityGroupRules(d, meta, rules, ors)
// We need to update this first to preserve the correct state
d.Set("rule", rules)
if err != nil {
return err
}
}
// Then loop through all the new rules and delete them
if nrs.Len() > 0 {
err := createSecurityGroupRules(d, meta, rules, nrs)
// We need to update this first to preserve the correct state
d.Set("rule", rules)
if err != nil {
return err
}
}
}
return resourceCloudStackSecurityGroupRuleRead(d, meta)
}
func resourceCloudStackSecurityGroupRuleDelete(d *schema.ResourceData, meta interface{}) error {
// Create an empty rule set to hold all rules that where
// not deleted correctly
rules := resourceCloudStackSecurityGroupRule().Schema["rule"].ZeroValue().(*schema.Set)
// Delete all rules
if ors := d.Get("rule").(*schema.Set); ors.Len() > 0 {
err := deleteSecurityGroupRules(d, meta, rules, ors)
// We need to update this first to preserve the correct state
d.Set("rule", rules)
if err != nil {
return err
}
}
return nil
}
func deleteSecurityGroupRules(d *schema.ResourceData, meta interface{}, rules *schema.Set, ors *schema.Set) error {
var errs *multierror.Error
var wg sync.WaitGroup
wg.Add(ors.Len())
sem := make(chan struct{}, d.Get("parallelism").(int))
for _, rule := range ors.List() {
// Put a sleep here to avoid DoS'ing the API
time.Sleep(500 * time.Millisecond)
go func(rule map[string]interface{}) {
defer wg.Done()
sem <- struct{}{}
// Create a single rule
err := deleteSecurityGroupRule(d, meta, rule)
if err != nil {
errs = multierror.Append(errs, err)
}
// If we have at least one UUID, we need to save the rule
if len(rule["uuids"].(map[string]interface{})) > 0 {
rules.Add(rule)
}
<-sem
}(rule.(map[string]interface{}))
}
wg.Wait()
return errs.ErrorOrNil()
}
func deleteSecurityGroupRule(d *schema.ResourceData, meta interface{}, rule map[string]interface{}) error {
cs := meta.(*cloudstack.CloudStackClient)
uuids := rule["uuids"].(map[string]interface{})
for k, id := range uuids {
// We don't care about the count here, so just continue
if k == "%" {
continue
}
var err error
switch rule["traffic_type"].(string) {
case "ingress":
p := cs.SecurityGroup.NewRevokeSecurityGroupIngressParams(id.(string))
_, err = cs.SecurityGroup.RevokeSecurityGroupIngress(p)
case "egress":
p := cs.SecurityGroup.NewRevokeSecurityGroupEgressParams(id.(string))
_, err = cs.SecurityGroup.RevokeSecurityGroupEgress(p)
}
if err != nil {
// This is a very poor way to be told the ID does no longer exist :(
if strings.Contains(err.Error(), fmt.Sprintf(
"Invalid parameter id value=%s due to incorrect long value format, "+
"or entity does not exist", id.(string))) {
delete(uuids, k)
continue
}
return err
}
// Delete the UUID of this rule
delete(uuids, k)
}
return nil
}
func verifySecurityGroupRuleParams(d *schema.ResourceData, rule map[string]interface{}) error {
cidrList, cidrListOK := rule["cidr_list"].(*schema.Set)
usgList, usgListOK := rule["user_security_group_list"].(*schema.Set)
if (!cidrListOK || cidrList.Len() == 0) && (!usgListOK || usgList.Len() == 0) {
return fmt.Errorf(
"You must supply at least one 'cidr_list' or `user_security_group_ids` entry")
}
protocol := rule["protocol"].(string)
switch protocol {
case "icmp":
if _, ok := rule["icmp_type"]; !ok {
return fmt.Errorf(
"Parameter icmp_type is a required parameter when using protocol 'icmp'")
}
if _, ok := rule["icmp_code"]; !ok {
return fmt.Errorf(
"Parameter icmp_code is a required parameter when using protocol 'icmp'")
}
case "tcp", "udp":
if ports, ok := rule["ports"].(*schema.Set); ok {
for _, port := range ports.List() {
m := splitPorts.FindStringSubmatch(port.(string))
if m == nil {
return fmt.Errorf(
"%q is not a valid port value. Valid options are '80' or '80-90'", port.(string))
}
}
} else {
return fmt.Errorf(
"Parameter ports is a required parameter when *not* using protocol 'icmp'")
}
default:
_, err := strconv.ParseInt(protocol, 0, 0)
if err != nil {
return fmt.Errorf(
"%q is not a valid protocol. Valid options are 'tcp', 'udp' and 'icmp'", protocol)
}
}
traffic := rule["traffic_type"].(string)
if traffic != "ingress" && traffic != "egress" {
return fmt.Errorf(
"Parameter traffic_type only accepts 'ingress' or 'egress' as values")
}
return nil
}