| // |
| // Licensed to the Apache Software Foundation (ASF) under one |
| // or more contributor license agreements. See the NOTICE file |
| // distributed with this work for additional information |
| // regarding copyright ownership. The ASF licenses this file |
| // to you under the Apache License, Version 2.0 (the |
| // "License"); you may not use this file except in compliance |
| // with the License. You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, |
| // software distributed under the License is distributed on an |
| // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| // KIND, either express or implied. See the License for the |
| // specific language governing permissions and limitations |
| // under the License. |
| // |
| |
| package cloudstack |
| |
| import ( |
| "fmt" |
| "log" |
| "net" |
| "strconv" |
| "strings" |
| |
| "github.com/apache/cloudstack-go/v2/cloudstack" |
| "github.com/hashicorp/terraform/helper/schema" |
| ) |
| |
| const none = "none" |
| |
| func resourceCloudStackNetwork() *schema.Resource { |
| aclidSchema := &schema.Schema{ |
| Type: schema.TypeString, |
| Optional: true, |
| Default: none, |
| } |
| |
| aclidSchema.StateFunc = func(v interface{}) string { |
| value := v.(string) |
| |
| if value == none { |
| aclidSchema.ForceNew = true |
| } else { |
| aclidSchema.ForceNew = false |
| } |
| |
| return value |
| } |
| |
| return &schema.Resource{ |
| Create: resourceCloudStackNetworkCreate, |
| Read: resourceCloudStackNetworkRead, |
| Update: resourceCloudStackNetworkUpdate, |
| Delete: resourceCloudStackNetworkDelete, |
| Importer: &schema.ResourceImporter{ |
| State: importStatePassthrough, |
| }, |
| |
| Schema: map[string]*schema.Schema{ |
| "name": { |
| Type: schema.TypeString, |
| Required: true, |
| }, |
| |
| "display_text": { |
| Type: schema.TypeString, |
| Optional: true, |
| Computed: true, |
| }, |
| |
| "cidr": { |
| Type: schema.TypeString, |
| Required: true, |
| ForceNew: true, |
| }, |
| |
| "gateway": { |
| Type: schema.TypeString, |
| Optional: true, |
| Computed: true, |
| ForceNew: true, |
| }, |
| |
| "startip": { |
| Type: schema.TypeString, |
| Optional: true, |
| Computed: true, |
| ForceNew: true, |
| }, |
| |
| "endip": { |
| Type: schema.TypeString, |
| Optional: true, |
| Computed: true, |
| ForceNew: true, |
| }, |
| |
| "network_domain": { |
| Type: schema.TypeString, |
| Optional: true, |
| Computed: true, |
| }, |
| |
| "network_offering": { |
| Type: schema.TypeString, |
| Required: true, |
| }, |
| |
| "vlan": { |
| Type: schema.TypeInt, |
| Optional: true, |
| ForceNew: true, |
| }, |
| |
| "vpc_id": { |
| Type: schema.TypeString, |
| Optional: true, |
| ForceNew: true, |
| }, |
| |
| "acl_id": aclidSchema, |
| |
| "project": { |
| Type: schema.TypeString, |
| Optional: true, |
| Computed: true, |
| ForceNew: true, |
| }, |
| |
| "source_nat_ip": { |
| Type: schema.TypeBool, |
| Optional: true, |
| ForceNew: true, |
| }, |
| |
| "source_nat_ip_id": { |
| Type: schema.TypeString, |
| Computed: true, |
| }, |
| |
| "zone": { |
| Type: schema.TypeString, |
| Required: true, |
| ForceNew: true, |
| }, |
| |
| "tags": tagsSchema(), |
| }, |
| } |
| } |
| |
| func resourceCloudStackNetworkCreate(d *schema.ResourceData, meta interface{}) error { |
| cs := meta.(*cloudstack.CloudStackClient) |
| d.Partial(true) |
| |
| name := d.Get("name").(string) |
| |
| // Retrieve the network_offering ID |
| networkofferingid, e := retrieveID(cs, "network_offering", d.Get("network_offering").(string)) |
| if e != nil { |
| return e.Error() |
| } |
| |
| // Retrieve the zone ID |
| zoneid, e := retrieveID(cs, "zone", d.Get("zone").(string)) |
| if e != nil { |
| return e.Error() |
| } |
| |
| // Compute/set the display text |
| displaytext, ok := d.GetOk("display_text") |
| if !ok { |
| displaytext = name |
| } |
| |
| // Create a new parameter struct |
| p := cs.Network.NewCreateNetworkParams(displaytext.(string), name, networkofferingid, zoneid) |
| |
| // Get the network offering to check if it supports specifying IP ranges |
| no, _, err := cs.NetworkOffering.GetNetworkOfferingByID(networkofferingid) |
| if err != nil { |
| return err |
| } |
| |
| m, err := parseCIDR(d, no.Specifyipranges) |
| if err != nil { |
| return err |
| } |
| |
| // Set the needed IP config |
| p.SetGateway(m["gateway"]) |
| p.SetNetmask(m["netmask"]) |
| |
| // Only set the start IP if we have one |
| if startip, ok := m["startip"]; ok { |
| p.SetStartip(startip) |
| } |
| |
| // Only set the end IP if we have one |
| if endip, ok := m["endip"]; ok { |
| p.SetEndip(endip) |
| } |
| |
| // Set the network domain if we have one |
| if networkDomain, ok := d.GetOk("network_domain"); ok { |
| p.SetNetworkdomain(networkDomain.(string)) |
| } |
| |
| if vlan, ok := d.GetOk("vlan"); ok { |
| p.SetVlan(strconv.Itoa(vlan.(int))) |
| } |
| |
| // Check is this network needs to be created in a VPC |
| if vpcid, ok := d.GetOk("vpc_id"); ok { |
| // Set the vpc id |
| p.SetVpcid(vpcid.(string)) |
| |
| // Since we're in a VPC, check if we want to assiciate an ACL list |
| if aclid, ok := d.GetOk("acl_id"); ok && aclid.(string) != none { |
| // Set the acl ID |
| p.SetAclid(aclid.(string)) |
| } |
| } |
| |
| // If there is a project supplied, we retrieve and set the project id |
| if err := setProjectid(p, cs, d); err != nil { |
| return err |
| } |
| |
| // Create the new network |
| r, err := cs.Network.CreateNetwork(p) |
| if err != nil { |
| return fmt.Errorf("Error creating network %s: %s", name, err) |
| } |
| |
| d.SetPartial("name") |
| d.SetPartial("display_text") |
| d.SetPartial("cidr") |
| d.SetPartial("gateway") |
| d.SetPartial("startip") |
| d.SetPartial("endip") |
| d.SetPartial("network_domain") |
| d.SetPartial("network_offering") |
| d.SetPartial("vlan") |
| d.SetPartial("vpc_id") |
| d.SetPartial("acl_id") |
| d.SetPartial("project") |
| d.SetPartial("zone") |
| |
| d.SetId(r.Id) |
| |
| // Set tags if necessary |
| if err = setTags(cs, d, "network"); err != nil { |
| return fmt.Errorf("Error setting tags: %v", err) |
| } |
| d.SetPartial("tags") |
| |
| if d.Get("source_nat_ip").(bool) { |
| // Create a new parameter struct |
| p := cs.Address.NewAssociateIpAddressParams() |
| |
| // Set required options |
| p.SetNetworkid(r.Id) |
| p.SetZoneid(zoneid) |
| |
| if vpcid, ok := d.GetOk("vpc_id"); ok { |
| // Set the vpcid |
| p.SetVpcid(vpcid.(string)) |
| } |
| |
| // If there is a project supplied, we retrieve and set the project id |
| if err := setProjectid(p, cs, d); err != nil { |
| return err |
| } |
| |
| // Associate a new IP address |
| ip, err := cs.Address.AssociateIpAddress(p) |
| if err != nil { |
| return fmt.Errorf("Error associating a new IP address: %s", err) |
| } |
| d.Set("source_nat_ip_id", ip.Id) |
| |
| // Set the additional partial |
| d.SetPartial("source_nat_ip") |
| d.SetPartial("source_nat_ip_id") |
| } |
| |
| d.Partial(false) |
| return resourceCloudStackNetworkRead(d, meta) |
| } |
| |
| func resourceCloudStackNetworkRead(d *schema.ResourceData, meta interface{}) error { |
| cs := meta.(*cloudstack.CloudStackClient) |
| |
| // Get the virtual machine details |
| n, count, err := cs.Network.GetNetworkByID( |
| d.Id(), |
| cloudstack.WithProject(d.Get("project").(string)), |
| ) |
| if err != nil { |
| if count == 0 { |
| log.Printf( |
| "[DEBUG] Network %s does no longer exist", d.Get("name").(string)) |
| d.SetId("") |
| return nil |
| } |
| |
| return err |
| } |
| |
| d.Set("name", n.Name) |
| d.Set("display_text", n.Displaytext) |
| d.Set("cidr", n.Cidr) |
| d.Set("gateway", n.Gateway) |
| d.Set("network_domain", n.Networkdomain) |
| d.Set("vpc_id", n.Vpcid) |
| |
| if n.Aclid == "" { |
| n.Aclid = none |
| } |
| d.Set("acl_id", n.Aclid) |
| |
| tags := make(map[string]interface{}) |
| for _, tag := range n.Tags { |
| tags[tag.Key] = tag.Value |
| } |
| d.Set("tags", tags) |
| |
| setValueOrID(d, "network_offering", n.Networkofferingname, n.Networkofferingid) |
| setValueOrID(d, "project", n.Project, n.Projectid) |
| setValueOrID(d, "zone", n.Zonename, n.Zoneid) |
| |
| if d.Get("source_nat_ip").(bool) { |
| ip, count, err := cs.Address.GetPublicIpAddressByID( |
| d.Get("source_nat_ip_id").(string), |
| cloudstack.WithProject(d.Get("project").(string)), |
| ) |
| if err != nil { |
| if count == 0 { |
| log.Printf( |
| "[DEBUG] Source NAT IP with ID %s is no longer associated", d.Id()) |
| d.Set("source_nat_ip", false) |
| d.Set("source_nat_ip_id", "") |
| return nil |
| } |
| |
| return err |
| } |
| |
| if n.Id != ip.Associatednetworkid { |
| d.Set("source_nat_ip", false) |
| d.Set("source_nat_ip_id", "") |
| } |
| } |
| |
| return nil |
| } |
| |
| func resourceCloudStackNetworkUpdate(d *schema.ResourceData, meta interface{}) error { |
| cs := meta.(*cloudstack.CloudStackClient) |
| name := d.Get("name").(string) |
| |
| // Create a new parameter struct |
| p := cs.Network.NewUpdateNetworkParams(d.Id()) |
| |
| // Check if the name or display text is changed |
| if d.HasChange("name") || d.HasChange("display_text") { |
| p.SetName(name) |
| |
| // Compute/set the display text |
| displaytext := d.Get("display_text").(string) |
| if displaytext == "" { |
| displaytext = name |
| } |
| p.SetDisplaytext(displaytext) |
| } |
| |
| // Check if the cidr is changed |
| if d.HasChange("cidr") { |
| p.SetGuestvmcidr(d.Get("cidr").(string)) |
| } |
| |
| // Check if the network domain is changed |
| if d.HasChange("network_domain") { |
| p.SetNetworkdomain(d.Get("network_domain").(string)) |
| } |
| |
| // Check if the network offering is changed |
| if d.HasChange("network_offering") { |
| // Retrieve the network_offering ID |
| networkofferingid, e := retrieveID(cs, "network_offering", d.Get("network_offering").(string)) |
| if e != nil { |
| return e.Error() |
| } |
| // Set the new network offering |
| p.SetNetworkofferingid(networkofferingid) |
| } |
| |
| // Update the network |
| _, err := cs.Network.UpdateNetwork(p) |
| if err != nil { |
| return fmt.Errorf( |
| "Error updating network %s: %s", name, err) |
| } |
| |
| // Replace the ACL if the ID has changed |
| if d.HasChange("acl_id") { |
| p := cs.NetworkACL.NewReplaceNetworkACLListParams(d.Get("acl_id").(string)) |
| p.SetNetworkid(d.Id()) |
| |
| _, err := cs.NetworkACL.ReplaceNetworkACLList(p) |
| if err != nil { |
| return fmt.Errorf("Error replacing ACL: %s", err) |
| } |
| } |
| |
| // Update tags if they have changed |
| if d.HasChange("tags") { |
| if err := updateTags(cs, d, "Network"); err != nil { |
| return fmt.Errorf("Error updating tags on ACL %s: %s", name, err) |
| } |
| } |
| |
| return resourceCloudStackNetworkRead(d, meta) |
| } |
| |
| func resourceCloudStackNetworkDelete(d *schema.ResourceData, meta interface{}) error { |
| cs := meta.(*cloudstack.CloudStackClient) |
| |
| // Create a new parameter struct |
| p := cs.Network.NewDeleteNetworkParams(d.Id()) |
| |
| // Delete the network |
| _, err := cs.Network.DeleteNetwork(p) |
| if err != nil { |
| // This is a very poor way to be told the ID does no longer exist :( |
| if strings.Contains(err.Error(), fmt.Sprintf( |
| "Invalid parameter id value=%s due to incorrect long value format, "+ |
| "or entity does not exist", d.Id())) { |
| return nil |
| } |
| |
| return fmt.Errorf("Error deleting network %s: %s", d.Get("name").(string), err) |
| } |
| return nil |
| } |
| |
| func parseCIDR(d *schema.ResourceData, specifyiprange bool) (map[string]string, error) { |
| m := make(map[string]string, 4) |
| |
| cidr := d.Get("cidr").(string) |
| ip, ipnet, err := net.ParseCIDR(cidr) |
| if err != nil { |
| return nil, fmt.Errorf("Unable to parse cidr %s: %s", cidr, err) |
| } |
| |
| msk := ipnet.Mask |
| sub := ip.Mask(msk) |
| |
| m["netmask"] = fmt.Sprintf("%d.%d.%d.%d", msk[0], msk[1], msk[2], msk[3]) |
| |
| if gateway, ok := d.GetOk("gateway"); ok { |
| m["gateway"] = gateway.(string) |
| } else { |
| m["gateway"] = fmt.Sprintf("%d.%d.%d.%d", sub[0], sub[1], sub[2], sub[3]+1) |
| } |
| |
| if startip, ok := d.GetOk("startip"); ok { |
| m["startip"] = startip.(string) |
| } else if specifyiprange { |
| m["startip"] = fmt.Sprintf("%d.%d.%d.%d", sub[0], sub[1], sub[2], sub[3]+2) |
| } |
| |
| if endip, ok := d.GetOk("endip"); ok { |
| m["endip"] = endip.(string) |
| } else if specifyiprange { |
| m["endip"] = fmt.Sprintf("%d.%d.%d.%d", |
| sub[0]+(0xff-msk[0]), sub[1]+(0xff-msk[1]), sub[2]+(0xff-msk[2]), sub[3]+(0xff-msk[3]-1)) |
| } |
| |
| return m, nil |
| } |