Merge pull request #1347 from jtopjian/compute-network-refactor

provider/openstack Compute Network Refactor
This commit is contained in:
Paul Hinze 2015-04-02 08:46:03 -05:00
commit e0cdadfc55
5 changed files with 355 additions and 172 deletions

View File

@ -63,4 +63,9 @@ func testAccPreCheck(t *testing.T) {
if v1 == "" && v2 == "" {
t.Fatal("OS_FLAVOR_ID or OS_FLAVOR_NAME must be set for acceptance tests")
}
v = os.Getenv("OS_NETWORK_ID")
if v == "" {
t.Fatal("OS_NETWORK_ID must be set for acceptance tests")
}
}

View File

@ -2,12 +2,14 @@ package openstack
import (
"fmt"
"os"
"testing"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/floatingip"
"github.com/rackspace/gophercloud/openstack/compute/v2/servers"
)
func TestAccComputeV2FloatingIP_basic(t *testing.T) {
@ -28,6 +30,40 @@ func TestAccComputeV2FloatingIP_basic(t *testing.T) {
})
}
func TestAccComputeV2FloatingIP_attach(t *testing.T) {
var instance servers.Server
var fip floatingip.FloatingIP
var testAccComputeV2FloatingIP_attach = fmt.Sprintf(`
resource "openstack_compute_floatingip_v2" "myip" {
}
resource "openstack_compute_instance_v2" "foo" {
name = "terraform-test"
floating_ip = "${openstack_compute_floatingip_v2.myip.address}"
network {
uuid = "%s"
}
}`,
os.Getenv("OS_NETWORK_ID"))
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckComputeV2FloatingIPDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccComputeV2FloatingIP_attach,
Check: resource.ComposeTestCheckFunc(
testAccCheckComputeV2FloatingIPExists(t, "openstack_compute_floatingip_v2.myip", &fip),
testAccCheckComputeV2InstanceExists(t, "openstack_compute_instance_v2.foo", &instance),
testAccCheckComputeV2InstanceFloatingIPAttach(&instance, &fip),
),
},
},
})
}
func testAccCheckComputeV2FloatingIPDestroy(s *terraform.State) error {
config := testAccProvider.Meta().(*Config)
computeClient, err := config.computeV2Client(OS_REGION_NAME)
@ -83,9 +119,4 @@ func testAccCheckComputeV2FloatingIPExists(t *testing.T, n string, kp *floatingi
var testAccComputeV2FloatingIP_basic = `
resource "openstack_compute_floatingip_v2" "foo" {
}
resource "openstack_compute_instance_v2" "bar" {
name = "terraform-acc-floating-ip-test"
floating_ip = "${openstack_compute_floatingip_v2.foo.address}"
}`

View File

@ -13,14 +13,14 @@ import (
"github.com/hashicorp/terraform/helper/schema"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume"
"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/floatingip"
"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs"
"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/secgroups"
"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/tenantnetworks"
"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/volumeattach"
"github.com/rackspace/gophercloud/openstack/compute/v2/flavors"
"github.com/rackspace/gophercloud/openstack/compute/v2/images"
"github.com/rackspace/gophercloud/openstack/compute/v2/servers"
"github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/floatingips"
"github.com/rackspace/gophercloud/openstack/networking/v2/ports"
"github.com/rackspace/gophercloud/pagination"
)
@ -76,7 +76,7 @@ func resourceComputeInstanceV2() *schema.Resource {
Optional: true,
ForceNew: false,
},
"user_data": &schema.Schema{
"user_data": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
@ -106,19 +106,37 @@ func resourceComputeInstanceV2() *schema.Resource {
Type: schema.TypeList,
Optional: true,
ForceNew: true,
Computed: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"uuid": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
},
"name": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
},
"port": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
},
"fixed_ip": &schema.Schema{
"fixed_ip_v4": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
},
"fixed_ip_v6": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
},
"mac": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
},
},
@ -230,13 +248,27 @@ func resourceComputeInstanceV2Create(d *schema.ResourceData, meta interface{}) e
return err
}
networkDetails, err := resourceInstanceNetworks(computeClient, d)
if err != nil {
return err
}
networks := make([]servers.Network, len(networkDetails))
for i, net := range networkDetails {
networks[i] = servers.Network{
UUID: net["uuid"].(string),
Port: net["port"].(string),
FixedIP: net["fixed_ip_v4"].(string),
}
}
createOpts = &servers.CreateOpts{
Name: d.Get("name").(string),
ImageRef: imageId,
FlavorRef: flavorId,
SecurityGroups: resourceInstanceSecGroupsV2(d),
AvailabilityZone: d.Get("availability_zone").(string),
Networks: resourceInstanceNetworksV2(d),
Networks: networks,
Metadata: resourceInstanceMetadataV2(d),
ConfigDrive: d.Get("config_drive").(bool),
AdminPass: d.Get("admin_pass").(string),
@ -291,18 +323,8 @@ func resourceComputeInstanceV2Create(d *schema.ResourceData, meta interface{}) e
}
floatingIP := d.Get("floating_ip").(string)
if floatingIP != "" {
networkingClient, err := config.networkingV2Client(d.Get("region").(string))
if err != nil {
return fmt.Errorf("Error creating OpenStack compute client: %s", err)
}
allFloatingIPs, err := getFloatingIPs(networkingClient)
if err != nil {
return fmt.Errorf("Error listing OpenStack floating IPs: %s", err)
}
err = assignFloatingIP(networkingClient, extractFloatingIPFromIP(allFloatingIPs, floatingIP), server.ID)
if err != nil {
return fmt.Errorf("Error assigning floating IP to OpenStack compute instance: %s", err)
if err := floatingip.Associate(computeClient, server.ID, floatingIP).ExtractErr(); err != nil {
return fmt.Errorf("Error associating floating IP: %s", err)
}
}
@ -338,67 +360,85 @@ func resourceComputeInstanceV2Read(d *schema.ResourceData, meta interface{}) err
log.Printf("[DEBUG] Retreived Server %s: %+v", d.Id(), server)
d.Set("name", server.Name)
// begin reading the network configuration
d.Set("access_ip_v4", server.AccessIPv4)
d.Set("access_ip_v6", server.AccessIPv6)
hostv4 := server.AccessIPv4
if hostv4 == "" {
if publicAddressesRaw, ok := server.Addresses["public"]; ok {
publicAddresses := publicAddressesRaw.([]interface{})
for _, paRaw := range publicAddresses {
pa := paRaw.(map[string]interface{})
if pa["version"].(float64) == 4 {
hostv4 = pa["addr"].(string)
break
}
}
}
}
// If no host found, just get the first IPv4 we find
if hostv4 == "" {
for _, networkAddresses := range server.Addresses {
for _, element := range networkAddresses.([]interface{}) {
address := element.(map[string]interface{})
if address["version"].(float64) == 4 {
hostv4 = address["addr"].(string)
break
}
}
}
}
d.Set("access_ip_v4", hostv4)
log.Printf("hostv4: %s", hostv4)
hostv6 := server.AccessIPv6
if hostv6 == "" {
if publicAddressesRaw, ok := server.Addresses["public"]; ok {
publicAddresses := publicAddressesRaw.([]interface{})
for _, paRaw := range publicAddresses {
pa := paRaw.(map[string]interface{})
if pa["version"].(float64) == 6 {
hostv6 = fmt.Sprintf("[%s]", pa["addr"].(string))
break
networkDetails, err := resourceInstanceNetworks(computeClient, d)
addresses := resourceInstanceAddresses(server.Addresses)
if err != nil {
return err
}
// if there are no networkDetails, make networks at least a length of 1
networkLength := 1
if len(networkDetails) > 0 {
networkLength = len(networkDetails)
}
networks := make([]map[string]interface{}, networkLength)
// Loop through all networks and addresses,
// merge relevant address details.
if len(networkDetails) == 0 {
for netName, n := range addresses {
if floatingIP, ok := n["floating_ip"]; ok {
hostv4 = floatingIP.(string)
} else {
if hostv4 == "" && n["fixed_ip_v4"] != nil {
hostv4 = n["fixed_ip_v4"].(string)
}
}
if hostv6 == "" && n["fixed_ip_v6"] != nil {
hostv6 = n["fixed_ip_v6"].(string)
}
networks[0] = map[string]interface{}{
"name": netName,
"fixed_ip_v4": n["fixed_ip_v4"],
"fixed_ip_v6": n["fixed_ip_v6"],
"mac": n["mac"],
}
}
} else {
for i, net := range networkDetails {
n := addresses[net["name"].(string)]
if floatingIP, ok := n["floating_ip"]; ok {
hostv4 = floatingIP.(string)
} else {
if hostv4 == "" && n["fixed_ip_v4"] != nil {
hostv4 = n["fixed_ip_v4"].(string)
}
}
if hostv6 == "" && n["fixed_ip_v6"] != nil {
hostv6 = n["fixed_ip_v6"].(string)
}
networks[i] = map[string]interface{}{
"uuid": networkDetails[i]["uuid"],
"name": networkDetails[i]["name"],
"port": networkDetails[i]["port"],
"fixed_ip_v4": n["fixed_ip_v4"],
"fixed_ip_v6": n["fixed_ip_v6"],
"mac": n["mac"],
}
}
}
// If no hostv6 found, just get the first IPv6 we find
if hostv6 == "" {
for _, networkAddresses := range server.Addresses {
for _, element := range networkAddresses.([]interface{}) {
address := element.(map[string]interface{})
if address["version"].(float64) == 6 {
hostv6 = fmt.Sprintf("[%s]", address["addr"].(string))
break
}
}
}
}
log.Printf("[DEBUG] new networks: %+v", networks)
d.Set("network", networks)
d.Set("access_ip_v4", hostv4)
d.Set("access_ip_v6", hostv6)
log.Printf("hostv4: %s", hostv4)
log.Printf("hostv6: %s", hostv6)
// prefer the v6 address if no v4 address exists.
preferredv := ""
if hostv4 != "" {
preferredv = hostv4
@ -413,6 +453,7 @@ func resourceComputeInstanceV2Read(d *schema.ResourceData, meta interface{}) err
"host": preferredv,
})
}
// end network configuration
d.Set("metadata", server.Metadata)
@ -553,20 +594,20 @@ func resourceComputeInstanceV2Update(d *schema.ResourceData, meta interface{}) e
}
if d.HasChange("floating_ip") {
floatingIP := d.Get("floating_ip").(string)
if floatingIP != "" {
networkingClient, err := config.networkingV2Client(d.Get("region").(string))
if err != nil {
return fmt.Errorf("Error creating OpenStack compute client: %s", err)
oldFIP, newFIP := d.GetChange("floating_ip")
log.Printf("[DEBUG] Old Floating IP: %v", oldFIP)
log.Printf("[DEBUG] New Floating IP: %v", newFIP)
if oldFIP.(string) != "" {
log.Printf("[DEBUG] Attemping to disassociate %s from %s", oldFIP, d.Id())
if err := floatingip.Disassociate(computeClient, d.Id(), oldFIP.(string)).ExtractErr(); err != nil {
return fmt.Errorf("Error disassociating Floating IP during update: %s", err)
}
}
allFloatingIPs, err := getFloatingIPs(networkingClient)
if err != nil {
return fmt.Errorf("Error listing OpenStack floating IPs: %s", err)
}
err = assignFloatingIP(networkingClient, extractFloatingIPFromIP(allFloatingIPs, floatingIP), d.Id())
if err != nil {
fmt.Errorf("Error assigning floating IP to OpenStack compute instance: %s", err)
if newFIP.(string) != "" {
log.Printf("[DEBUG] Attemping to associate %s to %s", newFIP, d.Id())
if err := floatingip.Associate(computeClient, d.Id(), newFIP.(string)).ExtractErr(); err != nil {
return fmt.Errorf("Error associating Floating IP during update: %s", err)
}
}
}
@ -722,18 +763,91 @@ func resourceInstanceSecGroupsV2(d *schema.ResourceData) []string {
return secgroups
}
func resourceInstanceNetworksV2(d *schema.ResourceData) []servers.Network {
func resourceInstanceNetworks(computeClient *gophercloud.ServiceClient, d *schema.ResourceData) ([]map[string]interface{}, error) {
rawNetworks := d.Get("network").([]interface{})
networks := make([]servers.Network, len(rawNetworks))
newNetworks := make([]map[string]interface{}, len(rawNetworks))
var tenantnet tenantnetworks.Network
tenantNetworkExt := true
for i, raw := range rawNetworks {
rawMap := raw.(map[string]interface{})
networks[i] = servers.Network{
UUID: rawMap["uuid"].(string),
Port: rawMap["port"].(string),
FixedIP: rawMap["fixed_ip"].(string),
allPages, err := tenantnetworks.List(computeClient).AllPages()
if err != nil {
errCode, ok := err.(*gophercloud.UnexpectedResponseCodeError)
if !ok {
return nil, err
}
if errCode.Actual == 404 {
tenantNetworkExt = false
} else {
return nil, err
}
}
networkID := ""
networkName := ""
if tenantNetworkExt {
networkList, err := tenantnetworks.ExtractNetworks(allPages)
if err != nil {
return nil, err
}
for _, network := range networkList {
if network.Name == rawMap["name"] {
tenantnet = network
}
if network.ID == rawMap["uuid"] {
tenantnet = network
}
}
networkID = tenantnet.ID
networkName = tenantnet.Name
} else {
networkID = rawMap["uuid"].(string)
networkName = rawMap["name"].(string)
}
newNetworks[i] = map[string]interface{}{
"uuid": networkID,
"name": networkName,
"port": rawMap["port"].(string),
"fixed_ip_v4": rawMap["fixed_ip_v4"].(string),
}
}
return networks
log.Printf("[DEBUG] networks: %+v", newNetworks)
return newNetworks, nil
}
func resourceInstanceAddresses(addresses map[string]interface{}) map[string]map[string]interface{} {
addrs := make(map[string]map[string]interface{})
for n, networkAddresses := range addresses {
addrs[n] = make(map[string]interface{})
for _, element := range networkAddresses.([]interface{}) {
address := element.(map[string]interface{})
if address["OS-EXT-IPS:type"] == "floating" {
addrs[n]["floating_ip"] = address["addr"]
} else {
if address["version"].(float64) == 4 {
addrs[n]["fixed_ip_v4"] = address["addr"].(string)
} else {
addrs[n]["fixed_ip_v6"] = fmt.Sprintf("[%s]", address["addr"].(string))
}
}
if mac, ok := address["OS-EXT-IPS-MAC:mac_addr"]; ok {
addrs[n]["mac"] = mac.(string)
}
}
}
log.Printf("[DEBUG] Addresses: %+v", addresses)
return addrs
}
func resourceInstanceMetadataV2(d *schema.ResourceData) map[string]string {
@ -759,75 +873,6 @@ func resourceInstanceBlockDeviceV2(d *schema.ResourceData, bd map[string]interfa
return bfvOpts
}
func extractFloatingIPFromIP(ips []floatingips.FloatingIP, IP string) *floatingips.FloatingIP {
for _, floatingIP := range ips {
if floatingIP.FloatingIP == IP {
return &floatingIP
}
}
return nil
}
func assignFloatingIP(networkingClient *gophercloud.ServiceClient, floatingIP *floatingips.FloatingIP, instanceID string) error {
portID, err := getInstancePortID(networkingClient, instanceID)
if err != nil {
return err
}
return floatingips.Update(networkingClient, floatingIP.ID, floatingips.UpdateOpts{
PortID: portID,
}).Err
}
func getInstancePortID(networkingClient *gophercloud.ServiceClient, instanceID string) (string, error) {
pager := ports.List(networkingClient, ports.ListOpts{
DeviceID: instanceID,
})
var portID string
err := pager.EachPage(func(page pagination.Page) (bool, error) {
portList, err := ports.ExtractPorts(page)
if err != nil {
return false, err
}
for _, port := range portList {
portID = port.ID
return false, nil
}
return true, nil
})
if err != nil {
return "", err
}
if portID == "" {
return "", fmt.Errorf("Cannot find port for instance %s", instanceID)
}
return portID, nil
}
func getFloatingIPs(networkingClient *gophercloud.ServiceClient) ([]floatingips.FloatingIP, error) {
pager := floatingips.List(networkingClient, floatingips.ListOpts{})
ips := []floatingips.FloatingIP{}
err := pager.EachPage(func(page pagination.Page) (bool, error) {
floatingipList, err := floatingips.ExtractFloatingIPs(page)
if err != nil {
return false, err
}
for _, f := range floatingipList {
ips = append(ips, f)
}
return true, nil
})
if err != nil {
return nil, err
}
return ips, nil
}
func getImageID(client *gophercloud.ServiceClient, d *schema.ResourceData) (string, error) {
imageId := d.Get("image_id").(string)

View File

@ -2,12 +2,14 @@ package openstack
import (
"fmt"
"os"
"testing"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
"github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes"
"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/floatingip"
"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/volumeattach"
"github.com/rackspace/gophercloud/openstack/compute/v2/servers"
"github.com/rackspace/gophercloud/pagination"
@ -15,6 +17,17 @@ import (
func TestAccComputeV2Instance_basic(t *testing.T) {
var instance servers.Server
var testAccComputeV2Instance_basic = fmt.Sprintf(`
resource "openstack_compute_instance_v2" "foo" {
name = "terraform-test"
network {
uuid = "%s"
}
metadata {
foo = "bar"
}
}`,
os.Getenv("OS_NETWORK_ID"))
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
@ -53,6 +66,40 @@ func TestAccComputeV2Instance_volumeAttach(t *testing.T) {
})
}
func TestAccComputeV2Instance_floatingIPAttach(t *testing.T) {
var instance servers.Server
var fip floatingip.FloatingIP
var testAccComputeV2Instance_floatingIPAttach = fmt.Sprintf(`
resource "openstack_compute_floatingip_v2" "myip" {
}
resource "openstack_compute_instance_v2" "foo" {
name = "terraform-test"
floating_ip = "${openstack_compute_floatingip_v2.myip.address}"
network {
uuid = "%s"
}
}`,
os.Getenv("OS_NETWORK_ID"))
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckComputeV2InstanceDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccComputeV2Instance_floatingIPAttach,
Check: resource.ComposeTestCheckFunc(
testAccCheckComputeV2FloatingIPExists(t, "openstack_compute_floatingip_v2.myip", &fip),
testAccCheckComputeV2InstanceExists(t, "openstack_compute_instance_v2.foo", &instance),
testAccCheckComputeV2InstanceFloatingIPAttach(&instance, &fip),
),
},
},
})
}
func testAccCheckComputeV2InstanceDestroy(s *terraform.State) error {
config := testAccProvider.Meta().(*Config)
computeClient, err := config.computeV2Client(OS_REGION_NAME)
@ -159,15 +206,17 @@ func testAccCheckComputeV2InstanceVolumeAttachment(
}
}
var testAccComputeV2Instance_basic = fmt.Sprintf(`
resource "openstack_compute_instance_v2" "foo" {
region = "%s"
name = "terraform-test"
metadata {
foo = "bar"
func testAccCheckComputeV2InstanceFloatingIPAttach(
instance *servers.Server, fip *floatingip.FloatingIP) resource.TestCheckFunc {
return func(s *terraform.State) error {
if fip.InstanceID == instance.ID {
return nil
}
}`,
OS_REGION_NAME)
return fmt.Errorf("Floating IP %s was not attached to instance %s", fip.ID, instance.ID)
}
}
var testAccComputeV2Instance_volumeAttach = fmt.Sprintf(`
resource "openstack_blockstorage_volume_v1" "myvol" {

View File

@ -2,11 +2,13 @@ package openstack
import (
"fmt"
"os"
"testing"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
"github.com/rackspace/gophercloud/openstack/compute/v2/servers"
"github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/floatingips"
)
@ -28,6 +30,40 @@ func TestAccNetworkingV2FloatingIP_basic(t *testing.T) {
})
}
func TestAccNetworkingV2FloatingIP_attach(t *testing.T) {
var instance servers.Server
var fip floatingips.FloatingIP
var testAccNetworkV2FloatingIP_attach = fmt.Sprintf(`
resource "openstack_networking_floatingip_v2" "myip" {
}
resource "openstack_compute_instance_v2" "foo" {
name = "terraform-test"
floating_ip = "${openstack_networking_floatingip_v2.myip.address}"
network {
uuid = "%s"
}
}`,
os.Getenv("OS_NETWORK_ID"))
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckNetworkingV2FloatingIPDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccNetworkV2FloatingIP_attach,
Check: resource.ComposeTestCheckFunc(
testAccCheckNetworkingV2FloatingIPExists(t, "openstack_networking_floatingip_v2.myip", &fip),
testAccCheckComputeV2InstanceExists(t, "openstack_compute_instance_v2.foo", &instance),
testAccCheckNetworkingV2InstanceFloatingIPAttach(&instance, &fip),
),
},
},
})
}
func testAccCheckNetworkingV2FloatingIPDestroy(s *terraform.State) error {
config := testAccProvider.Meta().(*Config)
networkClient, err := config.networkingV2Client(OS_REGION_NAME)
@ -81,11 +117,28 @@ func testAccCheckNetworkingV2FloatingIPExists(t *testing.T, n string, kp *floati
}
}
func testAccCheckNetworkingV2InstanceFloatingIPAttach(
instance *servers.Server, fip *floatingips.FloatingIP) resource.TestCheckFunc {
// When Neutron is used, the Instance sometimes does not know its floating IP until some time
// after the attachment happened. This can be anywhere from 2-20 seconds. Because of that delay,
// the test usually completes with failure.
// However, the Fixed IP is known on both sides immediately, so that can be used as a bridge
// to ensure the two are now related.
// I think a better option is to introduce some state changing config in the actual resource.
return func(s *terraform.State) error {
for _, networkAddresses := range instance.Addresses {
for _, element := range networkAddresses.([]interface{}) {
address := element.(map[string]interface{})
if address["OS-EXT-IPS:type"] == "fixed" && address["addr"] == fip.FixedIP {
return nil
}
}
}
return fmt.Errorf("Floating IP %+v was not attached to instance %+v", fip, instance)
}
}
var testAccNetworkingV2FloatingIP_basic = `
resource "openstack_networking_floatingip_v2" "foo" {
}
resource "openstack_compute_instance_v2" "bar" {
name = "terraform-acc-floating-ip-test"
floating_ip = "${openstack_networking_floatingip_v2.foo.address}"
}`