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