mirror of
https://github.com/opentofu/opentofu.git
synced 2025-01-15 19:22:46 -06:00
1619a8138f
* 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
632 lines
16 KiB
Go
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
|
|
}
|