github.com/vtorhonen/terraform@v0.9.0-beta2.0.20170307220345-5d894e4ffda7/builtin/providers/cloudstack/resource_cloudstack_port_forward.go (about)

     1  package cloudstack
     2  
     3  import (
     4  	"fmt"
     5  	"log"
     6  	"strconv"
     7  	"strings"
     8  	"sync"
     9  	"time"
    10  
    11  	"github.com/hashicorp/go-multierror"
    12  	"github.com/hashicorp/terraform/helper/schema"
    13  	"github.com/xanzy/go-cloudstack/cloudstack"
    14  )
    15  
    16  func resourceCloudStackPortForward() *schema.Resource {
    17  	return &schema.Resource{
    18  		Create: resourceCloudStackPortForwardCreate,
    19  		Read:   resourceCloudStackPortForwardRead,
    20  		Update: resourceCloudStackPortForwardUpdate,
    21  		Delete: resourceCloudStackPortForwardDelete,
    22  
    23  		Schema: map[string]*schema.Schema{
    24  			"ip_address_id": &schema.Schema{
    25  				Type:     schema.TypeString,
    26  				Required: true,
    27  				ForceNew: true,
    28  			},
    29  
    30  			"managed": &schema.Schema{
    31  				Type:     schema.TypeBool,
    32  				Optional: true,
    33  				Default:  false,
    34  			},
    35  
    36  			"project": &schema.Schema{
    37  				Type:     schema.TypeString,
    38  				Optional: true,
    39  				ForceNew: true,
    40  			},
    41  
    42  			"forward": &schema.Schema{
    43  				Type:     schema.TypeSet,
    44  				Required: true,
    45  				Elem: &schema.Resource{
    46  					Schema: map[string]*schema.Schema{
    47  						"protocol": &schema.Schema{
    48  							Type:     schema.TypeString,
    49  							Required: true,
    50  						},
    51  
    52  						"private_port": &schema.Schema{
    53  							Type:     schema.TypeInt,
    54  							Required: true,
    55  						},
    56  
    57  						"public_port": &schema.Schema{
    58  							Type:     schema.TypeInt,
    59  							Required: true,
    60  						},
    61  
    62  						"virtual_machine_id": &schema.Schema{
    63  							Type:     schema.TypeString,
    64  							Required: true,
    65  						},
    66  
    67  						"vm_guest_ip": &schema.Schema{
    68  							Type:     schema.TypeString,
    69  							Optional: true,
    70  						},
    71  
    72  						"uuid": &schema.Schema{
    73  							Type:     schema.TypeString,
    74  							Computed: true,
    75  						},
    76  					},
    77  				},
    78  			},
    79  		},
    80  	}
    81  }
    82  
    83  func resourceCloudStackPortForwardCreate(d *schema.ResourceData, meta interface{}) error {
    84  	// We need to set this upfront in order to be able to save a partial state
    85  	d.SetId(d.Get("ip_address_id").(string))
    86  
    87  	// Create all forwards that are configured
    88  	if nrs := d.Get("forward").(*schema.Set); nrs.Len() > 0 {
    89  		// Create an empty schema.Set to hold all forwards
    90  		forwards := resourceCloudStackPortForward().Schema["forward"].ZeroValue().(*schema.Set)
    91  
    92  		err := createPortForwards(d, meta, forwards, nrs)
    93  
    94  		// We need to update this first to preserve the correct state
    95  		d.Set("forward", forwards)
    96  
    97  		if err != nil {
    98  			return err
    99  		}
   100  	}
   101  
   102  	return resourceCloudStackPortForwardRead(d, meta)
   103  }
   104  
   105  func createPortForwards(d *schema.ResourceData, meta interface{}, forwards *schema.Set, nrs *schema.Set) error {
   106  	var errs *multierror.Error
   107  
   108  	var wg sync.WaitGroup
   109  	wg.Add(nrs.Len())
   110  
   111  	sem := make(chan struct{}, 10)
   112  	for _, forward := range nrs.List() {
   113  		// Put in a tiny sleep here to avoid DoS'ing the API
   114  		time.Sleep(500 * time.Millisecond)
   115  
   116  		go func(forward map[string]interface{}) {
   117  			defer wg.Done()
   118  			sem <- struct{}{}
   119  
   120  			// Create a single forward
   121  			err := createPortForward(d, meta, forward)
   122  
   123  			// If we have a UUID, we need to save the forward
   124  			if forward["uuid"].(string) != "" {
   125  				forwards.Add(forward)
   126  			}
   127  
   128  			if err != nil {
   129  				errs = multierror.Append(errs, err)
   130  			}
   131  
   132  			<-sem
   133  		}(forward.(map[string]interface{}))
   134  	}
   135  
   136  	wg.Wait()
   137  
   138  	return errs.ErrorOrNil()
   139  }
   140  
   141  func createPortForward(d *schema.ResourceData, meta interface{}, forward map[string]interface{}) error {
   142  	cs := meta.(*cloudstack.CloudStackClient)
   143  
   144  	// Make sure all required parameters are there
   145  	if err := verifyPortForwardParams(d, forward); err != nil {
   146  		return err
   147  	}
   148  
   149  	vm, _, err := cs.VirtualMachine.GetVirtualMachineByID(
   150  		forward["virtual_machine_id"].(string),
   151  		cloudstack.WithProject(d.Get("project").(string)),
   152  	)
   153  	if err != nil {
   154  		return err
   155  	}
   156  
   157  	// Create a new parameter struct
   158  	p := cs.Firewall.NewCreatePortForwardingRuleParams(d.Id(), forward["private_port"].(int),
   159  		forward["protocol"].(string), forward["public_port"].(int), vm.Id)
   160  
   161  	if vmGuestIP, ok := forward["vm_guest_ip"]; ok && vmGuestIP.(string) != "" {
   162  		p.SetVmguestip(vmGuestIP.(string))
   163  
   164  		// Set the network ID based on the guest IP, needed when the public IP address
   165  		// is not associated with any network yet
   166  	NICS:
   167  		for _, nic := range vm.Nic {
   168  			if vmGuestIP.(string) == nic.Ipaddress {
   169  				p.SetNetworkid(nic.Networkid)
   170  				break NICS
   171  			}
   172  			for _, ip := range nic.Secondaryip {
   173  				if vmGuestIP.(string) == ip.Ipaddress {
   174  					p.SetNetworkid(nic.Networkid)
   175  					break NICS
   176  				}
   177  			}
   178  		}
   179  	} else {
   180  		// If no guest IP is configured, use the primary NIC
   181  		p.SetNetworkid(vm.Nic[0].Networkid)
   182  	}
   183  
   184  	// Do not open the firewall automatically in any case
   185  	p.SetOpenfirewall(false)
   186  
   187  	r, err := cs.Firewall.CreatePortForwardingRule(p)
   188  	if err != nil {
   189  		return err
   190  	}
   191  
   192  	forward["uuid"] = r.Id
   193  
   194  	return nil
   195  }
   196  
   197  func resourceCloudStackPortForwardRead(d *schema.ResourceData, meta interface{}) error {
   198  	cs := meta.(*cloudstack.CloudStackClient)
   199  
   200  	// First check if the IP address is still associated
   201  	_, count, err := cs.Address.GetPublicIpAddressByID(
   202  		d.Id(),
   203  		cloudstack.WithProject(d.Get("project").(string)),
   204  	)
   205  	if err != nil {
   206  		if count == 0 {
   207  			log.Printf(
   208  				"[DEBUG] IP address with ID %s is no longer associated", d.Id())
   209  			d.SetId("")
   210  			return nil
   211  		}
   212  
   213  		return err
   214  	}
   215  
   216  	// Get all the forwards from the running environment
   217  	p := cs.Firewall.NewListPortForwardingRulesParams()
   218  	p.SetIpaddressid(d.Id())
   219  	p.SetListall(true)
   220  
   221  	if err := setProjectid(p, cs, d); err != nil {
   222  		return err
   223  	}
   224  
   225  	l, err := cs.Firewall.ListPortForwardingRules(p)
   226  	if err != nil {
   227  		return err
   228  	}
   229  
   230  	// Make a map of all the forwards so we can easily find a forward
   231  	forwardMap := make(map[string]*cloudstack.PortForwardingRule, l.Count)
   232  	for _, f := range l.PortForwardingRules {
   233  		forwardMap[f.Id] = f
   234  	}
   235  
   236  	// Create an empty schema.Set to hold all forwards
   237  	forwards := resourceCloudStackPortForward().Schema["forward"].ZeroValue().(*schema.Set)
   238  
   239  	// Read all forwards that are configured
   240  	if rs := d.Get("forward").(*schema.Set); rs.Len() > 0 {
   241  		for _, forward := range rs.List() {
   242  			forward := forward.(map[string]interface{})
   243  
   244  			id, ok := forward["uuid"]
   245  			if !ok || id.(string) == "" {
   246  				continue
   247  			}
   248  
   249  			// Get the forward
   250  			f, ok := forwardMap[id.(string)]
   251  			if !ok {
   252  				forward["uuid"] = ""
   253  				continue
   254  			}
   255  
   256  			// Delete the known rule so only unknown rules remain in the ruleMap
   257  			delete(forwardMap, id.(string))
   258  
   259  			privPort, err := strconv.Atoi(f.Privateport)
   260  			if err != nil {
   261  				return err
   262  			}
   263  
   264  			pubPort, err := strconv.Atoi(f.Publicport)
   265  			if err != nil {
   266  				return err
   267  			}
   268  
   269  			// Update the values
   270  			forward["protocol"] = f.Protocol
   271  			forward["private_port"] = privPort
   272  			forward["public_port"] = pubPort
   273  			forward["virtual_machine_id"] = f.Virtualmachineid
   274  
   275  			// This one is a bit tricky. We only want to update this optional value
   276  			// if we've set one ourselves. If not this would become a computed value
   277  			// and that would mess up the calculated hash of the set item.
   278  			if forward["vm_guest_ip"].(string) != "" {
   279  				forward["vm_guest_ip"] = f.Vmguestip
   280  			}
   281  
   282  			forwards.Add(forward)
   283  		}
   284  	}
   285  
   286  	// If this is a managed resource, add all unknown forwards to dummy forwards
   287  	managed := d.Get("managed").(bool)
   288  	if managed && len(forwardMap) > 0 {
   289  		for uuid := range forwardMap {
   290  			// Make a dummy forward to hold the unknown UUID
   291  			forward := map[string]interface{}{
   292  				"protocol":           uuid,
   293  				"private_port":       0,
   294  				"public_port":        0,
   295  				"virtual_machine_id": uuid,
   296  				"uuid":               uuid,
   297  			}
   298  
   299  			// Add the dummy forward to the forwards set
   300  			forwards.Add(forward)
   301  		}
   302  	}
   303  
   304  	if forwards.Len() > 0 {
   305  		d.Set("forward", forwards)
   306  	} else if !managed {
   307  		d.SetId("")
   308  	}
   309  
   310  	return nil
   311  }
   312  
   313  func resourceCloudStackPortForwardUpdate(d *schema.ResourceData, meta interface{}) error {
   314  	// Check if the forward set as a whole has changed
   315  	if d.HasChange("forward") {
   316  		o, n := d.GetChange("forward")
   317  		ors := o.(*schema.Set).Difference(n.(*schema.Set))
   318  		nrs := n.(*schema.Set).Difference(o.(*schema.Set))
   319  
   320  		// We need to start with a rule set containing all the rules we
   321  		// already have and want to keep. Any rules that are not deleted
   322  		// correctly and any newly created rules, will be added to this
   323  		// set to make sure we end up in a consistent state
   324  		forwards := o.(*schema.Set).Intersection(n.(*schema.Set))
   325  
   326  		// First loop through all the old forwards and delete them
   327  		if ors.Len() > 0 {
   328  			err := deletePortForwards(d, meta, forwards, ors)
   329  
   330  			// We need to update this first to preserve the correct state
   331  			d.Set("forward", forwards)
   332  
   333  			if err != nil {
   334  				return err
   335  			}
   336  		}
   337  
   338  		// Then loop through all the new forwards and create them
   339  		if nrs.Len() > 0 {
   340  			err := createPortForwards(d, meta, forwards, nrs)
   341  
   342  			// We need to update this first to preserve the correct state
   343  			d.Set("forward", forwards)
   344  
   345  			if err != nil {
   346  				return err
   347  			}
   348  		}
   349  	}
   350  
   351  	return resourceCloudStackPortForwardRead(d, meta)
   352  }
   353  
   354  func resourceCloudStackPortForwardDelete(d *schema.ResourceData, meta interface{}) error {
   355  	// Create an empty rule set to hold all rules that where
   356  	// not deleted correctly
   357  	forwards := resourceCloudStackPortForward().Schema["forward"].ZeroValue().(*schema.Set)
   358  
   359  	// Delete all forwards
   360  	if ors := d.Get("forward").(*schema.Set); ors.Len() > 0 {
   361  		err := deletePortForwards(d, meta, forwards, ors)
   362  
   363  		// We need to update this first to preserve the correct state
   364  		d.Set("forward", forwards)
   365  
   366  		if err != nil {
   367  			return err
   368  		}
   369  	}
   370  
   371  	return nil
   372  }
   373  
   374  func deletePortForwards(d *schema.ResourceData, meta interface{}, forwards *schema.Set, ors *schema.Set) error {
   375  	var errs *multierror.Error
   376  
   377  	var wg sync.WaitGroup
   378  	wg.Add(ors.Len())
   379  
   380  	sem := make(chan struct{}, 10)
   381  	for _, forward := range ors.List() {
   382  		// Put a sleep here to avoid DoS'ing the API
   383  		time.Sleep(500 * time.Millisecond)
   384  
   385  		go func(forward map[string]interface{}) {
   386  			defer wg.Done()
   387  			sem <- struct{}{}
   388  
   389  			// Delete a single forward
   390  			err := deletePortForward(d, meta, forward)
   391  
   392  			// If we have a UUID, we need to save the forward
   393  			if forward["uuid"].(string) != "" {
   394  				forwards.Add(forward)
   395  			}
   396  
   397  			if err != nil {
   398  				errs = multierror.Append(errs, err)
   399  			}
   400  
   401  			<-sem
   402  		}(forward.(map[string]interface{}))
   403  	}
   404  
   405  	wg.Wait()
   406  
   407  	return errs.ErrorOrNil()
   408  }
   409  
   410  func deletePortForward(d *schema.ResourceData, meta interface{}, forward map[string]interface{}) error {
   411  	cs := meta.(*cloudstack.CloudStackClient)
   412  
   413  	// Create the parameter struct
   414  	p := cs.Firewall.NewDeletePortForwardingRuleParams(forward["uuid"].(string))
   415  
   416  	// Delete the forward
   417  	if _, err := cs.Firewall.DeletePortForwardingRule(p); err != nil {
   418  		// This is a very poor way to be told the ID does no longer exist :(
   419  		if !strings.Contains(err.Error(), fmt.Sprintf(
   420  			"Invalid parameter id value=%s due to incorrect long value format, "+
   421  				"or entity does not exist", forward["uuid"].(string))) {
   422  			return err
   423  		}
   424  	}
   425  
   426  	// Empty the UUID of this rule
   427  	forward["uuid"] = ""
   428  
   429  	return nil
   430  }
   431  
   432  func verifyPortForwardParams(d *schema.ResourceData, forward map[string]interface{}) error {
   433  	protocol := forward["protocol"].(string)
   434  	if protocol != "tcp" && protocol != "udp" {
   435  		return fmt.Errorf(
   436  			"%s is not a valid protocol. Valid options are 'tcp' and 'udp'", protocol)
   437  	}
   438  	return nil
   439  }