github.com/jrasell/terraform@v0.6.17-0.20160523115548-2652f5232949/builtin/providers/cloudstack/resource_cloudstack_port_forward.go (about)

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