github.com/aspring/terraform@v0.8.2-0.20161216122603-6a8619a5db2e/builtin/providers/cloudstack/resource_cloudstack_port_forward.go (about)

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