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
|
||
|
}
|