github.com/vtorhonen/terraform@v0.9.0-beta2.0.20170307220345-5d894e4ffda7/builtin/providers/cloudstack/resource_cloudstack_security_group_rule.go (about) 1 package cloudstack 2 3 import ( 4 "fmt" 5 "log" 6 "strconv" 7 "strings" 8 "sync" 9 "time" 10 11 multierror "github.com/hashicorp/go-multierror" 12 "github.com/hashicorp/terraform/helper/schema" 13 "github.com/xanzy/go-cloudstack/cloudstack" 14 ) 15 16 type authorizeSecurityGroupParams interface { 17 SetCidrlist([]string) 18 SetIcmptype(int) 19 SetIcmpcode(int) 20 SetStartport(int) 21 SetEndport(int) 22 SetProtocol(string) 23 SetSecuritygroupid(string) 24 SetUsersecuritygrouplist(map[string]string) 25 } 26 27 func resourceCloudStackSecurityGroupRule() *schema.Resource { 28 return &schema.Resource{ 29 Create: resourceCloudStackSecurityGroupRuleCreate, 30 Read: resourceCloudStackSecurityGroupRuleRead, 31 Update: resourceCloudStackSecurityGroupRuleUpdate, 32 Delete: resourceCloudStackSecurityGroupRuleDelete, 33 34 Schema: map[string]*schema.Schema{ 35 "security_group_id": &schema.Schema{ 36 Type: schema.TypeString, 37 Required: true, 38 ForceNew: true, 39 }, 40 41 "rule": &schema.Schema{ 42 Type: schema.TypeSet, 43 Required: true, 44 Elem: &schema.Resource{ 45 Schema: map[string]*schema.Schema{ 46 "cidr_list": &schema.Schema{ 47 Type: schema.TypeSet, 48 Optional: true, 49 Elem: &schema.Schema{Type: schema.TypeString}, 50 Set: schema.HashString, 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 "traffic_type": &schema.Schema{ 78 Type: schema.TypeString, 79 Optional: true, 80 Default: "ingress", 81 }, 82 83 "user_security_group_list": &schema.Schema{ 84 Type: schema.TypeSet, 85 Optional: true, 86 Elem: &schema.Schema{Type: schema.TypeString}, 87 Set: schema.HashString, 88 }, 89 90 "uuids": &schema.Schema{ 91 Type: schema.TypeMap, 92 Computed: true, 93 }, 94 }, 95 }, 96 }, 97 98 "project": &schema.Schema{ 99 Type: schema.TypeString, 100 Optional: true, 101 ForceNew: true, 102 }, 103 104 "parallelism": &schema.Schema{ 105 Type: schema.TypeInt, 106 Optional: true, 107 Default: 2, 108 }, 109 }, 110 } 111 } 112 113 func resourceCloudStackSecurityGroupRuleCreate(d *schema.ResourceData, meta interface{}) error { 114 // We need to set this upfront in order to be able to save a partial state 115 d.SetId(d.Get("security_group_id").(string)) 116 117 // Create all rules that are configured 118 if nrs := d.Get("rule").(*schema.Set); nrs.Len() > 0 { 119 // Create an empty rule set to hold all newly created rules 120 rules := resourceCloudStackSecurityGroupRule().Schema["rule"].ZeroValue().(*schema.Set) 121 122 err := createSecurityGroupRules(d, meta, rules, nrs) 123 124 // We need to update this first to preserve the correct state 125 d.Set("rule", rules) 126 127 if err != nil { 128 return err 129 } 130 } 131 132 return resourceCloudStackSecurityGroupRuleRead(d, meta) 133 } 134 135 func createSecurityGroupRules(d *schema.ResourceData, meta interface{}, rules *schema.Set, nrs *schema.Set) error { 136 cs := meta.(*cloudstack.CloudStackClient) 137 var errs *multierror.Error 138 139 var wg sync.WaitGroup 140 wg.Add(nrs.Len()) 141 142 sem := make(chan struct{}, d.Get("parallelism").(int)) 143 for _, rule := range nrs.List() { 144 // Put in a tiny sleep here to avoid DoS'ing the API 145 time.Sleep(500 * time.Millisecond) 146 147 go func(rule map[string]interface{}) { 148 defer wg.Done() 149 sem <- struct{}{} 150 151 // Make sure all required parameters are there 152 if err := verifySecurityGroupRuleParams(d, rule); err != nil { 153 errs = multierror.Append(errs, err) 154 return 155 } 156 157 var p authorizeSecurityGroupParams 158 159 if cidrList, ok := rule["cidr_list"].(*schema.Set); ok && cidrList.Len() > 0 { 160 for _, cidr := range cidrList.List() { 161 // Create a new parameter struct 162 switch rule["traffic_type"].(string) { 163 case "ingress": 164 p = cs.SecurityGroup.NewAuthorizeSecurityGroupIngressParams() 165 case "egress": 166 p = cs.SecurityGroup.NewAuthorizeSecurityGroupEgressParams() 167 } 168 169 p.SetSecuritygroupid(d.Id()) 170 p.SetCidrlist([]string{cidr.(string)}) 171 172 // Create a single rule 173 err := createSecurityGroupRule(d, meta, rule, p, cidr.(string)) 174 if err != nil { 175 errs = multierror.Append(errs, err) 176 } 177 } 178 } 179 180 if usgList, ok := rule["user_security_group_list"].(*schema.Set); ok && usgList.Len() > 0 { 181 for _, usg := range usgList.List() { 182 sg, _, err := cs.SecurityGroup.GetSecurityGroupByName( 183 usg.(string), 184 cloudstack.WithProject(d.Get("project").(string)), 185 ) 186 if err != nil { 187 errs = multierror.Append(errs, err) 188 continue 189 } 190 191 // Create a new parameter struct 192 switch rule["traffic_type"].(string) { 193 case "ingress": 194 p = cs.SecurityGroup.NewAuthorizeSecurityGroupIngressParams() 195 case "egress": 196 p = cs.SecurityGroup.NewAuthorizeSecurityGroupEgressParams() 197 } 198 199 p.SetSecuritygroupid(d.Id()) 200 p.SetUsersecuritygrouplist(map[string]string{sg.Account: usg.(string)}) 201 202 // Create a single rule 203 err = createSecurityGroupRule(d, meta, rule, p, usg.(string)) 204 if err != nil { 205 errs = multierror.Append(errs, err) 206 } 207 } 208 } 209 210 // If we have at least one UUID, we need to save the rule 211 if len(rule["uuids"].(map[string]interface{})) > 0 { 212 rules.Add(rule) 213 } 214 215 <-sem 216 }(rule.(map[string]interface{})) 217 } 218 219 wg.Wait() 220 221 return errs.ErrorOrNil() 222 } 223 224 func createSecurityGroupRule(d *schema.ResourceData, meta interface{}, rule map[string]interface{}, p authorizeSecurityGroupParams, uuid string) error { 225 cs := meta.(*cloudstack.CloudStackClient) 226 uuids := rule["uuids"].(map[string]interface{}) 227 228 // Set the protocol 229 p.SetProtocol(rule["protocol"].(string)) 230 231 // If the protocol is ICMP set the needed ICMP parameters 232 if rule["protocol"].(string) == "icmp" { 233 p.SetIcmptype(rule["icmp_type"].(int)) 234 p.SetIcmpcode(rule["icmp_code"].(int)) 235 236 ruleID, err := createIngressOrEgressRule(cs, p) 237 if err != nil { 238 return err 239 } 240 241 uuids[uuid+"icmp"] = ruleID 242 rule["uuids"] = uuids 243 } 244 245 // If protocol is TCP or UDP, loop through all ports 246 if rule["protocol"].(string) == "tcp" || rule["protocol"].(string) == "udp" { 247 if ps := rule["ports"].(*schema.Set); ps.Len() > 0 { 248 249 // Create an empty schema.Set to hold all processed ports 250 ports := &schema.Set{F: schema.HashString} 251 252 for _, port := range ps.List() { 253 if _, ok := uuids[uuid+port.(string)]; ok { 254 ports.Add(port) 255 rule["ports"] = ports 256 continue 257 } 258 259 m := splitPorts.FindStringSubmatch(port.(string)) 260 261 startPort, err := strconv.Atoi(m[1]) 262 if err != nil { 263 return err 264 } 265 266 endPort := startPort 267 if m[2] != "" { 268 endPort, err = strconv.Atoi(m[2]) 269 if err != nil { 270 return err 271 } 272 } 273 274 p.SetStartport(startPort) 275 p.SetEndport(endPort) 276 277 ruleID, err := createIngressOrEgressRule(cs, p) 278 if err != nil { 279 return err 280 } 281 282 ports.Add(port) 283 rule["ports"] = ports 284 285 uuids[uuid+port.(string)] = ruleID 286 rule["uuids"] = uuids 287 } 288 } 289 } 290 291 return nil 292 } 293 294 func createIngressOrEgressRule(cs *cloudstack.CloudStackClient, p authorizeSecurityGroupParams) (string, error) { 295 switch p := p.(type) { 296 case *cloudstack.AuthorizeSecurityGroupIngressParams: 297 r, err := cs.SecurityGroup.AuthorizeSecurityGroupIngress(p) 298 if err != nil { 299 return "", err 300 } 301 return r.Ruleid, nil 302 case *cloudstack.AuthorizeSecurityGroupEgressParams: 303 r, err := cs.SecurityGroup.AuthorizeSecurityGroupEgress(p) 304 if err != nil { 305 return "", err 306 } 307 return r.Ruleid, nil 308 default: 309 return "", fmt.Errorf("Unknown authorize security group rule type: %v", p) 310 } 311 } 312 313 func resourceCloudStackSecurityGroupRuleRead(d *schema.ResourceData, meta interface{}) error { 314 cs := meta.(*cloudstack.CloudStackClient) 315 316 // Get the security group details 317 sg, count, err := cs.SecurityGroup.GetSecurityGroupByID( 318 d.Id(), 319 cloudstack.WithProject(d.Get("project").(string)), 320 ) 321 if err != nil { 322 if count == 0 { 323 log.Printf("[DEBUG] Security group %s does not longer exist", d.Get("name").(string)) 324 d.SetId("") 325 return nil 326 } 327 328 return err 329 } 330 331 // Make a map of all the rule indexes so we can easily find a rule 332 sgRules := append(sg.Ingressrule, sg.Egressrule...) 333 ruleIndex := make(map[string]int, len(sgRules)) 334 for idx, r := range sgRules { 335 ruleIndex[r.Ruleid] = idx 336 } 337 338 // Create an empty schema.Set to hold all rules 339 rules := resourceCloudStackSecurityGroupRule().Schema["rule"].ZeroValue().(*schema.Set) 340 341 // Read all rules that are configured 342 if rs := d.Get("rule").(*schema.Set); rs.Len() > 0 { 343 for _, rule := range rs.List() { 344 rule := rule.(map[string]interface{}) 345 346 // First get any existing values 347 cidrList, cidrListOK := rule["cidr_list"].(*schema.Set) 348 usgList, usgListOk := rule["user_security_group_list"].(*schema.Set) 349 350 // Then reset the values to a new empty set 351 rule["cidr_list"] = &schema.Set{F: schema.HashString} 352 rule["user_security_group_list"] = &schema.Set{F: schema.HashString} 353 354 if cidrListOK && cidrList.Len() > 0 { 355 for _, cidr := range cidrList.List() { 356 readSecurityGroupRule(sg, ruleIndex, rule, cidr.(string)) 357 } 358 } 359 360 if usgListOk && usgList.Len() > 0 { 361 for _, usg := range usgList.List() { 362 readSecurityGroupRule(sg, ruleIndex, rule, usg.(string)) 363 } 364 } 365 366 rules.Add(rule) 367 } 368 } 369 370 return nil 371 } 372 373 func readSecurityGroupRule(sg *cloudstack.SecurityGroup, ruleIndex map[string]int, rule map[string]interface{}, uuid string) { 374 uuids := rule["uuids"].(map[string]interface{}) 375 sgRules := append(sg.Ingressrule, sg.Egressrule...) 376 377 if rule["protocol"].(string) == "icmp" { 378 id, ok := uuids[uuid+"icmp"] 379 if !ok { 380 return 381 } 382 383 // Get the rule 384 idx, ok := ruleIndex[id.(string)] 385 if !ok { 386 delete(uuids, uuid+"icmp") 387 return 388 } 389 390 r := sgRules[idx] 391 392 // Update the values 393 if r.Cidr != "" { 394 rule["cidr_list"].(*schema.Set).Add(r.Cidr) 395 } 396 397 if r.Securitygroupname != "" { 398 rule["user_security_group_list"].(*schema.Set).Add(r.Securitygroupname) 399 } 400 401 rule["protocol"] = r.Protocol 402 rule["icmp_type"] = r.Icmptype 403 rule["icmp_code"] = r.Icmpcode 404 } 405 406 // If protocol is tcp or udp, loop through all ports 407 if rule["protocol"].(string) == "tcp" || rule["protocol"].(string) == "udp" { 408 if ps := rule["ports"].(*schema.Set); ps.Len() > 0 { 409 410 // Create an empty schema.Set to hold all ports 411 ports := &schema.Set{F: schema.HashString} 412 413 // Loop through all ports and retrieve their info 414 for _, port := range ps.List() { 415 id, ok := uuids[uuid+port.(string)] 416 if !ok { 417 continue 418 } 419 420 // Get the rule 421 idx, ok := ruleIndex[id.(string)] 422 if !ok { 423 delete(uuids, uuid+port.(string)) 424 continue 425 } 426 427 r := sgRules[idx] 428 429 // Create a set with all CIDR's 430 cidrs := &schema.Set{F: schema.HashString} 431 for _, cidr := range strings.Split(r.Cidr, ",") { 432 cidrs.Add(cidr) 433 } 434 435 // Update the values 436 rule["protocol"] = r.Protocol 437 ports.Add(port) 438 } 439 440 // If there is at least one port found, add this rule to the rules set 441 if ports.Len() > 0 { 442 rule["ports"] = ports 443 } 444 } 445 } 446 } 447 448 func resourceCloudStackSecurityGroupRuleUpdate(d *schema.ResourceData, meta interface{}) error { 449 // Check if the rule set as a whole has changed 450 if d.HasChange("rule") { 451 o, n := d.GetChange("rule") 452 ors := o.(*schema.Set).Difference(n.(*schema.Set)) 453 nrs := n.(*schema.Set).Difference(o.(*schema.Set)) 454 455 // We need to start with a rule set containing all the rules we 456 // already have and want to keep. Any rules that are not deleted 457 // correctly and any newly created rules, will be added to this 458 // set to make sure we end up in a consistent state 459 rules := o.(*schema.Set).Intersection(n.(*schema.Set)) 460 461 // First loop through all the old rules destroy them 462 if ors.Len() > 0 { 463 err := deleteSecurityGroupRules(d, meta, rules, ors) 464 465 // We need to update this first to preserve the correct state 466 d.Set("rule", rules) 467 468 if err != nil { 469 return err 470 } 471 } 472 473 // Then loop through all the new rules and delete them 474 if nrs.Len() > 0 { 475 err := createSecurityGroupRules(d, meta, rules, nrs) 476 477 // We need to update this first to preserve the correct state 478 d.Set("rule", rules) 479 480 if err != nil { 481 return err 482 } 483 } 484 } 485 486 return resourceCloudStackSecurityGroupRuleRead(d, meta) 487 } 488 489 func resourceCloudStackSecurityGroupRuleDelete(d *schema.ResourceData, meta interface{}) error { 490 // Create an empty rule set to hold all rules that where 491 // not deleted correctly 492 rules := resourceCloudStackSecurityGroupRule().Schema["rule"].ZeroValue().(*schema.Set) 493 494 // Delete all rules 495 if ors := d.Get("rule").(*schema.Set); ors.Len() > 0 { 496 err := deleteSecurityGroupRules(d, meta, rules, ors) 497 498 // We need to update this first to preserve the correct state 499 d.Set("rule", rules) 500 501 if err != nil { 502 return err 503 } 504 } 505 506 return nil 507 } 508 509 func deleteSecurityGroupRules(d *schema.ResourceData, meta interface{}, rules *schema.Set, ors *schema.Set) error { 510 var errs *multierror.Error 511 512 var wg sync.WaitGroup 513 wg.Add(ors.Len()) 514 515 sem := make(chan struct{}, d.Get("parallelism").(int)) 516 for _, rule := range ors.List() { 517 // Put a sleep here to avoid DoS'ing the API 518 time.Sleep(500 * time.Millisecond) 519 520 go func(rule map[string]interface{}) { 521 defer wg.Done() 522 sem <- struct{}{} 523 524 // Create a single rule 525 err := deleteSecurityGroupRule(d, meta, rule) 526 if err != nil { 527 errs = multierror.Append(errs, err) 528 } 529 530 // If we have at least one UUID, we need to save the rule 531 if len(rule["uuids"].(map[string]interface{})) > 0 { 532 rules.Add(rule) 533 } 534 535 <-sem 536 }(rule.(map[string]interface{})) 537 } 538 539 wg.Wait() 540 541 return errs.ErrorOrNil() 542 } 543 544 func deleteSecurityGroupRule(d *schema.ResourceData, meta interface{}, rule map[string]interface{}) error { 545 cs := meta.(*cloudstack.CloudStackClient) 546 uuids := rule["uuids"].(map[string]interface{}) 547 548 for k, id := range uuids { 549 // We don't care about the count here, so just continue 550 if k == "%" { 551 continue 552 } 553 554 var err error 555 switch rule["traffic_type"].(string) { 556 case "ingress": 557 p := cs.SecurityGroup.NewRevokeSecurityGroupIngressParams(id.(string)) 558 _, err = cs.SecurityGroup.RevokeSecurityGroupIngress(p) 559 case "egress": 560 p := cs.SecurityGroup.NewRevokeSecurityGroupEgressParams(id.(string)) 561 _, err = cs.SecurityGroup.RevokeSecurityGroupEgress(p) 562 } 563 564 if err != nil { 565 // This is a very poor way to be told the ID does no longer exist :( 566 if strings.Contains(err.Error(), fmt.Sprintf( 567 "Invalid parameter id value=%s due to incorrect long value format, "+ 568 "or entity does not exist", id.(string))) { 569 delete(uuids, k) 570 continue 571 } 572 573 return err 574 } 575 576 // Delete the UUID of this rule 577 delete(uuids, k) 578 } 579 580 return nil 581 } 582 583 func verifySecurityGroupRuleParams(d *schema.ResourceData, rule map[string]interface{}) error { 584 cidrList, cidrListOK := rule["cidr_list"].(*schema.Set) 585 usgList, usgListOK := rule["user_security_group_list"].(*schema.Set) 586 587 if (!cidrListOK || cidrList.Len() == 0) && (!usgListOK || usgList.Len() == 0) { 588 return fmt.Errorf( 589 "You must supply at least one 'cidr_list' or `user_security_group_ids` entry") 590 } 591 592 protocol := rule["protocol"].(string) 593 switch protocol { 594 case "icmp": 595 if _, ok := rule["icmp_type"]; !ok { 596 return fmt.Errorf( 597 "Parameter icmp_type is a required parameter when using protocol 'icmp'") 598 } 599 if _, ok := rule["icmp_code"]; !ok { 600 return fmt.Errorf( 601 "Parameter icmp_code is a required parameter when using protocol 'icmp'") 602 } 603 case "tcp", "udp": 604 if ports, ok := rule["ports"].(*schema.Set); ok { 605 for _, port := range ports.List() { 606 m := splitPorts.FindStringSubmatch(port.(string)) 607 if m == nil { 608 return fmt.Errorf( 609 "%q is not a valid port value. Valid options are '80' or '80-90'", port.(string)) 610 } 611 } 612 } else { 613 return fmt.Errorf( 614 "Parameter ports is a required parameter when *not* using protocol 'icmp'") 615 } 616 default: 617 _, err := strconv.ParseInt(protocol, 0, 0) 618 if err != nil { 619 return fmt.Errorf( 620 "%q is not a valid protocol. Valid options are 'tcp', 'udp' and 'icmp'", protocol) 621 } 622 } 623 624 traffic := rule["traffic_type"].(string) 625 if traffic != "ingress" && traffic != "egress" { 626 return fmt.Errorf( 627 "Parameter traffic_type only accepts 'ingress' or 'egress' as values") 628 } 629 630 return nil 631 }