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 }