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 }