opentofu/builtin/providers/cloudstack/resource_cloudstack_firewall.go
Sander van Harmelen 19776ba402 Updating some logic and tests
These fixes are needed to make the provider work with master again.
These are still some issues, but they seem not to be related to the
provider, but the changes in `helper/schema`.
2015-01-15 11:29:04 +01:00

486 lines
12 KiB
Go

package cloudstack
import (
"bytes"
"fmt"
"regexp"
"sort"
"strconv"
"strings"
"github.com/hashicorp/terraform/helper/hashcode"
"github.com/hashicorp/terraform/helper/schema"
"github.com/xanzy/go-cloudstack/cloudstack"
)
func resourceCloudStackFirewall() *schema.Resource {
return &schema.Resource{
Create: resourceCloudStackFirewallCreate,
Read: resourceCloudStackFirewallRead,
Update: resourceCloudStackFirewallUpdate,
Delete: resourceCloudStackFirewallDelete,
Schema: map[string]*schema.Schema{
"ipaddress": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"managed": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Default: false,
},
"rule": &schema.Schema{
Type: schema.TypeSet,
Required: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"source_cidr": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"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: func(v interface{}) int {
return hashcode.String(v.(string))
},
},
"uuids": &schema.Schema{
Type: schema.TypeMap,
Computed: true,
},
},
},
Set: resourceCloudStackFirewallRuleHash,
},
},
}
}
func resourceCloudStackFirewallCreate(d *schema.ResourceData, meta interface{}) error {
cs := meta.(*cloudstack.CloudStackClient)
// Retrieve the ipaddress UUID
ipaddressid, e := retrieveUUID(cs, "ipaddress", d.Get("ipaddress").(string))
if e != nil {
return e.Error()
}
// We need to set this upfront in order to be able to save a partial state
d.SetId(ipaddressid)
// Create all rules that are configured
if rs := d.Get("rule").(*schema.Set); rs.Len() > 0 {
// Create an empty schema.Set to hold all rules
rules := &schema.Set{
F: resourceCloudStackFirewallRuleHash,
}
for _, rule := range rs.List() {
// Create a single rule
err := resourceCloudStackFirewallCreateRule(d, meta, rule.(map[string]interface{}))
// We need to update this first to preserve the correct state
rules.Add(rule)
d.Set("rule", rules)
if err != nil {
return err
}
}
}
return resourceCloudStackFirewallRead(d, meta)
}
func resourceCloudStackFirewallCreateRule(
d *schema.ResourceData, meta interface{}, rule map[string]interface{}) error {
cs := meta.(*cloudstack.CloudStackClient)
uuids := rule["uuids"].(map[string]interface{})
// Make sure all required rule parameters are there
if err := verifyFirewallRuleParams(d, rule); err != nil {
return err
}
// Create a new parameter struct
p := cs.Firewall.NewCreateFirewallRuleParams(d.Id(), rule["protocol"].(string))
// Set the CIDR list
p.SetCidrlist([]string{rule["source_cidr"].(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))
r, err := cs.Firewall.CreateFirewallRule(p)
if err != nil {
return err
}
uuids["icmp"] = r.Id
rule["uuids"] = uuids
}
// If protocol is not ICMP, loop through all ports
if rule["protocol"].(string) != "icmp" {
if ps := rule["ports"].(*schema.Set); ps.Len() > 0 {
// Create an empty schema.Set to hold all processed ports
ports := &schema.Set{
F: func(v interface{}) int {
return hashcode.String(v.(string))
},
}
for _, port := range ps.List() {
re := regexp.MustCompile(`^(\d+)(?:-(\d+))?$`)
m := re.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)
r, err := cs.Firewall.CreateFirewallRule(p)
if err != nil {
return err
}
ports.Add(port)
rule["ports"] = ports
uuids[port.(string)] = r.Id
rule["uuids"] = uuids
}
}
}
return nil
}
func resourceCloudStackFirewallRead(d *schema.ResourceData, meta interface{}) error {
cs := meta.(*cloudstack.CloudStackClient)
// Create an empty schema.Set to hold all rules
rules := &schema.Set{
F: resourceCloudStackFirewallRuleHash,
}
// 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{})
uuids := rule["uuids"].(map[string]interface{})
if rule["protocol"].(string) == "icmp" {
id, ok := uuids["icmp"]
if !ok {
continue
}
// Get the rule
r, count, err := cs.Firewall.GetFirewallRuleByID(id.(string))
// If the count == 0, there is no object found for this UUID
if err != nil {
if count == 0 {
delete(uuids, "icmp")
continue
}
return err
}
// Update the values
rule["source_cidr"] = r.Cidrlist
rule["protocol"] = r.Protocol
rule["icmp_type"] = r.Icmptype
rule["icmp_code"] = r.Icmpcode
rules.Add(rule)
}
// If protocol is not ICMP, loop through all ports
if rule["protocol"].(string) != "icmp" {
if ps := rule["ports"].(*schema.Set); ps.Len() > 0 {
// Create an empty schema.Set to hold all ports
ports := &schema.Set{
F: func(v interface{}) int {
return hashcode.String(v.(string))
},
}
// Loop through all ports and retrieve their info
for _, port := range ps.List() {
id, ok := uuids[port.(string)]
if !ok {
continue
}
// Get the rule
r, count, err := cs.Firewall.GetFirewallRuleByID(id.(string))
if err != nil {
if count == 0 {
delete(uuids, port.(string))
continue
}
return err
}
// Update the values
rule["source_cidr"] = r.Cidrlist
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
rules.Add(rule)
}
}
}
}
}
// If this is a managed firewall, add all unknown rules into a single dummy rule
if d.Get("managed").(bool) {
// Get all the rules from the running environment
p := cs.Firewall.NewListFirewallRulesParams()
p.SetIpaddressid(d.Id())
p.SetListall(true)
r, err := cs.Firewall.ListFirewallRules(p)
if err != nil {
return err
}
// Add all UUIDs to the uuids map
uuids := make(map[string]interface{})
for _, r := range r.FirewallRules {
uuids[r.Id] = r.Id
}
// Delete all expected UUIDs from the uuids map
for _, rule := range rules.List() {
rule := rule.(map[string]interface{})
for _, id := range rule["uuids"].(map[string]interface{}) {
delete(uuids, id.(string))
}
}
if len(uuids) > 0 {
// Make a dummy rule to hold all unknown UUIDs
rule := map[string]interface{}{
"source_cidr": "N/A",
"protocol": "N/A",
"uuids": uuids,
}
// Add the dummy rule to the rules set
rules.Add(rule)
}
}
if rules.Len() > 0 {
d.Set("rule", rules)
} else {
d.SetId("")
}
return nil
}
func resourceCloudStackFirewallUpdate(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))
// Now first loop through all the old rules and delete any obsolete ones
for _, rule := range ors.List() {
// Delete the rule as it no longer exists in the config
err := resourceCloudStackFirewallDeleteRule(d, meta, rule.(map[string]interface{}))
if err != nil {
return err
}
}
// Make sure we save the state of the currently configured rules
rules := o.(*schema.Set).Intersection(n.(*schema.Set))
d.Set("rule", rules)
// Then loop through al the currently configured rules and create the new ones
for _, rule := range nrs.List() {
// When succesfully deleted, re-create it again if it still exists
err := resourceCloudStackFirewallCreateRule(
d, meta, rule.(map[string]interface{}))
// We need to update this first to preserve the correct state
rules.Add(rule)
d.Set("rule", rules)
if err != nil {
return err
}
}
}
return resourceCloudStackFirewallRead(d, meta)
}
func resourceCloudStackFirewallDelete(d *schema.ResourceData, meta interface{}) error {
// Delete all rules
if rs := d.Get("rule").(*schema.Set); rs.Len() > 0 {
for _, rule := range rs.List() {
// Delete a single rule
err := resourceCloudStackFirewallDeleteRule(d, meta, rule.(map[string]interface{}))
// We need to update this first to preserve the correct state
d.Set("rule", rs)
if err != nil {
return err
}
}
}
return nil
}
func resourceCloudStackFirewallDeleteRule(
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
}
// Create the parameter struct
p := cs.Firewall.NewDeleteFirewallRuleParams(id.(string))
// Delete the rule
if _, err := cs.Firewall.DeleteFirewallRule(p); err != nil {
// This is a very poor way to be told the UUID 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)
}
// Update the UUIDs
rule["uuids"] = uuids
return nil
}
func resourceCloudStackFirewallRuleHash(v interface{}) int {
var buf bytes.Buffer
m := v.(map[string]interface{})
buf.WriteString(fmt.Sprintf(
"%s-%s-", m["source_cidr"].(string), m["protocol"].(string)))
if v, ok := m["icmp_type"]; ok {
buf.WriteString(fmt.Sprintf("%d-", v.(int)))
}
if v, ok := m["icmp_code"]; ok {
buf.WriteString(fmt.Sprintf("%d-", v.(int)))
}
// We need to make sure to sort the strings below so that we always
// generate the same hash code no matter what is in the set.
if v, ok := m["ports"]; ok {
vs := v.(*schema.Set).List()
s := make([]string, len(vs))
for i, raw := range vs {
s[i] = raw.(string)
}
sort.Strings(s)
for _, v := range s {
buf.WriteString(fmt.Sprintf("%s-", v))
}
}
return hashcode.String(buf.String())
}
func verifyFirewallRuleParams(d *schema.ResourceData, rule map[string]interface{}) error {
protocol := rule["protocol"].(string)
if protocol != "tcp" && protocol != "udp" && protocol != "icmp" {
return fmt.Errorf(
"%s is not a valid protocol. Valid options are 'tcp', 'udp' and 'icmp'", protocol)
}
if protocol == "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'")
}
} else {
if _, ok := rule["ports"]; !ok {
return fmt.Errorf(
"Parameter port is a required parameter when using protocol 'tcp' or 'udp'")
}
}
return nil
}