mirror of
https://github.com/opentofu/opentofu.git
synced 2025-02-20 11:48:24 -06:00
Of course not all resources are covered by this first release, but there should be enough resources available to handle most common operations. Tests and docs are included.
443 lines
11 KiB
Go
443 lines
11 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,
|
|
},
|
|
|
|
"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(d.Get("ipaddress").(string))
|
|
|
|
// 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, ipaddressid, 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{}, ipaddressid string, rule map[string]interface{}) error {
|
|
cs := meta.(*cloudstack.CloudStackClient)
|
|
uuids := rule["uuids"].(map[string]interface{})
|
|
|
|
// Make sure all required parameters are there
|
|
if err := verifyFirewallParams(d, rule); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Create a new parameter struct
|
|
p := cs.Firewall.NewCreateFirewallRuleParams(ipaddressid, 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 rules.Len() > 0 {
|
|
d.Set("rule", rules)
|
|
} else {
|
|
d.SetId("")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func resourceCloudStackFirewallUpdate(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()
|
|
}
|
|
|
|
// 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, ipaddressid, 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 {
|
|
// 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 verifyFirewallParams(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
|
|
}
|