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