github.com/jrasell/terraform@v0.6.17-0.20160523115548-2652f5232949/builtin/providers/cloudstack/resource_cloudstack_firewall.go (about) 1 package cloudstack 2 3 import ( 4 "errors" 5 "fmt" 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 resourceCloudStackFirewall() *schema.Resource { 17 return &schema.Resource{ 18 Create: resourceCloudStackFirewallCreate, 19 Read: resourceCloudStackFirewallRead, 20 Update: resourceCloudStackFirewallUpdate, 21 Delete: resourceCloudStackFirewallDelete, 22 23 Schema: map[string]*schema.Schema{ 24 "ip_address_id": &schema.Schema{ 25 Type: schema.TypeString, 26 Optional: true, 27 ForceNew: true, 28 ConflictsWith: []string{"ipaddress"}, 29 }, 30 31 "ipaddress": &schema.Schema{ 32 Type: schema.TypeString, 33 Optional: true, 34 ForceNew: true, 35 Deprecated: "Please use the `ip_address_id` field instead", 36 ConflictsWith: []string{"ip_address_id"}, 37 }, 38 39 "managed": &schema.Schema{ 40 Type: schema.TypeBool, 41 Optional: true, 42 Default: false, 43 }, 44 45 "rule": &schema.Schema{ 46 Type: schema.TypeSet, 47 Optional: true, 48 Elem: &schema.Resource{ 49 Schema: map[string]*schema.Schema{ 50 "cidr_list": &schema.Schema{ 51 Type: schema.TypeSet, 52 Optional: true, 53 Elem: &schema.Schema{Type: schema.TypeString}, 54 Set: schema.HashString, 55 }, 56 57 "source_cidr": &schema.Schema{ 58 Type: schema.TypeString, 59 Optional: true, 60 Deprecated: "Please use the `cidr_list` field instead", 61 }, 62 63 "protocol": &schema.Schema{ 64 Type: schema.TypeString, 65 Required: true, 66 }, 67 68 "icmp_type": &schema.Schema{ 69 Type: schema.TypeInt, 70 Optional: true, 71 Computed: true, 72 }, 73 74 "icmp_code": &schema.Schema{ 75 Type: schema.TypeInt, 76 Optional: true, 77 Computed: true, 78 }, 79 80 "ports": &schema.Schema{ 81 Type: schema.TypeSet, 82 Optional: true, 83 Elem: &schema.Schema{Type: schema.TypeString}, 84 Set: schema.HashString, 85 }, 86 87 "uuids": &schema.Schema{ 88 Type: schema.TypeMap, 89 Computed: true, 90 }, 91 }, 92 }, 93 }, 94 95 "parallelism": &schema.Schema{ 96 Type: schema.TypeInt, 97 Optional: true, 98 Default: 2, 99 }, 100 }, 101 } 102 } 103 104 func resourceCloudStackFirewallCreate(d *schema.ResourceData, meta interface{}) error { 105 cs := meta.(*cloudstack.CloudStackClient) 106 107 // Make sure all required parameters are there 108 if err := verifyFirewallParams(d); err != nil { 109 return err 110 } 111 112 ipaddress, ok := d.GetOk("ip_address_id") 113 if !ok { 114 ipaddress, ok = d.GetOk("ipaddress") 115 } 116 if !ok { 117 return errors.New("Either `ip_address_id` or [deprecated] `ipaddress` must be provided.") 118 } 119 120 // Retrieve the ipaddress ID 121 ipaddressid, e := retrieveID(cs, "ip_address", ipaddress.(string)) 122 if e != nil { 123 return e.Error() 124 } 125 126 // We need to set this upfront in order to be able to save a partial state 127 d.SetId(ipaddressid) 128 129 // Create all rules that are configured 130 if nrs := d.Get("rule").(*schema.Set); nrs.Len() > 0 { 131 // Create an empty schema.Set to hold all rules 132 rules := resourceCloudStackFirewall().Schema["rule"].ZeroValue().(*schema.Set) 133 134 err := createFirewallRules(d, meta, rules, nrs) 135 136 // We need to update this first to preserve the correct state 137 d.Set("rule", rules) 138 139 if err != nil { 140 return err 141 } 142 } 143 144 return resourceCloudStackFirewallRead(d, meta) 145 } 146 func createFirewallRules( 147 d *schema.ResourceData, 148 meta interface{}, 149 rules *schema.Set, 150 nrs *schema.Set) error { 151 var errs *multierror.Error 152 153 var wg sync.WaitGroup 154 wg.Add(nrs.Len()) 155 156 sem := make(chan struct{}, d.Get("parallelism").(int)) 157 for _, rule := range nrs.List() { 158 // Put in a tiny sleep here to avoid DoS'ing the API 159 time.Sleep(500 * time.Millisecond) 160 161 go func(rule map[string]interface{}) { 162 defer wg.Done() 163 sem <- struct{}{} 164 165 // Create a single rule 166 err := createFirewallRule(d, meta, rule) 167 168 // If we have at least one UUID, we need to save the rule 169 if len(rule["uuids"].(map[string]interface{})) > 0 { 170 rules.Add(rule) 171 } 172 173 if err != nil { 174 errs = multierror.Append(errs, err) 175 } 176 177 <-sem 178 }(rule.(map[string]interface{})) 179 } 180 181 wg.Wait() 182 183 return errs.ErrorOrNil() 184 } 185 186 func createFirewallRule( 187 d *schema.ResourceData, 188 meta interface{}, 189 rule map[string]interface{}) error { 190 cs := meta.(*cloudstack.CloudStackClient) 191 uuids := rule["uuids"].(map[string]interface{}) 192 193 // Make sure all required rule parameters are there 194 if err := verifyFirewallRuleParams(d, rule); err != nil { 195 return err 196 } 197 198 // Create a new parameter struct 199 p := cs.Firewall.NewCreateFirewallRuleParams(d.Id(), rule["protocol"].(string)) 200 201 // Set the CIDR list 202 p.SetCidrlist(retrieveCidrList(rule)) 203 204 // If the protocol is ICMP set the needed ICMP parameters 205 if rule["protocol"].(string) == "icmp" { 206 p.SetIcmptype(rule["icmp_type"].(int)) 207 p.SetIcmpcode(rule["icmp_code"].(int)) 208 209 r, err := cs.Firewall.CreateFirewallRule(p) 210 if err != nil { 211 return err 212 } 213 214 uuids["icmp"] = r.Id 215 rule["uuids"] = uuids 216 } 217 218 // If protocol is not ICMP, loop through all ports 219 if rule["protocol"].(string) != "icmp" { 220 if ps := rule["ports"].(*schema.Set); ps.Len() > 0 { 221 222 // Create an empty schema.Set to hold all processed ports 223 ports := &schema.Set{F: schema.HashString} 224 225 for _, port := range ps.List() { 226 if _, ok := uuids[port.(string)]; ok { 227 ports.Add(port) 228 rule["ports"] = ports 229 continue 230 } 231 232 m := splitPorts.FindStringSubmatch(port.(string)) 233 234 startPort, err := strconv.Atoi(m[1]) 235 if err != nil { 236 return err 237 } 238 239 endPort := startPort 240 if m[2] != "" { 241 endPort, err = strconv.Atoi(m[2]) 242 if err != nil { 243 return err 244 } 245 } 246 247 p.SetStartport(startPort) 248 p.SetEndport(endPort) 249 250 r, err := cs.Firewall.CreateFirewallRule(p) 251 if err != nil { 252 return err 253 } 254 255 ports.Add(port) 256 rule["ports"] = ports 257 258 uuids[port.(string)] = r.Id 259 rule["uuids"] = uuids 260 } 261 } 262 } 263 264 return nil 265 } 266 267 func resourceCloudStackFirewallRead(d *schema.ResourceData, meta interface{}) error { 268 cs := meta.(*cloudstack.CloudStackClient) 269 270 // Get all the rules from the running environment 271 p := cs.Firewall.NewListFirewallRulesParams() 272 p.SetIpaddressid(d.Id()) 273 p.SetListall(true) 274 275 l, err := cs.Firewall.ListFirewallRules(p) 276 if err != nil { 277 return err 278 } 279 280 // Make a map of all the rules so we can easily find a rule 281 ruleMap := make(map[string]*cloudstack.FirewallRule, l.Count) 282 for _, r := range l.FirewallRules { 283 ruleMap[r.Id] = r 284 } 285 286 // Create an empty schema.Set to hold all rules 287 rules := resourceCloudStackFirewall().Schema["rule"].ZeroValue().(*schema.Set) 288 289 // Read all rules that are configured 290 if rs := d.Get("rule").(*schema.Set); rs.Len() > 0 { 291 for _, rule := range rs.List() { 292 rule := rule.(map[string]interface{}) 293 uuids := rule["uuids"].(map[string]interface{}) 294 295 if rule["protocol"].(string) == "icmp" { 296 id, ok := uuids["icmp"] 297 if !ok { 298 continue 299 } 300 301 // Get the rule 302 r, ok := ruleMap[id.(string)] 303 if !ok { 304 delete(uuids, "icmp") 305 continue 306 } 307 308 // Delete the known rule so only unknown rules remain in the ruleMap 309 delete(ruleMap, id.(string)) 310 311 // Update the values 312 rule["protocol"] = r.Protocol 313 rule["icmp_type"] = r.Icmptype 314 rule["icmp_code"] = r.Icmpcode 315 setCidrList(rule, r.Cidrlist) 316 rules.Add(rule) 317 } 318 319 // If protocol is not ICMP, loop through all ports 320 if rule["protocol"].(string) != "icmp" { 321 if ps := rule["ports"].(*schema.Set); ps.Len() > 0 { 322 323 // Create an empty schema.Set to hold all ports 324 ports := &schema.Set{F: schema.HashString} 325 326 // Loop through all ports and retrieve their info 327 for _, port := range ps.List() { 328 id, ok := uuids[port.(string)] 329 if !ok { 330 continue 331 } 332 333 // Get the rule 334 r, ok := ruleMap[id.(string)] 335 if !ok { 336 delete(uuids, port.(string)) 337 continue 338 } 339 340 // Delete the known rule so only unknown rules remain in the ruleMap 341 delete(ruleMap, id.(string)) 342 343 // Update the values 344 rule["protocol"] = r.Protocol 345 setCidrList(rule, r.Cidrlist) 346 ports.Add(port) 347 } 348 349 // If there is at least one port found, add this rule to the rules set 350 if ports.Len() > 0 { 351 rule["ports"] = ports 352 rules.Add(rule) 353 } 354 } 355 } 356 } 357 } 358 359 // If this is a managed firewall, add all unknown rules into a single dummy rule 360 managed := d.Get("managed").(bool) 361 if managed && len(ruleMap) > 0 { 362 for uuid := range ruleMap { 363 // We need to create and add a dummy value to a schema.Set as the 364 // cidr_list is a required field and thus needs a value 365 cidrs := &schema.Set{F: schema.HashString} 366 cidrs.Add(uuid) 367 368 // Make a dummy rule to hold the unknown UUID 369 rule := map[string]interface{}{ 370 "cidr_list": cidrs, 371 "protocol": uuid, 372 "uuids": map[string]interface{}{uuid: uuid}, 373 } 374 375 // Add the dummy rule to the rules set 376 rules.Add(rule) 377 } 378 } 379 380 if rules.Len() > 0 { 381 d.Set("rule", rules) 382 } else if !managed { 383 d.SetId("") 384 } 385 386 return nil 387 } 388 389 func resourceCloudStackFirewallUpdate(d *schema.ResourceData, meta interface{}) error { 390 // Make sure all required parameters are there 391 if err := verifyFirewallParams(d); err != nil { 392 return err 393 } 394 395 // Check if the rule set as a whole has changed 396 if d.HasChange("rule") { 397 o, n := d.GetChange("rule") 398 ors := o.(*schema.Set).Difference(n.(*schema.Set)) 399 nrs := n.(*schema.Set).Difference(o.(*schema.Set)) 400 401 // We need to start with a rule set containing all the rules we 402 // already have and want to keep. Any rules that are not deleted 403 // correctly and any newly created rules, will be added to this 404 // set to make sure we end up in a consistent state 405 rules := o.(*schema.Set).Intersection(n.(*schema.Set)) 406 407 // First loop through all the old rules and delete them 408 if ors.Len() > 0 { 409 err := deleteFirewallRules(d, meta, rules, ors) 410 411 // We need to update this first to preserve the correct state 412 d.Set("rule", rules) 413 414 if err != nil { 415 return err 416 } 417 } 418 419 // Then loop through all the new rules and create them 420 if nrs.Len() > 0 { 421 err := createFirewallRules(d, meta, rules, nrs) 422 423 // We need to update this first to preserve the correct state 424 d.Set("rule", rules) 425 426 if err != nil { 427 return err 428 } 429 } 430 } 431 432 return resourceCloudStackFirewallRead(d, meta) 433 } 434 435 func resourceCloudStackFirewallDelete(d *schema.ResourceData, meta interface{}) error { 436 // Create an empty rule set to hold all rules that where 437 // not deleted correctly 438 rules := resourceCloudStackFirewall().Schema["rule"].ZeroValue().(*schema.Set) 439 440 // Delete all rules 441 if ors := d.Get("rule").(*schema.Set); ors.Len() > 0 { 442 err := deleteFirewallRules(d, meta, rules, ors) 443 444 // We need to update this first to preserve the correct state 445 d.Set("rule", rules) 446 447 if err != nil { 448 return err 449 } 450 } 451 452 return nil 453 } 454 455 func deleteFirewallRules( 456 d *schema.ResourceData, 457 meta interface{}, 458 rules *schema.Set, 459 ors *schema.Set) error { 460 var errs *multierror.Error 461 462 var wg sync.WaitGroup 463 wg.Add(ors.Len()) 464 465 sem := make(chan struct{}, d.Get("parallelism").(int)) 466 for _, rule := range ors.List() { 467 // Put a sleep here to avoid DoS'ing the API 468 time.Sleep(500 * time.Millisecond) 469 470 go func(rule map[string]interface{}) { 471 defer wg.Done() 472 sem <- struct{}{} 473 474 // Delete a single rule 475 err := deleteFirewallRule(d, meta, rule) 476 477 // If we have at least one UUID, we need to save the rule 478 if len(rule["uuids"].(map[string]interface{})) > 0 { 479 rules.Add(rule) 480 } 481 482 if err != nil { 483 errs = multierror.Append(errs, err) 484 } 485 486 <-sem 487 }(rule.(map[string]interface{})) 488 } 489 490 wg.Wait() 491 492 return errs.ErrorOrNil() 493 } 494 495 func deleteFirewallRule( 496 d *schema.ResourceData, 497 meta interface{}, 498 rule map[string]interface{}) error { 499 cs := meta.(*cloudstack.CloudStackClient) 500 uuids := rule["uuids"].(map[string]interface{}) 501 502 for k, id := range uuids { 503 // We don't care about the count here, so just continue 504 if k == "#" { 505 continue 506 } 507 508 // Create the parameter struct 509 p := cs.Firewall.NewDeleteFirewallRuleParams(id.(string)) 510 511 // Delete the rule 512 if _, err := cs.Firewall.DeleteFirewallRule(p); err != nil { 513 514 // This is a very poor way to be told the ID does no longer exist :( 515 if strings.Contains(err.Error(), fmt.Sprintf( 516 "Invalid parameter id value=%s due to incorrect long value format, "+ 517 "or entity does not exist", id.(string))) { 518 delete(uuids, k) 519 continue 520 } 521 522 return err 523 } 524 525 // Delete the UUID of this rule 526 delete(uuids, k) 527 rule["uuids"] = uuids 528 } 529 530 return nil 531 } 532 533 func verifyFirewallParams(d *schema.ResourceData) error { 534 managed := d.Get("managed").(bool) 535 _, rules := d.GetOk("rule") 536 537 if !rules && !managed { 538 return fmt.Errorf( 539 "You must supply at least one 'rule' when not using the 'managed' firewall feature") 540 } 541 542 return nil 543 } 544 545 func verifyFirewallRuleParams(d *schema.ResourceData, rule map[string]interface{}) error { 546 cidrList := rule["cidr_list"].(*schema.Set) 547 sourceCidr := rule["source_cidr"].(string) 548 if cidrList.Len() == 0 && sourceCidr == "" { 549 return fmt.Errorf( 550 "Parameter cidr_list is a required parameter") 551 } 552 if cidrList.Len() > 0 && sourceCidr != "" { 553 return fmt.Errorf( 554 "Parameter source_cidr is deprecated and cannot be used together with cidr_list") 555 } 556 557 protocol := rule["protocol"].(string) 558 if protocol != "tcp" && protocol != "udp" && protocol != "icmp" { 559 return fmt.Errorf( 560 "%q is not a valid protocol. Valid options are 'tcp', 'udp' and 'icmp'", protocol) 561 } 562 563 if protocol == "icmp" { 564 if _, ok := rule["icmp_type"]; !ok { 565 return fmt.Errorf( 566 "Parameter icmp_type is a required parameter when using protocol 'icmp'") 567 } 568 if _, ok := rule["icmp_code"]; !ok { 569 return fmt.Errorf( 570 "Parameter icmp_code is a required parameter when using protocol 'icmp'") 571 } 572 } else { 573 if ports, ok := rule["ports"].(*schema.Set); ok { 574 for _, port := range ports.List() { 575 m := splitPorts.FindStringSubmatch(port.(string)) 576 if m == nil { 577 return fmt.Errorf( 578 "%q is not a valid port value. Valid options are '80' or '80-90'", port.(string)) 579 } 580 } 581 } else { 582 return fmt.Errorf( 583 "Parameter ports is a required parameter when *not* using protocol 'icmp'") 584 } 585 } 586 587 return nil 588 }