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