From c54a6b48f5ec00b787ae7b7ab45894ff5820908a Mon Sep 17 00:00:00 2001 From: Otto Boehrer Date: Fri, 18 Oct 2024 15:15:06 +0200 Subject: [PATCH] Enable golang CPI implementation --- CONTRIBUTING.md | 1 - jobs/openstack_cpi_golang/spec | 3 - jobs/openstack_cpi_golang/templates/cpi.erb | 39 +- src/openstack_cpi_golang/.tool-versions | 2 +- .../cpi/compute/availbility_zone_provider.go | 39 + .../compute/availbility_zone_provider_test.go | 58 + .../cpi/compute/compute_facade.go | 123 ++ .../cpi/compute/compute_service.go | 606 ++++++ .../cpi/compute/compute_service_builder.go | 46 + .../compute/compute_service_builder_test.go | 55 + .../cpi/compute/compute_service_test.go | 960 +++++++++ .../cpi/compute/compute_suite_test.go | 13 + .../fake_availability_zone_provider.go | 112 + .../computefakes/fake_compute_facade.go | 1251 +++++++++++ .../computefakes/fake_compute_service.go | 1154 ++++++++++ .../fake_compute_service_builder.go | 107 + .../computefakes/fake_flavor_resolver.go | 357 +++ .../computefakes/fake_volume_configurator.go | 126 ++ .../cpi/compute/flavor_resolver.go | 171 ++ .../cpi/compute/flavor_resolver_test.go | 214 ++ .../cpi/compute/volume_configurator.go | 65 + .../cpi/compute/volume_configurator_test.go | 149 ++ src/openstack_cpi_golang/cpi/config/config.go | 4 +- .../cpi/config/config_test.go | 6 +- src/openstack_cpi_golang/cpi/factory.go | 124 +- .../cpi/image/heavy_stemcell_creator.go | 39 + .../cpi/image/heavy_stemcell_creator_test.go | 80 + .../cpi/image/http_client.go | 28 + .../cpi/image/image_facade.go | 33 + .../cpi/image/image_service.go | 162 ++ .../cpi/image/image_service_builder.go | 42 + .../cpi/image/image_service_builder_test.go | 55 + .../cpi/image/image_service_test.go | 247 +++ .../cpi/image/image_suite_test.go | 13 + .../imagefakes/fake_heavy_stemcell_creator.go | 121 ++ .../cpi/image/imagefakes/fake_http_client.go | 201 ++ .../cpi/image/imagefakes/fake_image_facade.go | 277 +++ .../image/imagefakes/fake_image_service.go | 349 +++ .../imagefakes/fake_image_service_builder.go | 107 + .../imagefakes/fake_light_stemcell_creator.go | 119 + .../cpi/image/light_stemcell_creator.go | 42 + .../cpi/image/light_stemcell_creator_test.go | 46 + .../cpi/image/root_image/root_image.go | 35 + .../image/root_image/root_image_suite_test.go | 13 + .../cpi/image/root_image/root_image_test.go | 50 + .../root_imagefakes/fake_root_image.go | 118 + .../cpi/image/root_image/testdata/image | Bin 0 -> 352 bytes .../testdata/image_that_is_not_tar} | 0 .../testdata/image_without_root_img | Bin 0 -> 335 bytes .../cpi/image/testdata/root.img | 1 + .../cpi/loadbalancer/loadbalancer_facade.go | 73 + .../cpi/loadbalancer/loadbalancer_service.go | 303 +++ .../loadbalancer_service_builder.go | 41 + .../loadbalancer_service_builder_test.go | 55 + .../loadbalancer/loadbalancer_service_test.go | 402 ++++ .../loadbalancer/loadbalancer_suite_test.go | 13 + .../fake_loadbalancer_facade.go | 768 +++++++ .../fake_loadbalancer_service.go | 283 +++ .../fake_loadbalancer_service_builder.go | 107 + .../cpi/methods/attach_disk.go | 202 +- .../cpi/methods/attach_disk_test.go | 544 +++++ .../cpi/methods/attach_disk_wrapper.go | 19 + .../methods/calculate_vm_cloud_properties.go | 54 +- .../calculate_vm_cloud_properties_test.go | 153 ++ .../cpi/methods/create_disk.go | 88 +- .../cpi/methods/create_disk_test.go | 287 +++ .../cpi/methods/create_stemcell.go | 70 +- .../cpi/methods/create_stemcell_test.go | 188 ++ .../cpi/methods/create_vm.go | 231 +- .../cpi/methods/create_vm_test.go | 870 ++++++++ .../cpi/methods/delete_disk.go | 59 +- .../cpi/methods/delete_disk_test.go | 156 ++ .../cpi/methods/delete_snapshot.go | 42 +- .../cpi/methods/delete_snapshot_test.go | 69 + .../cpi/methods/delete_stemcell.go | 31 +- .../cpi/methods/delete_stemcell_test.go | 63 + .../cpi/methods/delete_vm.go | 81 +- .../cpi/methods/delete_vm_test.go | 328 +++ .../cpi/methods/detach_disk.go | 76 +- .../cpi/methods/detach_disk_test.go | 177 ++ .../cpi/methods/get_disks.go | 52 +- .../cpi/methods/get_disks_test.go | 113 + .../cpi/methods/has_disk.go | 43 +- .../cpi/methods/has_disk_test.go | 116 + .../cpi/methods/has_vm.go | 45 +- .../cpi/methods/has_vm_test.go | 130 ++ .../cpi/methods/reboot_vm.go | 34 +- .../cpi/methods/resize_disk.go | 66 +- .../cpi/methods/resize_disk_test.go | 217 ++ .../cpi/methods/set_disk_metadata.go | 51 +- .../cpi/methods/set_disk_metadata_test.go | 142 ++ .../cpi/methods/set_vm_metadata.go | 111 +- .../cpi/methods/set_vm_metadata_test.go | 307 +++ .../cpi/methods/snapshot_disk.go | 135 +- .../cpi/methods/snapshot_disk_test.go | 120 ++ .../cpi/network/network_config_builder.go | 199 ++ .../network/network_config_builder_test.go | 342 +++ .../cpi/network/network_service.go | 334 +++ .../cpi/network/network_service_builder.go | 41 + .../network/network_service_builder_test.go | 55 + .../cpi/network/network_service_test.go | 548 +++++ .../cpi/network/network_suite_test.go | 13 + .../networkfakes/fake_network_service.go | 531 +++++ .../fake_network_service_builder.go | 107 + .../networkfakes/fake_networking_facade.go | 1004 +++++++++ .../fake_security_groups_resolver.go | 121 ++ .../cpi/network/networking_facade.go | 90 + .../cpi/network/security_groups_resolver.go | 82 + .../network/security_groups_resolver_test.go | 141 ++ .../cpi/openstack/openstack_facade.go | 51 + .../cpi/openstack/openstack_service.go | 81 + .../cpi/openstack/openstack_service_test.go | 229 ++ .../cpi/openstack/openstack_suite_test.go | 13 + .../openstackfakes/fake_openstack_facade.go | 522 +++++ .../openstackfakes/fake_openstack_service.go | 434 ++++ .../properties/calculate_vm_cloud_props.go | 3 + .../cpi/properties/create_disk_cloud_props.go | 5 + .../cpi/properties/create_stemcell.go | 19 + .../cpi/properties/create_vm_cloud_props.go | 53 + .../properties/create_vm_cloud_props_test.go | 62 + .../cpi/properties/create_vm_user_data.go | 50 + .../cpi/properties/network_configuration.go | 50 + .../properties/network_configuration_test.go | 53 + .../cpi/properties/properties_suite_test.go | 13 + .../cpi/properties/server_tags.go | 3 + .../cpi/properties/user_data_builder.go | 91 + .../cpi/properties/user_data_builder_test.go | 93 + src/openstack_cpi_golang/cpi/utils/env_var.go | 18 + src/openstack_cpi_golang/cpi/utils/retry.go | 74 + .../cpi/utils/retry_test.go | 103 + .../cpi/utils/service_clients.go | 24 + .../cpi/utils/service_clients_test.go | 26 + .../cpi/utils/unique_array.go | 14 + .../cpi/utils/unique_array_test.go | 18 + .../cpi/utils/utils_suite_test.go | 13 + .../cpi/utils/utilsfakes/fake_env_var.go | 111 + .../cpi/utils/utilsfakes/fake_logger.go | 314 +++ .../cpi/volume/volume_facade.go | 73 + .../cpi/volume/volume_service.go | 270 +++ .../cpi/volume/volume_service_builder.go | 39 + .../cpi/volume/volume_service_builder_test.go | 55 + .../cpi/volume/volume_service_test.go | 209 ++ .../cpi/volume/volume_suite_test.go | 13 + .../volume/volumefakes/fake_volume_facade.go | 759 +++++++ .../volume/volumefakes/fake_volume_service.go | 906 ++++++++ .../fake_volume_service_builder.go | 107 + src/openstack_cpi_golang/go.mod | 1 + src/openstack_cpi_golang/go.sum | 2 + .../integration/attach_disk_test.go | 143 ++ .../calculate_vm_cloud_properties_test.go | 116 + .../integration/create_disk_test.go | 119 + .../integration/create_stemcell_test.go | 147 ++ .../integration/create_vm_test.go | 1910 +++++++++++++++++ .../integration/delete_disk_test.go | 84 + .../integration/delete_snapshot_test.go | 111 + .../integration/delete_stemcell_test.go | 40 + .../integration/delete_vm_test.go | 384 ++++ .../integration/detach_disk_test.go | 161 ++ .../integration/get_disks_test.go | 148 ++ .../integration/has_disk_test.go | 70 + .../integration/has_vm_test.go | 158 ++ .../integration/integration_suite_test.go | 22 +- .../integration/reboot_vm_test.go | 249 +++ .../integration/resize_disk_test.go | 260 +++ .../integration/set_disk_metadata_test.go | 117 + .../integration/set_vm_metadata_test.go | 578 +++++ .../integration/snapshot_disk_test.go | 320 +++ .../integration/testdata/image | Bin 0 -> 352 bytes src/openstack_cpi_golang/main.go | 1 + .../github.com/google/uuid/CHANGELOG.md | 41 + .../github.com/google/uuid/CONTRIBUTING.md | 26 + .../github.com/google/uuid/CONTRIBUTORS | 9 + .../vendor/github.com/google/uuid/LICENSE | 27 + .../vendor/github.com/google/uuid/README.md | 21 + .../vendor/github.com/google/uuid/dce.go | 80 + .../vendor/github.com/google/uuid/doc.go | 12 + .../vendor/github.com/google/uuid/hash.go | 59 + .../vendor/github.com/google/uuid/marshal.go | 38 + .../vendor/github.com/google/uuid/node.go | 90 + .../vendor/github.com/google/uuid/node_js.go | 12 + .../vendor/github.com/google/uuid/node_net.go | 33 + .../vendor/github.com/google/uuid/null.go | 118 + .../vendor/github.com/google/uuid/sql.go | 59 + .../vendor/github.com/google/uuid/time.go | 134 ++ .../vendor/github.com/google/uuid/util.go | 43 + .../vendor/github.com/google/uuid/uuid.go | 365 ++++ .../vendor/github.com/google/uuid/version1.go | 44 + .../vendor/github.com/google/uuid/version4.go | 76 + .../vendor/github.com/google/uuid/version6.go | 56 + .../vendor/github.com/google/uuid/version7.go | 104 + .../gophercloud/openstack/auth_env.go | 137 ++ .../extensions/volumeactions/doc.go | 108 + .../extensions/volumeactions/requests.go | 460 ++++ .../extensions/volumeactions/results.go | 226 ++ .../extensions/volumeactions/urls.go | 7 + .../blockstorage/v3/snapshots/doc.go | 61 + .../blockstorage/v3/snapshots/requests.go | 278 +++ .../blockstorage/v3/snapshots/results.go | 158 ++ .../blockstorage/v3/snapshots/urls.go | 43 + .../blockstorage/v3/snapshots/util.go | 22 + .../openstack/blockstorage/v3/volumes/doc.go | 23 + .../blockstorage/v3/volumes/requests.go | 208 ++ .../blockstorage/v3/volumes/results.go | 179 ++ .../openstack/blockstorage/v3/volumes/urls.go | 23 + .../openstack/blockstorage/v3/volumes/util.go | 22 + .../gophercloud/openstack/client.go | 503 +++++ .../v2/extensions/availabilityzones/doc.go | 61 + .../extensions/availabilityzones/requests.go | 20 + .../extensions/availabilityzones/results.go | 76 + .../v2/extensions/availabilityzones/urls.go | 11 + .../v2/extensions/bootfromvolume/doc.go | 152 ++ .../v2/extensions/bootfromvolume/requests.go | 142 ++ .../v2/extensions/bootfromvolume/results.go | 12 + .../v2/extensions/bootfromvolume/urls.go | 7 + .../compute/v2/extensions/keypairs/doc.go | 119 + .../v2/extensions/keypairs/requests.go | 183 ++ .../compute/v2/extensions/keypairs/results.go | 98 + .../compute/v2/extensions/keypairs/urls.go | 25 + .../compute/v2/extensions/volumeattach/doc.go | 30 + .../v2/extensions/volumeattach/requests.go | 71 + .../v2/extensions/volumeattach/results.go | 89 + .../v2/extensions/volumeattach/urls.go | 25 + .../openstack/compute/v2/flavors/doc.go | 150 ++ .../openstack/compute/v2/flavors/requests.go | 364 ++++ .../openstack/compute/v2/flavors/results.go | 271 +++ .../openstack/compute/v2/flavors/urls.go | 53 + .../openstack/compute/v2/servers/doc.go | 135 ++ .../openstack/compute/v2/servers/errors.go | 71 + .../openstack/compute/v2/servers/requests.go | 784 +++++++ .../openstack/compute/v2/servers/results.go | 444 ++++ .../openstack/compute/v2/servers/urls.go | 51 + .../openstack/compute/v2/servers/util.go | 21 + .../gophercloud/gophercloud/openstack/doc.go | 14 + .../openstack/endpoint_location.go | 111 + .../gophercloud/openstack/errors.go | 47 + .../openstack/identity/v2/tenants/doc.go | 65 + .../openstack/identity/v2/tenants/requests.go | 120 ++ .../openstack/identity/v2/tenants/results.go | 95 + .../openstack/identity/v2/tenants/urls.go | 23 + .../openstack/identity/v2/tokens/doc.go | 46 + .../openstack/identity/v2/tokens/requests.go | 105 + .../openstack/identity/v2/tokens/results.go | 174 ++ .../openstack/identity/v2/tokens/urls.go | 13 + .../identity/v3/extensions/ec2tokens/doc.go | 40 + .../v3/extensions/ec2tokens/requests.go | 377 ++++ .../identity/v3/extensions/ec2tokens/urls.go | 11 + .../identity/v3/extensions/oauth1/doc.go | 122 ++ .../identity/v3/extensions/oauth1/requests.go | 587 +++++ .../identity/v3/extensions/oauth1/results.go | 317 +++ .../identity/v3/extensions/oauth1/urls.go | 43 + .../openstack/identity/v3/tokens/doc.go | 107 + .../openstack/identity/v3/tokens/requests.go | 174 ++ .../openstack/identity/v3/tokens/results.go | 194 ++ .../openstack/identity/v3/tokens/urls.go | 7 + .../openstack/imageservice/v2/images/doc.go | 60 + .../imageservice/v2/images/requests.go | 418 ++++ .../imageservice/v2/images/results.go | 246 +++ .../openstack/imageservice/v2/images/types.go | 108 + .../openstack/imageservice/v2/images/urls.go | 65 + .../loadbalancer/v2/l7policies/doc.go | 123 ++ .../loadbalancer/v2/l7policies/requests.go | 418 ++++ .../loadbalancer/v2/l7policies/results.go | 253 +++ .../loadbalancer/v2/l7policies/urls.go | 25 + .../loadbalancer/v2/listeners/doc.go | 77 + .../loadbalancer/v2/listeners/requests.go | 303 +++ .../loadbalancer/v2/listeners/results.go | 230 ++ .../loadbalancer/v2/listeners/urls.go | 21 + .../loadbalancer/v2/loadbalancers/doc.go | 140 ++ .../loadbalancer/v2/loadbalancers/requests.go | 275 +++ .../loadbalancer/v2/loadbalancers/results.go | 256 +++ .../loadbalancer/v2/loadbalancers/urls.go | 31 + .../openstack/loadbalancer/v2/monitors/doc.go | 71 + .../loadbalancer/v2/monitors/requests.go | 255 +++ .../loadbalancer/v2/monitors/results.go | 167 ++ .../loadbalancer/v2/monitors/urls.go | 16 + .../openstack/loadbalancer/v2/pools/doc.go | 157 ++ .../loadbalancer/v2/pools/requests.go | 499 +++++ .../loadbalancer/v2/pools/results.go | 349 +++ .../openstack/loadbalancer/v2/pools/urls.go | 25 + .../v2/extensions/layer3/floatingips/doc.go | 71 + .../extensions/layer3/floatingips/requests.go | 186 ++ .../extensions/layer3/floatingips/results.go | 181 ++ .../v2/extensions/layer3/floatingips/urls.go | 13 + .../v2/extensions/security/groups/doc.go | 58 + .../v2/extensions/security/groups/requests.go | 133 ++ .../v2/extensions/security/groups/results.go | 158 ++ .../v2/extensions/security/groups/urls.go | 13 + .../v2/extensions/security/rules/doc.go | 50 + .../v2/extensions/security/rules/requests.go | 164 ++ .../v2/extensions/security/rules/results.go | 131 ++ .../v2/extensions/security/rules/urls.go | 13 + .../openstack/networking/v2/ports/doc.go | 73 + .../openstack/networking/v2/ports/requests.go | 209 ++ .../openstack/networking/v2/ports/results.go | 209 ++ .../openstack/networking/v2/ports/urls.go | 31 + .../openstack/networking/v2/subnets/doc.go | 138 ++ .../networking/v2/subnets/requests.go | 261 +++ .../networking/v2/subnets/results.go | 162 ++ .../openstack/networking/v2/subnets/urls.go | 31 + .../openstack/utils/base_endpoint.go | 28 + .../openstack/utils/choose_version.go | 111 + .../gophercloud/pagination/http.go | 62 + .../gophercloud/pagination/linked.go | 92 + .../gophercloud/pagination/marker.go | 58 + .../gophercloud/pagination/pager.go | 254 +++ .../gophercloud/gophercloud/pagination/pkg.go | 4 + .../gophercloud/pagination/single.go | 33 + src/openstack_cpi_golang/vendor/modules.txt | 31 + 308 files changed, 46777 insertions(+), 129 deletions(-) create mode 100644 src/openstack_cpi_golang/cpi/compute/availbility_zone_provider.go create mode 100644 src/openstack_cpi_golang/cpi/compute/availbility_zone_provider_test.go create mode 100644 src/openstack_cpi_golang/cpi/compute/compute_facade.go create mode 100644 src/openstack_cpi_golang/cpi/compute/compute_service.go create mode 100644 src/openstack_cpi_golang/cpi/compute/compute_service_builder.go create mode 100644 src/openstack_cpi_golang/cpi/compute/compute_service_builder_test.go create mode 100644 src/openstack_cpi_golang/cpi/compute/compute_service_test.go create mode 100644 src/openstack_cpi_golang/cpi/compute/compute_suite_test.go create mode 100644 src/openstack_cpi_golang/cpi/compute/computefakes/fake_availability_zone_provider.go create mode 100644 src/openstack_cpi_golang/cpi/compute/computefakes/fake_compute_facade.go create mode 100644 src/openstack_cpi_golang/cpi/compute/computefakes/fake_compute_service.go create mode 100644 src/openstack_cpi_golang/cpi/compute/computefakes/fake_compute_service_builder.go create mode 100644 src/openstack_cpi_golang/cpi/compute/computefakes/fake_flavor_resolver.go create mode 100644 src/openstack_cpi_golang/cpi/compute/computefakes/fake_volume_configurator.go create mode 100644 src/openstack_cpi_golang/cpi/compute/flavor_resolver.go create mode 100644 src/openstack_cpi_golang/cpi/compute/flavor_resolver_test.go create mode 100644 src/openstack_cpi_golang/cpi/compute/volume_configurator.go create mode 100644 src/openstack_cpi_golang/cpi/compute/volume_configurator_test.go create mode 100644 src/openstack_cpi_golang/cpi/image/heavy_stemcell_creator.go create mode 100644 src/openstack_cpi_golang/cpi/image/heavy_stemcell_creator_test.go create mode 100644 src/openstack_cpi_golang/cpi/image/http_client.go create mode 100644 src/openstack_cpi_golang/cpi/image/image_facade.go create mode 100644 src/openstack_cpi_golang/cpi/image/image_service.go create mode 100644 src/openstack_cpi_golang/cpi/image/image_service_builder.go create mode 100644 src/openstack_cpi_golang/cpi/image/image_service_builder_test.go create mode 100644 src/openstack_cpi_golang/cpi/image/image_service_test.go create mode 100644 src/openstack_cpi_golang/cpi/image/image_suite_test.go create mode 100644 src/openstack_cpi_golang/cpi/image/imagefakes/fake_heavy_stemcell_creator.go create mode 100644 src/openstack_cpi_golang/cpi/image/imagefakes/fake_http_client.go create mode 100644 src/openstack_cpi_golang/cpi/image/imagefakes/fake_image_facade.go create mode 100644 src/openstack_cpi_golang/cpi/image/imagefakes/fake_image_service.go create mode 100644 src/openstack_cpi_golang/cpi/image/imagefakes/fake_image_service_builder.go create mode 100644 src/openstack_cpi_golang/cpi/image/imagefakes/fake_light_stemcell_creator.go create mode 100644 src/openstack_cpi_golang/cpi/image/light_stemcell_creator.go create mode 100644 src/openstack_cpi_golang/cpi/image/light_stemcell_creator_test.go create mode 100644 src/openstack_cpi_golang/cpi/image/root_image/root_image.go create mode 100644 src/openstack_cpi_golang/cpi/image/root_image/root_image_suite_test.go create mode 100644 src/openstack_cpi_golang/cpi/image/root_image/root_image_test.go create mode 100644 src/openstack_cpi_golang/cpi/image/root_image/root_imagefakes/fake_root_image.go create mode 100644 src/openstack_cpi_golang/cpi/image/root_image/testdata/image rename src/openstack_cpi_golang/{.gitignore => cpi/image/root_image/testdata/image_that_is_not_tar} (100%) create mode 100644 src/openstack_cpi_golang/cpi/image/root_image/testdata/image_without_root_img create mode 100644 src/openstack_cpi_golang/cpi/image/testdata/root.img create mode 100644 src/openstack_cpi_golang/cpi/loadbalancer/loadbalancer_facade.go create mode 100644 src/openstack_cpi_golang/cpi/loadbalancer/loadbalancer_service.go create mode 100644 src/openstack_cpi_golang/cpi/loadbalancer/loadbalancer_service_builder.go create mode 100644 src/openstack_cpi_golang/cpi/loadbalancer/loadbalancer_service_builder_test.go create mode 100644 src/openstack_cpi_golang/cpi/loadbalancer/loadbalancer_service_test.go create mode 100644 src/openstack_cpi_golang/cpi/loadbalancer/loadbalancer_suite_test.go create mode 100644 src/openstack_cpi_golang/cpi/loadbalancer/loadbalancerfakes/fake_loadbalancer_facade.go create mode 100644 src/openstack_cpi_golang/cpi/loadbalancer/loadbalancerfakes/fake_loadbalancer_service.go create mode 100644 src/openstack_cpi_golang/cpi/loadbalancer/loadbalancerfakes/fake_loadbalancer_service_builder.go create mode 100644 src/openstack_cpi_golang/cpi/methods/attach_disk_test.go create mode 100644 src/openstack_cpi_golang/cpi/methods/attach_disk_wrapper.go create mode 100644 src/openstack_cpi_golang/cpi/methods/calculate_vm_cloud_properties_test.go create mode 100644 src/openstack_cpi_golang/cpi/methods/create_disk_test.go create mode 100644 src/openstack_cpi_golang/cpi/methods/create_stemcell_test.go create mode 100644 src/openstack_cpi_golang/cpi/methods/create_vm_test.go create mode 100644 src/openstack_cpi_golang/cpi/methods/delete_disk_test.go create mode 100644 src/openstack_cpi_golang/cpi/methods/delete_snapshot_test.go create mode 100644 src/openstack_cpi_golang/cpi/methods/delete_stemcell_test.go create mode 100644 src/openstack_cpi_golang/cpi/methods/delete_vm_test.go create mode 100644 src/openstack_cpi_golang/cpi/methods/detach_disk_test.go create mode 100644 src/openstack_cpi_golang/cpi/methods/get_disks_test.go create mode 100644 src/openstack_cpi_golang/cpi/methods/has_disk_test.go create mode 100644 src/openstack_cpi_golang/cpi/methods/has_vm_test.go create mode 100644 src/openstack_cpi_golang/cpi/methods/resize_disk_test.go create mode 100644 src/openstack_cpi_golang/cpi/methods/set_disk_metadata_test.go create mode 100644 src/openstack_cpi_golang/cpi/methods/set_vm_metadata_test.go create mode 100644 src/openstack_cpi_golang/cpi/methods/snapshot_disk_test.go create mode 100644 src/openstack_cpi_golang/cpi/network/network_config_builder.go create mode 100644 src/openstack_cpi_golang/cpi/network/network_config_builder_test.go create mode 100644 src/openstack_cpi_golang/cpi/network/network_service.go create mode 100644 src/openstack_cpi_golang/cpi/network/network_service_builder.go create mode 100644 src/openstack_cpi_golang/cpi/network/network_service_builder_test.go create mode 100644 src/openstack_cpi_golang/cpi/network/network_service_test.go create mode 100644 src/openstack_cpi_golang/cpi/network/network_suite_test.go create mode 100644 src/openstack_cpi_golang/cpi/network/networkfakes/fake_network_service.go create mode 100644 src/openstack_cpi_golang/cpi/network/networkfakes/fake_network_service_builder.go create mode 100644 src/openstack_cpi_golang/cpi/network/networkfakes/fake_networking_facade.go create mode 100644 src/openstack_cpi_golang/cpi/network/networkfakes/fake_security_groups_resolver.go create mode 100644 src/openstack_cpi_golang/cpi/network/networking_facade.go create mode 100644 src/openstack_cpi_golang/cpi/network/security_groups_resolver.go create mode 100644 src/openstack_cpi_golang/cpi/network/security_groups_resolver_test.go create mode 100644 src/openstack_cpi_golang/cpi/openstack/openstack_facade.go create mode 100644 src/openstack_cpi_golang/cpi/openstack/openstack_service.go create mode 100644 src/openstack_cpi_golang/cpi/openstack/openstack_service_test.go create mode 100644 src/openstack_cpi_golang/cpi/openstack/openstack_suite_test.go create mode 100644 src/openstack_cpi_golang/cpi/openstack/openstackfakes/fake_openstack_facade.go create mode 100644 src/openstack_cpi_golang/cpi/openstack/openstackfakes/fake_openstack_service.go create mode 100644 src/openstack_cpi_golang/cpi/properties/calculate_vm_cloud_props.go create mode 100644 src/openstack_cpi_golang/cpi/properties/create_disk_cloud_props.go create mode 100644 src/openstack_cpi_golang/cpi/properties/create_stemcell.go create mode 100644 src/openstack_cpi_golang/cpi/properties/create_vm_cloud_props.go create mode 100644 src/openstack_cpi_golang/cpi/properties/create_vm_cloud_props_test.go create mode 100644 src/openstack_cpi_golang/cpi/properties/create_vm_user_data.go create mode 100644 src/openstack_cpi_golang/cpi/properties/network_configuration.go create mode 100644 src/openstack_cpi_golang/cpi/properties/network_configuration_test.go create mode 100644 src/openstack_cpi_golang/cpi/properties/properties_suite_test.go create mode 100644 src/openstack_cpi_golang/cpi/properties/server_tags.go create mode 100644 src/openstack_cpi_golang/cpi/properties/user_data_builder.go create mode 100644 src/openstack_cpi_golang/cpi/properties/user_data_builder_test.go create mode 100644 src/openstack_cpi_golang/cpi/utils/env_var.go create mode 100644 src/openstack_cpi_golang/cpi/utils/retry.go create mode 100644 src/openstack_cpi_golang/cpi/utils/retry_test.go create mode 100644 src/openstack_cpi_golang/cpi/utils/service_clients.go create mode 100644 src/openstack_cpi_golang/cpi/utils/service_clients_test.go create mode 100644 src/openstack_cpi_golang/cpi/utils/unique_array.go create mode 100644 src/openstack_cpi_golang/cpi/utils/unique_array_test.go create mode 100644 src/openstack_cpi_golang/cpi/utils/utils_suite_test.go create mode 100644 src/openstack_cpi_golang/cpi/utils/utilsfakes/fake_env_var.go create mode 100644 src/openstack_cpi_golang/cpi/utils/utilsfakes/fake_logger.go create mode 100644 src/openstack_cpi_golang/cpi/volume/volume_facade.go create mode 100644 src/openstack_cpi_golang/cpi/volume/volume_service.go create mode 100644 src/openstack_cpi_golang/cpi/volume/volume_service_builder.go create mode 100644 src/openstack_cpi_golang/cpi/volume/volume_service_builder_test.go create mode 100644 src/openstack_cpi_golang/cpi/volume/volume_service_test.go create mode 100644 src/openstack_cpi_golang/cpi/volume/volume_suite_test.go create mode 100644 src/openstack_cpi_golang/cpi/volume/volumefakes/fake_volume_facade.go create mode 100644 src/openstack_cpi_golang/cpi/volume/volumefakes/fake_volume_service.go create mode 100644 src/openstack_cpi_golang/cpi/volume/volumefakes/fake_volume_service_builder.go create mode 100644 src/openstack_cpi_golang/integration/attach_disk_test.go create mode 100644 src/openstack_cpi_golang/integration/calculate_vm_cloud_properties_test.go create mode 100644 src/openstack_cpi_golang/integration/create_disk_test.go create mode 100644 src/openstack_cpi_golang/integration/create_stemcell_test.go create mode 100644 src/openstack_cpi_golang/integration/create_vm_test.go create mode 100644 src/openstack_cpi_golang/integration/delete_disk_test.go create mode 100644 src/openstack_cpi_golang/integration/delete_snapshot_test.go create mode 100644 src/openstack_cpi_golang/integration/delete_stemcell_test.go create mode 100644 src/openstack_cpi_golang/integration/delete_vm_test.go create mode 100644 src/openstack_cpi_golang/integration/detach_disk_test.go create mode 100644 src/openstack_cpi_golang/integration/get_disks_test.go create mode 100644 src/openstack_cpi_golang/integration/has_disk_test.go create mode 100644 src/openstack_cpi_golang/integration/has_vm_test.go create mode 100644 src/openstack_cpi_golang/integration/reboot_vm_test.go create mode 100644 src/openstack_cpi_golang/integration/resize_disk_test.go create mode 100644 src/openstack_cpi_golang/integration/set_disk_metadata_test.go create mode 100644 src/openstack_cpi_golang/integration/set_vm_metadata_test.go create mode 100644 src/openstack_cpi_golang/integration/snapshot_disk_test.go create mode 100644 src/openstack_cpi_golang/integration/testdata/image create mode 100644 src/openstack_cpi_golang/vendor/github.com/google/uuid/CHANGELOG.md create mode 100644 src/openstack_cpi_golang/vendor/github.com/google/uuid/CONTRIBUTING.md create mode 100644 src/openstack_cpi_golang/vendor/github.com/google/uuid/CONTRIBUTORS create mode 100644 src/openstack_cpi_golang/vendor/github.com/google/uuid/LICENSE create mode 100644 src/openstack_cpi_golang/vendor/github.com/google/uuid/README.md create mode 100644 src/openstack_cpi_golang/vendor/github.com/google/uuid/dce.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/google/uuid/doc.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/google/uuid/hash.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/google/uuid/marshal.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/google/uuid/node.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/google/uuid/node_js.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/google/uuid/node_net.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/google/uuid/null.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/google/uuid/sql.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/google/uuid/time.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/google/uuid/util.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/google/uuid/uuid.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/google/uuid/version1.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/google/uuid/version4.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/google/uuid/version6.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/google/uuid/version7.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/auth_env.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/volumeactions/doc.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/volumeactions/requests.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/volumeactions/results.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/volumeactions/urls.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v3/snapshots/doc.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v3/snapshots/requests.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v3/snapshots/results.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v3/snapshots/urls.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v3/snapshots/util.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v3/volumes/doc.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v3/volumes/requests.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v3/volumes/results.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v3/volumes/urls.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v3/volumes/util.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/client.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/availabilityzones/doc.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/availabilityzones/requests.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/availabilityzones/results.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/availabilityzones/urls.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/bootfromvolume/doc.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/bootfromvolume/requests.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/bootfromvolume/results.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/bootfromvolume/urls.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs/doc.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs/requests.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs/results.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs/urls.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/volumeattach/doc.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/volumeattach/requests.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/volumeattach/results.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/volumeattach/urls.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/doc.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/requests.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/results.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/urls.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/doc.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/errors.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/requests.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/results.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/urls.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/util.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/doc.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/endpoint_location.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/errors.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tenants/doc.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tenants/requests.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tenants/results.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tenants/urls.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tokens/doc.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tokens/requests.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tokens/results.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tokens/urls.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/ec2tokens/doc.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/ec2tokens/requests.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/ec2tokens/urls.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/oauth1/doc.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/oauth1/requests.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/oauth1/results.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/oauth1/urls.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/tokens/doc.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/tokens/requests.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/tokens/results.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/tokens/urls.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/doc.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/requests.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/results.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/types.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/urls.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/l7policies/doc.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/l7policies/requests.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/l7policies/results.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/l7policies/urls.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/listeners/doc.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/listeners/requests.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/listeners/results.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/listeners/urls.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/loadbalancers/doc.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/loadbalancers/requests.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/loadbalancers/results.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/loadbalancers/urls.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/monitors/doc.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/monitors/requests.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/monitors/results.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/monitors/urls.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/pools/doc.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/pools/requests.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/pools/results.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/pools/urls.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/doc.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/requests.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/results.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/urls.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/groups/doc.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/groups/requests.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/groups/results.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/groups/urls.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/rules/doc.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/rules/requests.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/rules/results.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/rules/urls.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/ports/doc.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/ports/requests.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/ports/results.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/ports/urls.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/subnets/doc.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/subnets/requests.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/subnets/results.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/subnets/urls.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/utils/base_endpoint.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/utils/choose_version.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/pagination/http.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/pagination/linked.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/pagination/marker.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/pagination/pager.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/pagination/pkg.go create mode 100644 src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/pagination/single.go diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e44d9dc5..f72ac8b9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -54,4 +54,3 @@ If you still want to run manual tests (e.g. in order to validate your use case) ``` - Deploy a BOSH Director using your CPI release (see [bosh.io](http://bosh.io/docs/init-openstack.html#create-manifest)) - diff --git a/jobs/openstack_cpi_golang/spec b/jobs/openstack_cpi_golang/spec index 812b94ea..f53e6f5f 100644 --- a/jobs/openstack_cpi_golang/spec +++ b/jobs/openstack_cpi_golang/spec @@ -4,13 +4,10 @@ templates: cpi.erb: bin/cpi cpi.json.erb: config/cpi.json cacert.pem.erb: config/cacert.pem - cpi-golang.erb: bin/cpi-golang - cpi-ruby.erb: bin/cpi-ruby packages: - openstack_cpi_golang - bosh_openstack_cpi -- openstack-ruby-3.1 properties: openstack.auth_url: diff --git a/jobs/openstack_cpi_golang/templates/cpi.erb b/jobs/openstack_cpi_golang/templates/cpi.erb index 6b6da2ff..9dc82128 100644 --- a/jobs/openstack_cpi_golang/templates/cpi.erb +++ b/jobs/openstack_cpi_golang/templates/cpi.erb @@ -2,24 +2,25 @@ set -e -BOSH_JOBS_DIR=${BOSH_JOBS_DIR:-/var/vcap/jobs} -GOLANG_CPI_CALLS=("info") -REGEXP="\"method\":\"([^\"]*)\"" +<% if_p('env.http_proxy') do |http_proxy| %> +export HTTP_PROXY="<%= http_proxy %>" +export http_proxy="<%= http_proxy %>" +<% end %> + +<% if_p('env.https_proxy') do |https_proxy| %> +export HTTPS_PROXY="<%= https_proxy %>" +export https_proxy="<%= https_proxy %>" +<% end %> -STDIN=$(cat /dev/stdin) +<% if_p('env.no_proxy') do |no_proxy| %> +export NO_PROXY="<%= no_proxy %>" +export no_proxy="<%= no_proxy %>" +<% end %> + +BOSH_PACKAGES_DIR=${BOSH_PACKAGES_DIR:-/var/vcap/packages} +BOSH_JOBS_DIR=${BOSH_JOBS_DIR:-/var/vcap/jobs} -if [[ ${STDIN} =~ ${REGEXP} ]] -then - for element in "${GOLANG_CPI_CALLS[@]}" - do - if [[ $element == ${BASH_REMATCH[1]} ]] - then - ${BOSH_JOBS_DIR}/openstack_cpi_golang/bin/cpi-golang <<< ${STDIN} - exit $? - fi - done - ${BOSH_JOBS_DIR}/openstack_cpi_golang/bin/cpi-ruby <<< ${STDIN} -else - echo "input stream is not defining a method" - exit 1 -fi +platform=`uname | tr '[:upper:]' '[:lower:]'` +exec ${BOSH_PACKAGES_DIR}/openstack_cpi_golang/bin/cpi-${platform} \ + -configFile=${BOSH_JOBS_DIR}/openstack_cpi_golang/config/cpi.json \ + -caCert=${BOSH_JOBS_DIR}/openstack_cpi_golang/config/cacert.pem diff --git a/src/openstack_cpi_golang/.tool-versions b/src/openstack_cpi_golang/.tool-versions index d70ca2f5..2f1d3539 100644 --- a/src/openstack_cpi_golang/.tool-versions +++ b/src/openstack_cpi_golang/.tool-versions @@ -1 +1 @@ -golang 1.21.3 +golang 1.22.6 diff --git a/src/openstack_cpi_golang/cpi/compute/availbility_zone_provider.go b/src/openstack_cpi_golang/cpi/compute/availbility_zone_provider.go new file mode 100644 index 00000000..12e0a4f5 --- /dev/null +++ b/src/openstack_cpi_golang/cpi/compute/availbility_zone_provider.go @@ -0,0 +1,39 @@ +package compute + +import ( + "math/rand" + "time" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/properties" +) + +//counterfeiter:generate . AvailabilityZoneProvider +type AvailabilityZoneProvider interface { + GetAvailabilityZones(cloudProperties properties.CreateVM) []string +} + +type availabilityZoneProvider struct { +} + +func NewAvailabilityZoneProvider() availabilityZoneProvider { + return availabilityZoneProvider{} +} + +func (a availabilityZoneProvider) GetAvailabilityZones(cloudProperties properties.CreateVM) []string { + + if len(cloudProperties.AvailabilityZones) > 0 { + return a.shuffleAZs(cloudProperties) + } + + return []string{cloudProperties.AvailabilityZone} +} + +func (a availabilityZoneProvider) shuffleAZs(cloudProperties properties.CreateVM) []string { + shuffledZones := cloudProperties.AvailabilityZones + r := rand.New(rand.NewSource(time.Now().UnixNano())) + + r.Shuffle(len(shuffledZones), func(i, j int) { + shuffledZones[i], shuffledZones[j] = shuffledZones[j], shuffledZones[i] + }) + return shuffledZones +} diff --git a/src/openstack_cpi_golang/cpi/compute/availbility_zone_provider_test.go b/src/openstack_cpi_golang/cpi/compute/availbility_zone_provider_test.go new file mode 100644 index 00000000..a4a4756c --- /dev/null +++ b/src/openstack_cpi_golang/cpi/compute/availbility_zone_provider_test.go @@ -0,0 +1,58 @@ +package compute_test + +import ( + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/compute" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/properties" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("AvailabilityZoneProvider", func() { + Context("GetAvailabilityZones", func() { + + var cloudProps properties.CreateVM + + Context("with cloud property availability_zones", func() { + + BeforeEach(func() { + cloudProps = properties.CreateVM{} + cloudProps.AvailabilityZone = "" + }) + + It("returns a single availability zone", func() { + cloudProps.AvailabilityZones = []string{"z1"} + zone := compute.NewAvailabilityZoneProvider(). + GetAvailabilityZones(cloudProps) + + Expect(len(zone)).To(Equal(1)) + Expect(zone).To(ContainElement("z1")) + }) + + It("returns a single availability zone", func() { + cloudProps.AvailabilityZones = []string{"z1", "z2", "z3"} + zone := compute.NewAvailabilityZoneProvider(). + GetAvailabilityZones(cloudProps) + + Expect(len(zone)).To(Equal(3)) + Expect(zone).To(ContainElements("z1", "z2", "z3")) + }) + }) + + Context("with cloud property availability_zone", func() { + + BeforeEach(func() { + cloudProps = properties.CreateVM{} + cloudProps.AvailabilityZones = []string{} + }) + + It("returns a single availability zone", func() { + cloudProps.AvailabilityZone = "z1" + zone := compute.NewAvailabilityZoneProvider(). + GetAvailabilityZones(cloudProps) + + Expect(len(zone)).To(Equal(1)) + Expect(zone).To(ContainElement("z1")) + }) + }) + }) +}) diff --git a/src/openstack_cpi_golang/cpi/compute/compute_facade.go b/src/openstack_cpi_golang/cpi/compute/compute_facade.go new file mode 100644 index 00000000..2a18135e --- /dev/null +++ b/src/openstack_cpi_golang/cpi/compute/compute_facade.go @@ -0,0 +1,123 @@ +package compute + +import ( + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils" + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/availabilityzones" + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs" + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/volumeattach" + "github.com/gophercloud/gophercloud/openstack/compute/v2/flavors" + "github.com/gophercloud/gophercloud/openstack/compute/v2/servers" + "github.com/gophercloud/gophercloud/pagination" +) + +type ServerWithAZ struct { + servers.Server + availabilityzones.ServerAvailabilityZoneExt +} + +//counterfeiter:generate . ComputeFacade +type ComputeFacade interface { + CreateServer(client utils.ServiceClient, opts servers.CreateOptsBuilder) (*servers.Server, error) + + DeleteServer(client utils.RetryableServiceClient, serverID string) error + + RebootServer(client utils.ServiceClient, serverID string, opts servers.RebootOptsBuilder) error + + GetServer(client utils.RetryableServiceClient, serverID string) (*servers.Server, error) + + GetServerWithAZ(client utils.RetryableServiceClient, serverID string) (*ServerWithAZ, error) + + ListFlavors(client utils.RetryableServiceClient, opts flavors.ListOpts) (pagination.Page, error) + + ExtractFlavors(page pagination.Page) ([]flavors.Flavor, error) + + GetOSKeyPair(client utils.RetryableServiceClient, keyPairName string, ops keypairs.GetOpts) (*keypairs.KeyPair, error) + + GetServerMetadata(client utils.RetryableServiceClient, serverID string) (map[string]string, error) + + UpdateServer(client utils.ServiceClient, serverID string, opt servers.UpdateOptsBuilder) (*servers.Server, error) + + UpdateServerMetadata(client utils.ServiceClient, serverID string, opts servers.UpdateMetadataOptsBuilder) (map[string]string, error) + + DeleteServerMetaData(client *gophercloud.ServiceClient, serverID string, key string) error + + AttachVolume(client *gophercloud.ServiceClient, serverID string, opts volumeattach.CreateOptsBuilder) (*volumeattach.VolumeAttachment, error) + + DetachVolume(client *gophercloud.ServiceClient, serverID string, volumeID string) error + + ListVolumeAttachments(client *gophercloud.ServiceClient, serverID string) ([]volumeattach.VolumeAttachment, error) +} + +type computeFacade struct { +} + +func NewComputeFacade() computeFacade { + return computeFacade{} +} + +func (c computeFacade) CreateServer(client utils.ServiceClient, opts servers.CreateOptsBuilder) (*servers.Server, error) { + return servers.Create(client, opts).Extract() +} + +func (c computeFacade) DeleteServer(client utils.RetryableServiceClient, serverID string) error { + return servers.Delete(client, serverID).ExtractErr() +} + +func (c computeFacade) RebootServer(client utils.ServiceClient, serverID string, opts servers.RebootOptsBuilder) error { + return servers.Reboot(client, serverID, opts).ExtractErr() +} + +func (c computeFacade) GetServer(client utils.RetryableServiceClient, serverID string) (*servers.Server, error) { + return servers.Get(client, serverID).Extract() +} + +func (c computeFacade) GetServerWithAZ(client utils.RetryableServiceClient, serverID string) (*ServerWithAZ, error) { + var serverWithAz ServerWithAZ + err := servers.Get(client, serverID).ExtractInto(&serverWithAz) + return &serverWithAz, err +} + +func (c computeFacade) ListFlavors(client utils.RetryableServiceClient, opts flavors.ListOpts) (pagination.Page, error) { + return flavors.ListDetail(client, opts).AllPages() +} + +func (c computeFacade) ExtractFlavors(page pagination.Page) ([]flavors.Flavor, error) { + return flavors.ExtractFlavors(page) +} + +func (c computeFacade) GetOSKeyPair(client utils.RetryableServiceClient, keyPairName string, opts keypairs.GetOpts) (*keypairs.KeyPair, error) { + return keypairs.Get(client, keyPairName, opts).Extract() +} + +func (c computeFacade) GetServerMetadata(client utils.RetryableServiceClient, serverID string) (map[string]string, error) { + return servers.Metadata(client, serverID).Extract() +} + +func (c computeFacade) UpdateServer(client utils.ServiceClient, serverID string, opts servers.UpdateOptsBuilder) (*servers.Server, error) { + return servers.Update(client, serverID, opts).Extract() +} + +func (c computeFacade) UpdateServerMetadata(client utils.ServiceClient, serverID string, opts servers.UpdateMetadataOptsBuilder) (map[string]string, error) { + return servers.UpdateMetadata(client, serverID, opts).Extract() +} + +func (c computeFacade) DeleteServerMetaData(client *gophercloud.ServiceClient, serverID string, key string) error { + return servers.DeleteMetadatum(client, serverID, key).ExtractErr() +} + +func (c computeFacade) AttachVolume(client *gophercloud.ServiceClient, serverID string, opts volumeattach.CreateOptsBuilder) (*volumeattach.VolumeAttachment, error) { + return volumeattach.Create(client, serverID, opts).Extract() +} + +func (c computeFacade) DetachVolume(client *gophercloud.ServiceClient, serverID string, volumeID string) error { + return volumeattach.Delete(client, serverID, volumeID).ExtractErr() +} + +func (c computeFacade) ListVolumeAttachments(client *gophercloud.ServiceClient, serverID string) ([]volumeattach.VolumeAttachment, error) { + page, err := volumeattach.List(client, serverID).AllPages() + if err != nil { + return nil, err + } + return volumeattach.ExtractVolumeAttachments(page) +} diff --git a/src/openstack_cpi_golang/cpi/compute/compute_service.go b/src/openstack_cpi_golang/cpi/compute/compute_service.go new file mode 100644 index 00000000..d1ff30b0 --- /dev/null +++ b/src/openstack_cpi_golang/cpi/compute/compute_service.go @@ -0,0 +1,606 @@ +package compute + +import ( + "encoding/json" + "errors" + "fmt" + "time" + + "github.com/cloudfoundry/bosh-cpi-go/apiv1" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/config" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/properties" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils" + "github.com/google/uuid" + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/bootfromvolume" + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs" + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/volumeattach" + "github.com/gophercloud/gophercloud/openstack/compute/v2/flavors" + "github.com/gophercloud/gophercloud/openstack/compute/v2/servers" +) + +var ComputeServicePollingInterval = 10 * time.Second + +//counterfeiter:generate . ComputeService +type ComputeService interface { + GetServer( + serverID string, + ) (*servers.Server, error) + + CreateServer( + stemcellCID apiv1.StemcellCID, + cloudProps properties.CreateVM, + networkConfig properties.NetworkConfig, + agentID apiv1.AgentID, + env apiv1.VMEnv, + cpiConfig config.CpiConfig, + ) (*servers.Server, error) + + DeleteServer( + serverID string, + cpiConfig config.CpiConfig, + ) error + + RebootServer( + serverID string, + cpiConfig config.CpiConfig, + ) error + + GetMetadata( + serverID string, + ) (map[string]string, error) + + UpdateServer( + serverID string, + serverName string, + ) (*servers.Server, error) + + UpdateServerMetadata( + serverID string, + serverMetadata properties.ServerMetadata, + ) error + + DeleteServerMetaData( + serverID string, + oldMetaDataMap map[string]string, + updateMetaDataMap properties.ServerMetadata, + ) error + + GetMatchingFlavor( + vmResources apiv1.VMResources, + bootFromVolume bool, + ) (flavors.Flavor, error) + + GetServerAZ( + vmcid string, + ) (string, error) + + AttachVolume( + serverID string, + volumeID string, + device string, + ) (*volumeattach.VolumeAttachment, error) + + DetachVolume( + serverID string, + volumeID string, + ) error + + ListVolumeAttachments( + serverID string, + ) ([]volumeattach.VolumeAttachment, error) + + GetFlavorById( + flavorId string, + ) (flavors.Flavor, error) +} + +type computeService struct { + serviceClients utils.ServiceClients + computeFacade ComputeFacade + flavorResolver FlavorResolver + volumeConfigurator VolumeConfigurator + availabilityZoneProvider AvailabilityZoneProvider + logger utils.Logger +} + +func NewComputeService( + serviceClients utils.ServiceClients, + computeFacade ComputeFacade, + flavorResolver FlavorResolver, + volumeConfigurator VolumeConfigurator, + availabilityZoneProvider AvailabilityZoneProvider, + logger utils.Logger, +) computeService { + return computeService{ + serviceClients: serviceClients, + computeFacade: computeFacade, + flavorResolver: flavorResolver, + volumeConfigurator: volumeConfigurator, + availabilityZoneProvider: availabilityZoneProvider, + logger: logger, + } +} + +func (c computeService) GetServer( + serverID string, +) (*servers.Server, error) { + server, err := c.computeFacade.GetServer(c.serviceClients.RetryableServiceClient, serverID) + if err != nil { + return nil, fmt.Errorf("failed to retrieve server information: %w", err) + } + return server, nil +} + +func (c computeService) GetServerAZ( + serverID string, +) (string, error) { + serverWithAz, err := c.computeFacade.GetServerWithAZ(c.serviceClients.RetryableServiceClient, serverID) + + if err != nil { + return "", fmt.Errorf("failed to retrieve server information: %w", err) + } + return serverWithAz.AvailabilityZone, nil +} + +func (c computeService) CreateServer( + stemcellCID apiv1.StemcellCID, + cloudProps properties.CreateVM, + networkConfig properties.NetworkConfig, + agentID apiv1.AgentID, + env apiv1.VMEnv, + cpiConfig config.CpiConfig, +) (*servers.Server, error) { + openstackConfig := cpiConfig.Cloud.Properties.Openstack + + flavor, err := c.flavorResolver.ResolveFlavorForInstanceType(cloudProps.InstanceType) + if err != nil { + return nil, fmt.Errorf("failed to resolve flavor of instance type '%s': %w", cloudProps.InstanceType, err) + } + + keyname, err := c.getKeyPairName(cloudProps, openstackConfig) + if err != nil { + return nil, fmt.Errorf("failed to resolve keypair: %w", err) + } + + blockDevices, err := c.volumeConfigurator.ConfigureVolumes(stemcellCID.AsString(), openstackConfig, cloudProps, flavor) + if err != nil { + return nil, fmt.Errorf("failed to configure volumes: %w", err) + } + + vmName := c.getVMName() + + userData, err := c.createServerUserData(networkConfig, cpiConfig, vmName, flavor, agentID, env) + if err != nil { + return nil, fmt.Errorf("failed to create user data: %w", err) + } + + userDataJson, err := json.Marshal(userData) + if err != nil { + return nil, fmt.Errorf("failed to marshal user data: %w", err) + } + + var server *servers.Server + availabilityZones := c.availabilityZoneProvider.GetAvailabilityZones(cloudProps) + + for _, availabilityZone := range availabilityZones { + createOpts := c.getServerCreateOpts(vmName, availabilityZone, stemcellCID, networkConfig, flavor, keyname, blockDevices, userDataJson) + + server, err = c.computeFacade.CreateServer(c.serviceClients.ServiceClient, createOpts) + if err != nil { + if availabilityZone == availabilityZones[len(availabilityZones)-1] { + return nil, fmt.Errorf("failed to create server in availability zone '%s': %w", availabilityZone, err) + } + c.logger.Warn("failed to create server in availability zone '%s': %v, "+ + "retrying in a different availability zone", availabilityZone, err) + + continue + } + + server, err = c.waitForServerToBecomeActive(server.ID, time.Duration(openstackConfig.StateTimeOut)*time.Second) + if err != nil { + if availabilityZone == availabilityZones[len(availabilityZones)-1] { + return server, fmt.Errorf("failed while waiting on the server creation in availability zone '%s': %w", availabilityZone, err) + } + c.logger.Warn("failed while waiting on the server creation in availability zone '%s': %v, "+ + "retrying in a different availability zone", availabilityZone, err) + + continue + } + + break + } + + return server, nil +} + +func (c computeService) DeleteServer( + serverID string, + cpiConfig config.CpiConfig, +) error { + var errDefault404 gophercloud.ErrDefault404 + + _, err := c.GetServer(serverID) + if err != nil { + if errors.As(err, &errDefault404) { + c.logger.Info("compute_service", fmt.Sprintf("SKIPPING: Server deletion with id '%s' is not found", serverID)) + return nil + } + return err + } + + err = c.computeFacade.DeleteServer(c.serviceClients.RetryableServiceClient, serverID) + if err != nil && !errors.As(err, &errDefault404) { + return fmt.Errorf("failed to delete server: %w", err) + } + + timeout := time.Duration(cpiConfig.Cloud.Properties.Openstack.StateTimeOut) * time.Second + err = c.waitForServerToBecomeDeleted(serverID, timeout) + if err != nil { + return fmt.Errorf("failed while waiting on the server deletion: %w", err) + } + + c.logger.Info("compute_service", fmt.Sprintf("Deleted server with id '%s'", serverID)) + + // deleting registry settings - Seems that it is not needed for V2 + // https://bosh.io/docs/cpi-api-v2/#reference-table-based-on-each-component-version + + return nil +} + +func (c computeService) RebootServer( + serverID string, + cpiConfig config.CpiConfig, +) error { + _, err := c.GetServer(serverID) + if err != nil { + return err + } + + rebootOpts := servers.RebootOpts{ + Type: servers.SoftReboot, + } + + err = c.computeFacade.RebootServer(c.serviceClients.ServiceClient, serverID, rebootOpts) + if err != nil { + return fmt.Errorf("failed to reboot server: %w", err) + } + + _, err = c.waitForServerToBecomeActive( + serverID, + time.Duration(cpiConfig.Cloud.Properties.Openstack.StateTimeOut)*time.Second, + ) + if err != nil { + return fmt.Errorf("compute_service: %w", err) + } + + return nil +} + +func (c computeService) GetMetadata(serverID string) (map[string]string, error) { + var errDefault404 gophercloud.ErrDefault404 + + serverMetadata, err := c.computeFacade.GetServerMetadata(c.serviceClients.RetryableServiceClient, serverID) + if err != nil { + if errors.As(err, &errDefault404) { + c.logger.Info("compute_service", fmt.Sprintf("SKIPPING: Metadata retrieval for server with id '%s' is not found", serverID)) + serverMetadata = map[string]string{} + } else { + return nil, fmt.Errorf("failed to retrieve server metadata: %w", err) + } + } + return serverMetadata, nil +} + +func (c computeService) UpdateServer(serverID string, serverName string) (*servers.Server, error) { + + updateOptsBuilder := servers.UpdateOpts{ + Name: serverName, + } + + server, err := c.computeFacade.UpdateServer(c.serviceClients.ServiceClient, serverID, updateOptsBuilder) + if err != nil { + return nil, fmt.Errorf("failed to update server: %w", err) + } + return server, nil + +} + +func (c computeService) UpdateServerMetadata(serverID string, serverMetadata properties.ServerMetadata) error { + var blacklistedMetadataKeys = []string{ + "id", + } + + updateMetadataOpts := servers.MetadataOpts{} + for k, v := range serverMetadata { + updateMetadataOpts[k] = v.(string) + } + + for _, key := range blacklistedMetadataKeys { + delete(updateMetadataOpts, key) + } + + if len(updateMetadataOpts) == 0 { + c.logger.Info("compute_service", fmt.Sprintf("SKIPPING: No Metadata was found to be updated for server with id '%s'", serverID)) + return nil + } + + _, err := c.computeFacade.UpdateServerMetadata(c.serviceClients.ServiceClient, serverID, updateMetadataOpts) + if err != nil { + return fmt.Errorf("failed to update server metadata: %w", err) + } + + return nil +} + +func (c computeService) DeleteServerMetaData( + serverID string, + oldMetaDataMap map[string]string, + updateMetaDataMap properties.ServerMetadata, +) error { + if length := len(updateMetaDataMap); length == 0 { + c.logger.Info("compute_service", fmt.Sprintf("SKIPPING: No metadata was provided to be deleted for server with id '%s'", serverID)) + return nil + } + + var oldMetaDataMapToBeDeleted = map[string]string{} + + //it is not required to delete blacklisted metadata (key); they get updated without prior deletion + //(keeping the sequence in the dashboard) + var blacklistedMetadataKeysOld = []string{ + "director", + "deployment", + "instance_group", + "job", + "id", + "name", + "index", + "created_at", + "compiling", + } + + for _, key := range blacklistedMetadataKeysOld { + delete(oldMetaDataMap, key) + } + + if length := len(oldMetaDataMap); length == 0 { + c.logger.Info("compute_service", fmt.Sprintf("SKIPPING: No metadata was provided to be deleted for server with id '%s'", serverID)) + return nil + } + + for key, value := range oldMetaDataMap { + if _, exists := updateMetaDataMap[key]; exists { + oldMetaDataMapToBeDeleted[key] = value + } + } + + for id := range oldMetaDataMapToBeDeleted { + err := c.computeFacade.DeleteServerMetaData(c.serviceClients.ServiceClient, serverID, id) + if err != nil { + return fmt.Errorf("failed to delete server metadata for key %s: %w", id, err) + } + } + + return nil +} + +func (c computeService) GetMatchingFlavor(vmResources apiv1.VMResources, bootFromVolume bool) (flavors.Flavor, error) { + possibleFlavors, err := c.flavorResolver.ResolveFlavorForRequirements(vmResources, bootFromVolume) + if err != nil { + return flavors.Flavor{}, fmt.Errorf("failed to get flavors: %w", err) + } + + if len(possibleFlavors) == 0 { + return flavors.Flavor{}, fmt.Errorf("Unable to meet requested VM requirements: %d CPU, %d MB RAM, %g GB Disk.\n", + vmResources.CPU, + vmResources.RAM, + float64(vmResources.EphemeralDiskSize)/1024, + ) + } + + matchedFlavor := c.flavorResolver.GetClosestMatchedFlavor(possibleFlavors) + + return matchedFlavor, nil + +} + +func (c computeService) createServerUserData( + networkConfig properties.NetworkConfig, + cpiConfig config.CpiConfig, + vmName string, + flavor flavors.Flavor, + agentID apiv1.AgentID, + env apiv1.VMEnv, +) (properties.UserData, error) { + userDataNetwork := map[string]properties.UserdataNetwork{} + for _, network := range networkConfig.AllNetworks() { + + userdataNetwork := properties.UserdataNetwork{ + Default: network.Default, + DNS: network.DNS, + IP: network.IP, + Gateway: network.Gateway, + Netmask: network.Netmask, + Type: network.Type, + CloudProps: network.CloudProps, + Mac: network.Mac, + } + + if network.Type != "vip" { + userdataNetwork.UseDHCP = &cpiConfig.Cloud.Properties.Openstack.UseDHCP + } + + userDataNetwork[network.Key] = userdataNetwork + } + + environment, err := env.MarshalJSON() + if err != nil { + return properties.UserData{}, fmt.Errorf("failed to marshal environment") + } + + return properties.NewUserDataBuilder(). + WithServer(properties.Server{Name: vmName}). + WithNetworks(userDataNetwork). + WithVM(properties.VM{Name: vmName}). + WithNetworks(userDataNetwork). + WithEphemeralDiskSize(flavor.Ephemeral). + WithAgentID(agentID). + WithEnvironment(environment). + WithConfig(cpiConfig). + Build(), nil +} + +func (c computeService) getServerCreateOpts( + vmName string, + availabilityZone string, + stemcellCID apiv1.StemcellCID, + networkConfig properties.NetworkConfig, + flavor flavors.Flavor, keyname string, + blockDevices []bootfromvolume.BlockDevice, + userDataJson []byte, +) servers.CreateOptsBuilder { + + var createOpts servers.CreateOptsBuilder + createOpts = servers.CreateOpts{ + Name: vmName, + ImageRef: stemcellCID.AsString(), + Networks: c.getServerNetworks(networkConfig), + AvailabilityZone: availabilityZone, + FlavorRef: flavor.ID, + UserData: userDataJson, + + //Security groups are set for dynamic networks here. + //For manual networks, security groups are set on the port. + SecurityGroups: networkConfig.SecurityGroups, + } + + createOpts = keypairs.CreateOptsExt{ + CreateOptsBuilder: createOpts, + KeyName: keyname, + } + + if len(blockDevices) > 0 { + createOpts = bootfromvolume.CreateOptsExt{ + CreateOptsBuilder: createOpts, + BlockDevice: blockDevices, + } + } + return createOpts +} + +func (c computeService) getKeyPairName(cloudProps properties.CreateVM, openstackConfig config.OpenstackConfig) (string, error) { + var keyPairName string + + if cloudProps.KeyName != "" { + keyPairName = cloudProps.KeyName + } else { + keyPairName = openstackConfig.DefaultKeyName + } + + if keyPairName == "" { + return "", fmt.Errorf("key pair name undefined") + } + + keypair, err := c.computeFacade.GetOSKeyPair(c.serviceClients.RetryableServiceClient, keyPairName, keypairs.GetOpts{}) + if err != nil { + return "", fmt.Errorf("failed to retrieve '%s': %w", keyPairName, err) + } + + return keypair.Name, nil +} + +func (c computeService) getServerNetworks(networkConfig properties.NetworkConfig) []servers.Network { + var serverNetworks []servers.Network + for _, network := range networkConfig.ManualNetworks { + serverNetworks = append(serverNetworks, servers.Network{UUID: network.CloudProps.NetID, Port: network.Port.ID}) + } + + dynamicNetwork := networkConfig.DynamicNetwork + if dynamicNetwork != nil { + serverNetworks = append(serverNetworks, servers.Network{UUID: dynamicNetwork.CloudProps.NetID}) + } + return serverNetworks +} + +func (c computeService) getVMName() string { + return "vm-" + uuid.New().String() +} + +func (c computeService) waitForServerToBecomeActive(serverID string, timeout time.Duration) (*servers.Server, error) { + timeoutTimer := time.NewTimer(timeout) + + for { + select { + case <-timeoutTimer.C: + return nil, fmt.Errorf("timeout while waiting for server to become active") + default: + server, err := c.GetServer(serverID) + if err != nil { + return nil, err + } + + switch server.Status { + case "ACTIVE": + return server, nil + case "ERROR": + return nil, fmt.Errorf("server became ERROR state while waiting to become ACTIVE") + case "DELETED": + return nil, fmt.Errorf("server became DELETED state while waiting to become ACTIVE") + } + + time.Sleep(ComputeServicePollingInterval) + } + } +} + +func (c computeService) waitForServerToBecomeDeleted(serverID string, timeout time.Duration) error { + var errDefault404 gophercloud.ErrDefault404 + timeoutTimer := time.NewTimer(timeout) + + for { + select { + case <-timeoutTimer.C: + return fmt.Errorf("timeout while waiting for server to become deleted") + default: + server, err := c.GetServer(serverID) + if err != nil { + if errors.As(err, &errDefault404) { + return nil + } + return err + } + + switch server.Status { + case "DELETED": + return nil + case "TERMINATED": + return nil + case "ERROR": + return fmt.Errorf("server became ERROR state while waiting to become DELETED") + } + + time.Sleep(ComputeServicePollingInterval) + } + } +} + +func (c computeService) AttachVolume(serverID string, volumeID string, device string) (*volumeattach.VolumeAttachment, error) { + // see: https://github.com/gophercloud/gophercloud/blob/master/openstack/compute/v2/volumeattach/doc.go + opts := volumeattach.CreateOpts{ + Device: device, + VolumeID: volumeID, + } + return c.computeFacade.AttachVolume(c.serviceClients.ServiceClient, serverID, opts) +} + +func (c computeService) DetachVolume(serverID string, volumeID string) error { + return c.computeFacade.DetachVolume(c.serviceClients.ServiceClient, serverID, volumeID) +} + +func (c computeService) ListVolumeAttachments(serverID string) ([]volumeattach.VolumeAttachment, error) { + return c.computeFacade.ListVolumeAttachments(c.serviceClients.ServiceClient, serverID) +} + +func (c computeService) GetFlavorById(flavorId string) (flavors.Flavor, error) { + return c.flavorResolver.GetFlavorById(flavorId) +} diff --git a/src/openstack_cpi_golang/cpi/compute/compute_service_builder.go b/src/openstack_cpi_golang/cpi/compute/compute_service_builder.go new file mode 100644 index 00000000..41ce286b --- /dev/null +++ b/src/openstack_cpi_golang/cpi/compute/compute_service_builder.go @@ -0,0 +1,46 @@ +package compute + +import ( + "fmt" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/config" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/openstack" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils" +) + +//counterfeiter:generate . ComputeServiceBuilder +type ComputeServiceBuilder interface { + Build() (ComputeService, error) +} + +type computeServiceBuilder struct { + openstackService openstack.OpenstackService + cpiConfig config.CpiConfig + logger utils.Logger +} + +func NewComputeServiceBuilder(openstackService openstack.OpenstackService, cpiConfig config.CpiConfig, logger utils.Logger) computeServiceBuilder { + return computeServiceBuilder{ + openstackService: openstackService, + cpiConfig: cpiConfig, + logger: logger, + } +} + +func (b computeServiceBuilder) Build() (ComputeService, error) { + serviceClient, err := b.openstackService.ComputeServiceV2(b.cpiConfig.OpenStackConfig()) + if err != nil { + return nil, fmt.Errorf("failed to retrieve compute service client: %w", err) + } + + serviceClients := utils.NewServiceClients(serviceClient, b.cpiConfig, b.logger) + computeFacade := NewComputeFacade() + return NewComputeService( + serviceClients, + computeFacade, + NewFlavorResolver(serviceClients, computeFacade), + NewVolumeConfigurator(), + NewAvailabilityZoneProvider(), + b.logger, + ), nil +} diff --git a/src/openstack_cpi_golang/cpi/compute/compute_service_builder_test.go b/src/openstack_cpi_golang/cpi/compute/compute_service_builder_test.go new file mode 100644 index 00000000..bda8fc0c --- /dev/null +++ b/src/openstack_cpi_golang/cpi/compute/compute_service_builder_test.go @@ -0,0 +1,55 @@ +package compute_test + +import ( + "errors" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/compute" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/config" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/openstack/openstackfakes" + "github.com/gophercloud/gophercloud" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils/utilsfakes" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("ComputeServiceBuilder", func() { + var openstackService openstackfakes.FakeOpenstackService + var logger utilsfakes.FakeLogger + var computeServiceBuilder compute.ComputeServiceBuilder + + BeforeEach(func() { + openstackService = openstackfakes.FakeOpenstackService{} + logger = utilsfakes.FakeLogger{} + cpiConfig := config.CpiConfig{} + cpiConfig.Cloud.Properties.RetryConfig = config.RetryConfigMap{} + + computeServiceBuilder = compute.NewComputeServiceBuilder( + &openstackService, + cpiConfig, + &logger, + ) + }) + + Context("Build", func() { + It("returns a compute service", func() { + providerClient := gophercloud.ProviderClient{TokenID: "the_token"} + serviceClient := gophercloud.ServiceClient{ProviderClient: &providerClient} + openstackService.ComputeServiceV2Returns(&serviceClient, nil) + + computeService, err := computeServiceBuilder.Build() + + Expect(err).ToNot(HaveOccurred()) + Expect(computeService).To(Not(BeNil())) + }) + + It("returns an error if the compute service client cannot be retrieved", func() { + openstackService.ComputeServiceV2Returns(nil, errors.New("boom")) + + computeService, err := computeServiceBuilder.Build() + + Expect(err.Error()).To(Equal("failed to retrieve compute service client: boom")) + Expect(computeService).To(BeNil()) + }) + }) +}) diff --git a/src/openstack_cpi_golang/cpi/compute/compute_service_test.go b/src/openstack_cpi_golang/cpi/compute/compute_service_test.go new file mode 100644 index 00000000..14932888 --- /dev/null +++ b/src/openstack_cpi_golang/cpi/compute/compute_service_test.go @@ -0,0 +1,960 @@ +package compute_test + +import ( + "encoding/base64" + "encoding/json" + "errors" + + "github.com/cloudfoundry/bosh-cpi-go/apiv1" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/compute" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/compute/computefakes" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/config" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/loadbalancer/loadbalancerfakes" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/properties" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils/utilsfakes" + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/bootfromvolume" + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs" + "github.com/gophercloud/gophercloud/openstack/compute/v2/flavors" + "github.com/gophercloud/gophercloud/openstack/compute/v2/servers" + "github.com/gophercloud/gophercloud/openstack/networking/v2/ports" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("ComputeService", func() { + var serviceClient gophercloud.ServiceClient + var retryableServiceClient gophercloud.ServiceClient + var serviceClients utils.ServiceClients + var utilsServiceClient utils.ServiceClient + var computeFacade computefakes.FakeComputeFacade + var flavorResolver computefakes.FakeFlavorResolver + var volumeConfigurator computefakes.FakeVolumeConfigurator + var availabilityZoneProvider computefakes.FakeAvailabilityZoneProvider + var logger utilsfakes.FakeLogger + var computeService compute.ComputeService + var networkConfig properties.NetworkConfig + var defaultCloudConfig properties.CreateVM + var loadbalancerService loadbalancerfakes.FakeLoadbalancerService + var agentID apiv1.AgentID + var env apiv1.VMEnv + + BeforeEach(func() { + serviceClient = gophercloud.ServiceClient{} + retryableServiceClient = gophercloud.ServiceClient{} + serviceClients = utils.ServiceClients{ServiceClient: &serviceClient, RetryableServiceClient: &retryableServiceClient} + computeFacade = computefakes.FakeComputeFacade{} + flavorResolver = computefakes.FakeFlavorResolver{} + volumeConfigurator = computefakes.FakeVolumeConfigurator{} + availabilityZoneProvider = computefakes.FakeAvailabilityZoneProvider{} + logger = utilsfakes.FakeLogger{} + + computeService = compute.NewComputeService(serviceClients, &computeFacade, &flavorResolver, &volumeConfigurator, &availabilityZoneProvider, &logger) + compute.ComputeServicePollingInterval = 0 + networkConfig = properties.NetworkConfig{} + computeFacade.CreateServerReturns(&servers.Server{ID: "123-456"}, nil) + flavorResolver.ResolveFlavorForInstanceTypeReturns(flavors.Flavor{ID: "the_flavor_id", Name: "the_instance_type", RAM: 4096, Ephemeral: 10}, nil) + computeFacade.GetOSKeyPairReturns(&keypairs.KeyPair{Name: "the_os_keypair_name"}, nil) + defaultCloudConfig = properties.CreateVM{InstanceType: "the_instance_type", RootDisk: properties.Disk{Size: 1}} + availabilityZoneProvider.GetAvailabilityZonesReturns([]string{"z1"}) + agentID = apiv1.NewAgentID("agent-id") + env = apiv1.VMEnv{} + }) + + Context("GetServer", func() { + + It("returns error if server was failed to retrieved", func() { + computeFacade.GetServerReturns(&servers.Server{ID: "123-456", Status: "ACTIVE"}, errors.New("boom")) + _, err := computeService.GetServer("123-456") + + Expect(err.Error()).To(Equal("failed to retrieve server information: boom")) + }) + + It("returns an active server", func() { + computeFacade.GetServerReturns(&servers.Server{ID: "123-456", Status: "ACTIVE"}, nil) + server, err := computeService.GetServer("123-456") + + Expect(err).ToNot(HaveOccurred()) + Expect(server).ToNot(BeNil()) + }) + }) + + Context("CreateServer", func() { + + BeforeEach(func() { + computeFacade.GetServerReturns(&servers.Server{ID: "123-456", Status: "ACTIVE"}, nil) + }) + + It("resolves flavors by instance type", func() { + _, err := computeService.CreateServer( + apiv1.StemcellCID{}, + defaultCloudConfig, + networkConfig, + agentID, + env, + createCpiConfig(10), + ) + + Expect(err).ToNot(HaveOccurred()) + Expect(flavorResolver.ResolveFlavorForInstanceTypeArgsForCall(0)).To(Equal("the_instance_type")) + }) + + It("returns error if flavors resolution fails", func() { + flavorResolver.ResolveFlavorForInstanceTypeReturns(flavors.Flavor{}, errors.New("boom")) + + _, err := computeService.CreateServer( + apiv1.StemcellCID{}, + defaultCloudConfig, + networkConfig, + agentID, + env, + createCpiConfig(10), + ) + + Expect(err.Error()).To(ContainSubstring("failed to resolve flavor of instance type 'the_instance_type': boom")) + }) + + It("resolves the key pair via cloud config name", func() { + computeFacade.GetOSKeyPairReturns(&keypairs.KeyPair{Name: "the_key_name"}, nil) + + _, _ = computeService.CreateServer( + apiv1.StemcellCID{}, + properties.CreateVM{ + InstanceType: "the_instance_type", + KeyName: "key_name_from_properties", + RootDisk: properties.Disk{Size: 0}, + }, + networkConfig, + agentID, + env, + createCpiConfig(10), + ) + + _, keyPairName, _ := computeFacade.GetOSKeyPairArgsForCall(0) + Expect(keyPairName).To(Equal("key_name_from_properties")) + }) + + It("resolves the key pair via openstack config name", func() { + computeFacade.GetOSKeyPairReturns(&keypairs.KeyPair{Name: "the_key_name"}, nil) + + cpiConfig := config.CpiConfig{} + openstackConfig := config.OpenstackConfig{StateTimeOut: 10, DefaultKeyName: "key_name_from_config"} + cpiConfig.Cloud.Properties.Openstack = openstackConfig + + _, _ = computeService.CreateServer( + apiv1.StemcellCID{}, + defaultCloudConfig, + networkConfig, + agentID, + env, + cpiConfig, + ) + + _, keyPairName, _ := computeFacade.GetOSKeyPairArgsForCall(0) + Expect(keyPairName).To(Equal("key_name_from_config")) + }) + + It("returns an error if key pair name IS NOT PROVIDED", func() { + computeFacade.GetOSKeyPairReturns(nil, errors.New("boom")) + + cpiConfig := config.CpiConfig{} + openstackConfig := config.OpenstackConfig{StateTimeOut: 10} + cpiConfig.Cloud.Properties.Openstack = openstackConfig + + _, err := computeService.CreateServer( + apiv1.StemcellCID{}, + defaultCloudConfig, + networkConfig, + agentID, + env, + cpiConfig, + ) + + Expect(err.Error()).To(Equal("failed to resolve keypair: key pair name undefined")) + }) + + It("returns an error id key pair name cannot be resolved", func() { + computeFacade.GetOSKeyPairReturns(nil, errors.New("boom")) + + _, err := computeService.CreateServer( + apiv1.StemcellCID{}, + defaultCloudConfig, + networkConfig, + agentID, + env, + createCpiConfig(10), + ) + + Expect(err.Error()).To(Equal("failed to resolve keypair: failed to retrieve 'the_key_name': boom")) + }) + + It("returns an error if the disksize is 0 in flavor and cloud properties", func() { + volumeConfigurator.ConfigureVolumesReturns(nil, errors.New("boom")) + + _, err := computeService.CreateServer( + apiv1.StemcellCID{}, + properties.CreateVM{InstanceType: "the_instance_type", RootDisk: properties.Disk{Size: 0}}, + networkConfig, + agentID, + env, + createCpiConfig(10), + ) + + Expect(err.Error()).To(ContainSubstring("failed to configure volumes: boom")) + }) + + Context("with server create opts", func() { + + var bootFromVolume bool + var networkConfig properties.NetworkConfig + + BeforeEach(func() { + volumeConfigurator.ConfigureVolumesReturns([]bootfromvolume.BlockDevice{{ + UUID: "the-stemcell-id", + SourceType: bootfromvolume.SourceImage, + DestinationType: bootfromvolume.DestinationVolume, + VolumeSize: 999, + BootIndex: 0, + DeleteOnTermination: true, + }}, nil) + + networkConfig = properties.NetworkConfig{ + ManualNetworks: []properties.Network{ + {Key: "bosh", Type: "manual", IP: "1.2.3.4", CloudProps: properties.NetworkCloudProps{NetID: "the_net_id"}, + Port: ports.Port{ID: "the_port_id"}}, + }, + VIPNetwork: &properties.Network{ + Key: "bosh-vip", Type: "vip", IP: "5.6.7.8", CloudProps: properties.NetworkCloudProps{NetID: "the_net_id"}, + }, + SecurityGroups: []string{"group_1", "group_2"}, + } + + bootFromVolume = true + }) + + It("creates opts for the server", func() { + _, err := computeService.CreateServer( + apiv1.NewStemcellCID("the_stemcell_id"), + properties.CreateVM{ + InstanceType: "the_instance_type", + AvailabilityZone: "z1", + RootDisk: properties.Disk{Size: 1}, + BootFromVolume: &bootFromVolume, + }, + networkConfig, + agentID, + env, + createCpiConfig(10), + ) + Expect(err).ToNot(HaveOccurred()) + + sClient, opts := computeFacade.CreateServerArgsForCall(0) + Expect(sClient).To(BeAssignableToTypeOf(utilsServiceClient)) + + createMap, err := opts.ToServerCreateMap() + Expect(err).ToNot(HaveOccurred()) + server := createMap["server"].(map[string]interface{}) + serverNetworks := server["networks"].([]map[string]interface{}) + serverSecurityGroups := server["security_groups"].([]map[string]interface{}) + blockDevice := server["block_device_mapping_v2"].([]map[string]interface{}) + + Expect(server["name"]).To(ContainSubstring("vm-")) + Expect(server["imageRef"]).To(Equal("the_stemcell_id")) + Expect(serverNetworks[0]["uuid"]).To(Equal("the_net_id")) + Expect(serverNetworks[0]["port"]).To(Equal("the_port_id")) + Expect(server["availability_zone"]).To(Equal("z1")) + Expect(server["flavorRef"]).To(Equal("the_flavor_id")) + Expect(server["key_name"]).To(Equal("the_os_keypair_name")) + Expect(blockDevice[0]["uuid"]).To(Equal("the-stemcell-id")) + Expect(blockDevice[0]["volume_size"]).To(Equal(999.0)) + Expect(serverSecurityGroups[0]["name"]).To(Equal("group_1")) + Expect(serverSecurityGroups[1]["name"]).To(Equal("group_2")) + }) + + It("creates user data", func() { + testEnv := map[string]interface{}{ + "key1": "value1", + "key2": 1, + } + env = apiv1.NewVMEnv(testEnv) + + _, err := computeService.CreateServer( + apiv1.NewStemcellCID("the_stemcell_id"), + properties.CreateVM{ + InstanceType: "the_instance_type", + AvailabilityZone: "z1", + RootDisk: properties.Disk{Size: 1}, + BootFromVolume: &bootFromVolume, + }, + networkConfig, + agentID, + env, + createCpiConfig(10), + ) + Expect(err).ToNot(HaveOccurred()) + + sClient, opts := computeFacade.CreateServerArgsForCall(0) + Expect(sClient).To(BeAssignableToTypeOf(utilsServiceClient)) + + createMap, err := opts.ToServerCreateMap() + Expect(err).ToNot(HaveOccurred()) + server := createMap["server"].(map[string]interface{}) + + userDataBytes, err := base64.StdEncoding.DecodeString(*server["user_data"].(*string)) + Expect(err).ToNot(HaveOccurred()) + + userData := properties.UserData{} + _ = json.Unmarshal(userDataBytes, &userData) + Expect(userData.Server.Name).To(Equal(server["name"])) + Expect(userData.VM.Name).To(Equal(server["name"])) + Expect(userData.Disks.System).To(Equal("/dev/sda")) + + Expect(userData.Networks["bosh"].IP).To(Equal("1.2.3.4")) + Expect(*userData.Networks["bosh"].UseDHCP).To(BeTrue()) + Expect(userData.Networks["bosh-vip"].IP).To(Equal("5.6.7.8")) + Expect(userData.Networks["bosh-vip"].UseDHCP).To(BeNil()) + Expect(userData.AgentID).To(Equal("agent-id")) + + environment, err := json.Marshal(userData.Env) + Expect(err).ToNot(HaveOccurred()) + + expectedEnv, err := json.Marshal(testEnv) + Expect(err).ToNot(HaveOccurred()) + + Expect(environment).To(Equal(expectedEnv)) + }) + }) + + It("runs server creation in multiple AZs on creation failure", func() { + availabilityZoneProvider.GetAvailabilityZonesReturns([]string{"z1", "z2"}) + + computeFacade.CreateServerReturnsOnCall(0, nil, errors.New("boom")) + computeFacade.CreateServerReturnsOnCall(1, &servers.Server{ID: "123-456"}, nil) + + _, _ = computeService.CreateServer( + apiv1.StemcellCID{}, + defaultCloudConfig, + networkConfig, + agentID, + env, + createCpiConfig(10), + ) + + _, opts := computeFacade.CreateServerArgsForCall(0) + createMap, _ := opts.ToServerCreateMap() + server := createMap["server"].(map[string]interface{}) + Expect(server["availability_zone"]).To(Equal("z1")) + + _, opts = computeFacade.CreateServerArgsForCall(1) + createMap, _ = opts.ToServerCreateMap() + server = createMap["server"].(map[string]interface{}) + Expect(server["availability_zone"]).To(Equal("z2")) + + Expect(computeFacade.CreateServerCallCount()).To(Equal(2)) + }) + + It("runs server creation in multiple AZs if waiting in server fails", func() { + availabilityZoneProvider.GetAvailabilityZonesReturns([]string{"z1", "z2"}) + + computeFacade.GetServerReturns(&servers.Server{ID: "123-456", Status: "not-active"}, nil) + + compute.ComputeServicePollingInterval = 0 + + _, _ = computeService.CreateServer( + apiv1.StemcellCID{}, + defaultCloudConfig, + networkConfig, + agentID, + env, + createCpiConfig(0), + ) + + _, opts := computeFacade.CreateServerArgsForCall(0) + createMap, _ := opts.ToServerCreateMap() + server := createMap["server"].(map[string]interface{}) + Expect(server["availability_zone"]).To(Equal("z1")) + + _, opts = computeFacade.CreateServerArgsForCall(1) + createMap, _ = opts.ToServerCreateMap() + server = createMap["server"].(map[string]interface{}) + Expect(server["availability_zone"]).To(Equal("z2")) + + Expect(computeFacade.CreateServerCallCount()).To(Equal(2)) + }) + + It("returns an error if the server creation fails", func() { + computeFacade.CreateServerReturns(nil, errors.New("boom")) + + server, err := computeService.CreateServer( + apiv1.StemcellCID{}, + defaultCloudConfig, + networkConfig, + agentID, + env, + createCpiConfig(10), + ) + + Expect(err.Error()).To(Equal("failed to create server in availability zone 'z1': boom")) + Expect(server).To(BeNil()) + }) + + It("waits for the server to become ACTIVE", func() { + computeFacade.GetServerReturnsOnCall(0, &servers.Server{ID: "123-456", Status: "not-active"}, nil) + computeFacade.GetServerReturnsOnCall(1, &servers.Server{ID: "123-456", Status: "ACTIVE"}, nil) + + server, err := computeService.CreateServer( + apiv1.StemcellCID{}, + defaultCloudConfig, + networkConfig, + agentID, + env, + createCpiConfig(10), + ) + + Expect(err).ToNot(HaveOccurred()) + Expect(server.ID).To(Equal("123-456")) + Expect(computeFacade.GetServerCallCount()).To(Equal(2)) + }) + + It("returns an error while waiting if getting server information fails", func() { + computeFacade.GetServerReturns(nil, errors.New("boom")) + + server, err := computeService.CreateServer( + apiv1.StemcellCID{}, + defaultCloudConfig, + networkConfig, + agentID, + env, + createCpiConfig(10), + ) + + Expect(err.Error()).To(Equal("failed while waiting on the server creation in availability zone 'z1': failed to retrieve server information: boom")) + Expect(server).To(BeNil()) + }) + + It("returns an error while waiting if the server creation finishes in state ERROR", func() { + computeFacade.GetServerReturns(&servers.Server{ID: "123-456", Status: "ERROR"}, nil) + + server, err := computeService.CreateServer( + apiv1.StemcellCID{}, + defaultCloudConfig, + networkConfig, + agentID, + env, + createCpiConfig(10), + ) + + Expect(err.Error()).To(Equal("failed while waiting on the server creation in availability zone 'z1': server became ERROR state while waiting to become ACTIVE")) + Expect(server).To(BeNil()) + }) + + It("returns an error while waiting if the server creation finishes in state DELETED", func() { + computeFacade.GetServerReturns(&servers.Server{ID: "123-456", Status: "DELETED"}, nil) + + server, err := computeService.CreateServer( + apiv1.StemcellCID{}, + defaultCloudConfig, + networkConfig, + agentID, + env, + createCpiConfig(10), + ) + + Expect(err.Error()).To(Equal("failed while waiting on the server creation in availability zone 'z1': server became DELETED state while waiting to become ACTIVE")) + Expect(server).To(BeNil()) + }) + + It("returns an error while waiting if the server creation times out", func() { + computeFacade.GetServerReturns(&servers.Server{ID: "123-456", Status: "not-active"}, nil) + + server, err := computeService.CreateServer( + apiv1.StemcellCID{}, + defaultCloudConfig, + networkConfig, + agentID, + env, + createCpiConfig(0), + ) + + Expect(err.Error()).To(Equal("failed while waiting on the server creation in availability zone 'z1': timeout while waiting for server to become active")) + Expect(server).To(BeNil()) + }) + + It("returns the id of the created server", func() { + server, err := computeService.CreateServer( + apiv1.StemcellCID{}, + defaultCloudConfig, + networkConfig, + agentID, + env, + createCpiConfig(10), + ) + + Expect(err).ToNot(HaveOccurred()) + Expect(server.ID).To(Equal("123-456")) + }) + }) + + Context("DeleteServer", func() { + BeforeEach(func() { + serverMetadata := make(map[string]string) + serverMetadata["tag1"] = "tag1Value" + serverMetadata["lbaas_pool_1"] = "poolID/memberID" + + computeFacade.GetServerReturnsOnCall(0, &servers.Server{ID: "123-456", Status: "ACTIVE"}, nil) + computeFacade.GetServerReturnsOnCall(1, &servers.Server{ID: "123-456", Status: "DELETED"}, nil) + computeFacade.GetServerMetadataReturns(serverMetadata, nil) + loadbalancerService.DeletePoolMemberReturns(nil) + computeFacade.DeleteServerReturns(nil) + }) + + It("deletes a server without raising errors", func() { + err := computeService.DeleteServer( + "123-456", + createCpiConfig(10), + ) + + Expect(err).ToNot(HaveOccurred()) + Expect(computeFacade.GetServerCallCount()).To(Equal(2)) + Expect(computeFacade.DeleteServerCallCount()).To(Equal(1)) + }) + + It("returns an error if getServer fails", func() { + computeFacade.GetServerReturnsOnCall(0, nil, errors.New("boom")) + + err := computeService.DeleteServer( + "123-456", + createCpiConfig(10), + ) + + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal("failed to retrieve server information: boom")) + }) + + It("still succeeds if no server is found", func() { + testError := gophercloud.ErrDefault404{ + ErrUnexpectedResponseCode: gophercloud.ErrUnexpectedResponseCode{Actual: 404}, + } + computeFacade.GetServerReturnsOnCall(0, nil, testError) + + err := computeService.DeleteServer( + "123-456", + createCpiConfig(10), + ) + + Expect(err).ToNot(HaveOccurred()) + }) + + It("returns an error if delete server fails", func() { + computeFacade.DeleteServerReturns(errors.New("boom")) + err := computeService.DeleteServer( + "123-456", + createCpiConfig(10), + ) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal("failed to delete server: boom")) + }) + + It("waits for the server to become TERMINATED", func() { + computeFacade.GetServerReturnsOnCall(1, &servers.Server{ID: "123-456", Status: "TERMINATED"}, nil) + + err := computeService.DeleteServer( + "123-456", + createCpiConfig(10), + ) + + Expect(err).ToNot(HaveOccurred()) + Expect(computeFacade.GetServerCallCount()).To(Equal(2)) + }) + + It("still succeeds if server is not found while deletion", func() { + testError := gophercloud.ErrDefault404{ + ErrUnexpectedResponseCode: gophercloud.ErrUnexpectedResponseCode{Actual: 404}, + } + computeFacade.GetServerReturnsOnCall(1, nil, testError) + + err := computeService.DeleteServer( + "123-456", + createCpiConfig(10), + ) + + Expect(err).ToNot(HaveOccurred()) + Expect(computeFacade.DeleteServerCallCount()).To(Equal(1)) + }) + + It("raises an error if it times out while waiting for the server to become DELETED", func() { + computeFacade.GetServerReturns(&servers.Server{ID: "123-456", Status: "ACTIVE"}, nil) + computeFacade.GetServerReturnsOnCall(1, &servers.Server{ID: "123-456", Status: "ACTIVE"}, nil) + + cpiConfig := createCpiConfig(0) + + err := computeService.DeleteServer( + "123-456", + cpiConfig, + ) + + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal("failed while waiting on the server deletion: timeout while waiting for server to become deleted")) + }) + + It("raises an error if server status has changed to ERROR instead of DELETED", func() { + computeFacade.GetServerReturnsOnCall(1, &servers.Server{ID: "123-456", Status: "ERROR"}, nil) + + err := computeService.DeleteServer( + "123-456", + createCpiConfig(10), + ) + + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal("failed while waiting on the server deletion: server became ERROR state while waiting to become DELETED")) + }) + + It("raises an error if it server retrieval fails while waiting for the server to become DELETED", func() { + computeFacade.GetServerReturnsOnCall(1, nil, errors.New("boom")) + + err := computeService.DeleteServer( + "123-456", + createCpiConfig(10), + ) + + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal("failed while waiting on the server deletion: failed to retrieve server information: boom")) + }) + }) + + Context("RebootServer", func() { + BeforeEach(func() { + computeFacade.GetServerReturns(&servers.Server{ID: "123-456", Status: "TERMINATED"}, nil) + computeFacade.RebootServerReturns(nil) + }) + + It("gets the server information", func() { + computeFacade.GetServerReturns(&servers.Server{ID: "123-456", Status: "ACTIVE"}, nil) + + err := computeService.RebootServer( + "123-456", + createCpiConfig(10), + ) + + Expect(err).ToNot(HaveOccurred()) + Expect(computeFacade.GetServerCallCount()).To(Equal(2)) + }) + + It("waits for the server to become active", func() { + computeFacade.GetServerReturnsOnCall(0, &servers.Server{ID: "123-456", Status: "ACTIVE"}, nil) + computeFacade.GetServerReturnsOnCall(1, &servers.Server{ID: "123-456", Status: "TERMINATED"}, nil) + computeFacade.GetServerReturnsOnCall(2, &servers.Server{ID: "123-456", Status: "ACTIVE"}, nil) + + err := computeService.RebootServer( + "123-456", + createCpiConfig(10), + ) + + Expect(err).ToNot(HaveOccurred()) + Expect(computeFacade.GetServerCallCount()).To(Equal(3)) + }) + + It("fails retrieving the server", func() { + computeFacade.GetServerReturns(nil, errors.New("boom")) + + err := computeService.RebootServer( + "123-456", + createCpiConfig(10), + ) + + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal("failed to retrieve server information: boom")) + }) + + It("failed to reboot the server", func() { + computeFacade.GetServerReturns(&servers.Server{ID: "123-456", Status: "ACTIVE"}, nil) + computeFacade.RebootServerReturns(errors.New("boom")) + + err := computeService.RebootServer( + "123-456", + createCpiConfig(10), + ) + + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal("failed to reboot server: boom")) + }) + + It("times out waiting for the server to become active", func() { + computeFacade.GetServerReturnsOnCall(0, &servers.Server{ID: "123-456", Status: "ACTIVE"}, nil) + + err := computeService.RebootServer( + "123-456", + createCpiConfig(0), + ) + + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal("compute_service: timeout while waiting for server to become active")) + }) + }) + + Context("GetMetadata", func() { + BeforeEach(func() { + serverMetadata := make(map[string]string) + serverMetadata["tag1"] = "tag1Value" + serverMetadata["lbaas_pool_1"] = "poolID/memberID" + + computeFacade.GetServerMetadataReturns(serverMetadata, nil) + + }) + + It("returns server metadata", func() { + serverMetadata, err := computeService.GetMetadata( + "123-456", + ) + + Expect(err).ToNot(HaveOccurred()) + Expect(serverMetadata).To(Equal(map[string]string{"tag1": "tag1Value", "lbaas_pool_1": "poolID/memberID"})) + }) + + It("returns empty metadata and no error if metadata not found", func() { + testError := gophercloud.ErrDefault404{ + ErrUnexpectedResponseCode: gophercloud.ErrUnexpectedResponseCode{Actual: 404}, + } + computeFacade.GetServerMetadataReturns(nil, testError) + + serverMetadata, err := computeService.GetMetadata( + "123-456", + ) + + Expect(err).ToNot(HaveOccurred()) + Expect(serverMetadata).To(Equal(map[string]string{})) + }) + + It("returns an error if metadata retrieval fail", func() { + computeFacade.GetServerMetadataReturns(nil, errors.New("boom")) + + _, err := computeService.GetMetadata( + "123-456", + ) + + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal("failed to retrieve server metadata: boom")) + }) + + }) + + Context("UpdateServer", func() { + serverExp := servers.Server{ID: "123-456", Status: "ACTIVE"} + + It("updates a server without raising errors", func() { + computeFacade.UpdateServerReturns(&serverExp, nil) + serverResult, err := computeService.UpdateServer( + "123-456", + "test-server", + ) + + Expect(err).ToNot(HaveOccurred()) + Expect(*serverResult).To(Equal(serverExp)) + }) + + It("returns an error if UpdateServer fails", func() { + computeFacade.UpdateServerReturns(nil, errors.New("boom")) + + serverResult, err := computeService.UpdateServer( + "123-456", + "test-server", + ) + + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal("failed to update server: boom")) + Expect(serverResult).To(BeNil()) + }) + + }) + + Context("UpdateServerMetadata", func() { + var server servers.Server + + BeforeEach(func() { + server = servers.Server{ID: "123-456"} + }) + + It("does not update metadata due to empty importing map", func() { + server := servers.Server{ID: "123-456"} + updateMetaDataMap := map[string]interface{}{} + + err := computeService.UpdateServerMetadata(server.ID, updateMetaDataMap) + firstLoggerInfo, secondLoggerInfo, _ := logger.InfoArgsForCall(0) + + Expect(computeFacade.UpdateServerMetadataCallCount()).To(Equal(0)) + Expect(logger.InfoCallCount()).To(Equal(1)) + Expect(firstLoggerInfo).To(Equal("compute_service")) + Expect(secondLoggerInfo).To(Equal("SKIPPING: No Metadata was found to be updated for server with id '123-456'")) + Expect(err).ToNot(HaveOccurred()) + }) + + It("does not update metadata due to importing map with only id", func() { + updateMetaDataMap := map[string]interface{}{ + "id": "value1", + } + + err := computeService.UpdateServerMetadata(server.ID, updateMetaDataMap) + firstLoggerInfo, secondLoggerInfo, _ := logger.InfoArgsForCall(0) + + Expect(computeFacade.UpdateServerMetadataCallCount()).To(Equal(0)) + Expect(logger.InfoCallCount()).To(Equal(1)) + Expect(firstLoggerInfo).To(Equal("compute_service")) + Expect(secondLoggerInfo).To(Equal("SKIPPING: No Metadata was found to be updated for server with id '123-456'")) + Expect(err).ToNot(HaveOccurred()) + }) + + It("returns an error if updating server metadata fails", func() { + computeFacade.UpdateServerMetadataReturns(nil, errors.New("boom")) + updateMetaDataMap := map[string]interface{}{ + "test": "value1", + } + + err := computeService.UpdateServerMetadata(server.ID, updateMetaDataMap) + + Expect(err.Error()).To(Equal("failed to update server metadata: boom")) + }) + + It("returns no error if server metadata was updated successfully", func() { + computeFacade.UpdateServerMetadataReturns(nil, nil) + updateMetaDataMap := map[string]interface{}{ + "test": "value1", + } + + err := computeService.UpdateServerMetadata(server.ID, updateMetaDataMap) + + Expect(err).ToNot(HaveOccurred()) + Expect(logger.InfoCallCount()).To(Equal(0)) + }) + }) + + Context("DeleteServerMetadata", func() { + var server servers.Server + + BeforeEach(func() { + server = servers.Server{ID: "123-456"} + }) + + It("does not delete metadata due to empty updated importing map", func() { + updateMetaDataMap := map[string]interface{}{} + oldMetaDataMap := map[string]string{} + + err := computeService.DeleteServerMetaData(server.ID, oldMetaDataMap, updateMetaDataMap) + firstLoggerInfo, secondLoggerInfo, _ := logger.InfoArgsForCall(0) + + Expect(computeFacade.UpdateServerMetadataCallCount()).To(Equal(0)) + Expect(logger.InfoCallCount()).To(Equal(1)) + Expect(firstLoggerInfo).To(Equal("compute_service")) + Expect(secondLoggerInfo).To(Equal("SKIPPING: No metadata was provided to be deleted for server with id '123-456'")) + Expect(err).ToNot(HaveOccurred()) + }) + + It("does not delete metadata due to empty old importing map", func() { + updateMetaDataMap := map[string]interface{}{ + "name": "value1", + "index": "value2", + "test": "value3", + } + oldMetaDataMap := map[string]string{ + "name": "value1", + "index": "value2", + } + + err := computeService.DeleteServerMetaData(server.ID, oldMetaDataMap, updateMetaDataMap) + firstLoggerInfo, secondLoggerInfo, _ := logger.InfoArgsForCall(0) + + Expect(computeFacade.UpdateServerMetadataCallCount()).To(Equal(0)) + Expect(logger.InfoCallCount()).To(Equal(1)) + Expect(firstLoggerInfo).To(Equal("compute_service")) + Expect(secondLoggerInfo).To(Equal("SKIPPING: No metadata was provided to be deleted for server with id '123-456'")) + Expect(err).ToNot(HaveOccurred()) + }) + + It("returns error when deleting server metadata fails", func() { + computeFacade.DeleteServerMetaDataReturns(errors.New("boom")) + updateMetaDataMap := map[string]interface{}{ + "name": "value1", + "index": "value2", + "test": "value3", + } + oldMetaDataMap := map[string]string{ + "name": "value1", + "index": "value2", + "test": "value3", + } + + err := computeService.DeleteServerMetaData(server.ID, oldMetaDataMap, updateMetaDataMap) + + Expect(err.Error()).To(Equal("failed to delete server metadata for key test: boom")) + Expect(computeFacade.DeleteServerMetaDataCallCount()).To(Equal(1)) + }) + + It("does not delete metadata due to empty importing map", func() { + updateMetaDataMap := map[string]interface{}{ + "name": "value1", + "index": "value2", + "test": "value3", + } + oldMetaDataMap := map[string]string{ + "name": "value1", + "index": "value2", + "test": "value3", + } + + err := computeService.DeleteServerMetaData(server.ID, oldMetaDataMap, updateMetaDataMap) + _, actServerID, actMapKey := computeFacade.DeleteServerMetaDataArgsForCall(0) + + Expect(err).ToNot(HaveOccurred()) + Expect(computeFacade.DeleteServerMetaDataCallCount()).To(Equal(1)) + Expect(actServerID).To(Equal(server.ID)) + Expect(actMapKey).To(Equal("test")) + }) + + }) + + Context("GetMatchingFlavor", func() { + var vmResources apiv1.VMResources + + BeforeEach(func() { + vmResources = apiv1.VMResources{CPU: 2, RAM: 4096, EphemeralDiskSize: 10} + }) + + Context("GetMatchingFlavor", func() { + It("returns the flavor", func() { + possibleFlavors := []flavors.Flavor{{ID: "the_flavor_id", Name: "the_instance_type", VCPUs: 2, RAM: 4096, Ephemeral: 10}} + matchedFlavor := flavors.Flavor{ID: "the_flavor_id", Name: "the_instance_type", VCPUs: 2, RAM: 4096, Ephemeral: 10} + flavorResolver.ResolveFlavorForRequirementsReturns(possibleFlavors, nil) + flavorResolver.GetClosestMatchedFlavorReturns(matchedFlavor) + + returnedFlavor, err := computeService.GetMatchingFlavor(vmResources, false) + inputFlavor := flavorResolver.GetClosestMatchedFlavorArgsForCall(0) + Expect(err).ToNot(HaveOccurred()) + Expect(inputFlavor).To(Equal(possibleFlavors)) + Expect(returnedFlavor).To(Equal(matchedFlavor)) + }) + }) + + It("returns an error if no flavor is found", func() { + flavorResolver.ResolveFlavorForRequirementsReturns([]flavors.Flavor{}, nil) + + _, err := computeService.GetMatchingFlavor(vmResources, false) + + Expect(err.Error()).To(ContainSubstring("Unable to meet requested VM requirements:")) + }) + + It("returns an error if flavorResolver.ResolveFlavorForRequirements returns an error", func() { + flavorResolver.ResolveFlavorForRequirementsReturns([]flavors.Flavor{}, errors.New("boom")) + + _, err := computeService.GetMatchingFlavor(vmResources, false) + + Expect(err.Error()).To(ContainSubstring("failed to get flavors:")) + }) + }) +}) + +func createCpiConfig(stateTimeOut int) config.CpiConfig { + cpiConfig := config.CpiConfig{} + cpiConfig.Cloud.Properties.Openstack = + config.OpenstackConfig{StateTimeOut: stateTimeOut, DefaultKeyName: "the_key_name", UseDHCP: true} + return cpiConfig +} diff --git a/src/openstack_cpi_golang/cpi/compute/compute_suite_test.go b/src/openstack_cpi_golang/cpi/compute/compute_suite_test.go new file mode 100644 index 00000000..64f29428 --- /dev/null +++ b/src/openstack_cpi_golang/cpi/compute/compute_suite_test.go @@ -0,0 +1,13 @@ +package compute_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestMethods(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Compute Suite") +} diff --git a/src/openstack_cpi_golang/cpi/compute/computefakes/fake_availability_zone_provider.go b/src/openstack_cpi_golang/cpi/compute/computefakes/fake_availability_zone_provider.go new file mode 100644 index 00000000..53819cb2 --- /dev/null +++ b/src/openstack_cpi_golang/cpi/compute/computefakes/fake_availability_zone_provider.go @@ -0,0 +1,112 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package computefakes + +import ( + "sync" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/compute" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/properties" +) + +type FakeAvailabilityZoneProvider struct { + GetAvailabilityZonesStub func(properties.CreateVM) []string + getAvailabilityZonesMutex sync.RWMutex + getAvailabilityZonesArgsForCall []struct { + arg1 properties.CreateVM + } + getAvailabilityZonesReturns struct { + result1 []string + } + getAvailabilityZonesReturnsOnCall map[int]struct { + result1 []string + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *FakeAvailabilityZoneProvider) GetAvailabilityZones(arg1 properties.CreateVM) []string { + fake.getAvailabilityZonesMutex.Lock() + ret, specificReturn := fake.getAvailabilityZonesReturnsOnCall[len(fake.getAvailabilityZonesArgsForCall)] + fake.getAvailabilityZonesArgsForCall = append(fake.getAvailabilityZonesArgsForCall, struct { + arg1 properties.CreateVM + }{arg1}) + stub := fake.GetAvailabilityZonesStub + fakeReturns := fake.getAvailabilityZonesReturns + fake.recordInvocation("GetAvailabilityZones", []interface{}{arg1}) + fake.getAvailabilityZonesMutex.Unlock() + if stub != nil { + return stub(arg1) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeAvailabilityZoneProvider) GetAvailabilityZonesCallCount() int { + fake.getAvailabilityZonesMutex.RLock() + defer fake.getAvailabilityZonesMutex.RUnlock() + return len(fake.getAvailabilityZonesArgsForCall) +} + +func (fake *FakeAvailabilityZoneProvider) GetAvailabilityZonesCalls(stub func(properties.CreateVM) []string) { + fake.getAvailabilityZonesMutex.Lock() + defer fake.getAvailabilityZonesMutex.Unlock() + fake.GetAvailabilityZonesStub = stub +} + +func (fake *FakeAvailabilityZoneProvider) GetAvailabilityZonesArgsForCall(i int) properties.CreateVM { + fake.getAvailabilityZonesMutex.RLock() + defer fake.getAvailabilityZonesMutex.RUnlock() + argsForCall := fake.getAvailabilityZonesArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeAvailabilityZoneProvider) GetAvailabilityZonesReturns(result1 []string) { + fake.getAvailabilityZonesMutex.Lock() + defer fake.getAvailabilityZonesMutex.Unlock() + fake.GetAvailabilityZonesStub = nil + fake.getAvailabilityZonesReturns = struct { + result1 []string + }{result1} +} + +func (fake *FakeAvailabilityZoneProvider) GetAvailabilityZonesReturnsOnCall(i int, result1 []string) { + fake.getAvailabilityZonesMutex.Lock() + defer fake.getAvailabilityZonesMutex.Unlock() + fake.GetAvailabilityZonesStub = nil + if fake.getAvailabilityZonesReturnsOnCall == nil { + fake.getAvailabilityZonesReturnsOnCall = make(map[int]struct { + result1 []string + }) + } + fake.getAvailabilityZonesReturnsOnCall[i] = struct { + result1 []string + }{result1} +} + +func (fake *FakeAvailabilityZoneProvider) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.getAvailabilityZonesMutex.RLock() + defer fake.getAvailabilityZonesMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *FakeAvailabilityZoneProvider) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ compute.AvailabilityZoneProvider = new(FakeAvailabilityZoneProvider) diff --git a/src/openstack_cpi_golang/cpi/compute/computefakes/fake_compute_facade.go b/src/openstack_cpi_golang/cpi/compute/computefakes/fake_compute_facade.go new file mode 100644 index 00000000..46bfc146 --- /dev/null +++ b/src/openstack_cpi_golang/cpi/compute/computefakes/fake_compute_facade.go @@ -0,0 +1,1251 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package computefakes + +import ( + "sync" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/compute" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils" + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs" + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/volumeattach" + "github.com/gophercloud/gophercloud/openstack/compute/v2/flavors" + "github.com/gophercloud/gophercloud/openstack/compute/v2/servers" + "github.com/gophercloud/gophercloud/pagination" +) + +type FakeComputeFacade struct { + AttachVolumeStub func(*gophercloud.ServiceClient, string, volumeattach.CreateOptsBuilder) (*volumeattach.VolumeAttachment, error) + attachVolumeMutex sync.RWMutex + attachVolumeArgsForCall []struct { + arg1 *gophercloud.ServiceClient + arg2 string + arg3 volumeattach.CreateOptsBuilder + } + attachVolumeReturns struct { + result1 *volumeattach.VolumeAttachment + result2 error + } + attachVolumeReturnsOnCall map[int]struct { + result1 *volumeattach.VolumeAttachment + result2 error + } + CreateServerStub func(utils.ServiceClient, servers.CreateOptsBuilder) (*servers.Server, error) + createServerMutex sync.RWMutex + createServerArgsForCall []struct { + arg1 utils.ServiceClient + arg2 servers.CreateOptsBuilder + } + createServerReturns struct { + result1 *servers.Server + result2 error + } + createServerReturnsOnCall map[int]struct { + result1 *servers.Server + result2 error + } + DeleteServerStub func(utils.RetryableServiceClient, string) error + deleteServerMutex sync.RWMutex + deleteServerArgsForCall []struct { + arg1 utils.RetryableServiceClient + arg2 string + } + deleteServerReturns struct { + result1 error + } + deleteServerReturnsOnCall map[int]struct { + result1 error + } + DeleteServerMetaDataStub func(*gophercloud.ServiceClient, string, string) error + deleteServerMetaDataMutex sync.RWMutex + deleteServerMetaDataArgsForCall []struct { + arg1 *gophercloud.ServiceClient + arg2 string + arg3 string + } + deleteServerMetaDataReturns struct { + result1 error + } + deleteServerMetaDataReturnsOnCall map[int]struct { + result1 error + } + DetachVolumeStub func(*gophercloud.ServiceClient, string, string) error + detachVolumeMutex sync.RWMutex + detachVolumeArgsForCall []struct { + arg1 *gophercloud.ServiceClient + arg2 string + arg3 string + } + detachVolumeReturns struct { + result1 error + } + detachVolumeReturnsOnCall map[int]struct { + result1 error + } + ExtractFlavorsStub func(pagination.Page) ([]flavors.Flavor, error) + extractFlavorsMutex sync.RWMutex + extractFlavorsArgsForCall []struct { + arg1 pagination.Page + } + extractFlavorsReturns struct { + result1 []flavors.Flavor + result2 error + } + extractFlavorsReturnsOnCall map[int]struct { + result1 []flavors.Flavor + result2 error + } + GetOSKeyPairStub func(utils.RetryableServiceClient, string, keypairs.GetOpts) (*keypairs.KeyPair, error) + getOSKeyPairMutex sync.RWMutex + getOSKeyPairArgsForCall []struct { + arg1 utils.RetryableServiceClient + arg2 string + arg3 keypairs.GetOpts + } + getOSKeyPairReturns struct { + result1 *keypairs.KeyPair + result2 error + } + getOSKeyPairReturnsOnCall map[int]struct { + result1 *keypairs.KeyPair + result2 error + } + GetServerStub func(utils.RetryableServiceClient, string) (*servers.Server, error) + getServerMutex sync.RWMutex + getServerArgsForCall []struct { + arg1 utils.RetryableServiceClient + arg2 string + } + getServerReturns struct { + result1 *servers.Server + result2 error + } + getServerReturnsOnCall map[int]struct { + result1 *servers.Server + result2 error + } + GetServerMetadataStub func(utils.RetryableServiceClient, string) (map[string]string, error) + getServerMetadataMutex sync.RWMutex + getServerMetadataArgsForCall []struct { + arg1 utils.RetryableServiceClient + arg2 string + } + getServerMetadataReturns struct { + result1 map[string]string + result2 error + } + getServerMetadataReturnsOnCall map[int]struct { + result1 map[string]string + result2 error + } + GetServerWithAZStub func(utils.RetryableServiceClient, string) (*compute.ServerWithAZ, error) + getServerWithAZMutex sync.RWMutex + getServerWithAZArgsForCall []struct { + arg1 utils.RetryableServiceClient + arg2 string + } + getServerWithAZReturns struct { + result1 *compute.ServerWithAZ + result2 error + } + getServerWithAZReturnsOnCall map[int]struct { + result1 *compute.ServerWithAZ + result2 error + } + ListFlavorsStub func(utils.RetryableServiceClient, flavors.ListOpts) (pagination.Page, error) + listFlavorsMutex sync.RWMutex + listFlavorsArgsForCall []struct { + arg1 utils.RetryableServiceClient + arg2 flavors.ListOpts + } + listFlavorsReturns struct { + result1 pagination.Page + result2 error + } + listFlavorsReturnsOnCall map[int]struct { + result1 pagination.Page + result2 error + } + ListVolumeAttachmentsStub func(*gophercloud.ServiceClient, string) ([]volumeattach.VolumeAttachment, error) + listVolumeAttachmentsMutex sync.RWMutex + listVolumeAttachmentsArgsForCall []struct { + arg1 *gophercloud.ServiceClient + arg2 string + } + listVolumeAttachmentsReturns struct { + result1 []volumeattach.VolumeAttachment + result2 error + } + listVolumeAttachmentsReturnsOnCall map[int]struct { + result1 []volumeattach.VolumeAttachment + result2 error + } + RebootServerStub func(utils.ServiceClient, string, servers.RebootOptsBuilder) error + rebootServerMutex sync.RWMutex + rebootServerArgsForCall []struct { + arg1 utils.ServiceClient + arg2 string + arg3 servers.RebootOptsBuilder + } + rebootServerReturns struct { + result1 error + } + rebootServerReturnsOnCall map[int]struct { + result1 error + } + UpdateServerStub func(utils.ServiceClient, string, servers.UpdateOptsBuilder) (*servers.Server, error) + updateServerMutex sync.RWMutex + updateServerArgsForCall []struct { + arg1 utils.ServiceClient + arg2 string + arg3 servers.UpdateOptsBuilder + } + updateServerReturns struct { + result1 *servers.Server + result2 error + } + updateServerReturnsOnCall map[int]struct { + result1 *servers.Server + result2 error + } + UpdateServerMetadataStub func(utils.ServiceClient, string, servers.UpdateMetadataOptsBuilder) (map[string]string, error) + updateServerMetadataMutex sync.RWMutex + updateServerMetadataArgsForCall []struct { + arg1 utils.ServiceClient + arg2 string + arg3 servers.UpdateMetadataOptsBuilder + } + updateServerMetadataReturns struct { + result1 map[string]string + result2 error + } + updateServerMetadataReturnsOnCall map[int]struct { + result1 map[string]string + result2 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *FakeComputeFacade) AttachVolume(arg1 *gophercloud.ServiceClient, arg2 string, arg3 volumeattach.CreateOptsBuilder) (*volumeattach.VolumeAttachment, error) { + fake.attachVolumeMutex.Lock() + ret, specificReturn := fake.attachVolumeReturnsOnCall[len(fake.attachVolumeArgsForCall)] + fake.attachVolumeArgsForCall = append(fake.attachVolumeArgsForCall, struct { + arg1 *gophercloud.ServiceClient + arg2 string + arg3 volumeattach.CreateOptsBuilder + }{arg1, arg2, arg3}) + stub := fake.AttachVolumeStub + fakeReturns := fake.attachVolumeReturns + fake.recordInvocation("AttachVolume", []interface{}{arg1, arg2, arg3}) + fake.attachVolumeMutex.Unlock() + if stub != nil { + return stub(arg1, arg2, arg3) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeComputeFacade) AttachVolumeCallCount() int { + fake.attachVolumeMutex.RLock() + defer fake.attachVolumeMutex.RUnlock() + return len(fake.attachVolumeArgsForCall) +} + +func (fake *FakeComputeFacade) AttachVolumeCalls(stub func(*gophercloud.ServiceClient, string, volumeattach.CreateOptsBuilder) (*volumeattach.VolumeAttachment, error)) { + fake.attachVolumeMutex.Lock() + defer fake.attachVolumeMutex.Unlock() + fake.AttachVolumeStub = stub +} + +func (fake *FakeComputeFacade) AttachVolumeArgsForCall(i int) (*gophercloud.ServiceClient, string, volumeattach.CreateOptsBuilder) { + fake.attachVolumeMutex.RLock() + defer fake.attachVolumeMutex.RUnlock() + argsForCall := fake.attachVolumeArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 +} + +func (fake *FakeComputeFacade) AttachVolumeReturns(result1 *volumeattach.VolumeAttachment, result2 error) { + fake.attachVolumeMutex.Lock() + defer fake.attachVolumeMutex.Unlock() + fake.AttachVolumeStub = nil + fake.attachVolumeReturns = struct { + result1 *volumeattach.VolumeAttachment + result2 error + }{result1, result2} +} + +func (fake *FakeComputeFacade) AttachVolumeReturnsOnCall(i int, result1 *volumeattach.VolumeAttachment, result2 error) { + fake.attachVolumeMutex.Lock() + defer fake.attachVolumeMutex.Unlock() + fake.AttachVolumeStub = nil + if fake.attachVolumeReturnsOnCall == nil { + fake.attachVolumeReturnsOnCall = make(map[int]struct { + result1 *volumeattach.VolumeAttachment + result2 error + }) + } + fake.attachVolumeReturnsOnCall[i] = struct { + result1 *volumeattach.VolumeAttachment + result2 error + }{result1, result2} +} + +func (fake *FakeComputeFacade) CreateServer(arg1 utils.ServiceClient, arg2 servers.CreateOptsBuilder) (*servers.Server, error) { + fake.createServerMutex.Lock() + ret, specificReturn := fake.createServerReturnsOnCall[len(fake.createServerArgsForCall)] + fake.createServerArgsForCall = append(fake.createServerArgsForCall, struct { + arg1 utils.ServiceClient + arg2 servers.CreateOptsBuilder + }{arg1, arg2}) + stub := fake.CreateServerStub + fakeReturns := fake.createServerReturns + fake.recordInvocation("CreateServer", []interface{}{arg1, arg2}) + fake.createServerMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeComputeFacade) CreateServerCallCount() int { + fake.createServerMutex.RLock() + defer fake.createServerMutex.RUnlock() + return len(fake.createServerArgsForCall) +} + +func (fake *FakeComputeFacade) CreateServerCalls(stub func(utils.ServiceClient, servers.CreateOptsBuilder) (*servers.Server, error)) { + fake.createServerMutex.Lock() + defer fake.createServerMutex.Unlock() + fake.CreateServerStub = stub +} + +func (fake *FakeComputeFacade) CreateServerArgsForCall(i int) (utils.ServiceClient, servers.CreateOptsBuilder) { + fake.createServerMutex.RLock() + defer fake.createServerMutex.RUnlock() + argsForCall := fake.createServerArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeComputeFacade) CreateServerReturns(result1 *servers.Server, result2 error) { + fake.createServerMutex.Lock() + defer fake.createServerMutex.Unlock() + fake.CreateServerStub = nil + fake.createServerReturns = struct { + result1 *servers.Server + result2 error + }{result1, result2} +} + +func (fake *FakeComputeFacade) CreateServerReturnsOnCall(i int, result1 *servers.Server, result2 error) { + fake.createServerMutex.Lock() + defer fake.createServerMutex.Unlock() + fake.CreateServerStub = nil + if fake.createServerReturnsOnCall == nil { + fake.createServerReturnsOnCall = make(map[int]struct { + result1 *servers.Server + result2 error + }) + } + fake.createServerReturnsOnCall[i] = struct { + result1 *servers.Server + result2 error + }{result1, result2} +} + +func (fake *FakeComputeFacade) DeleteServer(arg1 utils.RetryableServiceClient, arg2 string) error { + fake.deleteServerMutex.Lock() + ret, specificReturn := fake.deleteServerReturnsOnCall[len(fake.deleteServerArgsForCall)] + fake.deleteServerArgsForCall = append(fake.deleteServerArgsForCall, struct { + arg1 utils.RetryableServiceClient + arg2 string + }{arg1, arg2}) + stub := fake.DeleteServerStub + fakeReturns := fake.deleteServerReturns + fake.recordInvocation("DeleteServer", []interface{}{arg1, arg2}) + fake.deleteServerMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeComputeFacade) DeleteServerCallCount() int { + fake.deleteServerMutex.RLock() + defer fake.deleteServerMutex.RUnlock() + return len(fake.deleteServerArgsForCall) +} + +func (fake *FakeComputeFacade) DeleteServerCalls(stub func(utils.RetryableServiceClient, string) error) { + fake.deleteServerMutex.Lock() + defer fake.deleteServerMutex.Unlock() + fake.DeleteServerStub = stub +} + +func (fake *FakeComputeFacade) DeleteServerArgsForCall(i int) (utils.RetryableServiceClient, string) { + fake.deleteServerMutex.RLock() + defer fake.deleteServerMutex.RUnlock() + argsForCall := fake.deleteServerArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeComputeFacade) DeleteServerReturns(result1 error) { + fake.deleteServerMutex.Lock() + defer fake.deleteServerMutex.Unlock() + fake.DeleteServerStub = nil + fake.deleteServerReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeComputeFacade) DeleteServerReturnsOnCall(i int, result1 error) { + fake.deleteServerMutex.Lock() + defer fake.deleteServerMutex.Unlock() + fake.DeleteServerStub = nil + if fake.deleteServerReturnsOnCall == nil { + fake.deleteServerReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.deleteServerReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *FakeComputeFacade) DeleteServerMetaData(arg1 *gophercloud.ServiceClient, arg2 string, arg3 string) error { + fake.deleteServerMetaDataMutex.Lock() + ret, specificReturn := fake.deleteServerMetaDataReturnsOnCall[len(fake.deleteServerMetaDataArgsForCall)] + fake.deleteServerMetaDataArgsForCall = append(fake.deleteServerMetaDataArgsForCall, struct { + arg1 *gophercloud.ServiceClient + arg2 string + arg3 string + }{arg1, arg2, arg3}) + stub := fake.DeleteServerMetaDataStub + fakeReturns := fake.deleteServerMetaDataReturns + fake.recordInvocation("DeleteServerMetaData", []interface{}{arg1, arg2, arg3}) + fake.deleteServerMetaDataMutex.Unlock() + if stub != nil { + return stub(arg1, arg2, arg3) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeComputeFacade) DeleteServerMetaDataCallCount() int { + fake.deleteServerMetaDataMutex.RLock() + defer fake.deleteServerMetaDataMutex.RUnlock() + return len(fake.deleteServerMetaDataArgsForCall) +} + +func (fake *FakeComputeFacade) DeleteServerMetaDataCalls(stub func(*gophercloud.ServiceClient, string, string) error) { + fake.deleteServerMetaDataMutex.Lock() + defer fake.deleteServerMetaDataMutex.Unlock() + fake.DeleteServerMetaDataStub = stub +} + +func (fake *FakeComputeFacade) DeleteServerMetaDataArgsForCall(i int) (*gophercloud.ServiceClient, string, string) { + fake.deleteServerMetaDataMutex.RLock() + defer fake.deleteServerMetaDataMutex.RUnlock() + argsForCall := fake.deleteServerMetaDataArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 +} + +func (fake *FakeComputeFacade) DeleteServerMetaDataReturns(result1 error) { + fake.deleteServerMetaDataMutex.Lock() + defer fake.deleteServerMetaDataMutex.Unlock() + fake.DeleteServerMetaDataStub = nil + fake.deleteServerMetaDataReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeComputeFacade) DeleteServerMetaDataReturnsOnCall(i int, result1 error) { + fake.deleteServerMetaDataMutex.Lock() + defer fake.deleteServerMetaDataMutex.Unlock() + fake.DeleteServerMetaDataStub = nil + if fake.deleteServerMetaDataReturnsOnCall == nil { + fake.deleteServerMetaDataReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.deleteServerMetaDataReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *FakeComputeFacade) DetachVolume(arg1 *gophercloud.ServiceClient, arg2 string, arg3 string) error { + fake.detachVolumeMutex.Lock() + ret, specificReturn := fake.detachVolumeReturnsOnCall[len(fake.detachVolumeArgsForCall)] + fake.detachVolumeArgsForCall = append(fake.detachVolumeArgsForCall, struct { + arg1 *gophercloud.ServiceClient + arg2 string + arg3 string + }{arg1, arg2, arg3}) + stub := fake.DetachVolumeStub + fakeReturns := fake.detachVolumeReturns + fake.recordInvocation("DetachVolume", []interface{}{arg1, arg2, arg3}) + fake.detachVolumeMutex.Unlock() + if stub != nil { + return stub(arg1, arg2, arg3) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeComputeFacade) DetachVolumeCallCount() int { + fake.detachVolumeMutex.RLock() + defer fake.detachVolumeMutex.RUnlock() + return len(fake.detachVolumeArgsForCall) +} + +func (fake *FakeComputeFacade) DetachVolumeCalls(stub func(*gophercloud.ServiceClient, string, string) error) { + fake.detachVolumeMutex.Lock() + defer fake.detachVolumeMutex.Unlock() + fake.DetachVolumeStub = stub +} + +func (fake *FakeComputeFacade) DetachVolumeArgsForCall(i int) (*gophercloud.ServiceClient, string, string) { + fake.detachVolumeMutex.RLock() + defer fake.detachVolumeMutex.RUnlock() + argsForCall := fake.detachVolumeArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 +} + +func (fake *FakeComputeFacade) DetachVolumeReturns(result1 error) { + fake.detachVolumeMutex.Lock() + defer fake.detachVolumeMutex.Unlock() + fake.DetachVolumeStub = nil + fake.detachVolumeReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeComputeFacade) DetachVolumeReturnsOnCall(i int, result1 error) { + fake.detachVolumeMutex.Lock() + defer fake.detachVolumeMutex.Unlock() + fake.DetachVolumeStub = nil + if fake.detachVolumeReturnsOnCall == nil { + fake.detachVolumeReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.detachVolumeReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *FakeComputeFacade) ExtractFlavors(arg1 pagination.Page) ([]flavors.Flavor, error) { + fake.extractFlavorsMutex.Lock() + ret, specificReturn := fake.extractFlavorsReturnsOnCall[len(fake.extractFlavorsArgsForCall)] + fake.extractFlavorsArgsForCall = append(fake.extractFlavorsArgsForCall, struct { + arg1 pagination.Page + }{arg1}) + stub := fake.ExtractFlavorsStub + fakeReturns := fake.extractFlavorsReturns + fake.recordInvocation("ExtractFlavors", []interface{}{arg1}) + fake.extractFlavorsMutex.Unlock() + if stub != nil { + return stub(arg1) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeComputeFacade) ExtractFlavorsCallCount() int { + fake.extractFlavorsMutex.RLock() + defer fake.extractFlavorsMutex.RUnlock() + return len(fake.extractFlavorsArgsForCall) +} + +func (fake *FakeComputeFacade) ExtractFlavorsCalls(stub func(pagination.Page) ([]flavors.Flavor, error)) { + fake.extractFlavorsMutex.Lock() + defer fake.extractFlavorsMutex.Unlock() + fake.ExtractFlavorsStub = stub +} + +func (fake *FakeComputeFacade) ExtractFlavorsArgsForCall(i int) pagination.Page { + fake.extractFlavorsMutex.RLock() + defer fake.extractFlavorsMutex.RUnlock() + argsForCall := fake.extractFlavorsArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeComputeFacade) ExtractFlavorsReturns(result1 []flavors.Flavor, result2 error) { + fake.extractFlavorsMutex.Lock() + defer fake.extractFlavorsMutex.Unlock() + fake.ExtractFlavorsStub = nil + fake.extractFlavorsReturns = struct { + result1 []flavors.Flavor + result2 error + }{result1, result2} +} + +func (fake *FakeComputeFacade) ExtractFlavorsReturnsOnCall(i int, result1 []flavors.Flavor, result2 error) { + fake.extractFlavorsMutex.Lock() + defer fake.extractFlavorsMutex.Unlock() + fake.ExtractFlavorsStub = nil + if fake.extractFlavorsReturnsOnCall == nil { + fake.extractFlavorsReturnsOnCall = make(map[int]struct { + result1 []flavors.Flavor + result2 error + }) + } + fake.extractFlavorsReturnsOnCall[i] = struct { + result1 []flavors.Flavor + result2 error + }{result1, result2} +} + +func (fake *FakeComputeFacade) GetOSKeyPair(arg1 utils.RetryableServiceClient, arg2 string, arg3 keypairs.GetOpts) (*keypairs.KeyPair, error) { + fake.getOSKeyPairMutex.Lock() + ret, specificReturn := fake.getOSKeyPairReturnsOnCall[len(fake.getOSKeyPairArgsForCall)] + fake.getOSKeyPairArgsForCall = append(fake.getOSKeyPairArgsForCall, struct { + arg1 utils.RetryableServiceClient + arg2 string + arg3 keypairs.GetOpts + }{arg1, arg2, arg3}) + stub := fake.GetOSKeyPairStub + fakeReturns := fake.getOSKeyPairReturns + fake.recordInvocation("GetOSKeyPair", []interface{}{arg1, arg2, arg3}) + fake.getOSKeyPairMutex.Unlock() + if stub != nil { + return stub(arg1, arg2, arg3) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeComputeFacade) GetOSKeyPairCallCount() int { + fake.getOSKeyPairMutex.RLock() + defer fake.getOSKeyPairMutex.RUnlock() + return len(fake.getOSKeyPairArgsForCall) +} + +func (fake *FakeComputeFacade) GetOSKeyPairCalls(stub func(utils.RetryableServiceClient, string, keypairs.GetOpts) (*keypairs.KeyPair, error)) { + fake.getOSKeyPairMutex.Lock() + defer fake.getOSKeyPairMutex.Unlock() + fake.GetOSKeyPairStub = stub +} + +func (fake *FakeComputeFacade) GetOSKeyPairArgsForCall(i int) (utils.RetryableServiceClient, string, keypairs.GetOpts) { + fake.getOSKeyPairMutex.RLock() + defer fake.getOSKeyPairMutex.RUnlock() + argsForCall := fake.getOSKeyPairArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 +} + +func (fake *FakeComputeFacade) GetOSKeyPairReturns(result1 *keypairs.KeyPair, result2 error) { + fake.getOSKeyPairMutex.Lock() + defer fake.getOSKeyPairMutex.Unlock() + fake.GetOSKeyPairStub = nil + fake.getOSKeyPairReturns = struct { + result1 *keypairs.KeyPair + result2 error + }{result1, result2} +} + +func (fake *FakeComputeFacade) GetOSKeyPairReturnsOnCall(i int, result1 *keypairs.KeyPair, result2 error) { + fake.getOSKeyPairMutex.Lock() + defer fake.getOSKeyPairMutex.Unlock() + fake.GetOSKeyPairStub = nil + if fake.getOSKeyPairReturnsOnCall == nil { + fake.getOSKeyPairReturnsOnCall = make(map[int]struct { + result1 *keypairs.KeyPair + result2 error + }) + } + fake.getOSKeyPairReturnsOnCall[i] = struct { + result1 *keypairs.KeyPair + result2 error + }{result1, result2} +} + +func (fake *FakeComputeFacade) GetServer(arg1 utils.RetryableServiceClient, arg2 string) (*servers.Server, error) { + fake.getServerMutex.Lock() + ret, specificReturn := fake.getServerReturnsOnCall[len(fake.getServerArgsForCall)] + fake.getServerArgsForCall = append(fake.getServerArgsForCall, struct { + arg1 utils.RetryableServiceClient + arg2 string + }{arg1, arg2}) + stub := fake.GetServerStub + fakeReturns := fake.getServerReturns + fake.recordInvocation("GetServer", []interface{}{arg1, arg2}) + fake.getServerMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeComputeFacade) GetServerCallCount() int { + fake.getServerMutex.RLock() + defer fake.getServerMutex.RUnlock() + return len(fake.getServerArgsForCall) +} + +func (fake *FakeComputeFacade) GetServerCalls(stub func(utils.RetryableServiceClient, string) (*servers.Server, error)) { + fake.getServerMutex.Lock() + defer fake.getServerMutex.Unlock() + fake.GetServerStub = stub +} + +func (fake *FakeComputeFacade) GetServerArgsForCall(i int) (utils.RetryableServiceClient, string) { + fake.getServerMutex.RLock() + defer fake.getServerMutex.RUnlock() + argsForCall := fake.getServerArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeComputeFacade) GetServerReturns(result1 *servers.Server, result2 error) { + fake.getServerMutex.Lock() + defer fake.getServerMutex.Unlock() + fake.GetServerStub = nil + fake.getServerReturns = struct { + result1 *servers.Server + result2 error + }{result1, result2} +} + +func (fake *FakeComputeFacade) GetServerReturnsOnCall(i int, result1 *servers.Server, result2 error) { + fake.getServerMutex.Lock() + defer fake.getServerMutex.Unlock() + fake.GetServerStub = nil + if fake.getServerReturnsOnCall == nil { + fake.getServerReturnsOnCall = make(map[int]struct { + result1 *servers.Server + result2 error + }) + } + fake.getServerReturnsOnCall[i] = struct { + result1 *servers.Server + result2 error + }{result1, result2} +} + +func (fake *FakeComputeFacade) GetServerMetadata(arg1 utils.RetryableServiceClient, arg2 string) (map[string]string, error) { + fake.getServerMetadataMutex.Lock() + ret, specificReturn := fake.getServerMetadataReturnsOnCall[len(fake.getServerMetadataArgsForCall)] + fake.getServerMetadataArgsForCall = append(fake.getServerMetadataArgsForCall, struct { + arg1 utils.RetryableServiceClient + arg2 string + }{arg1, arg2}) + stub := fake.GetServerMetadataStub + fakeReturns := fake.getServerMetadataReturns + fake.recordInvocation("GetServerMetadata", []interface{}{arg1, arg2}) + fake.getServerMetadataMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeComputeFacade) GetServerMetadataCallCount() int { + fake.getServerMetadataMutex.RLock() + defer fake.getServerMetadataMutex.RUnlock() + return len(fake.getServerMetadataArgsForCall) +} + +func (fake *FakeComputeFacade) GetServerMetadataCalls(stub func(utils.RetryableServiceClient, string) (map[string]string, error)) { + fake.getServerMetadataMutex.Lock() + defer fake.getServerMetadataMutex.Unlock() + fake.GetServerMetadataStub = stub +} + +func (fake *FakeComputeFacade) GetServerMetadataArgsForCall(i int) (utils.RetryableServiceClient, string) { + fake.getServerMetadataMutex.RLock() + defer fake.getServerMetadataMutex.RUnlock() + argsForCall := fake.getServerMetadataArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeComputeFacade) GetServerMetadataReturns(result1 map[string]string, result2 error) { + fake.getServerMetadataMutex.Lock() + defer fake.getServerMetadataMutex.Unlock() + fake.GetServerMetadataStub = nil + fake.getServerMetadataReturns = struct { + result1 map[string]string + result2 error + }{result1, result2} +} + +func (fake *FakeComputeFacade) GetServerMetadataReturnsOnCall(i int, result1 map[string]string, result2 error) { + fake.getServerMetadataMutex.Lock() + defer fake.getServerMetadataMutex.Unlock() + fake.GetServerMetadataStub = nil + if fake.getServerMetadataReturnsOnCall == nil { + fake.getServerMetadataReturnsOnCall = make(map[int]struct { + result1 map[string]string + result2 error + }) + } + fake.getServerMetadataReturnsOnCall[i] = struct { + result1 map[string]string + result2 error + }{result1, result2} +} + +func (fake *FakeComputeFacade) GetServerWithAZ(arg1 utils.RetryableServiceClient, arg2 string) (*compute.ServerWithAZ, error) { + fake.getServerWithAZMutex.Lock() + ret, specificReturn := fake.getServerWithAZReturnsOnCall[len(fake.getServerWithAZArgsForCall)] + fake.getServerWithAZArgsForCall = append(fake.getServerWithAZArgsForCall, struct { + arg1 utils.RetryableServiceClient + arg2 string + }{arg1, arg2}) + stub := fake.GetServerWithAZStub + fakeReturns := fake.getServerWithAZReturns + fake.recordInvocation("GetServerWithAZ", []interface{}{arg1, arg2}) + fake.getServerWithAZMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeComputeFacade) GetServerWithAZCallCount() int { + fake.getServerWithAZMutex.RLock() + defer fake.getServerWithAZMutex.RUnlock() + return len(fake.getServerWithAZArgsForCall) +} + +func (fake *FakeComputeFacade) GetServerWithAZCalls(stub func(utils.RetryableServiceClient, string) (*compute.ServerWithAZ, error)) { + fake.getServerWithAZMutex.Lock() + defer fake.getServerWithAZMutex.Unlock() + fake.GetServerWithAZStub = stub +} + +func (fake *FakeComputeFacade) GetServerWithAZArgsForCall(i int) (utils.RetryableServiceClient, string) { + fake.getServerWithAZMutex.RLock() + defer fake.getServerWithAZMutex.RUnlock() + argsForCall := fake.getServerWithAZArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeComputeFacade) GetServerWithAZReturns(result1 *compute.ServerWithAZ, result2 error) { + fake.getServerWithAZMutex.Lock() + defer fake.getServerWithAZMutex.Unlock() + fake.GetServerWithAZStub = nil + fake.getServerWithAZReturns = struct { + result1 *compute.ServerWithAZ + result2 error + }{result1, result2} +} + +func (fake *FakeComputeFacade) GetServerWithAZReturnsOnCall(i int, result1 *compute.ServerWithAZ, result2 error) { + fake.getServerWithAZMutex.Lock() + defer fake.getServerWithAZMutex.Unlock() + fake.GetServerWithAZStub = nil + if fake.getServerWithAZReturnsOnCall == nil { + fake.getServerWithAZReturnsOnCall = make(map[int]struct { + result1 *compute.ServerWithAZ + result2 error + }) + } + fake.getServerWithAZReturnsOnCall[i] = struct { + result1 *compute.ServerWithAZ + result2 error + }{result1, result2} +} + +func (fake *FakeComputeFacade) ListFlavors(arg1 utils.RetryableServiceClient, arg2 flavors.ListOpts) (pagination.Page, error) { + fake.listFlavorsMutex.Lock() + ret, specificReturn := fake.listFlavorsReturnsOnCall[len(fake.listFlavorsArgsForCall)] + fake.listFlavorsArgsForCall = append(fake.listFlavorsArgsForCall, struct { + arg1 utils.RetryableServiceClient + arg2 flavors.ListOpts + }{arg1, arg2}) + stub := fake.ListFlavorsStub + fakeReturns := fake.listFlavorsReturns + fake.recordInvocation("ListFlavors", []interface{}{arg1, arg2}) + fake.listFlavorsMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeComputeFacade) ListFlavorsCallCount() int { + fake.listFlavorsMutex.RLock() + defer fake.listFlavorsMutex.RUnlock() + return len(fake.listFlavorsArgsForCall) +} + +func (fake *FakeComputeFacade) ListFlavorsCalls(stub func(utils.RetryableServiceClient, flavors.ListOpts) (pagination.Page, error)) { + fake.listFlavorsMutex.Lock() + defer fake.listFlavorsMutex.Unlock() + fake.ListFlavorsStub = stub +} + +func (fake *FakeComputeFacade) ListFlavorsArgsForCall(i int) (utils.RetryableServiceClient, flavors.ListOpts) { + fake.listFlavorsMutex.RLock() + defer fake.listFlavorsMutex.RUnlock() + argsForCall := fake.listFlavorsArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeComputeFacade) ListFlavorsReturns(result1 pagination.Page, result2 error) { + fake.listFlavorsMutex.Lock() + defer fake.listFlavorsMutex.Unlock() + fake.ListFlavorsStub = nil + fake.listFlavorsReturns = struct { + result1 pagination.Page + result2 error + }{result1, result2} +} + +func (fake *FakeComputeFacade) ListFlavorsReturnsOnCall(i int, result1 pagination.Page, result2 error) { + fake.listFlavorsMutex.Lock() + defer fake.listFlavorsMutex.Unlock() + fake.ListFlavorsStub = nil + if fake.listFlavorsReturnsOnCall == nil { + fake.listFlavorsReturnsOnCall = make(map[int]struct { + result1 pagination.Page + result2 error + }) + } + fake.listFlavorsReturnsOnCall[i] = struct { + result1 pagination.Page + result2 error + }{result1, result2} +} + +func (fake *FakeComputeFacade) ListVolumeAttachments(arg1 *gophercloud.ServiceClient, arg2 string) ([]volumeattach.VolumeAttachment, error) { + fake.listVolumeAttachmentsMutex.Lock() + ret, specificReturn := fake.listVolumeAttachmentsReturnsOnCall[len(fake.listVolumeAttachmentsArgsForCall)] + fake.listVolumeAttachmentsArgsForCall = append(fake.listVolumeAttachmentsArgsForCall, struct { + arg1 *gophercloud.ServiceClient + arg2 string + }{arg1, arg2}) + stub := fake.ListVolumeAttachmentsStub + fakeReturns := fake.listVolumeAttachmentsReturns + fake.recordInvocation("ListVolumeAttachments", []interface{}{arg1, arg2}) + fake.listVolumeAttachmentsMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeComputeFacade) ListVolumeAttachmentsCallCount() int { + fake.listVolumeAttachmentsMutex.RLock() + defer fake.listVolumeAttachmentsMutex.RUnlock() + return len(fake.listVolumeAttachmentsArgsForCall) +} + +func (fake *FakeComputeFacade) ListVolumeAttachmentsCalls(stub func(*gophercloud.ServiceClient, string) ([]volumeattach.VolumeAttachment, error)) { + fake.listVolumeAttachmentsMutex.Lock() + defer fake.listVolumeAttachmentsMutex.Unlock() + fake.ListVolumeAttachmentsStub = stub +} + +func (fake *FakeComputeFacade) ListVolumeAttachmentsArgsForCall(i int) (*gophercloud.ServiceClient, string) { + fake.listVolumeAttachmentsMutex.RLock() + defer fake.listVolumeAttachmentsMutex.RUnlock() + argsForCall := fake.listVolumeAttachmentsArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeComputeFacade) ListVolumeAttachmentsReturns(result1 []volumeattach.VolumeAttachment, result2 error) { + fake.listVolumeAttachmentsMutex.Lock() + defer fake.listVolumeAttachmentsMutex.Unlock() + fake.ListVolumeAttachmentsStub = nil + fake.listVolumeAttachmentsReturns = struct { + result1 []volumeattach.VolumeAttachment + result2 error + }{result1, result2} +} + +func (fake *FakeComputeFacade) ListVolumeAttachmentsReturnsOnCall(i int, result1 []volumeattach.VolumeAttachment, result2 error) { + fake.listVolumeAttachmentsMutex.Lock() + defer fake.listVolumeAttachmentsMutex.Unlock() + fake.ListVolumeAttachmentsStub = nil + if fake.listVolumeAttachmentsReturnsOnCall == nil { + fake.listVolumeAttachmentsReturnsOnCall = make(map[int]struct { + result1 []volumeattach.VolumeAttachment + result2 error + }) + } + fake.listVolumeAttachmentsReturnsOnCall[i] = struct { + result1 []volumeattach.VolumeAttachment + result2 error + }{result1, result2} +} + +func (fake *FakeComputeFacade) RebootServer(arg1 utils.ServiceClient, arg2 string, arg3 servers.RebootOptsBuilder) error { + fake.rebootServerMutex.Lock() + ret, specificReturn := fake.rebootServerReturnsOnCall[len(fake.rebootServerArgsForCall)] + fake.rebootServerArgsForCall = append(fake.rebootServerArgsForCall, struct { + arg1 utils.ServiceClient + arg2 string + arg3 servers.RebootOptsBuilder + }{arg1, arg2, arg3}) + stub := fake.RebootServerStub + fakeReturns := fake.rebootServerReturns + fake.recordInvocation("RebootServer", []interface{}{arg1, arg2, arg3}) + fake.rebootServerMutex.Unlock() + if stub != nil { + return stub(arg1, arg2, arg3) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeComputeFacade) RebootServerCallCount() int { + fake.rebootServerMutex.RLock() + defer fake.rebootServerMutex.RUnlock() + return len(fake.rebootServerArgsForCall) +} + +func (fake *FakeComputeFacade) RebootServerCalls(stub func(utils.ServiceClient, string, servers.RebootOptsBuilder) error) { + fake.rebootServerMutex.Lock() + defer fake.rebootServerMutex.Unlock() + fake.RebootServerStub = stub +} + +func (fake *FakeComputeFacade) RebootServerArgsForCall(i int) (utils.ServiceClient, string, servers.RebootOptsBuilder) { + fake.rebootServerMutex.RLock() + defer fake.rebootServerMutex.RUnlock() + argsForCall := fake.rebootServerArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 +} + +func (fake *FakeComputeFacade) RebootServerReturns(result1 error) { + fake.rebootServerMutex.Lock() + defer fake.rebootServerMutex.Unlock() + fake.RebootServerStub = nil + fake.rebootServerReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeComputeFacade) RebootServerReturnsOnCall(i int, result1 error) { + fake.rebootServerMutex.Lock() + defer fake.rebootServerMutex.Unlock() + fake.RebootServerStub = nil + if fake.rebootServerReturnsOnCall == nil { + fake.rebootServerReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.rebootServerReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *FakeComputeFacade) UpdateServer(arg1 utils.ServiceClient, arg2 string, arg3 servers.UpdateOptsBuilder) (*servers.Server, error) { + fake.updateServerMutex.Lock() + ret, specificReturn := fake.updateServerReturnsOnCall[len(fake.updateServerArgsForCall)] + fake.updateServerArgsForCall = append(fake.updateServerArgsForCall, struct { + arg1 utils.ServiceClient + arg2 string + arg3 servers.UpdateOptsBuilder + }{arg1, arg2, arg3}) + stub := fake.UpdateServerStub + fakeReturns := fake.updateServerReturns + fake.recordInvocation("UpdateServer", []interface{}{arg1, arg2, arg3}) + fake.updateServerMutex.Unlock() + if stub != nil { + return stub(arg1, arg2, arg3) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeComputeFacade) UpdateServerCallCount() int { + fake.updateServerMutex.RLock() + defer fake.updateServerMutex.RUnlock() + return len(fake.updateServerArgsForCall) +} + +func (fake *FakeComputeFacade) UpdateServerCalls(stub func(utils.ServiceClient, string, servers.UpdateOptsBuilder) (*servers.Server, error)) { + fake.updateServerMutex.Lock() + defer fake.updateServerMutex.Unlock() + fake.UpdateServerStub = stub +} + +func (fake *FakeComputeFacade) UpdateServerArgsForCall(i int) (utils.ServiceClient, string, servers.UpdateOptsBuilder) { + fake.updateServerMutex.RLock() + defer fake.updateServerMutex.RUnlock() + argsForCall := fake.updateServerArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 +} + +func (fake *FakeComputeFacade) UpdateServerReturns(result1 *servers.Server, result2 error) { + fake.updateServerMutex.Lock() + defer fake.updateServerMutex.Unlock() + fake.UpdateServerStub = nil + fake.updateServerReturns = struct { + result1 *servers.Server + result2 error + }{result1, result2} +} + +func (fake *FakeComputeFacade) UpdateServerReturnsOnCall(i int, result1 *servers.Server, result2 error) { + fake.updateServerMutex.Lock() + defer fake.updateServerMutex.Unlock() + fake.UpdateServerStub = nil + if fake.updateServerReturnsOnCall == nil { + fake.updateServerReturnsOnCall = make(map[int]struct { + result1 *servers.Server + result2 error + }) + } + fake.updateServerReturnsOnCall[i] = struct { + result1 *servers.Server + result2 error + }{result1, result2} +} + +func (fake *FakeComputeFacade) UpdateServerMetadata(arg1 utils.ServiceClient, arg2 string, arg3 servers.UpdateMetadataOptsBuilder) (map[string]string, error) { + fake.updateServerMetadataMutex.Lock() + ret, specificReturn := fake.updateServerMetadataReturnsOnCall[len(fake.updateServerMetadataArgsForCall)] + fake.updateServerMetadataArgsForCall = append(fake.updateServerMetadataArgsForCall, struct { + arg1 utils.ServiceClient + arg2 string + arg3 servers.UpdateMetadataOptsBuilder + }{arg1, arg2, arg3}) + stub := fake.UpdateServerMetadataStub + fakeReturns := fake.updateServerMetadataReturns + fake.recordInvocation("UpdateServerMetadata", []interface{}{arg1, arg2, arg3}) + fake.updateServerMetadataMutex.Unlock() + if stub != nil { + return stub(arg1, arg2, arg3) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeComputeFacade) UpdateServerMetadataCallCount() int { + fake.updateServerMetadataMutex.RLock() + defer fake.updateServerMetadataMutex.RUnlock() + return len(fake.updateServerMetadataArgsForCall) +} + +func (fake *FakeComputeFacade) UpdateServerMetadataCalls(stub func(utils.ServiceClient, string, servers.UpdateMetadataOptsBuilder) (map[string]string, error)) { + fake.updateServerMetadataMutex.Lock() + defer fake.updateServerMetadataMutex.Unlock() + fake.UpdateServerMetadataStub = stub +} + +func (fake *FakeComputeFacade) UpdateServerMetadataArgsForCall(i int) (utils.ServiceClient, string, servers.UpdateMetadataOptsBuilder) { + fake.updateServerMetadataMutex.RLock() + defer fake.updateServerMetadataMutex.RUnlock() + argsForCall := fake.updateServerMetadataArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 +} + +func (fake *FakeComputeFacade) UpdateServerMetadataReturns(result1 map[string]string, result2 error) { + fake.updateServerMetadataMutex.Lock() + defer fake.updateServerMetadataMutex.Unlock() + fake.UpdateServerMetadataStub = nil + fake.updateServerMetadataReturns = struct { + result1 map[string]string + result2 error + }{result1, result2} +} + +func (fake *FakeComputeFacade) UpdateServerMetadataReturnsOnCall(i int, result1 map[string]string, result2 error) { + fake.updateServerMetadataMutex.Lock() + defer fake.updateServerMetadataMutex.Unlock() + fake.UpdateServerMetadataStub = nil + if fake.updateServerMetadataReturnsOnCall == nil { + fake.updateServerMetadataReturnsOnCall = make(map[int]struct { + result1 map[string]string + result2 error + }) + } + fake.updateServerMetadataReturnsOnCall[i] = struct { + result1 map[string]string + result2 error + }{result1, result2} +} + +func (fake *FakeComputeFacade) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.attachVolumeMutex.RLock() + defer fake.attachVolumeMutex.RUnlock() + fake.createServerMutex.RLock() + defer fake.createServerMutex.RUnlock() + fake.deleteServerMutex.RLock() + defer fake.deleteServerMutex.RUnlock() + fake.deleteServerMetaDataMutex.RLock() + defer fake.deleteServerMetaDataMutex.RUnlock() + fake.detachVolumeMutex.RLock() + defer fake.detachVolumeMutex.RUnlock() + fake.extractFlavorsMutex.RLock() + defer fake.extractFlavorsMutex.RUnlock() + fake.getOSKeyPairMutex.RLock() + defer fake.getOSKeyPairMutex.RUnlock() + fake.getServerMutex.RLock() + defer fake.getServerMutex.RUnlock() + fake.getServerMetadataMutex.RLock() + defer fake.getServerMetadataMutex.RUnlock() + fake.getServerWithAZMutex.RLock() + defer fake.getServerWithAZMutex.RUnlock() + fake.listFlavorsMutex.RLock() + defer fake.listFlavorsMutex.RUnlock() + fake.listVolumeAttachmentsMutex.RLock() + defer fake.listVolumeAttachmentsMutex.RUnlock() + fake.rebootServerMutex.RLock() + defer fake.rebootServerMutex.RUnlock() + fake.updateServerMutex.RLock() + defer fake.updateServerMutex.RUnlock() + fake.updateServerMetadataMutex.RLock() + defer fake.updateServerMetadataMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *FakeComputeFacade) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ compute.ComputeFacade = new(FakeComputeFacade) diff --git a/src/openstack_cpi_golang/cpi/compute/computefakes/fake_compute_service.go b/src/openstack_cpi_golang/cpi/compute/computefakes/fake_compute_service.go new file mode 100644 index 00000000..7c57def9 --- /dev/null +++ b/src/openstack_cpi_golang/cpi/compute/computefakes/fake_compute_service.go @@ -0,0 +1,1154 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package computefakes + +import ( + "sync" + + "github.com/cloudfoundry/bosh-cpi-go/apiv1" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/compute" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/config" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/properties" + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/volumeattach" + "github.com/gophercloud/gophercloud/openstack/compute/v2/flavors" + "github.com/gophercloud/gophercloud/openstack/compute/v2/servers" +) + +type FakeComputeService struct { + AttachVolumeStub func(string, string, string) (*volumeattach.VolumeAttachment, error) + attachVolumeMutex sync.RWMutex + attachVolumeArgsForCall []struct { + arg1 string + arg2 string + arg3 string + } + attachVolumeReturns struct { + result1 *volumeattach.VolumeAttachment + result2 error + } + attachVolumeReturnsOnCall map[int]struct { + result1 *volumeattach.VolumeAttachment + result2 error + } + CreateServerStub func(apiv1.StemcellCID, properties.CreateVM, properties.NetworkConfig, apiv1.AgentID, apiv1.VMEnv, config.CpiConfig) (*servers.Server, error) + createServerMutex sync.RWMutex + createServerArgsForCall []struct { + arg1 apiv1.StemcellCID + arg2 properties.CreateVM + arg3 properties.NetworkConfig + arg4 apiv1.AgentID + arg5 apiv1.VMEnv + arg6 config.CpiConfig + } + createServerReturns struct { + result1 *servers.Server + result2 error + } + createServerReturnsOnCall map[int]struct { + result1 *servers.Server + result2 error + } + DeleteServerStub func(string, config.CpiConfig) error + deleteServerMutex sync.RWMutex + deleteServerArgsForCall []struct { + arg1 string + arg2 config.CpiConfig + } + deleteServerReturns struct { + result1 error + } + deleteServerReturnsOnCall map[int]struct { + result1 error + } + DeleteServerMetaDataStub func(string, map[string]string, properties.ServerMetadata) error + deleteServerMetaDataMutex sync.RWMutex + deleteServerMetaDataArgsForCall []struct { + arg1 string + arg2 map[string]string + arg3 properties.ServerMetadata + } + deleteServerMetaDataReturns struct { + result1 error + } + deleteServerMetaDataReturnsOnCall map[int]struct { + result1 error + } + DetachVolumeStub func(string, string) error + detachVolumeMutex sync.RWMutex + detachVolumeArgsForCall []struct { + arg1 string + arg2 string + } + detachVolumeReturns struct { + result1 error + } + detachVolumeReturnsOnCall map[int]struct { + result1 error + } + GetFlavorByIdStub func(string) (flavors.Flavor, error) + getFlavorByIdMutex sync.RWMutex + getFlavorByIdArgsForCall []struct { + arg1 string + } + getFlavorByIdReturns struct { + result1 flavors.Flavor + result2 error + } + getFlavorByIdReturnsOnCall map[int]struct { + result1 flavors.Flavor + result2 error + } + GetMatchingFlavorStub func(apiv1.VMResources, bool) (flavors.Flavor, error) + getMatchingFlavorMutex sync.RWMutex + getMatchingFlavorArgsForCall []struct { + arg1 apiv1.VMResources + arg2 bool + } + getMatchingFlavorReturns struct { + result1 flavors.Flavor + result2 error + } + getMatchingFlavorReturnsOnCall map[int]struct { + result1 flavors.Flavor + result2 error + } + GetMetadataStub func(string) (map[string]string, error) + getMetadataMutex sync.RWMutex + getMetadataArgsForCall []struct { + arg1 string + } + getMetadataReturns struct { + result1 map[string]string + result2 error + } + getMetadataReturnsOnCall map[int]struct { + result1 map[string]string + result2 error + } + GetServerStub func(string) (*servers.Server, error) + getServerMutex sync.RWMutex + getServerArgsForCall []struct { + arg1 string + } + getServerReturns struct { + result1 *servers.Server + result2 error + } + getServerReturnsOnCall map[int]struct { + result1 *servers.Server + result2 error + } + GetServerAZStub func(string) (string, error) + getServerAZMutex sync.RWMutex + getServerAZArgsForCall []struct { + arg1 string + } + getServerAZReturns struct { + result1 string + result2 error + } + getServerAZReturnsOnCall map[int]struct { + result1 string + result2 error + } + ListVolumeAttachmentsStub func(string) ([]volumeattach.VolumeAttachment, error) + listVolumeAttachmentsMutex sync.RWMutex + listVolumeAttachmentsArgsForCall []struct { + arg1 string + } + listVolumeAttachmentsReturns struct { + result1 []volumeattach.VolumeAttachment + result2 error + } + listVolumeAttachmentsReturnsOnCall map[int]struct { + result1 []volumeattach.VolumeAttachment + result2 error + } + RebootServerStub func(string, config.CpiConfig) error + rebootServerMutex sync.RWMutex + rebootServerArgsForCall []struct { + arg1 string + arg2 config.CpiConfig + } + rebootServerReturns struct { + result1 error + } + rebootServerReturnsOnCall map[int]struct { + result1 error + } + UpdateServerStub func(string, string) (*servers.Server, error) + updateServerMutex sync.RWMutex + updateServerArgsForCall []struct { + arg1 string + arg2 string + } + updateServerReturns struct { + result1 *servers.Server + result2 error + } + updateServerReturnsOnCall map[int]struct { + result1 *servers.Server + result2 error + } + UpdateServerMetadataStub func(string, properties.ServerMetadata) error + updateServerMetadataMutex sync.RWMutex + updateServerMetadataArgsForCall []struct { + arg1 string + arg2 properties.ServerMetadata + } + updateServerMetadataReturns struct { + result1 error + } + updateServerMetadataReturnsOnCall map[int]struct { + result1 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *FakeComputeService) AttachVolume(arg1 string, arg2 string, arg3 string) (*volumeattach.VolumeAttachment, error) { + fake.attachVolumeMutex.Lock() + ret, specificReturn := fake.attachVolumeReturnsOnCall[len(fake.attachVolumeArgsForCall)] + fake.attachVolumeArgsForCall = append(fake.attachVolumeArgsForCall, struct { + arg1 string + arg2 string + arg3 string + }{arg1, arg2, arg3}) + stub := fake.AttachVolumeStub + fakeReturns := fake.attachVolumeReturns + fake.recordInvocation("AttachVolume", []interface{}{arg1, arg2, arg3}) + fake.attachVolumeMutex.Unlock() + if stub != nil { + return stub(arg1, arg2, arg3) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeComputeService) AttachVolumeCallCount() int { + fake.attachVolumeMutex.RLock() + defer fake.attachVolumeMutex.RUnlock() + return len(fake.attachVolumeArgsForCall) +} + +func (fake *FakeComputeService) AttachVolumeCalls(stub func(string, string, string) (*volumeattach.VolumeAttachment, error)) { + fake.attachVolumeMutex.Lock() + defer fake.attachVolumeMutex.Unlock() + fake.AttachVolumeStub = stub +} + +func (fake *FakeComputeService) AttachVolumeArgsForCall(i int) (string, string, string) { + fake.attachVolumeMutex.RLock() + defer fake.attachVolumeMutex.RUnlock() + argsForCall := fake.attachVolumeArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 +} + +func (fake *FakeComputeService) AttachVolumeReturns(result1 *volumeattach.VolumeAttachment, result2 error) { + fake.attachVolumeMutex.Lock() + defer fake.attachVolumeMutex.Unlock() + fake.AttachVolumeStub = nil + fake.attachVolumeReturns = struct { + result1 *volumeattach.VolumeAttachment + result2 error + }{result1, result2} +} + +func (fake *FakeComputeService) AttachVolumeReturnsOnCall(i int, result1 *volumeattach.VolumeAttachment, result2 error) { + fake.attachVolumeMutex.Lock() + defer fake.attachVolumeMutex.Unlock() + fake.AttachVolumeStub = nil + if fake.attachVolumeReturnsOnCall == nil { + fake.attachVolumeReturnsOnCall = make(map[int]struct { + result1 *volumeattach.VolumeAttachment + result2 error + }) + } + fake.attachVolumeReturnsOnCall[i] = struct { + result1 *volumeattach.VolumeAttachment + result2 error + }{result1, result2} +} + +func (fake *FakeComputeService) CreateServer(arg1 apiv1.StemcellCID, arg2 properties.CreateVM, arg3 properties.NetworkConfig, arg4 apiv1.AgentID, arg5 apiv1.VMEnv, arg6 config.CpiConfig) (*servers.Server, error) { + fake.createServerMutex.Lock() + ret, specificReturn := fake.createServerReturnsOnCall[len(fake.createServerArgsForCall)] + fake.createServerArgsForCall = append(fake.createServerArgsForCall, struct { + arg1 apiv1.StemcellCID + arg2 properties.CreateVM + arg3 properties.NetworkConfig + arg4 apiv1.AgentID + arg5 apiv1.VMEnv + arg6 config.CpiConfig + }{arg1, arg2, arg3, arg4, arg5, arg6}) + stub := fake.CreateServerStub + fakeReturns := fake.createServerReturns + fake.recordInvocation("CreateServer", []interface{}{arg1, arg2, arg3, arg4, arg5, arg6}) + fake.createServerMutex.Unlock() + if stub != nil { + return stub(arg1, arg2, arg3, arg4, arg5, arg6) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeComputeService) CreateServerCallCount() int { + fake.createServerMutex.RLock() + defer fake.createServerMutex.RUnlock() + return len(fake.createServerArgsForCall) +} + +func (fake *FakeComputeService) CreateServerCalls(stub func(apiv1.StemcellCID, properties.CreateVM, properties.NetworkConfig, apiv1.AgentID, apiv1.VMEnv, config.CpiConfig) (*servers.Server, error)) { + fake.createServerMutex.Lock() + defer fake.createServerMutex.Unlock() + fake.CreateServerStub = stub +} + +func (fake *FakeComputeService) CreateServerArgsForCall(i int) (apiv1.StemcellCID, properties.CreateVM, properties.NetworkConfig, apiv1.AgentID, apiv1.VMEnv, config.CpiConfig) { + fake.createServerMutex.RLock() + defer fake.createServerMutex.RUnlock() + argsForCall := fake.createServerArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4, argsForCall.arg5, argsForCall.arg6 +} + +func (fake *FakeComputeService) CreateServerReturns(result1 *servers.Server, result2 error) { + fake.createServerMutex.Lock() + defer fake.createServerMutex.Unlock() + fake.CreateServerStub = nil + fake.createServerReturns = struct { + result1 *servers.Server + result2 error + }{result1, result2} +} + +func (fake *FakeComputeService) CreateServerReturnsOnCall(i int, result1 *servers.Server, result2 error) { + fake.createServerMutex.Lock() + defer fake.createServerMutex.Unlock() + fake.CreateServerStub = nil + if fake.createServerReturnsOnCall == nil { + fake.createServerReturnsOnCall = make(map[int]struct { + result1 *servers.Server + result2 error + }) + } + fake.createServerReturnsOnCall[i] = struct { + result1 *servers.Server + result2 error + }{result1, result2} +} + +func (fake *FakeComputeService) DeleteServer(arg1 string, arg2 config.CpiConfig) error { + fake.deleteServerMutex.Lock() + ret, specificReturn := fake.deleteServerReturnsOnCall[len(fake.deleteServerArgsForCall)] + fake.deleteServerArgsForCall = append(fake.deleteServerArgsForCall, struct { + arg1 string + arg2 config.CpiConfig + }{arg1, arg2}) + stub := fake.DeleteServerStub + fakeReturns := fake.deleteServerReturns + fake.recordInvocation("DeleteServer", []interface{}{arg1, arg2}) + fake.deleteServerMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeComputeService) DeleteServerCallCount() int { + fake.deleteServerMutex.RLock() + defer fake.deleteServerMutex.RUnlock() + return len(fake.deleteServerArgsForCall) +} + +func (fake *FakeComputeService) DeleteServerCalls(stub func(string, config.CpiConfig) error) { + fake.deleteServerMutex.Lock() + defer fake.deleteServerMutex.Unlock() + fake.DeleteServerStub = stub +} + +func (fake *FakeComputeService) DeleteServerArgsForCall(i int) (string, config.CpiConfig) { + fake.deleteServerMutex.RLock() + defer fake.deleteServerMutex.RUnlock() + argsForCall := fake.deleteServerArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeComputeService) DeleteServerReturns(result1 error) { + fake.deleteServerMutex.Lock() + defer fake.deleteServerMutex.Unlock() + fake.DeleteServerStub = nil + fake.deleteServerReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeComputeService) DeleteServerReturnsOnCall(i int, result1 error) { + fake.deleteServerMutex.Lock() + defer fake.deleteServerMutex.Unlock() + fake.DeleteServerStub = nil + if fake.deleteServerReturnsOnCall == nil { + fake.deleteServerReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.deleteServerReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *FakeComputeService) DeleteServerMetaData(arg1 string, arg2 map[string]string, arg3 properties.ServerMetadata) error { + fake.deleteServerMetaDataMutex.Lock() + ret, specificReturn := fake.deleteServerMetaDataReturnsOnCall[len(fake.deleteServerMetaDataArgsForCall)] + fake.deleteServerMetaDataArgsForCall = append(fake.deleteServerMetaDataArgsForCall, struct { + arg1 string + arg2 map[string]string + arg3 properties.ServerMetadata + }{arg1, arg2, arg3}) + stub := fake.DeleteServerMetaDataStub + fakeReturns := fake.deleteServerMetaDataReturns + fake.recordInvocation("DeleteServerMetaData", []interface{}{arg1, arg2, arg3}) + fake.deleteServerMetaDataMutex.Unlock() + if stub != nil { + return stub(arg1, arg2, arg3) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeComputeService) DeleteServerMetaDataCallCount() int { + fake.deleteServerMetaDataMutex.RLock() + defer fake.deleteServerMetaDataMutex.RUnlock() + return len(fake.deleteServerMetaDataArgsForCall) +} + +func (fake *FakeComputeService) DeleteServerMetaDataCalls(stub func(string, map[string]string, properties.ServerMetadata) error) { + fake.deleteServerMetaDataMutex.Lock() + defer fake.deleteServerMetaDataMutex.Unlock() + fake.DeleteServerMetaDataStub = stub +} + +func (fake *FakeComputeService) DeleteServerMetaDataArgsForCall(i int) (string, map[string]string, properties.ServerMetadata) { + fake.deleteServerMetaDataMutex.RLock() + defer fake.deleteServerMetaDataMutex.RUnlock() + argsForCall := fake.deleteServerMetaDataArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 +} + +func (fake *FakeComputeService) DeleteServerMetaDataReturns(result1 error) { + fake.deleteServerMetaDataMutex.Lock() + defer fake.deleteServerMetaDataMutex.Unlock() + fake.DeleteServerMetaDataStub = nil + fake.deleteServerMetaDataReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeComputeService) DeleteServerMetaDataReturnsOnCall(i int, result1 error) { + fake.deleteServerMetaDataMutex.Lock() + defer fake.deleteServerMetaDataMutex.Unlock() + fake.DeleteServerMetaDataStub = nil + if fake.deleteServerMetaDataReturnsOnCall == nil { + fake.deleteServerMetaDataReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.deleteServerMetaDataReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *FakeComputeService) DetachVolume(arg1 string, arg2 string) error { + fake.detachVolumeMutex.Lock() + ret, specificReturn := fake.detachVolumeReturnsOnCall[len(fake.detachVolumeArgsForCall)] + fake.detachVolumeArgsForCall = append(fake.detachVolumeArgsForCall, struct { + arg1 string + arg2 string + }{arg1, arg2}) + stub := fake.DetachVolumeStub + fakeReturns := fake.detachVolumeReturns + fake.recordInvocation("DetachVolume", []interface{}{arg1, arg2}) + fake.detachVolumeMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeComputeService) DetachVolumeCallCount() int { + fake.detachVolumeMutex.RLock() + defer fake.detachVolumeMutex.RUnlock() + return len(fake.detachVolumeArgsForCall) +} + +func (fake *FakeComputeService) DetachVolumeCalls(stub func(string, string) error) { + fake.detachVolumeMutex.Lock() + defer fake.detachVolumeMutex.Unlock() + fake.DetachVolumeStub = stub +} + +func (fake *FakeComputeService) DetachVolumeArgsForCall(i int) (string, string) { + fake.detachVolumeMutex.RLock() + defer fake.detachVolumeMutex.RUnlock() + argsForCall := fake.detachVolumeArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeComputeService) DetachVolumeReturns(result1 error) { + fake.detachVolumeMutex.Lock() + defer fake.detachVolumeMutex.Unlock() + fake.DetachVolumeStub = nil + fake.detachVolumeReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeComputeService) DetachVolumeReturnsOnCall(i int, result1 error) { + fake.detachVolumeMutex.Lock() + defer fake.detachVolumeMutex.Unlock() + fake.DetachVolumeStub = nil + if fake.detachVolumeReturnsOnCall == nil { + fake.detachVolumeReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.detachVolumeReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *FakeComputeService) GetFlavorById(arg1 string) (flavors.Flavor, error) { + fake.getFlavorByIdMutex.Lock() + ret, specificReturn := fake.getFlavorByIdReturnsOnCall[len(fake.getFlavorByIdArgsForCall)] + fake.getFlavorByIdArgsForCall = append(fake.getFlavorByIdArgsForCall, struct { + arg1 string + }{arg1}) + stub := fake.GetFlavorByIdStub + fakeReturns := fake.getFlavorByIdReturns + fake.recordInvocation("GetFlavorById", []interface{}{arg1}) + fake.getFlavorByIdMutex.Unlock() + if stub != nil { + return stub(arg1) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeComputeService) GetFlavorByIdCallCount() int { + fake.getFlavorByIdMutex.RLock() + defer fake.getFlavorByIdMutex.RUnlock() + return len(fake.getFlavorByIdArgsForCall) +} + +func (fake *FakeComputeService) GetFlavorByIdCalls(stub func(string) (flavors.Flavor, error)) { + fake.getFlavorByIdMutex.Lock() + defer fake.getFlavorByIdMutex.Unlock() + fake.GetFlavorByIdStub = stub +} + +func (fake *FakeComputeService) GetFlavorByIdArgsForCall(i int) string { + fake.getFlavorByIdMutex.RLock() + defer fake.getFlavorByIdMutex.RUnlock() + argsForCall := fake.getFlavorByIdArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeComputeService) GetFlavorByIdReturns(result1 flavors.Flavor, result2 error) { + fake.getFlavorByIdMutex.Lock() + defer fake.getFlavorByIdMutex.Unlock() + fake.GetFlavorByIdStub = nil + fake.getFlavorByIdReturns = struct { + result1 flavors.Flavor + result2 error + }{result1, result2} +} + +func (fake *FakeComputeService) GetFlavorByIdReturnsOnCall(i int, result1 flavors.Flavor, result2 error) { + fake.getFlavorByIdMutex.Lock() + defer fake.getFlavorByIdMutex.Unlock() + fake.GetFlavorByIdStub = nil + if fake.getFlavorByIdReturnsOnCall == nil { + fake.getFlavorByIdReturnsOnCall = make(map[int]struct { + result1 flavors.Flavor + result2 error + }) + } + fake.getFlavorByIdReturnsOnCall[i] = struct { + result1 flavors.Flavor + result2 error + }{result1, result2} +} + +func (fake *FakeComputeService) GetMatchingFlavor(arg1 apiv1.VMResources, arg2 bool) (flavors.Flavor, error) { + fake.getMatchingFlavorMutex.Lock() + ret, specificReturn := fake.getMatchingFlavorReturnsOnCall[len(fake.getMatchingFlavorArgsForCall)] + fake.getMatchingFlavorArgsForCall = append(fake.getMatchingFlavorArgsForCall, struct { + arg1 apiv1.VMResources + arg2 bool + }{arg1, arg2}) + stub := fake.GetMatchingFlavorStub + fakeReturns := fake.getMatchingFlavorReturns + fake.recordInvocation("GetMatchingFlavor", []interface{}{arg1, arg2}) + fake.getMatchingFlavorMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeComputeService) GetMatchingFlavorCallCount() int { + fake.getMatchingFlavorMutex.RLock() + defer fake.getMatchingFlavorMutex.RUnlock() + return len(fake.getMatchingFlavorArgsForCall) +} + +func (fake *FakeComputeService) GetMatchingFlavorCalls(stub func(apiv1.VMResources, bool) (flavors.Flavor, error)) { + fake.getMatchingFlavorMutex.Lock() + defer fake.getMatchingFlavorMutex.Unlock() + fake.GetMatchingFlavorStub = stub +} + +func (fake *FakeComputeService) GetMatchingFlavorArgsForCall(i int) (apiv1.VMResources, bool) { + fake.getMatchingFlavorMutex.RLock() + defer fake.getMatchingFlavorMutex.RUnlock() + argsForCall := fake.getMatchingFlavorArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeComputeService) GetMatchingFlavorReturns(result1 flavors.Flavor, result2 error) { + fake.getMatchingFlavorMutex.Lock() + defer fake.getMatchingFlavorMutex.Unlock() + fake.GetMatchingFlavorStub = nil + fake.getMatchingFlavorReturns = struct { + result1 flavors.Flavor + result2 error + }{result1, result2} +} + +func (fake *FakeComputeService) GetMatchingFlavorReturnsOnCall(i int, result1 flavors.Flavor, result2 error) { + fake.getMatchingFlavorMutex.Lock() + defer fake.getMatchingFlavorMutex.Unlock() + fake.GetMatchingFlavorStub = nil + if fake.getMatchingFlavorReturnsOnCall == nil { + fake.getMatchingFlavorReturnsOnCall = make(map[int]struct { + result1 flavors.Flavor + result2 error + }) + } + fake.getMatchingFlavorReturnsOnCall[i] = struct { + result1 flavors.Flavor + result2 error + }{result1, result2} +} + +func (fake *FakeComputeService) GetMetadata(arg1 string) (map[string]string, error) { + fake.getMetadataMutex.Lock() + ret, specificReturn := fake.getMetadataReturnsOnCall[len(fake.getMetadataArgsForCall)] + fake.getMetadataArgsForCall = append(fake.getMetadataArgsForCall, struct { + arg1 string + }{arg1}) + stub := fake.GetMetadataStub + fakeReturns := fake.getMetadataReturns + fake.recordInvocation("GetMetadata", []interface{}{arg1}) + fake.getMetadataMutex.Unlock() + if stub != nil { + return stub(arg1) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeComputeService) GetMetadataCallCount() int { + fake.getMetadataMutex.RLock() + defer fake.getMetadataMutex.RUnlock() + return len(fake.getMetadataArgsForCall) +} + +func (fake *FakeComputeService) GetMetadataCalls(stub func(string) (map[string]string, error)) { + fake.getMetadataMutex.Lock() + defer fake.getMetadataMutex.Unlock() + fake.GetMetadataStub = stub +} + +func (fake *FakeComputeService) GetMetadataArgsForCall(i int) string { + fake.getMetadataMutex.RLock() + defer fake.getMetadataMutex.RUnlock() + argsForCall := fake.getMetadataArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeComputeService) GetMetadataReturns(result1 map[string]string, result2 error) { + fake.getMetadataMutex.Lock() + defer fake.getMetadataMutex.Unlock() + fake.GetMetadataStub = nil + fake.getMetadataReturns = struct { + result1 map[string]string + result2 error + }{result1, result2} +} + +func (fake *FakeComputeService) GetMetadataReturnsOnCall(i int, result1 map[string]string, result2 error) { + fake.getMetadataMutex.Lock() + defer fake.getMetadataMutex.Unlock() + fake.GetMetadataStub = nil + if fake.getMetadataReturnsOnCall == nil { + fake.getMetadataReturnsOnCall = make(map[int]struct { + result1 map[string]string + result2 error + }) + } + fake.getMetadataReturnsOnCall[i] = struct { + result1 map[string]string + result2 error + }{result1, result2} +} + +func (fake *FakeComputeService) GetServer(arg1 string) (*servers.Server, error) { + fake.getServerMutex.Lock() + ret, specificReturn := fake.getServerReturnsOnCall[len(fake.getServerArgsForCall)] + fake.getServerArgsForCall = append(fake.getServerArgsForCall, struct { + arg1 string + }{arg1}) + stub := fake.GetServerStub + fakeReturns := fake.getServerReturns + fake.recordInvocation("GetServer", []interface{}{arg1}) + fake.getServerMutex.Unlock() + if stub != nil { + return stub(arg1) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeComputeService) GetServerCallCount() int { + fake.getServerMutex.RLock() + defer fake.getServerMutex.RUnlock() + return len(fake.getServerArgsForCall) +} + +func (fake *FakeComputeService) GetServerCalls(stub func(string) (*servers.Server, error)) { + fake.getServerMutex.Lock() + defer fake.getServerMutex.Unlock() + fake.GetServerStub = stub +} + +func (fake *FakeComputeService) GetServerArgsForCall(i int) string { + fake.getServerMutex.RLock() + defer fake.getServerMutex.RUnlock() + argsForCall := fake.getServerArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeComputeService) GetServerReturns(result1 *servers.Server, result2 error) { + fake.getServerMutex.Lock() + defer fake.getServerMutex.Unlock() + fake.GetServerStub = nil + fake.getServerReturns = struct { + result1 *servers.Server + result2 error + }{result1, result2} +} + +func (fake *FakeComputeService) GetServerReturnsOnCall(i int, result1 *servers.Server, result2 error) { + fake.getServerMutex.Lock() + defer fake.getServerMutex.Unlock() + fake.GetServerStub = nil + if fake.getServerReturnsOnCall == nil { + fake.getServerReturnsOnCall = make(map[int]struct { + result1 *servers.Server + result2 error + }) + } + fake.getServerReturnsOnCall[i] = struct { + result1 *servers.Server + result2 error + }{result1, result2} +} + +func (fake *FakeComputeService) GetServerAZ(arg1 string) (string, error) { + fake.getServerAZMutex.Lock() + ret, specificReturn := fake.getServerAZReturnsOnCall[len(fake.getServerAZArgsForCall)] + fake.getServerAZArgsForCall = append(fake.getServerAZArgsForCall, struct { + arg1 string + }{arg1}) + stub := fake.GetServerAZStub + fakeReturns := fake.getServerAZReturns + fake.recordInvocation("GetServerAZ", []interface{}{arg1}) + fake.getServerAZMutex.Unlock() + if stub != nil { + return stub(arg1) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeComputeService) GetServerAZCallCount() int { + fake.getServerAZMutex.RLock() + defer fake.getServerAZMutex.RUnlock() + return len(fake.getServerAZArgsForCall) +} + +func (fake *FakeComputeService) GetServerAZCalls(stub func(string) (string, error)) { + fake.getServerAZMutex.Lock() + defer fake.getServerAZMutex.Unlock() + fake.GetServerAZStub = stub +} + +func (fake *FakeComputeService) GetServerAZArgsForCall(i int) string { + fake.getServerAZMutex.RLock() + defer fake.getServerAZMutex.RUnlock() + argsForCall := fake.getServerAZArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeComputeService) GetServerAZReturns(result1 string, result2 error) { + fake.getServerAZMutex.Lock() + defer fake.getServerAZMutex.Unlock() + fake.GetServerAZStub = nil + fake.getServerAZReturns = struct { + result1 string + result2 error + }{result1, result2} +} + +func (fake *FakeComputeService) GetServerAZReturnsOnCall(i int, result1 string, result2 error) { + fake.getServerAZMutex.Lock() + defer fake.getServerAZMutex.Unlock() + fake.GetServerAZStub = nil + if fake.getServerAZReturnsOnCall == nil { + fake.getServerAZReturnsOnCall = make(map[int]struct { + result1 string + result2 error + }) + } + fake.getServerAZReturnsOnCall[i] = struct { + result1 string + result2 error + }{result1, result2} +} + +func (fake *FakeComputeService) ListVolumeAttachments(arg1 string) ([]volumeattach.VolumeAttachment, error) { + fake.listVolumeAttachmentsMutex.Lock() + ret, specificReturn := fake.listVolumeAttachmentsReturnsOnCall[len(fake.listVolumeAttachmentsArgsForCall)] + fake.listVolumeAttachmentsArgsForCall = append(fake.listVolumeAttachmentsArgsForCall, struct { + arg1 string + }{arg1}) + stub := fake.ListVolumeAttachmentsStub + fakeReturns := fake.listVolumeAttachmentsReturns + fake.recordInvocation("ListVolumeAttachments", []interface{}{arg1}) + fake.listVolumeAttachmentsMutex.Unlock() + if stub != nil { + return stub(arg1) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeComputeService) ListVolumeAttachmentsCallCount() int { + fake.listVolumeAttachmentsMutex.RLock() + defer fake.listVolumeAttachmentsMutex.RUnlock() + return len(fake.listVolumeAttachmentsArgsForCall) +} + +func (fake *FakeComputeService) ListVolumeAttachmentsCalls(stub func(string) ([]volumeattach.VolumeAttachment, error)) { + fake.listVolumeAttachmentsMutex.Lock() + defer fake.listVolumeAttachmentsMutex.Unlock() + fake.ListVolumeAttachmentsStub = stub +} + +func (fake *FakeComputeService) ListVolumeAttachmentsArgsForCall(i int) string { + fake.listVolumeAttachmentsMutex.RLock() + defer fake.listVolumeAttachmentsMutex.RUnlock() + argsForCall := fake.listVolumeAttachmentsArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeComputeService) ListVolumeAttachmentsReturns(result1 []volumeattach.VolumeAttachment, result2 error) { + fake.listVolumeAttachmentsMutex.Lock() + defer fake.listVolumeAttachmentsMutex.Unlock() + fake.ListVolumeAttachmentsStub = nil + fake.listVolumeAttachmentsReturns = struct { + result1 []volumeattach.VolumeAttachment + result2 error + }{result1, result2} +} + +func (fake *FakeComputeService) ListVolumeAttachmentsReturnsOnCall(i int, result1 []volumeattach.VolumeAttachment, result2 error) { + fake.listVolumeAttachmentsMutex.Lock() + defer fake.listVolumeAttachmentsMutex.Unlock() + fake.ListVolumeAttachmentsStub = nil + if fake.listVolumeAttachmentsReturnsOnCall == nil { + fake.listVolumeAttachmentsReturnsOnCall = make(map[int]struct { + result1 []volumeattach.VolumeAttachment + result2 error + }) + } + fake.listVolumeAttachmentsReturnsOnCall[i] = struct { + result1 []volumeattach.VolumeAttachment + result2 error + }{result1, result2} +} + +func (fake *FakeComputeService) RebootServer(arg1 string, arg2 config.CpiConfig) error { + fake.rebootServerMutex.Lock() + ret, specificReturn := fake.rebootServerReturnsOnCall[len(fake.rebootServerArgsForCall)] + fake.rebootServerArgsForCall = append(fake.rebootServerArgsForCall, struct { + arg1 string + arg2 config.CpiConfig + }{arg1, arg2}) + stub := fake.RebootServerStub + fakeReturns := fake.rebootServerReturns + fake.recordInvocation("RebootServer", []interface{}{arg1, arg2}) + fake.rebootServerMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeComputeService) RebootServerCallCount() int { + fake.rebootServerMutex.RLock() + defer fake.rebootServerMutex.RUnlock() + return len(fake.rebootServerArgsForCall) +} + +func (fake *FakeComputeService) RebootServerCalls(stub func(string, config.CpiConfig) error) { + fake.rebootServerMutex.Lock() + defer fake.rebootServerMutex.Unlock() + fake.RebootServerStub = stub +} + +func (fake *FakeComputeService) RebootServerArgsForCall(i int) (string, config.CpiConfig) { + fake.rebootServerMutex.RLock() + defer fake.rebootServerMutex.RUnlock() + argsForCall := fake.rebootServerArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeComputeService) RebootServerReturns(result1 error) { + fake.rebootServerMutex.Lock() + defer fake.rebootServerMutex.Unlock() + fake.RebootServerStub = nil + fake.rebootServerReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeComputeService) RebootServerReturnsOnCall(i int, result1 error) { + fake.rebootServerMutex.Lock() + defer fake.rebootServerMutex.Unlock() + fake.RebootServerStub = nil + if fake.rebootServerReturnsOnCall == nil { + fake.rebootServerReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.rebootServerReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *FakeComputeService) UpdateServer(arg1 string, arg2 string) (*servers.Server, error) { + fake.updateServerMutex.Lock() + ret, specificReturn := fake.updateServerReturnsOnCall[len(fake.updateServerArgsForCall)] + fake.updateServerArgsForCall = append(fake.updateServerArgsForCall, struct { + arg1 string + arg2 string + }{arg1, arg2}) + stub := fake.UpdateServerStub + fakeReturns := fake.updateServerReturns + fake.recordInvocation("UpdateServer", []interface{}{arg1, arg2}) + fake.updateServerMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeComputeService) UpdateServerCallCount() int { + fake.updateServerMutex.RLock() + defer fake.updateServerMutex.RUnlock() + return len(fake.updateServerArgsForCall) +} + +func (fake *FakeComputeService) UpdateServerCalls(stub func(string, string) (*servers.Server, error)) { + fake.updateServerMutex.Lock() + defer fake.updateServerMutex.Unlock() + fake.UpdateServerStub = stub +} + +func (fake *FakeComputeService) UpdateServerArgsForCall(i int) (string, string) { + fake.updateServerMutex.RLock() + defer fake.updateServerMutex.RUnlock() + argsForCall := fake.updateServerArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeComputeService) UpdateServerReturns(result1 *servers.Server, result2 error) { + fake.updateServerMutex.Lock() + defer fake.updateServerMutex.Unlock() + fake.UpdateServerStub = nil + fake.updateServerReturns = struct { + result1 *servers.Server + result2 error + }{result1, result2} +} + +func (fake *FakeComputeService) UpdateServerReturnsOnCall(i int, result1 *servers.Server, result2 error) { + fake.updateServerMutex.Lock() + defer fake.updateServerMutex.Unlock() + fake.UpdateServerStub = nil + if fake.updateServerReturnsOnCall == nil { + fake.updateServerReturnsOnCall = make(map[int]struct { + result1 *servers.Server + result2 error + }) + } + fake.updateServerReturnsOnCall[i] = struct { + result1 *servers.Server + result2 error + }{result1, result2} +} + +func (fake *FakeComputeService) UpdateServerMetadata(arg1 string, arg2 properties.ServerMetadata) error { + fake.updateServerMetadataMutex.Lock() + ret, specificReturn := fake.updateServerMetadataReturnsOnCall[len(fake.updateServerMetadataArgsForCall)] + fake.updateServerMetadataArgsForCall = append(fake.updateServerMetadataArgsForCall, struct { + arg1 string + arg2 properties.ServerMetadata + }{arg1, arg2}) + stub := fake.UpdateServerMetadataStub + fakeReturns := fake.updateServerMetadataReturns + fake.recordInvocation("UpdateServerMetadata", []interface{}{arg1, arg2}) + fake.updateServerMetadataMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeComputeService) UpdateServerMetadataCallCount() int { + fake.updateServerMetadataMutex.RLock() + defer fake.updateServerMetadataMutex.RUnlock() + return len(fake.updateServerMetadataArgsForCall) +} + +func (fake *FakeComputeService) UpdateServerMetadataCalls(stub func(string, properties.ServerMetadata) error) { + fake.updateServerMetadataMutex.Lock() + defer fake.updateServerMetadataMutex.Unlock() + fake.UpdateServerMetadataStub = stub +} + +func (fake *FakeComputeService) UpdateServerMetadataArgsForCall(i int) (string, properties.ServerMetadata) { + fake.updateServerMetadataMutex.RLock() + defer fake.updateServerMetadataMutex.RUnlock() + argsForCall := fake.updateServerMetadataArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeComputeService) UpdateServerMetadataReturns(result1 error) { + fake.updateServerMetadataMutex.Lock() + defer fake.updateServerMetadataMutex.Unlock() + fake.UpdateServerMetadataStub = nil + fake.updateServerMetadataReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeComputeService) UpdateServerMetadataReturnsOnCall(i int, result1 error) { + fake.updateServerMetadataMutex.Lock() + defer fake.updateServerMetadataMutex.Unlock() + fake.UpdateServerMetadataStub = nil + if fake.updateServerMetadataReturnsOnCall == nil { + fake.updateServerMetadataReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.updateServerMetadataReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *FakeComputeService) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.attachVolumeMutex.RLock() + defer fake.attachVolumeMutex.RUnlock() + fake.createServerMutex.RLock() + defer fake.createServerMutex.RUnlock() + fake.deleteServerMutex.RLock() + defer fake.deleteServerMutex.RUnlock() + fake.deleteServerMetaDataMutex.RLock() + defer fake.deleteServerMetaDataMutex.RUnlock() + fake.detachVolumeMutex.RLock() + defer fake.detachVolumeMutex.RUnlock() + fake.getFlavorByIdMutex.RLock() + defer fake.getFlavorByIdMutex.RUnlock() + fake.getMatchingFlavorMutex.RLock() + defer fake.getMatchingFlavorMutex.RUnlock() + fake.getMetadataMutex.RLock() + defer fake.getMetadataMutex.RUnlock() + fake.getServerMutex.RLock() + defer fake.getServerMutex.RUnlock() + fake.getServerAZMutex.RLock() + defer fake.getServerAZMutex.RUnlock() + fake.listVolumeAttachmentsMutex.RLock() + defer fake.listVolumeAttachmentsMutex.RUnlock() + fake.rebootServerMutex.RLock() + defer fake.rebootServerMutex.RUnlock() + fake.updateServerMutex.RLock() + defer fake.updateServerMutex.RUnlock() + fake.updateServerMetadataMutex.RLock() + defer fake.updateServerMetadataMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *FakeComputeService) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ compute.ComputeService = new(FakeComputeService) diff --git a/src/openstack_cpi_golang/cpi/compute/computefakes/fake_compute_service_builder.go b/src/openstack_cpi_golang/cpi/compute/computefakes/fake_compute_service_builder.go new file mode 100644 index 00000000..2aaa0dd2 --- /dev/null +++ b/src/openstack_cpi_golang/cpi/compute/computefakes/fake_compute_service_builder.go @@ -0,0 +1,107 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package computefakes + +import ( + "sync" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/compute" +) + +type FakeComputeServiceBuilder struct { + BuildStub func() (compute.ComputeService, error) + buildMutex sync.RWMutex + buildArgsForCall []struct { + } + buildReturns struct { + result1 compute.ComputeService + result2 error + } + buildReturnsOnCall map[int]struct { + result1 compute.ComputeService + result2 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *FakeComputeServiceBuilder) Build() (compute.ComputeService, error) { + fake.buildMutex.Lock() + ret, specificReturn := fake.buildReturnsOnCall[len(fake.buildArgsForCall)] + fake.buildArgsForCall = append(fake.buildArgsForCall, struct { + }{}) + stub := fake.BuildStub + fakeReturns := fake.buildReturns + fake.recordInvocation("Build", []interface{}{}) + fake.buildMutex.Unlock() + if stub != nil { + return stub() + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeComputeServiceBuilder) BuildCallCount() int { + fake.buildMutex.RLock() + defer fake.buildMutex.RUnlock() + return len(fake.buildArgsForCall) +} + +func (fake *FakeComputeServiceBuilder) BuildCalls(stub func() (compute.ComputeService, error)) { + fake.buildMutex.Lock() + defer fake.buildMutex.Unlock() + fake.BuildStub = stub +} + +func (fake *FakeComputeServiceBuilder) BuildReturns(result1 compute.ComputeService, result2 error) { + fake.buildMutex.Lock() + defer fake.buildMutex.Unlock() + fake.BuildStub = nil + fake.buildReturns = struct { + result1 compute.ComputeService + result2 error + }{result1, result2} +} + +func (fake *FakeComputeServiceBuilder) BuildReturnsOnCall(i int, result1 compute.ComputeService, result2 error) { + fake.buildMutex.Lock() + defer fake.buildMutex.Unlock() + fake.BuildStub = nil + if fake.buildReturnsOnCall == nil { + fake.buildReturnsOnCall = make(map[int]struct { + result1 compute.ComputeService + result2 error + }) + } + fake.buildReturnsOnCall[i] = struct { + result1 compute.ComputeService + result2 error + }{result1, result2} +} + +func (fake *FakeComputeServiceBuilder) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.buildMutex.RLock() + defer fake.buildMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *FakeComputeServiceBuilder) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ compute.ComputeServiceBuilder = new(FakeComputeServiceBuilder) diff --git a/src/openstack_cpi_golang/cpi/compute/computefakes/fake_flavor_resolver.go b/src/openstack_cpi_golang/cpi/compute/computefakes/fake_flavor_resolver.go new file mode 100644 index 00000000..8c4f6cf2 --- /dev/null +++ b/src/openstack_cpi_golang/cpi/compute/computefakes/fake_flavor_resolver.go @@ -0,0 +1,357 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package computefakes + +import ( + "sync" + + "github.com/cloudfoundry/bosh-cpi-go/apiv1" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/compute" + "github.com/gophercloud/gophercloud/openstack/compute/v2/flavors" +) + +type FakeFlavorResolver struct { + GetClosestMatchedFlavorStub func([]flavors.Flavor) flavors.Flavor + getClosestMatchedFlavorMutex sync.RWMutex + getClosestMatchedFlavorArgsForCall []struct { + arg1 []flavors.Flavor + } + getClosestMatchedFlavorReturns struct { + result1 flavors.Flavor + } + getClosestMatchedFlavorReturnsOnCall map[int]struct { + result1 flavors.Flavor + } + GetFlavorByIdStub func(string) (flavors.Flavor, error) + getFlavorByIdMutex sync.RWMutex + getFlavorByIdArgsForCall []struct { + arg1 string + } + getFlavorByIdReturns struct { + result1 flavors.Flavor + result2 error + } + getFlavorByIdReturnsOnCall map[int]struct { + result1 flavors.Flavor + result2 error + } + ResolveFlavorForInstanceTypeStub func(string) (flavors.Flavor, error) + resolveFlavorForInstanceTypeMutex sync.RWMutex + resolveFlavorForInstanceTypeArgsForCall []struct { + arg1 string + } + resolveFlavorForInstanceTypeReturns struct { + result1 flavors.Flavor + result2 error + } + resolveFlavorForInstanceTypeReturnsOnCall map[int]struct { + result1 flavors.Flavor + result2 error + } + ResolveFlavorForRequirementsStub func(apiv1.VMResources, bool) ([]flavors.Flavor, error) + resolveFlavorForRequirementsMutex sync.RWMutex + resolveFlavorForRequirementsArgsForCall []struct { + arg1 apiv1.VMResources + arg2 bool + } + resolveFlavorForRequirementsReturns struct { + result1 []flavors.Flavor + result2 error + } + resolveFlavorForRequirementsReturnsOnCall map[int]struct { + result1 []flavors.Flavor + result2 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *FakeFlavorResolver) GetClosestMatchedFlavor(arg1 []flavors.Flavor) flavors.Flavor { + var arg1Copy []flavors.Flavor + if arg1 != nil { + arg1Copy = make([]flavors.Flavor, len(arg1)) + copy(arg1Copy, arg1) + } + fake.getClosestMatchedFlavorMutex.Lock() + ret, specificReturn := fake.getClosestMatchedFlavorReturnsOnCall[len(fake.getClosestMatchedFlavorArgsForCall)] + fake.getClosestMatchedFlavorArgsForCall = append(fake.getClosestMatchedFlavorArgsForCall, struct { + arg1 []flavors.Flavor + }{arg1Copy}) + stub := fake.GetClosestMatchedFlavorStub + fakeReturns := fake.getClosestMatchedFlavorReturns + fake.recordInvocation("GetClosestMatchedFlavor", []interface{}{arg1Copy}) + fake.getClosestMatchedFlavorMutex.Unlock() + if stub != nil { + return stub(arg1) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeFlavorResolver) GetClosestMatchedFlavorCallCount() int { + fake.getClosestMatchedFlavorMutex.RLock() + defer fake.getClosestMatchedFlavorMutex.RUnlock() + return len(fake.getClosestMatchedFlavorArgsForCall) +} + +func (fake *FakeFlavorResolver) GetClosestMatchedFlavorCalls(stub func([]flavors.Flavor) flavors.Flavor) { + fake.getClosestMatchedFlavorMutex.Lock() + defer fake.getClosestMatchedFlavorMutex.Unlock() + fake.GetClosestMatchedFlavorStub = stub +} + +func (fake *FakeFlavorResolver) GetClosestMatchedFlavorArgsForCall(i int) []flavors.Flavor { + fake.getClosestMatchedFlavorMutex.RLock() + defer fake.getClosestMatchedFlavorMutex.RUnlock() + argsForCall := fake.getClosestMatchedFlavorArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeFlavorResolver) GetClosestMatchedFlavorReturns(result1 flavors.Flavor) { + fake.getClosestMatchedFlavorMutex.Lock() + defer fake.getClosestMatchedFlavorMutex.Unlock() + fake.GetClosestMatchedFlavorStub = nil + fake.getClosestMatchedFlavorReturns = struct { + result1 flavors.Flavor + }{result1} +} + +func (fake *FakeFlavorResolver) GetClosestMatchedFlavorReturnsOnCall(i int, result1 flavors.Flavor) { + fake.getClosestMatchedFlavorMutex.Lock() + defer fake.getClosestMatchedFlavorMutex.Unlock() + fake.GetClosestMatchedFlavorStub = nil + if fake.getClosestMatchedFlavorReturnsOnCall == nil { + fake.getClosestMatchedFlavorReturnsOnCall = make(map[int]struct { + result1 flavors.Flavor + }) + } + fake.getClosestMatchedFlavorReturnsOnCall[i] = struct { + result1 flavors.Flavor + }{result1} +} + +func (fake *FakeFlavorResolver) GetFlavorById(arg1 string) (flavors.Flavor, error) { + fake.getFlavorByIdMutex.Lock() + ret, specificReturn := fake.getFlavorByIdReturnsOnCall[len(fake.getFlavorByIdArgsForCall)] + fake.getFlavorByIdArgsForCall = append(fake.getFlavorByIdArgsForCall, struct { + arg1 string + }{arg1}) + stub := fake.GetFlavorByIdStub + fakeReturns := fake.getFlavorByIdReturns + fake.recordInvocation("GetFlavorById", []interface{}{arg1}) + fake.getFlavorByIdMutex.Unlock() + if stub != nil { + return stub(arg1) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeFlavorResolver) GetFlavorByIdCallCount() int { + fake.getFlavorByIdMutex.RLock() + defer fake.getFlavorByIdMutex.RUnlock() + return len(fake.getFlavorByIdArgsForCall) +} + +func (fake *FakeFlavorResolver) GetFlavorByIdCalls(stub func(string) (flavors.Flavor, error)) { + fake.getFlavorByIdMutex.Lock() + defer fake.getFlavorByIdMutex.Unlock() + fake.GetFlavorByIdStub = stub +} + +func (fake *FakeFlavorResolver) GetFlavorByIdArgsForCall(i int) string { + fake.getFlavorByIdMutex.RLock() + defer fake.getFlavorByIdMutex.RUnlock() + argsForCall := fake.getFlavorByIdArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeFlavorResolver) GetFlavorByIdReturns(result1 flavors.Flavor, result2 error) { + fake.getFlavorByIdMutex.Lock() + defer fake.getFlavorByIdMutex.Unlock() + fake.GetFlavorByIdStub = nil + fake.getFlavorByIdReturns = struct { + result1 flavors.Flavor + result2 error + }{result1, result2} +} + +func (fake *FakeFlavorResolver) GetFlavorByIdReturnsOnCall(i int, result1 flavors.Flavor, result2 error) { + fake.getFlavorByIdMutex.Lock() + defer fake.getFlavorByIdMutex.Unlock() + fake.GetFlavorByIdStub = nil + if fake.getFlavorByIdReturnsOnCall == nil { + fake.getFlavorByIdReturnsOnCall = make(map[int]struct { + result1 flavors.Flavor + result2 error + }) + } + fake.getFlavorByIdReturnsOnCall[i] = struct { + result1 flavors.Flavor + result2 error + }{result1, result2} +} + +func (fake *FakeFlavorResolver) ResolveFlavorForInstanceType(arg1 string) (flavors.Flavor, error) { + fake.resolveFlavorForInstanceTypeMutex.Lock() + ret, specificReturn := fake.resolveFlavorForInstanceTypeReturnsOnCall[len(fake.resolveFlavorForInstanceTypeArgsForCall)] + fake.resolveFlavorForInstanceTypeArgsForCall = append(fake.resolveFlavorForInstanceTypeArgsForCall, struct { + arg1 string + }{arg1}) + stub := fake.ResolveFlavorForInstanceTypeStub + fakeReturns := fake.resolveFlavorForInstanceTypeReturns + fake.recordInvocation("ResolveFlavorForInstanceType", []interface{}{arg1}) + fake.resolveFlavorForInstanceTypeMutex.Unlock() + if stub != nil { + return stub(arg1) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeFlavorResolver) ResolveFlavorForInstanceTypeCallCount() int { + fake.resolveFlavorForInstanceTypeMutex.RLock() + defer fake.resolveFlavorForInstanceTypeMutex.RUnlock() + return len(fake.resolveFlavorForInstanceTypeArgsForCall) +} + +func (fake *FakeFlavorResolver) ResolveFlavorForInstanceTypeCalls(stub func(string) (flavors.Flavor, error)) { + fake.resolveFlavorForInstanceTypeMutex.Lock() + defer fake.resolveFlavorForInstanceTypeMutex.Unlock() + fake.ResolveFlavorForInstanceTypeStub = stub +} + +func (fake *FakeFlavorResolver) ResolveFlavorForInstanceTypeArgsForCall(i int) string { + fake.resolveFlavorForInstanceTypeMutex.RLock() + defer fake.resolveFlavorForInstanceTypeMutex.RUnlock() + argsForCall := fake.resolveFlavorForInstanceTypeArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeFlavorResolver) ResolveFlavorForInstanceTypeReturns(result1 flavors.Flavor, result2 error) { + fake.resolveFlavorForInstanceTypeMutex.Lock() + defer fake.resolveFlavorForInstanceTypeMutex.Unlock() + fake.ResolveFlavorForInstanceTypeStub = nil + fake.resolveFlavorForInstanceTypeReturns = struct { + result1 flavors.Flavor + result2 error + }{result1, result2} +} + +func (fake *FakeFlavorResolver) ResolveFlavorForInstanceTypeReturnsOnCall(i int, result1 flavors.Flavor, result2 error) { + fake.resolveFlavorForInstanceTypeMutex.Lock() + defer fake.resolveFlavorForInstanceTypeMutex.Unlock() + fake.ResolveFlavorForInstanceTypeStub = nil + if fake.resolveFlavorForInstanceTypeReturnsOnCall == nil { + fake.resolveFlavorForInstanceTypeReturnsOnCall = make(map[int]struct { + result1 flavors.Flavor + result2 error + }) + } + fake.resolveFlavorForInstanceTypeReturnsOnCall[i] = struct { + result1 flavors.Flavor + result2 error + }{result1, result2} +} + +func (fake *FakeFlavorResolver) ResolveFlavorForRequirements(arg1 apiv1.VMResources, arg2 bool) ([]flavors.Flavor, error) { + fake.resolveFlavorForRequirementsMutex.Lock() + ret, specificReturn := fake.resolveFlavorForRequirementsReturnsOnCall[len(fake.resolveFlavorForRequirementsArgsForCall)] + fake.resolveFlavorForRequirementsArgsForCall = append(fake.resolveFlavorForRequirementsArgsForCall, struct { + arg1 apiv1.VMResources + arg2 bool + }{arg1, arg2}) + stub := fake.ResolveFlavorForRequirementsStub + fakeReturns := fake.resolveFlavorForRequirementsReturns + fake.recordInvocation("ResolveFlavorForRequirements", []interface{}{arg1, arg2}) + fake.resolveFlavorForRequirementsMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeFlavorResolver) ResolveFlavorForRequirementsCallCount() int { + fake.resolveFlavorForRequirementsMutex.RLock() + defer fake.resolveFlavorForRequirementsMutex.RUnlock() + return len(fake.resolveFlavorForRequirementsArgsForCall) +} + +func (fake *FakeFlavorResolver) ResolveFlavorForRequirementsCalls(stub func(apiv1.VMResources, bool) ([]flavors.Flavor, error)) { + fake.resolveFlavorForRequirementsMutex.Lock() + defer fake.resolveFlavorForRequirementsMutex.Unlock() + fake.ResolveFlavorForRequirementsStub = stub +} + +func (fake *FakeFlavorResolver) ResolveFlavorForRequirementsArgsForCall(i int) (apiv1.VMResources, bool) { + fake.resolveFlavorForRequirementsMutex.RLock() + defer fake.resolveFlavorForRequirementsMutex.RUnlock() + argsForCall := fake.resolveFlavorForRequirementsArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeFlavorResolver) ResolveFlavorForRequirementsReturns(result1 []flavors.Flavor, result2 error) { + fake.resolveFlavorForRequirementsMutex.Lock() + defer fake.resolveFlavorForRequirementsMutex.Unlock() + fake.ResolveFlavorForRequirementsStub = nil + fake.resolveFlavorForRequirementsReturns = struct { + result1 []flavors.Flavor + result2 error + }{result1, result2} +} + +func (fake *FakeFlavorResolver) ResolveFlavorForRequirementsReturnsOnCall(i int, result1 []flavors.Flavor, result2 error) { + fake.resolveFlavorForRequirementsMutex.Lock() + defer fake.resolveFlavorForRequirementsMutex.Unlock() + fake.ResolveFlavorForRequirementsStub = nil + if fake.resolveFlavorForRequirementsReturnsOnCall == nil { + fake.resolveFlavorForRequirementsReturnsOnCall = make(map[int]struct { + result1 []flavors.Flavor + result2 error + }) + } + fake.resolveFlavorForRequirementsReturnsOnCall[i] = struct { + result1 []flavors.Flavor + result2 error + }{result1, result2} +} + +func (fake *FakeFlavorResolver) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.getClosestMatchedFlavorMutex.RLock() + defer fake.getClosestMatchedFlavorMutex.RUnlock() + fake.getFlavorByIdMutex.RLock() + defer fake.getFlavorByIdMutex.RUnlock() + fake.resolveFlavorForInstanceTypeMutex.RLock() + defer fake.resolveFlavorForInstanceTypeMutex.RUnlock() + fake.resolveFlavorForRequirementsMutex.RLock() + defer fake.resolveFlavorForRequirementsMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *FakeFlavorResolver) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ compute.FlavorResolver = new(FakeFlavorResolver) diff --git a/src/openstack_cpi_golang/cpi/compute/computefakes/fake_volume_configurator.go b/src/openstack_cpi_golang/cpi/compute/computefakes/fake_volume_configurator.go new file mode 100644 index 00000000..715d1071 --- /dev/null +++ b/src/openstack_cpi_golang/cpi/compute/computefakes/fake_volume_configurator.go @@ -0,0 +1,126 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package computefakes + +import ( + "sync" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/compute" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/config" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/properties" + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/bootfromvolume" + "github.com/gophercloud/gophercloud/openstack/compute/v2/flavors" +) + +type FakeVolumeConfigurator struct { + ConfigureVolumesStub func(string, config.OpenstackConfig, properties.CreateVM, flavors.Flavor) ([]bootfromvolume.BlockDevice, error) + configureVolumesMutex sync.RWMutex + configureVolumesArgsForCall []struct { + arg1 string + arg2 config.OpenstackConfig + arg3 properties.CreateVM + arg4 flavors.Flavor + } + configureVolumesReturns struct { + result1 []bootfromvolume.BlockDevice + result2 error + } + configureVolumesReturnsOnCall map[int]struct { + result1 []bootfromvolume.BlockDevice + result2 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *FakeVolumeConfigurator) ConfigureVolumes(arg1 string, arg2 config.OpenstackConfig, arg3 properties.CreateVM, arg4 flavors.Flavor) ([]bootfromvolume.BlockDevice, error) { + fake.configureVolumesMutex.Lock() + ret, specificReturn := fake.configureVolumesReturnsOnCall[len(fake.configureVolumesArgsForCall)] + fake.configureVolumesArgsForCall = append(fake.configureVolumesArgsForCall, struct { + arg1 string + arg2 config.OpenstackConfig + arg3 properties.CreateVM + arg4 flavors.Flavor + }{arg1, arg2, arg3, arg4}) + stub := fake.ConfigureVolumesStub + fakeReturns := fake.configureVolumesReturns + fake.recordInvocation("ConfigureVolumes", []interface{}{arg1, arg2, arg3, arg4}) + fake.configureVolumesMutex.Unlock() + if stub != nil { + return stub(arg1, arg2, arg3, arg4) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeVolumeConfigurator) ConfigureVolumesCallCount() int { + fake.configureVolumesMutex.RLock() + defer fake.configureVolumesMutex.RUnlock() + return len(fake.configureVolumesArgsForCall) +} + +func (fake *FakeVolumeConfigurator) ConfigureVolumesCalls(stub func(string, config.OpenstackConfig, properties.CreateVM, flavors.Flavor) ([]bootfromvolume.BlockDevice, error)) { + fake.configureVolumesMutex.Lock() + defer fake.configureVolumesMutex.Unlock() + fake.ConfigureVolumesStub = stub +} + +func (fake *FakeVolumeConfigurator) ConfigureVolumesArgsForCall(i int) (string, config.OpenstackConfig, properties.CreateVM, flavors.Flavor) { + fake.configureVolumesMutex.RLock() + defer fake.configureVolumesMutex.RUnlock() + argsForCall := fake.configureVolumesArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4 +} + +func (fake *FakeVolumeConfigurator) ConfigureVolumesReturns(result1 []bootfromvolume.BlockDevice, result2 error) { + fake.configureVolumesMutex.Lock() + defer fake.configureVolumesMutex.Unlock() + fake.ConfigureVolumesStub = nil + fake.configureVolumesReturns = struct { + result1 []bootfromvolume.BlockDevice + result2 error + }{result1, result2} +} + +func (fake *FakeVolumeConfigurator) ConfigureVolumesReturnsOnCall(i int, result1 []bootfromvolume.BlockDevice, result2 error) { + fake.configureVolumesMutex.Lock() + defer fake.configureVolumesMutex.Unlock() + fake.ConfigureVolumesStub = nil + if fake.configureVolumesReturnsOnCall == nil { + fake.configureVolumesReturnsOnCall = make(map[int]struct { + result1 []bootfromvolume.BlockDevice + result2 error + }) + } + fake.configureVolumesReturnsOnCall[i] = struct { + result1 []bootfromvolume.BlockDevice + result2 error + }{result1, result2} +} + +func (fake *FakeVolumeConfigurator) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.configureVolumesMutex.RLock() + defer fake.configureVolumesMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *FakeVolumeConfigurator) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ compute.VolumeConfigurator = new(FakeVolumeConfigurator) diff --git a/src/openstack_cpi_golang/cpi/compute/flavor_resolver.go b/src/openstack_cpi_golang/cpi/compute/flavor_resolver.go new file mode 100644 index 00000000..696b1f7d --- /dev/null +++ b/src/openstack_cpi_golang/cpi/compute/flavor_resolver.go @@ -0,0 +1,171 @@ +package compute + +import ( + "fmt" + "math" + "sort" + + "github.com/cloudfoundry/bosh-cpi-go/apiv1" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/properties" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils" + "github.com/gophercloud/gophercloud/openstack/compute/v2/flavors" +) + +const NoDisk = 0 + +//counterfeiter:generate . FlavorResolver +type FlavorResolver interface { + ResolveFlavorForInstanceType(flavorName string) (flavors.Flavor, error) + ResolveFlavorForRequirements(vmResources apiv1.VMResources, bootFromVolume bool) ([]flavors.Flavor, error) + GetClosestMatchedFlavor(possibleFlavors []flavors.Flavor) flavors.Flavor + GetFlavorById(flavorId string) (flavors.Flavor, error) +} + +type flavorResolver struct { + serviceClients utils.ServiceClients + computeFacade ComputeFacade +} + +func NewFlavorResolver( + serviceClients utils.ServiceClients, + computeFacade ComputeFacade, +) flavorResolver { + return flavorResolver{ + serviceClients: serviceClients, + computeFacade: computeFacade, + } +} + +func (f flavorResolver) GetFlavorById(flavorId string) (flavors.Flavor, error) { + allFlavors, err := f.getFlavors() + if err != nil { + return flavors.Flavor{}, fmt.Errorf("failed to get flavors: %w", err) + } + + var flavor *flavors.Flavor + for _, singleFlavor := range allFlavors { + if singleFlavor.ID == flavorId { + flavor = &singleFlavor + break + } + } + + if flavor == nil { + return flavors.Flavor{}, fmt.Errorf("flavor for id '%s' not found", flavorId) + } + + return *flavor, nil +} + +func (f flavorResolver) ResolveFlavorForInstanceType(instanceType string) (flavors.Flavor, error) { + allFlavors, err := f.getFlavors() + if err != nil { + return flavors.Flavor{}, fmt.Errorf("failed to get flavors: %w", err) + } + + var flavor *flavors.Flavor + for _, singleFlavor := range allFlavors { + if singleFlavor.Name == instanceType { + flavor = &singleFlavor + break + } + } + + if flavor == nil { + return flavors.Flavor{}, fmt.Errorf("flavor for instance type '%s' not found", instanceType) + } + + if flavor.Ephemeral > 0 { + // Ephemeral disk size should be at least the double of the vm total memory size, as agent will need: + // - vm total memory size for swapon, + // - the rest for /var/vcap/data + minEphemeralSize := (flavor.RAM / 1024) * 2 + if flavor.Ephemeral < minEphemeralSize { + return flavors.Flavor{}, fmt.Errorf("flavor '%s' should have at least %dGb of ephemeral disk", flavor.Name, minEphemeralSize) + } + } + + return *flavor, nil +} + +func (f flavorResolver) ResolveFlavorForRequirements(vmResources apiv1.VMResources, bootFromVolume bool) ([]flavors.Flavor, error) { + normalizedEphemeralDiskSize := float64(vmResources.EphemeralDiskSize) / 1024 + + allFlavors, err := f.getFlavors() + if err != nil { + return []flavors.Flavor{}, fmt.Errorf("failed to get flavors: %w", err) + } + + var validFlavors []flavors.Flavor + for _, singleFlavor := range allFlavors { + if singleFlavor.RAM >= vmResources.RAM && singleFlavor.VCPUs >= vmResources.CPU { + validFlavors = append(validFlavors, singleFlavor) + } + } + + var possibleFlavors []flavors.Flavor + if bootFromVolume { + for _, singleFlavor := range validFlavors { + if singleFlavor.Ephemeral == NoDisk { + possibleFlavors = append(possibleFlavors, singleFlavor) + } + } + } else { + possibleFlavors = f.bootDefaultFlavors(int(math.Ceil(normalizedEphemeralDiskSize)), validFlavors) + } + return possibleFlavors, nil +} + +func (f flavorResolver) GetClosestMatchedFlavor(possibleFlavors []flavors.Flavor) flavors.Flavor { + // sort the flavors by vcpus, ram, disk and ephemeral disk + // the first element is the closest match + sort.Slice(possibleFlavors, func(i, j int) bool { + if possibleFlavors[i].VCPUs != possibleFlavors[j].VCPUs { + return possibleFlavors[i].VCPUs < possibleFlavors[j].VCPUs + } + if possibleFlavors[i].RAM != possibleFlavors[j].RAM { + return possibleFlavors[i].RAM < possibleFlavors[j].RAM + } + if (possibleFlavors[i].Disk + possibleFlavors[i].Ephemeral) != (possibleFlavors[j].Disk + possibleFlavors[j].Ephemeral) { + return (possibleFlavors[i].Disk + possibleFlavors[i].Ephemeral) < (possibleFlavors[j].Disk + possibleFlavors[j].Ephemeral) + } + return possibleFlavors[i].Disk < possibleFlavors[j].Disk + }) + + // After sorting, the first element is the closest match + return possibleFlavors[0] +} + +func (f flavorResolver) getFlavors() ([]flavors.Flavor, error) { + flavorPages, err := f.computeFacade.ListFlavors(f.serviceClients.RetryableServiceClient, flavors.ListOpts{}) + if err != nil { + return []flavors.Flavor{}, fmt.Errorf("failed to list flavors: %w", err) + } + + allFlavors, err := f.computeFacade.ExtractFlavors(flavorPages) + if err != nil { + return []flavors.Flavor{}, fmt.Errorf("failed to extract flavors: %w", err) + } + + return allFlavors, nil +} + +func (f flavorResolver) bootDefaultFlavors(ephemeralDiskSize int, validFlavors []flavors.Flavor) []flavors.Flavor { + var resultFlavors []flavors.Flavor + for _, singleFlavor := range validFlavors { + if singleFlavor.Ephemeral >= ephemeralDiskSize && singleFlavor.Disk >= properties.OsOverheadInGb { + resultFlavors = append(resultFlavors, singleFlavor) + } + } + + if len(resultFlavors) != 0 { + return resultFlavors + } + + for _, singleFlavor := range validFlavors { + if singleFlavor.Ephemeral == NoDisk && singleFlavor.Disk >= ephemeralDiskSize+properties.OsOverheadInGb { + resultFlavors = append(resultFlavors, singleFlavor) + } + } + return resultFlavors +} diff --git a/src/openstack_cpi_golang/cpi/compute/flavor_resolver_test.go b/src/openstack_cpi_golang/cpi/compute/flavor_resolver_test.go new file mode 100644 index 00000000..f3ae2dae --- /dev/null +++ b/src/openstack_cpi_golang/cpi/compute/flavor_resolver_test.go @@ -0,0 +1,214 @@ +package compute_test + +import ( + "errors" + + "github.com/cloudfoundry/bosh-cpi-go/apiv1" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/compute" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/compute/computefakes" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/mocks" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils" + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/compute/v2/flavors" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("FlavorResolver", func() { + + var serviceClient gophercloud.ServiceClient + var retryableServiceClient gophercloud.ServiceClient + var serviceClients utils.ServiceClients + var computeFacade computefakes.FakeComputeFacade + var flavorsPage mocks.MockPage + + BeforeEach(func() { + serviceClient = gophercloud.ServiceClient{} + retryableServiceClient = gophercloud.ServiceClient{} + serviceClients = utils.ServiceClients{ServiceClient: &serviceClient, RetryableServiceClient: &retryableServiceClient} + computeFacade = computefakes.FakeComputeFacade{} + + computeFacade.ListFlavorsReturns(flavorsPage, nil) + computeFacade.ExtractFlavorsReturns([]flavors.Flavor{{ID: "the_flavor_id", Name: "the_instance_type", VCPUs: 2, RAM: 4096, Ephemeral: 10}}, nil) + }) + + Context("ResolveFlavorForInstanceType", func() { + It("lists flavors", func() { + _, _ = compute.NewFlavorResolver(serviceClients, &computeFacade).ResolveFlavorForInstanceType("the_instance_type") + + Expect(computeFacade.ListFlavorsCallCount()).To(Equal(1)) + }) + + It("return error if list flavors fails", func() { + computeFacade.ListFlavorsReturns(nil, errors.New("boom")) + + _, err := compute.NewFlavorResolver(serviceClients, &computeFacade).ResolveFlavorForInstanceType("the_instance_type") + + Expect(err.Error()).To(ContainSubstring("failed to list flavors: boom")) + }) + + It("extract flavors", func() { + _, _ = compute.NewFlavorResolver(serviceClients, &computeFacade).ResolveFlavorForInstanceType("the_instance_type") + + Expect(computeFacade.ExtractFlavorsArgsForCall(0)).To(Equal(flavorsPage)) + Expect(computeFacade.ExtractFlavorsCallCount()).To(Equal(1)) + }) + + It("return error if extract flavors fails", func() { + computeFacade.ExtractFlavorsReturns(nil, errors.New("boom")) + + _, err := compute.NewFlavorResolver(serviceClients, &computeFacade).ResolveFlavorForInstanceType("the_instance_type") + + Expect(err.Error()).To(ContainSubstring("failed to extract flavors: boom")) + }) + + It("return an error if flavor name is not found", func() { + _, err := compute.NewFlavorResolver(serviceClients, &computeFacade).ResolveFlavorForInstanceType("not_existing_instance_type") + + Expect(err.Error()).To(ContainSubstring("flavor for instance type 'not_existing_instance_type' not found")) + }) + + It("return an error if flavor ephemeral disk is to small", func() { + computeFacade.ExtractFlavorsReturns([]flavors.Flavor{{ID: "the_flavor_id", Name: "the_instance_type", RAM: 4096, Ephemeral: 2}}, nil) + + _, err := compute.NewFlavorResolver(serviceClients, &computeFacade).ResolveFlavorForInstanceType("the_instance_type") + + Expect(err.Error()).To(ContainSubstring("flavor 'the_instance_type' should have at least 8Gb of ephemeral disk")) + }) + }) + + Context("ResolveFlavorForRequirements", func() { + var vmResources apiv1.VMResources + var bootFromVolume bool + + BeforeEach(func() { + vmResources = apiv1.VMResources{CPU: 2, RAM: 4096, EphemeralDiskSize: 10} + bootFromVolume = false + }) + + It("lists flavors", func() { + _, _ = compute.NewFlavorResolver(serviceClients, &computeFacade).ResolveFlavorForRequirements(vmResources, bootFromVolume) + + Expect(computeFacade.ListFlavorsCallCount()).To(Equal(1)) + }) + + It("return error if list flavors fails", func() { + computeFacade.ListFlavorsReturns(nil, errors.New("boom")) + + _, err := compute.NewFlavorResolver(serviceClients, &computeFacade).ResolveFlavorForRequirements(vmResources, bootFromVolume) + + Expect(err.Error()).To(ContainSubstring("failed to list flavors: boom")) + }) + + It("extract flavors", func() { + _, _ = compute.NewFlavorResolver(serviceClients, &computeFacade).ResolveFlavorForRequirements(vmResources, bootFromVolume) + + Expect(computeFacade.ExtractFlavorsArgsForCall(0)).To(Equal(flavorsPage)) + Expect(computeFacade.ExtractFlavorsCallCount()).To(Equal(1)) + }) + + It("return error if extract flavors fails", func() { + computeFacade.ExtractFlavorsReturns(nil, errors.New("boom")) + + _, err := compute.NewFlavorResolver(serviceClients, &computeFacade).ResolveFlavorForRequirements(vmResources, bootFromVolume) + + Expect(err.Error()).To(ContainSubstring("failed to extract flavors: boom")) + }) + + It("return an empty slice if no flavor fulfill all requirements regarding VCPUs", func() { + vmResources.CPU = 4 + possibleFlavors, err := compute.NewFlavorResolver(serviceClients, &computeFacade).ResolveFlavorForRequirements(vmResources, bootFromVolume) + + Expect(err).ToNot(HaveOccurred()) + Expect(possibleFlavors).To(BeEmpty()) + }) + + It("return an empty slice if no flavor fulfill all requirements regarding RAM", func() { + vmResources.RAM = 8192 + possibleFlavors, err := compute.NewFlavorResolver(serviceClients, &computeFacade).ResolveFlavorForRequirements(vmResources, bootFromVolume) + + Expect(err).ToNot(HaveOccurred()) + Expect(possibleFlavors).To(BeEmpty()) + }) + + Context("when booting from volume", func() { + BeforeEach(func() { + bootFromVolume = true + }) + + It("return an empty slice if all flavors have an ephemeral disk", func() { + computeFacade.ExtractFlavorsReturns( + []flavors.Flavor{ + {ID: "the_flavor_id_1", Name: "the_instance_type_1", VCPUs: 1, RAM: 2048, Ephemeral: 10}, + {ID: "the_flavor_id_2", Name: "the_instance_type_2", VCPUs: 2, RAM: 4096, Ephemeral: 20}, + }, nil) + possibleFlavors, err := compute.NewFlavorResolver(serviceClients, &computeFacade).ResolveFlavorForRequirements(vmResources, bootFromVolume) + + Expect(err).ToNot(HaveOccurred()) + Expect(possibleFlavors).To(BeEmpty()) + }) + + It("return flavor if valid flavor does not have a disk", func() { + computeFacade.ExtractFlavorsReturns( + []flavors.Flavor{ + {ID: "the_flavor_id_1", Name: "the_instance_type_1", VCPUs: 1, RAM: 2048, Ephemeral: 10}, + {ID: "the_flavor_id_2", Name: "the_instance_type_2", VCPUs: 2, RAM: 4096, Ephemeral: 0}, + }, nil) + possibleFlavors, err := compute.NewFlavorResolver(serviceClients, &computeFacade).ResolveFlavorForRequirements(vmResources, bootFromVolume) + + Expect(err).ToNot(HaveOccurred()) + Expect(possibleFlavors).To(HaveLen(1)) + Expect(possibleFlavors[0].Name).To(Equal("the_instance_type_2")) + }) + + }) + + Context("when not booting from volume", func() { + It("return flavor if valid flavor already fulfill ephemeral and os disk size requirements", func() { + vmResources.EphemeralDiskSize = 10241 + computeFacade.ExtractFlavorsReturns( + []flavors.Flavor{ + {ID: "the_flavor_id_1", Name: "the_instance_type_1", VCPUs: 2, RAM: 4096, Ephemeral: 10, Disk: 3}, + {ID: "the_flavor_id_2", Name: "the_instance_type_2", VCPUs: 2, RAM: 4096, Ephemeral: 20, Disk: 1}, + {ID: "the_flavor_id_3", Name: "the_instance_type_3", VCPUs: 2, RAM: 4096, Ephemeral: 20, Disk: 3}, + }, nil) + possibleFlavors, err := compute.NewFlavorResolver(serviceClients, &computeFacade).ResolveFlavorForRequirements(vmResources, bootFromVolume) + + Expect(err).ToNot(HaveOccurred()) + Expect(possibleFlavors).To(HaveLen(1)) + Expect(possibleFlavors[0].Name).To(Equal("the_instance_type_3")) + }) + + It("return flavor if valid flavor does not have ephemeral disk but os disk fulfills size requirements", func() { + vmResources.EphemeralDiskSize = 10241 + computeFacade.ExtractFlavorsReturns( + []flavors.Flavor{ + {ID: "the_flavor_id_1", Name: "the_instance_type_1", VCPUs: 2, RAM: 4096, Ephemeral: 10, Disk: 3}, + {ID: "the_flavor_id_2", Name: "the_instance_type_2", VCPUs: 2, RAM: 4096, Ephemeral: 20, Disk: 1}, + {ID: "the_flavor_id_3", Name: "the_instance_type_3", VCPUs: 2, RAM: 4096, Ephemeral: 0, Disk: 10}, + {ID: "the_flavor_id_4", Name: "the_instance_type_4", VCPUs: 2, RAM: 4096, Ephemeral: 0, Disk: 15}, + }, nil) + possibleFlavors, err := compute.NewFlavorResolver(serviceClients, &computeFacade).ResolveFlavorForRequirements(vmResources, bootFromVolume) + + Expect(err).ToNot(HaveOccurred()) + Expect(possibleFlavors).To(HaveLen(1)) + Expect(possibleFlavors[0].Name).To(Equal("the_instance_type_4")) + }) + }) + }) + + Context("GetClosestMatchedFlavor", func() { + It("returns the flavor that has the closest match to the requested resources", func() { + possibleFlavors := + []flavors.Flavor{ + {ID: "the_flavor_id_1", Name: "the_instance_type_1", VCPUs: 1, RAM: 2048, Ephemeral: 10, Disk: 10}, + {ID: "the_flavor_id_2", Name: "the_instance_type_2", VCPUs: 1, RAM: 4096, Ephemeral: 20}, + {ID: "the_flavor_id_3", Name: "the_instance_type_3", VCPUs: 4, RAM: 8192, Ephemeral: 40}, + {ID: "the_flavor_id_4", Name: "the_instance_type_4", VCPUs: 1, RAM: 2048, Ephemeral: 20, Disk: 0}, + } + possibleFlavor := compute.NewFlavorResolver(serviceClients, &computeFacade).GetClosestMatchedFlavor(possibleFlavors) + + Expect(possibleFlavor.Name).To(Equal("the_instance_type_4")) + }) + }) +}) diff --git a/src/openstack_cpi_golang/cpi/compute/volume_configurator.go b/src/openstack_cpi_golang/cpi/compute/volume_configurator.go new file mode 100644 index 00000000..f4de18c1 --- /dev/null +++ b/src/openstack_cpi_golang/cpi/compute/volume_configurator.go @@ -0,0 +1,65 @@ +package compute + +import ( + "fmt" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/config" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/properties" + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/bootfromvolume" + "github.com/gophercloud/gophercloud/openstack/compute/v2/flavors" +) + +//counterfeiter:generate . VolumeConfigurator +type VolumeConfigurator interface { + ConfigureVolumes( + imageID string, + openstackConfig config.OpenstackConfig, + cloudProperties properties.CreateVM, + flavor flavors.Flavor, + ) ([]bootfromvolume.BlockDevice, error) +} + +type volumeConfigurator struct{} + +func NewVolumeConfigurator() volumeConfigurator { + return volumeConfigurator{} +} +func (v volumeConfigurator) ConfigureVolumes(imageID string, openstackConfig config.OpenstackConfig, cloudProperties properties.CreateVM, flavor flavors.Flavor) ([]bootfromvolume.BlockDevice, error) { + bootVolumeSize, err := v.select_boot_volume_size(flavor, cloudProperties) + if err != nil { + return []bootfromvolume.BlockDevice{}, fmt.Errorf("failed to get volume size: %w", err) + } + + if !v.bootFromVolume(openstackConfig, cloudProperties) { + return []bootfromvolume.BlockDevice{}, nil + } + + return []bootfromvolume.BlockDevice{{ + UUID: imageID, + SourceType: bootfromvolume.SourceImage, + DestinationType: bootfromvolume.DestinationVolume, + VolumeSize: bootVolumeSize, + BootIndex: 0, + DeleteOnTermination: true, + }}, nil +} + +func (v volumeConfigurator) bootFromVolume(openstackConfig config.OpenstackConfig, cloudProperties properties.CreateVM) bool { + if cloudProperties.BootFromVolume == nil { + return openstackConfig.BootFromVolume + } + + return *cloudProperties.BootFromVolume +} + +func (v volumeConfigurator) select_boot_volume_size(flavor flavors.Flavor, cloudProperties properties.CreateVM) (int, error) { + rootDiskSize := cloudProperties.RootDisk.Size + if rootDiskSize == 0 { + if flavor.Disk == 0 { + return 0, fmt.Errorf("flavor '%s' has a root disk size of 0. Either pick a different flavor or define root_disk.size in your VM cloud_properties", flavor.ID) + } + return flavor.Disk, nil + } + + return rootDiskSize, nil +} diff --git a/src/openstack_cpi_golang/cpi/compute/volume_configurator_test.go b/src/openstack_cpi_golang/cpi/compute/volume_configurator_test.go new file mode 100644 index 00000000..2802cdd2 --- /dev/null +++ b/src/openstack_cpi_golang/cpi/compute/volume_configurator_test.go @@ -0,0 +1,149 @@ +package compute_test + +import ( + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/compute" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/config" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/properties" + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/bootfromvolume" + "github.com/gophercloud/gophercloud/openstack/compute/v2/flavors" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("VolumeConfigurator", func() { + + BeforeEach(func() { + }) + + Context("ConfigureVolumes", func() { + + It("uses disk size from cloud properties if defined", func() { + bootFromVolume := true + volumes, err := compute.NewVolumeConfigurator().ConfigureVolumes( + "the_image_id", + config.OpenstackConfig{}, + properties.CreateVM{ + RootDisk: properties.Disk{Size: 888}, + BootFromVolume: &bootFromVolume, + }, + flavors.Flavor{}, + ) + + Expect(err).ToNot(HaveOccurred()) + Expect(volumes[0].VolumeSize).To(Equal(888)) + }) + + It("uses disk size from the flavor if root disk size is 0 and flavor disk size is > 0", func() { + bootFromVolume := true + volumes, err := compute.NewVolumeConfigurator().ConfigureVolumes( + "the_image_id", + config.OpenstackConfig{}, + properties.CreateVM{ + RootDisk: properties.Disk{Size: 0}, + BootFromVolume: &bootFromVolume, + }, + flavors.Flavor{ + Disk: 999, + }, + ) + Expect(err).ToNot(HaveOccurred()) + Expect(volumes[0].VolumeSize).To(Equal(999)) + }) + + It("returns an error if root disk size is 0 and flavor disk size is 0", func() { + bootFromVolume := true + _, err := compute.NewVolumeConfigurator().ConfigureVolumes( + "the_image_id", + config.OpenstackConfig{}, + properties.CreateVM{ + RootDisk: properties.Disk{Size: 0}, + BootFromVolume: &bootFromVolume, + }, + flavors.Flavor{ + ID: "the_flavor_id", + Disk: 0, + }, + ) + + Expect(err.Error()).To(ContainSubstring("failed to get volume size: flavor 'the_flavor_id' has a root disk size of 0.")) + }) + + It("returns an empty array of block devices if boot volume is not defined", func() { + + bootFromVolume := true + volumes, err := compute.NewVolumeConfigurator().ConfigureVolumes( + "the_image_id", + config.OpenstackConfig{}, + properties.CreateVM{ + RootDisk: properties.Disk{Size: 0}, + BootFromVolume: &bootFromVolume, + }, + flavors.Flavor{ + Disk: 999, + }, + ) + + Expect(err).ToNot(HaveOccurred()) + Expect(volumes[0].UUID).To(Equal("the_image_id")) + Expect(volumes[0].SourceType).To(Equal(bootfromvolume.SourceImage)) + Expect(volumes[0].DestinationType).To(Equal(bootfromvolume.DestinationVolume)) + Expect(volumes[0].VolumeSize).To(Equal(999)) + Expect(volumes[0].BootIndex).To(Equal(0)) + Expect(volumes[0].DeleteOnTermination).To(BeTrue()) + }) + + It("returns a block device", func() { + + volumes, err := compute.NewVolumeConfigurator().ConfigureVolumes( + "the_image_id", + config.OpenstackConfig{}, + properties.CreateVM{ + RootDisk: properties.Disk{Size: 0}, + }, + flavors.Flavor{ + Disk: 999, + }, + ) + + Expect(err).ToNot(HaveOccurred()) + Expect(len(volumes)).To(Equal(0)) + }) + + //It("return error if list flavors fails", func() { + // computeFacade.ListFlavorsReturns(nil, errors.New("boom")) + // + // _, err := NewFlavorResolver(&serviceClient, &computeFacade).ResolveFlavorForInstanceType("the_instance_type") + // + // Expect(err.Error()).To(ContainSubstring("failed to list flavors: boom")) + //}) + // + //It("extract flavors", func() { + // NewFlavorResolver(&serviceClient, &computeFacade).ResolveFlavorForInstanceType("the_instance_type") + // + // Expect(computeFacade.ExtractFlavorsArgsForCall(0)).To(Equal(flavorsPage)) + // Expect(computeFacade.ExtractFlavorsCallCount()).To(Equal(1)) + //}) + // + //It("return error if extract flavors fails", func() { + // computeFacade.ExtractFlavorsReturns(nil, errors.New("boom")) + // + // _, err := NewFlavorResolver(&serviceClient, &computeFacade).ResolveFlavorForInstanceType("the_instance_type") + // + // Expect(err.Error()).To(ContainSubstring("failed to extract flavors: boom")) + //}) + // + //It("return an error if flavor name is not found", func() { + // _, err := NewFlavorResolver(&serviceClient, &computeFacade).ResolveFlavorForInstanceType("not_existing_instance_type") + // + // Expect(err.Error()).To(ContainSubstring("flavor for instance type 'not_existing_instance_type' not found")) + //}) + // + //It("return an error if flavor ephemeral disk is to small", func() { + // computeFacade.ExtractFlavorsReturns([]flavors.Flavor{{ID: "the_flavor_id", Name: "the_instance_type", RAM: 4096, Ephemeral: 2}}, nil) + // + // _, err := NewFlavorResolver(&serviceClient, &computeFacade).ResolveFlavorForInstanceType("the_instance_type") + // + // Expect(err.Error()).To(ContainSubstring("flavor 'the_instance_type' should have at least 8Gb of ephemeral disk")) + //}) + }) +}) diff --git a/src/openstack_cpi_golang/cpi/config/config.go b/src/openstack_cpi_golang/cpi/config/config.go index 5c70562e..9212f372 100644 --- a/src/openstack_cpi_golang/cpi/config/config.go +++ b/src/openstack_cpi_golang/cpi/config/config.go @@ -104,11 +104,11 @@ func (p Properties) Validate() error { func (o OpenstackConfig) Validate() error { if !((o.usernameIsSet() && !o.applicationCredentialIsSet()) || (!o.usernameIsSet() && o.applicationCredentialIsSet())) { - return fmt.Errorf("'Invalid OpenStack cloud properties: username and api_key or application_credential_id and application_credential_secret is required'") + return fmt.Errorf("'invalid OpenStack cloud properties: username and api_key or application_credential_id and application_credential_secret is required'") } if o.ConfigDrive != "" && o.ConfigDrive != "cdrom" && o.ConfigDrive != "disk" { - return fmt.Errorf("Invalid OpenStack cloud properties: config_drive must be either 'cdrom' or 'disk'") + return fmt.Errorf("invalid OpenStack cloud properties: config_drive must be either 'cdrom' or 'disk'") } return nil diff --git a/src/openstack_cpi_golang/cpi/config/config_test.go b/src/openstack_cpi_golang/cpi/config/config_test.go index 0a7bad48..d1bd0067 100644 --- a/src/openstack_cpi_golang/cpi/config/config_test.go +++ b/src/openstack_cpi_golang/cpi/config/config_test.go @@ -201,7 +201,7 @@ var _ = Describe("OpenstackConfig", func() { It("returns an error if username and application credential is set", func() { _, err := config.NewConfigFromPath(fileSystem, "some/path/invalid_user_config.json") - Expect(err.Error()).To(ContainSubstring("Invalid OpenStack cloud properties: username and api_key or application_credential_id and application_credential_secret is required")) + Expect(err.Error()).To(ContainSubstring("invalid OpenStack cloud properties: username and api_key or application_credential_id and application_credential_secret is required")) }) It("config drive can be set to disk", func() { @@ -221,13 +221,13 @@ var _ = Describe("OpenstackConfig", func() { It("returns an error if config drive is invalid", func() { _, err := config.NewConfigFromPath(fileSystem, "some/path/invalid_config_drive.json") - Expect(err.Error()).To(ContainSubstring("Invalid OpenStack cloud properties: config_drive must be either 'cdrom' or 'disk'")) + Expect(err.Error()).To(ContainSubstring("invalid OpenStack cloud properties: config_drive must be either 'cdrom' or 'disk'")) }) It("returns an error if config is empty", func() { _, err := config.NewConfigFromPath(fileSystem, "some/path/empty_config.json") - Expect(err.Error()).To(ContainSubstring("Invalid OpenStack cloud properties: username and api_key or application_credential_id and application_credential_secret is required")) + Expect(err.Error()).To(ContainSubstring("invalid OpenStack cloud properties: username and api_key or application_credential_id and application_credential_secret is required")) }) It("succeeds with username and api_key", func() { diff --git a/src/openstack_cpi_golang/cpi/factory.go b/src/openstack_cpi_golang/cpi/factory.go index 515cefaa..1dc840e0 100644 --- a/src/openstack_cpi_golang/cpi/factory.go +++ b/src/openstack_cpi_golang/cpi/factory.go @@ -2,9 +2,16 @@ package cpi import ( "github.com/cloudfoundry/bosh-cpi-go/apiv1" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/compute" "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/config" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/image" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/image/root_image" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/loadbalancer" "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/methods" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/network" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/openstack" "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/volume" ) type Factory struct { @@ -51,26 +58,107 @@ func NewFactory( } func (f Factory) New(ctx apiv1.CallContext) (apiv1.CPI, error) { + openstackService := openstack.NewOpenstackService(openstack.NewOpenstackFacade(), utils.NewEnvVar()) + return CPI{ methods.NewInfoMethod(), - methods.NewCreateStemcellMethod(), - methods.NewDeleteStemcellMethod(), - methods.NewCreateVMMethod(), - methods.NewDeleteVMMethod(), - methods.NewCalculateVMCloudPropertiesMethod(), - methods.NewHasVMMethod(), - methods.NewRebootVMMethod(), - methods.NewSetVMMetadataMethod(), - methods.NewGetDisksMethod(), - methods.NewCreateDiskMethod(), - methods.NewDeleteDiskMethod(), - methods.NewAttachDiskMethod(), - methods.NewDetachDiskMethod(), - methods.NewHasDiskMethod(), - methods.NewResizeDiskMethod(), - methods.NewSetDiskMetadataMethod(), - methods.NewDeleteSnapshotMethod(), - methods.NewSnapshotDiskMethod(), + methods.NewCreateStemcellMethod( + + image.NewImageServiceBuilder(openstackService, f.cpiConfig, f.logger), + image.NewHeavyStemcellCreator(f.openstackConfig), + image.NewLightStemcellCreator(f.openstackConfig), + root_image.NewRootImage(), + f.openstackConfig, + f.logger, + ), + + methods.NewDeleteStemcellMethod( + image.NewImageServiceBuilder(openstackService, f.cpiConfig, f.logger), + f.logger, + ), + + methods.NewCreateVMMethod( + image.NewImageServiceBuilder(openstackService, f.cpiConfig, f.logger), + network.NewNetworkServiceBuilder(openstackService, f.cpiConfig, f.logger), + compute.NewComputeServiceBuilder(openstackService, f.cpiConfig, f.logger), + loadbalancer.NewLoadbalancerServiceBuilder(openstackService, f.cpiConfig, f.logger), + f.cpiConfig, + f.logger, + ), + + methods.NewDeleteVMMethod( + network.NewNetworkServiceBuilder(openstackService, f.cpiConfig, f.logger), + compute.NewComputeServiceBuilder(openstackService, f.cpiConfig, f.logger), + loadbalancer.NewLoadbalancerServiceBuilder(openstackService, f.cpiConfig, f.logger), + f.cpiConfig, + f.logger, + ), + + methods.NewCalculateVMCloudPropertiesMethod( + compute.NewComputeServiceBuilder(openstackService, f.cpiConfig, f.logger), + f.cpiConfig, + f.logger, + ), + + methods.NewHasVMMethod( + compute.NewComputeServiceBuilder(openstackService, f.cpiConfig, f.logger), + f.logger, + ), + + methods.NewRebootVMMethod( + compute.NewComputeServiceBuilder(openstackService, f.cpiConfig, f.logger), + f.cpiConfig, + f.logger, + ), + + methods.NewSetVMMetadataMethod( + compute.NewComputeServiceBuilder(openstackService, f.cpiConfig, f.logger), + f.logger, + f.cpiConfig), + methods.NewGetDisksMethod( + compute.NewComputeServiceBuilder(openstackService, f.cpiConfig, f.logger), + f.logger), + methods.NewCreateDiskMethod( + compute.NewComputeServiceBuilder(openstackService, f.cpiConfig, f.logger), + volume.NewVolumeServiceBuilder(openstackService, f.cpiConfig, f.logger), + f.cpiConfig, + f.logger, + ), + methods.NewDeleteDiskMethod( + volume.NewVolumeServiceBuilder(openstackService, f.cpiConfig, f.logger), + f.cpiConfig, + f.logger, + ), + methods.NewAttachDiskMethod( + compute.NewComputeServiceBuilder(openstackService, f.cpiConfig, f.logger), + volume.NewVolumeServiceBuilder(openstackService, f.cpiConfig, f.logger), + f.cpiConfig, + f.logger), + methods.NewDetachDiskMethod( + compute.NewComputeServiceBuilder(openstackService, f.cpiConfig, f.logger), + volume.NewVolumeServiceBuilder(openstackService, f.cpiConfig, f.logger), + f.cpiConfig, + f.logger), + methods.NewHasDiskMethod( + volume.NewVolumeServiceBuilder(openstackService, f.cpiConfig, f.logger), + f.logger), + methods.NewResizeDiskMethod( + volume.NewVolumeServiceBuilder(openstackService, f.cpiConfig, f.logger), + f.cpiConfig, + f.logger, + ), + methods.NewSetDiskMetadataMethod( + compute.NewComputeServiceBuilder(openstackService, f.cpiConfig, f.logger), + volume.NewVolumeServiceBuilder(openstackService, f.cpiConfig, f.logger), + f.logger), + methods.NewDeleteSnapshotMethod( + volume.NewVolumeServiceBuilder(openstackService, f.cpiConfig, f.logger), + f.cpiConfig, + f.logger), + methods.NewSnapshotDiskMethod( + volume.NewVolumeServiceBuilder(openstackService, f.cpiConfig, f.logger), + f.cpiConfig, + f.logger), }, nil } diff --git a/src/openstack_cpi_golang/cpi/image/heavy_stemcell_creator.go b/src/openstack_cpi_golang/cpi/image/heavy_stemcell_creator.go new file mode 100644 index 00000000..143a1003 --- /dev/null +++ b/src/openstack_cpi_golang/cpi/image/heavy_stemcell_creator.go @@ -0,0 +1,39 @@ +package image + +import ( + "fmt" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/config" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/properties" +) + +//counterfeiter:generate . HeavyStemcellCreator +type HeavyStemcellCreator interface { + Create(imageService ImageService, cloudProps properties.CreateStemcell, imagePath string) (string, error) +} + +type heavyStemcellCreator struct { + config config.OpenstackConfig +} + +func NewHeavyStemcellCreator( + config config.OpenstackConfig, +) heavyStemcellCreator { + return heavyStemcellCreator{ + config: config, + } +} + +func (h heavyStemcellCreator) Create(imageService ImageService, cloudProps properties.CreateStemcell, rootImagePath string) (string, error) { + imageID, err := imageService.CreateImage(cloudProps, h.config) + if err != nil { + return "", fmt.Errorf("failed to create image: %w", err) + } + + err = imageService.UploadImage(imageID, rootImagePath) + if err != nil { + return "", fmt.Errorf("failed to upload root image: %w", err) + } + + return imageID, nil +} diff --git a/src/openstack_cpi_golang/cpi/image/heavy_stemcell_creator_test.go b/src/openstack_cpi_golang/cpi/image/heavy_stemcell_creator_test.go new file mode 100644 index 00000000..de0b5c1c --- /dev/null +++ b/src/openstack_cpi_golang/cpi/image/heavy_stemcell_creator_test.go @@ -0,0 +1,80 @@ +package image_test + +import ( + "errors" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/config" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/image" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/image/imagefakes" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/properties" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("heavyStemcellCreator", func() { + var imageServiceClient imagefakes.FakeImageService + var config = config.OpenstackConfig{} + + Context("Create", func() { + + BeforeEach(func() { + imageServiceClient = imagefakes.FakeImageService{} + }) + + It("returns the ID of a created OpenStack image", func() { + imageServiceClient.CreateImageReturns("1234", nil) + + imageID, err := image.NewHeavyStemcellCreator(config). + Create(&imageServiceClient, properties.CreateStemcell{}, "root/image/path") + + Expect(err).ToNot(HaveOccurred()) + Expect(imageID).To(Equal("1234")) + }) + + It("returns an error if the OpenStack image creation fails", func() { + imageServiceClient.CreateImageReturns("", errors.New("boom")) + + imageID, err := image.NewHeavyStemcellCreator(config). + Create(&imageServiceClient, properties.CreateStemcell{}, "root/image/path") + + Expect(err.Error()).To(Equal("failed to create image: boom")) + Expect(imageID).To(Equal("")) + }) + + It("returns an error if OpenStack image upload fails", func() { + imageServiceClient.CreateImageReturns("1234", nil) + imageServiceClient.UploadImageReturns(errors.New("boom")) + + imageID, err := image.NewHeavyStemcellCreator(config). + Create(&imageServiceClient, properties.CreateStemcell{}, "root/image/path") + + Expect(err.Error()).To(Equal("failed to upload root image: boom")) + Expect(imageID).To(Equal("")) + }) + + It("creates an OpenStack image", func() { + imageServiceClient.CreateImageReturns("1234", nil) + imageServiceClient.UploadImageReturns(nil) + theCloudProps := properties.CreateStemcell{} + + _, _ = image.NewHeavyStemcellCreator(config). + Create(&imageServiceClient, properties.CreateStemcell{}, "root/image/path") + + cloudProps, config := imageServiceClient.CreateImageArgsForCall(0) + Expect(cloudProps).To(Equal(theCloudProps)) + Expect(config).To(Equal(config)) + }) + + It("uploads the root.img file to a created OpenStack image", func() { + imageServiceClient.CreateImageReturns("1234", nil) + imageServiceClient.UploadImageReturns(nil) + + _, _ = image.NewHeavyStemcellCreator(config). + Create(&imageServiceClient, properties.CreateStemcell{}, "root/image/path") + + imageID, imageFilePath := imageServiceClient.UploadImageArgsForCall(0) + Expect(imageID).To(Equal("1234")) + Expect(imageFilePath).To(Equal("root/image/path")) + }) + }) +}) diff --git a/src/openstack_cpi_golang/cpi/image/http_client.go b/src/openstack_cpi_golang/cpi/image/http_client.go new file mode 100644 index 00000000..236aae1a --- /dev/null +++ b/src/openstack_cpi_golang/cpi/image/http_client.go @@ -0,0 +1,28 @@ +package image + +import ( + "io" + "net/http" +) + +//counterfeiter:generate . HttpClient +type HttpClient interface { + Do(req *http.Request) (*http.Response, error) + + NewRequest(method, url string, body io.Reader) (*http.Request, error) +} + +type httpClient struct { +} + +func NewHttpClient() HttpClient { + return httpClient{} +} + +func (c httpClient) Do(req *http.Request) (*http.Response, error) { + return http.DefaultClient.Do(req) +} + +func (c httpClient) NewRequest(method, url string, body io.Reader) (*http.Request, error) { + return http.NewRequest(method, url, body) +} diff --git a/src/openstack_cpi_golang/cpi/image/image_facade.go b/src/openstack_cpi_golang/cpi/image/image_facade.go new file mode 100644 index 00000000..4c8ca3b4 --- /dev/null +++ b/src/openstack_cpi_golang/cpi/image/image_facade.go @@ -0,0 +1,33 @@ +package image + +import ( + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils" + "github.com/gophercloud/gophercloud/openstack/imageservice/v2/images" +) + +//counterfeiter:generate . ImageFacade +type ImageFacade interface { + CreateImage(client utils.ServiceClient, opts images.CreateOptsBuilder) (*images.Image, error) + + GetImage(client utils.RetryableServiceClient, id string) (*images.Image, error) + + DeleteImage(client utils.RetryableServiceClient, id string) error +} + +type imagesFacade struct{} + +func NewImageFacade() ImageFacade { + return imagesFacade{} +} + +func (c imagesFacade) CreateImage(serviceClient utils.ServiceClient, createOpts images.CreateOptsBuilder) (*images.Image, error) { + return images.Create(serviceClient, createOpts).Extract() +} + +func (c imagesFacade) GetImage(serviceClient utils.RetryableServiceClient, id string) (*images.Image, error) { + return images.Get(serviceClient, id).Extract() +} + +func (c imagesFacade) DeleteImage(serviceClient utils.RetryableServiceClient, id string) error { + return images.Delete(serviceClient, id).ExtractErr() +} diff --git a/src/openstack_cpi_golang/cpi/image/image_service.go b/src/openstack_cpi_golang/cpi/image/image_service.go new file mode 100644 index 00000000..e53d0935 --- /dev/null +++ b/src/openstack_cpi_golang/cpi/image/image_service.go @@ -0,0 +1,162 @@ +package image + +import ( + "bytes" + "fmt" + "io" + "net/http" + "os" + "strconv" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/config" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/properties" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils" + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/imageservice/v2/images" +) + +//counterfeiter:generate . ImageService +type ImageService interface { + CreateImage( + cloudProps properties.CreateStemcell, + config config.OpenstackConfig, + ) (string, error) + + GetImage( + imageID string, + ) (string, error) + + UploadImage( + imageID string, + imageFilePath string, + ) error + + DeleteImage( + imageID string, + ) error +} + +type imageService struct { + serviceClients utils.ServiceClients + imagesFacade ImageFacade + httpClient HttpClient + logger utils.Logger +} + +func NewImageService(serviceClients utils.ServiceClients, imagesFacade ImageFacade, httpClient HttpClient, logger utils.Logger) imageService { + return imageService{ + serviceClients: serviceClients, + imagesFacade: imagesFacade, + httpClient: httpClient, + logger: logger, + } +} + +func (c imageService) CreateImage(cloudProps properties.CreateStemcell, config config.OpenstackConfig) (string, error) { + createOpts := images.CreateOpts{ + Name: fmt.Sprintf("%s/%s", cloudProps.Name, cloudProps.Version), + Visibility: c.getImageVisibility(config.StemcellPubliclyVisible), + DiskFormat: cloudProps.DiskFormat, + ContainerFormat: cloudProps.ContainerFormat, + Properties: c.getProperties(cloudProps), + } + + image, err := c.imagesFacade.CreateImage(c.serviceClients.ServiceClient, createOpts) + if err != nil { + return "", fmt.Errorf("failed to create image: %w", err) + } + + return image.ID, nil +} + +func (c imageService) GetImage(imageID string) (string, error) { + + image, err := c.imagesFacade.GetImage(c.serviceClients.RetryableServiceClient, imageID) + if err != nil { + return "", fmt.Errorf("could not find the image '%s' in OpenStack: %w", imageID, err) + } + if image.Status != images.ImageStatusActive { + return "", fmt.Errorf("image '%s' is not in active state, it is in state: %s", imageID, image.Status) + } + + return image.ID, nil +} + +func (c imageService) UploadImage(imageID string, imageFilePath string) error { + imageData, err := os.ReadFile(imageFilePath) + if err != nil { + return fmt.Errorf("failed to read image file: %w", err) + } + + endpoint := gophercloud.NormalizeURL(c.serviceClients.ServiceClient.Endpoint) + imageURL := endpoint + "v2/images/" + imageID + "/file" + + req, err := c.httpClient.NewRequest("PUT", imageURL, bytes.NewReader(imageData)) + if err != nil { + return fmt.Errorf("failed to create request: %w", err) + } + req.Header.Add("X-Auth-Token", c.serviceClients.ServiceClient.TokenID) + req.Header.Set("Content-Type", "application/octet-stream") + + resp, err := c.httpClient.Do(req) + if err != nil || resp.StatusCode != http.StatusNoContent { + errMessage := "" + if err != nil { + errMessage += fmt.Sprintf("err: %s", err) + } + + if resp.StatusCode != http.StatusNoContent { + defer resp.Body.Close() + bodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("failed to read response body: %w", err) + } + resp.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) + errMessage += fmt.Sprintf("response-status: '%s', response-body:'%s'\n", resp.Status, string(bodyBytes)) + } + return fmt.Errorf("failed to upload stemcell image to %s, %s", imageURL, errMessage) + } + return nil +} + +func (c imageService) DeleteImage(imageID string) error { + err := c.imagesFacade.DeleteImage(c.serviceClients.RetryableServiceClient, imageID) + if err != nil { + return fmt.Errorf("could not delete the image %s, due to the following: %w", imageID, err) + } + return nil +} + +func (c imageService) getImageVisibility(stemcellPubliclyVisible bool) *images.ImageVisibility { + var visibility images.ImageVisibility + if stemcellPubliclyVisible { + visibility = images.ImageVisibilityPublic + } else { + visibility = images.ImageVisibilityPrivate + } + return &visibility +} + +func (c imageService) getProperties(cloudProps properties.CreateStemcell) map[string]string { + properties := make(map[string]string) + properties["version"] = cloudProps.Version + properties["os_type"] = cloudProps.OsType + properties["os_distro"] = cloudProps.OsDistro + properties["architecture"] = cloudProps.Architecture + properties["auto_disk_config"] = strconv.FormatBool(cloudProps.AutoDiskConfig) + properties["hw_vif_model"] = cloudProps.HwVifModel + properties["hypervisor_type"] = cloudProps.Hypervisor + properties["vmware_adaptertype"] = cloudProps.VmwareAdapterType + properties["vmware_disktype"] = cloudProps.VmwareDiskType + properties["vmware_linked_clone"] = cloudProps.VmwareLinkedClone + properties["vmware_ostype"] = cloudProps.VmvareOsType + + // Delete the zero-values + for key, value := range properties { + if value == "" { + delete(properties, key) + } + } + + return properties +} diff --git a/src/openstack_cpi_golang/cpi/image/image_service_builder.go b/src/openstack_cpi_golang/cpi/image/image_service_builder.go new file mode 100644 index 00000000..f914e1fb --- /dev/null +++ b/src/openstack_cpi_golang/cpi/image/image_service_builder.go @@ -0,0 +1,42 @@ +package image + +import ( + "fmt" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/config" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/openstack" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils" +) + +//counterfeiter:generate . ImageServiceBuilder +type ImageServiceBuilder interface { + Build() (ImageService, error) +} + +type imageServiceBuilder struct { + openstackService openstack.OpenstackService + cpiConfig config.CpiConfig + logger utils.Logger +} + +func NewImageServiceBuilder(openstackService openstack.OpenstackService, cpiConfig config.CpiConfig, logger utils.Logger) imageServiceBuilder { + return imageServiceBuilder{ + openstackService: openstackService, + cpiConfig: cpiConfig, + logger: logger, + } +} + +func (b imageServiceBuilder) Build() (ImageService, error) { + serviceClient, err := b.openstackService.ImageServiceV2(b.cpiConfig.OpenStackConfig()) + if err != nil { + return nil, fmt.Errorf("failed to retrieve image service client: %w", err) + } + + return NewImageService( + utils.NewServiceClients(serviceClient, b.cpiConfig, b.logger), + NewImageFacade(), + NewHttpClient(), + b.logger, + ), nil +} diff --git a/src/openstack_cpi_golang/cpi/image/image_service_builder_test.go b/src/openstack_cpi_golang/cpi/image/image_service_builder_test.go new file mode 100644 index 00000000..52c325dd --- /dev/null +++ b/src/openstack_cpi_golang/cpi/image/image_service_builder_test.go @@ -0,0 +1,55 @@ +package image_test + +import ( + "errors" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/config" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/image" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/openstack/openstackfakes" + "github.com/gophercloud/gophercloud" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils/utilsfakes" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("ImageServiceBuilder", func() { + var openstackService openstackfakes.FakeOpenstackService + var logger utilsfakes.FakeLogger + var imageServiceBuilder image.ImageServiceBuilder + + BeforeEach(func() { + openstackService = openstackfakes.FakeOpenstackService{} + logger = utilsfakes.FakeLogger{} + cpiConfig := config.CpiConfig{} + cpiConfig.Cloud.Properties.RetryConfig = config.RetryConfigMap{} + + imageServiceBuilder = image.NewImageServiceBuilder( + &openstackService, + cpiConfig, + &logger, + ) + }) + + Context("Build", func() { + It("returns an image service", func() { + providerClient := gophercloud.ProviderClient{TokenID: "the_token"} + serviceClient := gophercloud.ServiceClient{ProviderClient: &providerClient} + openstackService.ImageServiceV2Returns(&serviceClient, nil) + + computeService, err := imageServiceBuilder.Build() + + Expect(err).ToNot(HaveOccurred()) + Expect(computeService).To(Not(BeNil())) + }) + + It("returns an error if the compute service client cannot be retrieved", func() { + openstackService.ImageServiceV2Returns(nil, errors.New("boom")) + + computeService, err := imageServiceBuilder.Build() + + Expect(err.Error()).To(Equal("failed to retrieve image service client: boom")) + Expect(computeService).To(BeNil()) + }) + }) +}) diff --git a/src/openstack_cpi_golang/cpi/image/image_service_test.go b/src/openstack_cpi_golang/cpi/image/image_service_test.go new file mode 100644 index 00000000..d4479569 --- /dev/null +++ b/src/openstack_cpi_golang/cpi/image/image_service_test.go @@ -0,0 +1,247 @@ +package image_test + +import ( + "errors" + "io" + "net/http" + "strings" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/image/imagefakes" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/config" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/image" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/properties" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils/utilsfakes" + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/imageservice/v2/images" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("ImageService", func() { + var serviceClient gophercloud.ServiceClient + var retryableServiceClient gophercloud.ServiceClient + var serviceClients utils.ServiceClients + var imagesFacade imagefakes.FakeImageFacade + var httpClient imagefakes.FakeHttpClient + var logger utilsfakes.FakeLogger + + BeforeEach(func() { + providerClient := gophercloud.ProviderClient{} + serviceClient = gophercloud.ServiceClient{ProviderClient: &providerClient} + retryableServiceClient = gophercloud.ServiceClient{} + serviceClients = utils.ServiceClients{ServiceClient: &serviceClient, RetryableServiceClient: &retryableServiceClient} + imagesFacade = imagefakes.FakeImageFacade{} + logger = utilsfakes.FakeLogger{} + }) + + Context("CreateImage", func() { + BeforeEach(func() { + + }) + + It("returns the id of the created image entity in OpenStack", func() { + imagesFacade.CreateImageReturns(&images.Image{ID: "123-456"}, nil) + + imageID, err := image.NewImageService(serviceClients, &imagesFacade, &httpClient, &logger). + CreateImage(properties.CreateStemcell{}, config.OpenstackConfig{}) + + Expect(err).ToNot(HaveOccurred()) + Expect(imageID).To(Equal("123-456")) + }) + + It("create an image entity in OpenStack", func() { + imagesFacade.CreateImageReturns(&images.Image{ID: "123-456"}, nil) + + cloudProps := properties.CreateStemcell{ + Name: "the_stemcell_name", + Version: "the_stemcell_version", + DiskFormat: "the_disk_format", + ContainerFormat: "the_container_format", + OsType: "the_os_type", + } + + openstackConfig := config.OpenstackConfig{ + StemcellPubliclyVisible: true, + } + + _, _ = image.NewImageService(serviceClients, &imagesFacade, &httpClient, &logger). + CreateImage(cloudProps, openstackConfig) + + public := images.ImageVisibilityPublic + createOpts := images.CreateOpts{ + Name: "the_stemcell_name/the_stemcell_version", + Visibility: &public, + DiskFormat: "the_disk_format", + ContainerFormat: "the_container_format", + Properties: map[string]string{ + "version": "the_stemcell_version", + "os_type": "the_os_type", + "auto_disk_config": "false", + }, + } + + serviceClient, opts := imagesFacade.CreateImageArgsForCall(0) + Expect(serviceClient).To(Equal(serviceClient)) + Expect(opts).To(Equal(createOpts)) + }) + + It("returns an error if image entity creation in OpenStack fails", func() { + imagesFacade.CreateImageReturns(&images.Image{ID: "123-456"}, errors.New("boom")) + + imageID, err := image.NewImageService(serviceClients, &imagesFacade, &httpClient, &logger). + CreateImage(properties.CreateStemcell{}, config.OpenstackConfig{}) + + Expect(err.Error()).To(Equal("failed to create image: boom")) + Expect(imageID).To(Equal("")) + }) + }) + + Context("GetImage", func() { + BeforeEach(func() { + + }) + + It("returns the id of an existing image entity in OpenStack", func() { + imagesFacade.GetImageReturns(&images.Image{ID: "123-456", Status: "active"}, nil) + + _, _ = image.NewImageService(serviceClients, &imagesFacade, &httpClient, &logger). + GetImage("123-456") + + serviceClient, imageID := imagesFacade.GetImageArgsForCall(0) + Expect(serviceClient).To(Equal(serviceClient)) + Expect(imageID).To(Equal("123-456")) + }) + + It("get an existing image entity in OpenStack", func() { + imagesFacade.GetImageReturns(&images.Image{ID: "123-456", Status: "active"}, nil) + + imageID, err := image.NewImageService(serviceClients, &imagesFacade, &httpClient, &logger). + GetImage("123-456") + + Expect(err).ToNot(HaveOccurred()) + Expect(imageID).To(Equal("123-456")) + }) + + It("returns an error if the image entity cannot be found in OpenStack", func() { + imagesFacade.GetImageReturns(&images.Image{ID: "123-456", Status: "active"}, errors.New("boom")) + + imageID, err := image.NewImageService(serviceClients, &imagesFacade, &httpClient, &logger). + GetImage("123-456") + + Expect(err.Error()).To(Equal("could not find the image '123-456' in OpenStack: boom")) + Expect(imageID).To(Equal("")) + }) + + It("returns an error if the image entity is not active in OpenStack", func() { + imagesFacade.GetImageReturns(&images.Image{ID: "123-456", Status: "not-active"}, nil) + + imageID, err := image.NewImageService(serviceClients, &imagesFacade, &httpClient, &logger). + GetImage("123-456") + + Expect(err.Error()).To(Equal("image '123-456' is not in active state, it is in state: not-active")) + Expect(imageID).To(Equal("")) + }) + + }) + + Context("UploadImage", func() { + BeforeEach(func() { + + }) + + It("succeeds without error", func() { + serviceClient.ProviderClient.TokenID = "token" + header := http.Header{} + request := http.Request{Header: header} + httpClient.NewRequestReturns(&request, nil) + httpClient.DoReturns(&http.Response{StatusCode: 204}, nil) + + err := image.NewImageService(serviceClients, &imagesFacade, &httpClient, &logger). + UploadImage("123-456", "testdata/root.img") + + Expect(err).To(BeNil()) + }) + + It("uploads the image via PUT", func() { + request := http.Request{Header: http.Header{}} + httpClient := imagefakes.FakeHttpClient{} + httpClient.NewRequestReturns(&request, nil) + httpClient.DoReturns(&http.Response{StatusCode: 204}, nil) + + _ = image.NewImageService(serviceClients, &imagesFacade, &httpClient, &logger). + UploadImage("123-456", "testdata/root.img") + + Expect(httpClient.DoCallCount()).To(Equal(1)) + }) + + It("returns an error if the PUT request cannot be created", func() { + httpClient.NewRequestReturns(nil, errors.New("boom")) + + err := image.NewImageService(serviceClients, &imagesFacade, &httpClient, &logger). + UploadImage("123-456", "testdata/root.img") + + Expect(err.Error()).To(Equal("failed to create request: boom")) + }) + + It("returns an error if the PUT request returns an error", func() { + request := http.Request{Header: http.Header{}} + httpClient.NewRequestReturns(&request, nil) + httpClient.DoReturns(&http.Response{StatusCode: 204}, errors.New("boom")) + + err := image.NewImageService(serviceClients, &imagesFacade, &httpClient, &logger). + UploadImage("123-456", "testdata/root.img") + + Expect(err.Error()).To(Equal("failed to upload stemcell image to /v2/images/123-456/file, err: boom")) + }) + + It("returns an error if the PUT request returns status code != 204", func() { + request := http.Request{Header: http.Header{}} + response := &http.Response{StatusCode: 404, Status: "not found", Body: io.NopCloser(strings.NewReader("content"))} + httpClient.NewRequestReturns(&request, nil) + httpClient.DoReturns(response, nil) + + err := image.NewImageService(serviceClients, &imagesFacade, &httpClient, &logger). + UploadImage("123-456", "testdata/root.img") + + Expect(err.Error()).To(Equal("failed to upload stemcell image to /v2/images/123-456/file, response-status: 'not found', response-body:'content'\n")) + }) + }) + + Context("DeleteImage", func() { + BeforeEach(func() { + + }) + + It("deletes an existing image in OpenStack", func() { + imagesFacade.DeleteImageReturns(nil) + + _ = image.NewImageService(serviceClients, &imagesFacade, &httpClient, &logger). + DeleteImage("123-456") + + serviceClient, imageID := imagesFacade.DeleteImageArgsForCall(0) + Expect(serviceClient).To(Equal(serviceClient)) + Expect(imageID).To(Equal("123-456")) + }) + + It("delete an existing image entity in OpenStack without errors", func() { + imagesFacade.DeleteImageReturns(nil) + + err := image.NewImageService(serviceClients, &imagesFacade, &httpClient, &logger). + DeleteImage("123-456") + + Expect(err).ToNot(HaveOccurred()) + Expect(imagesFacade.DeleteImageCallCount()).To(Equal(1)) + }) + + It("returns an error if the image entity cannot be found in OpenStack", func() { + imagesFacade.DeleteImageReturns(errors.New("boom")) + + err := image.NewImageService(serviceClients, &imagesFacade, &httpClient, &logger). + DeleteImage("123-456") + + Expect(err.Error()).To(Equal("could not delete the image 123-456, due to the following: boom")) + }) + }) +}) diff --git a/src/openstack_cpi_golang/cpi/image/image_suite_test.go b/src/openstack_cpi_golang/cpi/image/image_suite_test.go new file mode 100644 index 00000000..d58523df --- /dev/null +++ b/src/openstack_cpi_golang/cpi/image/image_suite_test.go @@ -0,0 +1,13 @@ +package image_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestMethods(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Image Suite") +} diff --git a/src/openstack_cpi_golang/cpi/image/imagefakes/fake_heavy_stemcell_creator.go b/src/openstack_cpi_golang/cpi/image/imagefakes/fake_heavy_stemcell_creator.go new file mode 100644 index 00000000..a7de722b --- /dev/null +++ b/src/openstack_cpi_golang/cpi/image/imagefakes/fake_heavy_stemcell_creator.go @@ -0,0 +1,121 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package imagefakes + +import ( + "sync" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/image" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/properties" +) + +type FakeHeavyStemcellCreator struct { + CreateStub func(image.ImageService, properties.CreateStemcell, string) (string, error) + createMutex sync.RWMutex + createArgsForCall []struct { + arg1 image.ImageService + arg2 properties.CreateStemcell + arg3 string + } + createReturns struct { + result1 string + result2 error + } + createReturnsOnCall map[int]struct { + result1 string + result2 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *FakeHeavyStemcellCreator) Create(arg1 image.ImageService, arg2 properties.CreateStemcell, arg3 string) (string, error) { + fake.createMutex.Lock() + ret, specificReturn := fake.createReturnsOnCall[len(fake.createArgsForCall)] + fake.createArgsForCall = append(fake.createArgsForCall, struct { + arg1 image.ImageService + arg2 properties.CreateStemcell + arg3 string + }{arg1, arg2, arg3}) + stub := fake.CreateStub + fakeReturns := fake.createReturns + fake.recordInvocation("Create", []interface{}{arg1, arg2, arg3}) + fake.createMutex.Unlock() + if stub != nil { + return stub(arg1, arg2, arg3) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeHeavyStemcellCreator) CreateCallCount() int { + fake.createMutex.RLock() + defer fake.createMutex.RUnlock() + return len(fake.createArgsForCall) +} + +func (fake *FakeHeavyStemcellCreator) CreateCalls(stub func(image.ImageService, properties.CreateStemcell, string) (string, error)) { + fake.createMutex.Lock() + defer fake.createMutex.Unlock() + fake.CreateStub = stub +} + +func (fake *FakeHeavyStemcellCreator) CreateArgsForCall(i int) (image.ImageService, properties.CreateStemcell, string) { + fake.createMutex.RLock() + defer fake.createMutex.RUnlock() + argsForCall := fake.createArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 +} + +func (fake *FakeHeavyStemcellCreator) CreateReturns(result1 string, result2 error) { + fake.createMutex.Lock() + defer fake.createMutex.Unlock() + fake.CreateStub = nil + fake.createReturns = struct { + result1 string + result2 error + }{result1, result2} +} + +func (fake *FakeHeavyStemcellCreator) CreateReturnsOnCall(i int, result1 string, result2 error) { + fake.createMutex.Lock() + defer fake.createMutex.Unlock() + fake.CreateStub = nil + if fake.createReturnsOnCall == nil { + fake.createReturnsOnCall = make(map[int]struct { + result1 string + result2 error + }) + } + fake.createReturnsOnCall[i] = struct { + result1 string + result2 error + }{result1, result2} +} + +func (fake *FakeHeavyStemcellCreator) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.createMutex.RLock() + defer fake.createMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *FakeHeavyStemcellCreator) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ image.HeavyStemcellCreator = new(FakeHeavyStemcellCreator) diff --git a/src/openstack_cpi_golang/cpi/image/imagefakes/fake_http_client.go b/src/openstack_cpi_golang/cpi/image/imagefakes/fake_http_client.go new file mode 100644 index 00000000..b3c4ae16 --- /dev/null +++ b/src/openstack_cpi_golang/cpi/image/imagefakes/fake_http_client.go @@ -0,0 +1,201 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package imagefakes + +import ( + "io" + "net/http" + "sync" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/image" +) + +type FakeHttpClient struct { + DoStub func(*http.Request) (*http.Response, error) + doMutex sync.RWMutex + doArgsForCall []struct { + arg1 *http.Request + } + doReturns struct { + result1 *http.Response + result2 error + } + doReturnsOnCall map[int]struct { + result1 *http.Response + result2 error + } + NewRequestStub func(string, string, io.Reader) (*http.Request, error) + newRequestMutex sync.RWMutex + newRequestArgsForCall []struct { + arg1 string + arg2 string + arg3 io.Reader + } + newRequestReturns struct { + result1 *http.Request + result2 error + } + newRequestReturnsOnCall map[int]struct { + result1 *http.Request + result2 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *FakeHttpClient) Do(arg1 *http.Request) (*http.Response, error) { + fake.doMutex.Lock() + ret, specificReturn := fake.doReturnsOnCall[len(fake.doArgsForCall)] + fake.doArgsForCall = append(fake.doArgsForCall, struct { + arg1 *http.Request + }{arg1}) + stub := fake.DoStub + fakeReturns := fake.doReturns + fake.recordInvocation("Do", []interface{}{arg1}) + fake.doMutex.Unlock() + if stub != nil { + return stub(arg1) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeHttpClient) DoCallCount() int { + fake.doMutex.RLock() + defer fake.doMutex.RUnlock() + return len(fake.doArgsForCall) +} + +func (fake *FakeHttpClient) DoCalls(stub func(*http.Request) (*http.Response, error)) { + fake.doMutex.Lock() + defer fake.doMutex.Unlock() + fake.DoStub = stub +} + +func (fake *FakeHttpClient) DoArgsForCall(i int) *http.Request { + fake.doMutex.RLock() + defer fake.doMutex.RUnlock() + argsForCall := fake.doArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeHttpClient) DoReturns(result1 *http.Response, result2 error) { + fake.doMutex.Lock() + defer fake.doMutex.Unlock() + fake.DoStub = nil + fake.doReturns = struct { + result1 *http.Response + result2 error + }{result1, result2} +} + +func (fake *FakeHttpClient) DoReturnsOnCall(i int, result1 *http.Response, result2 error) { + fake.doMutex.Lock() + defer fake.doMutex.Unlock() + fake.DoStub = nil + if fake.doReturnsOnCall == nil { + fake.doReturnsOnCall = make(map[int]struct { + result1 *http.Response + result2 error + }) + } + fake.doReturnsOnCall[i] = struct { + result1 *http.Response + result2 error + }{result1, result2} +} + +func (fake *FakeHttpClient) NewRequest(arg1 string, arg2 string, arg3 io.Reader) (*http.Request, error) { + fake.newRequestMutex.Lock() + ret, specificReturn := fake.newRequestReturnsOnCall[len(fake.newRequestArgsForCall)] + fake.newRequestArgsForCall = append(fake.newRequestArgsForCall, struct { + arg1 string + arg2 string + arg3 io.Reader + }{arg1, arg2, arg3}) + stub := fake.NewRequestStub + fakeReturns := fake.newRequestReturns + fake.recordInvocation("NewRequest", []interface{}{arg1, arg2, arg3}) + fake.newRequestMutex.Unlock() + if stub != nil { + return stub(arg1, arg2, arg3) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeHttpClient) NewRequestCallCount() int { + fake.newRequestMutex.RLock() + defer fake.newRequestMutex.RUnlock() + return len(fake.newRequestArgsForCall) +} + +func (fake *FakeHttpClient) NewRequestCalls(stub func(string, string, io.Reader) (*http.Request, error)) { + fake.newRequestMutex.Lock() + defer fake.newRequestMutex.Unlock() + fake.NewRequestStub = stub +} + +func (fake *FakeHttpClient) NewRequestArgsForCall(i int) (string, string, io.Reader) { + fake.newRequestMutex.RLock() + defer fake.newRequestMutex.RUnlock() + argsForCall := fake.newRequestArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 +} + +func (fake *FakeHttpClient) NewRequestReturns(result1 *http.Request, result2 error) { + fake.newRequestMutex.Lock() + defer fake.newRequestMutex.Unlock() + fake.NewRequestStub = nil + fake.newRequestReturns = struct { + result1 *http.Request + result2 error + }{result1, result2} +} + +func (fake *FakeHttpClient) NewRequestReturnsOnCall(i int, result1 *http.Request, result2 error) { + fake.newRequestMutex.Lock() + defer fake.newRequestMutex.Unlock() + fake.NewRequestStub = nil + if fake.newRequestReturnsOnCall == nil { + fake.newRequestReturnsOnCall = make(map[int]struct { + result1 *http.Request + result2 error + }) + } + fake.newRequestReturnsOnCall[i] = struct { + result1 *http.Request + result2 error + }{result1, result2} +} + +func (fake *FakeHttpClient) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.doMutex.RLock() + defer fake.doMutex.RUnlock() + fake.newRequestMutex.RLock() + defer fake.newRequestMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *FakeHttpClient) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ image.HttpClient = new(FakeHttpClient) diff --git a/src/openstack_cpi_golang/cpi/image/imagefakes/fake_image_facade.go b/src/openstack_cpi_golang/cpi/image/imagefakes/fake_image_facade.go new file mode 100644 index 00000000..8c82f770 --- /dev/null +++ b/src/openstack_cpi_golang/cpi/image/imagefakes/fake_image_facade.go @@ -0,0 +1,277 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package imagefakes + +import ( + "sync" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/image" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils" + "github.com/gophercloud/gophercloud/openstack/imageservice/v2/images" +) + +type FakeImageFacade struct { + CreateImageStub func(utils.ServiceClient, images.CreateOptsBuilder) (*images.Image, error) + createImageMutex sync.RWMutex + createImageArgsForCall []struct { + arg1 utils.ServiceClient + arg2 images.CreateOptsBuilder + } + createImageReturns struct { + result1 *images.Image + result2 error + } + createImageReturnsOnCall map[int]struct { + result1 *images.Image + result2 error + } + DeleteImageStub func(utils.RetryableServiceClient, string) error + deleteImageMutex sync.RWMutex + deleteImageArgsForCall []struct { + arg1 utils.RetryableServiceClient + arg2 string + } + deleteImageReturns struct { + result1 error + } + deleteImageReturnsOnCall map[int]struct { + result1 error + } + GetImageStub func(utils.RetryableServiceClient, string) (*images.Image, error) + getImageMutex sync.RWMutex + getImageArgsForCall []struct { + arg1 utils.RetryableServiceClient + arg2 string + } + getImageReturns struct { + result1 *images.Image + result2 error + } + getImageReturnsOnCall map[int]struct { + result1 *images.Image + result2 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *FakeImageFacade) CreateImage(arg1 utils.ServiceClient, arg2 images.CreateOptsBuilder) (*images.Image, error) { + fake.createImageMutex.Lock() + ret, specificReturn := fake.createImageReturnsOnCall[len(fake.createImageArgsForCall)] + fake.createImageArgsForCall = append(fake.createImageArgsForCall, struct { + arg1 utils.ServiceClient + arg2 images.CreateOptsBuilder + }{arg1, arg2}) + stub := fake.CreateImageStub + fakeReturns := fake.createImageReturns + fake.recordInvocation("CreateImage", []interface{}{arg1, arg2}) + fake.createImageMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeImageFacade) CreateImageCallCount() int { + fake.createImageMutex.RLock() + defer fake.createImageMutex.RUnlock() + return len(fake.createImageArgsForCall) +} + +func (fake *FakeImageFacade) CreateImageCalls(stub func(utils.ServiceClient, images.CreateOptsBuilder) (*images.Image, error)) { + fake.createImageMutex.Lock() + defer fake.createImageMutex.Unlock() + fake.CreateImageStub = stub +} + +func (fake *FakeImageFacade) CreateImageArgsForCall(i int) (utils.ServiceClient, images.CreateOptsBuilder) { + fake.createImageMutex.RLock() + defer fake.createImageMutex.RUnlock() + argsForCall := fake.createImageArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeImageFacade) CreateImageReturns(result1 *images.Image, result2 error) { + fake.createImageMutex.Lock() + defer fake.createImageMutex.Unlock() + fake.CreateImageStub = nil + fake.createImageReturns = struct { + result1 *images.Image + result2 error + }{result1, result2} +} + +func (fake *FakeImageFacade) CreateImageReturnsOnCall(i int, result1 *images.Image, result2 error) { + fake.createImageMutex.Lock() + defer fake.createImageMutex.Unlock() + fake.CreateImageStub = nil + if fake.createImageReturnsOnCall == nil { + fake.createImageReturnsOnCall = make(map[int]struct { + result1 *images.Image + result2 error + }) + } + fake.createImageReturnsOnCall[i] = struct { + result1 *images.Image + result2 error + }{result1, result2} +} + +func (fake *FakeImageFacade) DeleteImage(arg1 utils.RetryableServiceClient, arg2 string) error { + fake.deleteImageMutex.Lock() + ret, specificReturn := fake.deleteImageReturnsOnCall[len(fake.deleteImageArgsForCall)] + fake.deleteImageArgsForCall = append(fake.deleteImageArgsForCall, struct { + arg1 utils.RetryableServiceClient + arg2 string + }{arg1, arg2}) + stub := fake.DeleteImageStub + fakeReturns := fake.deleteImageReturns + fake.recordInvocation("DeleteImage", []interface{}{arg1, arg2}) + fake.deleteImageMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeImageFacade) DeleteImageCallCount() int { + fake.deleteImageMutex.RLock() + defer fake.deleteImageMutex.RUnlock() + return len(fake.deleteImageArgsForCall) +} + +func (fake *FakeImageFacade) DeleteImageCalls(stub func(utils.RetryableServiceClient, string) error) { + fake.deleteImageMutex.Lock() + defer fake.deleteImageMutex.Unlock() + fake.DeleteImageStub = stub +} + +func (fake *FakeImageFacade) DeleteImageArgsForCall(i int) (utils.RetryableServiceClient, string) { + fake.deleteImageMutex.RLock() + defer fake.deleteImageMutex.RUnlock() + argsForCall := fake.deleteImageArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeImageFacade) DeleteImageReturns(result1 error) { + fake.deleteImageMutex.Lock() + defer fake.deleteImageMutex.Unlock() + fake.DeleteImageStub = nil + fake.deleteImageReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeImageFacade) DeleteImageReturnsOnCall(i int, result1 error) { + fake.deleteImageMutex.Lock() + defer fake.deleteImageMutex.Unlock() + fake.DeleteImageStub = nil + if fake.deleteImageReturnsOnCall == nil { + fake.deleteImageReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.deleteImageReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *FakeImageFacade) GetImage(arg1 utils.RetryableServiceClient, arg2 string) (*images.Image, error) { + fake.getImageMutex.Lock() + ret, specificReturn := fake.getImageReturnsOnCall[len(fake.getImageArgsForCall)] + fake.getImageArgsForCall = append(fake.getImageArgsForCall, struct { + arg1 utils.RetryableServiceClient + arg2 string + }{arg1, arg2}) + stub := fake.GetImageStub + fakeReturns := fake.getImageReturns + fake.recordInvocation("GetImage", []interface{}{arg1, arg2}) + fake.getImageMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeImageFacade) GetImageCallCount() int { + fake.getImageMutex.RLock() + defer fake.getImageMutex.RUnlock() + return len(fake.getImageArgsForCall) +} + +func (fake *FakeImageFacade) GetImageCalls(stub func(utils.RetryableServiceClient, string) (*images.Image, error)) { + fake.getImageMutex.Lock() + defer fake.getImageMutex.Unlock() + fake.GetImageStub = stub +} + +func (fake *FakeImageFacade) GetImageArgsForCall(i int) (utils.RetryableServiceClient, string) { + fake.getImageMutex.RLock() + defer fake.getImageMutex.RUnlock() + argsForCall := fake.getImageArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeImageFacade) GetImageReturns(result1 *images.Image, result2 error) { + fake.getImageMutex.Lock() + defer fake.getImageMutex.Unlock() + fake.GetImageStub = nil + fake.getImageReturns = struct { + result1 *images.Image + result2 error + }{result1, result2} +} + +func (fake *FakeImageFacade) GetImageReturnsOnCall(i int, result1 *images.Image, result2 error) { + fake.getImageMutex.Lock() + defer fake.getImageMutex.Unlock() + fake.GetImageStub = nil + if fake.getImageReturnsOnCall == nil { + fake.getImageReturnsOnCall = make(map[int]struct { + result1 *images.Image + result2 error + }) + } + fake.getImageReturnsOnCall[i] = struct { + result1 *images.Image + result2 error + }{result1, result2} +} + +func (fake *FakeImageFacade) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.createImageMutex.RLock() + defer fake.createImageMutex.RUnlock() + fake.deleteImageMutex.RLock() + defer fake.deleteImageMutex.RUnlock() + fake.getImageMutex.RLock() + defer fake.getImageMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *FakeImageFacade) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ image.ImageFacade = new(FakeImageFacade) diff --git a/src/openstack_cpi_golang/cpi/image/imagefakes/fake_image_service.go b/src/openstack_cpi_golang/cpi/image/imagefakes/fake_image_service.go new file mode 100644 index 00000000..d72c3b25 --- /dev/null +++ b/src/openstack_cpi_golang/cpi/image/imagefakes/fake_image_service.go @@ -0,0 +1,349 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package imagefakes + +import ( + "sync" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/config" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/image" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/properties" +) + +type FakeImageService struct { + CreateImageStub func(properties.CreateStemcell, config.OpenstackConfig) (string, error) + createImageMutex sync.RWMutex + createImageArgsForCall []struct { + arg1 properties.CreateStemcell + arg2 config.OpenstackConfig + } + createImageReturns struct { + result1 string + result2 error + } + createImageReturnsOnCall map[int]struct { + result1 string + result2 error + } + DeleteImageStub func(string) error + deleteImageMutex sync.RWMutex + deleteImageArgsForCall []struct { + arg1 string + } + deleteImageReturns struct { + result1 error + } + deleteImageReturnsOnCall map[int]struct { + result1 error + } + GetImageStub func(string) (string, error) + getImageMutex sync.RWMutex + getImageArgsForCall []struct { + arg1 string + } + getImageReturns struct { + result1 string + result2 error + } + getImageReturnsOnCall map[int]struct { + result1 string + result2 error + } + UploadImageStub func(string, string) error + uploadImageMutex sync.RWMutex + uploadImageArgsForCall []struct { + arg1 string + arg2 string + } + uploadImageReturns struct { + result1 error + } + uploadImageReturnsOnCall map[int]struct { + result1 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *FakeImageService) CreateImage(arg1 properties.CreateStemcell, arg2 config.OpenstackConfig) (string, error) { + fake.createImageMutex.Lock() + ret, specificReturn := fake.createImageReturnsOnCall[len(fake.createImageArgsForCall)] + fake.createImageArgsForCall = append(fake.createImageArgsForCall, struct { + arg1 properties.CreateStemcell + arg2 config.OpenstackConfig + }{arg1, arg2}) + stub := fake.CreateImageStub + fakeReturns := fake.createImageReturns + fake.recordInvocation("CreateImage", []interface{}{arg1, arg2}) + fake.createImageMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeImageService) CreateImageCallCount() int { + fake.createImageMutex.RLock() + defer fake.createImageMutex.RUnlock() + return len(fake.createImageArgsForCall) +} + +func (fake *FakeImageService) CreateImageCalls(stub func(properties.CreateStemcell, config.OpenstackConfig) (string, error)) { + fake.createImageMutex.Lock() + defer fake.createImageMutex.Unlock() + fake.CreateImageStub = stub +} + +func (fake *FakeImageService) CreateImageArgsForCall(i int) (properties.CreateStemcell, config.OpenstackConfig) { + fake.createImageMutex.RLock() + defer fake.createImageMutex.RUnlock() + argsForCall := fake.createImageArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeImageService) CreateImageReturns(result1 string, result2 error) { + fake.createImageMutex.Lock() + defer fake.createImageMutex.Unlock() + fake.CreateImageStub = nil + fake.createImageReturns = struct { + result1 string + result2 error + }{result1, result2} +} + +func (fake *FakeImageService) CreateImageReturnsOnCall(i int, result1 string, result2 error) { + fake.createImageMutex.Lock() + defer fake.createImageMutex.Unlock() + fake.CreateImageStub = nil + if fake.createImageReturnsOnCall == nil { + fake.createImageReturnsOnCall = make(map[int]struct { + result1 string + result2 error + }) + } + fake.createImageReturnsOnCall[i] = struct { + result1 string + result2 error + }{result1, result2} +} + +func (fake *FakeImageService) DeleteImage(arg1 string) error { + fake.deleteImageMutex.Lock() + ret, specificReturn := fake.deleteImageReturnsOnCall[len(fake.deleteImageArgsForCall)] + fake.deleteImageArgsForCall = append(fake.deleteImageArgsForCall, struct { + arg1 string + }{arg1}) + stub := fake.DeleteImageStub + fakeReturns := fake.deleteImageReturns + fake.recordInvocation("DeleteImage", []interface{}{arg1}) + fake.deleteImageMutex.Unlock() + if stub != nil { + return stub(arg1) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeImageService) DeleteImageCallCount() int { + fake.deleteImageMutex.RLock() + defer fake.deleteImageMutex.RUnlock() + return len(fake.deleteImageArgsForCall) +} + +func (fake *FakeImageService) DeleteImageCalls(stub func(string) error) { + fake.deleteImageMutex.Lock() + defer fake.deleteImageMutex.Unlock() + fake.DeleteImageStub = stub +} + +func (fake *FakeImageService) DeleteImageArgsForCall(i int) string { + fake.deleteImageMutex.RLock() + defer fake.deleteImageMutex.RUnlock() + argsForCall := fake.deleteImageArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeImageService) DeleteImageReturns(result1 error) { + fake.deleteImageMutex.Lock() + defer fake.deleteImageMutex.Unlock() + fake.DeleteImageStub = nil + fake.deleteImageReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeImageService) DeleteImageReturnsOnCall(i int, result1 error) { + fake.deleteImageMutex.Lock() + defer fake.deleteImageMutex.Unlock() + fake.DeleteImageStub = nil + if fake.deleteImageReturnsOnCall == nil { + fake.deleteImageReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.deleteImageReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *FakeImageService) GetImage(arg1 string) (string, error) { + fake.getImageMutex.Lock() + ret, specificReturn := fake.getImageReturnsOnCall[len(fake.getImageArgsForCall)] + fake.getImageArgsForCall = append(fake.getImageArgsForCall, struct { + arg1 string + }{arg1}) + stub := fake.GetImageStub + fakeReturns := fake.getImageReturns + fake.recordInvocation("GetImage", []interface{}{arg1}) + fake.getImageMutex.Unlock() + if stub != nil { + return stub(arg1) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeImageService) GetImageCallCount() int { + fake.getImageMutex.RLock() + defer fake.getImageMutex.RUnlock() + return len(fake.getImageArgsForCall) +} + +func (fake *FakeImageService) GetImageCalls(stub func(string) (string, error)) { + fake.getImageMutex.Lock() + defer fake.getImageMutex.Unlock() + fake.GetImageStub = stub +} + +func (fake *FakeImageService) GetImageArgsForCall(i int) string { + fake.getImageMutex.RLock() + defer fake.getImageMutex.RUnlock() + argsForCall := fake.getImageArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeImageService) GetImageReturns(result1 string, result2 error) { + fake.getImageMutex.Lock() + defer fake.getImageMutex.Unlock() + fake.GetImageStub = nil + fake.getImageReturns = struct { + result1 string + result2 error + }{result1, result2} +} + +func (fake *FakeImageService) GetImageReturnsOnCall(i int, result1 string, result2 error) { + fake.getImageMutex.Lock() + defer fake.getImageMutex.Unlock() + fake.GetImageStub = nil + if fake.getImageReturnsOnCall == nil { + fake.getImageReturnsOnCall = make(map[int]struct { + result1 string + result2 error + }) + } + fake.getImageReturnsOnCall[i] = struct { + result1 string + result2 error + }{result1, result2} +} + +func (fake *FakeImageService) UploadImage(arg1 string, arg2 string) error { + fake.uploadImageMutex.Lock() + ret, specificReturn := fake.uploadImageReturnsOnCall[len(fake.uploadImageArgsForCall)] + fake.uploadImageArgsForCall = append(fake.uploadImageArgsForCall, struct { + arg1 string + arg2 string + }{arg1, arg2}) + stub := fake.UploadImageStub + fakeReturns := fake.uploadImageReturns + fake.recordInvocation("UploadImage", []interface{}{arg1, arg2}) + fake.uploadImageMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeImageService) UploadImageCallCount() int { + fake.uploadImageMutex.RLock() + defer fake.uploadImageMutex.RUnlock() + return len(fake.uploadImageArgsForCall) +} + +func (fake *FakeImageService) UploadImageCalls(stub func(string, string) error) { + fake.uploadImageMutex.Lock() + defer fake.uploadImageMutex.Unlock() + fake.UploadImageStub = stub +} + +func (fake *FakeImageService) UploadImageArgsForCall(i int) (string, string) { + fake.uploadImageMutex.RLock() + defer fake.uploadImageMutex.RUnlock() + argsForCall := fake.uploadImageArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeImageService) UploadImageReturns(result1 error) { + fake.uploadImageMutex.Lock() + defer fake.uploadImageMutex.Unlock() + fake.UploadImageStub = nil + fake.uploadImageReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeImageService) UploadImageReturnsOnCall(i int, result1 error) { + fake.uploadImageMutex.Lock() + defer fake.uploadImageMutex.Unlock() + fake.UploadImageStub = nil + if fake.uploadImageReturnsOnCall == nil { + fake.uploadImageReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.uploadImageReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *FakeImageService) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.createImageMutex.RLock() + defer fake.createImageMutex.RUnlock() + fake.deleteImageMutex.RLock() + defer fake.deleteImageMutex.RUnlock() + fake.getImageMutex.RLock() + defer fake.getImageMutex.RUnlock() + fake.uploadImageMutex.RLock() + defer fake.uploadImageMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *FakeImageService) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ image.ImageService = new(FakeImageService) diff --git a/src/openstack_cpi_golang/cpi/image/imagefakes/fake_image_service_builder.go b/src/openstack_cpi_golang/cpi/image/imagefakes/fake_image_service_builder.go new file mode 100644 index 00000000..4b3d3aa1 --- /dev/null +++ b/src/openstack_cpi_golang/cpi/image/imagefakes/fake_image_service_builder.go @@ -0,0 +1,107 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package imagefakes + +import ( + "sync" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/image" +) + +type FakeImageServiceBuilder struct { + BuildStub func() (image.ImageService, error) + buildMutex sync.RWMutex + buildArgsForCall []struct { + } + buildReturns struct { + result1 image.ImageService + result2 error + } + buildReturnsOnCall map[int]struct { + result1 image.ImageService + result2 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *FakeImageServiceBuilder) Build() (image.ImageService, error) { + fake.buildMutex.Lock() + ret, specificReturn := fake.buildReturnsOnCall[len(fake.buildArgsForCall)] + fake.buildArgsForCall = append(fake.buildArgsForCall, struct { + }{}) + stub := fake.BuildStub + fakeReturns := fake.buildReturns + fake.recordInvocation("Build", []interface{}{}) + fake.buildMutex.Unlock() + if stub != nil { + return stub() + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeImageServiceBuilder) BuildCallCount() int { + fake.buildMutex.RLock() + defer fake.buildMutex.RUnlock() + return len(fake.buildArgsForCall) +} + +func (fake *FakeImageServiceBuilder) BuildCalls(stub func() (image.ImageService, error)) { + fake.buildMutex.Lock() + defer fake.buildMutex.Unlock() + fake.BuildStub = stub +} + +func (fake *FakeImageServiceBuilder) BuildReturns(result1 image.ImageService, result2 error) { + fake.buildMutex.Lock() + defer fake.buildMutex.Unlock() + fake.BuildStub = nil + fake.buildReturns = struct { + result1 image.ImageService + result2 error + }{result1, result2} +} + +func (fake *FakeImageServiceBuilder) BuildReturnsOnCall(i int, result1 image.ImageService, result2 error) { + fake.buildMutex.Lock() + defer fake.buildMutex.Unlock() + fake.BuildStub = nil + if fake.buildReturnsOnCall == nil { + fake.buildReturnsOnCall = make(map[int]struct { + result1 image.ImageService + result2 error + }) + } + fake.buildReturnsOnCall[i] = struct { + result1 image.ImageService + result2 error + }{result1, result2} +} + +func (fake *FakeImageServiceBuilder) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.buildMutex.RLock() + defer fake.buildMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *FakeImageServiceBuilder) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ image.ImageServiceBuilder = new(FakeImageServiceBuilder) diff --git a/src/openstack_cpi_golang/cpi/image/imagefakes/fake_light_stemcell_creator.go b/src/openstack_cpi_golang/cpi/image/imagefakes/fake_light_stemcell_creator.go new file mode 100644 index 00000000..d6a0592f --- /dev/null +++ b/src/openstack_cpi_golang/cpi/image/imagefakes/fake_light_stemcell_creator.go @@ -0,0 +1,119 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package imagefakes + +import ( + "sync" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/image" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/properties" +) + +type FakeLightStemcellCreator struct { + CreateStub func(image.ImageService, properties.CreateStemcell) (string, error) + createMutex sync.RWMutex + createArgsForCall []struct { + arg1 image.ImageService + arg2 properties.CreateStemcell + } + createReturns struct { + result1 string + result2 error + } + createReturnsOnCall map[int]struct { + result1 string + result2 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *FakeLightStemcellCreator) Create(arg1 image.ImageService, arg2 properties.CreateStemcell) (string, error) { + fake.createMutex.Lock() + ret, specificReturn := fake.createReturnsOnCall[len(fake.createArgsForCall)] + fake.createArgsForCall = append(fake.createArgsForCall, struct { + arg1 image.ImageService + arg2 properties.CreateStemcell + }{arg1, arg2}) + stub := fake.CreateStub + fakeReturns := fake.createReturns + fake.recordInvocation("Create", []interface{}{arg1, arg2}) + fake.createMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeLightStemcellCreator) CreateCallCount() int { + fake.createMutex.RLock() + defer fake.createMutex.RUnlock() + return len(fake.createArgsForCall) +} + +func (fake *FakeLightStemcellCreator) CreateCalls(stub func(image.ImageService, properties.CreateStemcell) (string, error)) { + fake.createMutex.Lock() + defer fake.createMutex.Unlock() + fake.CreateStub = stub +} + +func (fake *FakeLightStemcellCreator) CreateArgsForCall(i int) (image.ImageService, properties.CreateStemcell) { + fake.createMutex.RLock() + defer fake.createMutex.RUnlock() + argsForCall := fake.createArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeLightStemcellCreator) CreateReturns(result1 string, result2 error) { + fake.createMutex.Lock() + defer fake.createMutex.Unlock() + fake.CreateStub = nil + fake.createReturns = struct { + result1 string + result2 error + }{result1, result2} +} + +func (fake *FakeLightStemcellCreator) CreateReturnsOnCall(i int, result1 string, result2 error) { + fake.createMutex.Lock() + defer fake.createMutex.Unlock() + fake.CreateStub = nil + if fake.createReturnsOnCall == nil { + fake.createReturnsOnCall = make(map[int]struct { + result1 string + result2 error + }) + } + fake.createReturnsOnCall[i] = struct { + result1 string + result2 error + }{result1, result2} +} + +func (fake *FakeLightStemcellCreator) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.createMutex.RLock() + defer fake.createMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *FakeLightStemcellCreator) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ image.LightStemcellCreator = new(FakeLightStemcellCreator) diff --git a/src/openstack_cpi_golang/cpi/image/light_stemcell_creator.go b/src/openstack_cpi_golang/cpi/image/light_stemcell_creator.go new file mode 100644 index 00000000..b88b9667 --- /dev/null +++ b/src/openstack_cpi_golang/cpi/image/light_stemcell_creator.go @@ -0,0 +1,42 @@ +package image + +import ( + "fmt" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/config" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/properties" +) + +//counterfeiter:generate . LightStemcellCreator +type LightStemcellCreator interface { + Create( + imageService ImageService, + cloudProps properties.CreateStemcell, + ) (string, error) +} + +type lightStemcellCreator struct { + config config.OpenstackConfig +} + +func NewLightStemcellCreator( + config config.OpenstackConfig, +) lightStemcellCreator { + return lightStemcellCreator{ + config: config, + } +} + +func (h lightStemcellCreator) Create( + imageService ImageService, + cloudProps properties.CreateStemcell, +) (string, error) { + imageID, err := imageService.GetImage(cloudProps.ImageID) + if err != nil { + if err != nil { + return "", fmt.Errorf("failed to retrieve image: %w", err) + } + } + + return imageID, nil +} diff --git a/src/openstack_cpi_golang/cpi/image/light_stemcell_creator_test.go b/src/openstack_cpi_golang/cpi/image/light_stemcell_creator_test.go new file mode 100644 index 00000000..051a1d9f --- /dev/null +++ b/src/openstack_cpi_golang/cpi/image/light_stemcell_creator_test.go @@ -0,0 +1,46 @@ +package image_test + +import ( + "fmt" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/config" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/image" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/image/imagefakes" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/properties" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("lightStemcellCreator", func() { + Context("Create", func() { + It("returns the stemcell id of an existing image", func() { + imageServiceClient := imagefakes.FakeImageService{} + imageServiceClient.GetImageReturns("1234", nil) + subject := image.NewLightStemcellCreator(config.OpenstackConfig{}) + imageID, err := subject.Create(&imageServiceClient, properties.CreateStemcell{}) + + Expect(err).ToNot(HaveOccurred()) + Expect(imageID).To(Equal("1234")) + }) + + It("returns an error if no image can be found", func() { + imageServiceClient := imagefakes.FakeImageService{} + imageServiceClient.GetImageReturns("", fmt.Errorf("boom")) + subject := image.NewLightStemcellCreator(config.OpenstackConfig{}) + imageID, err := subject.Create(&imageServiceClient, properties.CreateStemcell{}) + + Expect(err.Error()).To(Equal("failed to retrieve image: boom")) + Expect(imageID).To(Equal("")) + }) + + It("gets an image via imageID", func() { + imageServiceClient := imagefakes.FakeImageService{} + + subject := image.NewLightStemcellCreator(config.OpenstackConfig{}) + _, _ = subject.Create(&imageServiceClient, properties.CreateStemcell{ImageID: "123-456"}) + + imageID := imageServiceClient.GetImageArgsForCall(0) + Expect(imageID).To(Equal("123-456")) + }) + }) +}) diff --git a/src/openstack_cpi_golang/cpi/image/root_image/root_image.go b/src/openstack_cpi_golang/cpi/image/root_image/root_image.go new file mode 100644 index 00000000..487d560b --- /dev/null +++ b/src/openstack_cpi_golang/cpi/image/root_image/root_image.go @@ -0,0 +1,35 @@ +package root_image + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" +) + +//counterfeiter:generate . RootImage +type RootImage interface { + Get(stemcellImagePath string, targetDirPath string) (string, error) +} + +type rootImage struct { +} + +func NewRootImage() rootImage { + return rootImage{} +} + +func (h rootImage) Get(stemcellImagePath string, targetDirPath string) (string, error) { + cmd := exec.Command("tar", "-C", targetDirPath, "-xzf", stemcellImagePath) + out, err := cmd.CombinedOutput() + if err != nil { + return "", fmt.Errorf("failed to extract stemcell root image to %s, tar returned %s, error: %s", targetDirPath, out, err) + } + + rootImagePath := filepath.Join(targetDirPath, "root.img") + if _, err := os.Stat(rootImagePath); os.IsNotExist(err) { + return "", fmt.Errorf("root image is missing from stemcell archive") + } + + return rootImagePath, nil +} diff --git a/src/openstack_cpi_golang/cpi/image/root_image/root_image_suite_test.go b/src/openstack_cpi_golang/cpi/image/root_image/root_image_suite_test.go new file mode 100644 index 00000000..3e65482d --- /dev/null +++ b/src/openstack_cpi_golang/cpi/image/root_image/root_image_suite_test.go @@ -0,0 +1,13 @@ +package root_image_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestMethods(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "RootImage Suite") +} diff --git a/src/openstack_cpi_golang/cpi/image/root_image/root_image_test.go b/src/openstack_cpi_golang/cpi/image/root_image/root_image_test.go new file mode 100644 index 00000000..1d388fba --- /dev/null +++ b/src/openstack_cpi_golang/cpi/image/root_image/root_image_test.go @@ -0,0 +1,50 @@ +package root_image + +import ( + "os" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("RootImage", func() { + var targetDirPath string + + Context("Get", func() { + BeforeEach(func() { + targetDirPath, _ = os.MkdirTemp("/tmp", "unpacked-image-") + }) + + AfterEach(func() { + os.RemoveAll(targetDirPath) + }) + + It("returns the root.img path", func() { + rootImagePath, err := NewRootImage().Get("testdata/image", targetDirPath) + + Expect(err).ToNot(HaveOccurred()) + Expect(rootImagePath).To(MatchRegexp("/tmp/unpacked-image-[0-9]+/root.img")) + }) + + It("fails if the root.img cannot be found", func() { + rootImagePath, err := NewRootImage().Get("testdata/image_without_root_img", targetDirPath) + + Expect(err.Error()).To(Equal("root image is missing from stemcell archive")) + Expect(rootImagePath).To(Equal("")) + }) + + It("fails if the image cannot be extracted", func() { + rootImagePath, err := NewRootImage().Get("testdata/image_that_is_no_tar", targetDirPath) + + Expect(err.Error()).To(MatchRegexp("failed to extract stemcell root image to .*")) + Expect(rootImagePath).To(Equal("")) + }) + + It("fails if the image cannot be found", func() { + rootImagePath, err := NewRootImage().Get("testdata/image_not_existing", targetDirPath) + + Expect(err.Error()).To(MatchRegexp("failed to extract stemcell root image to .*")) + Expect(rootImagePath).To(Equal("")) + }) + }) +}) diff --git a/src/openstack_cpi_golang/cpi/image/root_image/root_imagefakes/fake_root_image.go b/src/openstack_cpi_golang/cpi/image/root_image/root_imagefakes/fake_root_image.go new file mode 100644 index 00000000..23130779 --- /dev/null +++ b/src/openstack_cpi_golang/cpi/image/root_image/root_imagefakes/fake_root_image.go @@ -0,0 +1,118 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package root_imagefakes + +import ( + "sync" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/image/root_image" +) + +type FakeRootImage struct { + GetStub func(string, string) (string, error) + getMutex sync.RWMutex + getArgsForCall []struct { + arg1 string + arg2 string + } + getReturns struct { + result1 string + result2 error + } + getReturnsOnCall map[int]struct { + result1 string + result2 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *FakeRootImage) Get(arg1 string, arg2 string) (string, error) { + fake.getMutex.Lock() + ret, specificReturn := fake.getReturnsOnCall[len(fake.getArgsForCall)] + fake.getArgsForCall = append(fake.getArgsForCall, struct { + arg1 string + arg2 string + }{arg1, arg2}) + stub := fake.GetStub + fakeReturns := fake.getReturns + fake.recordInvocation("Get", []interface{}{arg1, arg2}) + fake.getMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeRootImage) GetCallCount() int { + fake.getMutex.RLock() + defer fake.getMutex.RUnlock() + return len(fake.getArgsForCall) +} + +func (fake *FakeRootImage) GetCalls(stub func(string, string) (string, error)) { + fake.getMutex.Lock() + defer fake.getMutex.Unlock() + fake.GetStub = stub +} + +func (fake *FakeRootImage) GetArgsForCall(i int) (string, string) { + fake.getMutex.RLock() + defer fake.getMutex.RUnlock() + argsForCall := fake.getArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeRootImage) GetReturns(result1 string, result2 error) { + fake.getMutex.Lock() + defer fake.getMutex.Unlock() + fake.GetStub = nil + fake.getReturns = struct { + result1 string + result2 error + }{result1, result2} +} + +func (fake *FakeRootImage) GetReturnsOnCall(i int, result1 string, result2 error) { + fake.getMutex.Lock() + defer fake.getMutex.Unlock() + fake.GetStub = nil + if fake.getReturnsOnCall == nil { + fake.getReturnsOnCall = make(map[int]struct { + result1 string + result2 error + }) + } + fake.getReturnsOnCall[i] = struct { + result1 string + result2 error + }{result1, result2} +} + +func (fake *FakeRootImage) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.getMutex.RLock() + defer fake.getMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *FakeRootImage) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ root_image.RootImage = new(FakeRootImage) diff --git a/src/openstack_cpi_golang/cpi/image/root_image/testdata/image b/src/openstack_cpi_golang/cpi/image/root_image/testdata/image new file mode 100644 index 0000000000000000000000000000000000000000..a40e6f7ab09c8217960db3303cbbe4a31766b9bb GIT binary patch literal 352 zcmV-m0iXUKiwFSSsw-vy1JjEy%Fi#+%gjw@pcybQFfcPQQ2^2AW~N};zzD)OfUu1f z3{A`ojm(Wq%uUP{41g?iGX@0%+BpdUN{dSpi-1mXF)%SPG&aVm4yY_G4UahxuPDIj zp#oUN*cq4@7GC?|K05Lb35=Ml$ zXmWn8USdH(PO4r(QGQu!USeKyDoDYdyqRiCfAhVef+t4-E)bAd;gOn{l3JusyK;bF z{x>v0%l{^(MkWjj71VbMCK%!Q-`GGQw2Ll6rX1%_Ov<_bQZ zPL4s&9-d*YdKHN!B}IDJva79QprfO)yIGW1VT8F$X*rjPr9v=JlTRc*O-Om1YoNnn yqypO4{)F>CFu$YM|E8vRB!U=)mk!36+$Gq(N!5C8yIfs%Cq literal 0 HcmV?d00001 diff --git a/src/openstack_cpi_golang/.gitignore b/src/openstack_cpi_golang/cpi/image/root_image/testdata/image_that_is_not_tar similarity index 100% rename from src/openstack_cpi_golang/.gitignore rename to src/openstack_cpi_golang/cpi/image/root_image/testdata/image_that_is_not_tar diff --git a/src/openstack_cpi_golang/cpi/image/root_image/testdata/image_without_root_img b/src/openstack_cpi_golang/cpi/image/root_image/testdata/image_without_root_img new file mode 100644 index 0000000000000000000000000000000000000000..5a17ce35a06dd0cd18fb97af1df682cd446088a4 GIT binary patch literal 335 zcmV-V0kHlbiwFP_mM~@j1JjGIV4yWHFfcGPF;M`~=4Pf~+Q10HH-NB>6%0+x3{4CT zjZ6%U6$}i4JTnFb1Dd-G2uh1f5{rONaWOCf+HH(e9Z*?X8Xj{XUQvM4Lj|yku`@6+ zF!&}WEBFU1L?}Q3NQ{9Kh>d`_5Qsti#l#uu7!nc$WrB3f0Ag-5C5#Ah(d7JGy~Kio zoK(GnqWrSdyu`fZRFHx@c{A0P{^ol_1y7CwTp%E^!Xq^?CACPOcI5!U{BLN0mj8{7 z3{4mmXkRihjQIR-Y@m=^l9`)oYiMq0Vq$7(X=tivX=!9`Y5?SOnVKv3cse--IeU19 zx$0FUmXs9fVau+zj)9Jj#_ncOUWF0nE~VvMCYB1pKutc8_%tEqajt<5hmi{Ck;e$- he^7owtN)Em3`Xn!Q7{Td!6=|5003)8CHnvl008`Wj6(nb literal 0 HcmV?d00001 diff --git a/src/openstack_cpi_golang/cpi/image/testdata/root.img b/src/openstack_cpi_golang/cpi/image/testdata/root.img new file mode 100644 index 00000000..cf7b0186 --- /dev/null +++ b/src/openstack_cpi_golang/cpi/image/testdata/root.img @@ -0,0 +1 @@ +the root.img content \ No newline at end of file diff --git a/src/openstack_cpi_golang/cpi/loadbalancer/loadbalancer_facade.go b/src/openstack_cpi_golang/cpi/loadbalancer/loadbalancer_facade.go new file mode 100644 index 00000000..60614c8a --- /dev/null +++ b/src/openstack_cpi_golang/cpi/loadbalancer/loadbalancer_facade.go @@ -0,0 +1,73 @@ +package loadbalancer + +import ( + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils" + "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/listeners" + "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/loadbalancers" + "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/pools" + "github.com/gophercloud/gophercloud/pagination" +) + +//counterfeiter:generate . LoadbalancerFacade +type LoadbalancerFacade interface { + GetLoadbalancer(client utils.RetryableServiceClient, loadbalancerID string) (*loadbalancers.LoadBalancer, error) + + GetListener(client utils.RetryableServiceClient, listenerID string) (*listeners.Listener, error) + + GetPool(client utils.RetryableServiceClient, poolID string) (*pools.Pool, error) + + ListPools(client utils.RetryableServiceClient, listOpts pools.ListOpts) (pagination.Page, error) + + ExtractPools(allPages pagination.Page) ([]pools.Pool, error) + + ListPoolMembers(client utils.RetryableServiceClient, poolID string, opts pools.ListMembersOpts) (pagination.Page, error) + + ExtractPoolMembers(allPages pagination.Page) ([]pools.Member, error) + + CreatePoolMember(client utils.ServiceClient, poolID string, opts pools.CreateMemberOpts) (*pools.Member, error) + + DeletePoolMember(client utils.RetryableServiceClient, poolID string, memberID string) error +} + +type loadbalancerFacade struct { +} + +func NewLoadbalancerFacade() loadbalancerFacade { + return loadbalancerFacade{} +} + +func (l loadbalancerFacade) GetLoadbalancer(client utils.RetryableServiceClient, loadbalancerID string) (*loadbalancers.LoadBalancer, error) { + return loadbalancers.Get(client, loadbalancerID).Extract() +} + +func (l loadbalancerFacade) GetListener(client utils.RetryableServiceClient, listenerID string) (*listeners.Listener, error) { + return listeners.Get(client, listenerID).Extract() +} + +func (l loadbalancerFacade) GetPool(client utils.RetryableServiceClient, poolID string) (*pools.Pool, error) { + return pools.Get(client, poolID).Extract() +} + +func (l loadbalancerFacade) ListPools(client utils.RetryableServiceClient, listOpts pools.ListOpts) (pagination.Page, error) { + return pools.List(client, listOpts).AllPages() +} + +func (l loadbalancerFacade) ExtractPools(allPages pagination.Page) ([]pools.Pool, error) { + return pools.ExtractPools(allPages) +} + +func (l loadbalancerFacade) ListPoolMembers(client utils.RetryableServiceClient, poolID string, opts pools.ListMembersOpts) (pagination.Page, error) { + return pools.ListMembers(client, poolID, opts).AllPages() +} + +func (l loadbalancerFacade) ExtractPoolMembers(allPages pagination.Page) ([]pools.Member, error) { + return pools.ExtractMembers(allPages) +} + +func (l loadbalancerFacade) CreatePoolMember(client utils.ServiceClient, poolID string, opts pools.CreateMemberOpts) (*pools.Member, error) { + return pools.CreateMember(client, poolID, opts).Extract() +} + +func (l loadbalancerFacade) DeletePoolMember(client utils.RetryableServiceClient, poolID string, memberID string) error { + return pools.DeleteMember(client, poolID, memberID).ExtractErr() +} diff --git a/src/openstack_cpi_golang/cpi/loadbalancer/loadbalancer_service.go b/src/openstack_cpi_golang/cpi/loadbalancer/loadbalancer_service.go new file mode 100644 index 00000000..868012cb --- /dev/null +++ b/src/openstack_cpi_golang/cpi/loadbalancer/loadbalancer_service.go @@ -0,0 +1,303 @@ +package loadbalancer + +import ( + "errors" + "fmt" + "time" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/properties" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils" + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/loadbalancers" + "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/pools" +) + +var LoadbalancerServicePollingInterval = 10 * time.Second + +//counterfeiter:generate . LoadbalancerService +type LoadbalancerService interface { + GetPool(poolName string) (pools.Pool, error) + + CreatePoolMember(pool pools.Pool, ip string, poolProperties properties.LoadbalancerPool, subnetID string, timeout int) (*pools.Member, error) + + DeletePoolMember(poolID string, memberID string, timeout int) error +} + +type loadbalancerService struct { + serviceClients utils.ServiceClients + loadbalancerFacade LoadbalancerFacade + logger utils.Logger +} + +func NewLoadbalancerService( + serviceClients utils.ServiceClients, + loadbalancerFacade LoadbalancerFacade, + logger utils.Logger, +) loadbalancerService { + return loadbalancerService{ + serviceClients: serviceClients, + loadbalancerFacade: loadbalancerFacade, + logger: logger, + } +} + +func (l loadbalancerService) GetPool(poolName string) (pools.Pool, error) { + listOpts := pools.ListOpts{ + Name: poolName, + } + + page, err := l.loadbalancerFacade.ListPools(l.serviceClients.RetryableServiceClient, listOpts) + if err != nil { + return pools.Pool{}, fmt.Errorf("failed to list loadbalancer pools: %w", err) + } + + extractedPools, err := l.loadbalancerFacade.ExtractPools(page) + if err != nil { + return pools.Pool{}, fmt.Errorf("failed to extract loadbalancer pool pages: %w", err) + } + + if len(extractedPools) == 0 { + return pools.Pool{}, fmt.Errorf("loadbalancer pool '%s' does not exist", poolName) + } + + if len(extractedPools) > 1 { + return pools.Pool{}, fmt.Errorf("found more than one loadbalancer pool with name '%s'. Make sure to use unique naming", poolName) + } + + return extractedPools[0], nil +} + +func (l loadbalancerService) CreatePoolMember(pool pools.Pool, ip string, poolProperties properties.LoadbalancerPool, subnetID string, stateTimeOut int) (*pools.Member, error) { + var err error + var errDefault409 gophercloud.ErrDefault409 + var poolMember *pools.Member + + createMemberOpts := pools.CreateMemberOpts{ + Address: ip, + ProtocolPort: poolProperties.ProtocolPort, + SubnetID: subnetID, + } + + if poolProperties.MonitoringPort != nil { + createMemberOpts.MonitorPort = poolProperties.MonitoringPort + } + + loadbalancerId, err := l.getLoadbalancerId(pool) + if err != nil { + return nil, fmt.Errorf("failed to get loadbalancer ID: %w", err) + } + + timeoutDuration := time.Duration(stateTimeOut) * time.Second + createPoolMemberTimeoutTimer := time.NewTimer(timeoutDuration) + attempts := 0 + + for poolMember == nil { + select { + case <-createPoolMemberTimeoutTimer.C: + return nil, fmt.Errorf("timeout after %v attempts while creating pool membership with IP '%s' in pool '%s'", attempts, ip, pool.ID) + default: + poolMember, err = l.createPoolMember(loadbalancerId, pool.ID, createMemberOpts, timeoutDuration) + if err != nil { + if errors.As(err, &errDefault409) { + // If there is a conflict, try to find if the pool member already exists with the same IP and Port + poolMember := l.getPoolMember(pool.ID, createMemberOpts) + if poolMember != nil { + l.logger.Info("loadbalancer_service", fmt.Sprintf("SKIPPING creation: pool membership with pool id '%s', ip '%s', and port '%v' already exists", pool.ID, ip, poolProperties.ProtocolPort)) + return poolMember, nil + } else { + attempts++ + l.logger.Warn("loadbalancer_service", fmt.Sprintf("creating pool membership with IP '%s' in pool '%s' failed in attempt number '%v' with error: %s", ip, pool.ID, attempts, err.Error())) + } + } else { + return nil, err + } + } + } + } + + _, err = l.waitForLoadbalancerToBecomeActive(loadbalancerId, timeoutDuration) + if err != nil { + return nil, fmt.Errorf("failed while waiting for loadbalancer '%s' to become active: %w", loadbalancerId, err) + } + + return poolMember, nil +} + +func (l loadbalancerService) DeletePoolMember(poolID string, memberID string, stateTimeOut int) error { + var err error + var errDefault409 gophercloud.ErrDefault409 + var errDefault404 gophercloud.ErrDefault404 + + var isDeleted bool + + pool, err := l.loadbalancerFacade.GetPool(l.serviceClients.RetryableServiceClient, poolID) + if err != nil { + return fmt.Errorf("failed to get pool with ID '%s': %w", poolID, err) + } + + loadbalancerId, err := l.getLoadbalancerId(*pool) + if err != nil { + return fmt.Errorf("failed to get loadbalancer ID: %w", err) + } + + timeoutDuration := time.Duration(stateTimeOut) * time.Second + deletePoolMemberTimeoutTimer := time.NewTimer(timeoutDuration) + attempts := 0 + + for !isDeleted { + select { + case <-deletePoolMemberTimeoutTimer.C: + return fmt.Errorf("timeout after %v attempts while deleting pool membership with ID '%s' in pool '%s'", attempts, memberID, poolID) + default: + err = l.deletePoolMember(loadbalancerId, poolID, memberID, timeoutDuration) + if err != nil { + if errors.As(err, &errDefault409) { + attempts++ + l.logger.Warn("loadbalancer_service", fmt.Sprintf("deleting pool membership with ID '%s' in pool '%s' failed in attempt number '%v' with error: %s", memberID, poolID, attempts, err.Error())) + } else if errors.As(err, &errDefault404) { + l.logger.Info("loadbalancer_service", fmt.Sprintf("SKIPPING deletion: pool member with id '%s' in pool '%s' is not found", memberID, poolID)) + return nil + } else { + return err + } + } + isDeleted = true + l.logger.Info("loadbalancer_service", fmt.Sprintf("Deleted pool member with id '%s' from pool '%s'", memberID, poolID)) + } + } + + _, err = l.waitForLoadbalancerToBecomeActive(loadbalancerId, timeoutDuration) + if err != nil { + return fmt.Errorf("failed while waiting for loadbalancer '%s' to become active: %w", loadbalancerId, err) + } + + return nil +} + +func (l loadbalancerService) createPoolMember(loadbalancerID string, poolID string, createMemberOpts pools.CreateMemberOpts, timeout time.Duration) (*pools.Member, error) { + _, err := l.waitForLoadbalancerToBecomeActive(loadbalancerID, timeout) + if err != nil { + return nil, fmt.Errorf("failed while waiting for loadbalancer to become active: %w", err) + } + + member, err := l.loadbalancerFacade.CreatePoolMember(l.serviceClients.ServiceClient, poolID, createMemberOpts) + if err != nil { + return nil, fmt.Errorf("failed to create pool member: %w", err) + } + + return member, nil +} + +func (l loadbalancerService) deletePoolMember(loadbalancerID string, poolID string, memberID string, timeout time.Duration) error { + _, err := l.waitForLoadbalancerToBecomeActive(loadbalancerID, timeout) + if err != nil { + return fmt.Errorf("failed while waiting for loadbalancer to become active: %w", err) + } + + err = l.loadbalancerFacade.DeletePoolMember(l.serviceClients.RetryableServiceClient, poolID, memberID) + if err != nil { + return fmt.Errorf("failed to delete pool member: %w", err) + } + + return nil +} + +func (l loadbalancerService) getPoolMember(poolID string, poolMemberOpts pools.CreateMemberOpts) *pools.Member { + var result *pools.Member + + listOpts := pools.ListMembersOpts{ + Address: poolMemberOpts.Address, + ProtocolPort: poolMemberOpts.ProtocolPort, + } + + pages, err := l.loadbalancerFacade.ListPoolMembers(l.serviceClients.RetryableServiceClient, poolID, listOpts) + if err != nil { + return nil + } + + extractedPoolMembers, err := l.loadbalancerFacade.ExtractPoolMembers(pages) + if err != nil { + return nil + } + + if len(extractedPoolMembers) == 0 { + return nil + } + + for _, poolMember := range extractedPoolMembers { + if poolMember.SubnetID == poolMemberOpts.SubnetID { + if poolMemberOpts.MonitorPort != nil { + if poolMember.MonitorPort == *poolMemberOpts.MonitorPort { + result = &poolMember + break + } else { + continue + } + } + + result = &poolMember + break + } + } + + return result +} + +func (l loadbalancerService) getLoadbalancerId(pool pools.Pool) (string, error) { + loadbalancerList := pool.Loadbalancers + + if len(loadbalancerList) == 0 { + loadbalancerListeners := pool.Listeners + + if len(loadbalancerListeners) == 0 { + return "", fmt.Errorf("no load balancers or listeners associated with pool '%s'", pool.ID) + } else if len(loadbalancerListeners) > 1 { + return "", fmt.Errorf("more than one listener is associated with pool '%s'. It is not possible "+ + "to verify the status of the load balancer responsible for the pool membership", pool.ID) + } else { + listener, err := l.loadbalancerFacade.GetListener(l.serviceClients.RetryableServiceClient, loadbalancerListeners[0].ID) + if err != nil || listener == nil { + return "", fmt.Errorf("failed to retrieve listener '%s': %w", loadbalancerListeners[0].ID, err) + } + + for _, lb := range listener.Loadbalancers { + loadbalancerList = append(loadbalancerList, pools.LoadBalancerID{ID: lb.ID}) + } + } + } + + if len(loadbalancerList) == 0 { + return "", fmt.Errorf("no load balancers associated with pool '%s'", pool.ID) + } else if len(loadbalancerList) > 1 { + return "", fmt.Errorf("more than one load balancer is associated with pool '%s'. It is not possible "+ + "to verify the status of the load balancer responsible for the pool membership", pool.ID) + } + + return loadbalancerList[0].ID, nil +} + +func (l loadbalancerService) waitForLoadbalancerToBecomeActive(loadbalancerID string, timeout time.Duration) (*loadbalancers.LoadBalancer, error) { + timeoutTimer := time.NewTimer(timeout) + + for { + select { + case <-timeoutTimer.C: + return nil, fmt.Errorf("timeout while waiting for loadbalancer '%s' to become active", loadbalancerID) + default: + loadbalancer, err := l.loadbalancerFacade.GetLoadbalancer(l.serviceClients.RetryableServiceClient, loadbalancerID) + if err != nil || loadbalancer == nil { + return nil, fmt.Errorf("failed to retrieve loadbalancer '%s': %w", loadbalancerID, err) + } + + switch loadbalancer.ProvisioningStatus { + case "ACTIVE": + return loadbalancer, nil + case "ERROR": + return nil, fmt.Errorf("loadbalancer status ended up in '%s' state", loadbalancer.ProvisioningStatus) + default: + time.Sleep(LoadbalancerServicePollingInterval) + continue + } + } + } +} diff --git a/src/openstack_cpi_golang/cpi/loadbalancer/loadbalancer_service_builder.go b/src/openstack_cpi_golang/cpi/loadbalancer/loadbalancer_service_builder.go new file mode 100644 index 00000000..c529f8eb --- /dev/null +++ b/src/openstack_cpi_golang/cpi/loadbalancer/loadbalancer_service_builder.go @@ -0,0 +1,41 @@ +package loadbalancer + +import ( + "fmt" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/config" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/openstack" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils" +) + +//counterfeiter:generate . LoadbalancerServiceBuilder +type LoadbalancerServiceBuilder interface { + Build() (LoadbalancerService, error) +} + +type loadbalancerServiceBuilder struct { + openstackService openstack.OpenstackService + cpiConfig config.CpiConfig + logger utils.Logger +} + +func NewLoadbalancerServiceBuilder(openstackService openstack.OpenstackService, cpiConfig config.CpiConfig, logger utils.Logger) loadbalancerServiceBuilder { + return loadbalancerServiceBuilder{ + openstackService: openstackService, + cpiConfig: cpiConfig, + logger: logger, + } +} + +func (b loadbalancerServiceBuilder) Build() (LoadbalancerService, error) { + serviceClient, err := b.openstackService.LoadbalancerV2(b.cpiConfig.OpenStackConfig()) + if err != nil { + return nil, fmt.Errorf("failed to retrieve loadbalancer service client: %w", err) + } + + return NewLoadbalancerService( + utils.NewServiceClients(serviceClient, b.cpiConfig, b.logger), + NewLoadbalancerFacade(), + b.logger, + ), nil +} diff --git a/src/openstack_cpi_golang/cpi/loadbalancer/loadbalancer_service_builder_test.go b/src/openstack_cpi_golang/cpi/loadbalancer/loadbalancer_service_builder_test.go new file mode 100644 index 00000000..992323e0 --- /dev/null +++ b/src/openstack_cpi_golang/cpi/loadbalancer/loadbalancer_service_builder_test.go @@ -0,0 +1,55 @@ +package loadbalancer_test + +import ( + "errors" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/config" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/loadbalancer" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/openstack/openstackfakes" + "github.com/gophercloud/gophercloud" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils/utilsfakes" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("LoadbalancerServiceBuilder", func() { + var openstackService openstackfakes.FakeOpenstackService + var logger utilsfakes.FakeLogger + var loadbalancerServiceBuilder loadbalancer.LoadbalancerServiceBuilder + + BeforeEach(func() { + openstackService = openstackfakes.FakeOpenstackService{} + logger = utilsfakes.FakeLogger{} + cpiConfig := config.CpiConfig{} + cpiConfig.Cloud.Properties.RetryConfig = config.RetryConfigMap{} + + loadbalancerServiceBuilder = loadbalancer.NewLoadbalancerServiceBuilder( + &openstackService, + cpiConfig, + &logger, + ) + }) + + Context("Build", func() { + It("returns an loadbalancer service", func() { + providerClient := gophercloud.ProviderClient{TokenID: "the_token"} + serviceClient := gophercloud.ServiceClient{ProviderClient: &providerClient} + openstackService.LoadbalancerV2Returns(&serviceClient, nil) + + loadbalancerService, err := loadbalancerServiceBuilder.Build() + + Expect(err).ToNot(HaveOccurred()) + Expect(loadbalancerService).To(Not(BeNil())) + }) + + It("returns an error if the loadbalancer service client cannot be retrieved", func() { + openstackService.LoadbalancerV2Returns(nil, errors.New("boom")) + + loadbalancerService, err := loadbalancerServiceBuilder.Build() + + Expect(err.Error()).To(Equal("failed to retrieve loadbalancer service client: boom")) + Expect(loadbalancerService).To(BeNil()) + }) + }) +}) diff --git a/src/openstack_cpi_golang/cpi/loadbalancer/loadbalancer_service_test.go b/src/openstack_cpi_golang/cpi/loadbalancer/loadbalancer_service_test.go new file mode 100644 index 00000000..21eaec84 --- /dev/null +++ b/src/openstack_cpi_golang/cpi/loadbalancer/loadbalancer_service_test.go @@ -0,0 +1,402 @@ +package loadbalancer_test + +import ( + "errors" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/loadbalancer" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/loadbalancer/loadbalancerfakes" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/mocks" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/properties" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils/utilsfakes" + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/listeners" + "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/loadbalancers" + "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/pools" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("LoadbalancerService", func() { + var serviceClient gophercloud.ServiceClient + var retryableServiceClient gophercloud.ServiceClient + var serviceClients utils.ServiceClients + var loadbalancerFacade loadbalancerfakes.FakeLoadbalancerFacade + var logger utilsfakes.FakeLogger + var poolsPage mocks.MockPage + var mockPool pools.Pool + var mockListener listeners.Listener + var mockMember pools.Member + var poolProps properties.LoadbalancerPool + + BeforeEach(func() { + serviceClient = gophercloud.ServiceClient{} + retryableServiceClient = gophercloud.ServiceClient{} + serviceClients = utils.ServiceClients{ServiceClient: &serviceClient, RetryableServiceClient: &retryableServiceClient} + loadbalancerFacade = loadbalancerfakes.FakeLoadbalancerFacade{} + logger = utilsfakes.FakeLogger{} + poolsPage = mocks.MockPage{} + + loadbalancer.LoadbalancerServicePollingInterval = 0 + + monitoringPort := 5678 + poolProps = properties.LoadbalancerPool{ + Name: "pool-name", + ProtocolPort: 1234, + MonitoringPort: &monitoringPort, + } + + mockListener = listeners.Listener{ + ID: "the-listener-id", + Loadbalancers: []listeners.LoadBalancerID{{ID: "the-lb-id"}}, + } + + mockPool = pools.Pool{ + ID: "pool-id", + Name: "pool-name", + Loadbalancers: []pools.LoadBalancerID{{ID: "the-lb-id"}}, + Listeners: []pools.ListenerID{{ID: "the-listener-id"}}, + } + + mockMember = pools.Member{ + ID: "the-member-id", + SubnetID: "subnet-id", + Address: "1.1.1.1", + ProtocolPort: 1234, + MonitorPort: 5678, + } + + loadbalancerFacade.GetPoolReturns(&mockPool, nil) + + loadbalancerFacade.GetListenerReturns(&mockListener, nil) + + loadbalancerFacade.ExtractPoolMembersReturns([]pools.Member{mockMember}, nil) + }) + + Context("GetPool", func() { + BeforeEach(func() { + loadbalancerFacade.ListPoolsReturns(poolsPage, nil) + loadbalancerFacade.ExtractPoolsReturns([]pools.Pool{mockPool}, nil) + }) + + It("lists loadbalancer pools", func() { + _, _ = loadbalancer.NewLoadbalancerService(serviceClients, &loadbalancerFacade, &logger). + GetPool("pool-name") + + _, listOpts := loadbalancerFacade.ListPoolsArgsForCall(0) + Expect(listOpts.Name).To(Equal("pool-name")) + }) + + It("returns an error if listing loadbalancer pools fails", func() { + loadbalancerFacade.ListPoolsReturns(nil, errors.New("boom")) + + pool, err := loadbalancer.NewLoadbalancerService(serviceClients, &loadbalancerFacade, &logger). + GetPool("pool-name") + + Expect(err.Error()).To(Equal("failed to list loadbalancer pools: boom")) + Expect(pool).To(Equal(pools.Pool{})) + }) + + It("extracts loadbalancer pools", func() { + _, _ = loadbalancer.NewLoadbalancerService(serviceClients, &loadbalancerFacade, &logger). + GetPool("pool-name") + + Expect(loadbalancerFacade.ExtractPoolsArgsForCall(0)).To(Equal(poolsPage)) + }) + + It("returns an error if extracting loadbalancer pools fails", func() { + loadbalancerFacade.ExtractPoolsReturns(nil, errors.New("boom")) + + pool, err := loadbalancer.NewLoadbalancerService(serviceClients, &loadbalancerFacade, &logger). + GetPool("pool-name") + + Expect(err.Error()).To(Equal("failed to extract loadbalancer pool pages: boom")) + Expect(pool.ID).To(Equal("")) + }) + + It("returns an error if pools are empty", func() { + loadbalancerFacade.ExtractPoolsReturns([]pools.Pool{}, nil) + + pool, err := loadbalancer.NewLoadbalancerService(serviceClients, &loadbalancerFacade, &logger). + GetPool("pool-name") + + Expect(err.Error()).To(Equal("loadbalancer pool 'pool-name' does not exist")) + Expect(pool.ID).To(Equal("")) + }) + + It("returns an error if multiple pools with same name exists", func() { + loadbalancerFacade.ExtractPoolsReturns([]pools.Pool{{Name: "pool-name"}, {Name: "pool-name"}}, nil) + + pool, err := loadbalancer.NewLoadbalancerService(serviceClients, &loadbalancerFacade, &logger). + GetPool("pool-name") + + Expect(err.Error()).To(Equal("found more than one loadbalancer pool with name 'pool-name'. Make sure to use unique naming")) + Expect(pool.ID).To(Equal("")) + }) + + It("returns the pool ID", func() { + pool, err := loadbalancer.NewLoadbalancerService(serviceClients, &loadbalancerFacade, &logger). + GetPool("pool-name") + + Expect(err).To(Not(HaveOccurred())) + Expect(pool.ID).To(Equal("pool-id")) + }) + }) + + Context("CreatePoolMember", func() { + BeforeEach(func() { + loadbalancerFacade.GetLoadbalancerReturns(&loadbalancers.LoadBalancer{ID: "the-lb-id", ProvisioningStatus: "ACTIVE"}, nil) + loadbalancerFacade.CreatePoolMemberReturns(&mockMember, nil) + }) + + It("waits for the loadbalancer to become ACTIVE", func() { + loadbalancerFacade.GetLoadbalancerReturnsOnCall(0, &loadbalancers.LoadBalancer{ID: "the-lb-id", ProvisioningStatus: "PENDING_UPDATE"}, nil) + loadbalancerFacade.GetLoadbalancerReturnsOnCall(1, &loadbalancers.LoadBalancer{ID: "the-lb-id", ProvisioningStatus: "ACTIVE"}, nil) + + _, err := loadbalancer.NewLoadbalancerService(serviceClients, &loadbalancerFacade, &logger). + CreatePoolMember(mockPool, "1.1.1.1", poolProps, "subnet-id", 1) + + retryableServiceClient, poolId := loadbalancerFacade.GetLoadbalancerArgsForCall(0) + var utilsRetryableServiceClient utils.RetryableServiceClient + Expect(retryableServiceClient).To(BeAssignableToTypeOf(utilsRetryableServiceClient)) + + Expect(poolId).To(Equal("the-lb-id")) + Expect(err).ToNot(HaveOccurred()) + }) + + It("retrieves the loadbalancer via listeners", func() { + mockPool.Loadbalancers = []pools.LoadBalancerID{} + + _, err := loadbalancer.NewLoadbalancerService(serviceClients, &loadbalancerFacade, &logger). + CreatePoolMember(mockPool, "1.1.1.1", poolProps, "subnet-id", 1) + + retryableServiceClient, poolId := loadbalancerFacade.GetLoadbalancerArgsForCall(0) + var utilsRetryableServiceClient utils.RetryableServiceClient + Expect(retryableServiceClient).To(BeAssignableToTypeOf(utilsRetryableServiceClient)) + + Expect(poolId).To(Equal("the-lb-id")) + Expect(err).ToNot(HaveOccurred()) + }) + + It("fails if no loadbalancers nor listeners are associated with pool", func() { + mockPool.Loadbalancers = []pools.LoadBalancerID{} + mockPool.Listeners = []pools.ListenerID{} + + poolMember, err := loadbalancer.NewLoadbalancerService(serviceClients, &loadbalancerFacade, &logger). + CreatePoolMember(mockPool, "1.1.1.1", poolProps, "subnet-id", 1) + + Expect(err.Error()).To(ContainSubstring("no load balancers or listeners associated with pool 'pool-id'")) + Expect(poolMember).To(BeNil()) + }) + + It("fails if multiple listeners are associated with pool", func() { + mockPool.Loadbalancers = []pools.LoadBalancerID{} + mockPool.Listeners = append(mockPool.Listeners, []pools.ListenerID{{ID: "another-listener-id"}}...) + + poolMember, err := loadbalancer.NewLoadbalancerService(serviceClients, &loadbalancerFacade, &logger). + CreatePoolMember(mockPool, "1.1.1.1", poolProps, "subnet-id", 1) + + Expect(err.Error()).To(ContainSubstring("more than one listener is associated with pool 'pool-id'")) + Expect(poolMember).To(BeNil()) + }) + + It("fails if multiple loadbalancers are associated with pool", func() { + mockPool.Loadbalancers = append(mockPool.Loadbalancers, []pools.LoadBalancerID{{ID: "another-lb-id"}}...) + + poolMember, err := loadbalancer.NewLoadbalancerService(serviceClients, &loadbalancerFacade, &logger). + CreatePoolMember(mockPool, "1.1.1.1", poolProps, "subnet-id", 1) + + Expect(err.Error()).To(ContainSubstring("more than one load balancer is associated with pool 'pool-id'")) + Expect(poolMember).To(BeNil()) + }) + + It("fails if retrieving a listener fails", func() { + mockPool.Loadbalancers = []pools.LoadBalancerID{} + + loadbalancerFacade.GetListenerReturns(&listeners.Listener{}, errors.New("boom")) + + poolMember, err := loadbalancer.NewLoadbalancerService(serviceClients, &loadbalancerFacade, &logger). + CreatePoolMember(mockPool, "1.1.1.1", poolProps, "subnet-id", 1) + + Expect(err.Error()).To(ContainSubstring("failed to retrieve listener 'the-listener-id'")) + Expect(poolMember).To(BeNil()) + }) + + It("times out while waiting for loadbalancer to become ACTIVE", func() { + loadbalancerFacade.GetLoadbalancerReturns(&loadbalancers.LoadBalancer{ID: "the-lb-id", ProvisioningStatus: "PENDING_UPDATE"}, nil) + + poolMember, err := loadbalancer.NewLoadbalancerService(serviceClients, &loadbalancerFacade, &logger). + CreatePoolMember(mockPool, "1.1.1.1", poolProps, "subnet-id", 1) + + Expect(err.Error()).To(ContainSubstring("timeout while waiting for loadbalancer 'the-lb-id' to become active")) + Expect(poolMember).To(BeNil()) + }) + + It("returns an error while waiting if getting loadbalancer fails", func() { + loadbalancerFacade.GetLoadbalancerReturns(nil, errors.New("boom")) + + poolMember, err := loadbalancer.NewLoadbalancerService(serviceClients, &loadbalancerFacade, &logger). + CreatePoolMember(mockPool, "1.1.1.1", poolProps, "subnet-id", 1) + + Expect(err.Error()).To(ContainSubstring("failed to retrieve loadbalancer 'the-lb-id': boom")) + Expect(poolMember).To(BeNil()) + }) + + It("returns an error while waiting if the loadbalancer is in state ERROR", func() { + loadbalancerFacade.GetLoadbalancerReturns(&loadbalancers.LoadBalancer{ID: "the-lb-id", ProvisioningStatus: "ERROR"}, nil) + + poolMember, err := loadbalancer.NewLoadbalancerService(serviceClients, &loadbalancerFacade, &logger). + CreatePoolMember(mockPool, "1.1.1.1", poolProps, "subnet-id", 1) + + Expect(err.Error()).To(ContainSubstring("loadbalancer status ended up in 'ERROR' state")) + Expect(poolMember).To(BeNil()) + }) + + It("creates a pool member", func() { + poolMember, err := loadbalancer.NewLoadbalancerService(serviceClients, &loadbalancerFacade, &logger). + CreatePoolMember(mockPool, "1.1.1.1", poolProps, "subnet-id", 1) + + _, poolID, _ := loadbalancerFacade.CreatePoolMemberArgsForCall(0) + Expect(poolID).To(Equal("pool-id")) + + Expect(err).ToNot(HaveOccurred()) + Expect(poolMember.ID).To(Equal("the-member-id")) + Expect(poolMember.Address).To(Equal("1.1.1.1")) + Expect(poolMember.ProtocolPort).To(Equal(1234)) + Expect(poolMember.MonitorPort).To(Equal(5678)) + Expect(poolMember.SubnetID).To(Equal("subnet-id")) + }) + + It("returns an error if creating a pool member fails", func() { + loadbalancerFacade.CreatePoolMemberReturns(nil, errors.New("boom")) + + _, err := loadbalancer.NewLoadbalancerService(serviceClients, &loadbalancerFacade, &logger). + CreatePoolMember(mockPool, "1.1.1.1", poolProps, "subnet-id", 1) + + Expect(err.Error()).To(Equal("failed to create pool member: boom")) + }) + + It("tries to find the pool member causing the conflict and returns it", func() { + testError := gophercloud.ErrDefault409{ + ErrUnexpectedResponseCode: gophercloud.ErrUnexpectedResponseCode{Actual: 409}, + } + loadbalancerFacade.CreatePoolMemberReturns(nil, testError) + + member, err := loadbalancer.NewLoadbalancerService(serviceClients, &loadbalancerFacade, &logger). + CreatePoolMember(mockPool, "1.1.1.1", poolProps, "subnet-id", 1) + + Expect(err).ToNot(HaveOccurred()) + Expect(member).To(Equal(&mockMember)) + }) + }) + + Context("DeletePoolMember", func() { + BeforeEach(func() { + loadbalancerFacade.GetLoadbalancerReturns(&loadbalancers.LoadBalancer{ID: "the-lb-id", ProvisioningStatus: "ACTIVE"}, nil) + }) + + It("returns an error if getting pool fails", func() { + loadbalancerFacade.GetPoolReturns(nil, errors.New("boom")) + + err := loadbalancer.NewLoadbalancerService(serviceClients, &loadbalancerFacade, &logger). + DeletePoolMember("pool-id", "member-id", 1) + + Expect(err.Error()).To(ContainSubstring("failed to get pool with ID 'pool-id': boom")) + }) + + It("fails if no loadbalancers nor listeners are associated with pool", func() { + mockPool.Loadbalancers = []pools.LoadBalancerID{} + mockPool.Listeners = []pools.ListenerID{} + + err := loadbalancer.NewLoadbalancerService(serviceClients, &loadbalancerFacade, &logger). + DeletePoolMember("pool-id", "member-id", 1) + + Expect(err.Error()).To(ContainSubstring("no load balancers or listeners associated with pool 'pool-id'")) + }) + + It("waits for the loadbalancer to become ACTIVE", func() { + loadbalancerFacade.GetLoadbalancerReturnsOnCall(0, &loadbalancers.LoadBalancer{ID: "the-lb-id", ProvisioningStatus: "PENDING_UPDATE"}, nil) + loadbalancerFacade.GetLoadbalancerReturnsOnCall(1, &loadbalancers.LoadBalancer{ID: "the-lb-id", ProvisioningStatus: "ACTIVE"}, nil) + + err := loadbalancer.NewLoadbalancerService(serviceClients, &loadbalancerFacade, &logger). + DeletePoolMember("pool-id", "member-id", 1) + + retryableServiceClient, poolId := loadbalancerFacade.GetPoolArgsForCall(0) + var utilsRetryableServiceClient utils.RetryableServiceClient + Expect(retryableServiceClient).To(BeAssignableToTypeOf(utilsRetryableServiceClient)) + + Expect(poolId).To(Equal("pool-id")) + Expect(err).ToNot(HaveOccurred()) + Expect(loadbalancerFacade.DeletePoolMemberCallCount()).To(Equal(1)) + }) + + It("times out while waiting for loadbalancer to become ACTIVE", func() { + loadbalancerFacade.GetLoadbalancerReturns(&loadbalancers.LoadBalancer{ID: "the-lb-id", ProvisioningStatus: "PENDING_UPDATE"}, nil) + + err := loadbalancer.NewLoadbalancerService(serviceClients, &loadbalancerFacade, &logger). + DeletePoolMember("pool-id", "member-id", 1) + + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("timeout while waiting for loadbalancer 'the-lb-id' to become active")) + Expect(loadbalancerFacade.DeletePoolMemberCallCount()).To(Equal(0)) + }) + + It("returns an error while waiting if getting loadbalancer fails", func() { + loadbalancerFacade.GetLoadbalancerReturns(&loadbalancers.LoadBalancer{}, errors.New("boom")) + + err := loadbalancer.NewLoadbalancerService(serviceClients, &loadbalancerFacade, &logger). + DeletePoolMember("pool-id", "member-id", 1) + + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to retrieve loadbalancer 'the-lb-id': boom")) + Expect(loadbalancerFacade.DeletePoolMemberCallCount()).To(Equal(0)) + }) + + It("returns an error while waiting if the loadbalancer is in state ERROR", func() { + loadbalancerFacade.GetLoadbalancerReturns(&loadbalancers.LoadBalancer{ID: "the-lb-id", ProvisioningStatus: "ERROR"}, nil) + + err := loadbalancer.NewLoadbalancerService(serviceClients, &loadbalancerFacade, &logger). + DeletePoolMember("pool-id", "member-id", 1) + + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("loadbalancer status ended up in 'ERROR' state")) + Expect(loadbalancerFacade.DeletePoolMemberCallCount()).To(Equal(0)) + }) + + It("deletes pool member", func() { + err := loadbalancer.NewLoadbalancerService(serviceClients, &loadbalancerFacade, &logger). + DeletePoolMember("pool-id", "member-id", 1) + + retryableServiceClient, _, _ := loadbalancerFacade.DeletePoolMemberArgsForCall(0) + var utilsRetryableServiceClient utils.RetryableServiceClient + Expect(retryableServiceClient).To(BeAssignableToTypeOf(utilsRetryableServiceClient)) + + Expect(err).ToNot(HaveOccurred()) + Expect(loadbalancerFacade.DeletePoolMemberCallCount()).To(Equal(1)) + }) + + It("does not fail if delete pool member returns error-not-found", func() { + testError := gophercloud.ErrDefault404{ + ErrUnexpectedResponseCode: gophercloud.ErrUnexpectedResponseCode{Actual: 404}, + } + loadbalancerFacade.DeletePoolMemberReturns(testError) + + err := loadbalancer.NewLoadbalancerService(serviceClients, &loadbalancerFacade, &logger). + DeletePoolMember("pool-name", "member-id", 1) + + Expect(err).ToNot(HaveOccurred()) + Expect(loadbalancerFacade.DeletePoolMemberCallCount()).To(Equal(1)) + }) + + It("returns an error if deleting pool member fails", func() { + loadbalancerFacade.DeletePoolMemberReturns(errors.New("boom")) + + err := loadbalancer.NewLoadbalancerService(serviceClients, &loadbalancerFacade, &logger). + DeletePoolMember("pool-name", "member-id", 1) + + Expect(err.Error()).To(Equal("failed to delete pool member: boom")) + Expect(loadbalancerFacade.DeletePoolMemberCallCount()).To(Equal(1)) + }) + }) + +}) diff --git a/src/openstack_cpi_golang/cpi/loadbalancer/loadbalancer_suite_test.go b/src/openstack_cpi_golang/cpi/loadbalancer/loadbalancer_suite_test.go new file mode 100644 index 00000000..bf877372 --- /dev/null +++ b/src/openstack_cpi_golang/cpi/loadbalancer/loadbalancer_suite_test.go @@ -0,0 +1,13 @@ +package loadbalancer_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestMethods(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Loadbalancer Suite") +} diff --git a/src/openstack_cpi_golang/cpi/loadbalancer/loadbalancerfakes/fake_loadbalancer_facade.go b/src/openstack_cpi_golang/cpi/loadbalancer/loadbalancerfakes/fake_loadbalancer_facade.go new file mode 100644 index 00000000..5e99f240 --- /dev/null +++ b/src/openstack_cpi_golang/cpi/loadbalancer/loadbalancerfakes/fake_loadbalancer_facade.go @@ -0,0 +1,768 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package loadbalancerfakes + +import ( + "sync" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/loadbalancer" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils" + "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/listeners" + "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/loadbalancers" + "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/pools" + "github.com/gophercloud/gophercloud/pagination" +) + +type FakeLoadbalancerFacade struct { + CreatePoolMemberStub func(utils.ServiceClient, string, pools.CreateMemberOpts) (*pools.Member, error) + createPoolMemberMutex sync.RWMutex + createPoolMemberArgsForCall []struct { + arg1 utils.ServiceClient + arg2 string + arg3 pools.CreateMemberOpts + } + createPoolMemberReturns struct { + result1 *pools.Member + result2 error + } + createPoolMemberReturnsOnCall map[int]struct { + result1 *pools.Member + result2 error + } + DeletePoolMemberStub func(utils.RetryableServiceClient, string, string) error + deletePoolMemberMutex sync.RWMutex + deletePoolMemberArgsForCall []struct { + arg1 utils.RetryableServiceClient + arg2 string + arg3 string + } + deletePoolMemberReturns struct { + result1 error + } + deletePoolMemberReturnsOnCall map[int]struct { + result1 error + } + ExtractPoolMembersStub func(pagination.Page) ([]pools.Member, error) + extractPoolMembersMutex sync.RWMutex + extractPoolMembersArgsForCall []struct { + arg1 pagination.Page + } + extractPoolMembersReturns struct { + result1 []pools.Member + result2 error + } + extractPoolMembersReturnsOnCall map[int]struct { + result1 []pools.Member + result2 error + } + ExtractPoolsStub func(pagination.Page) ([]pools.Pool, error) + extractPoolsMutex sync.RWMutex + extractPoolsArgsForCall []struct { + arg1 pagination.Page + } + extractPoolsReturns struct { + result1 []pools.Pool + result2 error + } + extractPoolsReturnsOnCall map[int]struct { + result1 []pools.Pool + result2 error + } + GetListenerStub func(utils.RetryableServiceClient, string) (*listeners.Listener, error) + getListenerMutex sync.RWMutex + getListenerArgsForCall []struct { + arg1 utils.RetryableServiceClient + arg2 string + } + getListenerReturns struct { + result1 *listeners.Listener + result2 error + } + getListenerReturnsOnCall map[int]struct { + result1 *listeners.Listener + result2 error + } + GetLoadbalancerStub func(utils.RetryableServiceClient, string) (*loadbalancers.LoadBalancer, error) + getLoadbalancerMutex sync.RWMutex + getLoadbalancerArgsForCall []struct { + arg1 utils.RetryableServiceClient + arg2 string + } + getLoadbalancerReturns struct { + result1 *loadbalancers.LoadBalancer + result2 error + } + getLoadbalancerReturnsOnCall map[int]struct { + result1 *loadbalancers.LoadBalancer + result2 error + } + GetPoolStub func(utils.RetryableServiceClient, string) (*pools.Pool, error) + getPoolMutex sync.RWMutex + getPoolArgsForCall []struct { + arg1 utils.RetryableServiceClient + arg2 string + } + getPoolReturns struct { + result1 *pools.Pool + result2 error + } + getPoolReturnsOnCall map[int]struct { + result1 *pools.Pool + result2 error + } + ListPoolMembersStub func(utils.RetryableServiceClient, string, pools.ListMembersOpts) (pagination.Page, error) + listPoolMembersMutex sync.RWMutex + listPoolMembersArgsForCall []struct { + arg1 utils.RetryableServiceClient + arg2 string + arg3 pools.ListMembersOpts + } + listPoolMembersReturns struct { + result1 pagination.Page + result2 error + } + listPoolMembersReturnsOnCall map[int]struct { + result1 pagination.Page + result2 error + } + ListPoolsStub func(utils.RetryableServiceClient, pools.ListOpts) (pagination.Page, error) + listPoolsMutex sync.RWMutex + listPoolsArgsForCall []struct { + arg1 utils.RetryableServiceClient + arg2 pools.ListOpts + } + listPoolsReturns struct { + result1 pagination.Page + result2 error + } + listPoolsReturnsOnCall map[int]struct { + result1 pagination.Page + result2 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *FakeLoadbalancerFacade) CreatePoolMember(arg1 utils.ServiceClient, arg2 string, arg3 pools.CreateMemberOpts) (*pools.Member, error) { + fake.createPoolMemberMutex.Lock() + ret, specificReturn := fake.createPoolMemberReturnsOnCall[len(fake.createPoolMemberArgsForCall)] + fake.createPoolMemberArgsForCall = append(fake.createPoolMemberArgsForCall, struct { + arg1 utils.ServiceClient + arg2 string + arg3 pools.CreateMemberOpts + }{arg1, arg2, arg3}) + stub := fake.CreatePoolMemberStub + fakeReturns := fake.createPoolMemberReturns + fake.recordInvocation("CreatePoolMember", []interface{}{arg1, arg2, arg3}) + fake.createPoolMemberMutex.Unlock() + if stub != nil { + return stub(arg1, arg2, arg3) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeLoadbalancerFacade) CreatePoolMemberCallCount() int { + fake.createPoolMemberMutex.RLock() + defer fake.createPoolMemberMutex.RUnlock() + return len(fake.createPoolMemberArgsForCall) +} + +func (fake *FakeLoadbalancerFacade) CreatePoolMemberCalls(stub func(utils.ServiceClient, string, pools.CreateMemberOpts) (*pools.Member, error)) { + fake.createPoolMemberMutex.Lock() + defer fake.createPoolMemberMutex.Unlock() + fake.CreatePoolMemberStub = stub +} + +func (fake *FakeLoadbalancerFacade) CreatePoolMemberArgsForCall(i int) (utils.ServiceClient, string, pools.CreateMemberOpts) { + fake.createPoolMemberMutex.RLock() + defer fake.createPoolMemberMutex.RUnlock() + argsForCall := fake.createPoolMemberArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 +} + +func (fake *FakeLoadbalancerFacade) CreatePoolMemberReturns(result1 *pools.Member, result2 error) { + fake.createPoolMemberMutex.Lock() + defer fake.createPoolMemberMutex.Unlock() + fake.CreatePoolMemberStub = nil + fake.createPoolMemberReturns = struct { + result1 *pools.Member + result2 error + }{result1, result2} +} + +func (fake *FakeLoadbalancerFacade) CreatePoolMemberReturnsOnCall(i int, result1 *pools.Member, result2 error) { + fake.createPoolMemberMutex.Lock() + defer fake.createPoolMemberMutex.Unlock() + fake.CreatePoolMemberStub = nil + if fake.createPoolMemberReturnsOnCall == nil { + fake.createPoolMemberReturnsOnCall = make(map[int]struct { + result1 *pools.Member + result2 error + }) + } + fake.createPoolMemberReturnsOnCall[i] = struct { + result1 *pools.Member + result2 error + }{result1, result2} +} + +func (fake *FakeLoadbalancerFacade) DeletePoolMember(arg1 utils.RetryableServiceClient, arg2 string, arg3 string) error { + fake.deletePoolMemberMutex.Lock() + ret, specificReturn := fake.deletePoolMemberReturnsOnCall[len(fake.deletePoolMemberArgsForCall)] + fake.deletePoolMemberArgsForCall = append(fake.deletePoolMemberArgsForCall, struct { + arg1 utils.RetryableServiceClient + arg2 string + arg3 string + }{arg1, arg2, arg3}) + stub := fake.DeletePoolMemberStub + fakeReturns := fake.deletePoolMemberReturns + fake.recordInvocation("DeletePoolMember", []interface{}{arg1, arg2, arg3}) + fake.deletePoolMemberMutex.Unlock() + if stub != nil { + return stub(arg1, arg2, arg3) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeLoadbalancerFacade) DeletePoolMemberCallCount() int { + fake.deletePoolMemberMutex.RLock() + defer fake.deletePoolMemberMutex.RUnlock() + return len(fake.deletePoolMemberArgsForCall) +} + +func (fake *FakeLoadbalancerFacade) DeletePoolMemberCalls(stub func(utils.RetryableServiceClient, string, string) error) { + fake.deletePoolMemberMutex.Lock() + defer fake.deletePoolMemberMutex.Unlock() + fake.DeletePoolMemberStub = stub +} + +func (fake *FakeLoadbalancerFacade) DeletePoolMemberArgsForCall(i int) (utils.RetryableServiceClient, string, string) { + fake.deletePoolMemberMutex.RLock() + defer fake.deletePoolMemberMutex.RUnlock() + argsForCall := fake.deletePoolMemberArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 +} + +func (fake *FakeLoadbalancerFacade) DeletePoolMemberReturns(result1 error) { + fake.deletePoolMemberMutex.Lock() + defer fake.deletePoolMemberMutex.Unlock() + fake.DeletePoolMemberStub = nil + fake.deletePoolMemberReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeLoadbalancerFacade) DeletePoolMemberReturnsOnCall(i int, result1 error) { + fake.deletePoolMemberMutex.Lock() + defer fake.deletePoolMemberMutex.Unlock() + fake.DeletePoolMemberStub = nil + if fake.deletePoolMemberReturnsOnCall == nil { + fake.deletePoolMemberReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.deletePoolMemberReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *FakeLoadbalancerFacade) ExtractPoolMembers(arg1 pagination.Page) ([]pools.Member, error) { + fake.extractPoolMembersMutex.Lock() + ret, specificReturn := fake.extractPoolMembersReturnsOnCall[len(fake.extractPoolMembersArgsForCall)] + fake.extractPoolMembersArgsForCall = append(fake.extractPoolMembersArgsForCall, struct { + arg1 pagination.Page + }{arg1}) + stub := fake.ExtractPoolMembersStub + fakeReturns := fake.extractPoolMembersReturns + fake.recordInvocation("ExtractPoolMembers", []interface{}{arg1}) + fake.extractPoolMembersMutex.Unlock() + if stub != nil { + return stub(arg1) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeLoadbalancerFacade) ExtractPoolMembersCallCount() int { + fake.extractPoolMembersMutex.RLock() + defer fake.extractPoolMembersMutex.RUnlock() + return len(fake.extractPoolMembersArgsForCall) +} + +func (fake *FakeLoadbalancerFacade) ExtractPoolMembersCalls(stub func(pagination.Page) ([]pools.Member, error)) { + fake.extractPoolMembersMutex.Lock() + defer fake.extractPoolMembersMutex.Unlock() + fake.ExtractPoolMembersStub = stub +} + +func (fake *FakeLoadbalancerFacade) ExtractPoolMembersArgsForCall(i int) pagination.Page { + fake.extractPoolMembersMutex.RLock() + defer fake.extractPoolMembersMutex.RUnlock() + argsForCall := fake.extractPoolMembersArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeLoadbalancerFacade) ExtractPoolMembersReturns(result1 []pools.Member, result2 error) { + fake.extractPoolMembersMutex.Lock() + defer fake.extractPoolMembersMutex.Unlock() + fake.ExtractPoolMembersStub = nil + fake.extractPoolMembersReturns = struct { + result1 []pools.Member + result2 error + }{result1, result2} +} + +func (fake *FakeLoadbalancerFacade) ExtractPoolMembersReturnsOnCall(i int, result1 []pools.Member, result2 error) { + fake.extractPoolMembersMutex.Lock() + defer fake.extractPoolMembersMutex.Unlock() + fake.ExtractPoolMembersStub = nil + if fake.extractPoolMembersReturnsOnCall == nil { + fake.extractPoolMembersReturnsOnCall = make(map[int]struct { + result1 []pools.Member + result2 error + }) + } + fake.extractPoolMembersReturnsOnCall[i] = struct { + result1 []pools.Member + result2 error + }{result1, result2} +} + +func (fake *FakeLoadbalancerFacade) ExtractPools(arg1 pagination.Page) ([]pools.Pool, error) { + fake.extractPoolsMutex.Lock() + ret, specificReturn := fake.extractPoolsReturnsOnCall[len(fake.extractPoolsArgsForCall)] + fake.extractPoolsArgsForCall = append(fake.extractPoolsArgsForCall, struct { + arg1 pagination.Page + }{arg1}) + stub := fake.ExtractPoolsStub + fakeReturns := fake.extractPoolsReturns + fake.recordInvocation("ExtractPools", []interface{}{arg1}) + fake.extractPoolsMutex.Unlock() + if stub != nil { + return stub(arg1) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeLoadbalancerFacade) ExtractPoolsCallCount() int { + fake.extractPoolsMutex.RLock() + defer fake.extractPoolsMutex.RUnlock() + return len(fake.extractPoolsArgsForCall) +} + +func (fake *FakeLoadbalancerFacade) ExtractPoolsCalls(stub func(pagination.Page) ([]pools.Pool, error)) { + fake.extractPoolsMutex.Lock() + defer fake.extractPoolsMutex.Unlock() + fake.ExtractPoolsStub = stub +} + +func (fake *FakeLoadbalancerFacade) ExtractPoolsArgsForCall(i int) pagination.Page { + fake.extractPoolsMutex.RLock() + defer fake.extractPoolsMutex.RUnlock() + argsForCall := fake.extractPoolsArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeLoadbalancerFacade) ExtractPoolsReturns(result1 []pools.Pool, result2 error) { + fake.extractPoolsMutex.Lock() + defer fake.extractPoolsMutex.Unlock() + fake.ExtractPoolsStub = nil + fake.extractPoolsReturns = struct { + result1 []pools.Pool + result2 error + }{result1, result2} +} + +func (fake *FakeLoadbalancerFacade) ExtractPoolsReturnsOnCall(i int, result1 []pools.Pool, result2 error) { + fake.extractPoolsMutex.Lock() + defer fake.extractPoolsMutex.Unlock() + fake.ExtractPoolsStub = nil + if fake.extractPoolsReturnsOnCall == nil { + fake.extractPoolsReturnsOnCall = make(map[int]struct { + result1 []pools.Pool + result2 error + }) + } + fake.extractPoolsReturnsOnCall[i] = struct { + result1 []pools.Pool + result2 error + }{result1, result2} +} + +func (fake *FakeLoadbalancerFacade) GetListener(arg1 utils.RetryableServiceClient, arg2 string) (*listeners.Listener, error) { + fake.getListenerMutex.Lock() + ret, specificReturn := fake.getListenerReturnsOnCall[len(fake.getListenerArgsForCall)] + fake.getListenerArgsForCall = append(fake.getListenerArgsForCall, struct { + arg1 utils.RetryableServiceClient + arg2 string + }{arg1, arg2}) + stub := fake.GetListenerStub + fakeReturns := fake.getListenerReturns + fake.recordInvocation("GetListener", []interface{}{arg1, arg2}) + fake.getListenerMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeLoadbalancerFacade) GetListenerCallCount() int { + fake.getListenerMutex.RLock() + defer fake.getListenerMutex.RUnlock() + return len(fake.getListenerArgsForCall) +} + +func (fake *FakeLoadbalancerFacade) GetListenerCalls(stub func(utils.RetryableServiceClient, string) (*listeners.Listener, error)) { + fake.getListenerMutex.Lock() + defer fake.getListenerMutex.Unlock() + fake.GetListenerStub = stub +} + +func (fake *FakeLoadbalancerFacade) GetListenerArgsForCall(i int) (utils.RetryableServiceClient, string) { + fake.getListenerMutex.RLock() + defer fake.getListenerMutex.RUnlock() + argsForCall := fake.getListenerArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeLoadbalancerFacade) GetListenerReturns(result1 *listeners.Listener, result2 error) { + fake.getListenerMutex.Lock() + defer fake.getListenerMutex.Unlock() + fake.GetListenerStub = nil + fake.getListenerReturns = struct { + result1 *listeners.Listener + result2 error + }{result1, result2} +} + +func (fake *FakeLoadbalancerFacade) GetListenerReturnsOnCall(i int, result1 *listeners.Listener, result2 error) { + fake.getListenerMutex.Lock() + defer fake.getListenerMutex.Unlock() + fake.GetListenerStub = nil + if fake.getListenerReturnsOnCall == nil { + fake.getListenerReturnsOnCall = make(map[int]struct { + result1 *listeners.Listener + result2 error + }) + } + fake.getListenerReturnsOnCall[i] = struct { + result1 *listeners.Listener + result2 error + }{result1, result2} +} + +func (fake *FakeLoadbalancerFacade) GetLoadbalancer(arg1 utils.RetryableServiceClient, arg2 string) (*loadbalancers.LoadBalancer, error) { + fake.getLoadbalancerMutex.Lock() + ret, specificReturn := fake.getLoadbalancerReturnsOnCall[len(fake.getLoadbalancerArgsForCall)] + fake.getLoadbalancerArgsForCall = append(fake.getLoadbalancerArgsForCall, struct { + arg1 utils.RetryableServiceClient + arg2 string + }{arg1, arg2}) + stub := fake.GetLoadbalancerStub + fakeReturns := fake.getLoadbalancerReturns + fake.recordInvocation("GetLoadbalancer", []interface{}{arg1, arg2}) + fake.getLoadbalancerMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeLoadbalancerFacade) GetLoadbalancerCallCount() int { + fake.getLoadbalancerMutex.RLock() + defer fake.getLoadbalancerMutex.RUnlock() + return len(fake.getLoadbalancerArgsForCall) +} + +func (fake *FakeLoadbalancerFacade) GetLoadbalancerCalls(stub func(utils.RetryableServiceClient, string) (*loadbalancers.LoadBalancer, error)) { + fake.getLoadbalancerMutex.Lock() + defer fake.getLoadbalancerMutex.Unlock() + fake.GetLoadbalancerStub = stub +} + +func (fake *FakeLoadbalancerFacade) GetLoadbalancerArgsForCall(i int) (utils.RetryableServiceClient, string) { + fake.getLoadbalancerMutex.RLock() + defer fake.getLoadbalancerMutex.RUnlock() + argsForCall := fake.getLoadbalancerArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeLoadbalancerFacade) GetLoadbalancerReturns(result1 *loadbalancers.LoadBalancer, result2 error) { + fake.getLoadbalancerMutex.Lock() + defer fake.getLoadbalancerMutex.Unlock() + fake.GetLoadbalancerStub = nil + fake.getLoadbalancerReturns = struct { + result1 *loadbalancers.LoadBalancer + result2 error + }{result1, result2} +} + +func (fake *FakeLoadbalancerFacade) GetLoadbalancerReturnsOnCall(i int, result1 *loadbalancers.LoadBalancer, result2 error) { + fake.getLoadbalancerMutex.Lock() + defer fake.getLoadbalancerMutex.Unlock() + fake.GetLoadbalancerStub = nil + if fake.getLoadbalancerReturnsOnCall == nil { + fake.getLoadbalancerReturnsOnCall = make(map[int]struct { + result1 *loadbalancers.LoadBalancer + result2 error + }) + } + fake.getLoadbalancerReturnsOnCall[i] = struct { + result1 *loadbalancers.LoadBalancer + result2 error + }{result1, result2} +} + +func (fake *FakeLoadbalancerFacade) GetPool(arg1 utils.RetryableServiceClient, arg2 string) (*pools.Pool, error) { + fake.getPoolMutex.Lock() + ret, specificReturn := fake.getPoolReturnsOnCall[len(fake.getPoolArgsForCall)] + fake.getPoolArgsForCall = append(fake.getPoolArgsForCall, struct { + arg1 utils.RetryableServiceClient + arg2 string + }{arg1, arg2}) + stub := fake.GetPoolStub + fakeReturns := fake.getPoolReturns + fake.recordInvocation("GetPool", []interface{}{arg1, arg2}) + fake.getPoolMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeLoadbalancerFacade) GetPoolCallCount() int { + fake.getPoolMutex.RLock() + defer fake.getPoolMutex.RUnlock() + return len(fake.getPoolArgsForCall) +} + +func (fake *FakeLoadbalancerFacade) GetPoolCalls(stub func(utils.RetryableServiceClient, string) (*pools.Pool, error)) { + fake.getPoolMutex.Lock() + defer fake.getPoolMutex.Unlock() + fake.GetPoolStub = stub +} + +func (fake *FakeLoadbalancerFacade) GetPoolArgsForCall(i int) (utils.RetryableServiceClient, string) { + fake.getPoolMutex.RLock() + defer fake.getPoolMutex.RUnlock() + argsForCall := fake.getPoolArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeLoadbalancerFacade) GetPoolReturns(result1 *pools.Pool, result2 error) { + fake.getPoolMutex.Lock() + defer fake.getPoolMutex.Unlock() + fake.GetPoolStub = nil + fake.getPoolReturns = struct { + result1 *pools.Pool + result2 error + }{result1, result2} +} + +func (fake *FakeLoadbalancerFacade) GetPoolReturnsOnCall(i int, result1 *pools.Pool, result2 error) { + fake.getPoolMutex.Lock() + defer fake.getPoolMutex.Unlock() + fake.GetPoolStub = nil + if fake.getPoolReturnsOnCall == nil { + fake.getPoolReturnsOnCall = make(map[int]struct { + result1 *pools.Pool + result2 error + }) + } + fake.getPoolReturnsOnCall[i] = struct { + result1 *pools.Pool + result2 error + }{result1, result2} +} + +func (fake *FakeLoadbalancerFacade) ListPoolMembers(arg1 utils.RetryableServiceClient, arg2 string, arg3 pools.ListMembersOpts) (pagination.Page, error) { + fake.listPoolMembersMutex.Lock() + ret, specificReturn := fake.listPoolMembersReturnsOnCall[len(fake.listPoolMembersArgsForCall)] + fake.listPoolMembersArgsForCall = append(fake.listPoolMembersArgsForCall, struct { + arg1 utils.RetryableServiceClient + arg2 string + arg3 pools.ListMembersOpts + }{arg1, arg2, arg3}) + stub := fake.ListPoolMembersStub + fakeReturns := fake.listPoolMembersReturns + fake.recordInvocation("ListPoolMembers", []interface{}{arg1, arg2, arg3}) + fake.listPoolMembersMutex.Unlock() + if stub != nil { + return stub(arg1, arg2, arg3) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeLoadbalancerFacade) ListPoolMembersCallCount() int { + fake.listPoolMembersMutex.RLock() + defer fake.listPoolMembersMutex.RUnlock() + return len(fake.listPoolMembersArgsForCall) +} + +func (fake *FakeLoadbalancerFacade) ListPoolMembersCalls(stub func(utils.RetryableServiceClient, string, pools.ListMembersOpts) (pagination.Page, error)) { + fake.listPoolMembersMutex.Lock() + defer fake.listPoolMembersMutex.Unlock() + fake.ListPoolMembersStub = stub +} + +func (fake *FakeLoadbalancerFacade) ListPoolMembersArgsForCall(i int) (utils.RetryableServiceClient, string, pools.ListMembersOpts) { + fake.listPoolMembersMutex.RLock() + defer fake.listPoolMembersMutex.RUnlock() + argsForCall := fake.listPoolMembersArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 +} + +func (fake *FakeLoadbalancerFacade) ListPoolMembersReturns(result1 pagination.Page, result2 error) { + fake.listPoolMembersMutex.Lock() + defer fake.listPoolMembersMutex.Unlock() + fake.ListPoolMembersStub = nil + fake.listPoolMembersReturns = struct { + result1 pagination.Page + result2 error + }{result1, result2} +} + +func (fake *FakeLoadbalancerFacade) ListPoolMembersReturnsOnCall(i int, result1 pagination.Page, result2 error) { + fake.listPoolMembersMutex.Lock() + defer fake.listPoolMembersMutex.Unlock() + fake.ListPoolMembersStub = nil + if fake.listPoolMembersReturnsOnCall == nil { + fake.listPoolMembersReturnsOnCall = make(map[int]struct { + result1 pagination.Page + result2 error + }) + } + fake.listPoolMembersReturnsOnCall[i] = struct { + result1 pagination.Page + result2 error + }{result1, result2} +} + +func (fake *FakeLoadbalancerFacade) ListPools(arg1 utils.RetryableServiceClient, arg2 pools.ListOpts) (pagination.Page, error) { + fake.listPoolsMutex.Lock() + ret, specificReturn := fake.listPoolsReturnsOnCall[len(fake.listPoolsArgsForCall)] + fake.listPoolsArgsForCall = append(fake.listPoolsArgsForCall, struct { + arg1 utils.RetryableServiceClient + arg2 pools.ListOpts + }{arg1, arg2}) + stub := fake.ListPoolsStub + fakeReturns := fake.listPoolsReturns + fake.recordInvocation("ListPools", []interface{}{arg1, arg2}) + fake.listPoolsMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeLoadbalancerFacade) ListPoolsCallCount() int { + fake.listPoolsMutex.RLock() + defer fake.listPoolsMutex.RUnlock() + return len(fake.listPoolsArgsForCall) +} + +func (fake *FakeLoadbalancerFacade) ListPoolsCalls(stub func(utils.RetryableServiceClient, pools.ListOpts) (pagination.Page, error)) { + fake.listPoolsMutex.Lock() + defer fake.listPoolsMutex.Unlock() + fake.ListPoolsStub = stub +} + +func (fake *FakeLoadbalancerFacade) ListPoolsArgsForCall(i int) (utils.RetryableServiceClient, pools.ListOpts) { + fake.listPoolsMutex.RLock() + defer fake.listPoolsMutex.RUnlock() + argsForCall := fake.listPoolsArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeLoadbalancerFacade) ListPoolsReturns(result1 pagination.Page, result2 error) { + fake.listPoolsMutex.Lock() + defer fake.listPoolsMutex.Unlock() + fake.ListPoolsStub = nil + fake.listPoolsReturns = struct { + result1 pagination.Page + result2 error + }{result1, result2} +} + +func (fake *FakeLoadbalancerFacade) ListPoolsReturnsOnCall(i int, result1 pagination.Page, result2 error) { + fake.listPoolsMutex.Lock() + defer fake.listPoolsMutex.Unlock() + fake.ListPoolsStub = nil + if fake.listPoolsReturnsOnCall == nil { + fake.listPoolsReturnsOnCall = make(map[int]struct { + result1 pagination.Page + result2 error + }) + } + fake.listPoolsReturnsOnCall[i] = struct { + result1 pagination.Page + result2 error + }{result1, result2} +} + +func (fake *FakeLoadbalancerFacade) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.createPoolMemberMutex.RLock() + defer fake.createPoolMemberMutex.RUnlock() + fake.deletePoolMemberMutex.RLock() + defer fake.deletePoolMemberMutex.RUnlock() + fake.extractPoolMembersMutex.RLock() + defer fake.extractPoolMembersMutex.RUnlock() + fake.extractPoolsMutex.RLock() + defer fake.extractPoolsMutex.RUnlock() + fake.getListenerMutex.RLock() + defer fake.getListenerMutex.RUnlock() + fake.getLoadbalancerMutex.RLock() + defer fake.getLoadbalancerMutex.RUnlock() + fake.getPoolMutex.RLock() + defer fake.getPoolMutex.RUnlock() + fake.listPoolMembersMutex.RLock() + defer fake.listPoolMembersMutex.RUnlock() + fake.listPoolsMutex.RLock() + defer fake.listPoolsMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *FakeLoadbalancerFacade) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ loadbalancer.LoadbalancerFacade = new(FakeLoadbalancerFacade) diff --git a/src/openstack_cpi_golang/cpi/loadbalancer/loadbalancerfakes/fake_loadbalancer_service.go b/src/openstack_cpi_golang/cpi/loadbalancer/loadbalancerfakes/fake_loadbalancer_service.go new file mode 100644 index 00000000..9283d94f --- /dev/null +++ b/src/openstack_cpi_golang/cpi/loadbalancer/loadbalancerfakes/fake_loadbalancer_service.go @@ -0,0 +1,283 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package loadbalancerfakes + +import ( + "sync" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/loadbalancer" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/properties" + "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/pools" +) + +type FakeLoadbalancerService struct { + CreatePoolMemberStub func(pools.Pool, string, properties.LoadbalancerPool, string, int) (*pools.Member, error) + createPoolMemberMutex sync.RWMutex + createPoolMemberArgsForCall []struct { + arg1 pools.Pool + arg2 string + arg3 properties.LoadbalancerPool + arg4 string + arg5 int + } + createPoolMemberReturns struct { + result1 *pools.Member + result2 error + } + createPoolMemberReturnsOnCall map[int]struct { + result1 *pools.Member + result2 error + } + DeletePoolMemberStub func(string, string, int) error + deletePoolMemberMutex sync.RWMutex + deletePoolMemberArgsForCall []struct { + arg1 string + arg2 string + arg3 int + } + deletePoolMemberReturns struct { + result1 error + } + deletePoolMemberReturnsOnCall map[int]struct { + result1 error + } + GetPoolStub func(string) (pools.Pool, error) + getPoolMutex sync.RWMutex + getPoolArgsForCall []struct { + arg1 string + } + getPoolReturns struct { + result1 pools.Pool + result2 error + } + getPoolReturnsOnCall map[int]struct { + result1 pools.Pool + result2 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *FakeLoadbalancerService) CreatePoolMember(arg1 pools.Pool, arg2 string, arg3 properties.LoadbalancerPool, arg4 string, arg5 int) (*pools.Member, error) { + fake.createPoolMemberMutex.Lock() + ret, specificReturn := fake.createPoolMemberReturnsOnCall[len(fake.createPoolMemberArgsForCall)] + fake.createPoolMemberArgsForCall = append(fake.createPoolMemberArgsForCall, struct { + arg1 pools.Pool + arg2 string + arg3 properties.LoadbalancerPool + arg4 string + arg5 int + }{arg1, arg2, arg3, arg4, arg5}) + stub := fake.CreatePoolMemberStub + fakeReturns := fake.createPoolMemberReturns + fake.recordInvocation("CreatePoolMember", []interface{}{arg1, arg2, arg3, arg4, arg5}) + fake.createPoolMemberMutex.Unlock() + if stub != nil { + return stub(arg1, arg2, arg3, arg4, arg5) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeLoadbalancerService) CreatePoolMemberCallCount() int { + fake.createPoolMemberMutex.RLock() + defer fake.createPoolMemberMutex.RUnlock() + return len(fake.createPoolMemberArgsForCall) +} + +func (fake *FakeLoadbalancerService) CreatePoolMemberCalls(stub func(pools.Pool, string, properties.LoadbalancerPool, string, int) (*pools.Member, error)) { + fake.createPoolMemberMutex.Lock() + defer fake.createPoolMemberMutex.Unlock() + fake.CreatePoolMemberStub = stub +} + +func (fake *FakeLoadbalancerService) CreatePoolMemberArgsForCall(i int) (pools.Pool, string, properties.LoadbalancerPool, string, int) { + fake.createPoolMemberMutex.RLock() + defer fake.createPoolMemberMutex.RUnlock() + argsForCall := fake.createPoolMemberArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4, argsForCall.arg5 +} + +func (fake *FakeLoadbalancerService) CreatePoolMemberReturns(result1 *pools.Member, result2 error) { + fake.createPoolMemberMutex.Lock() + defer fake.createPoolMemberMutex.Unlock() + fake.CreatePoolMemberStub = nil + fake.createPoolMemberReturns = struct { + result1 *pools.Member + result2 error + }{result1, result2} +} + +func (fake *FakeLoadbalancerService) CreatePoolMemberReturnsOnCall(i int, result1 *pools.Member, result2 error) { + fake.createPoolMemberMutex.Lock() + defer fake.createPoolMemberMutex.Unlock() + fake.CreatePoolMemberStub = nil + if fake.createPoolMemberReturnsOnCall == nil { + fake.createPoolMemberReturnsOnCall = make(map[int]struct { + result1 *pools.Member + result2 error + }) + } + fake.createPoolMemberReturnsOnCall[i] = struct { + result1 *pools.Member + result2 error + }{result1, result2} +} + +func (fake *FakeLoadbalancerService) DeletePoolMember(arg1 string, arg2 string, arg3 int) error { + fake.deletePoolMemberMutex.Lock() + ret, specificReturn := fake.deletePoolMemberReturnsOnCall[len(fake.deletePoolMemberArgsForCall)] + fake.deletePoolMemberArgsForCall = append(fake.deletePoolMemberArgsForCall, struct { + arg1 string + arg2 string + arg3 int + }{arg1, arg2, arg3}) + stub := fake.DeletePoolMemberStub + fakeReturns := fake.deletePoolMemberReturns + fake.recordInvocation("DeletePoolMember", []interface{}{arg1, arg2, arg3}) + fake.deletePoolMemberMutex.Unlock() + if stub != nil { + return stub(arg1, arg2, arg3) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeLoadbalancerService) DeletePoolMemberCallCount() int { + fake.deletePoolMemberMutex.RLock() + defer fake.deletePoolMemberMutex.RUnlock() + return len(fake.deletePoolMemberArgsForCall) +} + +func (fake *FakeLoadbalancerService) DeletePoolMemberCalls(stub func(string, string, int) error) { + fake.deletePoolMemberMutex.Lock() + defer fake.deletePoolMemberMutex.Unlock() + fake.DeletePoolMemberStub = stub +} + +func (fake *FakeLoadbalancerService) DeletePoolMemberArgsForCall(i int) (string, string, int) { + fake.deletePoolMemberMutex.RLock() + defer fake.deletePoolMemberMutex.RUnlock() + argsForCall := fake.deletePoolMemberArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 +} + +func (fake *FakeLoadbalancerService) DeletePoolMemberReturns(result1 error) { + fake.deletePoolMemberMutex.Lock() + defer fake.deletePoolMemberMutex.Unlock() + fake.DeletePoolMemberStub = nil + fake.deletePoolMemberReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeLoadbalancerService) DeletePoolMemberReturnsOnCall(i int, result1 error) { + fake.deletePoolMemberMutex.Lock() + defer fake.deletePoolMemberMutex.Unlock() + fake.DeletePoolMemberStub = nil + if fake.deletePoolMemberReturnsOnCall == nil { + fake.deletePoolMemberReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.deletePoolMemberReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *FakeLoadbalancerService) GetPool(arg1 string) (pools.Pool, error) { + fake.getPoolMutex.Lock() + ret, specificReturn := fake.getPoolReturnsOnCall[len(fake.getPoolArgsForCall)] + fake.getPoolArgsForCall = append(fake.getPoolArgsForCall, struct { + arg1 string + }{arg1}) + stub := fake.GetPoolStub + fakeReturns := fake.getPoolReturns + fake.recordInvocation("GetPool", []interface{}{arg1}) + fake.getPoolMutex.Unlock() + if stub != nil { + return stub(arg1) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeLoadbalancerService) GetPoolCallCount() int { + fake.getPoolMutex.RLock() + defer fake.getPoolMutex.RUnlock() + return len(fake.getPoolArgsForCall) +} + +func (fake *FakeLoadbalancerService) GetPoolCalls(stub func(string) (pools.Pool, error)) { + fake.getPoolMutex.Lock() + defer fake.getPoolMutex.Unlock() + fake.GetPoolStub = stub +} + +func (fake *FakeLoadbalancerService) GetPoolArgsForCall(i int) string { + fake.getPoolMutex.RLock() + defer fake.getPoolMutex.RUnlock() + argsForCall := fake.getPoolArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeLoadbalancerService) GetPoolReturns(result1 pools.Pool, result2 error) { + fake.getPoolMutex.Lock() + defer fake.getPoolMutex.Unlock() + fake.GetPoolStub = nil + fake.getPoolReturns = struct { + result1 pools.Pool + result2 error + }{result1, result2} +} + +func (fake *FakeLoadbalancerService) GetPoolReturnsOnCall(i int, result1 pools.Pool, result2 error) { + fake.getPoolMutex.Lock() + defer fake.getPoolMutex.Unlock() + fake.GetPoolStub = nil + if fake.getPoolReturnsOnCall == nil { + fake.getPoolReturnsOnCall = make(map[int]struct { + result1 pools.Pool + result2 error + }) + } + fake.getPoolReturnsOnCall[i] = struct { + result1 pools.Pool + result2 error + }{result1, result2} +} + +func (fake *FakeLoadbalancerService) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.createPoolMemberMutex.RLock() + defer fake.createPoolMemberMutex.RUnlock() + fake.deletePoolMemberMutex.RLock() + defer fake.deletePoolMemberMutex.RUnlock() + fake.getPoolMutex.RLock() + defer fake.getPoolMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *FakeLoadbalancerService) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ loadbalancer.LoadbalancerService = new(FakeLoadbalancerService) diff --git a/src/openstack_cpi_golang/cpi/loadbalancer/loadbalancerfakes/fake_loadbalancer_service_builder.go b/src/openstack_cpi_golang/cpi/loadbalancer/loadbalancerfakes/fake_loadbalancer_service_builder.go new file mode 100644 index 00000000..6cfa7521 --- /dev/null +++ b/src/openstack_cpi_golang/cpi/loadbalancer/loadbalancerfakes/fake_loadbalancer_service_builder.go @@ -0,0 +1,107 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package loadbalancerfakes + +import ( + "sync" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/loadbalancer" +) + +type FakeLoadbalancerServiceBuilder struct { + BuildStub func() (loadbalancer.LoadbalancerService, error) + buildMutex sync.RWMutex + buildArgsForCall []struct { + } + buildReturns struct { + result1 loadbalancer.LoadbalancerService + result2 error + } + buildReturnsOnCall map[int]struct { + result1 loadbalancer.LoadbalancerService + result2 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *FakeLoadbalancerServiceBuilder) Build() (loadbalancer.LoadbalancerService, error) { + fake.buildMutex.Lock() + ret, specificReturn := fake.buildReturnsOnCall[len(fake.buildArgsForCall)] + fake.buildArgsForCall = append(fake.buildArgsForCall, struct { + }{}) + stub := fake.BuildStub + fakeReturns := fake.buildReturns + fake.recordInvocation("Build", []interface{}{}) + fake.buildMutex.Unlock() + if stub != nil { + return stub() + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeLoadbalancerServiceBuilder) BuildCallCount() int { + fake.buildMutex.RLock() + defer fake.buildMutex.RUnlock() + return len(fake.buildArgsForCall) +} + +func (fake *FakeLoadbalancerServiceBuilder) BuildCalls(stub func() (loadbalancer.LoadbalancerService, error)) { + fake.buildMutex.Lock() + defer fake.buildMutex.Unlock() + fake.BuildStub = stub +} + +func (fake *FakeLoadbalancerServiceBuilder) BuildReturns(result1 loadbalancer.LoadbalancerService, result2 error) { + fake.buildMutex.Lock() + defer fake.buildMutex.Unlock() + fake.BuildStub = nil + fake.buildReturns = struct { + result1 loadbalancer.LoadbalancerService + result2 error + }{result1, result2} +} + +func (fake *FakeLoadbalancerServiceBuilder) BuildReturnsOnCall(i int, result1 loadbalancer.LoadbalancerService, result2 error) { + fake.buildMutex.Lock() + defer fake.buildMutex.Unlock() + fake.BuildStub = nil + if fake.buildReturnsOnCall == nil { + fake.buildReturnsOnCall = make(map[int]struct { + result1 loadbalancer.LoadbalancerService + result2 error + }) + } + fake.buildReturnsOnCall[i] = struct { + result1 loadbalancer.LoadbalancerService + result2 error + }{result1, result2} +} + +func (fake *FakeLoadbalancerServiceBuilder) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.buildMutex.RLock() + defer fake.buildMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *FakeLoadbalancerServiceBuilder) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ loadbalancer.LoadbalancerServiceBuilder = new(FakeLoadbalancerServiceBuilder) diff --git a/src/openstack_cpi_golang/cpi/methods/attach_disk.go b/src/openstack_cpi_golang/cpi/methods/attach_disk.go index e0b6d626..ff20b15a 100644 --- a/src/openstack_cpi_golang/cpi/methods/attach_disk.go +++ b/src/openstack_cpi_golang/cpi/methods/attach_disk.go @@ -1,20 +1,214 @@ package methods import ( + "fmt" + "regexp" + "time" + "github.com/cloudfoundry/bosh-cpi-go/apiv1" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/compute" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/config" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/volume" + "github.com/gophercloud/gophercloud/openstack/blockstorage/v3/volumes" + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/volumeattach" + "github.com/gophercloud/gophercloud/openstack/compute/v2/servers" ) type AttachDiskMethod struct { + computeServiceBuilder compute.ComputeServiceBuilder + volumeServiceBuilder volume.VolumeServiceBuilder + cpiConfig config.CpiConfig + logger utils.Logger } -func NewAttachDiskMethod() AttachDiskMethod { - return AttachDiskMethod{} +func NewAttachDiskMethod( + computeServiceBuilder compute.ComputeServiceBuilder, + volumeServiceBuilder volume.VolumeServiceBuilder, + cpiConfig config.CpiConfig, + logger utils.Logger, +) AttachDiskMethod { + return AttachDiskMethod{ + computeServiceBuilder: computeServiceBuilder, + volumeServiceBuilder: volumeServiceBuilder, + cpiConfig: cpiConfig, + logger: logger, + } } -func (a AttachDiskMethod) AttachDisk(vmCID apiv1.VMCID, diskCID apiv1.DiskCID) error { +var initialDiskHint = apiv1.DiskHint{} + +func (a AttachDiskMethod) attachDisk(vmCID apiv1.VMCID, diskCID apiv1.DiskCID, returnDiskHint bool) (apiv1.DiskHint, error) { + + openstackConfig := a.cpiConfig.Cloud.Properties.Openstack + diskHint := initialDiskHint + + a.logger.Info("attach_disk", fmt.Sprintf("Execute attach disk ID %s to VM ID %s", diskCID.AsString(), vmCID.AsString())) + volumeService, err := a.volumeServiceBuilder.Build() + if err != nil { + return diskHint, fmt.Errorf("attach_disk: Failed to get volume service: %w", err) + } + diskVolume, err := volumeService.GetVolume(diskCID.AsString()) + if err != nil { + return diskHint, fmt.Errorf("attach_disk: Failed to get volume with ID %s: %w", diskCID.AsString(), err) + } + if len(diskVolume.Attachments) == 1 && diskVolume.Attachments[0].ServerID == vmCID.AsString() { + a.logger.Info("attach_disk", fmt.Sprintf("Volume ID %s is already attached to VM ID %s", diskCID.AsString(), vmCID.AsString())) + if returnDiskHint { + diskHint = a.getDiskHint(*diskVolume, nil) + } + return diskHint, nil + } + err = a.checkDiskAttach(*diskVolume, vmCID) + if err != nil { + return diskHint, fmt.Errorf("attach_disk: Disk with ID %s cannot be attached: %w", diskCID.AsString(), err) + } + // attach disk to VM + computeService, err := a.computeServiceBuilder.Build() + if err != nil { + return diskHint, fmt.Errorf("attach_disk: Failed to get compute service for disk ID %s: %w", diskCID.AsString(), err) + } + server, err := computeService.GetServer(vmCID.AsString()) + if err != nil { + return diskHint, fmt.Errorf("attach_disk: Failed to get VM %s for disk ID %s: %w", vmCID.AsString(), diskCID.AsString(), err) + } + mountPoint, err := a.getMountPoint(computeService, *server) + if err != nil { + return diskHint, fmt.Errorf("attach_disk: Failed to get mount point for disk ID %s: %w", diskCID.AsString(), err) + } + a.logger.Debug("attach_disk", fmt.Sprintf("Attaching volume ID: %s, server: %s, mountPoint: %s", diskCID.AsString(), vmCID.AsString(), mountPoint)) + volumeAttachment, err := computeService.AttachVolume(server.ID, diskCID.AsString(), mountPoint) + if err != nil { + return diskHint, fmt.Errorf("attach_disk: Failed to attach volume ID %s to VM ID %s: %w", diskVolume.ID, server.ID, err) + } + a.logger.Debug("attach_disk", fmt.Sprintf("Attaching volume DONE: Volume ID: %s, VM ID: %s, mountPoint: %s", volumeAttachment.VolumeID, volumeAttachment.ServerID, volumeAttachment.Device)) + a.logger.Debug("attach_disk", fmt.Sprintf("Waiting for volume ID %s to get in use by VM ID %s (time: %d secs)", diskCID.AsString(), vmCID.AsString(), openstackConfig.StateTimeOut)) + err = volumeService.WaitForVolumeToBecomeStatus(diskCID.AsString(), time.Duration(a.cpiConfig.Cloud.Properties.Openstack.StateTimeOut)*time.Second, "in-use") + if err != nil { + return diskHint, fmt.Errorf("attach_disk: Timeout on waiting to attach volume ID %s to VM %s (waiting: %d sec): %w", diskVolume.ID, server.ID, a.cpiConfig.Cloud.Properties.Openstack.StateTimeOut, err) + } + a.logger.Info("attach_disk", fmt.Sprintf("Successfully attached volume ID %s to VM %s (Volume status now: 'in-use')", diskCID.AsString(), vmCID.AsString())) + if returnDiskHint { + diskHint = a.getDiskHint(*diskVolume, volumeService) + } + return diskHint, nil +} + +func (a AttachDiskMethod) checkDiskAttach(diskVolume volumes.Volume, vmCID apiv1.VMCID) error { + + if (len(diskVolume.Attachments) > 1) || (len(diskVolume.Attachments) == 1 && diskVolume.Attachments[0].ServerID != vmCID.AsString()) { + return fmt.Errorf("volume %s is attached to another VM", diskVolume.ID) + } + // volume statuses: https://docs.openstack.org/api-ref/block-storage/v3/index.html#volumes-volumes + if diskVolume.Status != "available" { + return fmt.Errorf("volume %s has not status 'available', current status is '%s'", diskVolume.ID, diskVolume.Status) + } return nil } +func (a AttachDiskMethod) getFirstDeviceNameLetter(computeService compute.ComputeService, server servers.Server) (rune, error) { + inspectChar := 'b' + if server.Flavor == nil { + a.logger.Warn("getFirstDeviceNameLetter", fmt.Sprintf("No flavor for server %s found. Using device letter: %c", server.ID, inspectChar)) + return inspectChar, nil + } + idValue, ok := server.Flavor["id"].(string) + if !ok { + a.logger.Warn("getFirstDeviceNameLetter", fmt.Sprintf("No server flavor ID for server %s found. Using device letter: %c", server.ID, inspectChar)) + return inspectChar, nil + } + flavor, err := computeService.GetFlavorById(idValue) + if err != nil { + a.logger.Warn("getFirstDeviceNameLetter", fmt.Sprintf("No flavor setting for server %s found. Using device letter: %c", server.ID, inspectChar)) + return inspectChar, nil + } + if flavor.Ephemeral > 0 { + inspectChar = inspectChar + 1 + a.logger.Debug("getFirstDeviceNameLetter", fmt.Sprintf("Flavor ID %s has ephemeral disk. Switch device name letter: %c\n", flavor.ID, inspectChar)) + } + if flavor.Swap > 0 { + inspectChar = inspectChar + 1 + a.logger.Debug("getFirstDeviceNameLetter", fmt.Sprintf("Flavor ID %s has swap disk. Switch device name letter: %c\n", flavor.ID, inspectChar)) + } + configDrive := a.cpiConfig.OpenStackConfig().ConfigDrive + if configDrive == "disk" { + inspectChar = inspectChar + 1 + a.logger.Debug("getFirstDeviceNameLetter", fmt.Sprintf("ConfigDrive is set to 'disk'. Switch device name letter: %c\n", inspectChar)) + } + a.logger.Debug("getFirstDeviceNameLetter", fmt.Sprintf("Starting check for device letter: %c\n", inspectChar)) + return inspectChar, nil +} + +func (a AttachDiskMethod) getMountPoint(computeService compute.ComputeService, server servers.Server) (string, error) { + inspectChar, err := a.getFirstDeviceNameLetter(computeService, server) + if err != nil { + return "", fmt.Errorf("getMountPoint: Failed to get first device letter service: %w", err) + } + attachmentList, err := computeService.ListVolumeAttachments(server.ID) + if err != nil { + return "", fmt.Errorf("getMountPoint: Failed to get volume attachments for VM ID %s: %w", server.ID, err) + } + a.logger.Debug("getMountPoint", fmt.Sprintf("Attachments for VM ID %s", server.ID)) + for idx, attachment := range attachmentList { + a.logger.Debug("getMountPoint", fmt.Sprintf("%d: Existing attachment: device: %s, volume ID: %s", idx+1, attachment.Device, attachment.VolumeID)) + } + inspectChar, err = a.getDeviceChar(inspectChar, attachmentList) + if err != nil { + return "", fmt.Errorf("getMountPoint: failed to get device letter for server ID %s: %w", server.ID, err) + } + return fmt.Sprintf("/dev/sd%c", inspectChar), nil +} + +func (a AttachDiskMethod) getDeviceChar(inspectChar rune, attachments []volumeattach.VolumeAttachment) (rune, error) { + var deviceList []string + for _, attachment := range attachments { + deviceList = append(deviceList, attachment.Device) + } + for char := inspectChar; char <= 'z'; char++ { + devicePattern := fmt.Sprintf("\\/dev\\/(?:sd|vd|xvd)%c", char) + matcher := regexp.MustCompile(devicePattern) + found := false + for _, device := range deviceList { + if matcher.MatchString(device) { + found = true + break + } + } + if !found { + return char, nil + } + } + return ' ', fmt.Errorf("failed to get device letter") +} + +func (a AttachDiskMethod) getDiskHint(diskVolume volumes.Volume, volumeService volume.VolumeService) apiv1.DiskHint { + diskHint := initialDiskHint + if volumeService != nil { + currVolume, err := volumeService.GetVolume(diskVolume.ID) + if err != nil { + a.logger.Error("attach_disk", fmt.Sprintf("Failed to get volume: %v", err)) + } else { + if len(currVolume.Attachments) != 0 { + attachment := currVolume.Attachments[0] + diskHint = apiv1.NewDiskHintFromString(attachment.Device) + } + } + } else { + if len(diskVolume.Attachments) != 0 { + attachment := diskVolume.Attachments[0] + diskHint = apiv1.NewDiskHintFromString(attachment.Device) + } + } + a.logger.Debug("attach_disk", fmt.Sprintf("Use disk hint value: %v", diskHint)) + return diskHint +} + +func (a AttachDiskMethod) AttachDisk(vmCID apiv1.VMCID, diskCID apiv1.DiskCID) error { + _, err := a.attachDisk(vmCID, diskCID, false) + return err +} + func (a AttachDiskMethod) AttachDiskV2(vmCID apiv1.VMCID, diskCID apiv1.DiskCID) (apiv1.DiskHint, error) { - return apiv1.DiskHint{}, nil + diskHint, err := a.attachDisk(vmCID, diskCID, true) + return diskHint, err } diff --git a/src/openstack_cpi_golang/cpi/methods/attach_disk_test.go b/src/openstack_cpi_golang/cpi/methods/attach_disk_test.go new file mode 100644 index 00000000..7c9f4cb4 --- /dev/null +++ b/src/openstack_cpi_golang/cpi/methods/attach_disk_test.go @@ -0,0 +1,544 @@ +package methods_test + +import ( + "errors" + "fmt" + + "github.com/cloudfoundry/bosh-cpi-go/apiv1" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/compute/computefakes" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/config" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/methods" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils/utilsfakes" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/volume/volumefakes" + "github.com/gophercloud/gophercloud/openstack/blockstorage/v3/volumes" + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/volumeattach" + "github.com/gophercloud/gophercloud/openstack/compute/v2/flavors" + "github.com/gophercloud/gophercloud/openstack/compute/v2/servers" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("AttachDiskMethod Unit Tests", func() { + + const ( + volumeId1 = "vol1-id" + deviceA = "/dev/sda" + deviceB = "/dev/sdb" + deviceC = "/dev/sdc" + serverId = "VM-id" + anotherServerId = "anotherVM-id" + diskStatusAvailable = "available" + diskStatusNotAvailable = "creating" + diskStatusInUse = "in-use" + serverStatusActive = "ACTIVE" + serverStatusDeleted = "DELETED" + ) + + var ( + computeServiceBuilder *computefakes.FakeComputeServiceBuilder + computeService *computefakes.FakeComputeService + volumeServiceBuilder *volumefakes.FakeVolumeServiceBuilder + volumeService *volumefakes.FakeVolumeService + logger *utilsfakes.FakeLogger + cpiConfig config.CpiConfig + server servers.Server + ) + + Context("attaching disk to VM", func() { + BeforeEach(func() { + computeServiceBuilder = new(computefakes.FakeComputeServiceBuilder) + computeService = new(computefakes.FakeComputeService) + volumeServiceBuilder = new(volumefakes.FakeVolumeServiceBuilder) + volumeService = new(volumefakes.FakeVolumeService) + logger = new(utilsfakes.FakeLogger) + computeServiceBuilder.BuildReturns(computeService, nil) + cpiConfig = config.CpiConfig{} + }) + + It("fails on volume service builder (V1)", func() { + volumeServiceBuilder.BuildReturns(nil, errors.New("boom")) + attachDiskMethod := methods.NewAttachDiskMethod(computeServiceBuilder, volumeServiceBuilder, cpiConfig, logger) + vmCID := apiv1.VMCID{} + diskId := apiv1.DiskCID{} + err := attachDiskMethod.AttachDisk(vmCID, diskId) + Expect(err.Error()).To(Equal("attach_disk: Failed to get volume service: boom")) + }) + + It("fails on get server", func() { + volumeService.GetVolumeReturns(nil, errors.New("boom")) + volumeServiceBuilder.BuildReturns(volumeService, nil) + attachDiskMethod := methods.NewAttachDiskMethod(computeServiceBuilder, volumeServiceBuilder, cpiConfig, logger) + vmCID := apiv1.VMCID{} + diskId := apiv1.NewDiskCID(volumeId1) + err := attachDiskMethod.AttachDisk(vmCID, diskId) + Expect(err.Error()).To(Equal("attach_disk: Failed to get volume with ID vol1-id: boom")) + }) + + It("fails due to disk attach checks (V1): volume is attached to another VM", func() { + volume := volumes.Volume{ + ID: volumeId1, + Attachments: []volumes.Attachment{ + { + Device: deviceA, + ServerID: anotherServerId, + }, + }, + } + volumeService.GetVolumeReturns(&volume, nil) + volumeServiceBuilder.BuildReturns(volumeService, nil) + attachDiskMethod := methods.NewAttachDiskMethod(computeServiceBuilder, volumeServiceBuilder, cpiConfig, logger) + vmCID := apiv1.NewVMCID(serverId) + diskCID := apiv1.NewDiskCID(volumeId1) + err := attachDiskMethod.AttachDisk(vmCID, diskCID) + Expect(err.Error()).To(Equal(fmt.Sprintf("attach_disk: Disk with ID vol1-id cannot be attached: volume %s is attached to another VM", volumeId1))) + }) + + It("success attach (V1): volume is already attached to same VM", func() { + volume := volumes.Volume{ + ID: volumeId1, + Attachments: []volumes.Attachment{ + { + Device: deviceA, + ServerID: serverId, + }, + }, + } + volumeService.GetVolumeReturns(&volume, nil) + volumeServiceBuilder.BuildReturns(volumeService, nil) + attachDiskMethod := methods.NewAttachDiskMethod(computeServiceBuilder, volumeServiceBuilder, cpiConfig, logger) + vmCID := apiv1.NewVMCID(serverId) + diskCID := apiv1.NewDiskCID(volumeId1) + err := attachDiskMethod.AttachDisk(vmCID, diskCID) + Expect(err).NotTo(HaveOccurred()) + }) + + It("success attach (V2): volume is already attached to same VM", func() { + volume := volumes.Volume{ + ID: volumeId1, + Attachments: []volumes.Attachment{ + { + Device: deviceA, + ServerID: serverId, + }, + }, + } + volumeService.GetVolumeReturns(&volume, nil) + volumeServiceBuilder.BuildReturns(volumeService, nil) + attachDiskMethod := methods.NewAttachDiskMethod(computeServiceBuilder, volumeServiceBuilder, cpiConfig, logger) + vmCID := apiv1.NewVMCID(serverId) + diskCID := apiv1.NewDiskCID(volumeId1) + diskHint, err := attachDiskMethod.AttachDiskV2(vmCID, diskCID) + Expect(err).NotTo(HaveOccurred()) + diskHintExpected := apiv1.NewDiskHintFromString(deviceA) + Expect(diskHint).To(Equal(diskHintExpected)) + }) + + It("fails due to disk attach checks (V1): volume is not available", func() { + // volume statuses: https://docs.openstack.org/api-ref/block-storage/v3/index.html#volumes-volumes + volume := volumes.Volume{ + ID: volumeId1, + Status: diskStatusNotAvailable, + } + volumeService.GetVolumeReturns(&volume, nil) + volumeServiceBuilder.BuildReturns(volumeService, nil) + attachDiskMethod := methods.NewAttachDiskMethod(computeServiceBuilder, volumeServiceBuilder, cpiConfig, logger) + vmCID := apiv1.NewVMCID(serverId) + diskCID := apiv1.NewDiskCID(volumeId1) + err := attachDiskMethod.AttachDisk(vmCID, diskCID) + Expect(err.Error()).To(Equal(fmt.Sprintf("attach_disk: Disk with ID vol1-id cannot be attached: volume %s has not status 'available', current status is '%s'", volumeId1, diskStatusNotAvailable))) + }) + + It("fails due to compute service build (V1)", func() { + computeServiceBuilder.BuildReturns(nil, errors.New("boom")) + volume := volumes.Volume{ + ID: volumeId1, + Status: diskStatusAvailable, + } + volumeService.GetVolumeReturns(&volume, nil) + volumeServiceBuilder.BuildReturns(volumeService, nil) + attachDiskMethod := methods.NewAttachDiskMethod(computeServiceBuilder, volumeServiceBuilder, cpiConfig, logger) + vmCID := apiv1.NewVMCID(serverId) + diskCID := apiv1.NewDiskCID(volumeId1) + err := attachDiskMethod.AttachDisk(vmCID, diskCID) + Expect(err.Error()).To(Equal("attach_disk: Failed to get compute service for disk ID vol1-id: boom")) + }) + + It("fails due to get VM (V1)", func() { + server := servers.Server{} + computeService.GetServerReturns(&server, errors.New("boom")) + volume := volumes.Volume{ + ID: volumeId1, + Status: diskStatusAvailable, + } + volumeService.GetVolumeReturns(&volume, nil) + volumeServiceBuilder.BuildReturns(volumeService, nil) + attachDiskMethod := methods.NewAttachDiskMethod(computeServiceBuilder, volumeServiceBuilder, cpiConfig, logger) + vmCID := apiv1.NewVMCID(serverId) + diskCID := apiv1.NewDiskCID(volumeId1) + err := attachDiskMethod.AttachDisk(vmCID, diskCID) + Expect(err.Error()).To(Equal(fmt.Sprintf("attach_disk: Failed to get VM %s for disk ID %s: boom", serverId, volumeId1))) + }) + + It("fails due to VM status is DELETED or TERMINATED (V1)", func() { + server := servers.Server{ + ID: serverId, + Status: serverStatusDeleted, + } + computeService.GetServerReturns(&server, nil) + volume := volumes.Volume{ + ID: volumeId1, + Status: diskStatusAvailable, + } + volumeService.GetVolumeReturns(&volume, nil) + volumeServiceBuilder.BuildReturns(volumeService, nil) + attachDiskMethod := methods.NewAttachDiskMethod(computeServiceBuilder, volumeServiceBuilder, cpiConfig, logger) + vmCID := apiv1.NewVMCID(serverId) + diskCID := apiv1.NewDiskCID(volumeId1) + // since we do not explicitly check any more for VM status (DELETED, TERMINATED), we expect an error from attachDisk itself + // this is just a simulation of the previous behavior with explicit check + computeService.AttachVolumeReturns(nil, errors.New("VM is in invalid state")) + + err := attachDiskMethod.AttachDisk(vmCID, diskCID) + Expect(err.Error()).To(Equal(fmt.Sprintf("attach_disk: Failed to attach volume ID %s to VM ID %s: VM is in invalid state", volumeId1, serverId))) + }) + + It("fails due to attach disk failure (V1)", func() { + server := servers.Server{ + ID: serverId, + Status: serverStatusActive, + } + computeService.GetServerReturns(&server, nil) + volume := volumes.Volume{ + ID: volumeId1, + Status: diskStatusAvailable, + } + volumeService.GetVolumeReturns(&volume, nil) + volumeServiceBuilder.BuildReturns(volumeService, nil) + computeService.AttachVolumeReturns(nil, errors.New("boom")) + attachDiskMethod := methods.NewAttachDiskMethod(computeServiceBuilder, volumeServiceBuilder, cpiConfig, logger) + vmCID := apiv1.NewVMCID(serverId) + diskCID := apiv1.NewDiskCID(volumeId1) + err := attachDiskMethod.AttachDisk(vmCID, diskCID) + Expect(err.Error()).To(Equal(fmt.Sprintf("attach_disk: Failed to attach volume ID %s to VM ID %s: boom", volumeId1, serverId))) + }) + + It("fails on attach disk becoming available within timeout period (V1)", func() { + server := servers.Server{ + ID: serverId, + Status: serverStatusActive, + } + computeService.GetServerReturns(&server, nil) + volume := volumes.Volume{ + ID: volumeId1, + Status: diskStatusAvailable, + } + volumeService.GetVolumeReturns(&volume, nil) + volumeServiceBuilder.BuildReturns(volumeService, nil) + volumeAttach := volumeattach.VolumeAttachment{} + computeService.AttachVolumeReturns(&volumeAttach, nil) + volumeService.WaitForVolumeToBecomeStatusReturns(errors.New("boom")) + cpiConfig.Cloud.Properties.Openstack.StateTimeOut = 60 + attachDiskMethod := methods.NewAttachDiskMethod(computeServiceBuilder, volumeServiceBuilder, cpiConfig, logger) + vmCID := apiv1.NewVMCID(serverId) + diskCID := apiv1.NewDiskCID(volumeId1) + err := attachDiskMethod.AttachDisk(vmCID, diskCID) + Expect(err.Error()).To(Equal(fmt.Sprintf("attach_disk: Timeout on waiting to attach volume ID %s to VM %s (waiting: 60 sec): boom", volumeId1, serverId))) + }) + + It("success on attach disk (V1)", func() { + server := servers.Server{ + ID: serverId, + Status: serverStatusActive, + } + computeService.GetServerReturns(&server, nil) + volume := volumes.Volume{ + ID: volumeId1, + Status: diskStatusAvailable, + } + volumeService.GetVolumeReturns(&volume, nil) + volumeServiceBuilder.BuildReturns(volumeService, nil) + volumeAttach := volumeattach.VolumeAttachment{} + computeService.AttachVolumeReturns(&volumeAttach, nil) + volumeService.WaitForVolumeToBecomeStatusReturns(nil) + attachDiskMethod := methods.NewAttachDiskMethod(computeServiceBuilder, volumeServiceBuilder, cpiConfig, logger) + vmCID := apiv1.NewVMCID(serverId) + diskCID := apiv1.NewDiskCID(volumeId1) + err := attachDiskMethod.AttachDisk(vmCID, diskCID) + Expect(err).NotTo(HaveOccurred()) + }) + + It("success on attach disk - includes disk hint (V2)", func() { + server := servers.Server{ + ID: serverId, + Status: serverStatusActive, + } + computeService.GetServerReturns(&server, nil) + volumeNoAttachment := volumes.Volume{ + ID: volumeId1, + Status: diskStatusAvailable, + } + volume := volumes.Volume{ + ID: volumeId1, + Status: diskStatusInUse, + Attachments: []volumes.Attachment{ + { + Device: deviceB, + ServerID: serverId, + }, + }, + } + // given disk/volume is changing its status, reflecting its status before + after attaching + volumeService.GetVolumeReturnsOnCall(0, &volumeNoAttachment, nil) + volumeService.GetVolumeReturnsOnCall(1, &volume, nil) + volumeServiceBuilder.BuildReturns(volumeService, nil) + volumeAttach := volumeattach.VolumeAttachment{} + computeService.AttachVolumeReturns(&volumeAttach, nil) + volumeService.WaitForVolumeToBecomeStatusReturns(nil) + attachDiskMethod := methods.NewAttachDiskMethod(computeServiceBuilder, volumeServiceBuilder, cpiConfig, logger) + vmCID := apiv1.NewVMCID(serverId) + diskCID := apiv1.NewDiskCID(volumeId1) + diskHint, err := attachDiskMethod.AttachDiskV2(vmCID, diskCID) + Expect(err).NotTo(HaveOccurred()) + diskHintExpected := apiv1.NewDiskHintFromString(deviceB) + Expect(diskHint).To(Equal(diskHintExpected)) + }) + + It("success on attach disk - failure on hint build - failure on getting volume (V2)", func() { + server := servers.Server{ + ID: serverId, + Status: serverStatusActive, + } + computeService.GetServerReturns(&server, nil) + volumeNoAttachment := volumes.Volume{ + ID: volumeId1, + Status: diskStatusAvailable, + } + // second get volume call fails on disk hint creation + volumeService.GetVolumeReturnsOnCall(0, &volumeNoAttachment, nil) + volumeService.GetVolumeReturnsOnCall(1, nil, errors.New("boom")) + + volumeServiceBuilder.BuildReturns(volumeService, nil) + volumeAttach := volumeattach.VolumeAttachment{} + computeService.AttachVolumeReturns(&volumeAttach, nil) + volumeService.WaitForVolumeToBecomeStatusReturns(nil) + attachDiskMethod := methods.NewAttachDiskMethod(computeServiceBuilder, volumeServiceBuilder, cpiConfig, logger) + vmCID := apiv1.NewVMCID(serverId) + diskCID := apiv1.NewDiskCID(volumeId1) + diskHint, err := attachDiskMethod.AttachDiskV2(vmCID, diskCID) + Expect(err).NotTo(HaveOccurred()) + diskHintExpected := apiv1.DiskHint{} + Expect(diskHint).To(Equal(diskHintExpected)) + }) + + }) + + Context("getting mount point", func() { + + BeforeEach(func() { + computeServiceBuilder = new(computefakes.FakeComputeServiceBuilder) + computeService = new(computefakes.FakeComputeService) + volumeServiceBuilder = new(volumefakes.FakeVolumeServiceBuilder) + volumeService = new(volumefakes.FakeVolumeService) + logger = new(utilsfakes.FakeLogger) + computeServiceBuilder.BuildReturns(computeService, nil) + cpiConfig = config.CpiConfig{} + }) + + It("fails on get for first device letter on flavor ID read", func() { + flavorMap := map[string]interface{}{ + "id": "1", + } + server = servers.Server{Flavor: flavorMap} + computeService.GetFlavorByIdReturns(flavors.Flavor{}, errors.New("boom")) + volume := volumes.Volume{} + volumeService.GetVolumeReturns(&volume, nil) + attachDiskMethod := methods.NewAttachDiskMethod(computeServiceBuilder, volumeServiceBuilder, cpiConfig, logger) + result, err := attachDiskMethod.GetMountPoint(computeService, server) + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(Equal(deviceB)) + }) + + It("server w/o attached volumes", func() { + volumeService.GetVolumeReturns(nil, errors.New("boom")) + volumeServiceBuilder.BuildReturns(volumeService, nil) + server = servers.Server{} + attachDiskMethod := methods.NewAttachDiskMethod(computeServiceBuilder, volumeServiceBuilder, cpiConfig, logger) + result, err := attachDiskMethod.GetMountPoint(computeService, server) + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(Equal(deviceB)) + }) + + It("server w/ attached volumes: checking disk attachments", func() { + var volumeAttachments []volumeattach.VolumeAttachment + volume1 := volumeattach.VolumeAttachment{VolumeID: volumeId1, Device: deviceA} + volume2 := volumeattach.VolumeAttachment{VolumeID: volumeId1, Device: deviceB} + volumeAttachments = append(volumeAttachments, volume1, volume2) + computeService.ListVolumeAttachmentsReturns(volumeAttachments, nil) + server = servers.Server{ID: serverId} + attachDiskMethod := methods.NewAttachDiskMethod(computeServiceBuilder, volumeServiceBuilder, cpiConfig, logger) + result, err := attachDiskMethod.GetMountPoint(computeService, server) + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(Equal(deviceC)) + }) + + It("check overflow of device character search", func() { + var attachedVolumes []volumeattach.VolumeAttachment + for i := 1; i < 26; i++ { // omit "a"; search start with "b" + id := fmt.Sprintf("vol%d-id", i+1) + device := fmt.Sprintf("/dev/sd%c", 'a'+i) + attachedVolumes = append(attachedVolumes, volumeattach.VolumeAttachment{VolumeID: id, Device: device}) + } + attachDiskMethod := methods.NewAttachDiskMethod(computeServiceBuilder, volumeServiceBuilder, cpiConfig, logger) + _, err := attachDiskMethod.GetDeviceChar('b', attachedVolumes) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal("failed to get device letter")) + }) + + It("check for the lowest device char", func() { + var attachedVolumes []volumeattach.VolumeAttachment + for i := 1; i < 26; i++ { // omit "a"; search start with "b" + // omit additional drive letter, e.g. 'e' (i == 4), i.e. /dev/...e not in list + if i == 4 { + continue + } + var device string + id := fmt.Sprintf("vol%d-id", i+1) + if i%2 == 0 { + device = fmt.Sprintf("/dev/sd%c", 'a'+i) + } else if i%3 == 0 { + device = fmt.Sprintf("/dev/vd%c", 'a'+i) + } else { + device = fmt.Sprintf("/dev/xvd%c", 'a'+i) + } + attachedVolumes = append(attachedVolumes, volumeattach.VolumeAttachment{VolumeID: id, Device: device}) + } + attachDiskMethod := methods.NewAttachDiskMethod(computeServiceBuilder, volumeServiceBuilder, cpiConfig, logger) + driveLetter, err := attachDiskMethod.GetDeviceChar('b', attachedVolumes) + Expect(err).NotTo(HaveOccurred()) + Expect(driveLetter).To(Equal('e')) + }) + + }) + + Context("determine device name letter", func() { + + BeforeEach(func() { + computeServiceBuilder = new(computefakes.FakeComputeServiceBuilder) + volumeServiceBuilder = new(volumefakes.FakeVolumeServiceBuilder) + computeService = new(computefakes.FakeComputeService) + logger = new(utilsfakes.FakeLogger) + computeServiceBuilder.BuildReturns(computeService, nil) + cpiConfig = config.CpiConfig{} + }) + + It("returns default first device name letter on no flavor + no config", func() { + attachDiskMethod := methods.NewAttachDiskMethod(computeServiceBuilder, volumeServiceBuilder, cpiConfig, logger) + result, err := attachDiskMethod.GetFirstDeviceNameLetterWrapper(computeService, server) + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(Equal('b')) + }) + + It("on first device name letter: fails to get flavor by ID", func() { + flavorMap := map[string]interface{}{ + "id": "1", + } + server = servers.Server{Flavor: flavorMap} + computeService.GetFlavorByIdReturns(flavors.Flavor{}, errors.New("boom")) + + attachDiskMethod := methods.NewAttachDiskMethod(computeServiceBuilder, volumeServiceBuilder, cpiConfig, logger) + result, err := attachDiskMethod.GetFirstDeviceNameLetterWrapper(computeService, server) + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(Equal('b')) + }) + + It("returns specific first device name letter when using flavor: using Ephemeral ", func() { + flavorMap := map[string]interface{}{ + "id": "1", + } + flavor := flavors.Flavor{ + ID: "1", + Ephemeral: 1, + } + server = servers.Server{Flavor: flavorMap} + computeService.GetFlavorByIdReturns(flavor, nil) + + attachDiskMethod := methods.NewAttachDiskMethod(computeServiceBuilder, volumeServiceBuilder, cpiConfig, logger) + result, err := attachDiskMethod.GetFirstDeviceNameLetterWrapper(computeService, server) + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(Equal('c')) + }) + + It("returns specific first device name letter when using flavor: using Swap ", func() { + flavorMap := map[string]interface{}{ + "id": "1", + } + flavor := flavors.Flavor{ + ID: "1", + Swap: 1, + } + server = servers.Server{Flavor: flavorMap} + computeService.GetFlavorByIdReturns(flavor, nil) + + attachDiskMethod := methods.NewAttachDiskMethod(computeServiceBuilder, volumeServiceBuilder, cpiConfig, logger) + result, err := attachDiskMethod.GetFirstDeviceNameLetterWrapper(computeService, server) + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(Equal('c')) + }) + + It("returns specific first device name letter when using flavor: using Ephemeral + Swap ", func() { + flavorMap := map[string]interface{}{ + "id": "1", + } + flavor := flavors.Flavor{ + ID: "1", + Swap: 1, + Ephemeral: 1, + } + server = servers.Server{Flavor: flavorMap} + computeService.GetFlavorByIdReturns(flavor, nil) + + attachDiskMethod := methods.NewAttachDiskMethod(computeServiceBuilder, volumeServiceBuilder, cpiConfig, logger) + result, err := attachDiskMethod.GetFirstDeviceNameLetterWrapper(computeService, server) + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(Equal('d')) + }) + + It("returns specific first device name letter when using config: setting 'disk'", func() { + flavorMap := map[string]interface{}{ + "id": "1", + } + flavor := flavors.Flavor{ + ID: "1", + } + cpiConfig.Cloud.Properties.Openstack.ConfigDrive = "disk" + server = servers.Server{Flavor: flavorMap} + computeService.GetFlavorByIdReturns(flavor, nil) + + attachDiskMethod := methods.NewAttachDiskMethod(computeServiceBuilder, volumeServiceBuilder, cpiConfig, logger) + result, err := attachDiskMethod.GetFirstDeviceNameLetterWrapper(computeService, server) + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(Equal('c')) + }) + + It("returns specific first device name letter when using flavor + config: Ephemeral + Swap, setting 'disk'", func() { + flavorMap := map[string]interface{}{ + "id": "1", + } + flavor := flavors.Flavor{ + ID: "1", + Swap: 1, + Ephemeral: 1, + } + cpiConfig.Cloud.Properties.Openstack.ConfigDrive = "disk" + server = servers.Server{Flavor: flavorMap} + computeService.GetFlavorByIdReturns(flavor, nil) + + attachDiskMethod := methods.NewAttachDiskMethod(computeServiceBuilder, volumeServiceBuilder, cpiConfig, logger) + result, err := attachDiskMethod.GetFirstDeviceNameLetterWrapper(computeService, server) + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(Equal('e')) + }) + + }) + +}) diff --git a/src/openstack_cpi_golang/cpi/methods/attach_disk_wrapper.go b/src/openstack_cpi_golang/cpi/methods/attach_disk_wrapper.go new file mode 100644 index 00000000..410a6ff6 --- /dev/null +++ b/src/openstack_cpi_golang/cpi/methods/attach_disk_wrapper.go @@ -0,0 +1,19 @@ +package methods + +import ( + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/compute" + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/volumeattach" + "github.com/gophercloud/gophercloud/openstack/compute/v2/servers" +) + +func (a AttachDiskMethod) GetFirstDeviceNameLetterWrapper(computeService compute.ComputeService, server servers.Server) (rune, error) { + return a.getFirstDeviceNameLetter(computeService, server) +} + +func (a AttachDiskMethod) GetMountPoint(computeService compute.ComputeService, server servers.Server) (string, error) { + return a.getMountPoint(computeService, server) +} + +func (a AttachDiskMethod) GetDeviceChar(inspectChar rune, attachments []volumeattach.VolumeAttachment) (rune, error) { + return a.getDeviceChar(inspectChar, attachments) +} diff --git a/src/openstack_cpi_golang/cpi/methods/calculate_vm_cloud_properties.go b/src/openstack_cpi_golang/cpi/methods/calculate_vm_cloud_properties.go index 306e9eae..13c95d26 100644 --- a/src/openstack_cpi_golang/cpi/methods/calculate_vm_cloud_properties.go +++ b/src/openstack_cpi_golang/cpi/methods/calculate_vm_cloud_properties.go @@ -1,15 +1,61 @@ package methods import ( + "fmt" + "github.com/cloudfoundry/bosh-cpi-go/apiv1" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/compute" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/config" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/properties" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils" + "github.com/gophercloud/gophercloud/openstack/compute/v2/flavors" ) -type CalculateVMCloudPropertiesMethod struct{} +type CalculateVMCloudPropertiesMethod struct { + computeServiceBuilder compute.ComputeServiceBuilder + cpiConfig config.CpiConfig + logger utils.Logger +} -func NewCalculateVMCloudPropertiesMethod() CalculateVMCloudPropertiesMethod { - return CalculateVMCloudPropertiesMethod{} +func NewCalculateVMCloudPropertiesMethod( + computeServiceBuilder compute.ComputeServiceBuilder, + cpiConfig config.CpiConfig, + logger utils.Logger, +) CalculateVMCloudPropertiesMethod { + return CalculateVMCloudPropertiesMethod{ + computeServiceBuilder: computeServiceBuilder, + cpiConfig: cpiConfig, + logger: logger, + } } func (a CalculateVMCloudPropertiesMethod) CalculateVMCloudProperties(requirements apiv1.VMResources) (apiv1.VMCloudProps, error) { - return apiv1.NewVMCloudPropsFromMap(map[string]interface{}{}), nil + computeService, err := a.computeServiceBuilder.Build() + if err != nil { + return nil, fmt.Errorf("calculate_vm_cloud_properties: %w", err) + } + + bootFromVolume := a.cpiConfig.Cloud.Properties.Openstack.BootFromVolume + + matchedFlavor, err := computeService.GetMatchingFlavor(requirements, bootFromVolume) + + if err != nil { + return nil, fmt.Errorf("calculate_vm_cloud_properties: %w", err) + } + + return a.vmCloudProperties(requirements, matchedFlavor, bootFromVolume), nil +} + +func (a CalculateVMCloudPropertiesMethod) vmCloudProperties(requirements apiv1.VMResources, flavor flavors.Flavor, bootFromVolume bool) apiv1.VMCloudProps { + requiredBootVolumeSize := properties.OsOverheadInGb + float64(requirements.EphemeralDiskSize)/1024 + vmProperties := map[string]interface{}{ + "instance_type": flavor.Name, + } + + if bootFromVolume && float64(flavor.Disk) < requiredBootVolumeSize { + vmProperties["root_disk"] = map[string]interface{}{ + "size": fmt.Sprintf("%.1f", requiredBootVolumeSize), + } + } + return apiv1.NewVMCloudPropsFromMap(vmProperties) } diff --git a/src/openstack_cpi_golang/cpi/methods/calculate_vm_cloud_properties_test.go b/src/openstack_cpi_golang/cpi/methods/calculate_vm_cloud_properties_test.go new file mode 100644 index 00000000..5de57697 --- /dev/null +++ b/src/openstack_cpi_golang/cpi/methods/calculate_vm_cloud_properties_test.go @@ -0,0 +1,153 @@ +package methods_test + +import ( + "errors" + "fmt" + + "github.com/cloudfoundry/bosh-cpi-go/apiv1" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/compute/computefakes" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/config" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/methods" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/properties" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils/utilsfakes" + "github.com/gophercloud/gophercloud/openstack/compute/v2/flavors" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("HasVMMethod", func() { + + var computeServiceBuilder computefakes.FakeComputeServiceBuilder + var computeService computefakes.FakeComputeService + var logger utilsfakes.FakeLogger + var vmResources apiv1.VMResources + var cpiConfig config.CpiConfig + + Context("CalculateVMCloudProperties", func() { + BeforeEach(func() { + computeServiceBuilder = computefakes.FakeComputeServiceBuilder{} + logger = utilsfakes.FakeLogger{} + computeService = computefakes.FakeComputeService{} + computeServiceBuilder.BuildReturns(&computeService, nil) + computeService.GetMatchingFlavorReturns(flavors.Flavor{ID: "the_flavor_id", Name: "the_instance_type", VCPUs: 2, RAM: 4096, Ephemeral: 5}, nil) + vmResources = apiv1.VMResources{CPU: 2, RAM: 4096, EphemeralDiskSize: 10240} + cpiConfig = config.CpiConfig{ + Cloud: struct { + Properties config.Properties `json:"properties"` + }{ + Properties: config.Properties{ + Openstack: config.OpenstackConfig{ + BootFromVolume: false, + }, + }, + }, + } + }) + + Context("bootFromVolume is false", func() { + BeforeEach(func() { + cpiConfig = config.CpiConfig{ + Cloud: struct { + Properties config.Properties `json:"properties"` + }{ + Properties: config.Properties{ + Openstack: config.OpenstackConfig{ + BootFromVolume: false, + }, + }, + }, + } + }) + It("calculate the vm cloud properties without root disk", func() { + vmCloudProperties, err := methods.NewCalculateVMCloudPropertiesMethod( + &computeServiceBuilder, + cpiConfig, + &logger, + ).CalculateVMCloudProperties( + vmResources, + ) + + Expect(computeServiceBuilder.BuildCallCount()).To(Equal(1)) + Expect(computeService.GetMatchingFlavorCallCount()).To(Equal(1)) + flavorArgs, bootFromVolumeArg := computeService.GetMatchingFlavorArgsForCall(0) + Expect(flavorArgs).To(Equal(vmResources)) + Expect(bootFromVolumeArg).To(Equal(false)) + Expect(vmCloudProperties).To(Equal(apiv1.NewVMCloudPropsFromMap(map[string]interface{}{ + "instance_type": "the_instance_type", + }))) + Expect(err).ToNot(HaveOccurred()) + }) + }) + + Context("bootFromVolume is true", func() { + BeforeEach(func() { + cpiConfig = config.CpiConfig{ + Cloud: struct { + Properties config.Properties `json:"properties"` + }{ + Properties: config.Properties{ + Openstack: config.OpenstackConfig{ + BootFromVolume: true, + }, + }, + }, + } + }) + + It("calculate the vm cloud properties with root disk", func() { + vmCloudProperties, err := methods.NewCalculateVMCloudPropertiesMethod( + &computeServiceBuilder, + cpiConfig, + &logger, + ).CalculateVMCloudProperties( + vmResources, + ) + + Expect(computeServiceBuilder.BuildCallCount()).To(Equal(1)) + Expect(computeService.GetMatchingFlavorCallCount()).To(Equal(1)) + flavorArgs, bootFromVolumeArg := computeService.GetMatchingFlavorArgsForCall(0) + Expect(flavorArgs).To(Equal(vmResources)) + Expect(bootFromVolumeArg).To(Equal(true)) + Expect(vmCloudProperties).To(Equal(apiv1.NewVMCloudPropsFromMap(map[string]interface{}{ + "instance_type": "the_instance_type", + "root_disk": map[string]interface{}{ + "size": fmt.Sprintf("%.1f", float64(10+properties.OsOverheadInGb)), + }, + }))) + Expect(err).ToNot(HaveOccurred()) + }) + }) + + It("returns an error and false if the compute service cannot be retrieved", func() { + computeServiceBuilder.BuildReturns(nil, errors.New("boom")) + + vmCloudProperties, err := methods.NewCalculateVMCloudPropertiesMethod( + &computeServiceBuilder, + cpiConfig, + &logger, + ).CalculateVMCloudProperties( + vmResources, + ) + + Expect(vmCloudProperties).To(BeNil()) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal("calculate_vm_cloud_properties: boom")) + }) + + It("returns false and error if GetServer fails", func() { + computeService.GetMatchingFlavorReturns(flavors.Flavor{}, errors.New("boom")) + + vmCloudProperties, err := methods.NewCalculateVMCloudPropertiesMethod( + &computeServiceBuilder, + cpiConfig, + &logger, + ).CalculateVMCloudProperties( + vmResources, + ) + + Expect(vmCloudProperties).To(BeNil()) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal("calculate_vm_cloud_properties: boom")) + }) + }) +}) diff --git a/src/openstack_cpi_golang/cpi/methods/create_disk.go b/src/openstack_cpi_golang/cpi/methods/create_disk.go index d5542363..c7c1f06d 100644 --- a/src/openstack_cpi_golang/cpi/methods/create_disk.go +++ b/src/openstack_cpi_golang/cpi/methods/create_disk.go @@ -1,15 +1,93 @@ package methods import ( + "errors" + "fmt" + "math" + "time" + "github.com/cloudfoundry/bosh-cpi-go/apiv1" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/compute" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/config" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/properties" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/volume" + "github.com/gophercloud/gophercloud" ) -type CreateDiskMethod struct{} +type CreateDiskMethod struct { + computeServiceBuilder compute.ComputeServiceBuilder + volumeServiceBuilder volume.VolumeServiceBuilder + cpiConfig config.CpiConfig + logger utils.Logger +} -func NewCreateDiskMethod() CreateDiskMethod { - return CreateDiskMethod{} +func NewCreateDiskMethod( + computeServiceBuilder compute.ComputeServiceBuilder, + volumeServiceBuilder volume.VolumeServiceBuilder, + cpiConfig config.CpiConfig, + logger utils.Logger, +) CreateDiskMethod { + return CreateDiskMethod{ + computeServiceBuilder: computeServiceBuilder, + volumeServiceBuilder: volumeServiceBuilder, + cpiConfig: cpiConfig, + logger: logger, + } } -func (a CreateDiskMethod) CreateDisk(size int, props apiv1.DiskCloudProps, vmCID *apiv1.VMCID) (apiv1.DiskCID, error) { - return apiv1.DiskCID{}, nil +func (a CreateDiskMethod) CreateDisk( + size int, props apiv1.DiskCloudProps, vmCID *apiv1.VMCID) (apiv1.DiskCID, error) { + var errDefault404 gophercloud.ErrDefault404 + + cloudProps := properties.CreateDisk{} + err := props.As(&cloudProps) + if err != nil { + return apiv1.DiskCID{}, fmt.Errorf("failed to parse disk cloud properties: %w", err) + } + + openstackConfig := a.cpiConfig.Cloud.Properties.Openstack + + if size < 1024 { + return apiv1.DiskCID{}, fmt.Errorf("minimum disk size is 1 GiB") + } + sizeInGB := int(math.Ceil(float64(size) / 1024)) + volumeService, err := a.volumeServiceBuilder.Build() + if err != nil { + return apiv1.DiskCID{}, fmt.Errorf("failed to create volume service: %w", err) + } + computeService, err := a.computeServiceBuilder.Build() + if err != nil { + return apiv1.DiskCID{}, fmt.Errorf("failed to create compute service: %w", err) + } + + server, err := computeService.GetServer(vmCID.AsString()) + if err != nil { + if errors.As(err, &errDefault404) { + return apiv1.DiskCID{}, nil + } + return apiv1.DiskCID{}, fmt.Errorf("create_disk: %w", err) + } + + var az string + if server.ID != "" && !openstackConfig.IgnoreServerAvailabilityZone { + az, err = computeService.GetServerAZ(vmCID.AsString()) + if err != nil { + return apiv1.DiskCID{}, fmt.Errorf("create_disk: %w", err) + } + } + + a.logger.Info("create_disk", "Creating new volume...") + volume, err := volumeService.CreateVolume(sizeInGB, cloudProps, az) + if err != nil { + return apiv1.DiskCID{}, fmt.Errorf("failed to create volume: %w", err) + } + + a.logger.Info("create_disk", fmt.Sprintf("Creating new volume %s ...", volume.ID)) + err = volumeService.WaitForVolumeToBecomeStatus(volume.ID, time.Duration(openstackConfig.StateTimeOut)*time.Second, "available") + if err != nil { + return apiv1.DiskCID{}, fmt.Errorf("create disk: %w", err) + } + + return apiv1.NewDiskCID(volume.ID), nil } diff --git a/src/openstack_cpi_golang/cpi/methods/create_disk_test.go b/src/openstack_cpi_golang/cpi/methods/create_disk_test.go new file mode 100644 index 00000000..d7a1a94c --- /dev/null +++ b/src/openstack_cpi_golang/cpi/methods/create_disk_test.go @@ -0,0 +1,287 @@ +package methods_test + +import ( + "errors" + + "github.com/cloudfoundry/bosh-cpi-go/apiv1" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/compute/computefakes" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/config" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/methods" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils/utilsfakes" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/volume/volumefakes" + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/blockstorage/v3/volumes" + "github.com/gophercloud/gophercloud/openstack/compute/v2/servers" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("CreateDisk", func() { + + var cpiConfig config.CpiConfig + var logger utilsfakes.FakeLogger + var computeService computefakes.FakeComputeService + var volumeService volumefakes.FakeVolumeService + var volumeServiceBuilder volumefakes.FakeVolumeServiceBuilder + var computeServiceBuilder computefakes.FakeComputeServiceBuilder + var jsonStr string + var size int + + Context("CreateDisk", func() { + BeforeEach(func() { + computeServiceBuilder = computefakes.FakeComputeServiceBuilder{} + volumeServiceBuilder = volumefakes.FakeVolumeServiceBuilder{} + computeService = computefakes.FakeComputeService{} + volumeService = volumefakes.FakeVolumeService{} + + computeServiceBuilder.BuildReturns(&computeService, nil) + volumeServiceBuilder.BuildReturns(&volumeService, nil) + computeService.GetServerReturns(&servers.Server{ID: "123-456"}, nil) + volumeService.CreateVolumeReturns(&volumes.Volume{ID: "789-size12"}, nil) + volumeService.WaitForVolumeToBecomeStatusReturns(nil) + cpiConfig = config.CpiConfig{} + cpiConfig.Cloud.Properties.Openstack = config.OpenstackConfig{IgnoreServerAvailabilityZone: true} + + logger = utilsfakes.FakeLogger{} + size = 1025 + + jsonStr = `{ + "type": "vmware" + }` + }) + + It("returns an error if the volume size is below 1 GiB", func() { + size = 1023 + + _, err := methods.NewCreateDiskMethod( + &computeServiceBuilder, + &volumeServiceBuilder, + cpiConfig, + &logger, + ).CreateDisk( + size, + apiv1.CloudPropsImpl{RawMessage: []byte(jsonStr)}, + &apiv1.VMCID{}, + ) + + Expect(err.Error()).To(Equal("minimum disk size is 1 GiB")) + }) + + It("creates the volume service", func() { + _, _ = methods.NewCreateDiskMethod( + &computeServiceBuilder, + &volumeServiceBuilder, + cpiConfig, + &logger, + ).CreateDisk( + size, + apiv1.CloudPropsImpl{RawMessage: []byte(jsonStr)}, + &apiv1.VMCID{}, + ) + + Expect(volumeServiceBuilder.BuildCallCount()).To(Equal(1)) + }) + + It("returns an error if the volume service cannot be retrieved", func() { + volumeServiceBuilder.BuildReturns(nil, errors.New("boom")) + + diskCID, err := methods.NewCreateDiskMethod( + &computeServiceBuilder, + &volumeServiceBuilder, + cpiConfig, + &logger, + ).CreateDisk( + size, + apiv1.CloudPropsImpl{RawMessage: []byte(jsonStr)}, + &apiv1.VMCID{}, + ) + + Expect(err.Error()).To(Equal("failed to create volume service: boom")) + Expect(diskCID).To(Equal(apiv1.DiskCID{})) + }) + + It("creates the compute service", func() { + _, _ = methods.NewCreateDiskMethod( + &computeServiceBuilder, + &volumeServiceBuilder, + cpiConfig, + &logger, + ).CreateDisk( + size, + apiv1.CloudPropsImpl{RawMessage: []byte(jsonStr)}, + &apiv1.VMCID{}, + ) + + Expect(computeServiceBuilder.BuildCallCount()).To(Equal(1)) + }) + + It("returns an error if the compute service cannot be retrieved", func() { + computeServiceBuilder.BuildReturns(nil, errors.New("boom")) + + diskCID, err := methods.NewCreateDiskMethod( + &computeServiceBuilder, + &volumeServiceBuilder, + cpiConfig, + &logger, + ).CreateDisk( + size, + apiv1.CloudPropsImpl{RawMessage: []byte(jsonStr)}, + &apiv1.VMCID{}, + ) + + Expect(err.Error()).To(Equal("failed to create compute service: boom")) + Expect(diskCID).To(Equal(apiv1.DiskCID{})) + }) + + Context("when ignore_server_availability_zone is false", func() { + BeforeEach(func() { + computeService.GetServerAZReturns("AZ", nil) + cpiConfig.Cloud.Properties.Openstack = config.OpenstackConfig{IgnoreServerAvailabilityZone: false} + }) + + It("gets the server availability zone", func() { + _, _ = methods.NewCreateDiskMethod( + &computeServiceBuilder, + &volumeServiceBuilder, + cpiConfig, + &logger, + ).CreateDisk( + size, + apiv1.CloudPropsImpl{RawMessage: []byte(jsonStr)}, + &apiv1.VMCID{}, + ) + + _, _, az := volumeService.CreateVolumeArgsForCall(0) + + Expect(computeService.GetServerAZCallCount()).To(Equal(1)) + Expect(az).To(Equal("AZ")) + }) + + It("returns an error if GetServerAZ failed", func() { + computeService.GetServerAZReturns("", errors.New("boom")) + + diskCID, err := methods.NewCreateDiskMethod( + &computeServiceBuilder, + &volumeServiceBuilder, + cpiConfig, + &logger, + ).CreateDisk( + size, + apiv1.CloudPropsImpl{RawMessage: []byte(jsonStr)}, + &apiv1.VMCID{}, + ) + + Expect(err.Error()).To(Equal("create_disk: boom")) + Expect(diskCID).To(Equal(apiv1.DiskCID{})) + }) + }) + + Context("when ignore_server_availability_zone is true", func() { + It("does not get the server availability zone", func() { + _, _ = methods.NewCreateDiskMethod( + &computeServiceBuilder, + &volumeServiceBuilder, + cpiConfig, + &logger, + ).CreateDisk( + size, + apiv1.CloudPropsImpl{RawMessage: []byte(jsonStr)}, + &apiv1.VMCID{}, + ) + + Expect(computeService.GetServerAZCallCount()).To(Equal(0)) + }) + }) + + It("creates the volume", func() { + _, _ = methods.NewCreateDiskMethod( + &computeServiceBuilder, + &volumeServiceBuilder, + cpiConfig, + &logger, + ).CreateDisk( + size, + apiv1.CloudPropsImpl{RawMessage: []byte(jsonStr)}, + &apiv1.VMCID{}, + ) + + size, _, _ := volumeService.CreateVolumeArgsForCall(0) + Expect(size).To(Equal(2)) + Expect(volumeService.CreateVolumeCallCount()).To(Equal(1)) + }) + + It("returns an error if CreateVolume fails", func() { + volumeService.CreateVolumeReturns(nil, errors.New("boom")) + + diskCID, err := methods.NewCreateDiskMethod( + &computeServiceBuilder, + &volumeServiceBuilder, + cpiConfig, + &logger, + ).CreateDisk( + size, + apiv1.CloudPropsImpl{RawMessage: []byte(jsonStr)}, + &apiv1.VMCID{}, + ) + + Expect(err.Error()).To(Equal("failed to create volume: boom")) + Expect(diskCID).To(Equal(apiv1.DiskCID{})) + }) + + It("returns an error if GetServer fails", func() { + computeService.GetServerReturns(nil, errors.New("boom")) + + diskCID, err := methods.NewCreateDiskMethod( + &computeServiceBuilder, + &volumeServiceBuilder, + cpiConfig, + &logger, + ).CreateDisk( + size, + apiv1.CloudPropsImpl{RawMessage: []byte(jsonStr)}, + &apiv1.VMCID{}, + ) + + Expect(err.Error()).To(Equal("create_disk: boom")) + Expect(diskCID).To(Equal(apiv1.DiskCID{})) + }) + + It("does not returns an error if no server can be retrieved", func() { + var errDefault404 gophercloud.ErrDefault404 + computeService.GetServerReturns(nil, errDefault404) + + diskCID, err := methods.NewCreateDiskMethod( + &computeServiceBuilder, + &volumeServiceBuilder, + cpiConfig, + &logger, + ).CreateDisk( + size, + apiv1.CloudPropsImpl{RawMessage: []byte(jsonStr)}, + &apiv1.VMCID{}, + ) + + Expect(err).To(BeNil()) + Expect(diskCID).To(Equal(apiv1.DiskCID{})) + }) + + It("fails while waiting for the volume to become available", func() { + volumeService.WaitForVolumeToBecomeStatusReturns(errors.New("some_error_while_waiting_for_volume")) + + diskCID, err := methods.NewCreateDiskMethod( + &computeServiceBuilder, + &volumeServiceBuilder, + cpiConfig, + &logger, + ).CreateDisk( + size, + apiv1.CloudPropsImpl{RawMessage: []byte(jsonStr)}, + &apiv1.VMCID{}, + ) + + Expect(volumeService.WaitForVolumeToBecomeStatusCallCount()).To(Equal(1)) + Expect(err.Error()).To(Equal("create disk: some_error_while_waiting_for_volume")) + Expect(diskCID).To(Equal(apiv1.DiskCID{})) + }) + }) +}) diff --git a/src/openstack_cpi_golang/cpi/methods/create_stemcell.go b/src/openstack_cpi_golang/cpi/methods/create_stemcell.go index d77a7d78..29114af1 100644 --- a/src/openstack_cpi_golang/cpi/methods/create_stemcell.go +++ b/src/openstack_cpi_golang/cpi/methods/create_stemcell.go @@ -1,19 +1,83 @@ package methods import ( + "fmt" + "os" + "github.com/cloudfoundry/bosh-cpi-go/apiv1" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/config" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/image" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/image/root_image" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/properties" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils" ) type CreateStemcellMethod struct { + imageServiceBuilder image.ImageServiceBuilder + heavyStemcellCreator image.HeavyStemcellCreator + lightStemcellCreator image.LightStemcellCreator + rootImageProvider root_image.RootImage + config config.OpenstackConfig + logger utils.Logger } -func NewCreateStemcellMethod() CreateStemcellMethod { - return CreateStemcellMethod{} +func NewCreateStemcellMethod( + imageServiceBuilder image.ImageServiceBuilder, + heavyStemcellCreator image.HeavyStemcellCreator, + lightStemcellCreator image.LightStemcellCreator, + rootImageProvider root_image.RootImage, + config config.OpenstackConfig, + logger utils.Logger, +) CreateStemcellMethod { + return CreateStemcellMethod{ + imageServiceBuilder: imageServiceBuilder, + heavyStemcellCreator: heavyStemcellCreator, + lightStemcellCreator: lightStemcellCreator, + rootImageProvider: rootImageProvider, + config: config, + logger: logger, + } } func (a CreateStemcellMethod) CreateStemcell( imagePath string, props apiv1.StemcellCloudProps, ) (apiv1.StemcellCID, error) { - return apiv1.StemcellCID{}, nil + a.logger.Info("create_stemcell", "Creating new image...") + + var cloudProps = properties.CreateStemcell{} + err := props.As(&cloudProps) + if err != nil { + return apiv1.StemcellCID{}, fmt.Errorf("failed to parse stemcell cloud properties: %w", err) + } + + imageService, err := a.imageServiceBuilder.Build() + if err != nil { + return apiv1.StemcellCID{}, fmt.Errorf("failed to create image service: %w", err) + } + + var imageID string + var creationError error + if cloudProps.ImageID != "" { + imageID, creationError = a.lightStemcellCreator.Create(imageService, cloudProps) + + } else { + tempDirPath, err := os.MkdirTemp("/tmp", "unpacked-image-") + if err != nil { + return apiv1.StemcellCID{}, fmt.Errorf("failed to create temp dir: %w", err) + } + defer os.RemoveAll(tempDirPath) + + rootImagePath, err := a.rootImageProvider.Get(imagePath, tempDirPath) + if err != nil { + return apiv1.StemcellCID{}, fmt.Errorf("failed to get root image: %w", err) + } + + imageID, creationError = a.heavyStemcellCreator.Create(imageService, cloudProps, rootImagePath) + } + + if creationError != nil { + return apiv1.StemcellCID{}, fmt.Errorf("failed to create a stemcell: %w", creationError) + } + return apiv1.NewStemcellCID(imageID), nil } diff --git a/src/openstack_cpi_golang/cpi/methods/create_stemcell_test.go b/src/openstack_cpi_golang/cpi/methods/create_stemcell_test.go new file mode 100644 index 00000000..c0c032f9 --- /dev/null +++ b/src/openstack_cpi_golang/cpi/methods/create_stemcell_test.go @@ -0,0 +1,188 @@ +package methods_test + +import ( + "errors" + + "github.com/cloudfoundry/bosh-cpi-go/apiv1" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/config" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/image" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/image/imagefakes" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/image/root_image/root_imagefakes" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/methods" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/properties" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils/utilsfakes" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +type MockStemcellCloudProps struct { + ImageID string + Version string +} + +func (m *MockStemcellCloudProps) As(v interface{}) error { + stemcell := v.(*properties.CreateStemcell) + stemcell.Version = "0.1" + stemcell.ImageID = m.ImageID + + return nil +} + +var _ = Describe("CreateStemcellMethod", func() { + + var imageServiceBuilder imagefakes.FakeImageServiceBuilder + var heavyStemcellCreator imagefakes.FakeHeavyStemcellCreator + var lightStemcellCreator imagefakes.FakeLightStemcellCreator + var rootImageProvider root_imagefakes.FakeRootImage + var logger utilsfakes.FakeLogger + + Context("CreateStemcell", func() { + + BeforeEach(func() { + imageServiceBuilder = imagefakes.FakeImageServiceBuilder{} + heavyStemcellCreator = imagefakes.FakeHeavyStemcellCreator{} + lightStemcellCreator = imagefakes.FakeLightStemcellCreator{} + rootImageProvider = root_imagefakes.FakeRootImage{} + logger = utilsfakes.FakeLogger{} + }) + + It("returns a stemcell ID", func() { + imageServiceBuilder.BuildReturns(image.NewImageService(utils.ServiceClients{}, nil, nil, nil), nil) + heavyStemcellCreator.CreateReturns("123-456", nil) + props := &MockStemcellCloudProps{} + stemcellCID, err := methods.NewCreateStemcellMethod( + &imageServiceBuilder, + &heavyStemcellCreator, + &lightStemcellCreator, + &rootImageProvider, + config.OpenstackConfig{}, + &logger, + ).CreateStemcell("imagePath", props) + + Expect(err).ToNot(HaveOccurred()) + Expect(stemcellCID.AsString()).To(Equal("123-456")) + }) + + It("returns an error if the stemcell creation fails", func() { + imageServiceBuilder.BuildReturns(image.NewImageService(utils.ServiceClients{}, nil, nil, nil), nil) + heavyStemcellCreator.CreateReturns("", errors.New("boom")) + props := &MockStemcellCloudProps{} + stemcellCID, err := methods.NewCreateStemcellMethod( + &imageServiceBuilder, + &heavyStemcellCreator, + &lightStemcellCreator, + &rootImageProvider, + config.OpenstackConfig{}, + &logger, + ).CreateStemcell("imagePath", props) + + Expect(err.Error()).To(Equal("failed to create a stemcell: boom")) + Expect(stemcellCID).To(Equal(apiv1.StemcellCID{})) + }) + + It("returns an error if the image service cannot be retrieved", func() { + imageServiceBuilder.BuildReturns(nil, errors.New("boom")) + props := &MockStemcellCloudProps{} + stemcellCID, err := methods.NewCreateStemcellMethod( + &imageServiceBuilder, + &heavyStemcellCreator, + &lightStemcellCreator, + &rootImageProvider, + config.OpenstackConfig{}, + &logger, + ).CreateStemcell("imagePath", props) + + Expect(err.Error()).To(Equal("failed to create image service: boom")) + Expect(stemcellCID).To(Equal(apiv1.StemcellCID{})) + }) + + It("uses the light stemcell creation if cloud properties are containing an imageID", func() { + theImageService := image.NewImageService(utils.ServiceClients{}, nil, nil, nil) + imageServiceBuilder.BuildReturns(theImageService, nil) + lightStemcellCreator.CreateReturns("123-456", nil) + + theCloudProps := &MockStemcellCloudProps{ImageID: "123-456"} + + _, _ = methods.NewCreateStemcellMethod( + &imageServiceBuilder, + &heavyStemcellCreator, + &lightStemcellCreator, + &rootImageProvider, + config.OpenstackConfig{}, + &logger, + ).CreateStemcell("imagePath", theCloudProps) + + imageService, cloudProps := lightStemcellCreator.CreateArgsForCall(0) + Expect(lightStemcellCreator.CreateCallCount()).To(Equal(1)) + Expect(imageService).To(Equal(theImageService)) + Expect(cloudProps.ImageID).To(Equal("123-456")) + }) + + It("uses the heavy stemcell creation if cloud properties are NOT containing an imageID", func() { + theImageService := image.NewImageService(utils.ServiceClients{}, nil, nil, nil) + imageServiceBuilder.BuildReturns(theImageService, nil) + heavyStemcellCreator.CreateReturns("123-456", nil) + rootImageProvider.GetReturns("rootImagePath", nil) + + theCloudProps := &MockStemcellCloudProps{} + + _, _ = methods.NewCreateStemcellMethod( + &imageServiceBuilder, + &heavyStemcellCreator, + &lightStemcellCreator, + &rootImageProvider, + config.OpenstackConfig{}, + &logger, + ).CreateStemcell("imagePath", theCloudProps) + + imageService, cloudProps, path := heavyStemcellCreator.CreateArgsForCall(0) + Expect(heavyStemcellCreator.CreateCallCount()).To(Equal(1)) + Expect(imageService).To(Equal(theImageService)) + Expect(cloudProps.Version).To(Equal("0.1")) + Expect(path).To(Equal("rootImagePath")) + }) + + It("returns an error if root.img cannot be retrieved", func() { + theImageService := image.NewImageService(utils.ServiceClients{}, nil, nil, nil) + imageServiceBuilder.BuildReturns(theImageService, nil) + rootImageProvider.GetReturns("", errors.New("boom")) + + theCloudProps := &MockStemcellCloudProps{} + + rootImagePath, err := methods.NewCreateStemcellMethod( + &imageServiceBuilder, + &heavyStemcellCreator, + &lightStemcellCreator, + &rootImageProvider, + config.OpenstackConfig{}, + &logger, + ).CreateStemcell("imagePath", theCloudProps) + + Expect(err.Error()).To(Equal("failed to get root image: boom")) + Expect(rootImagePath).To(Equal(apiv1.StemcellCID{})) + }) + + It("extracts the rootImage to a temp dir path", func() { + theImageService := image.NewImageService(utils.ServiceClients{}, nil, nil, nil) + imageServiceBuilder.BuildReturns(theImageService, nil) + rootImageProvider.GetReturns("", errors.New("boom")) + + theCloudProps := &MockStemcellCloudProps{} + + _, _ = methods.NewCreateStemcellMethod( + &imageServiceBuilder, + &heavyStemcellCreator, + &lightStemcellCreator, + &rootImageProvider, + config.OpenstackConfig{}, + &logger, + ).CreateStemcell("imagePath", theCloudProps) + + imagePath, tempDirPath := rootImageProvider.GetArgsForCall(0) + Expect(imagePath).To(Equal("imagePath")) + Expect(tempDirPath).To(MatchRegexp("/tmp/unpacked-image-\\d+")) + }) + + }) +}) diff --git a/src/openstack_cpi_golang/cpi/methods/create_vm.go b/src/openstack_cpi_golang/cpi/methods/create_vm.go index a8b8e85a..4373ac4e 100644 --- a/src/openstack_cpi_golang/cpi/methods/create_vm.go +++ b/src/openstack_cpi_golang/cpi/methods/create_vm.go @@ -1,13 +1,47 @@ package methods import ( + "fmt" + "strconv" + "github.com/cloudfoundry/bosh-cpi-go/apiv1" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/compute" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/config" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/image" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/loadbalancer" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/network" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/properties" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils" + "github.com/gophercloud/gophercloud/openstack/compute/v2/servers" + "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/pools" + "github.com/gophercloud/gophercloud/openstack/networking/v2/ports" ) -type CreateVMMethod struct{} +type CreateVMMethod struct { + imageServiceBuilder image.ImageServiceBuilder + networkServiceBuilder network.NetworkServiceBuilder + computeServiceBuilder compute.ComputeServiceBuilder + loadbalancerServiceBuilder loadbalancer.LoadbalancerServiceBuilder + cpiConfig config.CpiConfig + logger utils.Logger +} -func NewCreateVMMethod() CreateVMMethod { - return CreateVMMethod{} +func NewCreateVMMethod( + imageServiceBuilder image.ImageServiceBuilder, + networkServiceBuilder network.NetworkServiceBuilder, + computeServiceBuilder compute.ComputeServiceBuilder, + loadbalancerServiceBuilder loadbalancer.LoadbalancerServiceBuilder, + cpiConfig config.CpiConfig, + logger utils.Logger, +) CreateVMMethod { + return CreateVMMethod{ + imageServiceBuilder: imageServiceBuilder, + networkServiceBuilder: networkServiceBuilder, + computeServiceBuilder: computeServiceBuilder, + loadbalancerServiceBuilder: loadbalancerServiceBuilder, + cpiConfig: cpiConfig, + logger: logger, + } } func (m CreateVMMethod) CreateVM( @@ -21,5 +55,194 @@ func (m CreateVMMethod) CreateVMV2( agentID apiv1.AgentID, stemcellCID apiv1.StemcellCID, props apiv1.VMCloudProps, networks apiv1.Networks, diskCIDs []apiv1.DiskCID, env apiv1.VMEnv) (apiv1.VMCID, apiv1.Networks, error) { - return apiv1.VMCID{}, apiv1.Networks{}, nil + cloudProps := properties.CreateVM{} + err := props.As(&cloudProps) + if err != nil { + return apiv1.VMCID{}, apiv1.Networks{}, fmt.Errorf("failed to parse vm cloud properties: %w", err) + } + + createdPortsIds := make([]ports.Port, 0) + + err = cloudProps.Validate(m.cpiConfig.Cloud.Properties.Openstack) + if err != nil { + return apiv1.VMCID{}, apiv1.Networks{}, fmt.Errorf("failed to validate cloud properties: %w", err) + } + + computeService, err := m.computeServiceBuilder.Build() + if err != nil { + return apiv1.VMCID{}, apiv1.Networks{}, fmt.Errorf("failed to create compute service: %w", err) + } + + networkService, err := m.networkServiceBuilder.Build() + if err != nil { + return apiv1.VMCID{}, apiv1.Networks{}, fmt.Errorf("failed to create networking service: %w", err) + } + + imageService, err := m.imageServiceBuilder.Build() + if err != nil { + return apiv1.VMCID{}, apiv1.Networks{}, fmt.Errorf("failed to create image service: %w", err) + } + + loadbalancerService, err := m.loadbalancerServiceBuilder.Build() + if err != nil { + return apiv1.VMCID{}, apiv1.Networks{}, fmt.Errorf("failed to create loadbalancer service: %w", err) + } + + _, err = imageService.GetImage(stemcellCID.AsString()) + if err != nil { + return apiv1.VMCID{}, apiv1.Networks{}, fmt.Errorf("failed to resolve stemcell: %w", err) + } + + networkConfig, err := networkService.GetNetworkConfiguration(networks, m.cpiConfig.Cloud.Properties.Openstack, cloudProps) + if err != nil { + return apiv1.VMCID{}, apiv1.Networks{}, fmt.Errorf("failed to create network config: %w", err) + } + + manualNetworks := networkConfig.ManualNetworks + for i := 0; i < len(manualNetworks); i++ { + manualNetwork := &manualNetworks[i] + port, err := networkService.CreatePort(*manualNetwork, networkConfig.SecurityGroups, cloudProps) + if err != nil { + return apiv1.VMCID{}, apiv1.Networks{}, fmt.Errorf("failed to create port: %w", err) + } + manualNetwork.ConfigurePort(port) + createdPortsIds = append(createdPortsIds, port) + } + + server, err := computeService.CreateServer(stemcellCID, cloudProps, networkConfig, agentID, env, m.cpiConfig) + if err != nil { + return m.cleanupServerResources( + server, + createdPortsIds, + []pools.Member{}, + computeService, + loadbalancerService, + networkService, + fmt.Errorf("failed to create server: %w", err), + ) + } + + err = networkService.ConfigureVIPNetwork(server.ID, networkConfig) + if err != nil { + return m.cleanupServerResources( + server, + createdPortsIds, + []pools.Member{}, + computeService, + loadbalancerService, + networkService, + fmt.Errorf("failed to configure vip network for server '%s' with error: %w", server.ID, err), + ) + } + + poolMembers, err := m.configureLoadbalancerPools(loadbalancerService, networkService, cloudProps, networkConfig) + if err != nil { + return m.cleanupServerResources( + server, + createdPortsIds, + poolMembers, + computeService, + loadbalancerService, + networkService, + fmt.Errorf("failed to configure loadbalancer pools: %w", err), + ) + } + + err = computeService.UpdateServerMetadata(server.ID, m.getServerMetadata(poolMembers)) + if err != nil { + return m.cleanupServerResources( + server, + createdPortsIds, + poolMembers, + computeService, + loadbalancerService, + networkService, + fmt.Errorf("failed to update metadata for server '%s' with error: %w", server.ID, err), + ) + } + + return apiv1.NewVMCID(server.ID), networks, nil +} + +func (m CreateVMMethod) configureLoadbalancerPools( + loadbalancerService loadbalancer.LoadbalancerService, + networkService network.NetworkService, + cloudProps properties.CreateVM, + networkConfig properties.NetworkConfig, +) ([]pools.Member, error) { + var poolMemberships []pools.Member + + for _, poolProperties := range cloudProps.LoadbalancerPools { + pool, err := loadbalancerService.GetPool(poolProperties.Name) + if err != nil { + return poolMemberships, fmt.Errorf("failed to get pool ID of pool '%s': %w", poolProperties.Name, err) + } + m.logger.Info("create_vm_method", fmt.Sprintf("Resolved pool id '%s' for pool '%s'", pool.ID, poolProperties.Name)) + + ip := networkConfig.DefaultNetwork.IP + + defaultNetworkID := networkConfig.DefaultNetwork.CloudProps.NetID + + subnetID, err := networkService.GetSubnetID(defaultNetworkID, ip) + if err != nil { + return poolMemberships, fmt.Errorf("failed to get subnet: %w", err) + } + + poolMember, err := loadbalancerService.CreatePoolMember(pool, ip, poolProperties, subnetID, m.cpiConfig.Cloud.Properties.Openstack.StateTimeOut) + if err != nil { + return poolMemberships, fmt.Errorf("failed to create pool membership of IP '%s' in pool '%s': %w", ip, pool.ID, err) + } + + poolMember.PoolID = pool.ID + poolMemberships = append(poolMemberships, *poolMember) + + m.logger.Info("create_vm_method", fmt.Sprintf("created pool member '%+v' in pool '%s'", *poolMember, pool.ID)) + } + + return poolMemberships, nil +} + +func (m CreateVMMethod) getServerMetadata(members []pools.Member) properties.ServerMetadata { + tags := properties.ServerMetadata{} + + var index = 1 + for _, member := range members { + itoa := strconv.Itoa(index) + tags["lbaas_pool_"+itoa] = member.PoolID + "/" + member.ID + index++ + } + + return tags +} + +func (m CreateVMMethod) cleanupServerResources( + server *servers.Server, ports []ports.Port, poolMembers []pools.Member, computeService compute.ComputeService, + loadbalancerService loadbalancer.LoadbalancerService, networkService network.NetworkService, errorMsg error) ( + apiv1.VMCID, apiv1.Networks, error) { + + for _, poolMember := range poolMembers { + err := loadbalancerService.DeletePoolMember(poolMember.PoolID, poolMember.ID, m.cpiConfig.Cloud.Properties.Openstack.StateTimeOut) + if err != nil { + m.logger.Warn("create_vm_method", + fmt.Sprintf("failed while cleaning up pool member: '%s' in pool '%s' with error: %s", + poolMember.ID, poolMember.PoolID, err.Error())) + continue + } + } + + if server != nil { + err := computeService.DeleteServer(server.ID, m.cpiConfig) + if err != nil { + m.logger.Warn("create_vm_method", + fmt.Sprintf("failed while cleaning up server '%s' with error: %s", server.ID, err.Error())) + } + } + + err := networkService.DeletePorts(ports) + if err != nil { + m.logger.Warn("create_vm_method", + fmt.Sprintf("failed while cleaning up ports: '%+v' with error: %s", ports, err.Error())) + } + + return apiv1.VMCID{}, apiv1.Networks{}, errorMsg } diff --git a/src/openstack_cpi_golang/cpi/methods/create_vm_test.go b/src/openstack_cpi_golang/cpi/methods/create_vm_test.go new file mode 100644 index 00000000..4c5bfd35 --- /dev/null +++ b/src/openstack_cpi_golang/cpi/methods/create_vm_test.go @@ -0,0 +1,870 @@ +package methods_test + +import ( + "errors" + + "github.com/cloudfoundry/bosh-cpi-go/apiv1" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/compute/computefakes" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/config" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/image/imagefakes" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/loadbalancer/loadbalancerfakes" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/methods" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/network/networkfakes" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/properties" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils/utilsfakes" + "github.com/gophercloud/gophercloud/openstack/compute/v2/servers" + "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/pools" + "github.com/gophercloud/gophercloud/openstack/networking/v2/ports" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("CreateVMMethod", func() { + + var computeServiceBuilder computefakes.FakeComputeServiceBuilder + var networkServiceBuilder networkfakes.FakeNetworkServiceBuilder + var imageServiceBuilder imagefakes.FakeImageServiceBuilder + var loadbalancerServiceBuilder loadbalancerfakes.FakeLoadbalancerServiceBuilder + var computeService computefakes.FakeComputeService + var networkService networkfakes.FakeNetworkService + var imageService imagefakes.FakeImageService + var loadbalancerService loadbalancerfakes.FakeLoadbalancerService + var logger utilsfakes.FakeLogger + var networks apiv1.Networks + var jsonStr string + var cpiConfig config.CpiConfig + var env apiv1.VMEnv + var networkConfig properties.NetworkConfig + var port ports.Port + + Context("CreateVMV2", func() { + + BeforeEach(func() { + computeServiceBuilder = computefakes.FakeComputeServiceBuilder{} + networkServiceBuilder = networkfakes.FakeNetworkServiceBuilder{} + imageServiceBuilder = imagefakes.FakeImageServiceBuilder{} + loadbalancerServiceBuilder = loadbalancerfakes.FakeLoadbalancerServiceBuilder{} + computeService = computefakes.FakeComputeService{} + networkService = networkfakes.FakeNetworkService{} + imageService = imagefakes.FakeImageService{} + loadbalancerService = loadbalancerfakes.FakeLoadbalancerService{} + logger = utilsfakes.FakeLogger{} + env = apiv1.VMEnv{} + + computeServiceBuilder.BuildReturns(&computeService, nil) + networkServiceBuilder.BuildReturns(&networkService, nil) + imageServiceBuilder.BuildReturns(&imageService, nil) + loadbalancerServiceBuilder.BuildReturns(&loadbalancerService, nil) + computeService.CreateServerReturns(&servers.Server{ID: "123-456"}, nil) + networkService.ConfigureVIPNetworkReturns(nil) + + cpiConfig = config.CpiConfig{} + cpiConfig.Cloud.Properties.Openstack = config.OpenstackConfig{ + IgnoreServerAvailabilityZone: true, StateTimeOut: 1, + } + + networkConfig = properties.NetworkConfig{ + DefaultNetwork: properties.Network{ + Type: "manual", + IP: "1.1.1.1", + CloudProps: properties.NetworkCloudProps{NetID: "the-net-id"}, + }, + ManualNetworks: []properties.Network{{ + Key: "key-1", + Type: "manual", + IP: "1.1.1.1", + CloudProps: properties.NetworkCloudProps{NetID: "the-net-id-1"}, + }, { + Key: "key-2", + Type: "manual", + IP: "2.2.2.2", + CloudProps: properties.NetworkCloudProps{NetID: "the-net-id-2"}, + }}, + } + networkService.GetNetworkConfigurationReturns(networkConfig, nil) + port = ports.Port{ID: "the-port-id"} + networkService.CreatePortReturns(port, nil) + + networks = apiv1.Networks{} + + jsonStr = `{ + "instance_type": "type1", + "loadbalancer_pools": [{"name": "the-pool-name-1","port": 1234,"monitoring_port": 5678}], + "availability_zones": ["z1", "z2"] + + }` + networkService.GetSubnetIDReturns("the-subnet-id", nil) + loadbalancerService.GetPoolReturnsOnCall(0, pools.Pool{ID: "the-pool-id-1"}, nil) + loadbalancerService.GetPoolReturnsOnCall(1, pools.Pool{ID: "the-pool-id-2"}, nil) + loadbalancerService.CreatePoolMemberReturns(&pools.Member{ID: "the-member-id", PoolID: "the-pool-id-1"}, nil) + }) + + Context("Services creation and validation", func() { + It("creates the compute service", func() { + _, _, _ = methods.NewCreateVMMethod( + &imageServiceBuilder, + &networkServiceBuilder, + &computeServiceBuilder, + &loadbalancerServiceBuilder, + cpiConfig, + &logger, + ).CreateVMV2( + apiv1.NewAgentID("the_agent-id"), + apiv1.NewStemcellCID("stemcell-id"), + apiv1.CloudPropsImpl{RawMessage: []byte(jsonStr)}, + networks, + []apiv1.DiskCID{}, + env, + ) + + Expect(computeServiceBuilder.BuildCallCount()).To(Equal(1)) + }) + + It("returns an error if the compute service cannot be retrieved", func() { + computeServiceBuilder.BuildReturns(nil, errors.New("boom")) + + stemcellCID, networks, err := methods.NewCreateVMMethod( + &imageServiceBuilder, + &networkServiceBuilder, + &computeServiceBuilder, + &loadbalancerServiceBuilder, + cpiConfig, + &logger, + ).CreateVMV2( + apiv1.NewAgentID("the_agent-id"), + apiv1.NewStemcellCID("stemcell-id"), + apiv1.CloudPropsImpl{RawMessage: []byte(jsonStr)}, + networks, + []apiv1.DiskCID{}, + env, + ) + + Expect(err.Error()).To(Equal("failed to create compute service: boom")) + Expect(stemcellCID).To(Equal(apiv1.VMCID{})) + Expect(networks).To(Equal(apiv1.Networks{})) + }) + + It("creates the network service", func() { + _, _, _ = methods.NewCreateVMMethod( + &imageServiceBuilder, + &networkServiceBuilder, + &computeServiceBuilder, + &loadbalancerServiceBuilder, + cpiConfig, + &logger, + ).CreateVMV2( + apiv1.NewAgentID("the_agent-id"), + apiv1.NewStemcellCID("stemcell-id"), + apiv1.CloudPropsImpl{RawMessage: []byte(jsonStr)}, + networks, + []apiv1.DiskCID{}, + env, + ) + + Expect(networkServiceBuilder.BuildCallCount()).To(Equal(1)) + }) + + It("returns an error if the network service cannot be retrieved", func() { + networkServiceBuilder.BuildReturns(nil, errors.New("boom")) + + stemcellCID, networks, err := methods.NewCreateVMMethod( + &imageServiceBuilder, + &networkServiceBuilder, + &computeServiceBuilder, + &loadbalancerServiceBuilder, + cpiConfig, + &logger, + ).CreateVMV2( + apiv1.NewAgentID("the_agent-id"), + apiv1.NewStemcellCID("stemcell-id"), + apiv1.CloudPropsImpl{RawMessage: []byte(jsonStr)}, + networks, + []apiv1.DiskCID{}, + env, + ) + + Expect(err.Error()).To(Equal("failed to create networking service: boom")) + Expect(stemcellCID).To(Equal(apiv1.VMCID{})) + Expect(networks).To(Equal(apiv1.Networks{})) + }) + + It("creates the image service", func() { + _, _, _ = methods.NewCreateVMMethod( + &imageServiceBuilder, + &networkServiceBuilder, + &computeServiceBuilder, + &loadbalancerServiceBuilder, + cpiConfig, + &logger, + ).CreateVMV2( + apiv1.NewAgentID("the_agent-id"), + apiv1.NewStemcellCID("stemcell-id"), + apiv1.CloudPropsImpl{RawMessage: []byte(jsonStr)}, + networks, + []apiv1.DiskCID{}, + env, + ) + + Expect(imageServiceBuilder.BuildCallCount()).To(Equal(1)) + }) + + It("returns an error if the image service cannot be retrieved", func() { + imageServiceBuilder.BuildReturns(nil, errors.New("boom")) + + stemcellCID, networks, err := methods.NewCreateVMMethod( + &imageServiceBuilder, + &networkServiceBuilder, + &computeServiceBuilder, + &loadbalancerServiceBuilder, + cpiConfig, + &logger, + ).CreateVMV2( + apiv1.NewAgentID("the_agent-id"), + apiv1.NewStemcellCID("stemcell-id"), + apiv1.CloudPropsImpl{RawMessage: []byte(jsonStr)}, + networks, + []apiv1.DiskCID{}, + env, + ) + + Expect(err.Error()).To(Equal("failed to create image service: boom")) + Expect(stemcellCID).To(Equal(apiv1.VMCID{})) + Expect(networks).To(Equal(apiv1.Networks{})) + }) + + It("creates the loadbalancer service", func() { + _, _, _ = methods.NewCreateVMMethod( + &imageServiceBuilder, + &networkServiceBuilder, + &computeServiceBuilder, + &loadbalancerServiceBuilder, + cpiConfig, + &logger, + ).CreateVMV2( + apiv1.NewAgentID("the_agent-id"), + apiv1.NewStemcellCID("stemcell-id"), + apiv1.CloudPropsImpl{RawMessage: []byte(jsonStr)}, + networks, + []apiv1.DiskCID{}, + env, + ) + + Expect(loadbalancerServiceBuilder.BuildCallCount()).To(Equal(1)) + }) + + It("returns an error if the loadbalancer service cannot be retrieved", func() { + loadbalancerServiceBuilder.BuildReturns(nil, errors.New("boom")) + + stemcellCID, networks, err := methods.NewCreateVMMethod( + &imageServiceBuilder, + &networkServiceBuilder, + &computeServiceBuilder, + &loadbalancerServiceBuilder, + cpiConfig, + &logger, + ).CreateVMV2( + apiv1.NewAgentID("the_agent-id"), + apiv1.NewStemcellCID("stemcell-id"), + apiv1.CloudPropsImpl{RawMessage: []byte(jsonStr)}, + networks, + []apiv1.DiskCID{}, + env, + ) + + Expect(err.Error()).To(Equal("failed to create loadbalancer service: boom")) + Expect(stemcellCID).To(Equal(apiv1.VMCID{})) + Expect(networks).To(Equal(apiv1.Networks{})) + }) + + It("returns an error if the stemcell cannot be found", func() { + imageService.GetImageReturns("", errors.New("boom")) + + stemcellCID, networks, err := methods.NewCreateVMMethod( + &imageServiceBuilder, + &networkServiceBuilder, + &computeServiceBuilder, + &loadbalancerServiceBuilder, + cpiConfig, + &logger, + ).CreateVMV2( + apiv1.NewAgentID("the_agent-id"), + apiv1.NewStemcellCID("stemcell-id"), + apiv1.CloudPropsImpl{RawMessage: []byte(jsonStr)}, + networks, + []apiv1.DiskCID{}, + env, + ) + + Expect(err.Error()).To(ContainSubstring("failed to resolve stemcell: boom")) + Expect(stemcellCID).To(Equal(apiv1.VMCID{})) + Expect(networks).To(Equal(apiv1.Networks{})) + }) + + It("returns an error if the network config creation fails", func() { + networkService.GetNetworkConfigurationReturns(properties.NetworkConfig{}, errors.New("boom")) + + stemcellCID, networks, err := methods.NewCreateVMMethod( + &imageServiceBuilder, + &networkServiceBuilder, + &computeServiceBuilder, + &loadbalancerServiceBuilder, + cpiConfig, + &logger, + ).CreateVMV2( + apiv1.NewAgentID("the_agent-id"), + apiv1.NewStemcellCID("stemcell-id"), + apiv1.CloudPropsImpl{RawMessage: []byte(jsonStr)}, + networks, + []apiv1.DiskCID{}, + env, + ) + + Expect(err.Error()).To(ContainSubstring("failed to create network config: boom")) + Expect(stemcellCID).To(Equal(apiv1.VMCID{})) + Expect(networks).To(Equal(apiv1.Networks{})) + }) + }) + + Context("Port creation", func() { + It("creates a port per manual network", func() { + + _, _, _ = methods.NewCreateVMMethod( + &imageServiceBuilder, + &networkServiceBuilder, + &computeServiceBuilder, + &loadbalancerServiceBuilder, + cpiConfig, + &logger, + ).CreateVMV2( + apiv1.NewAgentID("the_agent-id"), + apiv1.NewStemcellCID("stemcell-id"), + apiv1.CloudPropsImpl{RawMessage: []byte(jsonStr)}, + networks, + []apiv1.DiskCID{}, + env, + ) + + Expect(networkService.CreatePortCallCount()).To(Equal(2)) + }) + + It("returns an error if port creation fails", func() { + networkService.CreatePortReturns(ports.Port{}, errors.New("boom")) + + _, _, err := methods.NewCreateVMMethod( + &imageServiceBuilder, + &networkServiceBuilder, + &computeServiceBuilder, + &loadbalancerServiceBuilder, + cpiConfig, + &logger, + ).CreateVMV2( + apiv1.NewAgentID("the_agent-id"), + apiv1.NewStemcellCID("stemcell-id"), + apiv1.CloudPropsImpl{RawMessage: []byte(jsonStr)}, + networks, + []apiv1.DiskCID{}, + env, + ) + + Expect(err.Error()).To(Equal("failed to create port: boom")) + }) + + It("configures the created ports in the network config", func() { + _, _, _ = methods.NewCreateVMMethod( + &imageServiceBuilder, + &networkServiceBuilder, + &computeServiceBuilder, + &loadbalancerServiceBuilder, + cpiConfig, + &logger, + ).CreateVMV2( + apiv1.NewAgentID("the_agent-id"), + apiv1.NewStemcellCID("stemcell-id"), + apiv1.CloudPropsImpl{RawMessage: []byte(jsonStr)}, + networks, + []apiv1.DiskCID{}, + env, + ) + + manualNetworks := networkConfig.ManualNetworks + Expect(manualNetworks[0].Port).To(Equal(port)) + Expect(manualNetworks[1].Port).To(Equal(port)) + }) + }) + + Context("Server creation", func() { + It("creates a server", func() { + _, _, _ = methods.NewCreateVMMethod( + &imageServiceBuilder, + &networkServiceBuilder, + &computeServiceBuilder, + &loadbalancerServiceBuilder, + cpiConfig, + &logger, + ).CreateVMV2( + apiv1.NewAgentID("the_agent-id"), + apiv1.NewStemcellCID("stemcell-id"), + apiv1.CloudPropsImpl{RawMessage: []byte(jsonStr)}, + networks, + []apiv1.DiskCID{}, + env, + ) + + stemcellCID, _, _, agentID, environment, _ := computeService.CreateServerArgsForCall(0) + Expect(stemcellCID.AsString()).To(Equal("stemcell-id")) + Expect(agentID.AsString()).To(Equal("the_agent-id")) + Expect(environment).To(Equal(env)) + }) + + It("returns an error if the server creation fails", func() { + computeService.CreateServerReturns(nil, errors.New("boom")) + + stemcellCID, networks, err := methods.NewCreateVMMethod( + &imageServiceBuilder, + &networkServiceBuilder, + &computeServiceBuilder, + &loadbalancerServiceBuilder, + cpiConfig, + &logger, + ).CreateVMV2( + apiv1.NewAgentID("the_agent-id"), + apiv1.NewStemcellCID("stemcell-id"), + apiv1.CloudPropsImpl{RawMessage: []byte(jsonStr)}, + networks, + []apiv1.DiskCID{}, + env, + ) + + Expect(err.Error()).To(Equal("failed to create server: boom")) + Expect(stemcellCID).To(Equal(apiv1.VMCID{})) + Expect(networks).To(Equal(apiv1.Networks{})) + }) + + It("returns a server ID and a network spec", func() { + stemcellCID, networkSpec, err := methods.NewCreateVMMethod( + &imageServiceBuilder, + &networkServiceBuilder, + &computeServiceBuilder, + &loadbalancerServiceBuilder, + cpiConfig, + &logger, + ).CreateVMV2( + apiv1.NewAgentID("the_agent-id"), + apiv1.NewStemcellCID("stemcell-id"), + apiv1.CloudPropsImpl{RawMessage: []byte(jsonStr)}, + networks, + []apiv1.DiskCID{}, + env, + ) + + Expect(err).ToNot(HaveOccurred()) + Expect(stemcellCID.AsString()).To(Equal("123-456")) + Expect(networkSpec).To(Equal(networks)) + }) + }) + + Context("VIP Network configuration", func() { + It("configures the VIP network of the created server", func() { + _, _, _ = methods.NewCreateVMMethod( + &imageServiceBuilder, + &networkServiceBuilder, + &computeServiceBuilder, + &loadbalancerServiceBuilder, + cpiConfig, + &logger, + ).CreateVMV2( + apiv1.NewAgentID("the_agent-id"), + apiv1.NewStemcellCID("stemcell-id"), + apiv1.CloudPropsImpl{RawMessage: []byte(jsonStr)}, + networks, + []apiv1.DiskCID{}, + env, + ) + + serverID, _ := networkService.ConfigureVIPNetworkArgsForCall(0) + Expect(serverID).To(Equal("123-456")) + }) + + It("returns an error if the vip network configuration fails", func() { + networkService.ConfigureVIPNetworkReturns(errors.New("boom")) + + stemcellCID, networks, err := methods.NewCreateVMMethod( + &imageServiceBuilder, + &networkServiceBuilder, + &computeServiceBuilder, + &loadbalancerServiceBuilder, + cpiConfig, + &logger, + ).CreateVMV2( + apiv1.NewAgentID("the_agent-id"), + apiv1.NewStemcellCID("stemcell-id"), + apiv1.CloudPropsImpl{RawMessage: []byte(jsonStr)}, + networks, + []apiv1.DiskCID{}, + env, + ) + + Expect(err.Error()).To(Equal("failed to configure vip network for server '123-456' with error: boom")) + Expect(stemcellCID).To(Equal(apiv1.VMCID{})) + Expect(networks).To(Equal(apiv1.Networks{})) + }) + }) + + Context("Loadbalancer configuration", func() { + It("gets pool ids of provided pools", func() { + + _, _, _ = methods.NewCreateVMMethod( + &imageServiceBuilder, + &networkServiceBuilder, + &computeServiceBuilder, + &loadbalancerServiceBuilder, + cpiConfig, + &logger, + ).CreateVMV2( + apiv1.NewAgentID("the_agent-id"), + apiv1.NewStemcellCID("stemcell-id"), + apiv1.CloudPropsImpl{RawMessage: []byte(jsonStr)}, + networks, + []apiv1.DiskCID{}, + env, + ) + + poolName := loadbalancerService.GetPoolArgsForCall(0) + Expect(poolName).To(Equal("the-pool-name-1")) + }) + + It("returns an error if getting pool ids fails", func() { + loadbalancerService.GetPoolReturnsOnCall(0, pools.Pool{}, errors.New("boom")) + + _, _, err := methods.NewCreateVMMethod( + &imageServiceBuilder, + &networkServiceBuilder, + &computeServiceBuilder, + &loadbalancerServiceBuilder, + cpiConfig, + &logger, + ).CreateVMV2( + apiv1.NewAgentID("the_agent-id"), + apiv1.NewStemcellCID("stemcell-id"), + apiv1.CloudPropsImpl{RawMessage: []byte(jsonStr)}, + networks, + []apiv1.DiskCID{}, + env, + ) + + Expect(err.Error()).To(ContainSubstring("failed to get pool ID of pool 'the-pool-name-1': boom")) + }) + + It("gets subnets of the default network", func() { + jsonStr := `{ + "instance_type": "type1", + "loadbalancer_pools": [{"name": "the-pool-name-1","port": 1234,"monitoring_port": 5678}], + "availability_zones": ["z1", "z2"] + }` + + _, _, _ = methods.NewCreateVMMethod( + &imageServiceBuilder, + &networkServiceBuilder, + &computeServiceBuilder, + &loadbalancerServiceBuilder, + cpiConfig, + &logger, + ).CreateVMV2( + apiv1.NewAgentID("the_agent-id"), + apiv1.NewStemcellCID("stemcell-id"), + apiv1.CloudPropsImpl{RawMessage: []byte(jsonStr)}, + networks, + []apiv1.DiskCID{}, + env, + ) + + Expect(loadbalancerService.CreatePoolMemberCallCount()).To(Equal(1)) + }) + + It("returns an error if getting subnets fails", func() { + networkService.GetSubnetIDReturns("", errors.New("boom")) + + _, _, err := methods.NewCreateVMMethod( + &imageServiceBuilder, + &networkServiceBuilder, + &computeServiceBuilder, + &loadbalancerServiceBuilder, + cpiConfig, + &logger, + ).CreateVMV2( + apiv1.NewAgentID("the_agent-id"), + apiv1.NewStemcellCID("stemcell-id"), + apiv1.CloudPropsImpl{RawMessage: []byte(jsonStr)}, + networks, + []apiv1.DiskCID{}, + env, + ) + + Expect(err.Error()).To(ContainSubstring("failed to get subnet: boom")) + }) + + It("Creates a single pool member", func() { + _, _, _ = methods.NewCreateVMMethod( + &imageServiceBuilder, + &networkServiceBuilder, + &computeServiceBuilder, + &loadbalancerServiceBuilder, + cpiConfig, + &logger, + ).CreateVMV2( + apiv1.NewAgentID("the_agent-id"), + apiv1.NewStemcellCID("stemcell-id"), + apiv1.CloudPropsImpl{RawMessage: []byte(jsonStr)}, + networks, + []apiv1.DiskCID{}, + env, + ) + + pool, ip, poolProps, subnetID, stateTimeOut := loadbalancerService.CreatePoolMemberArgsForCall(0) + Expect(pool.ID).To(Equal("the-pool-id-1")) + Expect(ip).To(Equal("1.1.1.1")) + Expect(poolProps.Name).To(Equal("the-pool-name-1")) + Expect(subnetID).To(Equal("the-subnet-id")) + Expect(stateTimeOut).To(Equal(1)) + }) + + It("Creates multiple pool members", func() { + jsonStr = `{ + "instance_type": "type1", + "loadbalancer_pools": [ + {"name": "the-pool-name-1","port": 1234,"monitoring_port": 5678}, + {"name": "the-pool-name-2","port": 1234,"monitoring_port": 5678} + ], + "availability_zones": ["z1", "z2"] + }` + + _, _, _ = methods.NewCreateVMMethod( + &imageServiceBuilder, + &networkServiceBuilder, + &computeServiceBuilder, + &loadbalancerServiceBuilder, + cpiConfig, + &logger, + ).CreateVMV2( + apiv1.NewAgentID("the_agent-id"), + apiv1.NewStemcellCID("stemcell-id"), + apiv1.CloudPropsImpl{RawMessage: []byte(jsonStr)}, + networks, + []apiv1.DiskCID{}, + env, + ) + + pool, ip, poolProps, subnetID, stateTimeOut := loadbalancerService.CreatePoolMemberArgsForCall(0) + Expect(pool.ID).To(Equal("the-pool-id-1")) + Expect(ip).To(Equal("1.1.1.1")) + Expect(poolProps.Name).To(Equal("the-pool-name-1")) + Expect(subnetID).To(Equal("the-subnet-id")) + Expect(stateTimeOut).To(Equal(1)) + + pool, ip, poolProps, subnetID, stateTimeOut = loadbalancerService.CreatePoolMemberArgsForCall(1) + Expect(pool.ID).To(Equal("the-pool-id-2")) + Expect(ip).To(Equal("1.1.1.1")) + Expect(poolProps.Name).To(Equal("the-pool-name-2")) + Expect(subnetID).To(Equal("the-subnet-id")) + Expect(stateTimeOut).To(Equal(1)) + }) + + It("returns an error if pool member creation fails", func() { + loadbalancerService.CreatePoolMemberReturns(nil, errors.New("boom")) + + _, _, err := methods.NewCreateVMMethod( + &imageServiceBuilder, + &networkServiceBuilder, + &computeServiceBuilder, + &loadbalancerServiceBuilder, + cpiConfig, + &logger, + ).CreateVMV2( + apiv1.NewAgentID("the_agent-id"), + apiv1.NewStemcellCID("stemcell-id"), + apiv1.CloudPropsImpl{RawMessage: []byte(jsonStr)}, + networks, + []apiv1.DiskCID{}, + env, + ) + + Expect(err.Error()).To(ContainSubstring("failed to create pool membership of IP '1.1.1.1' in pool 'the-pool-id-1': boom")) + }) + }) + + Context("VM Metadata", func() { + It("sets VM metadata", func() { + jsonStr = `{ + "instance_type": "type1", + "loadbalancer_pools": [ + {"name": "the-pool-name-1","port": 1234,"monitoring_port": 5678}, + {"name": "the-pool-name-2","port": 1234,"monitoring_port": 5678} + ], + "availability_zones": ["z1", "z2"] + }` + + loadbalancerService.CreatePoolMemberReturnsOnCall(0, &pools.Member{ID: "the-member-id", PoolID: "the-pool-id-1"}, nil) + loadbalancerService.CreatePoolMemberReturnsOnCall(1, &pools.Member{ID: "the-member-id-1", PoolID: "the-pool-id-2"}, nil) + + _, _, _ = methods.NewCreateVMMethod( + &imageServiceBuilder, + &networkServiceBuilder, + &computeServiceBuilder, + &loadbalancerServiceBuilder, + cpiConfig, + &logger, + ).CreateVMV2( + apiv1.NewAgentID("the_agent-id"), + apiv1.NewStemcellCID("stemcell-id"), + apiv1.CloudPropsImpl{RawMessage: []byte(jsonStr)}, + networks, + []apiv1.DiskCID{}, + env, + ) + + _, tags := computeService.UpdateServerMetadataArgsForCall(0) + Expect(tags["lbaas_pool_1"]).To(Equal("the-pool-id-1/the-member-id")) + Expect(tags["lbaas_pool_2"]).To(Equal("the-pool-id-2/the-member-id-1")) + }) + + It("returns an error if setting VM metadata fails", func() { + jsonStr = `{ + "instance_type": "type1", + "loadbalancer_pools": [ + {"name": "the-pool-name-1","port": 1234,"monitoring_port": 5678}, + {"name": "the-pool-name-2","port": 1234,"monitoring_port": 5678} + ], + "availability_zones": ["z1", "z2"] + }` + + computeService.UpdateServerMetadataReturns(errors.New("boom")) + loadbalancerService.CreatePoolMemberReturnsOnCall(0, &pools.Member{ID: "the-member-id", PoolID: "the-pool-id-1"}, nil) + loadbalancerService.CreatePoolMemberReturnsOnCall(1, &pools.Member{ID: "the-member-id-1", PoolID: "the-pool-id-2"}, nil) + + _, _, err := methods.NewCreateVMMethod( + &imageServiceBuilder, + &networkServiceBuilder, + &computeServiceBuilder, + &loadbalancerServiceBuilder, + cpiConfig, + &logger, + ).CreateVMV2( + apiv1.NewAgentID("the_agent-id"), + apiv1.NewStemcellCID("stemcell-id"), + apiv1.CloudPropsImpl{RawMessage: []byte(jsonStr)}, + networks, + []apiv1.DiskCID{}, + env, + ) + + Expect(err.Error()).To(ContainSubstring("failed to update metadata for server '123-456' with error: boom")) + }) + }) + + Context("Cleanup resources on error", func() { + It("deletes ports if server creation fails", func() { + computeService.CreateServerReturns(nil, errors.New("boom")) + + _, _, _ = methods.NewCreateVMMethod( + &imageServiceBuilder, + &networkServiceBuilder, + &computeServiceBuilder, + &loadbalancerServiceBuilder, + cpiConfig, + &logger, + ).CreateVMV2( + apiv1.NewAgentID("the_agent-id"), + apiv1.NewStemcellCID("stemcell-id"), + apiv1.CloudPropsImpl{RawMessage: []byte(jsonStr)}, + networks, + []apiv1.DiskCID{}, + env, + ) + + Expect(networkService.DeletePortsCallCount()).To(Equal(1)) + }) + + It("deletes ports and server if configuring vip network fails", func() { + networkService.ConfigureVIPNetworkReturns(errors.New("boom")) + + _, _, _ = methods.NewCreateVMMethod( + &imageServiceBuilder, + &networkServiceBuilder, + &computeServiceBuilder, + &loadbalancerServiceBuilder, + cpiConfig, + &logger, + ).CreateVMV2( + apiv1.NewAgentID("the_agent-id"), + apiv1.NewStemcellCID("stemcell-id"), + apiv1.CloudPropsImpl{RawMessage: []byte(jsonStr)}, + networks, + []apiv1.DiskCID{}, + env, + ) + + Expect(networkService.DeletePortsCallCount()).To(Equal(1)) + Expect(computeService.DeleteServerCallCount()).To(Equal(1)) + }) + + It("deletes ports, server, and pool members if update server metadata fails", func() { + networkService.ConfigureVIPNetworkReturns(errors.New("boom")) + + _, _, _ = methods.NewCreateVMMethod( + &imageServiceBuilder, + &networkServiceBuilder, + &computeServiceBuilder, + &loadbalancerServiceBuilder, + cpiConfig, + &logger, + ).CreateVMV2( + apiv1.NewAgentID("the_agent-id"), + apiv1.NewStemcellCID("stemcell-id"), + apiv1.CloudPropsImpl{RawMessage: []byte(jsonStr)}, + networks, + []apiv1.DiskCID{}, + env, + ) + + Expect(networkService.DeletePortsCallCount()).To(Equal(1)) + Expect(computeService.DeleteServerCallCount()).To(Equal(1)) + }) + + It("ignores errors from cleaning up resources", func() { + jsonStr = `{ + "instance_type": "type1", + "loadbalancer_pools": [ + {"name": "the-pool-name-1","port": 1234,"monitoring_port": 5678}, + {"name": "the-pool-name-2","port": 1234,"monitoring_port": 5678} + ], + "availability_zones": ["z1", "z2"] + }` + + loadbalancerService.DeletePoolMemberReturns(errors.New("boom")) + computeService.DeleteServerReturns(errors.New("boom")) + networkService.DeletePortsReturns(errors.New("boom")) + computeService.UpdateServerMetadataReturns(errors.New("boom")) + loadbalancerService.CreatePoolMemberReturnsOnCall(0, &pools.Member{ID: "the-member-id", PoolID: "the-pool-id-1"}, nil) + loadbalancerService.CreatePoolMemberReturnsOnCall(1, &pools.Member{ID: "the-member-id-1", PoolID: "the-pool-id-2"}, nil) + + _, _, err := methods.NewCreateVMMethod( + &imageServiceBuilder, + &networkServiceBuilder, + &computeServiceBuilder, + &loadbalancerServiceBuilder, + cpiConfig, + &logger, + ).CreateVMV2( + apiv1.NewAgentID("the_agent-id"), + apiv1.NewStemcellCID("stemcell-id"), + apiv1.CloudPropsImpl{RawMessage: []byte(jsonStr)}, + networks, + []apiv1.DiskCID{}, + env, + ) + + Expect(err.Error()).To(ContainSubstring("failed to update metadata for server '123-456' with error: boom")) + }) + }) + }) +}) diff --git a/src/openstack_cpi_golang/cpi/methods/delete_disk.go b/src/openstack_cpi_golang/cpi/methods/delete_disk.go index e8a9d62f..a87e4b37 100644 --- a/src/openstack_cpi_golang/cpi/methods/delete_disk.go +++ b/src/openstack_cpi_golang/cpi/methods/delete_disk.go @@ -1,15 +1,68 @@ package methods import ( + "errors" + "fmt" + "time" + "github.com/cloudfoundry/bosh-cpi-go/apiv1" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/config" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/volume" + "github.com/gophercloud/gophercloud" ) -type DeleteDiskMethod struct{} +type DeleteDiskMethod struct { + volumeServiceBuilder volume.VolumeServiceBuilder + cpiConfig config.CpiConfig + logger utils.Logger +} -func NewDeleteDiskMethod() DeleteDiskMethod { - return DeleteDiskMethod{} +func NewDeleteDiskMethod( + volumeServiceBuilder volume.VolumeServiceBuilder, + cpiConfig config.CpiConfig, + logger utils.Logger, +) DeleteDiskMethod { + return DeleteDiskMethod{ + volumeServiceBuilder: volumeServiceBuilder, + cpiConfig: cpiConfig, + logger: logger, + } } func (a DeleteDiskMethod) DeleteDisk(diskCID apiv1.DiskCID) error { + var errDefault404 gophercloud.ErrDefault404 + openstackConfig := a.cpiConfig.Cloud.Properties.Openstack + + volumeService, err := a.volumeServiceBuilder.Build() + if err != nil { + return fmt.Errorf("failed to create volume service: %w", err) + } + + volume, err := volumeService.GetVolume(diskCID.AsString()) + if err != nil { + if errors.As(err, &errDefault404) { + a.logger.Info("volume %s not found. Skipping.", diskCID.AsString()) + return nil + } + return fmt.Errorf("delete disk: %w", err) + } + + if volume.ID != "" { + if volume.Status != "available" { + return fmt.Errorf("cannot delete volume %s, state is %s", diskCID.AsString(), volume.Status) + } + err := volumeService.DeleteVolume(volume.ID) + if err != nil { + return fmt.Errorf("delete disk: %w", err) + } + + err = volumeService.WaitForVolumeToBecomeStatus(volume.ID, time.Duration(openstackConfig.StateTimeOut)*time.Second, "deleted") + if err != nil { + return fmt.Errorf("delete disk: %w", err) + } + } else { + a.logger.Info("volume %s not found. Skipping.", diskCID.AsString()) + } return nil } diff --git a/src/openstack_cpi_golang/cpi/methods/delete_disk_test.go b/src/openstack_cpi_golang/cpi/methods/delete_disk_test.go new file mode 100644 index 00000000..9c1690bd --- /dev/null +++ b/src/openstack_cpi_golang/cpi/methods/delete_disk_test.go @@ -0,0 +1,156 @@ +package methods_test + +import ( + "errors" + + "github.com/cloudfoundry/bosh-cpi-go/apiv1" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/config" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/methods" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils/utilsfakes" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/volume/volumefakes" + "github.com/gophercloud/gophercloud/openstack/blockstorage/v3/volumes" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("DeleteDisk", func() { + + var cpiConfig config.CpiConfig + var logger utilsfakes.FakeLogger + var volumeService volumefakes.FakeVolumeService + var volumeServiceBuilder volumefakes.FakeVolumeServiceBuilder + + Context("DeleteDisk", func() { + BeforeEach(func() { + volumeServiceBuilder = volumefakes.FakeVolumeServiceBuilder{} + volumeService = volumefakes.FakeVolumeService{} + + volumeServiceBuilder.BuildReturns(&volumeService, nil) + volumeService.GetVolumeReturns(&volumes.Volume{ID: "some-disk-cid", Status: "available"}, nil) + volumeService.DeleteVolumeReturns(nil) + volumeService.WaitForVolumeToBecomeStatusReturns(nil) + cpiConfig = config.CpiConfig{} + cpiConfig.Cloud.Properties.Openstack = config.OpenstackConfig{} + + logger = utilsfakes.FakeLogger{} + }) + + It("returns an error if the volume service cannot be retrieved", func() { + volumeServiceBuilder.BuildReturns(nil, errors.New("boom")) + + err := methods.NewDeleteDiskMethod( + &volumeServiceBuilder, + cpiConfig, + &logger, + ).DeleteDisk( + apiv1.NewDiskCID("some-disk-cid"), + ) + + Expect(err.Error()).To(Equal("failed to create volume service: boom")) + }) + + It("creates the volume service", func() { + err := methods.NewDeleteDiskMethod( + &volumeServiceBuilder, + cpiConfig, + &logger, + ).DeleteDisk( + apiv1.NewDiskCID("some-disk-cid"), + ) + + Expect(err).ToNot(HaveOccurred()) + Expect(volumeServiceBuilder.BuildCallCount()).To(Equal(1)) + }) + + It("returns an error if the volume cannot be retrieved", func() { + volumeService.GetVolumeReturns(&volumes.Volume{}, errors.New("some_error_while_getting_volume")) + + err := methods.NewDeleteDiskMethod( + &volumeServiceBuilder, + cpiConfig, + &logger, + ).DeleteDisk( + apiv1.NewDiskCID("some-disk-cid"), + ) + + Expect(err.Error()).To(Equal("delete disk: some_error_while_getting_volume")) + }) + + It("returns an error if volume is not in status available", func() { + volumeService.GetVolumeReturns(&volumes.Volume{ID: "some-disk-cid", Status: "not_available"}, nil) + + err := methods.NewDeleteDiskMethod( + &volumeServiceBuilder, + cpiConfig, + &logger, + ).DeleteDisk( + apiv1.NewDiskCID("some-disk-cid"), + ) + + Expect(err.Error()).To(Equal("cannot delete volume some-disk-cid, state is not_available")) + }) + + It("returns an error if the volume cannot be deleted", func() { + volumeService.DeleteVolumeReturns(errors.New("some_error_while_deleting_volume")) + + err := methods.NewDeleteDiskMethod( + &volumeServiceBuilder, + cpiConfig, + &logger, + ).DeleteDisk( + apiv1.NewDiskCID("some-disk-cid"), + ) + + Expect(err.Error()).To(Equal("delete disk: some_error_while_deleting_volume")) + }) + + It("fails while waiting for the volume to become deleted", func() { + volumeService.WaitForVolumeToBecomeStatusReturns(errors.New("some_error_while_waiting_for_volume")) + + err := methods.NewDeleteDiskMethod( + &volumeServiceBuilder, + cpiConfig, + &logger, + ).DeleteDisk( + apiv1.NewDiskCID("some-disk-cid"), + ) + + Expect(volumeService.WaitForVolumeToBecomeStatusCallCount()).To(Equal(1)) + Expect(err.Error()).To(Equal("delete disk: some_error_while_waiting_for_volume")) + }) + + It("deletes the volume", func() { + err := methods.NewDeleteDiskMethod( + &volumeServiceBuilder, + cpiConfig, + &logger, + ).DeleteDisk( + apiv1.NewDiskCID("some-disk-cid"), + ) + + Expect(volumeService.DeleteVolumeCallCount()).To(Equal(1)) + Expect(err).ToNot(HaveOccurred()) + }) + + Context("when volume is not found. Skipping.", func() { + BeforeEach(func() { + volumeService.GetVolumeReturns(&volumes.Volume{}, nil) + }) + + It("issues a logger message", func() { + _ = methods.NewDeleteDiskMethod( + &volumeServiceBuilder, + cpiConfig, + &logger, + ).DeleteDisk( + apiv1.NewDiskCID("some-disk-cid"), + ) + firstLoggerInfo, secondLoggerInfo, _ := logger.InfoArgsForCall(0) + + Expect(logger.InfoCallCount()).To(Equal(1)) + Expect(firstLoggerInfo).To(Equal("volume %s not found. Skipping.")) + Expect(secondLoggerInfo).To(Equal("some-disk-cid")) + }) + }) + }) +}) diff --git a/src/openstack_cpi_golang/cpi/methods/delete_snapshot.go b/src/openstack_cpi_golang/cpi/methods/delete_snapshot.go index 0d151ea7..7275a289 100644 --- a/src/openstack_cpi_golang/cpi/methods/delete_snapshot.go +++ b/src/openstack_cpi_golang/cpi/methods/delete_snapshot.go @@ -1,15 +1,51 @@ package methods import ( + "fmt" + "time" + "github.com/cloudfoundry/bosh-cpi-go/apiv1" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/config" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/volume" ) -type DeleteSnapshotMethod struct{} +type DeleteSnapshotMethod struct { + volumeServiceBuilder volume.VolumeServiceBuilder + cpiConfig config.CpiConfig + logger utils.Logger +} -func NewDeleteSnapshotMethod() DeleteSnapshotMethod { - return DeleteSnapshotMethod{} +func NewDeleteSnapshotMethod( + volumeServiceBuilder volume.VolumeServiceBuilder, + cpiConfig config.CpiConfig, + logger utils.Logger) DeleteSnapshotMethod { + return DeleteSnapshotMethod{ + volumeServiceBuilder: volumeServiceBuilder, + cpiConfig: cpiConfig, + logger: logger, + } } func (s DeleteSnapshotMethod) DeleteSnapshot(cid apiv1.SnapshotCID) error { + + s.logger.Info("delete_snapshot", fmt.Sprintf("Execute delete snapshot ID %s", cid.AsString())) + + volumeService, err := s.volumeServiceBuilder.Build() + if err != nil { + return fmt.Errorf("deleteSnapshot: Failed to get volume service: %w", err) + } + + err = volumeService.DeleteSnapshot(cid.AsString()) + if err != nil { + return fmt.Errorf("deleteSnapshot: Failed to delete snapshot ID %s: %w", cid.AsString(), err) + } + + s.logger.Info("delete_snapshot", fmt.Sprintf("Waiting for snapshot ID %s to be deleted", cid.AsString())) + err = volumeService.WaitForSnapshotToBecomeStatus(cid.AsString(), time.Duration(s.cpiConfig.OpenStackConfig().StateTimeOut)*time.Second, "deleted") + if err != nil { + return fmt.Errorf("deleteSnapshot: Failed while waiting for snapshot ID %s to be deleted: %w", cid.AsString(), err) + } + return nil } diff --git a/src/openstack_cpi_golang/cpi/methods/delete_snapshot_test.go b/src/openstack_cpi_golang/cpi/methods/delete_snapshot_test.go new file mode 100644 index 00000000..b6911809 --- /dev/null +++ b/src/openstack_cpi_golang/cpi/methods/delete_snapshot_test.go @@ -0,0 +1,69 @@ +package methods_test + +import ( + "errors" + "time" + + "github.com/cloudfoundry/bosh-cpi-go/apiv1" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/config" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/methods" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils/utilsfakes" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/volume/volumefakes" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("DeleteSnapshotMethod Unit Tests", func() { + var ( + volumeServiceBuilder *volumefakes.FakeVolumeServiceBuilder + volumeService *volumefakes.FakeVolumeService + logger *utilsfakes.FakeLogger + cpiConfig config.CpiConfig + deleteSnapshot methods.DeleteSnapshotMethod + snapshotCID apiv1.SnapshotCID + ) + + BeforeEach(func() { + volumeServiceBuilder = new(volumefakes.FakeVolumeServiceBuilder) + volumeService = new(volumefakes.FakeVolumeService) + logger = new(utilsfakes.FakeLogger) + + volumeServiceBuilder.BuildReturns(volumeService, nil) + snapshotCID = apiv1.NewSnapshotCID("snap1-id") + + cpiConfig = config.CpiConfig{} + deleteSnapshot = methods.NewDeleteSnapshotMethod(volumeServiceBuilder, cpiConfig, logger) + }) + + It("fails on volume service builder", func() { + volumeServiceBuilder.BuildReturns(nil, errors.New("boom")) + err := deleteSnapshot.DeleteSnapshot(snapshotCID) + Expect(err.Error()).To(Equal("deleteSnapshot: Failed to get volume service: boom")) + }) + + It("fails to delete snapshot", func() { + volumeService.DeleteSnapshotReturns(errors.New("boom")) + err := deleteSnapshot.DeleteSnapshot(snapshotCID) + Expect(err.Error()).To(Equal("deleteSnapshot: Failed to delete snapshot ID snap1-id: boom")) + }) + + It("fails to wait for snapshot to be deleted", func() { + volumeService.WaitForSnapshotToBecomeStatusReturns(errors.New("boom")) + err := deleteSnapshot.DeleteSnapshot(snapshotCID) + Expect(err.Error()).To(Equal("deleteSnapshot: Failed while waiting for snapshot ID snap1-id to be deleted: boom")) + }) + + It("successfully deletes snapshot", func() { + err := deleteSnapshot.DeleteSnapshot(snapshotCID) + Expect(err).ToNot(HaveOccurred()) + + Expect(volumeService.DeleteSnapshotCallCount()).To(Equal(1)) + Expect(volumeService.DeleteSnapshotArgsForCall(0)).To(Equal("snap1-id")) + + Expect(volumeService.WaitForSnapshotToBecomeStatusCallCount()).To(Equal(1)) + snapshotID, timeout, status := volumeService.WaitForSnapshotToBecomeStatusArgsForCall(0) + Expect(snapshotID).To(Equal("snap1-id")) + Expect(timeout).To(Equal(time.Duration(cpiConfig.OpenStackConfig().StateTimeOut) * time.Second)) + Expect(status).To(Equal("deleted")) + }) +}) diff --git a/src/openstack_cpi_golang/cpi/methods/delete_stemcell.go b/src/openstack_cpi_golang/cpi/methods/delete_stemcell.go index 82252320..b5fdf042 100644 --- a/src/openstack_cpi_golang/cpi/methods/delete_stemcell.go +++ b/src/openstack_cpi_golang/cpi/methods/delete_stemcell.go @@ -1,15 +1,40 @@ package methods import ( + "fmt" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/image" + "github.com/cloudfoundry/bosh-cpi-go/apiv1" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils" ) -type DeleteStemcellMethod struct{} +type DeleteStemcellMethod struct { + imageServiceBuilder image.ImageServiceBuilder + logger utils.Logger +} -func NewDeleteStemcellMethod() DeleteStemcellMethod { - return DeleteStemcellMethod{} +func NewDeleteStemcellMethod( + serviceFactory image.ImageServiceBuilder, + logger utils.Logger, +) DeleteStemcellMethod { + return DeleteStemcellMethod{ + imageServiceBuilder: serviceFactory, + logger: logger, + } } func (a DeleteStemcellMethod) DeleteStemcell(cid apiv1.StemcellCID) error { + a.logger.Info("delete_stemcell", "Creating image service ...") + imageService, err := a.imageServiceBuilder.Build() + if err != nil { + return fmt.Errorf("failed to create image service: %w", err) + } + + deletionError := imageService.DeleteImage(cid.AsString()) + + if deletionError != nil { + return fmt.Errorf("failed to delete stemcell with cid %s due to the following: %w", cid.AsString(), deletionError) + } return nil } diff --git a/src/openstack_cpi_golang/cpi/methods/delete_stemcell_test.go b/src/openstack_cpi_golang/cpi/methods/delete_stemcell_test.go new file mode 100644 index 00000000..0548595a --- /dev/null +++ b/src/openstack_cpi_golang/cpi/methods/delete_stemcell_test.go @@ -0,0 +1,63 @@ +package methods_test + +import ( + "errors" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/image/imagefakes" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/methods" + + "github.com/cloudfoundry/bosh-cpi-go/apiv1" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils/utilsfakes" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("DeleteStemcellMethod", func() { + + var imageServiceBuilder imagefakes.FakeImageServiceBuilder + var logger utilsfakes.FakeLogger + var imageService imagefakes.FakeImageService + + Context("DeleteStemcell", func() { + + BeforeEach(func() { + imageServiceBuilder = imagefakes.FakeImageServiceBuilder{} + logger = utilsfakes.FakeLogger{} + }) + + It("deletes an image ID", func() { + imageServiceBuilder.BuildReturns(&imageService, nil) + imageService.DeleteImageReturns(nil) + err := methods.NewDeleteStemcellMethod( + &imageServiceBuilder, + &logger, + ).DeleteStemcell(apiv1.NewStemcellCID("cloudID")) + + cid := imageService.DeleteImageArgsForCall(0) + Expect(err).ToNot(HaveOccurred()) + Expect(imageService.DeleteImageCallCount()).To(Equal(1)) + Expect(cid).To(Equal("cloudID")) + }) + + It("returns an error if image cannot be deleted", func() { + imageServiceBuilder.BuildReturns(&imageService, nil) + imageService.DeleteImageReturns(errors.New("boom")) + err := methods.NewDeleteStemcellMethod( + &imageServiceBuilder, + &logger, + ).DeleteStemcell(apiv1.NewStemcellCID("cloudID")) + + Expect(err.Error()).To(Equal("failed to delete stemcell with cid cloudID due to the following: boom")) + }) + + It("returns an error if the image service cannot be retrieved", func() { + imageServiceBuilder.BuildReturns(nil, errors.New("boom")) + err := methods.NewDeleteStemcellMethod( + &imageServiceBuilder, + &logger, + ).DeleteStemcell(apiv1.NewStemcellCID("cloudID")) + + Expect(err.Error()).To(Equal("failed to create image service: boom")) + }) + }) +}) diff --git a/src/openstack_cpi_golang/cpi/methods/delete_vm.go b/src/openstack_cpi_golang/cpi/methods/delete_vm.go index d1daacfb..69204860 100644 --- a/src/openstack_cpi_golang/cpi/methods/delete_vm.go +++ b/src/openstack_cpi_golang/cpi/methods/delete_vm.go @@ -1,15 +1,90 @@ package methods import ( + "fmt" + "strings" + "github.com/cloudfoundry/bosh-cpi-go/apiv1" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/compute" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/config" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/loadbalancer" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/network" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/properties" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils" ) -type DeleteVMMethod struct{} +type DeleteVMMethod struct { + networkServiceBuilder network.NetworkServiceBuilder + computeServiceBuilder compute.ComputeServiceBuilder + loadbalancerServiceBuilder loadbalancer.LoadbalancerServiceBuilder + cpiConfig config.CpiConfig + logger utils.Logger +} -func NewDeleteVMMethod() DeleteVMMethod { - return DeleteVMMethod{} +func NewDeleteVMMethod( + networkServiceBuilder network.NetworkServiceBuilder, + computeServiceBuilder compute.ComputeServiceBuilder, + loadbalancerServiceBuilder loadbalancer.LoadbalancerServiceBuilder, + config config.CpiConfig, + logger utils.Logger, +) DeleteVMMethod { + return DeleteVMMethod{ + networkServiceBuilder: networkServiceBuilder, + computeServiceBuilder: computeServiceBuilder, + loadbalancerServiceBuilder: loadbalancerServiceBuilder, + cpiConfig: config, + logger: logger, + } } func (a DeleteVMMethod) DeleteVM(cid apiv1.VMCID) error { + computeService, err := a.computeServiceBuilder.Build() + if err != nil { + return fmt.Errorf("delete_vm: %w", err) + } + + networkService, err := a.networkServiceBuilder.Build() + if err != nil { + return fmt.Errorf("delete_vm: %w", err) + } + + // Get ports before deleting the server so that it is still assigned to the server + ports, err := networkService.GetPorts(cid.AsString(), properties.Network{}, true) + if err != nil { + return fmt.Errorf("delete_vm: %w", err) + } + + serverMetadata, err := computeService.GetMetadata(cid.AsString()) + if err != nil { + return fmt.Errorf("delete_vm: %w", err) + } + + if len(serverMetadata) > 0 { + loadbalancerService, err := a.loadbalancerServiceBuilder.Build() + if err != nil { + return fmt.Errorf("delete_vm: %w", err) + } + + for key, value := range serverMetadata { + if strings.HasPrefix(key, "lbaas_pool_") { + parts := strings.Split(value, "/") + err = loadbalancerService.DeletePoolMember(parts[0], parts[1], a.cpiConfig.Cloud.Properties.Openstack.StateTimeOut) + if err != nil { + return fmt.Errorf("delete_vm: %w", err) + } + } + } + } + + err = computeService.DeleteServer(cid.AsString(), a.cpiConfig) + if err != nil { + return fmt.Errorf("delete_vm: %w", err) + } + + err = networkService.DeletePorts(ports) + if err != nil { + return fmt.Errorf("delete_vm: %w", err) + } + return nil } diff --git a/src/openstack_cpi_golang/cpi/methods/delete_vm_test.go b/src/openstack_cpi_golang/cpi/methods/delete_vm_test.go new file mode 100644 index 00000000..a80393a0 --- /dev/null +++ b/src/openstack_cpi_golang/cpi/methods/delete_vm_test.go @@ -0,0 +1,328 @@ +package methods_test + +import ( + "errors" + + "github.com/cloudfoundry/bosh-cpi-go/apiv1" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/compute/computefakes" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/config" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/loadbalancer/loadbalancerfakes" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/methods" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/network/networkfakes" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils/utilsfakes" + "github.com/gophercloud/gophercloud/openstack/networking/v2/ports" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("DeleteVMMethod", func() { + + var computeServiceBuilder computefakes.FakeComputeServiceBuilder + var networkServiceBuilder networkfakes.FakeNetworkServiceBuilder + var loadbalancerServiceBuilder loadbalancerfakes.FakeLoadbalancerServiceBuilder + var computeService computefakes.FakeComputeService + var networkService networkfakes.FakeNetworkService + var loadbalancerService loadbalancerfakes.FakeLoadbalancerService + var logger utilsfakes.FakeLogger + + Context("DELETEVMV", func() { + + BeforeEach(func() { + computeServiceBuilder = computefakes.FakeComputeServiceBuilder{} + networkServiceBuilder = networkfakes.FakeNetworkServiceBuilder{} + loadbalancerServiceBuilder = loadbalancerfakes.FakeLoadbalancerServiceBuilder{} + + computeService = computefakes.FakeComputeService{} + networkService = networkfakes.FakeNetworkService{} + loadbalancerService = loadbalancerfakes.FakeLoadbalancerService{} + + computeServiceBuilder.BuildReturns(&computeService, nil) + networkServiceBuilder.BuildReturns(&networkService, nil) + loadbalancerServiceBuilder.BuildReturns(&loadbalancerService, nil) + + computeService.DeleteServerReturns(nil) + computeService.GetMetadataReturns(map[string]string{"tag1": "tag1Value", "lbaas_pool_1": "poolID/memberID"}, nil) + networkService.GetPortsReturns([]ports.Port{{ID: "test"}}, nil) + networkService.DeletePortsReturns(nil) + loadbalancerService.DeletePoolMemberReturns(nil) + + logger = utilsfakes.FakeLogger{} + }) + + It("creates the compute service", func() { + _ = methods.NewDeleteVMMethod( + &networkServiceBuilder, + &computeServiceBuilder, + &loadbalancerServiceBuilder, + config.CpiConfig{}, + &logger, + ).DeleteVM( + apiv1.NewVMCID("vm-id"), + ) + + Expect(computeServiceBuilder.BuildCallCount()).To(Equal(1)) + }) + + It("returns an error if the compute service cannot be retrieved", func() { + computeServiceBuilder.BuildReturns(nil, errors.New("boom")) + + err := methods.NewDeleteVMMethod( + &networkServiceBuilder, + &computeServiceBuilder, + &loadbalancerServiceBuilder, + config.CpiConfig{}, + &logger, + ).DeleteVM( + apiv1.NewVMCID("vm-id"), + ) + + Expect(err.Error()).To(Equal("delete_vm: boom")) + }) + + It("creates the network service", func() { + _ = methods.NewDeleteVMMethod( + &networkServiceBuilder, + &computeServiceBuilder, + &loadbalancerServiceBuilder, + config.CpiConfig{}, + &logger, + ).DeleteVM( + apiv1.NewVMCID("vm-id"), + ) + + Expect(networkServiceBuilder.BuildCallCount()).To(Equal(1)) + }) + + It("returns an error if the network service cannot be retrieved", func() { + networkServiceBuilder.BuildReturns(nil, errors.New("boom")) + + err := methods.NewDeleteVMMethod( + &networkServiceBuilder, + &computeServiceBuilder, + &loadbalancerServiceBuilder, + config.CpiConfig{}, + &logger, + ).DeleteVM( + apiv1.NewVMCID("vm-id"), + ) + + Expect(err.Error()).To(Equal("delete_vm: boom")) + }) + + It("get ports has been called once with the correct parameters", func() { + err := methods.NewDeleteVMMethod( + &networkServiceBuilder, + &computeServiceBuilder, + &loadbalancerServiceBuilder, + config.CpiConfig{}, + &logger, + ).DeleteVM( + apiv1.NewVMCID("vm-id"), + ) + + serverID, _, _ := networkService.GetPortsArgsForCall(0) + Expect(serverID).To(Equal("vm-id")) + Expect(err).ToNot(HaveOccurred()) + }) + + It("returns an error if no ports were found", func() { + networkService.GetPortsReturns(nil, errors.New("boom")) + + err := methods.NewDeleteVMMethod( + &networkServiceBuilder, + &computeServiceBuilder, + &loadbalancerServiceBuilder, + config.CpiConfig{}, + &logger, + ).DeleteVM( + apiv1.NewVMCID("vm-id"), + ) + + Expect(err.Error()).To(Equal("delete_vm: boom")) + + }) + + It("calls serverMetadata with correct cid", func() { + _ = methods.NewDeleteVMMethod( + &networkServiceBuilder, + &computeServiceBuilder, + &loadbalancerServiceBuilder, + config.CpiConfig{}, + &logger, + ).DeleteVM( + apiv1.NewVMCID("vm-id"), + ) + + serverID := computeService.GetMetadataArgsForCall(0) + Expect(serverID).To(Equal("vm-id")) + }) + + It("returns an error if no server metadata was found", func() { + computeService.GetMetadataReturns(nil, errors.New("boom")) + + err := methods.NewDeleteVMMethod( + &networkServiceBuilder, + &computeServiceBuilder, + &loadbalancerServiceBuilder, + config.CpiConfig{}, + &logger, + ).DeleteVM( + apiv1.NewVMCID("vm-id"), + ) + + Expect(err.Error()).To(Equal("delete_vm: boom")) + }) + + It("creates the loadbalancer service", func() { + err := methods.NewDeleteVMMethod( + &networkServiceBuilder, + &computeServiceBuilder, + &loadbalancerServiceBuilder, + config.CpiConfig{}, + &logger, + ).DeleteVM( + apiv1.NewVMCID("vm-id"), + ) + + Expect(err).ToNot(HaveOccurred()) + Expect(loadbalancerServiceBuilder.BuildCallCount()).To(Equal(1)) + }) + + It("returns an error if the loadbalancer service cannot be retrieved", func() { + loadbalancerServiceBuilder.BuildReturns(nil, errors.New("boom")) + + err := methods.NewDeleteVMMethod( + &networkServiceBuilder, + &computeServiceBuilder, + &loadbalancerServiceBuilder, + config.CpiConfig{}, + &logger, + ).DeleteVM( + apiv1.NewVMCID("vm-id"), + ) + + Expect(err.Error()).To(Equal("delete_vm: boom")) + }) + + It("does not remove pool memberships if no server tags are found", func() { + computeService.GetMetadataReturns(map[string]string{}, nil) + + err := methods.NewDeleteVMMethod( + &networkServiceBuilder, + &computeServiceBuilder, + &loadbalancerServiceBuilder, + config.CpiConfig{}, + &logger, + ).DeleteVM( + apiv1.NewVMCID("vm-id"), + ) + + Expect(err).ToNot(HaveOccurred()) + Expect(loadbalancerService.DeletePoolMemberCallCount()).To(Equal(0)) + Expect(computeService.DeleteServerCallCount()).To(Equal(1)) + }) + + It("deletes a pool member for tags with prefix 'lbaas_pool_'", func() { + err := methods.NewDeleteVMMethod( + &networkServiceBuilder, + &computeServiceBuilder, + &loadbalancerServiceBuilder, + config.CpiConfig{}, + &logger, + ).DeleteVM( + apiv1.NewVMCID("vm-id"), + ) + + poolID, memberID, _ := loadbalancerService.DeletePoolMemberArgsForCall(0) + + Expect(poolID).To(Equal("poolID")) + Expect(memberID).To(Equal("memberID")) + Expect(err).ToNot(HaveOccurred()) + Expect(loadbalancerService.DeletePoolMemberCallCount()).To(Equal(1)) + }) + + It("returns an error if deleting a pool member fails", func() { + loadbalancerService.DeletePoolMemberReturns(errors.New("boom")) + + err := methods.NewDeleteVMMethod( + &networkServiceBuilder, + &computeServiceBuilder, + &loadbalancerServiceBuilder, + config.CpiConfig{}, + &logger, + ).DeleteVM( + apiv1.NewVMCID("vm-id"), + ) + + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal("delete_vm: boom")) + }) + + It("deletes a server", func() { + err := methods.NewDeleteVMMethod( + &networkServiceBuilder, + &computeServiceBuilder, + &loadbalancerServiceBuilder, + config.CpiConfig{}, + &logger, + ).DeleteVM( + apiv1.NewVMCID("vm-id"), + ) + + serverID, _ := computeService.DeleteServerArgsForCall(0) + Expect(serverID).To(Equal("vm-id")) + Expect(err).ToNot(HaveOccurred()) + }) + + It("returns an error if the server deletion fails", func() { + computeService.DeleteServerReturns(errors.New("boom")) + + err := methods.NewDeleteVMMethod( + &networkServiceBuilder, + &computeServiceBuilder, + &loadbalancerServiceBuilder, + config.CpiConfig{}, + &logger, + ).DeleteVM( + apiv1.NewVMCID("vm-id"), + ) + + Expect(err.Error()).To(Equal("delete_vm: boom")) + }) + + It("delete ports has been called once with the correct parameters", func() { + err := methods.NewDeleteVMMethod( + &networkServiceBuilder, + &computeServiceBuilder, + &loadbalancerServiceBuilder, + config.CpiConfig{}, + &logger, + ).DeleteVM( + apiv1.NewVMCID("vm-id"), + ) + + exp := []ports.Port{{ID: "test"}} + ports := networkService.DeletePortsArgsForCall(0) + Expect(ports).To(Equal(exp)) + Expect(err).ToNot(HaveOccurred()) + }) + + It("returns an error if deleting ports fails", func() { + networkService.DeletePortsReturns(errors.New("boom")) + + err := methods.NewDeleteVMMethod( + &networkServiceBuilder, + &computeServiceBuilder, + &loadbalancerServiceBuilder, + config.CpiConfig{}, + &logger, + ).DeleteVM( + apiv1.NewVMCID("vm-id"), + ) + + Expect(err.Error()).To(Equal("delete_vm: boom")) + + }) + + }) +}) diff --git a/src/openstack_cpi_golang/cpi/methods/detach_disk.go b/src/openstack_cpi_golang/cpi/methods/detach_disk.go index 3cd08d50..c6640381 100644 --- a/src/openstack_cpi_golang/cpi/methods/detach_disk.go +++ b/src/openstack_cpi_golang/cpi/methods/detach_disk.go @@ -1,15 +1,85 @@ package methods import ( + "fmt" + "time" + "github.com/cloudfoundry/bosh-cpi-go/apiv1" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/compute" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/config" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/volume" ) -type DetachDiskMethod struct{} +type DetachDiskMethod struct { + computeServiceBuilder compute.ComputeServiceBuilder + volumeServiceBuilder volume.VolumeServiceBuilder + cpiConfig config.CpiConfig + logger utils.Logger +} -func NewDetachDiskMethod() DetachDiskMethod { - return DetachDiskMethod{} +func NewDetachDiskMethod( + computeServiceBuilder compute.ComputeServiceBuilder, + volumeServiceBuilder volume.VolumeServiceBuilder, + cpiConfig config.CpiConfig, + logger utils.Logger, +) DetachDiskMethod { + return DetachDiskMethod{ + computeServiceBuilder: computeServiceBuilder, + volumeServiceBuilder: volumeServiceBuilder, + cpiConfig: cpiConfig, + logger: logger, + } } func (a DetachDiskMethod) DetachDisk(vmCID apiv1.VMCID, diskCID apiv1.DiskCID) error { + + openstackConfig := a.cpiConfig.Cloud.Properties.Openstack + + a.logger.Info("detach_disk", fmt.Sprintf("Execute detach disk ID %s from VM ID %s", diskCID.AsString(), vmCID.AsString())) + + computeService, err := a.computeServiceBuilder.Build() + if err != nil { + return fmt.Errorf("detach_disk: Failed to get compute service: %w", err) + } + _, err = computeService.GetServer(vmCID.AsString()) + if err != nil { + return fmt.Errorf("detach_disk: Failed to get VM %s: %w", vmCID.AsString(), err) + } + attachmentList, err := computeService.ListVolumeAttachments(vmCID.AsString()) + if err != nil { + return fmt.Errorf("detach_disk: Failed to get volume attachments for VM ID %s: %w", vmCID.AsString(), err) + } + attachmentFound := false + for idx, attachment := range attachmentList { + if idx == 0 { + a.logger.Debug("detach_disk", fmt.Sprintf("Attachments for VM ID %s", vmCID.AsString())) + } + if attachment.VolumeID == diskCID.AsString() { + attachmentFound = true + } + a.logger.Debug("detach_disk", fmt.Sprintf("%d: Existing attachment: device: %s, volume ID: %s", idx+1, attachment.Device, attachment.VolumeID)) + } + if attachmentFound { + a.logger.Debug("detach_disk", fmt.Sprintf("Detaching volume ID: %s, server: %s", diskCID.AsString(), vmCID.AsString())) + err = computeService.DetachVolume(vmCID.AsString(), diskCID.AsString()) + if err != nil { + return fmt.Errorf("detach_disk: Failed to detach volume %s from VM %s: %w", diskCID.AsString(), vmCID.AsString(), err) + } + a.logger.Debug("detach_disk", fmt.Sprintf("Detaching volume DONE: Volume ID: %s, VM ID: %s", diskCID.AsString(), vmCID.AsString())) + a.logger.Debug("detach_disk", fmt.Sprintf("Waiting for volume ID %s to become available (time: %d secs)", diskCID.AsString(), openstackConfig.StateTimeOut)) + volumeService, err := a.volumeServiceBuilder.Build() + if err != nil { + return fmt.Errorf("detach_disk: Failed to get volume service (detach_disk): %w", err) + } + err = volumeService.WaitForVolumeToBecomeStatus(diskCID.AsString(), time.Duration(openstackConfig.StateTimeOut)*time.Second, "available") + if err != nil { + return fmt.Errorf("detach_disk: Timeout on waiting for volume ID %s become available (waiting: %d sec): %w", + diskCID.AsString(), openstackConfig.StateTimeOut, err) + } + a.logger.Info("detach_disk", fmt.Sprintf("Successfully detached volume ID %s from VM %s (Volume status now: 'available')", diskCID.AsString(), vmCID.AsString())) + } else { + a.logger.Info("detach_disk", fmt.Sprintf("Volume ID %s is not attached to VM ID %s", diskCID.AsString(), vmCID.AsString())) + } return nil } diff --git a/src/openstack_cpi_golang/cpi/methods/detach_disk_test.go b/src/openstack_cpi_golang/cpi/methods/detach_disk_test.go new file mode 100644 index 00000000..277fbe44 --- /dev/null +++ b/src/openstack_cpi_golang/cpi/methods/detach_disk_test.go @@ -0,0 +1,177 @@ +package methods_test + +import ( + "errors" + "fmt" + + "github.com/cloudfoundry/bosh-cpi-go/apiv1" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/compute/computefakes" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/config" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/methods" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils/utilsfakes" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/volume/volumefakes" + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/volumeattach" + "github.com/gophercloud/gophercloud/openstack/compute/v2/servers" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("DetachDiskMethod Unit Tests", func() { + + const ( + volumeId1 = "vol1-id" + volumeId2 = "vol2-id" + volumeId3 = "vol3-id" + deviceA = "/dev/sda" + deviceB = "/dev/sdb" + serverId = "VM-id" + serverStatusActive = "ACTIVE" + ) + + var ( + computeServiceBuilder *computefakes.FakeComputeServiceBuilder + computeService *computefakes.FakeComputeService + volumeServiceBuilder *volumefakes.FakeVolumeServiceBuilder + volumeService *volumefakes.FakeVolumeService + logger *utilsfakes.FakeLogger + cpiConfig config.CpiConfig + ) + + Context("detaching disk from VM", func() { + BeforeEach(func() { + computeServiceBuilder = new(computefakes.FakeComputeServiceBuilder) + computeService = new(computefakes.FakeComputeService) + volumeServiceBuilder = new(volumefakes.FakeVolumeServiceBuilder) + volumeService = new(volumefakes.FakeVolumeService) + logger = new(utilsfakes.FakeLogger) + computeServiceBuilder.BuildReturns(computeService, nil) + cpiConfig = config.CpiConfig{} + }) + + It("fails on compute service builder", func() { + computeServiceBuilder.BuildReturns(nil, errors.New("boom")) + detachDiskMethod := methods.NewDetachDiskMethod(computeServiceBuilder, volumeServiceBuilder, cpiConfig, logger) + vmCID := apiv1.VMCID{} + diskCID := apiv1.DiskCID{} + err := detachDiskMethod.DetachDisk(vmCID, diskCID) + Expect(err.Error()).To(Equal("detach_disk: Failed to get compute service: boom")) + }) + + It("fails on get VM", func() { + server := servers.Server{} + computeService.GetServerReturns(&server, errors.New("boom")) + detachDiskMethod := methods.NewDetachDiskMethod(computeServiceBuilder, volumeServiceBuilder, cpiConfig, logger) + vmCID := apiv1.NewVMCID(serverId) + diskCID := apiv1.DiskCID{} + err := detachDiskMethod.DetachDisk(vmCID, diskCID) + Expect(err.Error()).To(Equal(fmt.Sprintf("detach_disk: Failed to get VM %s: boom", serverId))) + }) + + It("fails on list volume attachments", func() { + computeService = new(computefakes.FakeComputeService) + computeServiceBuilder.BuildReturns(computeService, nil) + server := servers.Server{ + ID: serverId, + Status: serverStatusActive, + } + computeService.GetServerReturns(&server, nil) + computeService.ListVolumeAttachmentsReturns(nil, errors.New("boom")) + detachDiskMethod := methods.NewDetachDiskMethod(computeServiceBuilder, volumeServiceBuilder, cpiConfig, logger) + vmCID := apiv1.NewVMCID(serverId) + diskCID := apiv1.NewDiskCID(volumeId1) + err := detachDiskMethod.DetachDisk(vmCID, diskCID) + Expect(err.Error()).To(Equal(fmt.Sprintf("detach_disk: Failed to get volume attachments for VM ID %s: boom", serverId))) + }) + + It("success: volume is not attached to VM", func() { + computeService = new(computefakes.FakeComputeService) + computeServiceBuilder.BuildReturns(computeService, nil) + server := servers.Server{ + ID: serverId, + Status: serverStatusActive, + } + computeService.GetServerReturns(&server, nil) + volume1 := volumeattach.VolumeAttachment{VolumeID: volumeId1, Device: deviceA} + volume2 := volumeattach.VolumeAttachment{VolumeID: volumeId2, Device: deviceB} + var volumeAttachments []volumeattach.VolumeAttachment + volumeAttachments = append(volumeAttachments, volume1, volume2) + computeService.ListVolumeAttachmentsReturns(volumeAttachments, nil) + detachDiskMethod := methods.NewDetachDiskMethod(computeServiceBuilder, volumeServiceBuilder, cpiConfig, logger) + vmCID := apiv1.NewVMCID(serverId) + diskCID := apiv1.NewDiskCID(volumeId3) + err := detachDiskMethod.DetachDisk(vmCID, diskCID) + Expect(err).NotTo(HaveOccurred()) + }) + + It("fails on detach volume", func() { + computeService = new(computefakes.FakeComputeService) + computeServiceBuilder.BuildReturns(computeService, nil) + server := servers.Server{ + ID: serverId, + Status: serverStatusActive, + } + computeService.GetServerReturns(&server, nil) + volume1 := volumeattach.VolumeAttachment{VolumeID: volumeId1, Device: deviceA} + volume2 := volumeattach.VolumeAttachment{VolumeID: volumeId2, Device: deviceB} + var volumeAttachments []volumeattach.VolumeAttachment + volumeAttachments = append(volumeAttachments, volume1, volume2) + computeService.ListVolumeAttachmentsReturns(volumeAttachments, nil) + computeService.DetachVolumeReturns(errors.New("boom")) + detachDiskMethod := methods.NewDetachDiskMethod(computeServiceBuilder, volumeServiceBuilder, cpiConfig, logger) + vmCID := apiv1.NewVMCID(serverId) + diskCID := apiv1.NewDiskCID(volumeId2) + err := detachDiskMethod.DetachDisk(vmCID, diskCID) + Expect(err.Error()).To(Equal(fmt.Sprintf("detach_disk: Failed to detach volume %s from VM %s: boom", diskCID.AsString(), vmCID.AsString()))) + }) + + It("fails on waiting after detached volume", func() { + computeService = new(computefakes.FakeComputeService) + computeServiceBuilder.BuildReturns(computeService, nil) + server := servers.Server{ + ID: serverId, + Status: serverStatusActive, + } + computeService.GetServerReturns(&server, nil) + volume1 := volumeattach.VolumeAttachment{VolumeID: volumeId1, Device: deviceA} + volume2 := volumeattach.VolumeAttachment{VolumeID: volumeId2, Device: deviceB} + var volumeAttachments []volumeattach.VolumeAttachment + volumeAttachments = append(volumeAttachments, volume1, volume2) + computeService.ListVolumeAttachmentsReturns(volumeAttachments, nil) + computeService.DetachVolumeReturns(nil) + volumeServiceBuilder.BuildReturns(volumeService, nil) + cpiConfig.Cloud.Properties.Openstack.StateTimeOut = 10 + volumeService.WaitForVolumeToBecomeStatusReturns(errors.New("boom")) + detachDiskMethod := methods.NewDetachDiskMethod(computeServiceBuilder, volumeServiceBuilder, cpiConfig, logger) + vmCID := apiv1.NewVMCID(serverId) + diskCID := apiv1.NewDiskCID(volumeId2) + err := detachDiskMethod.DetachDisk(vmCID, diskCID) + Expect(err.Error()).To(Equal(fmt.Sprintf("detach_disk: Timeout on waiting for volume ID %s become available (waiting: %d sec): boom", diskCID.AsString(), cpiConfig.Cloud.Properties.Openstack.StateTimeOut))) + }) + + It("success on detach volume", func() { + computeService = new(computefakes.FakeComputeService) + computeServiceBuilder.BuildReturns(computeService, nil) + server := servers.Server{ + ID: serverId, + Status: serverStatusActive, + } + computeService.GetServerReturns(&server, nil) + volume1 := volumeattach.VolumeAttachment{VolumeID: volumeId1, Device: deviceA} + volume2 := volumeattach.VolumeAttachment{VolumeID: volumeId2, Device: deviceB} + var volumeAttachments []volumeattach.VolumeAttachment + volumeAttachments = append(volumeAttachments, volume1, volume2) + computeService.ListVolumeAttachmentsReturns(volumeAttachments, nil) + computeService.DetachVolumeReturns(nil) + volumeServiceBuilder.BuildReturns(volumeService, nil) + cpiConfig.Cloud.Properties.Openstack.StateTimeOut = 10 + volumeService.WaitForVolumeToBecomeStatusReturns(nil) + detachDiskMethod := methods.NewDetachDiskMethod(computeServiceBuilder, volumeServiceBuilder, cpiConfig, logger) + vmCID := apiv1.NewVMCID(serverId) + diskCID := apiv1.NewDiskCID(volumeId2) + err := detachDiskMethod.DetachDisk(vmCID, diskCID) + Expect(err).NotTo(HaveOccurred()) + }) + + }) + +}) diff --git a/src/openstack_cpi_golang/cpi/methods/get_disks.go b/src/openstack_cpi_golang/cpi/methods/get_disks.go index 7c39e61c..c57543a8 100644 --- a/src/openstack_cpi_golang/cpi/methods/get_disks.go +++ b/src/openstack_cpi_golang/cpi/methods/get_disks.go @@ -1,16 +1,58 @@ package methods import ( + "fmt" + "github.com/cloudfoundry/bosh-cpi-go/apiv1" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/compute" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils" ) -type GetDisksMethod struct{} +type GetDisksMethod struct { + computeServiceBuilder compute.ComputeServiceBuilder + logger utils.Logger +} -func NewGetDisksMethod() GetDisksMethod { - return GetDisksMethod{} +func NewGetDisksMethod( + computeServiceBuilder compute.ComputeServiceBuilder, + logger utils.Logger, +) GetDisksMethod { + return GetDisksMethod{ + computeServiceBuilder: computeServiceBuilder, + logger: logger, + } } func (a GetDisksMethod) GetDisks(cid apiv1.VMCID) ([]apiv1.DiskCID, error) { - // todo implement - return nil, nil + + a.logger.Info("get_disks", fmt.Sprintf("Execute get disks ID for VM ID %s", cid.AsString())) + + computeService, err := a.computeServiceBuilder.Build() + if err != nil { + return nil, fmt.Errorf("get_disks: Failed to get compute service: %w", err) + } + + _, err = computeService.GetServer(cid.AsString()) + if err != nil { + return nil, fmt.Errorf("get_disks: Failed to get VM %s: %w", cid.AsString(), err) + } + + attachmentList, err := computeService.ListVolumeAttachments(cid.AsString()) + if err != nil { + return nil, fmt.Errorf("get_disks: Failed to get volume attachments for VM ID %s: %w", cid.AsString(), err) + } + if len(attachmentList) == 0 { + a.logger.Debug("get_disks", fmt.Sprintf("No disks attached to VM %s", cid.AsString())) + return []apiv1.DiskCID{}, nil + } + + disks := make([]apiv1.DiskCID, len(attachmentList)) + for idx, attachment := range attachmentList { + disks[idx] = apiv1.NewDiskCID(attachment.VolumeID) + } + a.logger.Info("get_disks", fmt.Sprintf("Found %d disk(s) attached to VM %s", len(disks), cid.AsString())) + for idx, disk := range disks { + a.logger.Debug("get_disks", fmt.Sprintf("%d: Disk ID: %s", idx, disk.AsString())) + } + return disks, nil } diff --git a/src/openstack_cpi_golang/cpi/methods/get_disks_test.go b/src/openstack_cpi_golang/cpi/methods/get_disks_test.go new file mode 100644 index 00000000..817c20f6 --- /dev/null +++ b/src/openstack_cpi_golang/cpi/methods/get_disks_test.go @@ -0,0 +1,113 @@ +package methods_test + +import ( + "errors" + "fmt" + + "github.com/cloudfoundry/bosh-cpi-go/apiv1" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/compute/computefakes" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/methods" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils/utilsfakes" + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/volumeattach" + "github.com/gophercloud/gophercloud/openstack/compute/v2/servers" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("GetDisksMethod Unit Tests", func() { + + const ( + volumeId1 = "vol1-id" + volumeId2 = "vol2-id" + deviceA = "/dev/sda" + deviceB = "/dev/sdb" + serverId = "VM-id" + serverStatusActive = "ACTIVE" + ) + + var ( + computeServiceBuilder *computefakes.FakeComputeServiceBuilder + computeService *computefakes.FakeComputeService + logger *utilsfakes.FakeLogger + ) + + Context("get disks attached to a VM", func() { + BeforeEach(func() { + computeServiceBuilder = new(computefakes.FakeComputeServiceBuilder) + computeService = new(computefakes.FakeComputeService) + logger = new(utilsfakes.FakeLogger) + computeServiceBuilder.BuildReturns(computeService, nil) + }) + + It("fails on compute service builder", func() { + computeServiceBuilder.BuildReturns(nil, errors.New("boom")) + getDisksMethod := methods.NewGetDisksMethod(computeServiceBuilder, logger) + vmCID := apiv1.VMCID{} + _, err := getDisksMethod.GetDisks(vmCID) + Expect(err.Error()).To(Equal("get_disks: Failed to get compute service: boom")) + }) + + It("fails on get VM", func() { + server := servers.Server{} + computeService.GetServerReturns(&server, errors.New("boom")) + getDisksMethod := methods.NewGetDisksMethod(computeServiceBuilder, logger) + vmCID := apiv1.NewVMCID(serverId) + _, err := getDisksMethod.GetDisks(vmCID) + Expect(err.Error()).To(Equal(fmt.Sprintf("get_disks: Failed to get VM %s: boom", serverId))) + }) + + It("fails on list volume attachments", func() { + computeService = new(computefakes.FakeComputeService) + computeServiceBuilder.BuildReturns(computeService, nil) + server := servers.Server{ + ID: serverId, + Status: serverStatusActive, + } + computeService.GetServerReturns(&server, nil) + computeService.ListVolumeAttachmentsReturns(nil, errors.New("boom")) + getDisksMethod := methods.NewGetDisksMethod(computeServiceBuilder, logger) + vmCID := apiv1.NewVMCID(serverId) + _, err := getDisksMethod.GetDisks(vmCID) + Expect(err.Error()).To(Equal(fmt.Sprintf("get_disks: Failed to get volume attachments for VM ID %s: boom", serverId))) + }) + + It("does not fail if volume attachments returns an empty list", func() { + computeService = new(computefakes.FakeComputeService) + computeServiceBuilder.BuildReturns(computeService, nil) + server := servers.Server{ + ID: serverId, + Status: serverStatusActive, + } + computeService.GetServerReturns(&server, nil) + var volumeAttachments []volumeattach.VolumeAttachment + computeService.ListVolumeAttachmentsReturns(volumeAttachments, nil) + getDisksMethod := methods.NewGetDisksMethod(computeServiceBuilder, logger) + vmCID := apiv1.NewVMCID(serverId) + diskCIDs, err := getDisksMethod.GetDisks(vmCID) + Expect(err).NotTo(HaveOccurred()) + Expect(diskCIDs).To(Equal([]apiv1.DiskCID{})) + }) + + It("successfully gets the disks", func() { + computeService = new(computefakes.FakeComputeService) + computeServiceBuilder.BuildReturns(computeService, nil) + server := servers.Server{ + ID: serverId, + Status: serverStatusActive, + } + computeService.GetServerReturns(&server, nil) + volume1 := volumeattach.VolumeAttachment{VolumeID: volumeId1, Device: deviceA} + volume2 := volumeattach.VolumeAttachment{VolumeID: volumeId2, Device: deviceB} + var volumeAttachments []volumeattach.VolumeAttachment + volumeAttachments = append(volumeAttachments, volume1, volume2) + computeService.ListVolumeAttachmentsReturns(volumeAttachments, nil) + getDisksMethod := methods.NewGetDisksMethod(computeServiceBuilder, logger) + vmCID := apiv1.NewVMCID(serverId) + diskCIDs, err := getDisksMethod.GetDisks(vmCID) + Expect(err).NotTo(HaveOccurred()) + Expect(diskCIDs).To(Equal([]apiv1.DiskCID{apiv1.NewDiskCID("vol1-id"), apiv1.NewDiskCID("vol2-id")})) + }) + + }) + +}) diff --git a/src/openstack_cpi_golang/cpi/methods/has_disk.go b/src/openstack_cpi_golang/cpi/methods/has_disk.go index 2fdecd79..85289afa 100644 --- a/src/openstack_cpi_golang/cpi/methods/has_disk.go +++ b/src/openstack_cpi_golang/cpi/methods/has_disk.go @@ -1,15 +1,50 @@ package methods import ( + "errors" + "fmt" + "github.com/cloudfoundry/bosh-cpi-go/apiv1" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/volume" + "github.com/gophercloud/gophercloud" ) -type HasDiskMethod struct{} +type HasDiskMethod struct { + volumeServiceBuilder volume.VolumeServiceBuilder + logger utils.Logger +} -func NewHasDiskMethod() HasDiskMethod { - return HasDiskMethod{} +func NewHasDiskMethod( + volumeServiceBuilder volume.VolumeServiceBuilder, + logger utils.Logger, +) HasDiskMethod { + return HasDiskMethod{ + volumeServiceBuilder: volumeServiceBuilder, + logger: logger, + } } func (a HasDiskMethod) HasDisk(cid apiv1.DiskCID) (bool, error) { - return false, nil + var errDefault404 gophercloud.ErrDefault404 + + a.logger.Info("has_disk", "Check the presence of disk with id %s", cid.AsString()) + + volumeService, err := a.volumeServiceBuilder.Build() + if err != nil { + return false, fmt.Errorf("has_disk: %w", err) + } + volume, err := volumeService.GetVolume(cid.AsString()) + if err != nil { + if errors.As(err, &errDefault404) { + return false, nil + } + return false, fmt.Errorf("has_disk: %w", err) + } + + if volume.ID == "" { + return false, nil + } else { + return true, nil + } } diff --git a/src/openstack_cpi_golang/cpi/methods/has_disk_test.go b/src/openstack_cpi_golang/cpi/methods/has_disk_test.go new file mode 100644 index 00000000..c1d74b68 --- /dev/null +++ b/src/openstack_cpi_golang/cpi/methods/has_disk_test.go @@ -0,0 +1,116 @@ +package methods_test + +import ( + "errors" + + "github.com/cloudfoundry/bosh-cpi-go/apiv1" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/methods" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils/utilsfakes" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/volume/volumefakes" + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/blockstorage/v3/volumes" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("HasDiskMethod", func() { + + var volumeServicebuilder volumefakes.FakeVolumeServiceBuilder + var volumeService volumefakes.FakeVolumeService + var logger utilsfakes.FakeLogger + + Context("HasDisk", func() { + BeforeEach(func() { + volumeServicebuilder = volumefakes.FakeVolumeServiceBuilder{} + logger = utilsfakes.FakeLogger{} + + volumeServicebuilder.BuildReturns(&volumeService, nil) + volumeService.GetVolumeReturns(&volumes.Volume{ID: "123-456"}, nil) + + }) + + It("creates the volume service", func() { + _, _ = methods.NewHasDiskMethod( + &volumeServicebuilder, + &logger, + ).HasDisk( + apiv1.NewDiskCID("disk-id"), + ) + + Expect(volumeServicebuilder.BuildCallCount()).To(Equal(1)) + }) + + It("returns an error and false if the volume service cannot be retrieved", func() { + volumeServicebuilder.BuildReturns(nil, errors.New("boom")) + + exists, err := methods.NewHasDiskMethod( + &volumeServicebuilder, + &logger, + ).HasDisk( + apiv1.NewDiskCID("disk-id"), + ) + + Expect(exists).To(Equal(false)) + Expect(err.Error()).To(Equal("has_disk: boom")) + }) + + It("returns false and no error if GetVolume fails with notFound", func() { + testError := gophercloud.ErrDefault404{ + ErrUnexpectedResponseCode: gophercloud.ErrUnexpectedResponseCode{Actual: 404}, + } + volumeService.GetVolumeReturns(nil, testError) + + exists, err := methods.NewHasDiskMethod( + &volumeServicebuilder, + &logger, + ).HasDisk( + apiv1.NewDiskCID("disk-id"), + ) + + Expect(exists).To(Equal(false)) + Expect(err).ToNot(HaveOccurred()) + }) + + It("returns false and error if GetVolume fails", func() { + volumeService.GetVolumeReturns(nil, errors.New("boom")) + + exists, err := methods.NewHasDiskMethod( + &volumeServicebuilder, + &logger, + ).HasDisk( + apiv1.NewDiskCID("disk-id"), + ) + + Expect(exists).To(Equal(false)) + Expect(err).To(HaveOccurred()) + }) + + It("returns false if no error and volume id is nil", func() { + volumeService.GetVolumeReturns(&volumes.Volume{ID: ""}, nil) + + exists, err := methods.NewHasDiskMethod( + &volumeServicebuilder, + &logger, + ).HasDisk( + apiv1.NewDiskCID("disk-id"), + ) + + Expect(exists).To(Equal(false)) + Expect(err).ToNot(HaveOccurred()) + }) + + It("returns true if no error and volume id is not nil", func() { + volumeService.GetVolumeReturns(&volumes.Volume{ID: "123-456"}, nil) + + exists, err := methods.NewHasDiskMethod( + &volumeServicebuilder, + &logger, + ).HasDisk( + apiv1.NewDiskCID("disk-id"), + ) + + Expect(exists).To(Equal(true)) + Expect(err).ToNot(HaveOccurred()) + }) + }) +}) diff --git a/src/openstack_cpi_golang/cpi/methods/has_vm.go b/src/openstack_cpi_golang/cpi/methods/has_vm.go index 22c34a94..6df65884 100644 --- a/src/openstack_cpi_golang/cpi/methods/has_vm.go +++ b/src/openstack_cpi_golang/cpi/methods/has_vm.go @@ -1,15 +1,52 @@ package methods import ( + "errors" + "fmt" + "github.com/cloudfoundry/bosh-cpi-go/apiv1" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/compute" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils" + "github.com/gophercloud/gophercloud" ) -type HasVMMethod struct{} +type HasVMMethod struct { + computeServiceBuilder compute.ComputeServiceBuilder + logger utils.Logger +} -func NewHasVMMethod() HasVMMethod { - return HasVMMethod{} +func NewHasVMMethod( + computeServiceBuilder compute.ComputeServiceBuilder, + logger utils.Logger, +) HasVMMethod { + return HasVMMethod{ + computeServiceBuilder: computeServiceBuilder, + logger: logger, + } } func (a HasVMMethod) HasVM(vmCID apiv1.VMCID) (bool, error) { - return false, nil + var errDefault404 gophercloud.ErrDefault404 + + computeService, err := a.computeServiceBuilder.Build() + if err != nil { + return false, fmt.Errorf("has_vm: %w", err) + } + + server, err := computeService.GetServer(vmCID.AsString()) + if err != nil { + if errors.As(err, &errDefault404) { + return false, nil + } + return false, fmt.Errorf("has_vm: %w", err) + } + + switch server.Status { + case "DELETED": + return false, nil + case "TERMINATED": + return false, nil + default: + return true, nil + } } diff --git a/src/openstack_cpi_golang/cpi/methods/has_vm_test.go b/src/openstack_cpi_golang/cpi/methods/has_vm_test.go new file mode 100644 index 00000000..a8beddaa --- /dev/null +++ b/src/openstack_cpi_golang/cpi/methods/has_vm_test.go @@ -0,0 +1,130 @@ +package methods_test + +import ( + "errors" + + "github.com/cloudfoundry/bosh-cpi-go/apiv1" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/compute/computefakes" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/methods" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils/utilsfakes" + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/compute/v2/servers" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("HasVMMethod", func() { + + var computeServiceBuilder computefakes.FakeComputeServiceBuilder + var computeService computefakes.FakeComputeService + var logger utilsfakes.FakeLogger + + Context("HasVM", func() { + BeforeEach(func() { + computeServiceBuilder = computefakes.FakeComputeServiceBuilder{} + logger = utilsfakes.FakeLogger{} + + computeServiceBuilder.BuildReturns(&computeService, nil) + computeService.GetServerReturns(&servers.Server{ID: "123-456"}, nil) + + }) + + It("creates the compute service", func() { + _, _ = methods.NewHasVMMethod( + &computeServiceBuilder, + &logger, + ).HasVM( + apiv1.NewVMCID("vm-id"), + ) + + Expect(computeServiceBuilder.BuildCallCount()).To(Equal(1)) + }) + + It("returns an error and false if the compute service cannot be retrieved", func() { + computeServiceBuilder.BuildReturns(nil, errors.New("boom")) + + exists, err := methods.NewHasVMMethod( + &computeServiceBuilder, + &logger, + ).HasVM( + apiv1.NewVMCID("vm-id"), + ) + + Expect(exists).To(Equal(false)) + Expect(err.Error()).To(Equal("has_vm: boom")) + }) + + It("returns false and no error if GetServer fails with notFound", func() { + testError := gophercloud.ErrDefault404{ + ErrUnexpectedResponseCode: gophercloud.ErrUnexpectedResponseCode{Actual: 404}, + } + computeService.GetServerReturns(nil, testError) + + exists, err := methods.NewHasVMMethod( + &computeServiceBuilder, + &logger, + ).HasVM( + apiv1.NewVMCID("vm-id"), + ) + + Expect(exists).To(Equal(false)) + Expect(err).ToNot(HaveOccurred()) + }) + + It("returns false and error if GetServer fails", func() { + computeService.GetServerReturns(nil, errors.New("boom")) + + exists, err := methods.NewHasVMMethod( + &computeServiceBuilder, + &logger, + ).HasVM( + apiv1.NewVMCID("vm-id"), + ) + + Expect(exists).To(Equal(false)) + Expect(err).To(HaveOccurred()) + }) + + It("returns false if no error and server.status != terminated", func() { + computeService.GetServerReturns(&servers.Server{ID: "123-456", Status: "TERMINATED"}, nil) + + exists, err := methods.NewHasVMMethod( + &computeServiceBuilder, + &logger, + ).HasVM( + apiv1.NewVMCID("vm-id"), + ) + + Expect(exists).To(Equal(false)) + Expect(err).ToNot(HaveOccurred()) + }) + + It("returns false if no error and server.status != deleted", func() { + computeService.GetServerReturns(&servers.Server{ID: "123-456", Status: "DELETED"}, nil) + + exists, err := methods.NewHasVMMethod( + &computeServiceBuilder, + &logger, + ).HasVM( + apiv1.NewVMCID("vm-id"), + ) + + Expect(exists).To(Equal(false)) + Expect(err).ToNot(HaveOccurred()) + }) + + It("returns true if no error and server.status != terminated or deleted", func() { + computeService.GetServerReturns(&servers.Server{ID: "123-456", Status: "ACTIVE"}, nil) + + exists, err := methods.NewHasVMMethod( + &computeServiceBuilder, + &logger, + ).HasVM( + apiv1.NewVMCID("vm-id"), + ) + + Expect(exists).To(Equal(true)) + Expect(err).ToNot(HaveOccurred()) + }) + }) +}) diff --git a/src/openstack_cpi_golang/cpi/methods/reboot_vm.go b/src/openstack_cpi_golang/cpi/methods/reboot_vm.go index 077a1504..e7105cb9 100644 --- a/src/openstack_cpi_golang/cpi/methods/reboot_vm.go +++ b/src/openstack_cpi_golang/cpi/methods/reboot_vm.go @@ -1,15 +1,43 @@ package methods import ( + "fmt" + "github.com/cloudfoundry/bosh-cpi-go/apiv1" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/compute" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/config" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils" ) -type RebootVMMethod struct{} +type RebootVMMethod struct { + computeServiceBuilder compute.ComputeServiceBuilder + cpiConfig config.CpiConfig + logger utils.Logger +} -func NewRebootVMMethod() RebootVMMethod { - return RebootVMMethod{} +func NewRebootVMMethod( + computeServiceBuilder compute.ComputeServiceBuilder, + cpiConfig config.CpiConfig, + logger utils.Logger, +) RebootVMMethod { + return RebootVMMethod{ + computeServiceBuilder: computeServiceBuilder, + cpiConfig: cpiConfig, + logger: logger, + } } func (a RebootVMMethod) RebootVM(vmCID apiv1.VMCID) error { + computeService, err := a.computeServiceBuilder.Build() + if err != nil { + return fmt.Errorf("reboot_vm: %w", err) + } + + err = computeService.RebootServer(vmCID.AsString(), a.cpiConfig) + if err != nil { + return fmt.Errorf("reboot_vm: %w", err) + } + + a.logger.Info("reboot_vm_method", fmt.Sprintf("Rebooted server with ID: '%s'", vmCID.AsString())) return nil } diff --git a/src/openstack_cpi_golang/cpi/methods/resize_disk.go b/src/openstack_cpi_golang/cpi/methods/resize_disk.go index fdb41ad7..5d9adc1e 100644 --- a/src/openstack_cpi_golang/cpi/methods/resize_disk.go +++ b/src/openstack_cpi_golang/cpi/methods/resize_disk.go @@ -1,15 +1,75 @@ package methods import ( + "fmt" + "math" + "time" + "github.com/cloudfoundry/bosh-cpi-go/apiv1" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/config" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/volume" ) -type ResizeDiskMethod struct{} +type ResizeDiskMethod struct { + volumeServiceBuilder volume.VolumeServiceBuilder + cpiConfig config.CpiConfig + logger utils.Logger +} -func NewResizeDiskMethod() ResizeDiskMethod { - return ResizeDiskMethod{} +func NewResizeDiskMethod( + volumeServiceBuilder volume.VolumeServiceBuilder, + cpiConfig config.CpiConfig, + logger utils.Logger, +) ResizeDiskMethod { + return ResizeDiskMethod{ + volumeServiceBuilder: volumeServiceBuilder, + cpiConfig: cpiConfig, + logger: logger, + } } func (r ResizeDiskMethod) ResizeDisk(cid apiv1.DiskCID, size int) error { + sizeInGib := mibToGib(size) + + r.logger.Info("resize_disk", fmt.Sprintf("Resizing volume %s to %d GiB (%v MiB)", cid.AsString(), sizeInGib, size)) + + volumeService, err := r.volumeServiceBuilder.Build() + if err != nil { + return fmt.Errorf("failed to create volume service: %w", err) + } + + volume, err := volumeService.GetVolume(cid.AsString()) + if err != nil { + return fmt.Errorf("cannot resize volume because volume with id %s not found, error: %w", cid.AsString(), err) + } + + switch { + case volume.Size == sizeInGib: + r.logger.Info("resize_disk", fmt.Sprintf("Skipping resize of disk %s because current value %d GiB is equal new value %d GiB", cid.AsString(), volume.Size, sizeInGib)) + return nil + case volume.Size > sizeInGib: + return fmt.Errorf("cannot resize volume to a smaller size from %d GiB to %d GiB", volume.Size, sizeInGib) + case len(volume.Attachments) > 0: + return fmt.Errorf("cannot resize volume %s due to attachments", cid.AsString()) + } + + err = volumeService.ExtendVolumeSize(cid.AsString(), sizeInGib) + if err != nil { + return fmt.Errorf("failed to resize volume %s: %w", cid.AsString(), err) + } + + r.logger.Info("resize_disk", fmt.Sprintf("Resizing volume %s to %d GiB ...", cid.AsString(), sizeInGib)) + err = volumeService.WaitForVolumeToBecomeStatus(volume.ID, time.Duration(r.cpiConfig.OpenStackConfig().StateTimeOut)*time.Second, "available") + if err != nil { + return fmt.Errorf("failed while waiting on resizing volume %s: %w", cid.AsString(), err) + } + + r.logger.Info("resize_disk", fmt.Sprintf("Resized volume %s to %d GiB", cid.AsString(), sizeInGib)) + return nil } + +func mibToGib(size int) int { + return int(math.Ceil(float64(size) / 1024.0)) +} diff --git a/src/openstack_cpi_golang/cpi/methods/resize_disk_test.go b/src/openstack_cpi_golang/cpi/methods/resize_disk_test.go new file mode 100644 index 00000000..69ce8ef8 --- /dev/null +++ b/src/openstack_cpi_golang/cpi/methods/resize_disk_test.go @@ -0,0 +1,217 @@ +package methods_test + +import ( + "errors" + + "github.com/cloudfoundry/bosh-cpi-go/apiv1" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/config" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/methods" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils/utilsfakes" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/volume/volumefakes" + "github.com/gophercloud/gophercloud/openstack/blockstorage/v3/volumes" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("NewResizeDiskMethod", func() { + + var volumeService volumefakes.FakeVolumeService + var volumeServiceBuilder volumefakes.FakeVolumeServiceBuilder + var logger utilsfakes.FakeLogger + var cpiConfig config.CpiConfig + + Context("ResizeDisk", func() { + newSizeSmaller := 4000 + newSizeEqual := 5000 + newSizeLarger := 6000 + diskCID := apiv1.NewDiskCID("test-disk-cid") + attachmentNotNull := []volumes.Attachment{{AttachmentID: "test-attachment-id"}} + volumeWithAttachment := &volumes.Volume{ID: "test-disk-cid", Size: 5, Attachments: attachmentNotNull} + volumeWithoutAttachment := &volumes.Volume{ID: "test-disk-cid", Size: 5} + + BeforeEach(func() { + volumeServiceBuilder = volumefakes.FakeVolumeServiceBuilder{} + volumeService = volumefakes.FakeVolumeService{} + logger = utilsfakes.FakeLogger{} + cpiConfig = config.CpiConfig{} + + volumeServiceBuilder.BuildReturns(&volumeService, nil) + volumeService.GetVolumeReturns(volumeWithoutAttachment, nil) + volumeService.ExtendVolumeSizeReturns(nil) + volumeService.WaitForVolumeToBecomeStatusReturns(nil) + }) + + It("creates the volume service", func() { + err := methods.NewResizeDiskMethod( + &volumeServiceBuilder, + cpiConfig, + &logger, + ).ResizeDisk( + diskCID, + newSizeLarger, + ) + + Expect(err).ToNot(HaveOccurred()) + Expect(volumeServiceBuilder.BuildCallCount()).To(Equal(1)) + }) + + It("returns an error if the volume service cannot be retrieved", func() { + volumeServiceBuilder.BuildReturns(nil, errors.New("boom")) + + err := methods.NewResizeDiskMethod( + &volumeServiceBuilder, + cpiConfig, + &logger, + ).ResizeDisk( + diskCID, + newSizeLarger, + ) + + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal("failed to create volume service: boom")) + }) + + It("gets a volume back", func() { + err := methods.NewResizeDiskMethod( + &volumeServiceBuilder, + cpiConfig, + &logger, + ).ResizeDisk( + diskCID, + newSizeLarger, + ) + + Expect(err).ToNot(HaveOccurred()) + Expect(volumeService.GetVolumeCallCount()).To(Equal(1)) + }) + + It("returns an error if the volume cannot be retrieved", func() { + volumeService.GetVolumeReturns(nil, errors.New("boom")) + + err := methods.NewResizeDiskMethod( + &volumeServiceBuilder, + cpiConfig, + &logger, + ).ResizeDisk( + diskCID, + newSizeLarger, + ) + + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal("cannot resize volume because volume with id test-disk-cid not found, error: boom")) + }) + + It("returns nil because new and current volumesize are the same", func() { + err := methods.NewResizeDiskMethod( + &volumeServiceBuilder, + cpiConfig, + &logger, + ).ResizeDisk( + diskCID, + newSizeEqual, + ) + + Expect(err).ToNot(HaveOccurred()) + Expect(volumeService.ExtendVolumeSizeCallCount()).To(Equal(0)) + arg1, arg2, _ := logger.InfoArgsForCall(1) + Expect(arg1).To(Equal("resize_disk")) + Expect(arg2).To(Equal("Skipping resize of disk test-disk-cid because current value 5 GiB is equal new value 5 GiB")) + Expect(volumeService.GetVolumeCallCount()).To(Equal(1)) + }) + + It("returns error because new volumesize is smaller than the current one", func() { + err := methods.NewResizeDiskMethod( + &volumeServiceBuilder, + cpiConfig, + &logger, + ).ResizeDisk( + diskCID, + newSizeSmaller, + ) + + Expect(err).To(HaveOccurred()) + Expect(volumeService.ExtendVolumeSizeCallCount()).To(Equal(0)) + Expect(err.Error()).To(Equal("cannot resize volume to a smaller size from 5 GiB to 4 GiB")) + }) + + It("returns error because volume.Attachment is not nil", func() { + volumeService.GetVolumeReturns(volumeWithAttachment, nil) + err := methods.NewResizeDiskMethod( + &volumeServiceBuilder, + cpiConfig, + &logger, + ).ResizeDisk( + diskCID, + newSizeLarger, + ) + + Expect(err).To(HaveOccurred()) + Expect(volumeService.ExtendVolumeSizeCallCount()).To(Equal(0)) + Expect(err.Error()).To(Equal("cannot resize volume test-disk-cid due to attachments")) + }) + + It("extends the volume size successfully", func() { + err := methods.NewResizeDiskMethod( + &volumeServiceBuilder, + cpiConfig, + &logger, + ).ResizeDisk( + diskCID, + newSizeLarger, + ) + + Expect(err).ToNot(HaveOccurred()) + Expect(volumeService.ExtendVolumeSizeCallCount()).To(Equal(1)) + arg1, arg2, _ := logger.InfoArgsForCall(2) + Expect(arg1).To(Equal("resize_disk")) + Expect(arg2).To(Equal("Resized volume test-disk-cid to 6 GiB")) + }) + + It("returns error because volume could not be extended", func() { + volumeService.ExtendVolumeSizeReturns(errors.New("boom")) + err := methods.NewResizeDiskMethod( + &volumeServiceBuilder, + cpiConfig, + &logger, + ).ResizeDisk( + diskCID, + newSizeLarger, + ) + + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal("failed to resize volume test-disk-cid: boom")) + }) + + It("waits for volume to become available successfully", func() { + err := methods.NewResizeDiskMethod( + &volumeServiceBuilder, + cpiConfig, + &logger, + ).ResizeDisk( + diskCID, + newSizeLarger, + ) + + Expect(err).ToNot(HaveOccurred()) + Expect(volumeService.WaitForVolumeToBecomeStatusCallCount()).To(Equal(1)) + arg1, arg2, _ := logger.InfoArgsForCall(2) + Expect(arg1).To(Equal("resize_disk")) + Expect(arg2).To(Equal("Resized volume test-disk-cid to 6 GiB")) + }) + + It("returns error while waiting for volume to be extended", func() { + volumeService.WaitForVolumeToBecomeStatusReturns(errors.New("boom")) + err := methods.NewResizeDiskMethod( + &volumeServiceBuilder, + cpiConfig, + &logger, + ).ResizeDisk( + diskCID, + newSizeLarger, + ) + + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal("failed while waiting on resizing volume test-disk-cid: boom")) + }) + }) +}) diff --git a/src/openstack_cpi_golang/cpi/methods/set_disk_metadata.go b/src/openstack_cpi_golang/cpi/methods/set_disk_metadata.go index b578ba00..93c2f7a6 100644 --- a/src/openstack_cpi_golang/cpi/methods/set_disk_metadata.go +++ b/src/openstack_cpi_golang/cpi/methods/set_disk_metadata.go @@ -1,15 +1,58 @@ package methods import ( + "encoding/json" + "fmt" + "github.com/cloudfoundry/bosh-cpi-go/apiv1" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/compute" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/volume" ) -type SetDiskMetadataMethod struct{} +type SetDiskMetadataMethod struct { + computeServiceBuilder compute.ComputeServiceBuilder + volumeServiceBuilder volume.VolumeServiceBuilder + logger utils.Logger +} -func NewSetDiskMetadataMethod() SetDiskMetadataMethod { - return SetDiskMetadataMethod{} +func NewSetDiskMetadataMethod( + computeServiceBuilder compute.ComputeServiceBuilder, + volumeServiceBuilder volume.VolumeServiceBuilder, + logger utils.Logger, +) SetDiskMetadataMethod { + return SetDiskMetadataMethod{ + computeServiceBuilder: computeServiceBuilder, + volumeServiceBuilder: volumeServiceBuilder, + logger: logger, + } } -func (s SetDiskMetadataMethod) SetDiskMetadata(cid apiv1.DiskCID, meta apiv1.DiskMeta) error { +func (s SetDiskMetadataMethod) SetDiskMetadata(diskCID apiv1.DiskCID, metaData apiv1.DiskMeta) error { + s.logger.Info("set_disk_metadata", fmt.Sprintf("Execute set disk metadata disk ID %s", diskCID.AsString())) + volumeService, err := s.volumeServiceBuilder.Build() + if err != nil { + return fmt.Errorf("set_disk_metadata: Failed to get volume service: %w", err) + } + volumeInfo, err := volumeService.GetVolume(diskCID.AsString()) + if err != nil { + return fmt.Errorf("set_disk_metadata: Failed to get volume ID %s: %w", diskCID.AsString(), err) + } + s.logger.Info("set_disk_metadata", fmt.Sprintf("Successfully got volume ID %s. Current volume metadata: %v", diskCID.AsString(), volumeInfo.Metadata)) + var metaDataStringMap = make(map[string]string) + jsonData, err := json.Marshal(metaData) + if err != nil { + return fmt.Errorf("set_disk_metadata: Failed to marshal metadata for volume ID %s: %w", diskCID.AsString(), err) + } + err = json.Unmarshal(jsonData, &metaDataStringMap) + if err != nil { + return fmt.Errorf("set_disk_metadata: Failed to unmarshal metadata for volume ID %s: %w", diskCID.AsString(), err) + } + s.logger.Info("set_disk_metadata", fmt.Sprintf("Setting metadata for volume ID %s: %v", diskCID.AsString(), metaDataStringMap)) + err = volumeService.SetDiskMetadata(diskCID.AsString(), metaDataStringMap) + if err != nil { + return fmt.Errorf("set_disk_metadata: Failed to set metadata for volume ID %s: %w", diskCID.AsString(), err) + } + s.logger.Info("set_disk_metadata", fmt.Sprintf("Successfully set metadata for volume ID %s)", diskCID.AsString())) return nil } diff --git a/src/openstack_cpi_golang/cpi/methods/set_disk_metadata_test.go b/src/openstack_cpi_golang/cpi/methods/set_disk_metadata_test.go new file mode 100644 index 00000000..bbddd132 --- /dev/null +++ b/src/openstack_cpi_golang/cpi/methods/set_disk_metadata_test.go @@ -0,0 +1,142 @@ +package methods_test + +import ( + "errors" + + "github.com/cloudfoundry/bosh-cpi-go/apiv1" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/compute/computefakes" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/methods" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils/utilsfakes" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/volume/volumefakes" + "github.com/gophercloud/gophercloud/openstack/blockstorage/v3/volumes" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("SetDiskMetadataMethod Unit Tests", func() { + + const ( + volumeId1 = "vol1-id" + key1 = "key1" + key2 = "key2" + value1 = "value1" + value2 = "value2" + ) + + var ( + computeServiceBuilder *computefakes.FakeComputeServiceBuilder + volumeServiceBuilder *volumefakes.FakeVolumeServiceBuilder + volumeService *volumefakes.FakeVolumeService + logger *utilsfakes.FakeLogger + ) + + Context("setting disk metadata", func() { + BeforeEach(func() { + volumeServiceBuilder = new(volumefakes.FakeVolumeServiceBuilder) + logger = new(utilsfakes.FakeLogger) + }) + + It("fails on volume service builder", func() { + volumeServiceBuilder.BuildReturns(nil, errors.New("boom")) + setDiskMetadata := methods.NewSetDiskMetadataMethod(computeServiceBuilder, volumeServiceBuilder, logger) + diskCID := apiv1.NewDiskCID(volumeId1) + diskMeta := apiv1.DiskMeta{} + err := setDiskMetadata.SetDiskMetadata(diskCID, diskMeta) + Expect(err.Error()).To(Equal("set_disk_metadata: Failed to get volume service: boom")) + }) + + It("fails on get volume", func() { + volumeService = new(volumefakes.FakeVolumeService) + volumeServiceBuilder.BuildReturns(volumeService, nil) + volumeService.GetVolumeReturns(nil, errors.New("boom")) + setDiskMetadata := methods.NewSetDiskMetadataMethod(computeServiceBuilder, volumeServiceBuilder, logger) + diskCID := apiv1.NewDiskCID(volumeId1) + diskMeta := apiv1.DiskMeta{} + err := setDiskMetadata.SetDiskMetadata(diskCID, diskMeta) + Expect(err.Error()).To(Equal("set_disk_metadata: Failed to get volume ID vol1-id: boom")) + }) + + It("fails to marshal metadata", func() { + volumeService = new(volumefakes.FakeVolumeService) + volumeServiceBuilder.BuildReturns(volumeService, nil) + volume := volumes.Volume{ + ID: volumeId1, + } + volumeService.GetVolumeReturns(&volume, nil) + diskCID := apiv1.NewDiskCID(volumeId1) + invalid := map[string]interface{}{ + key1: func() {}, // cannot be marshaled + } + diskMeta := apiv1.NewDiskMeta(invalid) + setDiskMetadata := methods.NewSetDiskMetadataMethod(computeServiceBuilder, volumeServiceBuilder, logger) + err := setDiskMetadata.SetDiskMetadata(diskCID, diskMeta) + Expect(err).To(MatchError(ContainSubstring("set_disk_metadata: Failed to marshal metadata for volume ID vol1-id"))) + }) + + It("fails to unmarshal metadata", func() { + volumeService = new(volumefakes.FakeVolumeService) + volumeServiceBuilder.BuildReturns(volumeService, nil) + volume := volumes.Volume{ + ID: volumeId1, + } + volumeService.GetVolumeReturns(&volume, nil) + diskCID := apiv1.NewDiskCID(volumeId1) + invalid := map[string]interface{}{ + key1: value1, + key2: map[string]interface{}{ + "key": "value", + }, + } + diskMeta := apiv1.NewDiskMeta(invalid) + setDiskMetadata := methods.NewSetDiskMetadataMethod(computeServiceBuilder, volumeServiceBuilder, logger) + err := setDiskMetadata.SetDiskMetadata(diskCID, diskMeta) + Expect(err).To(MatchError(ContainSubstring("set_disk_metadata: Failed to unmarshal metadata for volume ID vol1-id"))) + }) + + It("success on metadata set", func() { + volumeService = new(volumefakes.FakeVolumeService) + volumeServiceBuilder.BuildReturns(volumeService, nil) + volume := volumes.Volume{ + ID: volumeId1, + Metadata: map[string]string{ + key1: value1, + key2: value2, + }, + } + volumeService.GetVolumeReturns(&volume, nil) + diskCID := apiv1.NewDiskCID(volumeId1) + valid := map[string]interface{}{ + key1: value1, + key2: value2, + } + diskMeta := apiv1.NewDiskMeta(valid) + setDiskMetadata := methods.NewSetDiskMetadataMethod(computeServiceBuilder, volumeServiceBuilder, logger) + err := setDiskMetadata.SetDiskMetadata(diskCID, diskMeta) + Expect(err).NotTo(HaveOccurred()) + }) + + It("fails on metadata set", func() { + volumeService = new(volumefakes.FakeVolumeService) + volumeServiceBuilder.BuildReturns(volumeService, nil) + volume := volumes.Volume{ + ID: volumeId1, + Metadata: map[string]string{ + key1: value1, + key2: value2, + }, + } + volumeService.GetVolumeReturns(&volume, nil) + diskCID := apiv1.NewDiskCID(volumeId1) + valid := map[string]interface{}{ + key1: value1, + key2: value2, + } + diskMeta := apiv1.NewDiskMeta(valid) + setDiskMetadata := methods.NewSetDiskMetadataMethod(computeServiceBuilder, volumeServiceBuilder, logger) + volumeService.SetDiskMetadataReturns(errors.New("boom")) + err := setDiskMetadata.SetDiskMetadata(diskCID, diskMeta) + Expect(err.Error()).To(Equal("set_disk_metadata: Failed to set metadata for volume ID vol1-id: boom")) + }) + }) + +}) diff --git a/src/openstack_cpi_golang/cpi/methods/set_vm_metadata.go b/src/openstack_cpi_golang/cpi/methods/set_vm_metadata.go index e5ce4c52..0383bf53 100644 --- a/src/openstack_cpi_golang/cpi/methods/set_vm_metadata.go +++ b/src/openstack_cpi_golang/cpi/methods/set_vm_metadata.go @@ -1,15 +1,120 @@ package methods import ( + "encoding/json" + "fmt" + "github.com/cloudfoundry/bosh-cpi-go/apiv1" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/compute" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/config" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils" ) -type SetVMMetadataMethod struct{} +type SetVMMetadataMethod struct { + computeServiceBuilder compute.ComputeServiceBuilder + logger utils.Logger + cpiConfig config.CpiConfig +} -func NewSetVMMetadataMethod() SetVMMetadataMethod { - return SetVMMetadataMethod{} +func NewSetVMMetadataMethod( + computeServiceBuilder compute.ComputeServiceBuilder, + logger utils.Logger, + cpiConfig config.CpiConfig, +) SetVMMetadataMethod { + return SetVMMetadataMethod{ + computeServiceBuilder: computeServiceBuilder, + logger: logger, + cpiConfig: cpiConfig, + } } func (s SetVMMetadataMethod) SetVMMetadata(vmCID apiv1.VMCID, meta apiv1.VMMeta) error { + + computeService, err := s.computeServiceBuilder.Build() + if err != nil { + return fmt.Errorf("failed to create compute service: %w", err) + } + + updateMetaDataMap, err := s.metadataToMap(meta) + if err != nil { + return err + } + + oldMetaDataMap, err := computeService.GetMetadata(vmCID.AsString()) + if err != nil { + return fmt.Errorf("failed to get Metadata: %w", err) + } + + err = computeService.DeleteServerMetaData(vmCID.AsString(), oldMetaDataMap, updateMetaDataMap) + if err != nil { + return fmt.Errorf("failed to delete Metadata: %w", err) + } + + err = computeService.UpdateServerMetadata(vmCID.AsString(), updateMetaDataMap) + if err != nil { + return fmt.Errorf("failed to update Metadata for key %s: %w", vmCID.AsString(), err) + } + + metaDataMap, err := computeService.GetMetadata(vmCID.AsString()) + if err != nil { + return fmt.Errorf("failed to get Metadata: %w", err) + } + + if (s.cpiConfig.OpenStackConfig().HumanReadableVMNames && + s.cpiConfig.OpenStackConfig().VM.Stemcell.APIVersion >= 2) || + len(metaDataMap) > 0 { + err = s.applyHumanReadableName(computeService, vmCID, updateMetaDataMap) + if err != nil { + return err + } + } + + return nil +} + +func (s SetVMMetadataMethod) metadataToMap(meta apiv1.VMMeta) (map[string]interface{}, error) { + jsonBytes, err := meta.MarshalJSON() + if err != nil { + return nil, fmt.Errorf("failed to convert apiv1.VMMeta to json: %w", err) + } + + var intermediateMap map[string]interface{} + err = json.Unmarshal(jsonBytes, &intermediateMap) + if err != nil { + return nil, fmt.Errorf("failed to convert json to map: %w", err) + } + for k, v := range intermediateMap { + if v == nil || k == "" { + delete(intermediateMap, k) + } + } + return intermediateMap, nil +} + +func (s SetVMMetadataMethod) applyHumanReadableName(computeService compute.ComputeService, vmCID apiv1.VMCID, metaMap map[string]interface{}) error { + + name, nameOk := metaMap["name"] + job, jobOk := metaMap["job"] + index, indexOk := metaMap["index"] + compiling, compilingOk := metaMap["compiling"] + + var newServerName string + if nameOk { + newServerName = name.(string) + } else if jobOk && indexOk { + newServerName = job.(string) + "/" + index.(string) + } else if compilingOk { + newServerName = "compiling/" + compiling.(string) + } else { + s.logger.Debug("set_vm_metadata_method", "did not apply human readable name: no name, job/index and compiling provided", nil) + return nil + } + + _, err := computeService.UpdateServer(vmCID.AsString(), newServerName) + if err != nil { + return fmt.Errorf("failed to update human readable name on server: %w", err) + } + s.logger.Info("set_vm_metadata_method", "Renamed VM with id '"+vmCID.AsString(), "' to '", newServerName, "'") + return nil } diff --git a/src/openstack_cpi_golang/cpi/methods/set_vm_metadata_test.go b/src/openstack_cpi_golang/cpi/methods/set_vm_metadata_test.go new file mode 100644 index 00000000..495f7b44 --- /dev/null +++ b/src/openstack_cpi_golang/cpi/methods/set_vm_metadata_test.go @@ -0,0 +1,307 @@ +package methods_test + +import ( + "errors" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/properties" + + "github.com/cloudfoundry/bosh-cpi-go/apiv1" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/compute/computefakes" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/config" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/methods" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils/utilsfakes" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("NewSetVMMetadataMethod", func() { + + var computeServiceBuilder *computefakes.FakeComputeServiceBuilder + var computeService *computefakes.FakeComputeService + var logger *utilsfakes.FakeLogger + var cpiConfig config.CpiConfig + + Context("SetVMMetadata", func() { + + id := apiv1.NewVMCID("123-456") + + metaDataReturn := map[string]string{ + "name": "new-name", + } + + metaDataName := apiv1.NewVMMeta(map[string]interface{}{ + "name": "new-name", + }) + + metaDataWithNil := apiv1.NewVMMeta(map[string]interface{}{ + "name": "new-name", + "test": nil, + }) + + metaDataJobIndex := apiv1.NewVMMeta(map[string]interface{}{ + "job": "new-job", + "index": "1", + }) + + metaDataCompiling := apiv1.NewVMMeta(map[string]interface{}{ + "compiling": "compiling-new", + }) + + metaDataNotRelevant := apiv1.NewVMMeta(map[string]interface{}{ + "test": "test-value", + }) + + BeforeEach(func() { + computeServiceBuilder = new(computefakes.FakeComputeServiceBuilder) + computeService = new(computefakes.FakeComputeService) + logger = new(utilsfakes.FakeLogger) + + computeServiceBuilder.BuildReturns(computeService, nil) + + cpiConfig = config.CpiConfig{} + cpiConfig.Cloud.Properties.Openstack = config.OpenstackConfig{IgnoreServerAvailabilityZone: true} + + cpiConfig.Cloud.Properties.Openstack.HumanReadableVMNames = false + cpiConfig.Cloud.Properties.Openstack.VM.Stemcell.APIVersion = 2 + }) + + It("creates the compute service", func() { + _ = methods.NewSetVMMetadataMethod( + computeServiceBuilder, + logger, + cpiConfig, + ).SetVMMetadata( + id, + metaDataName, + ) + + Expect(computeServiceBuilder.BuildCallCount()).To(Equal(1)) + }) + + It("fails on create the compute service", func() { + computeServiceBuilder.BuildReturns(nil, errors.New("boom")) + err := methods.NewSetVMMetadataMethod( + computeServiceBuilder, + logger, + cpiConfig, + ).SetVMMetadata( + id, + metaDataName, + ) + + Expect(err.Error()).To(Equal("failed to create compute service: boom")) + }) + + It("deletes nil value out imported metadata map", func() { + _ = methods.NewSetVMMetadataMethod( + computeServiceBuilder, + logger, + cpiConfig, + ).SetVMMetadata( + id, + metaDataWithNil, + ) + + updateMetaExp := properties.ServerMetadata{ + "name": "new-name", + } + _, _, updateMetaAct := computeService.DeleteServerMetaDataArgsForCall(0) + Expect(updateMetaAct).To(Equal(updateMetaExp)) + }) + + It("fails on get MetaData", func() { + computeService.GetMetadataReturns(nil, errors.New("boom")) + err := methods.NewSetVMMetadataMethod( + computeServiceBuilder, + logger, + cpiConfig, + ).SetVMMetadata( + id, + metaDataName, + ) + + Expect(err.Error()).To(Equal("failed to get Metadata: boom")) + }) + + It("get MetaData: all calls successful", func() { + computeService.GetMetadataReturns(metaDataReturn, nil) + err := methods.NewSetVMMetadataMethod( + computeServiceBuilder, + logger, + cpiConfig, + ).SetVMMetadata( + id, + metaDataName, + ) + + Expect(computeService.GetMetadataCallCount()).To(Equal(2)) + Expect(err).ToNot(HaveOccurred()) + }) + + It("fails on get MetaData on second call", func() { + computeService.GetMetadataReturnsOnCall(0, metaDataReturn, nil) + computeService.GetMetadataReturnsOnCall(1, nil, errors.New("boom")) + err := methods.NewSetVMMetadataMethod( + computeServiceBuilder, + logger, + cpiConfig, + ).SetVMMetadata( + id, + metaDataName, + ) + + Expect(computeService.GetMetadataCallCount()).To(Equal(2)) + Expect(err.Error()).To(Equal("failed to get Metadata: boom")) + }) + + It("fails on delete Metadata", func() { + computeService.DeleteServerMetaDataReturns(errors.New("boom")) + + err := methods.NewSetVMMetadataMethod( + computeServiceBuilder, + logger, + cpiConfig, + ).SetVMMetadata( + id, + metaDataName, + ) + + Expect(computeService.DeleteServerMetaDataCallCount()).To(Equal(1)) + Expect(err.Error()).To(Equal("failed to delete Metadata: boom")) + }) + + It("delete Metadata", func() { + computeService.DeleteServerMetaDataReturns(nil) + + err := methods.NewSetVMMetadataMethod( + computeServiceBuilder, + logger, + cpiConfig, + ).SetVMMetadata( + id, + metaDataName, + ) + + Expect(computeService.DeleteServerMetaDataCallCount()).To(Equal(1)) + Expect(err).ToNot(HaveOccurred()) + }) + + It("fails on update Metadata", func() { + computeService.UpdateServerMetadataReturns(errors.New("boom")) + + err := methods.NewSetVMMetadataMethod( + computeServiceBuilder, + logger, + cpiConfig, + ).SetVMMetadata( + id, + metaDataName, + ) + + Expect(computeService.DeleteServerMetaDataCallCount()).To(Equal(1)) + Expect(err.Error()).To(Equal("failed to update Metadata for key 123-456: boom")) + }) + + It("applies human readable: input: server name", func() { + + computeService.GetMetadataReturns(metaDataReturn, nil) + err := methods.NewSetVMMetadataMethod( + computeServiceBuilder, + logger, + cpiConfig, + ).SetVMMetadata( + id, + metaDataName, + ) + + Expect(computeService.UpdateServerCallCount()).To(Equal(1)) + Expect(err).ToNot(HaveOccurred()) + arg1, arg2, arg3 := logger.InfoArgsForCall(0) + Expect(arg1).To(Equal("set_vm_metadata_method")) + Expect(arg2).To(Equal("Renamed VM with id '123-456")) + Expect(arg3[0]).To(Equal("' to '")) + Expect(arg3[1]).To(Equal("new-name")) + Expect(arg3[2]).To(Equal("'")) + }) + + It("applies human readable: input: job + index", func() { + + computeService.GetMetadataReturns(metaDataReturn, nil) + err := methods.NewSetVMMetadataMethod( + computeServiceBuilder, + logger, + cpiConfig, + ).SetVMMetadata( + id, + metaDataJobIndex, + ) + + Expect(computeService.UpdateServerCallCount()).To(Equal(1)) + Expect(err).ToNot(HaveOccurred()) + arg1, arg2, arg3 := logger.InfoArgsForCall(0) + Expect(arg1).To(Equal("set_vm_metadata_method")) + Expect(arg2).To(Equal("Renamed VM with id '123-456")) + Expect(arg3[0]).To(Equal("' to '")) + Expect(arg3[1]).To(Equal("new-job/1")) + Expect(arg3[2]).To(Equal("'")) + }) + + It("applies human readable: input: compiling", func() { + + computeService.GetMetadataReturns(metaDataReturn, nil) + err := methods.NewSetVMMetadataMethod( + computeServiceBuilder, + logger, + cpiConfig, + ).SetVMMetadata( + id, + metaDataCompiling, + ) + + Expect(computeService.UpdateServerCallCount()).To(Equal(1)) + Expect(err).ToNot(HaveOccurred()) + arg1, arg2, arg3 := logger.InfoArgsForCall(0) + Expect(arg1).To(Equal("set_vm_metadata_method")) + Expect(arg2).To(Equal("Renamed VM with id '123-456")) + Expect(arg3[0]).To(Equal("' to '")) + Expect(arg3[1]).To(Equal("compiling/compiling-new")) + Expect(arg3[2]).To(Equal("'")) + }) + + It("applies human readable: input: not relevant for naming", func() { + + computeService.GetMetadataReturns(metaDataReturn, nil) + err := methods.NewSetVMMetadataMethod( + computeServiceBuilder, + logger, + cpiConfig, + ).SetVMMetadata( + id, + metaDataNotRelevant, + ) + + Expect(computeService.UpdateServerCallCount()).To(Equal(0)) + Expect(logger.DebugCallCount()).To(Equal(1)) + arg1, arg2, arg3 := logger.DebugArgsForCall(0) + Expect(arg1).To(Equal("set_vm_metadata_method")) + Expect(arg2).To(Equal("did not apply human readable name: no name, job/index and compiling provided")) + Expect(len(arg3)).To(Equal(1)) + Expect(err).ToNot(HaveOccurred()) + }) + + It("applies human readable: update server failed", func() { + computeService.GetMetadataReturns(metaDataReturn, nil) + computeService.UpdateServerReturns(nil, errors.New("boom")) + err := methods.NewSetVMMetadataMethod( + computeServiceBuilder, + logger, + cpiConfig, + ).SetVMMetadata( + id, + metaDataName, + ) + Expect(computeService.UpdateServerCallCount()).To(Equal(1)) + Expect(err.Error()).To(Equal("failed to update human readable name on server: boom")) + }) + }) +}) diff --git a/src/openstack_cpi_golang/cpi/methods/snapshot_disk.go b/src/openstack_cpi_golang/cpi/methods/snapshot_disk.go index 8bb3fee4..d77b8bd9 100644 --- a/src/openstack_cpi_golang/cpi/methods/snapshot_disk.go +++ b/src/openstack_cpi_golang/cpi/methods/snapshot_disk.go @@ -1,13 +1,138 @@ package methods -import "github.com/cloudfoundry/bosh-cpi-go/apiv1" +import ( + "encoding/json" + "fmt" + "strings" + "time" -type SnapshotDiskMethod struct{} + "github.com/cloudfoundry/bosh-cpi-go/apiv1" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/config" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/volume" + "github.com/google/uuid" +) -func NewSnapshotDiskMethod() SnapshotDiskMethod { - return SnapshotDiskMethod{} +type SnapshotDiskMethod struct { + volumeServiceBuilder volume.VolumeServiceBuilder + cpiConfig config.CpiConfig + logger utils.Logger +} + +func NewSnapshotDiskMethod( + volumeServiceBuilder volume.VolumeServiceBuilder, + cpiConfig config.CpiConfig, + logger utils.Logger) SnapshotDiskMethod { + return SnapshotDiskMethod{ + volumeServiceBuilder: volumeServiceBuilder, + cpiConfig: cpiConfig, + logger: logger, + } } func (s SnapshotDiskMethod) SnapshotDisk(cid apiv1.DiskCID, meta apiv1.DiskMeta) (apiv1.SnapshotCID, error) { - return apiv1.SnapshotCID{}, nil + + s.logger.Info("snapshot_disk", fmt.Sprintf("Execute snapshot for disk ID %s", cid.AsString())) + + volumeService, err := s.volumeServiceBuilder.Build() + if err != nil { + return apiv1.SnapshotCID{}, + fmt.Errorf("snapShotDisk: Failed to get volume service: %w", err) + } + currentVolume, err := volumeService.GetVolume(cid.AsString()) + if err != nil { + return apiv1.SnapshotCID{}, + fmt.Errorf("snapShotDisk: Failed to get volume ID %s: %w", cid.AsString(), err) + } + var devices []string + devices = make([]string, 0) + for _, attachment := range currentVolume.Attachments { + if attachment.Device != "" { + devices = append(devices, attachment.Device) + } + } + stringKeyMap, err := metaDataToMap(meta) + if err != nil { + return apiv1.SnapshotCID{}, fmt.Errorf("snapShotDisk: Failed to convert disk metadata: %w", err) + } + s.logger.Debug("snapshot_disk", fmt.Sprintf("Provided disk metadata: %v", stringKeyMap)) + + description := []interface{}{ + getMapValueOrDefault(stringKeyMap, "deployment", "deployment-not-set"), + getMapValueOrDefault(stringKeyMap, "job", "job-not-set"), + getMapValueOrDefault(stringKeyMap, "index", "index-not-set"), + } + if len(devices) > 0 { + parts := strings.Split(devices[0], "/") + description = append(description, parts[len(parts)-1]) + } + randomUUID, err := uuid.NewRandom() + if err != nil { + return apiv1.SnapshotCID{}, fmt.Errorf("snapShotDisk: Failed to create random UUID for snapshot name: %w", err) + } + snapshotName := "snapshot-" + randomUUID.String() + var descriptionParts []string + for _, value := range description { + descriptionParts = append(descriptionParts, fmt.Sprintf("%v", value)) + } + snapshotDescription := strings.Join(descriptionParts, "/") + + s.logger.Info("snapShotDisk", fmt.Sprintf("Creating new snapshot %s for volume %s", snapshotName, cid.AsString())) + stringKeyMap["director"] = getMapValueOrDefault(stringKeyMap, "director_name", "director-not-set") + stringKeyMap["instance_index"] = fmt.Sprintf("%v", getMapValueOrDefault(stringKeyMap, "index", "index-not-set")) + stringKeyMap["instance_name"] = fmt.Sprintf("%v", fmt.Sprintf("%v", getMapValueOrDefault(stringKeyMap, "job", "job-not-set"))+"/"+fmt.Sprintf("%v", getMapValueOrDefault(stringKeyMap, "instance_id", "instance_id-not-set"))) + delete(stringKeyMap, "director_name") + delete(stringKeyMap, "index") + delete(stringKeyMap, "job") + + snapshot, err := volumeService.CreateSnapshot( + cid.AsString(), + true, + snapshotName, + snapshotDescription, + convertMapToString(stringKeyMap), + ) + if err != nil { + return apiv1.SnapshotCID{}, fmt.Errorf("snapShotDisk: Failed to create snapshot %s for volume %s: %w", snapshotName, cid.AsString(), err) + } + s.logger.Info("snapShotDisk", fmt.Sprintf("Waiting for new snapshot %s for volume %s to become available", snapshotName, cid.AsString())) + err = volumeService.WaitForSnapshotToBecomeStatus(snapshot.ID, time.Duration(s.cpiConfig.OpenStackConfig().StateTimeOut)*time.Second, "available") + if err != nil { + return apiv1.SnapshotCID{}, fmt.Errorf("snapShotDisk: Failed while waiting for creating snapshot %s for volume %s: %w", snapshotName, cid.AsString(), err) + } + return apiv1.NewSnapshotCID(snapshot.ID), nil +} + +func getMapValueOrDefault(m map[string]interface{}, key string, defaultValue interface{}) interface{} { + if val, ok := m[key]; ok { + return val + } + return defaultValue +} + +func metaDataToMap(meta apiv1.DiskMeta) (map[string]interface{}, error) { + jsonBytes, err := meta.MarshalJSON() + if err != nil { + return nil, fmt.Errorf("metaDataToMap: Failed to convert apiv1.DiskMeta using MarshalJSON") + } + + var resultMap map[string]interface{} + err = json.Unmarshal(jsonBytes, &resultMap) + if err != nil { + return nil, fmt.Errorf("metaDataToMap: failed to convert apiv1.DiskMeta to map using Unmarshal") + } + for k, v := range resultMap { + if v == nil || k == "" { + delete(resultMap, k) + } + } + return resultMap, nil +} + +func convertMapToString(mapInterface map[string]interface{}) map[string]string { + result := make(map[string]string) + for key, value := range mapInterface { + result[key] = fmt.Sprintf("%v", value) + } + return result } diff --git a/src/openstack_cpi_golang/cpi/methods/snapshot_disk_test.go b/src/openstack_cpi_golang/cpi/methods/snapshot_disk_test.go new file mode 100644 index 00000000..73bfde6b --- /dev/null +++ b/src/openstack_cpi_golang/cpi/methods/snapshot_disk_test.go @@ -0,0 +1,120 @@ +package methods_test + +import ( + "errors" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/config" + "github.com/gophercloud/gophercloud/openstack/blockstorage/v3/snapshots" + "github.com/gophercloud/gophercloud/openstack/blockstorage/v3/volumes" + + "github.com/cloudfoundry/bosh-cpi-go/apiv1" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/methods" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils/utilsfakes" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/volume/volumefakes" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("SnapshotDisk Unit Tests", func() { + + var volumeServiceBuilder *volumefakes.FakeVolumeServiceBuilder + var volumeService *volumefakes.FakeVolumeService + var logger *utilsfakes.FakeLogger + var cpiConfig config.CpiConfig + var volumeCid = apiv1.NewDiskCID("vol1-id") + var metadata apiv1.DiskMeta + var snapshotDisk methods.SnapshotDiskMethod + var volume volumes.Volume + var snapshot snapshots.Snapshot + + Context("setting disk metadata", func() { + + BeforeEach(func() { + volumeServiceBuilder = new(volumefakes.FakeVolumeServiceBuilder) + volumeService = new(volumefakes.FakeVolumeService) + logger = new(utilsfakes.FakeLogger) + + volumeServiceBuilder.BuildReturns(volumeService, nil) + volume = volumes.Volume{ + ID: "vol1-id", + Attachments: []volumes.Attachment{ + {Device: "dev1/dev2/dev3", ServerID: "server1", VolumeID: "vol1-id"}, + {Device: "dev5/dev6/dev7", ServerID: "server2", VolumeID: "vol1-id"}, + }, + } + volumeService.GetVolumeReturns(&volume, nil) + + diskMeta := map[string]interface{}{ + "deployment": "deployment", + "job": "job", + "index": 1, + "test": "test", + "director_name": "director_name", + "instance_id": "instance_id", + } + metadata = apiv1.NewDiskMeta(diskMeta) + + snapshot = snapshots.Snapshot{ + ID: "snap1-id", + } + volumeService.CreateSnapshotReturns(&snapshot, nil) + + cpiConfig = config.CpiConfig{} + snapshotDisk = methods.NewSnapshotDiskMethod(volumeServiceBuilder, cpiConfig, logger) + }) + + It("fails on volume service builder", func() { + volumeServiceBuilder.BuildReturns(nil, errors.New("boom")) + _, err := snapshotDisk.SnapshotDisk(volumeCid, metadata) + Expect(err.Error()).To(Equal("snapShotDisk: Failed to get volume service: boom")) + }) + + It("fails on get volume", func() { + volumeService.GetVolumeReturns(nil, errors.New("boom")) + _, err := snapshotDisk.SnapshotDisk(volumeCid, metadata) + Expect(err.Error()).To(Equal("snapShotDisk: Failed to get volume ID vol1-id: boom")) + }) + + It("fails to convert metadata to map[string]interface for MarshalJSON", func() { + diskMeta := map[string]interface{}{ + "key1": func() {}, + } + metadataFalse := apiv1.NewDiskMeta(diskMeta) + _, err := snapshotDisk.SnapshotDisk(volumeCid, metadataFalse) + Expect(err).To(MatchError(ContainSubstring("snapShotDisk: Failed to convert disk metadata"))) + }) + + It("fails to create snapshot", func() { + volumeService.CreateSnapshotReturns(nil, errors.New("boom")) + _, err := snapshotDisk.SnapshotDisk(volumeCid, metadata) + Expect(err).To(MatchError(ContainSubstring(("snapShotDisk: Failed to create snapshot")))) + }) + + It("creates snapshot", func() { + snapshotCID, err := snapshotDisk.SnapshotDisk(volumeCid, metadata) + + volumeCidReturn, boolForce, snapShotName, joinedDescription, finalMetaDataMap := volumeService.CreateSnapshotArgsForCall(0) + Expect(volumeCidReturn).To(Equal(volumeCid.AsString())) + Expect(boolForce).To(BeTrue()) + Expect(snapShotName).To(ContainSubstring("snapshot-")) + Expect(joinedDescription).To(Equal("deployment/job/1/dev3")) + Expect(finalMetaDataMap).To(Equal(map[string]string{ + "deployment": "deployment", + "test": "test", + "director": "director_name", + "instance_id": "instance_id", + "instance_name": "job/instance_id", + "instance_index": "1", + })) + Expect(err).ToNot(HaveOccurred()) + Expect(snapshotCID.AsString()).To(Equal("snap1-id")) + }) + + It("fails to waitForSnapshotToBeAvailable", func() { + volumeService.WaitForSnapshotToBecomeStatusReturns(errors.New("boom")) + _, err := snapshotDisk.SnapshotDisk(volumeCid, metadata) + Expect(err).To(MatchError(ContainSubstring("snapShotDisk: Failed while waiting for creating snapshot"))) + }) + }) + +}) diff --git a/src/openstack_cpi_golang/cpi/network/network_config_builder.go b/src/openstack_cpi_golang/cpi/network/network_config_builder.go new file mode 100644 index 00000000..1ceccd15 --- /dev/null +++ b/src/openstack_cpi_golang/cpi/network/network_config_builder.go @@ -0,0 +1,199 @@ +package network + +import ( + "fmt" + "slices" + + "github.com/cloudfoundry/bosh-cpi-go/apiv1" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/config" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/properties" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils" +) + +type networkConfigBuilder struct { + securityGroupsResolver SecurityGroupsResolver + networks apiv1.Networks + openstackConfig config.OpenstackConfig + cloudProps properties.CreateVM + logger utils.Logger +} + +func NewNetworkConfigBuilder( + securityGroupsResolver SecurityGroupsResolver, + networks apiv1.Networks, + openstackConfig config.OpenstackConfig, + cloudProps properties.CreateVM, + logger utils.Logger, +) networkConfigBuilder { + return networkConfigBuilder{ + securityGroupsResolver: securityGroupsResolver, + networks: networks, + openstackConfig: openstackConfig, + cloudProps: cloudProps, + logger: logger, + } +} + +func (b networkConfigBuilder) Build() (properties.NetworkConfig, error) { + defaultNetwork := b.createNetwork("", b.networks.Default()) + + manualNetworks, err := b.createManualNetwork(b.networks, b.openstackConfig) + if err != nil { + return properties.NetworkConfig{}, fmt.Errorf("invalid manual network configuration: %w", err) + } + + vipNetwork, err := b.createSingleNetwork(b.networks, "vip") + if err != nil { + return properties.NetworkConfig{}, fmt.Errorf("invalid vip network configuration: %w", err) + } + + dynamicNetwork, err := b.createSingleNetwork(b.networks, "dynamic") + if err != nil { + return properties.NetworkConfig{}, fmt.Errorf("invalid dynamic network configuration: %w", err) + } + + err = b.validateNetIDs(b.combineNetworks(manualNetworks, dynamicNetwork, nil)) + if err != nil { + return properties.NetworkConfig{}, fmt.Errorf("invalid network configuration: %w", err) + } + + securityGroups, err := b.securityGroups(b.combineNetworks(manualNetworks, dynamicNetwork, vipNetwork)) + if err != nil { + return properties.NetworkConfig{}, fmt.Errorf("invalid security group configuration: %w", err) + } + + resultNetworkConfig := properties.NetworkConfig{ + DefaultNetwork: defaultNetwork, + ManualNetworks: manualNetworks, + VIPNetwork: vipNetwork, + DynamicNetwork: dynamicNetwork, + SecurityGroups: securityGroups, + } + + return resultNetworkConfig, nil +} + +func (b networkConfigBuilder) securityGroups(networks []properties.Network) ([]string, error) { + var securityGroups []string + + securityGroups = b.cloudProps.SecurityGroups + if len(securityGroups) == 0 { + + securityGroups = b.securityGroupsFromNetworks(networks) + if len(securityGroups) == 0 { + + securityGroups = b.openstackConfig.DefaultSecurityGroups + } + + } + + securityGroupIDs, err := b.securityGroupsResolver.Resolve(securityGroups) + if err != nil { + return []string{}, fmt.Errorf("failed to resolve security group: %w", err) + } + + b.logger.Info("network-config-builder", "resolved security groups ids: %v", securityGroupIDs) + return securityGroupIDs, nil +} + +func (b networkConfigBuilder) securityGroupsFromNetworks(networks []properties.Network) []string { + var securityGroups []string + + for _, network := range networks { + securityGroups = append(securityGroups, network.CloudProps.SecurityGroups...) + } + + return utils.UniqueArray(securityGroups) +} + +func (b networkConfigBuilder) combineNetworks(manualNetworks []properties.Network, dynamicNetwork *properties.Network, vipNetwork *properties.Network) []properties.Network { + var networks []properties.Network + + networks = append(networks, manualNetworks...) + + if dynamicNetwork != nil { + networks = append(networks, *dynamicNetwork) + } + + if vipNetwork != nil { + networks = append(networks, *vipNetwork) + } + + return networks +} + +func (b networkConfigBuilder) createManualNetwork(networks apiv1.Networks, openstackConfig config.OpenstackConfig) ([]properties.Network, error) { + var manualNetworks []properties.Network + + for key, network := range networks { + if network.Type() == "manual" { + createdNetwork := b.createNetwork(key, network) + + netID := createdNetwork.CloudProps.NetID + if netID == "" { + return []properties.Network{}, fmt.Errorf("manual network must have a net_id") + } + + manualNetworks = append(manualNetworks, createdNetwork) + } + } + + if len(manualNetworks) > 1 { + if openstackConfig.UseDHCP || openstackConfig.ConfigDrive != "" { + return []properties.Network{}, fmt.Errorf("multiple manual networks can only be used with 'openstack.use_dhcp=false' and 'openstack.config_drive=cdrom|disk'") + } + } + + return manualNetworks, nil +} + +func (b networkConfigBuilder) createSingleNetwork(networks apiv1.Networks, networkType string) (*properties.Network, error) { + var network *properties.Network + + for key, net := range networks { + if net.Type() == networkType { + if network != nil { + return &properties.Network{}, fmt.Errorf("only one %s should be defined per instance", networkType) + } + + createdNetwork := b.createNetwork(key, net) + network = &createdNetwork + } + } + + return network, nil +} + +func (b networkConfigBuilder) createNetwork(key string, network apiv1.Network) properties.Network { + vmNetworkProps := properties.NetworkCloudProps{} + err := network.CloudProps().As(&vmNetworkProps) + if err != nil { + return properties.Network{} + } + + return properties.Network{ + Key: key, + Default: network.Default(), + DNS: network.DNS(), + IP: network.IP(), + Gateway: network.Gateway(), + Netmask: network.Netmask(), + Type: network.Type(), + CloudProps: vmNetworkProps, + } +} + +func (b networkConfigBuilder) validateNetIDs(networks []properties.Network) error { + var usedNetIDs []string + + for _, network := range networks { + netID := network.CloudProps.NetID + if slices.Contains(usedNetIDs, netID) { + return fmt.Errorf("network with id %s is defined multiple times", netID) + } + + usedNetIDs = append(usedNetIDs, netID) + } + + return nil +} diff --git a/src/openstack_cpi_golang/cpi/network/network_config_builder_test.go b/src/openstack_cpi_golang/cpi/network/network_config_builder_test.go new file mode 100644 index 00000000..98d51647 --- /dev/null +++ b/src/openstack_cpi_golang/cpi/network/network_config_builder_test.go @@ -0,0 +1,342 @@ +package network_test + +import ( + "encoding/binary" + "encoding/json" + "net" + "sort" + + "github.com/cloudfoundry/bosh-cpi-go/apiv1" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/config" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/network" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/network/networkfakes" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/properties" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils/utilsfakes" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("NetworkConfigBuilder", func() { + var networkingConfig properties.NetworkConfig + var securityGroupsResolver networkfakes.FakeSecurityGroupsResolver + var openstackConfig config.OpenstackConfig + var cloudProperties properties.CreateVM + var logger utils.Logger + + BeforeEach(func() { + openstackConfig = config.OpenstackConfig{} + cloudProperties = properties.CreateVM{} + securityGroupsResolver = networkfakes.FakeSecurityGroupsResolver{} + logger = &utilsfakes.FakeLogger{} + }) + + Context("NewNetworkConfig", func() { + BeforeEach(func() { + networkingConfig, _ = createNetworkConfig(&securityGroupsResolver, []byte(`{ + "name1": { + "type": "manual", + "ip": "1.1.1.1", + "default": ["gateway"], + "cloud_properties": {"net_id": "the_net_id_1", "security_groups": ["security_group_1", "security_group_2"]} + }, + "name2": { + "type": "manual", + "ip": "2.2.2.2", + "cloud_properties": {"net_id": "the_net_id_2"} + }, + "name3": { + "type": "vip", + "ip": "3.3.3.3", + "cloud_properties": {"net_id": "the_net_id_3", "security_groups": ["security_group_3"]} + }, + "name4": { + "type": "dynamic", + "ip": "4.4.4.4", + "cloud_properties": {"net_id": "the_net_id_4", "security_groups": ["security_group_4"]} + } + }`), openstackConfig, cloudProperties, logger) + }) + + It("returns an error if a manual network is missing a netid", func() { + _, err := createNetworkConfig(&securityGroupsResolver, []byte(`{ + "name1": { + "type": "manual", + "ip": "", + "cloud_properties": {"missing_net_id": ""} + }, + "name2": { + "type": "manual", + "ip": "", + "cloud_properties": {"net_id": "the_net_id_2"} + } + }`), openstackConfig, cloudProperties, logger) + + Expect(err.Error()).To(Equal("invalid manual network configuration: manual network must have a net_id")) + }) + + It("returns an error if multiple manual network exists while dhcp should be used and config drive is not defined", func() { + _, err := createNetworkConfig(&securityGroupsResolver, []byte(`{ + "name1": { + "type": "manual", + "ip": "", + "cloud_properties": {"net_id": "the_net_id_1"} + }, + "name2": { + "type": "manual", + "ip": "", + "cloud_properties": {"net_id": "the_net_id_2"} + } + }`), config.OpenstackConfig{UseDHCP: true}, cloudProperties, logger) + + Expect(err.Error()).To(Equal("invalid manual network configuration: multiple manual networks can only be used with 'openstack.use_dhcp=false' and 'openstack.config_drive=cdrom|disk'")) + }) + + It("returns an error if multiple vip networks exists", func() { + _, err := createNetworkConfig(&securityGroupsResolver, []byte(`{ + "name1": { + "type": "vip", + "ip": "", + "cloud_properties": {} + }, + "name2": { + "type": "vip", + "ip": "", + "cloud_properties": {} + } + }`), openstackConfig, cloudProperties, logger) + + Expect(err.Error()).To(Equal("invalid vip network configuration: only one vip should be defined per instance")) + }) + + It("returns an error if multiple dynamic networks exists", func() { + _, err := createNetworkConfig(&securityGroupsResolver, []byte(`{ + "name1": { + "type": "dynamic", + "ip": "", + "cloud_properties": {} + }, + "name2": { + "type": "dynamic", + "ip": "", + "cloud_properties": {} + } + }`), openstackConfig, cloudProperties, logger) + + Expect(err.Error()).To(Equal("invalid dynamic network configuration: only one dynamic should be defined per instance")) + }) + + It("returns an error if same net_id is used by multiple networks", func() { + _, err := createNetworkConfig(&securityGroupsResolver, []byte(`{ + "name1": { + "type": "manual", + "ip": "", + "cloud_properties": {"net_id": "same_net_id"} + }, + "name2": { + "type": "dynamic", + "ip": "", + "cloud_properties": {"net_id": "same_net_id"} + } + }`), openstackConfig, cloudProperties, logger) + + Expect(err.Error()).To(Equal("invalid network configuration: network with id same_net_id is defined multiple times")) + }) + }) + + Context("DefaultNetwork", func() { + It("returns the default network", func() { + defaultNetwork := networkingConfig.DefaultNetwork + + Expect(defaultNetwork.Type).To(Equal("manual")) + Expect(defaultNetwork.IP).To(Equal("1.1.1.1")) + Expect(defaultNetwork.CloudProps.NetID).To(Equal("the_net_id_1")) + }) + + It("returns an empty network if no network is provided", func() { + networkingConfig, err := createNetworkConfig(&securityGroupsResolver, []byte(`{}`), openstackConfig, cloudProperties, logger) + Expect(err).ToNot(HaveOccurred()) + defaultNetwork := networkingConfig.DefaultNetwork + + Expect(defaultNetwork.Type).To(Equal("")) + Expect(defaultNetwork.IP).To(Equal("")) + Expect(defaultNetwork.CloudProps.NetID).To(Equal("")) + }) + }) + + Context("GetManualNetworks", func() { + It("returns the manual networks", func() { + manualNetworks := sortNetworks(networkingConfig.ManualNetworks) + + Expect(manualNetworks[0].Type).To(Equal("manual")) + Expect(manualNetworks[0].IP).To(Equal("1.1.1.1")) + Expect(manualNetworks[0].CloudProps.NetID).To(Equal("the_net_id_1")) + + Expect(manualNetworks[1].Type).To(Equal("manual")) + Expect(manualNetworks[1].IP).To(Equal("2.2.2.2")) + Expect(manualNetworks[1].CloudProps.NetID).To(Equal("the_net_id_2")) + }) + }) + + Context("GetVIPNetwork", func() { + It("returns the vip network", func() { + vipNetwork := networkingConfig.VIPNetwork + + Expect(vipNetwork.Type).To(Equal("vip")) + Expect(vipNetwork.IP).To(Equal("3.3.3.3")) + Expect(vipNetwork.CloudProps.NetID).To(Equal("the_net_id_3")) + }) + }) + + Context("GetDynamicNetwork", func() { + It("returns the dynamic network", func() { + dynamicNetwork := networkingConfig.DynamicNetwork + + Expect(dynamicNetwork.Type).To(Equal("dynamic")) + Expect(dynamicNetwork.IP).To(Equal("4.4.4.4")) + Expect(dynamicNetwork.CloudProps.NetID).To(Equal("the_net_id_4")) + }) + }) + + Context("SecurityGroups", func() { + It("returns cloud properties security groups", func() { + securityGroupsResolver.ResolveReturns([]string{"resolved_security_group_1", "resolved_security_group_2"}, nil) + + networkingConfig, _ = createNetworkConfig(&securityGroupsResolver, []byte(`{ + "name1": { + "type": "manual", + "ip": "1.1.1.1", + "default": ["gateway"], + "cloud_properties": {"net_id": "the_net_id_1", "security_groups": ["security_group_1", "security_group_2"]} + } + }`), + config.OpenstackConfig{ + DefaultSecurityGroups: []string{"default_security_group_1", "default_security_group_2"}, + }, + properties.CreateVM{ + SecurityGroups: []string{"cloud_config_security_group_1", "cloud_config_security_group_2"}, + }, logger) + securityGroups := networkingConfig.SecurityGroups + + securityGroupsParam := securityGroupsResolver.ResolveArgsForCall(0) + Expect(securityGroupsParam).To(ContainElements("cloud_config_security_group_1", "cloud_config_security_group_2")) + Expect(securityGroups).To(ContainElements("resolved_security_group_1", "resolved_security_group_2")) + }) + + It("returns network security groups if cloud properties do not define security groups", func() { + securityGroupsResolver.ResolveReturns([]string{"resolved_network_security_group_1", "resolved_network_security_group_2"}, nil) + + networkingConfig, _ = createNetworkConfig(&securityGroupsResolver, []byte(`{ + "name1": { + "type": "manual", + "ip": "1.1.1.1", + "default": ["gateway"], + "cloud_properties": {"net_id": "the_net_id_1", "security_groups": ["security_group_1", "security_group_2"]} + } + }`), + config.OpenstackConfig{ + DefaultSecurityGroups: []string{"default_security_group_1", "default_security_group_2"}, + }, + properties.CreateVM{}, + logger, + ) + securityGroups := networkingConfig.SecurityGroups + + securityGroupsParam := securityGroupsResolver.ResolveArgsForCall(0) + Expect(securityGroupsParam).To(ContainElements("security_group_1", "security_group_2")) + Expect(securityGroups).To(ContainElements("resolved_network_security_group_1", "resolved_network_security_group_2")) + }) + + It("returns default security groups if network security groups are not defined", func() { + securityGroupsResolver.ResolveReturns([]string{"resolved_default_group_1", "resolved_default_group_2"}, nil) + + networkingConfig, _ = createNetworkConfig(&securityGroupsResolver, []byte(`{ + "name1": { + "type": "manual", + "ip": "1.1.1.1", + "default": ["gateway"], + "cloud_properties": {"net_id": "the_net_id_1", "security_groups": ["security_group_1", "security_group_2"]} + } + }`), + config.OpenstackConfig{ + DefaultSecurityGroups: []string{"default_security_group_1", "default_security_group_2"}, + }, + properties.CreateVM{}, + logger, + ) + securityGroups := networkingConfig.SecurityGroups + + securityGroupsParam := securityGroupsResolver.ResolveArgsForCall(0) + Expect(securityGroupsParam).To(ContainElements("security_group_1", "security_group_2")) + Expect(securityGroups).To(ContainElements("resolved_default_group_1", "resolved_default_group_2")) + }) + + It("network security groups merge all networks", func() { + securityGroupsResolver.ResolveReturns([]string{ + "resolved_security_group_1", "resolved_security_group_2", "resolved_security_group_3", "resolved_security_group_4", + }, nil) + + networkingConfig, _ = createNetworkConfig(&securityGroupsResolver, []byte(`{ + "name1": { + "type": "manual", + "ip": "1.1.1.1", + "default": ["gateway"], + "cloud_properties": {"net_id": "the_net_id_1", "security_groups": ["security_group_1", "security_group_2"]} + }, + "name2": { + "type": "manual", + "ip": "2.2.2.2", + "cloud_properties": {"net_id": "the_net_id_2"} + }, + "name3": { + "type": "vip", + "ip": "3.3.3.3", + "cloud_properties": {"net_id": "the_net_id_3", "security_groups": ["security_group_3"]} + }, + "name4": { + "type": "dynamic", + "ip": "4.4.4.4", + "cloud_properties": {"net_id": "the_net_id_4", "security_groups": ["security_group_4"]} + } + }`), openstackConfig, cloudProperties, logger) + securityGroups := sortSecurityGroups(networkingConfig.SecurityGroups) + + securityGroupsParam := securityGroupsResolver.ResolveArgsForCall(0) + Expect(securityGroupsParam).To(ContainElements("security_group_1", "security_group_2", "security_group_4", "security_group_3")) + Expect(securityGroups).To(ContainElements("resolved_security_group_1", "resolved_security_group_2", "resolved_security_group_3", "resolved_security_group_4")) + }) + }) +}) + +func createNetworkConfig(securityGroupsResolver network.SecurityGroupsResolver, bytes []byte, openstackConfig config.OpenstackConfig, cloudProperties properties.CreateVM, logger utils.Logger) (properties.NetworkConfig, error) { + var networks apiv1.Networks + err := json.Unmarshal(bytes, &networks) + Expect(err).ToNot(HaveOccurred()) + + networkConfig, err := network.NewNetworkConfigBuilder(securityGroupsResolver, networks, openstackConfig, cloudProperties, logger).Build() + + return networkConfig, err +} + +func sortSecurityGroups(securityGroups []string) []string { + sort.Slice(securityGroups, func(i, j int) bool { + return securityGroups[i] < securityGroups[j] + }) + + return securityGroups +} + +func sortNetworks(networks []properties.Network) []properties.Network { + sort.Slice(networks, func(i, j int) bool { + return ip2int(net.ParseIP(networks[i].IP)) < ip2int(net.ParseIP(networks[j].IP)) + }) + + return networks +} + +func ip2int(ip net.IP) uint32 { + if len(ip) == 16 { + return binary.BigEndian.Uint32(ip[12:16]) + } + return binary.BigEndian.Uint32(ip) +} diff --git a/src/openstack_cpi_golang/cpi/network/network_service.go b/src/openstack_cpi_golang/cpi/network/network_service.go new file mode 100644 index 00000000..4b0891f8 --- /dev/null +++ b/src/openstack_cpi_golang/cpi/network/network_service.go @@ -0,0 +1,334 @@ +package network + +import ( + "errors" + "fmt" + "net" + + "github.com/cloudfoundry/bosh-cpi-go/apiv1" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/config" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/properties" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils" + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips" + "github.com/gophercloud/gophercloud/openstack/networking/v2/ports" + "github.com/gophercloud/gophercloud/openstack/networking/v2/subnets" +) + +//counterfeiter:generate . NetworkService +type NetworkService interface { + ConfigureVIPNetwork( + instanceId string, + networkConfig properties.NetworkConfig, + ) error + + GetNetworkConfiguration( + networks apiv1.Networks, + openstackConfig config.OpenstackConfig, + cloudProps properties.CreateVM, + ) (properties.NetworkConfig, error) + + GetSubnetID(networkID string, ip string) (string, error) + + CreatePort(networkConfig properties.Network, securityGroups []string, cloudProperties properties.CreateVM) (ports.Port, error) + + GetPorts( + instanceId string, + defaultNetwork properties.Network, + retryable bool, + ) ([]ports.Port, error) + + DeletePorts( + ports []ports.Port, + ) error +} + +type networkService struct { + serviceClients utils.ServiceClients + networkingFacade NetworkingFacade + logger utils.Logger +} + +func NewNetworkService( + serviceClients utils.ServiceClients, + networkingFacade NetworkingFacade, + logger utils.Logger, +) networkService { + return networkService{ + serviceClients: serviceClients, + networkingFacade: networkingFacade, + logger: logger, + } +} + +func (c networkService) ConfigureVIPNetwork( + instanceId string, + networkConfig properties.NetworkConfig, +) error { + vipNetwork := networkConfig.VIPNetwork + + if vipNetwork != nil { + floatingIp, err := c.getFloatingIp(vipNetwork) + if err != nil { + return fmt.Errorf("failed to get floating IP: %w", err) + } + + instancePorts, err := c.GetPorts(instanceId, networkConfig.DefaultNetwork, false) + if err != nil { + return fmt.Errorf("failed to get port: %w", err) + } + if len(instancePorts) == 0 { + return fmt.Errorf("no port allocated by instance %s and network %s", instanceId, networkConfig.DefaultNetwork.CloudProps.NetID) + } + + err = c.associateFloatingIp(floatingIp.ID, instancePorts[0].ID) + if err != nil { + return fmt.Errorf("failed to associate floating ip to port: %w", err) + } + } + return nil +} + +func (c networkService) GetNetworkConfiguration( + networks apiv1.Networks, + openstackConfig config.OpenstackConfig, + cloudProps properties.CreateVM, +) (properties.NetworkConfig, error) { + securityGroupsResolver := NewSecurityGroupsResolver(c.serviceClients, c.networkingFacade, c.logger) + + networkProperties, err := NewNetworkConfigBuilder(securityGroupsResolver, networks, openstackConfig, cloudProps, c.logger).Build() + return networkProperties, err +} + +func (c networkService) GetSubnetID(networkID string, ip string) (string, error) { + ipAddress := net.ParseIP(ip) + if ipAddress == nil { + return "", fmt.Errorf("failed to parse ip address '%s'", ip) + } + + listOpts := subnets.ListOpts{ + NetworkID: networkID, + } + + allPages, err := c.networkingFacade.ListSubnets(c.serviceClients.RetryableServiceClient, listOpts) + if err != nil { + return "", fmt.Errorf("failed to list subnets: %w", err) + } + + allSubnets, err := c.networkingFacade.ExtractSubnets(allPages) + if err != nil { + return "", fmt.Errorf("failed to extract subnets: %w", err) + } + + if len(allSubnets) == 0 { + return "", fmt.Errorf("no subnet found for network '%s'", networkID) + } + + var matchingSubnets []string + for _, subnet := range allSubnets { + _, ipNet, err := net.ParseCIDR(subnet.CIDR) + if ipNet == nil { + return "", fmt.Errorf("failed to parse subnet cidr '%s': %w", subnet.CIDR, err) + } + + if ipNet.Contains(ipAddress) { + matchingSubnets = append(matchingSubnets, subnet.ID) + } + } + + if len(matchingSubnets) > 1 { + return "", fmt.Errorf("found more than one matching subnet for the ip '%s' in '%v'", ipAddress, matchingSubnets) + } else if len(matchingSubnets) == 0 { + return "", fmt.Errorf("no matching subnet found for the ip '%s'", ipAddress) + } + + return matchingSubnets[0], nil +} + +func (c networkService) CreatePort(network properties.Network, securityGroups []string, cloudProperties properties.CreateVM) (ports.Port, error) { + createOpts, err := c.getPortCreationNetworkOpts(network, securityGroups, cloudProperties) + if err != nil { + return ports.Port{}, fmt.Errorf("failed create network opts: %w", err) + } + + c.logger.Info("network-service", fmt.Sprintf("creating port with opts '%+v', using security groups %v", createOpts, securityGroups)) + + createdPort, err := c.networkingFacade.CreatePort(c.serviceClients.ServiceClient, createOpts) + if err != nil { + c.logger.Warn("network-service", + fmt.Sprintf("failed to create port on network '%s' for ip '%s': %v", + network.CloudProps.NetID, network.IP, err)) + c.logger.Warn("network-service", "checking for conflicting ports now") + + listOpts := ports.ListOpts{ + NetworkID: network.CloudProps.NetID, + FixedIPs: []ports.FixedIPOpts{{IPAddress: network.IP}}, + } + page, err := c.networkingFacade.ListPorts(c.serviceClients.RetryableServiceClient, listOpts) + if err != nil { + return ports.Port{}, fmt.Errorf("failed to list Ports: %w", err) + } + + existingPorts, err := c.networkingFacade.ExtractPorts(page) + if err != nil { + return ports.Port{}, fmt.Errorf("failed to extract ports: %w", err) + } + + for _, port := range existingPorts { + if port.Status == "DOWN" && port.DeviceID == "" && port.DeviceOwner == "" { + c.logger.Warn("network-service", fmt.Sprintf("port on network '%s' for ip '%s' "+ + "is already allocated but unused, deleting conflicting port now.", + network.CloudProps.NetID, network.IP)) + + err := c.networkingFacade.DeletePort(c.serviceClients.RetryableServiceClient, port.ID) + if err != nil { + return ports.Port{}, fmt.Errorf("failed to delete port: %w", err) + } + } + } + + createdPort, err = c.networkingFacade.CreatePort(c.serviceClients.ServiceClient, createOpts) + if err != nil { + return ports.Port{}, fmt.Errorf("failed to recreate port on network '%s' for ip '%s' %w", + network.CloudProps.NetID, network.IP, err) + } + + if createdPort == nil { + return ports.Port{}, fmt.Errorf("failed to create port for network '%s' with ip '%s'. Port must not be nil", + network.CloudProps.NetID, network.IP) + } + + c.logger.Info("network-service", + fmt.Sprintf("recreated port with id '%s' on network '%s' for ip '%s'", + createdPort.ID, network.CloudProps.NetID, network.IP)) + } + + return *createdPort, nil +} + +func (c networkService) GetPorts(instanceId string, defaultNetwork properties.Network, retryable bool) ([]ports.Port, error) { + listOpts := ports.ListOpts{ + DeviceID: instanceId, + } + + if defaultNetwork.CloudProps.NetID != "" { + listOpts.NetworkID = defaultNetwork.CloudProps.NetID + } + + allPages, err := c.networkingFacade.ListPorts(c.serviceClients.RetryableServiceClient, listOpts) + if err != nil { + return []ports.Port{}, fmt.Errorf("failed to list ports: %w", err) + } + + allPorts, err := c.networkingFacade.ExtractPorts(allPages) + if err != nil { + return []ports.Port{}, fmt.Errorf("failed to extract ports: %w", err) + } + + return allPorts, nil +} + +func (c networkService) DeletePorts(ports []ports.Port) error { + var errDefault404 gophercloud.ErrDefault404 + + for _, port := range ports { + err := c.networkingFacade.DeletePort(c.serviceClients.RetryableServiceClient, port.ID) + if err != nil { + if errors.As(err, &errDefault404) { + c.logger.Info("network_service", fmt.Sprintf("SKIPPING: Port deletion with id '%s' is not found", port.ID)) + return nil + } + return fmt.Errorf("failed to delete port: %w", err) + } + c.logger.Info("network_service", fmt.Sprintf("Deleted port with id '%s'", port.ID)) + } + + return nil +} + +func (c networkService) getPortCreationNetworkOpts( + network properties.Network, + securityGroups []string, + cloudProperties properties.CreateVM, +) (ports.CreateOpts, error) { + subnetID, err := c.GetSubnetID(network.CloudProps.NetID, network.IP) + if err != nil { + return ports.CreateOpts{}, fmt.Errorf("failed to get subnet: %w", err) + } + + createOpts := ports.CreateOpts{ + NetworkID: network.CloudProps.NetID, + FixedIPs: []ports.IP{ + {SubnetID: subnetID, IPAddress: network.IP}, + }, + SecurityGroups: &securityGroups, + } + + if cloudProperties.AllowedAddressPairs != "" { + vrrpPortExisting, err := c.isVRRPPortExisting(cloudProperties) + if err != nil { + return ports.CreateOpts{}, fmt.Errorf("VRRP port existence check failed: %w", err) + } + + if !vrrpPortExisting { + return ports.CreateOpts{}, fmt.Errorf("configured VRRP port with ip '%s' does not exist", cloudProperties.AllowedAddressPairs) + } + + createOpts.AllowedAddressPairs = []ports.AddressPair{{IPAddress: cloudProperties.AllowedAddressPairs}} + } + return createOpts, nil +} + +func (c networkService) isVRRPPortExisting(cloudProperties properties.CreateVM) (bool, error) { + vrrpPortCheck := cloudProperties.VRRPPortCheck + if vrrpPortCheck != nil && *vrrpPortCheck { + listOpts := ports.ListOpts{ + FixedIPs: []ports.FixedIPOpts{{IPAddress: cloudProperties.AllowedAddressPairs}}, + } + page, err := c.networkingFacade.ListPorts(c.serviceClients.RetryableServiceClient, listOpts) + if err != nil { + return false, fmt.Errorf("failed to list VRRP ports: %w", err) + } + + vrrpPorts, err := c.networkingFacade.ExtractPorts(page) + if err != nil { + return false, fmt.Errorf("failed to extract VRRP ports: %w", err) + } + + if len(vrrpPorts) == 0 { + return false, nil + } + } + return true, nil +} + +func (c networkService) getFloatingIp(vipNetwork *properties.Network) (floatingips.FloatingIP, error) { + listOpts := floatingips.ListOpts{ + FloatingIP: vipNetwork.IP, + } + + allPages, err := c.networkingFacade.ListFloatingIps(c.serviceClients.RetryableServiceClient, listOpts) + if err != nil { + return floatingips.FloatingIP{}, fmt.Errorf("failed to list floating IPs: %w", err) + } + + allFIPs, err := c.networkingFacade.ExtractFloatingIPs(allPages) + if err != nil { + return floatingips.FloatingIP{}, fmt.Errorf("failed to extract floating IPs: %w", err) + } + + if len(allFIPs) == 0 { + return floatingips.FloatingIP{}, fmt.Errorf("floating IP %s not allocated", vipNetwork.IP) + } + + return allFIPs[0], err +} + +func (c networkService) associateFloatingIp(floatingIpId string, portId string) error { + updateOpts := floatingips.UpdateOpts{ + PortID: &portId, + } + + _, err := c.networkingFacade.UpdateFloatingIP(c.serviceClients.ServiceClient, floatingIpId, updateOpts) + return err +} diff --git a/src/openstack_cpi_golang/cpi/network/network_service_builder.go b/src/openstack_cpi_golang/cpi/network/network_service_builder.go new file mode 100644 index 00000000..1a33fbe5 --- /dev/null +++ b/src/openstack_cpi_golang/cpi/network/network_service_builder.go @@ -0,0 +1,41 @@ +package network + +import ( + "fmt" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/config" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/openstack" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils" +) + +//counterfeiter:generate . NetworkServiceBuilder +type NetworkServiceBuilder interface { + Build() (NetworkService, error) +} + +type networkServiceBuilder struct { + openstackService openstack.OpenstackService + cpiConfig config.CpiConfig + logger utils.Logger +} + +func NewNetworkServiceBuilder(openstackService openstack.OpenstackService, cpiConfig config.CpiConfig, logger utils.Logger) networkServiceBuilder { + return networkServiceBuilder{ + openstackService: openstackService, + cpiConfig: cpiConfig, + logger: logger, + } +} + +func (b networkServiceBuilder) Build() (NetworkService, error) { + serviceClient, err := b.openstackService.NetworkServiceV2(b.cpiConfig.OpenStackConfig()) + if err != nil { + return nil, fmt.Errorf("failed to retrieve network service client: %w", err) + } + + return NewNetworkService( + utils.NewServiceClients(serviceClient, b.cpiConfig, b.logger), + NewNetworkingFacade(), + b.logger, + ), nil +} diff --git a/src/openstack_cpi_golang/cpi/network/network_service_builder_test.go b/src/openstack_cpi_golang/cpi/network/network_service_builder_test.go new file mode 100644 index 00000000..7a1e5b7f --- /dev/null +++ b/src/openstack_cpi_golang/cpi/network/network_service_builder_test.go @@ -0,0 +1,55 @@ +package network_test + +import ( + "errors" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/config" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/network" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/openstack/openstackfakes" + "github.com/gophercloud/gophercloud" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils/utilsfakes" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("NetworkServiceBuilder", func() { + var openstackService openstackfakes.FakeOpenstackService + var logger utilsfakes.FakeLogger + var networkServiceBuilder network.NetworkServiceBuilder + + BeforeEach(func() { + openstackService = openstackfakes.FakeOpenstackService{} + logger = utilsfakes.FakeLogger{} + cpiConfig := config.CpiConfig{} + cpiConfig.Cloud.Properties.RetryConfig = config.RetryConfigMap{} + + networkServiceBuilder = network.NewNetworkServiceBuilder( + &openstackService, + cpiConfig, + &logger, + ) + }) + + Context("CreateNetworkService", func() { + It("returns a network service", func() { + providerClient := gophercloud.ProviderClient{TokenID: "the_token"} + serviceClient := gophercloud.ServiceClient{ProviderClient: &providerClient} + openstackService.NetworkServiceV2Returns(&serviceClient, nil) + + computeService, err := networkServiceBuilder.Build() + + Expect(err).ToNot(HaveOccurred()) + Expect(computeService).To(Not(BeNil())) + }) + + It("returns an error if the compute service client cannot be retrieved", func() { + openstackService.NetworkServiceV2Returns(nil, errors.New("boom")) + + computeService, err := networkServiceBuilder.Build() + + Expect(err.Error()).To(Equal("failed to retrieve network service client: boom")) + Expect(computeService).To(BeNil()) + }) + }) +}) diff --git a/src/openstack_cpi_golang/cpi/network/network_service_test.go b/src/openstack_cpi_golang/cpi/network/network_service_test.go new file mode 100644 index 00000000..22015248 --- /dev/null +++ b/src/openstack_cpi_golang/cpi/network/network_service_test.go @@ -0,0 +1,548 @@ +package network_test + +import ( + "errors" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/mocks" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/network" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/network/networkfakes" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/properties" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils/utilsfakes" + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips" + "github.com/gophercloud/gophercloud/openstack/networking/v2/ports" + "github.com/gophercloud/gophercloud/openstack/networking/v2/subnets" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("NetworkService", func() { + var serviceClient gophercloud.ServiceClient + var retryableServiceClient gophercloud.ServiceClient + var serviceClients utils.ServiceClients + var utilsRetryableServiceClient utils.RetryableServiceClient + var defaultNetwork properties.Network + var networkConfig properties.NetworkConfig + var networkingFacade networkfakes.FakeNetworkingFacade + var logger utilsfakes.FakeLogger + var floatingIpPage mocks.MockPage + var portPage mocks.MockPage + var subnetsPage mocks.MockPage + + BeforeEach(func() { + serviceClient = gophercloud.ServiceClient{} + retryableServiceClient = gophercloud.ServiceClient{} + serviceClients = utils.ServiceClients{ServiceClient: &serviceClient, RetryableServiceClient: &retryableServiceClient} + networkingFacade = networkfakes.FakeNetworkingFacade{} + logger = utilsfakes.FakeLogger{} + floatingIpPage = mocks.MockPage{} + portPage = mocks.MockPage{} + subnetsPage = mocks.MockPage{} + + networkingFacade.ListFloatingIpsReturns(floatingIpPage, nil) + networkingFacade.ExtractFloatingIPsReturns([]floatingips.FloatingIP{{ID: "the_floating_ip_id"}}, nil) + networkingFacade.ListPortsReturns(portPage, nil) + networkingFacade.ExtractPortsReturns([]ports.Port{{ID: "5678"}}, nil) + networkingFacade.ListSubnetsReturns(subnetsPage, nil) + networkingFacade.ExtractSubnetsReturns([]subnets.Subnet{ + {ID: "the-subnet-id-1", CIDR: "1.1.1.0/24"}, {ID: "the-subnet-id-2", CIDR: "1.1.2.0/24"}, + }, nil) + + defaultNetwork = properties.Network{ + IP: "1.1.1.1", + CloudProps: properties.NetworkCloudProps{ + NetID: "the_net_id_1", + }, + } + networkConfig = properties.NetworkConfig{ + DefaultNetwork: defaultNetwork, + VIPNetwork: &properties.Network{IP: "3.3.3.3"}, + SecurityGroups: []string{"sec-id1", "sec-id2"}, + } + }) + + Context("ConfigureVIPNetwork", func() { + It("lists floating ips", func() { + _ = network.NewNetworkService(serviceClients, &networkingFacade, &logger).ConfigureVIPNetwork("123-456", networkConfig) + + _, listOpts := networkingFacade.ListFloatingIpsArgsForCall(0) + Expect(listOpts.FloatingIP).To(Equal("3.3.3.3")) + }) + + It("returns an error if floating ips cannot be fetched from openstack", func() { + networkingFacade.ListFloatingIpsReturns(nil, errors.New("boom")) + + err := network.NewNetworkService(serviceClients, &networkingFacade, &logger).ConfigureVIPNetwork("123-456", networkConfig) + Expect(err.Error()).To(Equal("failed to get floating IP: failed to list floating IPs: boom")) + }) + + It("extracts floating ips", func() { + _ = network.NewNetworkService(serviceClients, &networkingFacade, &logger).ConfigureVIPNetwork("123-456", networkConfig) + + pages := networkingFacade.ExtractFloatingIPsArgsForCall(0) + Expect(pages).To(Equal(floatingIpPage)) + }) + + It("returns an error if floating ips cannot be extracted from pages", func() { + networkingFacade.ExtractFloatingIPsReturns(nil, errors.New("boom")) + + err := network.NewNetworkService(serviceClients, &networkingFacade, &logger).ConfigureVIPNetwork("123-456", networkConfig) + Expect(err.Error()).To(Equal("failed to get floating IP: failed to extract floating IPs: boom")) + }) + + It("returns an error if floating ips are empty", func() { + networkingFacade.ExtractFloatingIPsReturns([]floatingips.FloatingIP{}, nil) + + err := network.NewNetworkService(serviceClients, &networkingFacade, &logger).ConfigureVIPNetwork("123-456", networkConfig) + Expect(err.Error()).To(Equal("failed to get floating IP: floating IP 3.3.3.3 not allocated")) + }) + + It("gets ports", func() { + _ = network.NewNetworkService(serviceClients, &networkingFacade, &logger).ConfigureVIPNetwork("123-456", networkConfig) + + serviceClient, listOpts := networkingFacade.ListPortsArgsForCall(0) + Expect(listOpts.DeviceID).To(Equal("123-456")) + Expect(listOpts.NetworkID).To(Equal("the_net_id_1")) + + Expect(serviceClient).To(BeAssignableToTypeOf(utilsRetryableServiceClient)) + }) + + It("returns an error if getting ports failed", func() { + networkingFacade.ListPortsReturns(nil, errors.New("boom")) + + err := network.NewNetworkService(serviceClients, &networkingFacade, &logger).ConfigureVIPNetwork("123-456", networkConfig) + Expect(err.Error()).To(Equal("failed to get port: failed to list ports: boom")) + }) + + It("returns an error if no ports are allocated", func() { + networkingFacade.ExtractPortsReturns([]ports.Port{}, nil) + + err := network.NewNetworkService(serviceClients, &networkingFacade, &logger).ConfigureVIPNetwork("123-456", networkConfig) + Expect(err.Error()).To(Equal("no port allocated by instance 123-456 and network the_net_id_1")) + }) + + It("associates the floating ip to a port", func() { + _ = network.NewNetworkService(serviceClients, &networkingFacade, &logger).ConfigureVIPNetwork("123-456", networkConfig) + + _, floatingIpId, updateOpts := networkingFacade.UpdateFloatingIPArgsForCall(0) + Expect(floatingIpId).To(Equal("the_floating_ip_id")) + Expect(*updateOpts.PortID).To(Equal("5678")) + }) + + It("returns an error if port association fails", func() { + networkingFacade.UpdateFloatingIPReturns(nil, errors.New("boom")) + + err := network.NewNetworkService(serviceClients, &networkingFacade, &logger).ConfigureVIPNetwork("123-456", networkConfig) + Expect(err.Error()).To(Equal("failed to associate floating ip to port: boom")) + }) + }) + + Context("GetSubnetID", func() { + + It("lists subnets", func() { + _, _ = network.NewNetworkService(serviceClients, &networkingFacade, &logger).GetSubnetID("the-net-id", "1.1.1.1") + + Expect(networkingFacade.ListSubnetsCallCount()).To(Equal(1)) + }) + + It("returns an error if listing subnets fails", func() { + networkingFacade.ListSubnetsReturns(nil, errors.New("boom")) + + _, err := network.NewNetworkService(serviceClients, &networkingFacade, &logger).GetSubnetID("the-net-id", "1.1.1.1") + + Expect(err.Error()).To(Equal("failed to list subnets: boom")) + }) + + It("extracts subnets", func() { + _, _ = network.NewNetworkService(serviceClients, &networkingFacade, &logger).GetSubnetID("the-net-id", "1.1.1.1") + + page := networkingFacade.ExtractSubnetsArgsForCall(0) + + Expect(page).To(Equal(subnetsPage)) + }) + + It("returns an error if extracting subnets fails", func() { + networkingFacade.ExtractSubnetsReturns(nil, errors.New("boom")) + + _, err := network.NewNetworkService(serviceClients, &networkingFacade, &logger).GetSubnetID("the-net-id", "1.1.1.1") + + Expect(err.Error()).To(Equal("failed to extract subnets: boom")) + }) + + It("returns an error if subnets are empty", func() { + networkingFacade.ExtractSubnetsReturns([]subnets.Subnet{}, nil) + + _, err := network.NewNetworkService(serviceClients, &networkingFacade, &logger).GetSubnetID("the-net-id", "1.1.1.1") + + Expect(err.Error()).To(Equal("no subnet found for network 'the-net-id'")) + }) + + It("calculates the matching subnet of the offered IP", func() { + networkingFacade.ExtractSubnetsReturns([]subnets.Subnet{ + {ID: "the-subnet-id-1", CIDR: "1.1.1.0/24"}, {ID: "the-subnet-id-2", CIDR: "1.1.2.0/24"}, + }, nil) + + subnet, err := network.NewNetworkService(serviceClients, &networkingFacade, &logger).GetSubnetID("the-net-id", "1.1.1.1") + + Expect(err).To(Not(HaveOccurred())) + Expect(subnet).To(Equal("the-subnet-id-1")) + }) + + It("returns an error if subnet CIDR cannot be parsed", func() { + networkingFacade.ExtractSubnetsReturns([]subnets.Subnet{ + {ID: "the-subnet-id-1", CIDR: "invalid-cidr"}, + }, nil) + + _, err := network.NewNetworkService(serviceClients, &networkingFacade, &logger).GetSubnetID("the-net-id", "1.1.1.1") + + Expect(err.Error()).To(Equal("failed to parse subnet cidr 'invalid-cidr': invalid CIDR address: invalid-cidr")) + }) + + It("returns an error if multiple subnet CIDRs match the offered IP", func() { + networkingFacade.ExtractSubnetsReturns([]subnets.Subnet{ + {ID: "the-subnet-id-1", CIDR: "1.1.1.0/24"}, {ID: "the-subnet-id-2", CIDR: "1.1.1.0/24"}, + }, nil) + + _, err := network.NewNetworkService(serviceClients, &networkingFacade, &logger).GetSubnetID("the-net-id", "1.1.1.1") + + Expect(err.Error()).To(ContainSubstring("found more than one matching subnet for the ip")) + }) + + It("returns an error if no subnet CIDRs match the offered IP", func() { + networkingFacade.ExtractSubnetsReturns([]subnets.Subnet{ + {ID: "the-subnet-id-1", CIDR: "1.1.1.0/24"}, {ID: "the-subnet-id-2", CIDR: "1.1.1.0/24"}, + }, nil) + + _, err := network.NewNetworkService(serviceClients, &networkingFacade, &logger).GetSubnetID("the-net-id", "2.1.1.1") + + Expect(err.Error()).To(ContainSubstring("no matching subnet found for the ip '2.1.1.1'")) + }) + + It("returns the subnet ID of the matching subnet", func() { + subnetID, err := network.NewNetworkService(serviceClients, &networkingFacade, &logger).GetSubnetID("the-net-id", "1.1.1.1") + + Expect(err).To(Not(HaveOccurred())) + Expect(subnetID).To(Equal("the-subnet-id-1")) + }) + }) + + Context("CreatePort", func() { + + var cloudProperties properties.CreateVM + var createdPort ports.Port + var securityGroups []string + + BeforeEach(func() { + createdPort = ports.Port{ID: "the-port-id"} + networkingFacade.CreatePortReturns(&createdPort, nil) + networkingFacade.ExtractPortsReturns([]ports.Port{createdPort}, nil) + securityGroups = []string{"sec-id1", "sec-id2"} + + vrrpPortCheck := true + cloudProperties = properties.CreateVM{ + AllowedAddressPairs: "allowed-address-pairs", + VRRPPortCheck: &vrrpPortCheck, + } + }) + + It("lists VRRP ports if the port check is enabled", func() { + _, _ = network.NewNetworkService(serviceClients, &networkingFacade, &logger). + CreatePort(defaultNetwork, securityGroups, cloudProperties) + + Expect(networkingFacade.ListPortsCallCount()).To(Equal(1)) + }) + + It("skips listing VRRP ports if the port check is not defined", func() { + cloudProperties := properties.CreateVM{ + AllowedAddressPairs: "allowed-address-pairs", + } + _, _ = network.NewNetworkService(serviceClients, &networkingFacade, &logger). + CreatePort(defaultNetwork, securityGroups, cloudProperties) + + Expect(networkingFacade.ListPortsCallCount()).To(Equal(0)) + }) + + It("skips listing VRRP ports if the port check is false", func() { + cloudProperties := properties.CreateVM{ + AllowedAddressPairs: "allowed-address-pairs", + VRRPPortCheck: new(bool), + } + _, _ = network.NewNetworkService(serviceClients, &networkingFacade, &logger). + CreatePort(defaultNetwork, securityGroups, cloudProperties) + + Expect(networkingFacade.ListPortsCallCount()).To(Equal(0)) + }) + + It("returns an error if listing VRRP ports fails", func() { + networkingFacade.ListPortsReturns(nil, errors.New("boom")) + + _, err := network.NewNetworkService(serviceClients, &networkingFacade, &logger). + CreatePort(defaultNetwork, securityGroups, cloudProperties) + + Expect(err.Error()).To(Equal("failed create network opts: VRRP port existence check failed: " + + "failed to list VRRP ports: boom")) + }) + + It("extracts VRRP ports if the port check is enabled", func() { + _, _ = network.NewNetworkService(serviceClients, &networkingFacade, &logger). + CreatePort(defaultNetwork, securityGroups, cloudProperties) + + Expect(networkingFacade.ExtractPortsCallCount()).To(Equal(1)) + }) + + It("returns an error if extracting VRRP ports fails", func() { + networkingFacade.ExtractPortsReturns(nil, errors.New("boom")) + + _, err := network.NewNetworkService(serviceClients, &networkingFacade, &logger). + CreatePort(defaultNetwork, securityGroups, cloudProperties) + + Expect(err.Error()).To(Equal("failed create network opts: VRRP port existence check failed: " + + "failed to extract VRRP ports: boom")) + }) + + It("returns an error if VRRP ports cannot be found", func() { + networkingFacade.ExtractPortsReturns([]ports.Port{}, nil) + + _, err := network.NewNetworkService(serviceClients, &networkingFacade, &logger). + CreatePort(defaultNetwork, securityGroups, cloudProperties) + + Expect(err.Error()).To(Equal("failed create network opts: " + + "configured VRRP port with ip 'allowed-address-pairs' does not exist")) + }) + + It("creates the port", func() { + cloudProperties = properties.CreateVM{ + VRRPPortCheck: new(bool), + } + + _, _ = network.NewNetworkService(serviceClients, &networkingFacade, &logger). + CreatePort(defaultNetwork, securityGroups, cloudProperties) + + _, createOpts := networkingFacade.CreatePortArgsForCall(0) + + Expect(createOpts.NetworkID).To(ContainSubstring("the_net_id_1")) + Expect(createOpts.FixedIPs.([]ports.IP)[0].SubnetID).To(Equal("the-subnet-id-1")) + Expect(createOpts.FixedIPs.([]ports.IP)[0].IPAddress).To(Equal("1.1.1.1")) + + securityGroups := *createOpts.SecurityGroups + Expect(securityGroups[0]).To(Equal("sec-id1")) + Expect(securityGroups[1]).To(Equal("sec-id2")) + Expect(createOpts.AllowedAddressPairs).To(BeNil()) + + }) + + It("creates the port with VRRP port", func() { + _, _ = network.NewNetworkService(serviceClients, &networkingFacade, &logger). + CreatePort(defaultNetwork, securityGroups, cloudProperties) + + _, createOpts := networkingFacade.CreatePortArgsForCall(0) + + Expect(createOpts.NetworkID).To(ContainSubstring("the_net_id_1")) + Expect(createOpts.FixedIPs.([]ports.IP)[0].IPAddress).To(Equal("1.1.1.1")) + Expect(createOpts.AllowedAddressPairs[0].IPAddress).To(Equal("allowed-address-pairs")) + }) + + It("logs that if initial port creation fails", func() { + networkingFacade.CreatePortReturns(nil, errors.New("boom")) + + _, _ = network.NewNetworkService(serviceClients, &networkingFacade, &logger). + CreatePort(defaultNetwork, securityGroups, cloudProperties) + + tag, msg, _ := logger.WarnArgsForCall(0) + + Expect(tag).To(Equal("network-service")) + Expect(msg).To(ContainSubstring("failed to create port on network 'the_net_id_1' for ip '1.1.1.1': boom")) + }) + + It("lists potentially conflicting ports", func() { + networkingFacade.CreatePortReturns(nil, errors.New("boom")) + + _, _ = network.NewNetworkService(serviceClients, &networkingFacade, &logger). + CreatePort(defaultNetwork, securityGroups, cloudProperties) + + _, listOpts := networkingFacade.ListPortsArgsForCall(1) + + Expect(listOpts.NetworkID).To(Equal("the_net_id_1")) + Expect(listOpts.FixedIPs[0].IPAddress).To(Equal("1.1.1.1")) + }) + + It("returns an error if lists potentially conflicting ports fails", func() { + networkingFacade.CreatePortReturns(nil, errors.New("boom")) + networkingFacade.ListPortsReturnsOnCall(0, nil, errors.New("boom")) + + _, err := network.NewNetworkService(serviceClients, &networkingFacade, &logger). + CreatePort(defaultNetwork, securityGroups, properties.CreateVM{}) + + Expect(err.Error()).To(Equal("failed to list Ports: boom")) + }) + + It("extracts potentially conflicting ports", func() { + networkingFacade.CreatePortReturnsOnCall(0, nil, errors.New("boom")) + + _, _ = network.NewNetworkService(serviceClients, &networkingFacade, &logger). + CreatePort(defaultNetwork, securityGroups, properties.CreateVM{}) + + Expect(networkingFacade.ExtractPortsCallCount()).To(Equal(1)) + }) + + It("returns an error if extracting potentially conflicting ports fails", func() { + networkingFacade.CreatePortReturnsOnCall(0, nil, errors.New("boom")) + networkingFacade.ExtractPortsReturns(nil, errors.New("boom")) + + _, err := network.NewNetworkService(serviceClients, &networkingFacade, &logger). + CreatePort(defaultNetwork, securityGroups, properties.CreateVM{}) + + Expect(err.Error()).To(Equal("failed to extract ports: boom")) + }) + + It("deletes a conflicting ports", func() { + networkingFacade.CreatePortReturnsOnCall(0, nil, errors.New("boom")) + networkingFacade.ExtractPortsReturns( + []ports.Port{ + {ID: "the-port-id-1", Status: "DOWN", FixedIPs: []ports.IP{{IPAddress: "9.9.9.9"}}}, + {ID: "the-port-id-2", Status: "DOWN", FixedIPs: []ports.IP{{IPAddress: "9.9.9.9"}}}, + }, nil) + + _, _ = network.NewNetworkService(serviceClients, &networkingFacade, &logger). + CreatePort(defaultNetwork, securityGroups, properties.CreateVM{}) + + Expect(networkingFacade.DeletePortCallCount()).To(Equal(2)) + _, portID := networkingFacade.DeletePortArgsForCall(0) + Expect(portID).To(Equal("the-port-id-1")) + _, portID = networkingFacade.DeletePortArgsForCall(1) + Expect(portID).To(Equal("the-port-id-2")) + }) + + It("skips deleting conflicting ports, if they are used", func() { + networkingFacade.CreatePortReturnsOnCall(0, nil, errors.New("boom")) + networkingFacade.ExtractPortsReturns( + []ports.Port{ + {ID: "the-port-id-1", Status: "DOWN", FixedIPs: []ports.IP{{IPAddress: "9.9.9.9"}}}, + {ID: "the-port-id-2", Status: "UP", FixedIPs: []ports.IP{{IPAddress: "9.9.9.9"}}}, + }, nil) + + _, _ = network.NewNetworkService(serviceClients, &networkingFacade, &logger). + CreatePort(defaultNetwork, securityGroups, properties.CreateVM{}) + + Expect(networkingFacade.DeletePortCallCount()).To(Equal(1)) + _, portID := networkingFacade.DeletePortArgsForCall(0) + Expect(portID).To(Equal("the-port-id-1")) + }) + + It("retries the port creation", func() { + networkingFacade.CreatePortReturnsOnCall(0, nil, errors.New("boom")) + networkingFacade.ExtractPortsReturns( + []ports.Port{ + {ID: "the-port-id-1", Status: "DOWN", FixedIPs: []ports.IP{{IPAddress: "9.9.9.9"}}}, + }, nil) + + _, _ = network.NewNetworkService(serviceClients, &networkingFacade, &logger). + CreatePort(defaultNetwork, securityGroups, properties.CreateVM{}) + + Expect(networkingFacade.CreatePortCallCount()).To(Equal(2)) + }) + + It("returns an error if the second port creation fails as well", func() { + networkingFacade.CreatePortReturnsOnCall(0, nil, errors.New("boom")) + networkingFacade.CreatePortReturnsOnCall(1, nil, errors.New("boom")) + networkingFacade.ExtractPortsReturns( + []ports.Port{ + {ID: "the-port-id-1", Status: "DOWN", FixedIPs: []ports.IP{{IPAddress: "1.1.1.1"}}}, + }, nil) + + _, err := network.NewNetworkService(serviceClients, &networkingFacade, &logger). + CreatePort(defaultNetwork, securityGroups, properties.CreateVM{}) + + Expect(err.Error()).To(Equal("failed to recreate port on network 'the_net_id_1' for ip '1.1.1.1' boom")) + }) + + It("returns the created port", func() { + port, err := network.NewNetworkService(serviceClients, &networkingFacade, &logger). + CreatePort(defaultNetwork, securityGroups, properties.CreateVM{}) + + Expect(err).To(Not(HaveOccurred())) + Expect(port.ID).To(Equal(createdPort.ID)) + }) + }) + + Context("GetPorts", func() { + + It("serviceClient is retryable", func() { + _, _ = network.NewNetworkService(serviceClients, &networkingFacade, &logger).GetPorts("123-456", networkConfig.DefaultNetwork, true) + + serviceClient, _ := networkingFacade.ListPortsArgsForCall(0) + + Expect(serviceClient).To(BeAssignableToTypeOf(utilsRetryableServiceClient)) + }) + + It("serviceClient is not retryable", func() { + _, _ = network.NewNetworkService(serviceClients, &networkingFacade, &logger).GetPorts("123-456", networkConfig.DefaultNetwork, false) + + serviceClient, _ := networkingFacade.ListPortsArgsForCall(0) + + Expect(serviceClient).To(BeAssignableToTypeOf(utilsRetryableServiceClient)) + }) + + It("lists ports", func() { + _, _ = network.NewNetworkService(serviceClients, &networkingFacade, &logger).GetPorts("123-456", networkConfig.DefaultNetwork, false) + + _, listOpts := networkingFacade.ListPortsArgsForCall(0) + Expect(listOpts.DeviceID).To(Equal("123-456")) + Expect(listOpts.NetworkID).To(Equal("the_net_id_1")) + }) + + It("returns an error if port listing fails", func() { + networkingFacade.ListPortsReturns(nil, errors.New("boom")) + + _, err := network.NewNetworkService(serviceClients, &networkingFacade, &logger).GetPorts("123-456", networkConfig.DefaultNetwork, false) + Expect(err.Error()).To(Equal("failed to list ports: boom")) + }) + + It("extracts ports", func() { + _, _ = network.NewNetworkService(serviceClients, &networkingFacade, &logger).GetPorts("123-456", networkConfig.DefaultNetwork, false) + + pages := networkingFacade.ExtractPortsArgsForCall(0) + Expect(pages).To(Equal(portPage)) + }) + + It("returns an error if ports cannot be extracted from pages", func() { + networkingFacade.ExtractPortsReturns(nil, errors.New("boom")) + + _, err := network.NewNetworkService(serviceClients, &networkingFacade, &logger).GetPorts("123-456", networkConfig.DefaultNetwork, false) + Expect(err.Error()).To(Equal("failed to extract ports: boom")) + }) + }) + + Context("DeletePorts", func() { + + var ports = []ports.Port{{ID: "test"}} + + It("serviceClient is retryable", func() { + _ = network.NewNetworkService(serviceClients, &networkingFacade, &logger).DeletePorts(ports) + serviceClient, _ := networkingFacade.DeletePortArgsForCall(0) + + Expect(serviceClient).To(BeAssignableToTypeOf(utilsRetryableServiceClient)) + }) + + It("deletes the port with the correct ID", func() { + _ = network.NewNetworkService(serviceClients, &networkingFacade, &logger).DeletePorts(ports) + _, act := networkingFacade.DeletePortArgsForCall(0) + + Expect(act).To(Equal("test")) + }) + + It("returns an error while deleting a port", func() { + networkingFacade.DeletePortReturns(errors.New("boom")) + + err := network.NewNetworkService(serviceClients, &networkingFacade, &logger).DeletePorts(ports) + Expect(err.Error()).To(Equal("failed to delete port: boom")) + }) + + It("returns nil, ports were deleted", func() { + err := network.NewNetworkService(serviceClients, &networkingFacade, &logger).DeletePorts(ports) + + Expect(err).ToNot(HaveOccurred()) + }) + }) +}) diff --git a/src/openstack_cpi_golang/cpi/network/network_suite_test.go b/src/openstack_cpi_golang/cpi/network/network_suite_test.go new file mode 100644 index 00000000..c4b42a51 --- /dev/null +++ b/src/openstack_cpi_golang/cpi/network/network_suite_test.go @@ -0,0 +1,13 @@ +package network_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestMethods(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Network Suite") +} diff --git a/src/openstack_cpi_golang/cpi/network/networkfakes/fake_network_service.go b/src/openstack_cpi_golang/cpi/network/networkfakes/fake_network_service.go new file mode 100644 index 00000000..8aba8b14 --- /dev/null +++ b/src/openstack_cpi_golang/cpi/network/networkfakes/fake_network_service.go @@ -0,0 +1,531 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package networkfakes + +import ( + "sync" + + "github.com/cloudfoundry/bosh-cpi-go/apiv1" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/config" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/network" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/properties" + "github.com/gophercloud/gophercloud/openstack/networking/v2/ports" +) + +type FakeNetworkService struct { + ConfigureVIPNetworkStub func(string, properties.NetworkConfig) error + configureVIPNetworkMutex sync.RWMutex + configureVIPNetworkArgsForCall []struct { + arg1 string + arg2 properties.NetworkConfig + } + configureVIPNetworkReturns struct { + result1 error + } + configureVIPNetworkReturnsOnCall map[int]struct { + result1 error + } + CreatePortStub func(properties.Network, []string, properties.CreateVM) (ports.Port, error) + createPortMutex sync.RWMutex + createPortArgsForCall []struct { + arg1 properties.Network + arg2 []string + arg3 properties.CreateVM + } + createPortReturns struct { + result1 ports.Port + result2 error + } + createPortReturnsOnCall map[int]struct { + result1 ports.Port + result2 error + } + DeletePortsStub func([]ports.Port) error + deletePortsMutex sync.RWMutex + deletePortsArgsForCall []struct { + arg1 []ports.Port + } + deletePortsReturns struct { + result1 error + } + deletePortsReturnsOnCall map[int]struct { + result1 error + } + GetNetworkConfigurationStub func(apiv1.Networks, config.OpenstackConfig, properties.CreateVM) (properties.NetworkConfig, error) + getNetworkConfigurationMutex sync.RWMutex + getNetworkConfigurationArgsForCall []struct { + arg1 apiv1.Networks + arg2 config.OpenstackConfig + arg3 properties.CreateVM + } + getNetworkConfigurationReturns struct { + result1 properties.NetworkConfig + result2 error + } + getNetworkConfigurationReturnsOnCall map[int]struct { + result1 properties.NetworkConfig + result2 error + } + GetPortsStub func(string, properties.Network, bool) ([]ports.Port, error) + getPortsMutex sync.RWMutex + getPortsArgsForCall []struct { + arg1 string + arg2 properties.Network + arg3 bool + } + getPortsReturns struct { + result1 []ports.Port + result2 error + } + getPortsReturnsOnCall map[int]struct { + result1 []ports.Port + result2 error + } + GetSubnetIDStub func(string, string) (string, error) + getSubnetIDMutex sync.RWMutex + getSubnetIDArgsForCall []struct { + arg1 string + arg2 string + } + getSubnetIDReturns struct { + result1 string + result2 error + } + getSubnetIDReturnsOnCall map[int]struct { + result1 string + result2 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *FakeNetworkService) ConfigureVIPNetwork(arg1 string, arg2 properties.NetworkConfig) error { + fake.configureVIPNetworkMutex.Lock() + ret, specificReturn := fake.configureVIPNetworkReturnsOnCall[len(fake.configureVIPNetworkArgsForCall)] + fake.configureVIPNetworkArgsForCall = append(fake.configureVIPNetworkArgsForCall, struct { + arg1 string + arg2 properties.NetworkConfig + }{arg1, arg2}) + stub := fake.ConfigureVIPNetworkStub + fakeReturns := fake.configureVIPNetworkReturns + fake.recordInvocation("ConfigureVIPNetwork", []interface{}{arg1, arg2}) + fake.configureVIPNetworkMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeNetworkService) ConfigureVIPNetworkCallCount() int { + fake.configureVIPNetworkMutex.RLock() + defer fake.configureVIPNetworkMutex.RUnlock() + return len(fake.configureVIPNetworkArgsForCall) +} + +func (fake *FakeNetworkService) ConfigureVIPNetworkCalls(stub func(string, properties.NetworkConfig) error) { + fake.configureVIPNetworkMutex.Lock() + defer fake.configureVIPNetworkMutex.Unlock() + fake.ConfigureVIPNetworkStub = stub +} + +func (fake *FakeNetworkService) ConfigureVIPNetworkArgsForCall(i int) (string, properties.NetworkConfig) { + fake.configureVIPNetworkMutex.RLock() + defer fake.configureVIPNetworkMutex.RUnlock() + argsForCall := fake.configureVIPNetworkArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeNetworkService) ConfigureVIPNetworkReturns(result1 error) { + fake.configureVIPNetworkMutex.Lock() + defer fake.configureVIPNetworkMutex.Unlock() + fake.ConfigureVIPNetworkStub = nil + fake.configureVIPNetworkReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeNetworkService) ConfigureVIPNetworkReturnsOnCall(i int, result1 error) { + fake.configureVIPNetworkMutex.Lock() + defer fake.configureVIPNetworkMutex.Unlock() + fake.ConfigureVIPNetworkStub = nil + if fake.configureVIPNetworkReturnsOnCall == nil { + fake.configureVIPNetworkReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.configureVIPNetworkReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *FakeNetworkService) CreatePort(arg1 properties.Network, arg2 []string, arg3 properties.CreateVM) (ports.Port, error) { + var arg2Copy []string + if arg2 != nil { + arg2Copy = make([]string, len(arg2)) + copy(arg2Copy, arg2) + } + fake.createPortMutex.Lock() + ret, specificReturn := fake.createPortReturnsOnCall[len(fake.createPortArgsForCall)] + fake.createPortArgsForCall = append(fake.createPortArgsForCall, struct { + arg1 properties.Network + arg2 []string + arg3 properties.CreateVM + }{arg1, arg2Copy, arg3}) + stub := fake.CreatePortStub + fakeReturns := fake.createPortReturns + fake.recordInvocation("CreatePort", []interface{}{arg1, arg2Copy, arg3}) + fake.createPortMutex.Unlock() + if stub != nil { + return stub(arg1, arg2, arg3) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeNetworkService) CreatePortCallCount() int { + fake.createPortMutex.RLock() + defer fake.createPortMutex.RUnlock() + return len(fake.createPortArgsForCall) +} + +func (fake *FakeNetworkService) CreatePortCalls(stub func(properties.Network, []string, properties.CreateVM) (ports.Port, error)) { + fake.createPortMutex.Lock() + defer fake.createPortMutex.Unlock() + fake.CreatePortStub = stub +} + +func (fake *FakeNetworkService) CreatePortArgsForCall(i int) (properties.Network, []string, properties.CreateVM) { + fake.createPortMutex.RLock() + defer fake.createPortMutex.RUnlock() + argsForCall := fake.createPortArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 +} + +func (fake *FakeNetworkService) CreatePortReturns(result1 ports.Port, result2 error) { + fake.createPortMutex.Lock() + defer fake.createPortMutex.Unlock() + fake.CreatePortStub = nil + fake.createPortReturns = struct { + result1 ports.Port + result2 error + }{result1, result2} +} + +func (fake *FakeNetworkService) CreatePortReturnsOnCall(i int, result1 ports.Port, result2 error) { + fake.createPortMutex.Lock() + defer fake.createPortMutex.Unlock() + fake.CreatePortStub = nil + if fake.createPortReturnsOnCall == nil { + fake.createPortReturnsOnCall = make(map[int]struct { + result1 ports.Port + result2 error + }) + } + fake.createPortReturnsOnCall[i] = struct { + result1 ports.Port + result2 error + }{result1, result2} +} + +func (fake *FakeNetworkService) DeletePorts(arg1 []ports.Port) error { + var arg1Copy []ports.Port + if arg1 != nil { + arg1Copy = make([]ports.Port, len(arg1)) + copy(arg1Copy, arg1) + } + fake.deletePortsMutex.Lock() + ret, specificReturn := fake.deletePortsReturnsOnCall[len(fake.deletePortsArgsForCall)] + fake.deletePortsArgsForCall = append(fake.deletePortsArgsForCall, struct { + arg1 []ports.Port + }{arg1Copy}) + stub := fake.DeletePortsStub + fakeReturns := fake.deletePortsReturns + fake.recordInvocation("DeletePorts", []interface{}{arg1Copy}) + fake.deletePortsMutex.Unlock() + if stub != nil { + return stub(arg1) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeNetworkService) DeletePortsCallCount() int { + fake.deletePortsMutex.RLock() + defer fake.deletePortsMutex.RUnlock() + return len(fake.deletePortsArgsForCall) +} + +func (fake *FakeNetworkService) DeletePortsCalls(stub func([]ports.Port) error) { + fake.deletePortsMutex.Lock() + defer fake.deletePortsMutex.Unlock() + fake.DeletePortsStub = stub +} + +func (fake *FakeNetworkService) DeletePortsArgsForCall(i int) []ports.Port { + fake.deletePortsMutex.RLock() + defer fake.deletePortsMutex.RUnlock() + argsForCall := fake.deletePortsArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeNetworkService) DeletePortsReturns(result1 error) { + fake.deletePortsMutex.Lock() + defer fake.deletePortsMutex.Unlock() + fake.DeletePortsStub = nil + fake.deletePortsReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeNetworkService) DeletePortsReturnsOnCall(i int, result1 error) { + fake.deletePortsMutex.Lock() + defer fake.deletePortsMutex.Unlock() + fake.DeletePortsStub = nil + if fake.deletePortsReturnsOnCall == nil { + fake.deletePortsReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.deletePortsReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *FakeNetworkService) GetNetworkConfiguration(arg1 apiv1.Networks, arg2 config.OpenstackConfig, arg3 properties.CreateVM) (properties.NetworkConfig, error) { + fake.getNetworkConfigurationMutex.Lock() + ret, specificReturn := fake.getNetworkConfigurationReturnsOnCall[len(fake.getNetworkConfigurationArgsForCall)] + fake.getNetworkConfigurationArgsForCall = append(fake.getNetworkConfigurationArgsForCall, struct { + arg1 apiv1.Networks + arg2 config.OpenstackConfig + arg3 properties.CreateVM + }{arg1, arg2, arg3}) + stub := fake.GetNetworkConfigurationStub + fakeReturns := fake.getNetworkConfigurationReturns + fake.recordInvocation("GetNetworkConfiguration", []interface{}{arg1, arg2, arg3}) + fake.getNetworkConfigurationMutex.Unlock() + if stub != nil { + return stub(arg1, arg2, arg3) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeNetworkService) GetNetworkConfigurationCallCount() int { + fake.getNetworkConfigurationMutex.RLock() + defer fake.getNetworkConfigurationMutex.RUnlock() + return len(fake.getNetworkConfigurationArgsForCall) +} + +func (fake *FakeNetworkService) GetNetworkConfigurationCalls(stub func(apiv1.Networks, config.OpenstackConfig, properties.CreateVM) (properties.NetworkConfig, error)) { + fake.getNetworkConfigurationMutex.Lock() + defer fake.getNetworkConfigurationMutex.Unlock() + fake.GetNetworkConfigurationStub = stub +} + +func (fake *FakeNetworkService) GetNetworkConfigurationArgsForCall(i int) (apiv1.Networks, config.OpenstackConfig, properties.CreateVM) { + fake.getNetworkConfigurationMutex.RLock() + defer fake.getNetworkConfigurationMutex.RUnlock() + argsForCall := fake.getNetworkConfigurationArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 +} + +func (fake *FakeNetworkService) GetNetworkConfigurationReturns(result1 properties.NetworkConfig, result2 error) { + fake.getNetworkConfigurationMutex.Lock() + defer fake.getNetworkConfigurationMutex.Unlock() + fake.GetNetworkConfigurationStub = nil + fake.getNetworkConfigurationReturns = struct { + result1 properties.NetworkConfig + result2 error + }{result1, result2} +} + +func (fake *FakeNetworkService) GetNetworkConfigurationReturnsOnCall(i int, result1 properties.NetworkConfig, result2 error) { + fake.getNetworkConfigurationMutex.Lock() + defer fake.getNetworkConfigurationMutex.Unlock() + fake.GetNetworkConfigurationStub = nil + if fake.getNetworkConfigurationReturnsOnCall == nil { + fake.getNetworkConfigurationReturnsOnCall = make(map[int]struct { + result1 properties.NetworkConfig + result2 error + }) + } + fake.getNetworkConfigurationReturnsOnCall[i] = struct { + result1 properties.NetworkConfig + result2 error + }{result1, result2} +} + +func (fake *FakeNetworkService) GetPorts(arg1 string, arg2 properties.Network, arg3 bool) ([]ports.Port, error) { + fake.getPortsMutex.Lock() + ret, specificReturn := fake.getPortsReturnsOnCall[len(fake.getPortsArgsForCall)] + fake.getPortsArgsForCall = append(fake.getPortsArgsForCall, struct { + arg1 string + arg2 properties.Network + arg3 bool + }{arg1, arg2, arg3}) + stub := fake.GetPortsStub + fakeReturns := fake.getPortsReturns + fake.recordInvocation("GetPorts", []interface{}{arg1, arg2, arg3}) + fake.getPortsMutex.Unlock() + if stub != nil { + return stub(arg1, arg2, arg3) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeNetworkService) GetPortsCallCount() int { + fake.getPortsMutex.RLock() + defer fake.getPortsMutex.RUnlock() + return len(fake.getPortsArgsForCall) +} + +func (fake *FakeNetworkService) GetPortsCalls(stub func(string, properties.Network, bool) ([]ports.Port, error)) { + fake.getPortsMutex.Lock() + defer fake.getPortsMutex.Unlock() + fake.GetPortsStub = stub +} + +func (fake *FakeNetworkService) GetPortsArgsForCall(i int) (string, properties.Network, bool) { + fake.getPortsMutex.RLock() + defer fake.getPortsMutex.RUnlock() + argsForCall := fake.getPortsArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 +} + +func (fake *FakeNetworkService) GetPortsReturns(result1 []ports.Port, result2 error) { + fake.getPortsMutex.Lock() + defer fake.getPortsMutex.Unlock() + fake.GetPortsStub = nil + fake.getPortsReturns = struct { + result1 []ports.Port + result2 error + }{result1, result2} +} + +func (fake *FakeNetworkService) GetPortsReturnsOnCall(i int, result1 []ports.Port, result2 error) { + fake.getPortsMutex.Lock() + defer fake.getPortsMutex.Unlock() + fake.GetPortsStub = nil + if fake.getPortsReturnsOnCall == nil { + fake.getPortsReturnsOnCall = make(map[int]struct { + result1 []ports.Port + result2 error + }) + } + fake.getPortsReturnsOnCall[i] = struct { + result1 []ports.Port + result2 error + }{result1, result2} +} + +func (fake *FakeNetworkService) GetSubnetID(arg1 string, arg2 string) (string, error) { + fake.getSubnetIDMutex.Lock() + ret, specificReturn := fake.getSubnetIDReturnsOnCall[len(fake.getSubnetIDArgsForCall)] + fake.getSubnetIDArgsForCall = append(fake.getSubnetIDArgsForCall, struct { + arg1 string + arg2 string + }{arg1, arg2}) + stub := fake.GetSubnetIDStub + fakeReturns := fake.getSubnetIDReturns + fake.recordInvocation("GetSubnetID", []interface{}{arg1, arg2}) + fake.getSubnetIDMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeNetworkService) GetSubnetIDCallCount() int { + fake.getSubnetIDMutex.RLock() + defer fake.getSubnetIDMutex.RUnlock() + return len(fake.getSubnetIDArgsForCall) +} + +func (fake *FakeNetworkService) GetSubnetIDCalls(stub func(string, string) (string, error)) { + fake.getSubnetIDMutex.Lock() + defer fake.getSubnetIDMutex.Unlock() + fake.GetSubnetIDStub = stub +} + +func (fake *FakeNetworkService) GetSubnetIDArgsForCall(i int) (string, string) { + fake.getSubnetIDMutex.RLock() + defer fake.getSubnetIDMutex.RUnlock() + argsForCall := fake.getSubnetIDArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeNetworkService) GetSubnetIDReturns(result1 string, result2 error) { + fake.getSubnetIDMutex.Lock() + defer fake.getSubnetIDMutex.Unlock() + fake.GetSubnetIDStub = nil + fake.getSubnetIDReturns = struct { + result1 string + result2 error + }{result1, result2} +} + +func (fake *FakeNetworkService) GetSubnetIDReturnsOnCall(i int, result1 string, result2 error) { + fake.getSubnetIDMutex.Lock() + defer fake.getSubnetIDMutex.Unlock() + fake.GetSubnetIDStub = nil + if fake.getSubnetIDReturnsOnCall == nil { + fake.getSubnetIDReturnsOnCall = make(map[int]struct { + result1 string + result2 error + }) + } + fake.getSubnetIDReturnsOnCall[i] = struct { + result1 string + result2 error + }{result1, result2} +} + +func (fake *FakeNetworkService) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.configureVIPNetworkMutex.RLock() + defer fake.configureVIPNetworkMutex.RUnlock() + fake.createPortMutex.RLock() + defer fake.createPortMutex.RUnlock() + fake.deletePortsMutex.RLock() + defer fake.deletePortsMutex.RUnlock() + fake.getNetworkConfigurationMutex.RLock() + defer fake.getNetworkConfigurationMutex.RUnlock() + fake.getPortsMutex.RLock() + defer fake.getPortsMutex.RUnlock() + fake.getSubnetIDMutex.RLock() + defer fake.getSubnetIDMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *FakeNetworkService) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ network.NetworkService = new(FakeNetworkService) diff --git a/src/openstack_cpi_golang/cpi/network/networkfakes/fake_network_service_builder.go b/src/openstack_cpi_golang/cpi/network/networkfakes/fake_network_service_builder.go new file mode 100644 index 00000000..677dd63d --- /dev/null +++ b/src/openstack_cpi_golang/cpi/network/networkfakes/fake_network_service_builder.go @@ -0,0 +1,107 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package networkfakes + +import ( + "sync" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/network" +) + +type FakeNetworkServiceBuilder struct { + BuildStub func() (network.NetworkService, error) + buildMutex sync.RWMutex + buildArgsForCall []struct { + } + buildReturns struct { + result1 network.NetworkService + result2 error + } + buildReturnsOnCall map[int]struct { + result1 network.NetworkService + result2 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *FakeNetworkServiceBuilder) Build() (network.NetworkService, error) { + fake.buildMutex.Lock() + ret, specificReturn := fake.buildReturnsOnCall[len(fake.buildArgsForCall)] + fake.buildArgsForCall = append(fake.buildArgsForCall, struct { + }{}) + stub := fake.BuildStub + fakeReturns := fake.buildReturns + fake.recordInvocation("Build", []interface{}{}) + fake.buildMutex.Unlock() + if stub != nil { + return stub() + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeNetworkServiceBuilder) BuildCallCount() int { + fake.buildMutex.RLock() + defer fake.buildMutex.RUnlock() + return len(fake.buildArgsForCall) +} + +func (fake *FakeNetworkServiceBuilder) BuildCalls(stub func() (network.NetworkService, error)) { + fake.buildMutex.Lock() + defer fake.buildMutex.Unlock() + fake.BuildStub = stub +} + +func (fake *FakeNetworkServiceBuilder) BuildReturns(result1 network.NetworkService, result2 error) { + fake.buildMutex.Lock() + defer fake.buildMutex.Unlock() + fake.BuildStub = nil + fake.buildReturns = struct { + result1 network.NetworkService + result2 error + }{result1, result2} +} + +func (fake *FakeNetworkServiceBuilder) BuildReturnsOnCall(i int, result1 network.NetworkService, result2 error) { + fake.buildMutex.Lock() + defer fake.buildMutex.Unlock() + fake.BuildStub = nil + if fake.buildReturnsOnCall == nil { + fake.buildReturnsOnCall = make(map[int]struct { + result1 network.NetworkService + result2 error + }) + } + fake.buildReturnsOnCall[i] = struct { + result1 network.NetworkService + result2 error + }{result1, result2} +} + +func (fake *FakeNetworkServiceBuilder) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.buildMutex.RLock() + defer fake.buildMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *FakeNetworkServiceBuilder) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ network.NetworkServiceBuilder = new(FakeNetworkServiceBuilder) diff --git a/src/openstack_cpi_golang/cpi/network/networkfakes/fake_networking_facade.go b/src/openstack_cpi_golang/cpi/network/networkfakes/fake_networking_facade.go new file mode 100644 index 00000000..137e7ddd --- /dev/null +++ b/src/openstack_cpi_golang/cpi/network/networkfakes/fake_networking_facade.go @@ -0,0 +1,1004 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package networkfakes + +import ( + "sync" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/network" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/groups" + "github.com/gophercloud/gophercloud/openstack/networking/v2/ports" + "github.com/gophercloud/gophercloud/openstack/networking/v2/subnets" + "github.com/gophercloud/gophercloud/pagination" +) + +type FakeNetworkingFacade struct { + CreatePortStub func(utils.ServiceClient, ports.CreateOpts) (*ports.Port, error) + createPortMutex sync.RWMutex + createPortArgsForCall []struct { + arg1 utils.ServiceClient + arg2 ports.CreateOpts + } + createPortReturns struct { + result1 *ports.Port + result2 error + } + createPortReturnsOnCall map[int]struct { + result1 *ports.Port + result2 error + } + DeletePortStub func(utils.RetryableServiceClient, string) error + deletePortMutex sync.RWMutex + deletePortArgsForCall []struct { + arg1 utils.RetryableServiceClient + arg2 string + } + deletePortReturns struct { + result1 error + } + deletePortReturnsOnCall map[int]struct { + result1 error + } + ExtractFloatingIPsStub func(pagination.Page) ([]floatingips.FloatingIP, error) + extractFloatingIPsMutex sync.RWMutex + extractFloatingIPsArgsForCall []struct { + arg1 pagination.Page + } + extractFloatingIPsReturns struct { + result1 []floatingips.FloatingIP + result2 error + } + extractFloatingIPsReturnsOnCall map[int]struct { + result1 []floatingips.FloatingIP + result2 error + } + ExtractPortsStub func(pagination.Page) ([]ports.Port, error) + extractPortsMutex sync.RWMutex + extractPortsArgsForCall []struct { + arg1 pagination.Page + } + extractPortsReturns struct { + result1 []ports.Port + result2 error + } + extractPortsReturnsOnCall map[int]struct { + result1 []ports.Port + result2 error + } + ExtractSecurityGroupsStub func(pagination.Page) ([]groups.SecGroup, error) + extractSecurityGroupsMutex sync.RWMutex + extractSecurityGroupsArgsForCall []struct { + arg1 pagination.Page + } + extractSecurityGroupsReturns struct { + result1 []groups.SecGroup + result2 error + } + extractSecurityGroupsReturnsOnCall map[int]struct { + result1 []groups.SecGroup + result2 error + } + ExtractSubnetsStub func(pagination.Page) ([]subnets.Subnet, error) + extractSubnetsMutex sync.RWMutex + extractSubnetsArgsForCall []struct { + arg1 pagination.Page + } + extractSubnetsReturns struct { + result1 []subnets.Subnet + result2 error + } + extractSubnetsReturnsOnCall map[int]struct { + result1 []subnets.Subnet + result2 error + } + GetSecurityGroupsStub func(utils.RetryableServiceClient, string) (*groups.SecGroup, error) + getSecurityGroupsMutex sync.RWMutex + getSecurityGroupsArgsForCall []struct { + arg1 utils.RetryableServiceClient + arg2 string + } + getSecurityGroupsReturns struct { + result1 *groups.SecGroup + result2 error + } + getSecurityGroupsReturnsOnCall map[int]struct { + result1 *groups.SecGroup + result2 error + } + ListFloatingIpsStub func(utils.RetryableServiceClient, floatingips.ListOpts) (pagination.Page, error) + listFloatingIpsMutex sync.RWMutex + listFloatingIpsArgsForCall []struct { + arg1 utils.RetryableServiceClient + arg2 floatingips.ListOpts + } + listFloatingIpsReturns struct { + result1 pagination.Page + result2 error + } + listFloatingIpsReturnsOnCall map[int]struct { + result1 pagination.Page + result2 error + } + ListPortsStub func(utils.RetryableServiceClient, ports.ListOpts) (pagination.Page, error) + listPortsMutex sync.RWMutex + listPortsArgsForCall []struct { + arg1 utils.RetryableServiceClient + arg2 ports.ListOpts + } + listPortsReturns struct { + result1 pagination.Page + result2 error + } + listPortsReturnsOnCall map[int]struct { + result1 pagination.Page + result2 error + } + ListSecurityGroupsStub func(utils.RetryableServiceClient, groups.ListOpts) (pagination.Page, error) + listSecurityGroupsMutex sync.RWMutex + listSecurityGroupsArgsForCall []struct { + arg1 utils.RetryableServiceClient + arg2 groups.ListOpts + } + listSecurityGroupsReturns struct { + result1 pagination.Page + result2 error + } + listSecurityGroupsReturnsOnCall map[int]struct { + result1 pagination.Page + result2 error + } + ListSubnetsStub func(utils.RetryableServiceClient, subnets.ListOpts) (pagination.Page, error) + listSubnetsMutex sync.RWMutex + listSubnetsArgsForCall []struct { + arg1 utils.RetryableServiceClient + arg2 subnets.ListOpts + } + listSubnetsReturns struct { + result1 pagination.Page + result2 error + } + listSubnetsReturnsOnCall map[int]struct { + result1 pagination.Page + result2 error + } + UpdateFloatingIPStub func(utils.ServiceClient, string, floatingips.UpdateOpts) (*floatingips.FloatingIP, error) + updateFloatingIPMutex sync.RWMutex + updateFloatingIPArgsForCall []struct { + arg1 utils.ServiceClient + arg2 string + arg3 floatingips.UpdateOpts + } + updateFloatingIPReturns struct { + result1 *floatingips.FloatingIP + result2 error + } + updateFloatingIPReturnsOnCall map[int]struct { + result1 *floatingips.FloatingIP + result2 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *FakeNetworkingFacade) CreatePort(arg1 utils.ServiceClient, arg2 ports.CreateOpts) (*ports.Port, error) { + fake.createPortMutex.Lock() + ret, specificReturn := fake.createPortReturnsOnCall[len(fake.createPortArgsForCall)] + fake.createPortArgsForCall = append(fake.createPortArgsForCall, struct { + arg1 utils.ServiceClient + arg2 ports.CreateOpts + }{arg1, arg2}) + stub := fake.CreatePortStub + fakeReturns := fake.createPortReturns + fake.recordInvocation("CreatePort", []interface{}{arg1, arg2}) + fake.createPortMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeNetworkingFacade) CreatePortCallCount() int { + fake.createPortMutex.RLock() + defer fake.createPortMutex.RUnlock() + return len(fake.createPortArgsForCall) +} + +func (fake *FakeNetworkingFacade) CreatePortCalls(stub func(utils.ServiceClient, ports.CreateOpts) (*ports.Port, error)) { + fake.createPortMutex.Lock() + defer fake.createPortMutex.Unlock() + fake.CreatePortStub = stub +} + +func (fake *FakeNetworkingFacade) CreatePortArgsForCall(i int) (utils.ServiceClient, ports.CreateOpts) { + fake.createPortMutex.RLock() + defer fake.createPortMutex.RUnlock() + argsForCall := fake.createPortArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeNetworkingFacade) CreatePortReturns(result1 *ports.Port, result2 error) { + fake.createPortMutex.Lock() + defer fake.createPortMutex.Unlock() + fake.CreatePortStub = nil + fake.createPortReturns = struct { + result1 *ports.Port + result2 error + }{result1, result2} +} + +func (fake *FakeNetworkingFacade) CreatePortReturnsOnCall(i int, result1 *ports.Port, result2 error) { + fake.createPortMutex.Lock() + defer fake.createPortMutex.Unlock() + fake.CreatePortStub = nil + if fake.createPortReturnsOnCall == nil { + fake.createPortReturnsOnCall = make(map[int]struct { + result1 *ports.Port + result2 error + }) + } + fake.createPortReturnsOnCall[i] = struct { + result1 *ports.Port + result2 error + }{result1, result2} +} + +func (fake *FakeNetworkingFacade) DeletePort(arg1 utils.RetryableServiceClient, arg2 string) error { + fake.deletePortMutex.Lock() + ret, specificReturn := fake.deletePortReturnsOnCall[len(fake.deletePortArgsForCall)] + fake.deletePortArgsForCall = append(fake.deletePortArgsForCall, struct { + arg1 utils.RetryableServiceClient + arg2 string + }{arg1, arg2}) + stub := fake.DeletePortStub + fakeReturns := fake.deletePortReturns + fake.recordInvocation("DeletePort", []interface{}{arg1, arg2}) + fake.deletePortMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeNetworkingFacade) DeletePortCallCount() int { + fake.deletePortMutex.RLock() + defer fake.deletePortMutex.RUnlock() + return len(fake.deletePortArgsForCall) +} + +func (fake *FakeNetworkingFacade) DeletePortCalls(stub func(utils.RetryableServiceClient, string) error) { + fake.deletePortMutex.Lock() + defer fake.deletePortMutex.Unlock() + fake.DeletePortStub = stub +} + +func (fake *FakeNetworkingFacade) DeletePortArgsForCall(i int) (utils.RetryableServiceClient, string) { + fake.deletePortMutex.RLock() + defer fake.deletePortMutex.RUnlock() + argsForCall := fake.deletePortArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeNetworkingFacade) DeletePortReturns(result1 error) { + fake.deletePortMutex.Lock() + defer fake.deletePortMutex.Unlock() + fake.DeletePortStub = nil + fake.deletePortReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeNetworkingFacade) DeletePortReturnsOnCall(i int, result1 error) { + fake.deletePortMutex.Lock() + defer fake.deletePortMutex.Unlock() + fake.DeletePortStub = nil + if fake.deletePortReturnsOnCall == nil { + fake.deletePortReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.deletePortReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *FakeNetworkingFacade) ExtractFloatingIPs(arg1 pagination.Page) ([]floatingips.FloatingIP, error) { + fake.extractFloatingIPsMutex.Lock() + ret, specificReturn := fake.extractFloatingIPsReturnsOnCall[len(fake.extractFloatingIPsArgsForCall)] + fake.extractFloatingIPsArgsForCall = append(fake.extractFloatingIPsArgsForCall, struct { + arg1 pagination.Page + }{arg1}) + stub := fake.ExtractFloatingIPsStub + fakeReturns := fake.extractFloatingIPsReturns + fake.recordInvocation("ExtractFloatingIPs", []interface{}{arg1}) + fake.extractFloatingIPsMutex.Unlock() + if stub != nil { + return stub(arg1) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeNetworkingFacade) ExtractFloatingIPsCallCount() int { + fake.extractFloatingIPsMutex.RLock() + defer fake.extractFloatingIPsMutex.RUnlock() + return len(fake.extractFloatingIPsArgsForCall) +} + +func (fake *FakeNetworkingFacade) ExtractFloatingIPsCalls(stub func(pagination.Page) ([]floatingips.FloatingIP, error)) { + fake.extractFloatingIPsMutex.Lock() + defer fake.extractFloatingIPsMutex.Unlock() + fake.ExtractFloatingIPsStub = stub +} + +func (fake *FakeNetworkingFacade) ExtractFloatingIPsArgsForCall(i int) pagination.Page { + fake.extractFloatingIPsMutex.RLock() + defer fake.extractFloatingIPsMutex.RUnlock() + argsForCall := fake.extractFloatingIPsArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeNetworkingFacade) ExtractFloatingIPsReturns(result1 []floatingips.FloatingIP, result2 error) { + fake.extractFloatingIPsMutex.Lock() + defer fake.extractFloatingIPsMutex.Unlock() + fake.ExtractFloatingIPsStub = nil + fake.extractFloatingIPsReturns = struct { + result1 []floatingips.FloatingIP + result2 error + }{result1, result2} +} + +func (fake *FakeNetworkingFacade) ExtractFloatingIPsReturnsOnCall(i int, result1 []floatingips.FloatingIP, result2 error) { + fake.extractFloatingIPsMutex.Lock() + defer fake.extractFloatingIPsMutex.Unlock() + fake.ExtractFloatingIPsStub = nil + if fake.extractFloatingIPsReturnsOnCall == nil { + fake.extractFloatingIPsReturnsOnCall = make(map[int]struct { + result1 []floatingips.FloatingIP + result2 error + }) + } + fake.extractFloatingIPsReturnsOnCall[i] = struct { + result1 []floatingips.FloatingIP + result2 error + }{result1, result2} +} + +func (fake *FakeNetworkingFacade) ExtractPorts(arg1 pagination.Page) ([]ports.Port, error) { + fake.extractPortsMutex.Lock() + ret, specificReturn := fake.extractPortsReturnsOnCall[len(fake.extractPortsArgsForCall)] + fake.extractPortsArgsForCall = append(fake.extractPortsArgsForCall, struct { + arg1 pagination.Page + }{arg1}) + stub := fake.ExtractPortsStub + fakeReturns := fake.extractPortsReturns + fake.recordInvocation("ExtractPorts", []interface{}{arg1}) + fake.extractPortsMutex.Unlock() + if stub != nil { + return stub(arg1) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeNetworkingFacade) ExtractPortsCallCount() int { + fake.extractPortsMutex.RLock() + defer fake.extractPortsMutex.RUnlock() + return len(fake.extractPortsArgsForCall) +} + +func (fake *FakeNetworkingFacade) ExtractPortsCalls(stub func(pagination.Page) ([]ports.Port, error)) { + fake.extractPortsMutex.Lock() + defer fake.extractPortsMutex.Unlock() + fake.ExtractPortsStub = stub +} + +func (fake *FakeNetworkingFacade) ExtractPortsArgsForCall(i int) pagination.Page { + fake.extractPortsMutex.RLock() + defer fake.extractPortsMutex.RUnlock() + argsForCall := fake.extractPortsArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeNetworkingFacade) ExtractPortsReturns(result1 []ports.Port, result2 error) { + fake.extractPortsMutex.Lock() + defer fake.extractPortsMutex.Unlock() + fake.ExtractPortsStub = nil + fake.extractPortsReturns = struct { + result1 []ports.Port + result2 error + }{result1, result2} +} + +func (fake *FakeNetworkingFacade) ExtractPortsReturnsOnCall(i int, result1 []ports.Port, result2 error) { + fake.extractPortsMutex.Lock() + defer fake.extractPortsMutex.Unlock() + fake.ExtractPortsStub = nil + if fake.extractPortsReturnsOnCall == nil { + fake.extractPortsReturnsOnCall = make(map[int]struct { + result1 []ports.Port + result2 error + }) + } + fake.extractPortsReturnsOnCall[i] = struct { + result1 []ports.Port + result2 error + }{result1, result2} +} + +func (fake *FakeNetworkingFacade) ExtractSecurityGroups(arg1 pagination.Page) ([]groups.SecGroup, error) { + fake.extractSecurityGroupsMutex.Lock() + ret, specificReturn := fake.extractSecurityGroupsReturnsOnCall[len(fake.extractSecurityGroupsArgsForCall)] + fake.extractSecurityGroupsArgsForCall = append(fake.extractSecurityGroupsArgsForCall, struct { + arg1 pagination.Page + }{arg1}) + stub := fake.ExtractSecurityGroupsStub + fakeReturns := fake.extractSecurityGroupsReturns + fake.recordInvocation("ExtractSecurityGroups", []interface{}{arg1}) + fake.extractSecurityGroupsMutex.Unlock() + if stub != nil { + return stub(arg1) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeNetworkingFacade) ExtractSecurityGroupsCallCount() int { + fake.extractSecurityGroupsMutex.RLock() + defer fake.extractSecurityGroupsMutex.RUnlock() + return len(fake.extractSecurityGroupsArgsForCall) +} + +func (fake *FakeNetworkingFacade) ExtractSecurityGroupsCalls(stub func(pagination.Page) ([]groups.SecGroup, error)) { + fake.extractSecurityGroupsMutex.Lock() + defer fake.extractSecurityGroupsMutex.Unlock() + fake.ExtractSecurityGroupsStub = stub +} + +func (fake *FakeNetworkingFacade) ExtractSecurityGroupsArgsForCall(i int) pagination.Page { + fake.extractSecurityGroupsMutex.RLock() + defer fake.extractSecurityGroupsMutex.RUnlock() + argsForCall := fake.extractSecurityGroupsArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeNetworkingFacade) ExtractSecurityGroupsReturns(result1 []groups.SecGroup, result2 error) { + fake.extractSecurityGroupsMutex.Lock() + defer fake.extractSecurityGroupsMutex.Unlock() + fake.ExtractSecurityGroupsStub = nil + fake.extractSecurityGroupsReturns = struct { + result1 []groups.SecGroup + result2 error + }{result1, result2} +} + +func (fake *FakeNetworkingFacade) ExtractSecurityGroupsReturnsOnCall(i int, result1 []groups.SecGroup, result2 error) { + fake.extractSecurityGroupsMutex.Lock() + defer fake.extractSecurityGroupsMutex.Unlock() + fake.ExtractSecurityGroupsStub = nil + if fake.extractSecurityGroupsReturnsOnCall == nil { + fake.extractSecurityGroupsReturnsOnCall = make(map[int]struct { + result1 []groups.SecGroup + result2 error + }) + } + fake.extractSecurityGroupsReturnsOnCall[i] = struct { + result1 []groups.SecGroup + result2 error + }{result1, result2} +} + +func (fake *FakeNetworkingFacade) ExtractSubnets(arg1 pagination.Page) ([]subnets.Subnet, error) { + fake.extractSubnetsMutex.Lock() + ret, specificReturn := fake.extractSubnetsReturnsOnCall[len(fake.extractSubnetsArgsForCall)] + fake.extractSubnetsArgsForCall = append(fake.extractSubnetsArgsForCall, struct { + arg1 pagination.Page + }{arg1}) + stub := fake.ExtractSubnetsStub + fakeReturns := fake.extractSubnetsReturns + fake.recordInvocation("ExtractSubnets", []interface{}{arg1}) + fake.extractSubnetsMutex.Unlock() + if stub != nil { + return stub(arg1) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeNetworkingFacade) ExtractSubnetsCallCount() int { + fake.extractSubnetsMutex.RLock() + defer fake.extractSubnetsMutex.RUnlock() + return len(fake.extractSubnetsArgsForCall) +} + +func (fake *FakeNetworkingFacade) ExtractSubnetsCalls(stub func(pagination.Page) ([]subnets.Subnet, error)) { + fake.extractSubnetsMutex.Lock() + defer fake.extractSubnetsMutex.Unlock() + fake.ExtractSubnetsStub = stub +} + +func (fake *FakeNetworkingFacade) ExtractSubnetsArgsForCall(i int) pagination.Page { + fake.extractSubnetsMutex.RLock() + defer fake.extractSubnetsMutex.RUnlock() + argsForCall := fake.extractSubnetsArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeNetworkingFacade) ExtractSubnetsReturns(result1 []subnets.Subnet, result2 error) { + fake.extractSubnetsMutex.Lock() + defer fake.extractSubnetsMutex.Unlock() + fake.ExtractSubnetsStub = nil + fake.extractSubnetsReturns = struct { + result1 []subnets.Subnet + result2 error + }{result1, result2} +} + +func (fake *FakeNetworkingFacade) ExtractSubnetsReturnsOnCall(i int, result1 []subnets.Subnet, result2 error) { + fake.extractSubnetsMutex.Lock() + defer fake.extractSubnetsMutex.Unlock() + fake.ExtractSubnetsStub = nil + if fake.extractSubnetsReturnsOnCall == nil { + fake.extractSubnetsReturnsOnCall = make(map[int]struct { + result1 []subnets.Subnet + result2 error + }) + } + fake.extractSubnetsReturnsOnCall[i] = struct { + result1 []subnets.Subnet + result2 error + }{result1, result2} +} + +func (fake *FakeNetworkingFacade) GetSecurityGroups(arg1 utils.RetryableServiceClient, arg2 string) (*groups.SecGroup, error) { + fake.getSecurityGroupsMutex.Lock() + ret, specificReturn := fake.getSecurityGroupsReturnsOnCall[len(fake.getSecurityGroupsArgsForCall)] + fake.getSecurityGroupsArgsForCall = append(fake.getSecurityGroupsArgsForCall, struct { + arg1 utils.RetryableServiceClient + arg2 string + }{arg1, arg2}) + stub := fake.GetSecurityGroupsStub + fakeReturns := fake.getSecurityGroupsReturns + fake.recordInvocation("GetSecurityGroups", []interface{}{arg1, arg2}) + fake.getSecurityGroupsMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeNetworkingFacade) GetSecurityGroupsCallCount() int { + fake.getSecurityGroupsMutex.RLock() + defer fake.getSecurityGroupsMutex.RUnlock() + return len(fake.getSecurityGroupsArgsForCall) +} + +func (fake *FakeNetworkingFacade) GetSecurityGroupsCalls(stub func(utils.RetryableServiceClient, string) (*groups.SecGroup, error)) { + fake.getSecurityGroupsMutex.Lock() + defer fake.getSecurityGroupsMutex.Unlock() + fake.GetSecurityGroupsStub = stub +} + +func (fake *FakeNetworkingFacade) GetSecurityGroupsArgsForCall(i int) (utils.RetryableServiceClient, string) { + fake.getSecurityGroupsMutex.RLock() + defer fake.getSecurityGroupsMutex.RUnlock() + argsForCall := fake.getSecurityGroupsArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeNetworkingFacade) GetSecurityGroupsReturns(result1 *groups.SecGroup, result2 error) { + fake.getSecurityGroupsMutex.Lock() + defer fake.getSecurityGroupsMutex.Unlock() + fake.GetSecurityGroupsStub = nil + fake.getSecurityGroupsReturns = struct { + result1 *groups.SecGroup + result2 error + }{result1, result2} +} + +func (fake *FakeNetworkingFacade) GetSecurityGroupsReturnsOnCall(i int, result1 *groups.SecGroup, result2 error) { + fake.getSecurityGroupsMutex.Lock() + defer fake.getSecurityGroupsMutex.Unlock() + fake.GetSecurityGroupsStub = nil + if fake.getSecurityGroupsReturnsOnCall == nil { + fake.getSecurityGroupsReturnsOnCall = make(map[int]struct { + result1 *groups.SecGroup + result2 error + }) + } + fake.getSecurityGroupsReturnsOnCall[i] = struct { + result1 *groups.SecGroup + result2 error + }{result1, result2} +} + +func (fake *FakeNetworkingFacade) ListFloatingIps(arg1 utils.RetryableServiceClient, arg2 floatingips.ListOpts) (pagination.Page, error) { + fake.listFloatingIpsMutex.Lock() + ret, specificReturn := fake.listFloatingIpsReturnsOnCall[len(fake.listFloatingIpsArgsForCall)] + fake.listFloatingIpsArgsForCall = append(fake.listFloatingIpsArgsForCall, struct { + arg1 utils.RetryableServiceClient + arg2 floatingips.ListOpts + }{arg1, arg2}) + stub := fake.ListFloatingIpsStub + fakeReturns := fake.listFloatingIpsReturns + fake.recordInvocation("ListFloatingIps", []interface{}{arg1, arg2}) + fake.listFloatingIpsMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeNetworkingFacade) ListFloatingIpsCallCount() int { + fake.listFloatingIpsMutex.RLock() + defer fake.listFloatingIpsMutex.RUnlock() + return len(fake.listFloatingIpsArgsForCall) +} + +func (fake *FakeNetworkingFacade) ListFloatingIpsCalls(stub func(utils.RetryableServiceClient, floatingips.ListOpts) (pagination.Page, error)) { + fake.listFloatingIpsMutex.Lock() + defer fake.listFloatingIpsMutex.Unlock() + fake.ListFloatingIpsStub = stub +} + +func (fake *FakeNetworkingFacade) ListFloatingIpsArgsForCall(i int) (utils.RetryableServiceClient, floatingips.ListOpts) { + fake.listFloatingIpsMutex.RLock() + defer fake.listFloatingIpsMutex.RUnlock() + argsForCall := fake.listFloatingIpsArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeNetworkingFacade) ListFloatingIpsReturns(result1 pagination.Page, result2 error) { + fake.listFloatingIpsMutex.Lock() + defer fake.listFloatingIpsMutex.Unlock() + fake.ListFloatingIpsStub = nil + fake.listFloatingIpsReturns = struct { + result1 pagination.Page + result2 error + }{result1, result2} +} + +func (fake *FakeNetworkingFacade) ListFloatingIpsReturnsOnCall(i int, result1 pagination.Page, result2 error) { + fake.listFloatingIpsMutex.Lock() + defer fake.listFloatingIpsMutex.Unlock() + fake.ListFloatingIpsStub = nil + if fake.listFloatingIpsReturnsOnCall == nil { + fake.listFloatingIpsReturnsOnCall = make(map[int]struct { + result1 pagination.Page + result2 error + }) + } + fake.listFloatingIpsReturnsOnCall[i] = struct { + result1 pagination.Page + result2 error + }{result1, result2} +} + +func (fake *FakeNetworkingFacade) ListPorts(arg1 utils.RetryableServiceClient, arg2 ports.ListOpts) (pagination.Page, error) { + fake.listPortsMutex.Lock() + ret, specificReturn := fake.listPortsReturnsOnCall[len(fake.listPortsArgsForCall)] + fake.listPortsArgsForCall = append(fake.listPortsArgsForCall, struct { + arg1 utils.RetryableServiceClient + arg2 ports.ListOpts + }{arg1, arg2}) + stub := fake.ListPortsStub + fakeReturns := fake.listPortsReturns + fake.recordInvocation("ListPorts", []interface{}{arg1, arg2}) + fake.listPortsMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeNetworkingFacade) ListPortsCallCount() int { + fake.listPortsMutex.RLock() + defer fake.listPortsMutex.RUnlock() + return len(fake.listPortsArgsForCall) +} + +func (fake *FakeNetworkingFacade) ListPortsCalls(stub func(utils.RetryableServiceClient, ports.ListOpts) (pagination.Page, error)) { + fake.listPortsMutex.Lock() + defer fake.listPortsMutex.Unlock() + fake.ListPortsStub = stub +} + +func (fake *FakeNetworkingFacade) ListPortsArgsForCall(i int) (utils.RetryableServiceClient, ports.ListOpts) { + fake.listPortsMutex.RLock() + defer fake.listPortsMutex.RUnlock() + argsForCall := fake.listPortsArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeNetworkingFacade) ListPortsReturns(result1 pagination.Page, result2 error) { + fake.listPortsMutex.Lock() + defer fake.listPortsMutex.Unlock() + fake.ListPortsStub = nil + fake.listPortsReturns = struct { + result1 pagination.Page + result2 error + }{result1, result2} +} + +func (fake *FakeNetworkingFacade) ListPortsReturnsOnCall(i int, result1 pagination.Page, result2 error) { + fake.listPortsMutex.Lock() + defer fake.listPortsMutex.Unlock() + fake.ListPortsStub = nil + if fake.listPortsReturnsOnCall == nil { + fake.listPortsReturnsOnCall = make(map[int]struct { + result1 pagination.Page + result2 error + }) + } + fake.listPortsReturnsOnCall[i] = struct { + result1 pagination.Page + result2 error + }{result1, result2} +} + +func (fake *FakeNetworkingFacade) ListSecurityGroups(arg1 utils.RetryableServiceClient, arg2 groups.ListOpts) (pagination.Page, error) { + fake.listSecurityGroupsMutex.Lock() + ret, specificReturn := fake.listSecurityGroupsReturnsOnCall[len(fake.listSecurityGroupsArgsForCall)] + fake.listSecurityGroupsArgsForCall = append(fake.listSecurityGroupsArgsForCall, struct { + arg1 utils.RetryableServiceClient + arg2 groups.ListOpts + }{arg1, arg2}) + stub := fake.ListSecurityGroupsStub + fakeReturns := fake.listSecurityGroupsReturns + fake.recordInvocation("ListSecurityGroups", []interface{}{arg1, arg2}) + fake.listSecurityGroupsMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeNetworkingFacade) ListSecurityGroupsCallCount() int { + fake.listSecurityGroupsMutex.RLock() + defer fake.listSecurityGroupsMutex.RUnlock() + return len(fake.listSecurityGroupsArgsForCall) +} + +func (fake *FakeNetworkingFacade) ListSecurityGroupsCalls(stub func(utils.RetryableServiceClient, groups.ListOpts) (pagination.Page, error)) { + fake.listSecurityGroupsMutex.Lock() + defer fake.listSecurityGroupsMutex.Unlock() + fake.ListSecurityGroupsStub = stub +} + +func (fake *FakeNetworkingFacade) ListSecurityGroupsArgsForCall(i int) (utils.RetryableServiceClient, groups.ListOpts) { + fake.listSecurityGroupsMutex.RLock() + defer fake.listSecurityGroupsMutex.RUnlock() + argsForCall := fake.listSecurityGroupsArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeNetworkingFacade) ListSecurityGroupsReturns(result1 pagination.Page, result2 error) { + fake.listSecurityGroupsMutex.Lock() + defer fake.listSecurityGroupsMutex.Unlock() + fake.ListSecurityGroupsStub = nil + fake.listSecurityGroupsReturns = struct { + result1 pagination.Page + result2 error + }{result1, result2} +} + +func (fake *FakeNetworkingFacade) ListSecurityGroupsReturnsOnCall(i int, result1 pagination.Page, result2 error) { + fake.listSecurityGroupsMutex.Lock() + defer fake.listSecurityGroupsMutex.Unlock() + fake.ListSecurityGroupsStub = nil + if fake.listSecurityGroupsReturnsOnCall == nil { + fake.listSecurityGroupsReturnsOnCall = make(map[int]struct { + result1 pagination.Page + result2 error + }) + } + fake.listSecurityGroupsReturnsOnCall[i] = struct { + result1 pagination.Page + result2 error + }{result1, result2} +} + +func (fake *FakeNetworkingFacade) ListSubnets(arg1 utils.RetryableServiceClient, arg2 subnets.ListOpts) (pagination.Page, error) { + fake.listSubnetsMutex.Lock() + ret, specificReturn := fake.listSubnetsReturnsOnCall[len(fake.listSubnetsArgsForCall)] + fake.listSubnetsArgsForCall = append(fake.listSubnetsArgsForCall, struct { + arg1 utils.RetryableServiceClient + arg2 subnets.ListOpts + }{arg1, arg2}) + stub := fake.ListSubnetsStub + fakeReturns := fake.listSubnetsReturns + fake.recordInvocation("ListSubnets", []interface{}{arg1, arg2}) + fake.listSubnetsMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeNetworkingFacade) ListSubnetsCallCount() int { + fake.listSubnetsMutex.RLock() + defer fake.listSubnetsMutex.RUnlock() + return len(fake.listSubnetsArgsForCall) +} + +func (fake *FakeNetworkingFacade) ListSubnetsCalls(stub func(utils.RetryableServiceClient, subnets.ListOpts) (pagination.Page, error)) { + fake.listSubnetsMutex.Lock() + defer fake.listSubnetsMutex.Unlock() + fake.ListSubnetsStub = stub +} + +func (fake *FakeNetworkingFacade) ListSubnetsArgsForCall(i int) (utils.RetryableServiceClient, subnets.ListOpts) { + fake.listSubnetsMutex.RLock() + defer fake.listSubnetsMutex.RUnlock() + argsForCall := fake.listSubnetsArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeNetworkingFacade) ListSubnetsReturns(result1 pagination.Page, result2 error) { + fake.listSubnetsMutex.Lock() + defer fake.listSubnetsMutex.Unlock() + fake.ListSubnetsStub = nil + fake.listSubnetsReturns = struct { + result1 pagination.Page + result2 error + }{result1, result2} +} + +func (fake *FakeNetworkingFacade) ListSubnetsReturnsOnCall(i int, result1 pagination.Page, result2 error) { + fake.listSubnetsMutex.Lock() + defer fake.listSubnetsMutex.Unlock() + fake.ListSubnetsStub = nil + if fake.listSubnetsReturnsOnCall == nil { + fake.listSubnetsReturnsOnCall = make(map[int]struct { + result1 pagination.Page + result2 error + }) + } + fake.listSubnetsReturnsOnCall[i] = struct { + result1 pagination.Page + result2 error + }{result1, result2} +} + +func (fake *FakeNetworkingFacade) UpdateFloatingIP(arg1 utils.ServiceClient, arg2 string, arg3 floatingips.UpdateOpts) (*floatingips.FloatingIP, error) { + fake.updateFloatingIPMutex.Lock() + ret, specificReturn := fake.updateFloatingIPReturnsOnCall[len(fake.updateFloatingIPArgsForCall)] + fake.updateFloatingIPArgsForCall = append(fake.updateFloatingIPArgsForCall, struct { + arg1 utils.ServiceClient + arg2 string + arg3 floatingips.UpdateOpts + }{arg1, arg2, arg3}) + stub := fake.UpdateFloatingIPStub + fakeReturns := fake.updateFloatingIPReturns + fake.recordInvocation("UpdateFloatingIP", []interface{}{arg1, arg2, arg3}) + fake.updateFloatingIPMutex.Unlock() + if stub != nil { + return stub(arg1, arg2, arg3) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeNetworkingFacade) UpdateFloatingIPCallCount() int { + fake.updateFloatingIPMutex.RLock() + defer fake.updateFloatingIPMutex.RUnlock() + return len(fake.updateFloatingIPArgsForCall) +} + +func (fake *FakeNetworkingFacade) UpdateFloatingIPCalls(stub func(utils.ServiceClient, string, floatingips.UpdateOpts) (*floatingips.FloatingIP, error)) { + fake.updateFloatingIPMutex.Lock() + defer fake.updateFloatingIPMutex.Unlock() + fake.UpdateFloatingIPStub = stub +} + +func (fake *FakeNetworkingFacade) UpdateFloatingIPArgsForCall(i int) (utils.ServiceClient, string, floatingips.UpdateOpts) { + fake.updateFloatingIPMutex.RLock() + defer fake.updateFloatingIPMutex.RUnlock() + argsForCall := fake.updateFloatingIPArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 +} + +func (fake *FakeNetworkingFacade) UpdateFloatingIPReturns(result1 *floatingips.FloatingIP, result2 error) { + fake.updateFloatingIPMutex.Lock() + defer fake.updateFloatingIPMutex.Unlock() + fake.UpdateFloatingIPStub = nil + fake.updateFloatingIPReturns = struct { + result1 *floatingips.FloatingIP + result2 error + }{result1, result2} +} + +func (fake *FakeNetworkingFacade) UpdateFloatingIPReturnsOnCall(i int, result1 *floatingips.FloatingIP, result2 error) { + fake.updateFloatingIPMutex.Lock() + defer fake.updateFloatingIPMutex.Unlock() + fake.UpdateFloatingIPStub = nil + if fake.updateFloatingIPReturnsOnCall == nil { + fake.updateFloatingIPReturnsOnCall = make(map[int]struct { + result1 *floatingips.FloatingIP + result2 error + }) + } + fake.updateFloatingIPReturnsOnCall[i] = struct { + result1 *floatingips.FloatingIP + result2 error + }{result1, result2} +} + +func (fake *FakeNetworkingFacade) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.createPortMutex.RLock() + defer fake.createPortMutex.RUnlock() + fake.deletePortMutex.RLock() + defer fake.deletePortMutex.RUnlock() + fake.extractFloatingIPsMutex.RLock() + defer fake.extractFloatingIPsMutex.RUnlock() + fake.extractPortsMutex.RLock() + defer fake.extractPortsMutex.RUnlock() + fake.extractSecurityGroupsMutex.RLock() + defer fake.extractSecurityGroupsMutex.RUnlock() + fake.extractSubnetsMutex.RLock() + defer fake.extractSubnetsMutex.RUnlock() + fake.getSecurityGroupsMutex.RLock() + defer fake.getSecurityGroupsMutex.RUnlock() + fake.listFloatingIpsMutex.RLock() + defer fake.listFloatingIpsMutex.RUnlock() + fake.listPortsMutex.RLock() + defer fake.listPortsMutex.RUnlock() + fake.listSecurityGroupsMutex.RLock() + defer fake.listSecurityGroupsMutex.RUnlock() + fake.listSubnetsMutex.RLock() + defer fake.listSubnetsMutex.RUnlock() + fake.updateFloatingIPMutex.RLock() + defer fake.updateFloatingIPMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *FakeNetworkingFacade) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ network.NetworkingFacade = new(FakeNetworkingFacade) diff --git a/src/openstack_cpi_golang/cpi/network/networkfakes/fake_security_groups_resolver.go b/src/openstack_cpi_golang/cpi/network/networkfakes/fake_security_groups_resolver.go new file mode 100644 index 00000000..b9be1065 --- /dev/null +++ b/src/openstack_cpi_golang/cpi/network/networkfakes/fake_security_groups_resolver.go @@ -0,0 +1,121 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package networkfakes + +import ( + "sync" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/network" +) + +type FakeSecurityGroupsResolver struct { + ResolveStub func([]string) ([]string, error) + resolveMutex sync.RWMutex + resolveArgsForCall []struct { + arg1 []string + } + resolveReturns struct { + result1 []string + result2 error + } + resolveReturnsOnCall map[int]struct { + result1 []string + result2 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *FakeSecurityGroupsResolver) Resolve(arg1 []string) ([]string, error) { + var arg1Copy []string + if arg1 != nil { + arg1Copy = make([]string, len(arg1)) + copy(arg1Copy, arg1) + } + fake.resolveMutex.Lock() + ret, specificReturn := fake.resolveReturnsOnCall[len(fake.resolveArgsForCall)] + fake.resolveArgsForCall = append(fake.resolveArgsForCall, struct { + arg1 []string + }{arg1Copy}) + stub := fake.ResolveStub + fakeReturns := fake.resolveReturns + fake.recordInvocation("Resolve", []interface{}{arg1Copy}) + fake.resolveMutex.Unlock() + if stub != nil { + return stub(arg1) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeSecurityGroupsResolver) ResolveCallCount() int { + fake.resolveMutex.RLock() + defer fake.resolveMutex.RUnlock() + return len(fake.resolveArgsForCall) +} + +func (fake *FakeSecurityGroupsResolver) ResolveCalls(stub func([]string) ([]string, error)) { + fake.resolveMutex.Lock() + defer fake.resolveMutex.Unlock() + fake.ResolveStub = stub +} + +func (fake *FakeSecurityGroupsResolver) ResolveArgsForCall(i int) []string { + fake.resolveMutex.RLock() + defer fake.resolveMutex.RUnlock() + argsForCall := fake.resolveArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeSecurityGroupsResolver) ResolveReturns(result1 []string, result2 error) { + fake.resolveMutex.Lock() + defer fake.resolveMutex.Unlock() + fake.ResolveStub = nil + fake.resolveReturns = struct { + result1 []string + result2 error + }{result1, result2} +} + +func (fake *FakeSecurityGroupsResolver) ResolveReturnsOnCall(i int, result1 []string, result2 error) { + fake.resolveMutex.Lock() + defer fake.resolveMutex.Unlock() + fake.ResolveStub = nil + if fake.resolveReturnsOnCall == nil { + fake.resolveReturnsOnCall = make(map[int]struct { + result1 []string + result2 error + }) + } + fake.resolveReturnsOnCall[i] = struct { + result1 []string + result2 error + }{result1, result2} +} + +func (fake *FakeSecurityGroupsResolver) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.resolveMutex.RLock() + defer fake.resolveMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *FakeSecurityGroupsResolver) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ network.SecurityGroupsResolver = new(FakeSecurityGroupsResolver) diff --git a/src/openstack_cpi_golang/cpi/network/networking_facade.go b/src/openstack_cpi_golang/cpi/network/networking_facade.go new file mode 100644 index 00000000..fed516ba --- /dev/null +++ b/src/openstack_cpi_golang/cpi/network/networking_facade.go @@ -0,0 +1,90 @@ +package network + +import ( + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/groups" + "github.com/gophercloud/gophercloud/openstack/networking/v2/ports" + "github.com/gophercloud/gophercloud/openstack/networking/v2/subnets" + "github.com/gophercloud/gophercloud/pagination" +) + +//counterfeiter:generate . NetworkingFacade +type NetworkingFacade interface { + ListFloatingIps(serviceClient utils.RetryableServiceClient, opts floatingips.ListOpts) (pagination.Page, error) + + ExtractFloatingIPs(page pagination.Page) ([]floatingips.FloatingIP, error) + + UpdateFloatingIP(serviceClient utils.ServiceClient, floatingIpId string, updateOpts floatingips.UpdateOpts) (*floatingips.FloatingIP, error) + + CreatePort(serviceClient utils.ServiceClient, createOpts ports.CreateOpts) (*ports.Port, error) + + DeletePort(serviceClient utils.RetryableServiceClient, portID string) error + + ListPorts(client utils.RetryableServiceClient, opts ports.ListOpts) (pagination.Page, error) + + ExtractPorts(page pagination.Page) ([]ports.Port, error) + + GetSecurityGroups(serviceClient utils.RetryableServiceClient, id string) (*groups.SecGroup, error) + + ListSecurityGroups(serviceClient utils.RetryableServiceClient, opts groups.ListOpts) (pagination.Page, error) + + ExtractSecurityGroups(page pagination.Page) ([]groups.SecGroup, error) + + ListSubnets(serviceClient utils.RetryableServiceClient, opts subnets.ListOpts) (pagination.Page, error) + + ExtractSubnets(page pagination.Page) ([]subnets.Subnet, error) +} + +type networkingFacade struct{} + +func NewNetworkingFacade() NetworkingFacade { + return networkingFacade{} +} + +func (n networkingFacade) ListFloatingIps(serviceClient utils.RetryableServiceClient, opts floatingips.ListOpts) (pagination.Page, error) { + return floatingips.List(serviceClient, opts).AllPages() +} + +func (n networkingFacade) ExtractFloatingIPs(page pagination.Page) ([]floatingips.FloatingIP, error) { + return floatingips.ExtractFloatingIPs(page) +} + +func (n networkingFacade) UpdateFloatingIP(serviceClient utils.ServiceClient, floatingIpId string, updateOpts floatingips.UpdateOpts) (*floatingips.FloatingIP, error) { + return floatingips.Update(serviceClient, floatingIpId, updateOpts).Extract() +} + +func (n networkingFacade) CreatePort(serviceClient utils.ServiceClient, createOpts ports.CreateOpts) (*ports.Port, error) { + return ports.Create(serviceClient, createOpts).Extract() +} + +func (n networkingFacade) DeletePort(serviceClient utils.RetryableServiceClient, portID string) error { + return ports.Delete(serviceClient, portID).ExtractErr() +} + +func (n networkingFacade) ListPorts(serviceClient utils.RetryableServiceClient, opts ports.ListOpts) (pagination.Page, error) { + return ports.List(serviceClient, opts).AllPages() +} + +func (n networkingFacade) ExtractPorts(page pagination.Page) ([]ports.Port, error) { + return ports.ExtractPorts(page) +} + +func (n networkingFacade) GetSecurityGroups(serviceClient utils.RetryableServiceClient, id string) (*groups.SecGroup, error) { + return groups.Get(serviceClient, id).Extract() +} + +func (n networkingFacade) ListSecurityGroups(serviceClient utils.RetryableServiceClient, opts groups.ListOpts) (pagination.Page, error) { + return groups.List(serviceClient, opts).AllPages() +} + +func (n networkingFacade) ExtractSecurityGroups(page pagination.Page) ([]groups.SecGroup, error) { + return groups.ExtractGroups(page) +} +func (n networkingFacade) ListSubnets(serviceClient utils.RetryableServiceClient, opts subnets.ListOpts) (pagination.Page, error) { + return subnets.List(serviceClient, opts).AllPages() +} + +func (n networkingFacade) ExtractSubnets(page pagination.Page) ([]subnets.Subnet, error) { + return subnets.ExtractSubnets(page) +} diff --git a/src/openstack_cpi_golang/cpi/network/security_groups_resolver.go b/src/openstack_cpi_golang/cpi/network/security_groups_resolver.go new file mode 100644 index 00000000..92c2d2c4 --- /dev/null +++ b/src/openstack_cpi_golang/cpi/network/security_groups_resolver.go @@ -0,0 +1,82 @@ +package network + +import ( + "fmt" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/groups" +) + +//counterfeiter:generate . SecurityGroupsResolver +type SecurityGroupsResolver interface { + Resolve(securityGroupIDsAndNames []string) ([]string, error) +} + +type securityGroupsResolver struct { + serviceClients utils.ServiceClients + networkingFacade NetworkingFacade + logger utils.Logger +} + +func NewSecurityGroupsResolver( + serviceClients utils.ServiceClients, + networkingFacade NetworkingFacade, + logger utils.Logger, +) securityGroupsResolver { + return securityGroupsResolver{ + serviceClients: serviceClients, + networkingFacade: networkingFacade, + logger: logger, + } +} + +func (s securityGroupsResolver) Resolve(securityGroupIDsAndNames []string) ([]string, error) { + var securityGroupIds []string + var resolvedSecurityGroup *groups.SecGroup + var err error + + for _, securityGroup := range securityGroupIDsAndNames { + resolvedSecurityGroup, err = s.resolveSecurityGroupById(securityGroup) + if err != nil { + s.logger.Warn("security-group-resolver", fmt.Sprintf("failed to get security group '%s' by id: %v. Trying to get security group by name", securityGroup, err)) + + resolvedSecurityGroup, err = s.resolveSecurityGroupByName(securityGroup) + if err != nil { + return []string{}, fmt.Errorf("failed to get security group '%s' by name: %w", securityGroup, err) + } + } + + if resolvedSecurityGroup == nil { + return []string{}, fmt.Errorf("could not resolve security group '%s'", securityGroup) + } + + securityGroupIds = append(securityGroupIds, resolvedSecurityGroup.ID) + } + return securityGroupIds, nil +} + +func (s securityGroupsResolver) resolveSecurityGroupById(securityGroupID string) (*groups.SecGroup, error) { + return s.networkingFacade.GetSecurityGroups(s.serviceClients.RetryableServiceClient, securityGroupID) +} + +func (s securityGroupsResolver) resolveSecurityGroupByName(securityGroupName string) (*groups.SecGroup, error) { + listOpts := groups.ListOpts{ + Name: securityGroupName, + } + + allPages, err := s.networkingFacade.ListSecurityGroups(s.serviceClients.RetryableServiceClient, listOpts) + if err != nil { + return nil, fmt.Errorf("failed to list security groups: %w", err) + } + + allSecurityGroups, err := s.networkingFacade.ExtractSecurityGroups(allPages) + if err != nil { + return nil, fmt.Errorf("failed to extract security groups: %w", err) + } + + if len(allSecurityGroups) == 0 { + return nil, fmt.Errorf("security group '%s' could not be found", securityGroupName) + } + + return &allSecurityGroups[0], nil +} diff --git a/src/openstack_cpi_golang/cpi/network/security_groups_resolver_test.go b/src/openstack_cpi_golang/cpi/network/security_groups_resolver_test.go new file mode 100644 index 00000000..686937b2 --- /dev/null +++ b/src/openstack_cpi_golang/cpi/network/security_groups_resolver_test.go @@ -0,0 +1,141 @@ +package network_test + +import ( + "encoding/json" + "errors" + + "github.com/cloudfoundry/bosh-cpi-go/apiv1" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/mocks" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/network" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/network/networkfakes" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils/utilsfakes" + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/groups" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("NetworkService", func() { + var serviceClient gophercloud.ServiceClient + var retryableServiceClient gophercloud.ServiceClient + var serviceClients utils.ServiceClients + var networkingFacade networkfakes.FakeNetworkingFacade + var securityGroupsPage mocks.MockPage + var logger utilsfakes.FakeLogger + + BeforeEach(func() { + serviceClient = gophercloud.ServiceClient{} + retryableServiceClient = gophercloud.ServiceClient{} + serviceClients = utils.ServiceClients{ServiceClient: &serviceClient, RetryableServiceClient: &retryableServiceClient} + networkingFacade = networkfakes.FakeNetworkingFacade{} + securityGroupsPage = mocks.MockPage{} + logger = utilsfakes.FakeLogger{} + + var networks apiv1.Networks + err := json.Unmarshal([]byte(`{ + "name1": { + "type": "manual", + "ip": "1.1.1.1", + "default": ["gateway"], + "cloud_properties": {"net_id": "the_net_id_1", "security_groups": ["security_group_1", "security_group_2"]} + }, + "name3": { + "type": "vip", + "ip": "3.3.3.3", + "cloud_properties": {"net_id": "the_net_id_3", "security_groups": ["security_group_3"]} + } + }`), &networks) + Expect(err).ToNot(HaveOccurred()) + + //networkConfig, err = network.NewNetworkConfigBuilder(&networkService, networks, config.OpenstackConfig{}, properties.CreateVM{}).Build() + Expect(err).ToNot(HaveOccurred()) + }) + + Context("Resolve", func() { + It("resolves security groups by id", func() { + _, _ = network.NewSecurityGroupsResolver(serviceClients, &networkingFacade, &logger).Resolve([]string{"the_group_id"}) + + _, securityGroupID := networkingFacade.GetSecurityGroupsArgsForCall(0) + Expect(securityGroupID).To(Equal("the_group_id")) + }) + + It("logs a warning if getting security group by id fails", func() { + networkingFacade.GetSecurityGroupsReturns(nil, errors.New("boom")) + + _, _ = network.NewSecurityGroupsResolver(serviceClients, &networkingFacade, &logger).Resolve([]string{"the_group_id"}) + + _, msg, _ := logger.WarnArgsForCall(0) + Expect(msg).To(Equal("failed to get security group 'the_group_id' by id: boom. Trying to get security group by name")) + }) + + It("returns an error is resolved security group is nil", func() { + networkingFacade.GetSecurityGroupsReturns(nil, nil) + + _, err := network.NewSecurityGroupsResolver(serviceClients, &networkingFacade, &logger).Resolve([]string{"the_group_id"}) + + Expect(err.Error()).To(Equal("could not resolve security group 'the_group_id'")) + }) + + Context("resolution by ID failed", func() { + It("list security groups by name", func() { + networkingFacade.GetSecurityGroupsReturns(nil, nil) + networkingFacade.ListSecurityGroupsReturns(securityGroupsPage, nil) + + _, _ = network.NewSecurityGroupsResolver(serviceClients, &networkingFacade, &logger).Resolve([]string{"the_group_id"}) + + Expect(networkingFacade.GetSecurityGroupsCallCount()).To(Equal(1)) + }) + + It("returns an error if listing security groups fails", func() { + networkingFacade.GetSecurityGroupsReturns(nil, errors.New("baam")) + networkingFacade.ListSecurityGroupsReturns(nil, errors.New("boom")) + + _, err := network.NewSecurityGroupsResolver(serviceClients, &networkingFacade, &logger).Resolve([]string{"the_group_name"}) + + Expect(err.Error()).To(Equal("failed to get security group 'the_group_name' by name: failed to list security groups: boom")) + }) + + It("extracts security groups", func() { + networkingFacade.GetSecurityGroupsReturns(nil, errors.New("baam")) + networkingFacade.ListSecurityGroupsReturns(securityGroupsPage, nil) + + _, _ = network.NewSecurityGroupsResolver(serviceClients, &networkingFacade, &logger).Resolve([]string{"the_group_id"}) + + page := networkingFacade.ExtractSecurityGroupsArgsForCall(0) + Expect(page).To(Equal(securityGroupsPage)) + }) + + It("returns an error if extracts security groups fails", func() { + networkingFacade.GetSecurityGroupsReturns(nil, errors.New("baam")) + networkingFacade.ListSecurityGroupsReturns(securityGroupsPage, nil) + networkingFacade.ExtractSecurityGroupsReturns(nil, errors.New("boom")) + + _, err := network.NewSecurityGroupsResolver(serviceClients, &networkingFacade, &logger).Resolve([]string{"the_group_name"}) + + Expect(err.Error()).To(Equal("failed to get security group 'the_group_name' by name: failed to extract security groups: boom")) + }) + + It("returns an error if extracts security groups are empty", func() { + networkingFacade.GetSecurityGroupsReturns(nil, errors.New("baam")) + networkingFacade.ListSecurityGroupsReturns(securityGroupsPage, nil) + networkingFacade.ExtractSecurityGroupsReturns([]groups.SecGroup{}, nil) + + _, err := network.NewSecurityGroupsResolver(serviceClients, &networkingFacade, &logger).Resolve([]string{"the_group_name"}) + + Expect(err.Error()).To(Equal("failed to get security group 'the_group_name' by name: security group 'the_group_name' could not be found")) + }) + + It("returns security group ids", func() { + networkingFacade.GetSecurityGroupsReturnsOnCall(0, &groups.SecGroup{ID: "id1"}, nil) + networkingFacade.GetSecurityGroupsReturnsOnCall(1, nil, errors.New("baam")) + networkingFacade.ListSecurityGroupsReturns(securityGroupsPage, nil) + networkingFacade.ExtractSecurityGroupsReturns([]groups.SecGroup{{ID: "id2"}}, nil) + + securityGroups, err := network.NewSecurityGroupsResolver(serviceClients, &networkingFacade, &logger).Resolve([]string{"id1", "not_id"}) + Expect(err).ToNot(HaveOccurred()) + Expect(securityGroups).To(Equal([]string{"id1", "id2"})) + }) + }) + }) +}) diff --git a/src/openstack_cpi_golang/cpi/openstack/openstack_facade.go b/src/openstack_cpi_golang/cpi/openstack/openstack_facade.go new file mode 100644 index 00000000..c2a31c1e --- /dev/null +++ b/src/openstack_cpi_golang/cpi/openstack/openstack_facade.go @@ -0,0 +1,51 @@ +package openstack + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack" +) + +//counterfeiter:generate . OpenstackFacade +type OpenstackFacade interface { + NewComputeV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) + + NewLoadBalancerV2(client *gophercloud.ProviderClient, endpointOpts gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) + + NewNetworkV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) + + NewImageServiceV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) + + NewBlockStorageV3(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) + + AuthenticatedClient(options gophercloud.AuthOptions) (*gophercloud.ProviderClient, error) +} + +type openstackFacade struct{} + +func NewOpenstackFacade() OpenstackFacade { + return openstackFacade{} +} + +func (c openstackFacade) NewComputeV2(client *gophercloud.ProviderClient, endpointOpts gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + return openstack.NewComputeV2(client, endpointOpts) +} + +func (c openstackFacade) NewLoadBalancerV2(client *gophercloud.ProviderClient, endpointOpts gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + return openstack.NewLoadBalancerV2(client, endpointOpts) +} + +func (c openstackFacade) NewNetworkV2(client *gophercloud.ProviderClient, endpointOpts gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + return openstack.NewNetworkV2(client, endpointOpts) +} + +func (c openstackFacade) NewImageServiceV2(client *gophercloud.ProviderClient, endpointOpts gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + return openstack.NewImageServiceV2(client, endpointOpts) +} + +func (c openstackFacade) NewBlockStorageV3(client *gophercloud.ProviderClient, endpointOpts gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + return openstack.NewBlockStorageV3(client, endpointOpts) +} + +func (c openstackFacade) AuthenticatedClient(options gophercloud.AuthOptions) (*gophercloud.ProviderClient, error) { + return openstack.AuthenticatedClient(options) +} diff --git a/src/openstack_cpi_golang/cpi/openstack/openstack_service.go b/src/openstack_cpi_golang/cpi/openstack/openstack_service.go new file mode 100644 index 00000000..a623c143 --- /dev/null +++ b/src/openstack_cpi_golang/cpi/openstack/openstack_service.go @@ -0,0 +1,81 @@ +package openstack + +import ( + "fmt" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/config" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils" + "github.com/gophercloud/gophercloud" +) + +//counterfeiter:generate . OpenstackService +type OpenstackService interface { + ComputeServiceV2(config config.OpenstackConfig) (*gophercloud.ServiceClient, error) + LoadbalancerV2(config config.OpenstackConfig) (*gophercloud.ServiceClient, error) + NetworkServiceV2(config config.OpenstackConfig) (*gophercloud.ServiceClient, error) + ImageServiceV2(config config.OpenstackConfig) (*gophercloud.ServiceClient, error) + BlockStorageV3(config config.OpenstackConfig) (*gophercloud.ServiceClient, error) +} + +type openstackService struct { + openstackFacade OpenstackFacade + envVar utils.EnvVar +} + +func NewOpenstackService(openstackFacade OpenstackFacade, envVar utils.EnvVar) OpenstackService { + return openstackService{ + openstackFacade: openstackFacade, + envVar: envVar, + } +} + +func (c openstackService) ComputeServiceV2(config config.OpenstackConfig) (*gophercloud.ServiceClient, error) { + authenticatedClient, err := c.openstackFacade.AuthenticatedClient(config.AuthOptions()) + if err != nil { + return nil, fmt.Errorf("failed to authenticate: %w", err) + } + + return c.openstackFacade.NewComputeV2(authenticatedClient, c.endpointOpts()) +} + +func (c openstackService) LoadbalancerV2(config config.OpenstackConfig) (*gophercloud.ServiceClient, error) { + authenticatedClient, err := c.openstackFacade.AuthenticatedClient(config.AuthOptions()) + if err != nil { + return nil, fmt.Errorf("failed to authenticate: %w", err) + } + + return c.openstackFacade.NewLoadBalancerV2(authenticatedClient, c.endpointOpts()) +} + +func (c openstackService) NetworkServiceV2(config config.OpenstackConfig) (*gophercloud.ServiceClient, error) { + authenticatedClient, err := c.openstackFacade.AuthenticatedClient(config.AuthOptions()) + if err != nil { + return nil, fmt.Errorf("failed to authenticate: %w", err) + } + + return c.openstackFacade.NewNetworkV2(authenticatedClient, c.endpointOpts()) +} + +func (c openstackService) ImageServiceV2(config config.OpenstackConfig) (*gophercloud.ServiceClient, error) { + authenticatedClient, err := c.openstackFacade.AuthenticatedClient(config.AuthOptions()) + if err != nil { + return nil, fmt.Errorf("failed to authenticate: %w", err) + } + + return c.openstackFacade.NewImageServiceV2(authenticatedClient, c.endpointOpts()) +} + +func (c openstackService) BlockStorageV3(config config.OpenstackConfig) (*gophercloud.ServiceClient, error) { + authenticatedClient, err := c.openstackFacade.AuthenticatedClient(config.AuthOptions()) + if err != nil { + return nil, fmt.Errorf("failed to authenticate: %w", err) + } + + return c.openstackFacade.NewBlockStorageV3(authenticatedClient, c.endpointOpts()) +} + +func (c openstackService) endpointOpts() gophercloud.EndpointOpts { + return gophercloud.EndpointOpts{ + Region: c.envVar.Get("OS_REGION_NAME"), + } +} diff --git a/src/openstack_cpi_golang/cpi/openstack/openstack_service_test.go b/src/openstack_cpi_golang/cpi/openstack/openstack_service_test.go new file mode 100644 index 00000000..34956026 --- /dev/null +++ b/src/openstack_cpi_golang/cpi/openstack/openstack_service_test.go @@ -0,0 +1,229 @@ +package openstack_test + +import ( + "errors" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/config" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/openstack" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/openstack/openstackfakes" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils/utilsfakes" + "github.com/gophercloud/gophercloud" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("OpenstackService", func() { + var openstackFacade openstackfakes.FakeOpenstackFacade + var serviceClient gophercloud.ServiceClient + var envVar utilsfakes.FakeEnvVar + + BeforeEach(func() { + openstackFacade = openstackfakes.FakeOpenstackFacade{} + serviceClient = gophercloud.ServiceClient{} + envVar = utilsfakes.FakeEnvVar{} + + openstackFacade.AuthenticatedClientReturns(&gophercloud.ProviderClient{}, nil) + openstackFacade.NewImageServiceV2Returns(&serviceClient, nil) + openstackFacade.NewComputeV2Returns(&serviceClient, nil) + openstackFacade.NewNetworkV2Returns(&serviceClient, nil) + openstackFacade.NewLoadBalancerV2Returns(&serviceClient, nil) + + envVar.GetReturns("the_os_region_name") + }) + + Context("ImageServiceV2", func() { + It("returns a ImageServiceV2 instance", func() { + client, err := openstack.NewOpenstackService(&openstackFacade, &envVar).ImageServiceV2(config.OpenstackConfig{}) + + Expect(err).ToNot(HaveOccurred()) + Expect(client).To(Equal(&serviceClient)) + }) + + It("authenticates using the cpi config", func() { + openstackConfig := config.OpenstackConfig{ + AuthURL: "the_auth_url", + Username: "the_username", + APIKey: "the_api_key", + DomainName: "the_domain_name", + ProjectName: "the_tenant", + } + + _, _ = openstack.NewOpenstackService(&openstackFacade, &envVar).ImageServiceV2(openstackConfig) + + opts := openstackFacade.AuthenticatedClientArgsForCall(0) + Expect(opts).To(Equal(gophercloud.AuthOptions{ + IdentityEndpoint: "the_auth_url", + Username: "the_username", + Password: "the_api_key", + DomainName: "the_domain_name", + TenantName: "the_tenant", + })) + }) + + It("gets the region of the service from the environment", func() { + _, _ = openstack.NewOpenstackService(&openstackFacade, &envVar).ImageServiceV2(config.OpenstackConfig{}) + + _, endpointOpts := openstackFacade.NewImageServiceV2ArgsForCall(0) + Expect(endpointOpts).To(Equal(gophercloud.EndpointOpts{ + Region: "the_os_region_name", + })) + }) + + It("returns an error on failing authentication", func() { + openstackFacade.AuthenticatedClientReturns(nil, errors.New("boom")) + + client, err := openstack.NewOpenstackService(&openstackFacade, &envVar).ImageServiceV2(config.OpenstackConfig{}) + + Expect(err.Error()).To(Equal("failed to authenticate: boom")) + Expect(client).To(BeNil()) + }) + + }) + + Context("ComputeServiceV2", func() { + It("returns a ComputeServiceV2 instance", func() { + client, err := openstack.NewOpenstackService(&openstackFacade, &envVar).ComputeServiceV2(config.OpenstackConfig{}) + + Expect(err).ToNot(HaveOccurred()) + Expect(client).To(Equal(&serviceClient)) + }) + + It("authenticates using the cpi config", func() { + openstackConfig := config.OpenstackConfig{ + AuthURL: "the_auth_url", + Username: "the_username", + APIKey: "the_api_key", + DomainName: "the_domain_name", + ProjectName: "the_tenant", + } + + _, _ = openstack.NewOpenstackService(&openstackFacade, &envVar).ComputeServiceV2(openstackConfig) + + opts := openstackFacade.AuthenticatedClientArgsForCall(0) + Expect(opts).To(Equal(gophercloud.AuthOptions{ + IdentityEndpoint: "the_auth_url", + Username: "the_username", + Password: "the_api_key", + DomainName: "the_domain_name", + TenantName: "the_tenant", + })) + }) + + It("gets the region of the service from the environment", func() { + _, _ = openstack.NewOpenstackService(&openstackFacade, &envVar).ComputeServiceV2(config.OpenstackConfig{}) + + _, endpointOpts := openstackFacade.NewComputeV2ArgsForCall(0) + Expect(endpointOpts).To(Equal(gophercloud.EndpointOpts{ + Region: "the_os_region_name", + })) + }) + + It("returns an error on failing authentication", func() { + openstackFacade.AuthenticatedClientReturns(nil, errors.New("boom")) + + client, err := openstack.NewOpenstackService(&openstackFacade, &envVar).ComputeServiceV2(config.OpenstackConfig{}) + + Expect(err.Error()).To(Equal("failed to authenticate: boom")) + Expect(client).To(BeNil()) + }) + + }) + + Context("LoadbalancerServiceV2", func() { + It("returns a LoadbalancerV2 instance", func() { + client, err := openstack.NewOpenstackService(&openstackFacade, &envVar).LoadbalancerV2(config.OpenstackConfig{}) + + Expect(err).ToNot(HaveOccurred()) + Expect(client).To(Equal(&serviceClient)) + }) + + It("authenticates using the cpi config", func() { + openstackConfig := config.OpenstackConfig{ + AuthURL: "the_auth_url", + Username: "the_username", + APIKey: "the_api_key", + DomainName: "the_domain_name", + ProjectName: "the_tenant", + } + + _, _ = openstack.NewOpenstackService(&openstackFacade, &envVar).LoadbalancerV2(openstackConfig) + + opts := openstackFacade.AuthenticatedClientArgsForCall(0) + Expect(opts).To(Equal(gophercloud.AuthOptions{ + IdentityEndpoint: "the_auth_url", + Username: "the_username", + Password: "the_api_key", + DomainName: "the_domain_name", + TenantName: "the_tenant", + })) + }) + + It("gets the region of the service from the environment", func() { + _, _ = openstack.NewOpenstackService(&openstackFacade, &envVar).LoadbalancerV2(config.OpenstackConfig{}) + + _, endpointOpts := openstackFacade.NewLoadBalancerV2ArgsForCall(0) + Expect(endpointOpts).To(Equal(gophercloud.EndpointOpts{ + Region: "the_os_region_name", + })) + }) + + It("returns an error on failing authentication", func() { + openstackFacade.AuthenticatedClientReturns(nil, errors.New("boom")) + + client, err := openstack.NewOpenstackService(&openstackFacade, &envVar).LoadbalancerV2(config.OpenstackConfig{}) + + Expect(err.Error()).To(Equal("failed to authenticate: boom")) + Expect(client).To(BeNil()) + }) + + }) + + Context("NetworkServiceV2", func() { + It("returns a NetworkServiceV2 instance", func() { + client, err := openstack.NewOpenstackService(&openstackFacade, &envVar).NetworkServiceV2(config.OpenstackConfig{}) + + Expect(err).ToNot(HaveOccurred()) + Expect(client).To(Equal(&serviceClient)) + }) + + It("authenticates using the cpi config", func() { + openstackConfig := config.OpenstackConfig{ + AuthURL: "the_auth_url", + Username: "the_username", + APIKey: "the_api_key", + DomainName: "the_domain_name", + ProjectName: "the_tenant", + } + + _, _ = openstack.NewOpenstackService(&openstackFacade, &envVar).NetworkServiceV2(openstackConfig) + + opts := openstackFacade.AuthenticatedClientArgsForCall(0) + Expect(opts).To(Equal(gophercloud.AuthOptions{ + IdentityEndpoint: "the_auth_url", + Username: "the_username", + Password: "the_api_key", + DomainName: "the_domain_name", + TenantName: "the_tenant", + })) + }) + + It("gets the region of the service from the environment", func() { + _, _ = openstack.NewOpenstackService(&openstackFacade, &envVar).NetworkServiceV2(config.OpenstackConfig{}) + + _, endpointOpts := openstackFacade.NewNetworkV2ArgsForCall(0) + Expect(endpointOpts).To(Equal(gophercloud.EndpointOpts{ + Region: "the_os_region_name", + })) + }) + + It("returns an error on failing authentication", func() { + openstackFacade.AuthenticatedClientReturns(nil, errors.New("boom")) + + client, err := openstack.NewOpenstackService(&openstackFacade, &envVar).NetworkServiceV2(config.OpenstackConfig{}) + + Expect(err.Error()).To(Equal("failed to authenticate: boom")) + Expect(client).To(BeNil()) + }) + + }) +}) diff --git a/src/openstack_cpi_golang/cpi/openstack/openstack_suite_test.go b/src/openstack_cpi_golang/cpi/openstack/openstack_suite_test.go new file mode 100644 index 00000000..f78094e4 --- /dev/null +++ b/src/openstack_cpi_golang/cpi/openstack/openstack_suite_test.go @@ -0,0 +1,13 @@ +package openstack_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestMethods(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Openstack Suite") +} diff --git a/src/openstack_cpi_golang/cpi/openstack/openstackfakes/fake_openstack_facade.go b/src/openstack_cpi_golang/cpi/openstack/openstackfakes/fake_openstack_facade.go new file mode 100644 index 00000000..be5032a0 --- /dev/null +++ b/src/openstack_cpi_golang/cpi/openstack/openstackfakes/fake_openstack_facade.go @@ -0,0 +1,522 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package openstackfakes + +import ( + "sync" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/openstack" + "github.com/gophercloud/gophercloud" +) + +type FakeOpenstackFacade struct { + AuthenticatedClientStub func(gophercloud.AuthOptions) (*gophercloud.ProviderClient, error) + authenticatedClientMutex sync.RWMutex + authenticatedClientArgsForCall []struct { + arg1 gophercloud.AuthOptions + } + authenticatedClientReturns struct { + result1 *gophercloud.ProviderClient + result2 error + } + authenticatedClientReturnsOnCall map[int]struct { + result1 *gophercloud.ProviderClient + result2 error + } + NewBlockStorageV3Stub func(*gophercloud.ProviderClient, gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) + newBlockStorageV3Mutex sync.RWMutex + newBlockStorageV3ArgsForCall []struct { + arg1 *gophercloud.ProviderClient + arg2 gophercloud.EndpointOpts + } + newBlockStorageV3Returns struct { + result1 *gophercloud.ServiceClient + result2 error + } + newBlockStorageV3ReturnsOnCall map[int]struct { + result1 *gophercloud.ServiceClient + result2 error + } + NewComputeV2Stub func(*gophercloud.ProviderClient, gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) + newComputeV2Mutex sync.RWMutex + newComputeV2ArgsForCall []struct { + arg1 *gophercloud.ProviderClient + arg2 gophercloud.EndpointOpts + } + newComputeV2Returns struct { + result1 *gophercloud.ServiceClient + result2 error + } + newComputeV2ReturnsOnCall map[int]struct { + result1 *gophercloud.ServiceClient + result2 error + } + NewImageServiceV2Stub func(*gophercloud.ProviderClient, gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) + newImageServiceV2Mutex sync.RWMutex + newImageServiceV2ArgsForCall []struct { + arg1 *gophercloud.ProviderClient + arg2 gophercloud.EndpointOpts + } + newImageServiceV2Returns struct { + result1 *gophercloud.ServiceClient + result2 error + } + newImageServiceV2ReturnsOnCall map[int]struct { + result1 *gophercloud.ServiceClient + result2 error + } + NewLoadBalancerV2Stub func(*gophercloud.ProviderClient, gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) + newLoadBalancerV2Mutex sync.RWMutex + newLoadBalancerV2ArgsForCall []struct { + arg1 *gophercloud.ProviderClient + arg2 gophercloud.EndpointOpts + } + newLoadBalancerV2Returns struct { + result1 *gophercloud.ServiceClient + result2 error + } + newLoadBalancerV2ReturnsOnCall map[int]struct { + result1 *gophercloud.ServiceClient + result2 error + } + NewNetworkV2Stub func(*gophercloud.ProviderClient, gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) + newNetworkV2Mutex sync.RWMutex + newNetworkV2ArgsForCall []struct { + arg1 *gophercloud.ProviderClient + arg2 gophercloud.EndpointOpts + } + newNetworkV2Returns struct { + result1 *gophercloud.ServiceClient + result2 error + } + newNetworkV2ReturnsOnCall map[int]struct { + result1 *gophercloud.ServiceClient + result2 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *FakeOpenstackFacade) AuthenticatedClient(arg1 gophercloud.AuthOptions) (*gophercloud.ProviderClient, error) { + fake.authenticatedClientMutex.Lock() + ret, specificReturn := fake.authenticatedClientReturnsOnCall[len(fake.authenticatedClientArgsForCall)] + fake.authenticatedClientArgsForCall = append(fake.authenticatedClientArgsForCall, struct { + arg1 gophercloud.AuthOptions + }{arg1}) + stub := fake.AuthenticatedClientStub + fakeReturns := fake.authenticatedClientReturns + fake.recordInvocation("AuthenticatedClient", []interface{}{arg1}) + fake.authenticatedClientMutex.Unlock() + if stub != nil { + return stub(arg1) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeOpenstackFacade) AuthenticatedClientCallCount() int { + fake.authenticatedClientMutex.RLock() + defer fake.authenticatedClientMutex.RUnlock() + return len(fake.authenticatedClientArgsForCall) +} + +func (fake *FakeOpenstackFacade) AuthenticatedClientCalls(stub func(gophercloud.AuthOptions) (*gophercloud.ProviderClient, error)) { + fake.authenticatedClientMutex.Lock() + defer fake.authenticatedClientMutex.Unlock() + fake.AuthenticatedClientStub = stub +} + +func (fake *FakeOpenstackFacade) AuthenticatedClientArgsForCall(i int) gophercloud.AuthOptions { + fake.authenticatedClientMutex.RLock() + defer fake.authenticatedClientMutex.RUnlock() + argsForCall := fake.authenticatedClientArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeOpenstackFacade) AuthenticatedClientReturns(result1 *gophercloud.ProviderClient, result2 error) { + fake.authenticatedClientMutex.Lock() + defer fake.authenticatedClientMutex.Unlock() + fake.AuthenticatedClientStub = nil + fake.authenticatedClientReturns = struct { + result1 *gophercloud.ProviderClient + result2 error + }{result1, result2} +} + +func (fake *FakeOpenstackFacade) AuthenticatedClientReturnsOnCall(i int, result1 *gophercloud.ProviderClient, result2 error) { + fake.authenticatedClientMutex.Lock() + defer fake.authenticatedClientMutex.Unlock() + fake.AuthenticatedClientStub = nil + if fake.authenticatedClientReturnsOnCall == nil { + fake.authenticatedClientReturnsOnCall = make(map[int]struct { + result1 *gophercloud.ProviderClient + result2 error + }) + } + fake.authenticatedClientReturnsOnCall[i] = struct { + result1 *gophercloud.ProviderClient + result2 error + }{result1, result2} +} + +func (fake *FakeOpenstackFacade) NewBlockStorageV3(arg1 *gophercloud.ProviderClient, arg2 gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + fake.newBlockStorageV3Mutex.Lock() + ret, specificReturn := fake.newBlockStorageV3ReturnsOnCall[len(fake.newBlockStorageV3ArgsForCall)] + fake.newBlockStorageV3ArgsForCall = append(fake.newBlockStorageV3ArgsForCall, struct { + arg1 *gophercloud.ProviderClient + arg2 gophercloud.EndpointOpts + }{arg1, arg2}) + stub := fake.NewBlockStorageV3Stub + fakeReturns := fake.newBlockStorageV3Returns + fake.recordInvocation("NewBlockStorageV3", []interface{}{arg1, arg2}) + fake.newBlockStorageV3Mutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeOpenstackFacade) NewBlockStorageV3CallCount() int { + fake.newBlockStorageV3Mutex.RLock() + defer fake.newBlockStorageV3Mutex.RUnlock() + return len(fake.newBlockStorageV3ArgsForCall) +} + +func (fake *FakeOpenstackFacade) NewBlockStorageV3Calls(stub func(*gophercloud.ProviderClient, gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error)) { + fake.newBlockStorageV3Mutex.Lock() + defer fake.newBlockStorageV3Mutex.Unlock() + fake.NewBlockStorageV3Stub = stub +} + +func (fake *FakeOpenstackFacade) NewBlockStorageV3ArgsForCall(i int) (*gophercloud.ProviderClient, gophercloud.EndpointOpts) { + fake.newBlockStorageV3Mutex.RLock() + defer fake.newBlockStorageV3Mutex.RUnlock() + argsForCall := fake.newBlockStorageV3ArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeOpenstackFacade) NewBlockStorageV3Returns(result1 *gophercloud.ServiceClient, result2 error) { + fake.newBlockStorageV3Mutex.Lock() + defer fake.newBlockStorageV3Mutex.Unlock() + fake.NewBlockStorageV3Stub = nil + fake.newBlockStorageV3Returns = struct { + result1 *gophercloud.ServiceClient + result2 error + }{result1, result2} +} + +func (fake *FakeOpenstackFacade) NewBlockStorageV3ReturnsOnCall(i int, result1 *gophercloud.ServiceClient, result2 error) { + fake.newBlockStorageV3Mutex.Lock() + defer fake.newBlockStorageV3Mutex.Unlock() + fake.NewBlockStorageV3Stub = nil + if fake.newBlockStorageV3ReturnsOnCall == nil { + fake.newBlockStorageV3ReturnsOnCall = make(map[int]struct { + result1 *gophercloud.ServiceClient + result2 error + }) + } + fake.newBlockStorageV3ReturnsOnCall[i] = struct { + result1 *gophercloud.ServiceClient + result2 error + }{result1, result2} +} + +func (fake *FakeOpenstackFacade) NewComputeV2(arg1 *gophercloud.ProviderClient, arg2 gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + fake.newComputeV2Mutex.Lock() + ret, specificReturn := fake.newComputeV2ReturnsOnCall[len(fake.newComputeV2ArgsForCall)] + fake.newComputeV2ArgsForCall = append(fake.newComputeV2ArgsForCall, struct { + arg1 *gophercloud.ProviderClient + arg2 gophercloud.EndpointOpts + }{arg1, arg2}) + stub := fake.NewComputeV2Stub + fakeReturns := fake.newComputeV2Returns + fake.recordInvocation("NewComputeV2", []interface{}{arg1, arg2}) + fake.newComputeV2Mutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeOpenstackFacade) NewComputeV2CallCount() int { + fake.newComputeV2Mutex.RLock() + defer fake.newComputeV2Mutex.RUnlock() + return len(fake.newComputeV2ArgsForCall) +} + +func (fake *FakeOpenstackFacade) NewComputeV2Calls(stub func(*gophercloud.ProviderClient, gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error)) { + fake.newComputeV2Mutex.Lock() + defer fake.newComputeV2Mutex.Unlock() + fake.NewComputeV2Stub = stub +} + +func (fake *FakeOpenstackFacade) NewComputeV2ArgsForCall(i int) (*gophercloud.ProviderClient, gophercloud.EndpointOpts) { + fake.newComputeV2Mutex.RLock() + defer fake.newComputeV2Mutex.RUnlock() + argsForCall := fake.newComputeV2ArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeOpenstackFacade) NewComputeV2Returns(result1 *gophercloud.ServiceClient, result2 error) { + fake.newComputeV2Mutex.Lock() + defer fake.newComputeV2Mutex.Unlock() + fake.NewComputeV2Stub = nil + fake.newComputeV2Returns = struct { + result1 *gophercloud.ServiceClient + result2 error + }{result1, result2} +} + +func (fake *FakeOpenstackFacade) NewComputeV2ReturnsOnCall(i int, result1 *gophercloud.ServiceClient, result2 error) { + fake.newComputeV2Mutex.Lock() + defer fake.newComputeV2Mutex.Unlock() + fake.NewComputeV2Stub = nil + if fake.newComputeV2ReturnsOnCall == nil { + fake.newComputeV2ReturnsOnCall = make(map[int]struct { + result1 *gophercloud.ServiceClient + result2 error + }) + } + fake.newComputeV2ReturnsOnCall[i] = struct { + result1 *gophercloud.ServiceClient + result2 error + }{result1, result2} +} + +func (fake *FakeOpenstackFacade) NewImageServiceV2(arg1 *gophercloud.ProviderClient, arg2 gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + fake.newImageServiceV2Mutex.Lock() + ret, specificReturn := fake.newImageServiceV2ReturnsOnCall[len(fake.newImageServiceV2ArgsForCall)] + fake.newImageServiceV2ArgsForCall = append(fake.newImageServiceV2ArgsForCall, struct { + arg1 *gophercloud.ProviderClient + arg2 gophercloud.EndpointOpts + }{arg1, arg2}) + stub := fake.NewImageServiceV2Stub + fakeReturns := fake.newImageServiceV2Returns + fake.recordInvocation("NewImageServiceV2", []interface{}{arg1, arg2}) + fake.newImageServiceV2Mutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeOpenstackFacade) NewImageServiceV2CallCount() int { + fake.newImageServiceV2Mutex.RLock() + defer fake.newImageServiceV2Mutex.RUnlock() + return len(fake.newImageServiceV2ArgsForCall) +} + +func (fake *FakeOpenstackFacade) NewImageServiceV2Calls(stub func(*gophercloud.ProviderClient, gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error)) { + fake.newImageServiceV2Mutex.Lock() + defer fake.newImageServiceV2Mutex.Unlock() + fake.NewImageServiceV2Stub = stub +} + +func (fake *FakeOpenstackFacade) NewImageServiceV2ArgsForCall(i int) (*gophercloud.ProviderClient, gophercloud.EndpointOpts) { + fake.newImageServiceV2Mutex.RLock() + defer fake.newImageServiceV2Mutex.RUnlock() + argsForCall := fake.newImageServiceV2ArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeOpenstackFacade) NewImageServiceV2Returns(result1 *gophercloud.ServiceClient, result2 error) { + fake.newImageServiceV2Mutex.Lock() + defer fake.newImageServiceV2Mutex.Unlock() + fake.NewImageServiceV2Stub = nil + fake.newImageServiceV2Returns = struct { + result1 *gophercloud.ServiceClient + result2 error + }{result1, result2} +} + +func (fake *FakeOpenstackFacade) NewImageServiceV2ReturnsOnCall(i int, result1 *gophercloud.ServiceClient, result2 error) { + fake.newImageServiceV2Mutex.Lock() + defer fake.newImageServiceV2Mutex.Unlock() + fake.NewImageServiceV2Stub = nil + if fake.newImageServiceV2ReturnsOnCall == nil { + fake.newImageServiceV2ReturnsOnCall = make(map[int]struct { + result1 *gophercloud.ServiceClient + result2 error + }) + } + fake.newImageServiceV2ReturnsOnCall[i] = struct { + result1 *gophercloud.ServiceClient + result2 error + }{result1, result2} +} + +func (fake *FakeOpenstackFacade) NewLoadBalancerV2(arg1 *gophercloud.ProviderClient, arg2 gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + fake.newLoadBalancerV2Mutex.Lock() + ret, specificReturn := fake.newLoadBalancerV2ReturnsOnCall[len(fake.newLoadBalancerV2ArgsForCall)] + fake.newLoadBalancerV2ArgsForCall = append(fake.newLoadBalancerV2ArgsForCall, struct { + arg1 *gophercloud.ProviderClient + arg2 gophercloud.EndpointOpts + }{arg1, arg2}) + stub := fake.NewLoadBalancerV2Stub + fakeReturns := fake.newLoadBalancerV2Returns + fake.recordInvocation("NewLoadBalancerV2", []interface{}{arg1, arg2}) + fake.newLoadBalancerV2Mutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeOpenstackFacade) NewLoadBalancerV2CallCount() int { + fake.newLoadBalancerV2Mutex.RLock() + defer fake.newLoadBalancerV2Mutex.RUnlock() + return len(fake.newLoadBalancerV2ArgsForCall) +} + +func (fake *FakeOpenstackFacade) NewLoadBalancerV2Calls(stub func(*gophercloud.ProviderClient, gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error)) { + fake.newLoadBalancerV2Mutex.Lock() + defer fake.newLoadBalancerV2Mutex.Unlock() + fake.NewLoadBalancerV2Stub = stub +} + +func (fake *FakeOpenstackFacade) NewLoadBalancerV2ArgsForCall(i int) (*gophercloud.ProviderClient, gophercloud.EndpointOpts) { + fake.newLoadBalancerV2Mutex.RLock() + defer fake.newLoadBalancerV2Mutex.RUnlock() + argsForCall := fake.newLoadBalancerV2ArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeOpenstackFacade) NewLoadBalancerV2Returns(result1 *gophercloud.ServiceClient, result2 error) { + fake.newLoadBalancerV2Mutex.Lock() + defer fake.newLoadBalancerV2Mutex.Unlock() + fake.NewLoadBalancerV2Stub = nil + fake.newLoadBalancerV2Returns = struct { + result1 *gophercloud.ServiceClient + result2 error + }{result1, result2} +} + +func (fake *FakeOpenstackFacade) NewLoadBalancerV2ReturnsOnCall(i int, result1 *gophercloud.ServiceClient, result2 error) { + fake.newLoadBalancerV2Mutex.Lock() + defer fake.newLoadBalancerV2Mutex.Unlock() + fake.NewLoadBalancerV2Stub = nil + if fake.newLoadBalancerV2ReturnsOnCall == nil { + fake.newLoadBalancerV2ReturnsOnCall = make(map[int]struct { + result1 *gophercloud.ServiceClient + result2 error + }) + } + fake.newLoadBalancerV2ReturnsOnCall[i] = struct { + result1 *gophercloud.ServiceClient + result2 error + }{result1, result2} +} + +func (fake *FakeOpenstackFacade) NewNetworkV2(arg1 *gophercloud.ProviderClient, arg2 gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + fake.newNetworkV2Mutex.Lock() + ret, specificReturn := fake.newNetworkV2ReturnsOnCall[len(fake.newNetworkV2ArgsForCall)] + fake.newNetworkV2ArgsForCall = append(fake.newNetworkV2ArgsForCall, struct { + arg1 *gophercloud.ProviderClient + arg2 gophercloud.EndpointOpts + }{arg1, arg2}) + stub := fake.NewNetworkV2Stub + fakeReturns := fake.newNetworkV2Returns + fake.recordInvocation("NewNetworkV2", []interface{}{arg1, arg2}) + fake.newNetworkV2Mutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeOpenstackFacade) NewNetworkV2CallCount() int { + fake.newNetworkV2Mutex.RLock() + defer fake.newNetworkV2Mutex.RUnlock() + return len(fake.newNetworkV2ArgsForCall) +} + +func (fake *FakeOpenstackFacade) NewNetworkV2Calls(stub func(*gophercloud.ProviderClient, gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error)) { + fake.newNetworkV2Mutex.Lock() + defer fake.newNetworkV2Mutex.Unlock() + fake.NewNetworkV2Stub = stub +} + +func (fake *FakeOpenstackFacade) NewNetworkV2ArgsForCall(i int) (*gophercloud.ProviderClient, gophercloud.EndpointOpts) { + fake.newNetworkV2Mutex.RLock() + defer fake.newNetworkV2Mutex.RUnlock() + argsForCall := fake.newNetworkV2ArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeOpenstackFacade) NewNetworkV2Returns(result1 *gophercloud.ServiceClient, result2 error) { + fake.newNetworkV2Mutex.Lock() + defer fake.newNetworkV2Mutex.Unlock() + fake.NewNetworkV2Stub = nil + fake.newNetworkV2Returns = struct { + result1 *gophercloud.ServiceClient + result2 error + }{result1, result2} +} + +func (fake *FakeOpenstackFacade) NewNetworkV2ReturnsOnCall(i int, result1 *gophercloud.ServiceClient, result2 error) { + fake.newNetworkV2Mutex.Lock() + defer fake.newNetworkV2Mutex.Unlock() + fake.NewNetworkV2Stub = nil + if fake.newNetworkV2ReturnsOnCall == nil { + fake.newNetworkV2ReturnsOnCall = make(map[int]struct { + result1 *gophercloud.ServiceClient + result2 error + }) + } + fake.newNetworkV2ReturnsOnCall[i] = struct { + result1 *gophercloud.ServiceClient + result2 error + }{result1, result2} +} + +func (fake *FakeOpenstackFacade) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.authenticatedClientMutex.RLock() + defer fake.authenticatedClientMutex.RUnlock() + fake.newBlockStorageV3Mutex.RLock() + defer fake.newBlockStorageV3Mutex.RUnlock() + fake.newComputeV2Mutex.RLock() + defer fake.newComputeV2Mutex.RUnlock() + fake.newImageServiceV2Mutex.RLock() + defer fake.newImageServiceV2Mutex.RUnlock() + fake.newLoadBalancerV2Mutex.RLock() + defer fake.newLoadBalancerV2Mutex.RUnlock() + fake.newNetworkV2Mutex.RLock() + defer fake.newNetworkV2Mutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *FakeOpenstackFacade) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ openstack.OpenstackFacade = new(FakeOpenstackFacade) diff --git a/src/openstack_cpi_golang/cpi/openstack/openstackfakes/fake_openstack_service.go b/src/openstack_cpi_golang/cpi/openstack/openstackfakes/fake_openstack_service.go new file mode 100644 index 00000000..4dad44f6 --- /dev/null +++ b/src/openstack_cpi_golang/cpi/openstack/openstackfakes/fake_openstack_service.go @@ -0,0 +1,434 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package openstackfakes + +import ( + "sync" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/config" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/openstack" + "github.com/gophercloud/gophercloud" +) + +type FakeOpenstackService struct { + BlockStorageV3Stub func(config.OpenstackConfig) (*gophercloud.ServiceClient, error) + blockStorageV3Mutex sync.RWMutex + blockStorageV3ArgsForCall []struct { + arg1 config.OpenstackConfig + } + blockStorageV3Returns struct { + result1 *gophercloud.ServiceClient + result2 error + } + blockStorageV3ReturnsOnCall map[int]struct { + result1 *gophercloud.ServiceClient + result2 error + } + ComputeServiceV2Stub func(config.OpenstackConfig) (*gophercloud.ServiceClient, error) + computeServiceV2Mutex sync.RWMutex + computeServiceV2ArgsForCall []struct { + arg1 config.OpenstackConfig + } + computeServiceV2Returns struct { + result1 *gophercloud.ServiceClient + result2 error + } + computeServiceV2ReturnsOnCall map[int]struct { + result1 *gophercloud.ServiceClient + result2 error + } + ImageServiceV2Stub func(config.OpenstackConfig) (*gophercloud.ServiceClient, error) + imageServiceV2Mutex sync.RWMutex + imageServiceV2ArgsForCall []struct { + arg1 config.OpenstackConfig + } + imageServiceV2Returns struct { + result1 *gophercloud.ServiceClient + result2 error + } + imageServiceV2ReturnsOnCall map[int]struct { + result1 *gophercloud.ServiceClient + result2 error + } + LoadbalancerV2Stub func(config.OpenstackConfig) (*gophercloud.ServiceClient, error) + loadbalancerV2Mutex sync.RWMutex + loadbalancerV2ArgsForCall []struct { + arg1 config.OpenstackConfig + } + loadbalancerV2Returns struct { + result1 *gophercloud.ServiceClient + result2 error + } + loadbalancerV2ReturnsOnCall map[int]struct { + result1 *gophercloud.ServiceClient + result2 error + } + NetworkServiceV2Stub func(config.OpenstackConfig) (*gophercloud.ServiceClient, error) + networkServiceV2Mutex sync.RWMutex + networkServiceV2ArgsForCall []struct { + arg1 config.OpenstackConfig + } + networkServiceV2Returns struct { + result1 *gophercloud.ServiceClient + result2 error + } + networkServiceV2ReturnsOnCall map[int]struct { + result1 *gophercloud.ServiceClient + result2 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *FakeOpenstackService) BlockStorageV3(arg1 config.OpenstackConfig) (*gophercloud.ServiceClient, error) { + fake.blockStorageV3Mutex.Lock() + ret, specificReturn := fake.blockStorageV3ReturnsOnCall[len(fake.blockStorageV3ArgsForCall)] + fake.blockStorageV3ArgsForCall = append(fake.blockStorageV3ArgsForCall, struct { + arg1 config.OpenstackConfig + }{arg1}) + stub := fake.BlockStorageV3Stub + fakeReturns := fake.blockStorageV3Returns + fake.recordInvocation("BlockStorageV3", []interface{}{arg1}) + fake.blockStorageV3Mutex.Unlock() + if stub != nil { + return stub(arg1) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeOpenstackService) BlockStorageV3CallCount() int { + fake.blockStorageV3Mutex.RLock() + defer fake.blockStorageV3Mutex.RUnlock() + return len(fake.blockStorageV3ArgsForCall) +} + +func (fake *FakeOpenstackService) BlockStorageV3Calls(stub func(config.OpenstackConfig) (*gophercloud.ServiceClient, error)) { + fake.blockStorageV3Mutex.Lock() + defer fake.blockStorageV3Mutex.Unlock() + fake.BlockStorageV3Stub = stub +} + +func (fake *FakeOpenstackService) BlockStorageV3ArgsForCall(i int) config.OpenstackConfig { + fake.blockStorageV3Mutex.RLock() + defer fake.blockStorageV3Mutex.RUnlock() + argsForCall := fake.blockStorageV3ArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeOpenstackService) BlockStorageV3Returns(result1 *gophercloud.ServiceClient, result2 error) { + fake.blockStorageV3Mutex.Lock() + defer fake.blockStorageV3Mutex.Unlock() + fake.BlockStorageV3Stub = nil + fake.blockStorageV3Returns = struct { + result1 *gophercloud.ServiceClient + result2 error + }{result1, result2} +} + +func (fake *FakeOpenstackService) BlockStorageV3ReturnsOnCall(i int, result1 *gophercloud.ServiceClient, result2 error) { + fake.blockStorageV3Mutex.Lock() + defer fake.blockStorageV3Mutex.Unlock() + fake.BlockStorageV3Stub = nil + if fake.blockStorageV3ReturnsOnCall == nil { + fake.blockStorageV3ReturnsOnCall = make(map[int]struct { + result1 *gophercloud.ServiceClient + result2 error + }) + } + fake.blockStorageV3ReturnsOnCall[i] = struct { + result1 *gophercloud.ServiceClient + result2 error + }{result1, result2} +} + +func (fake *FakeOpenstackService) ComputeServiceV2(arg1 config.OpenstackConfig) (*gophercloud.ServiceClient, error) { + fake.computeServiceV2Mutex.Lock() + ret, specificReturn := fake.computeServiceV2ReturnsOnCall[len(fake.computeServiceV2ArgsForCall)] + fake.computeServiceV2ArgsForCall = append(fake.computeServiceV2ArgsForCall, struct { + arg1 config.OpenstackConfig + }{arg1}) + stub := fake.ComputeServiceV2Stub + fakeReturns := fake.computeServiceV2Returns + fake.recordInvocation("ComputeServiceV2", []interface{}{arg1}) + fake.computeServiceV2Mutex.Unlock() + if stub != nil { + return stub(arg1) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeOpenstackService) ComputeServiceV2CallCount() int { + fake.computeServiceV2Mutex.RLock() + defer fake.computeServiceV2Mutex.RUnlock() + return len(fake.computeServiceV2ArgsForCall) +} + +func (fake *FakeOpenstackService) ComputeServiceV2Calls(stub func(config.OpenstackConfig) (*gophercloud.ServiceClient, error)) { + fake.computeServiceV2Mutex.Lock() + defer fake.computeServiceV2Mutex.Unlock() + fake.ComputeServiceV2Stub = stub +} + +func (fake *FakeOpenstackService) ComputeServiceV2ArgsForCall(i int) config.OpenstackConfig { + fake.computeServiceV2Mutex.RLock() + defer fake.computeServiceV2Mutex.RUnlock() + argsForCall := fake.computeServiceV2ArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeOpenstackService) ComputeServiceV2Returns(result1 *gophercloud.ServiceClient, result2 error) { + fake.computeServiceV2Mutex.Lock() + defer fake.computeServiceV2Mutex.Unlock() + fake.ComputeServiceV2Stub = nil + fake.computeServiceV2Returns = struct { + result1 *gophercloud.ServiceClient + result2 error + }{result1, result2} +} + +func (fake *FakeOpenstackService) ComputeServiceV2ReturnsOnCall(i int, result1 *gophercloud.ServiceClient, result2 error) { + fake.computeServiceV2Mutex.Lock() + defer fake.computeServiceV2Mutex.Unlock() + fake.ComputeServiceV2Stub = nil + if fake.computeServiceV2ReturnsOnCall == nil { + fake.computeServiceV2ReturnsOnCall = make(map[int]struct { + result1 *gophercloud.ServiceClient + result2 error + }) + } + fake.computeServiceV2ReturnsOnCall[i] = struct { + result1 *gophercloud.ServiceClient + result2 error + }{result1, result2} +} + +func (fake *FakeOpenstackService) ImageServiceV2(arg1 config.OpenstackConfig) (*gophercloud.ServiceClient, error) { + fake.imageServiceV2Mutex.Lock() + ret, specificReturn := fake.imageServiceV2ReturnsOnCall[len(fake.imageServiceV2ArgsForCall)] + fake.imageServiceV2ArgsForCall = append(fake.imageServiceV2ArgsForCall, struct { + arg1 config.OpenstackConfig + }{arg1}) + stub := fake.ImageServiceV2Stub + fakeReturns := fake.imageServiceV2Returns + fake.recordInvocation("ImageServiceV2", []interface{}{arg1}) + fake.imageServiceV2Mutex.Unlock() + if stub != nil { + return stub(arg1) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeOpenstackService) ImageServiceV2CallCount() int { + fake.imageServiceV2Mutex.RLock() + defer fake.imageServiceV2Mutex.RUnlock() + return len(fake.imageServiceV2ArgsForCall) +} + +func (fake *FakeOpenstackService) ImageServiceV2Calls(stub func(config.OpenstackConfig) (*gophercloud.ServiceClient, error)) { + fake.imageServiceV2Mutex.Lock() + defer fake.imageServiceV2Mutex.Unlock() + fake.ImageServiceV2Stub = stub +} + +func (fake *FakeOpenstackService) ImageServiceV2ArgsForCall(i int) config.OpenstackConfig { + fake.imageServiceV2Mutex.RLock() + defer fake.imageServiceV2Mutex.RUnlock() + argsForCall := fake.imageServiceV2ArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeOpenstackService) ImageServiceV2Returns(result1 *gophercloud.ServiceClient, result2 error) { + fake.imageServiceV2Mutex.Lock() + defer fake.imageServiceV2Mutex.Unlock() + fake.ImageServiceV2Stub = nil + fake.imageServiceV2Returns = struct { + result1 *gophercloud.ServiceClient + result2 error + }{result1, result2} +} + +func (fake *FakeOpenstackService) ImageServiceV2ReturnsOnCall(i int, result1 *gophercloud.ServiceClient, result2 error) { + fake.imageServiceV2Mutex.Lock() + defer fake.imageServiceV2Mutex.Unlock() + fake.ImageServiceV2Stub = nil + if fake.imageServiceV2ReturnsOnCall == nil { + fake.imageServiceV2ReturnsOnCall = make(map[int]struct { + result1 *gophercloud.ServiceClient + result2 error + }) + } + fake.imageServiceV2ReturnsOnCall[i] = struct { + result1 *gophercloud.ServiceClient + result2 error + }{result1, result2} +} + +func (fake *FakeOpenstackService) LoadbalancerV2(arg1 config.OpenstackConfig) (*gophercloud.ServiceClient, error) { + fake.loadbalancerV2Mutex.Lock() + ret, specificReturn := fake.loadbalancerV2ReturnsOnCall[len(fake.loadbalancerV2ArgsForCall)] + fake.loadbalancerV2ArgsForCall = append(fake.loadbalancerV2ArgsForCall, struct { + arg1 config.OpenstackConfig + }{arg1}) + stub := fake.LoadbalancerV2Stub + fakeReturns := fake.loadbalancerV2Returns + fake.recordInvocation("LoadbalancerV2", []interface{}{arg1}) + fake.loadbalancerV2Mutex.Unlock() + if stub != nil { + return stub(arg1) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeOpenstackService) LoadbalancerV2CallCount() int { + fake.loadbalancerV2Mutex.RLock() + defer fake.loadbalancerV2Mutex.RUnlock() + return len(fake.loadbalancerV2ArgsForCall) +} + +func (fake *FakeOpenstackService) LoadbalancerV2Calls(stub func(config.OpenstackConfig) (*gophercloud.ServiceClient, error)) { + fake.loadbalancerV2Mutex.Lock() + defer fake.loadbalancerV2Mutex.Unlock() + fake.LoadbalancerV2Stub = stub +} + +func (fake *FakeOpenstackService) LoadbalancerV2ArgsForCall(i int) config.OpenstackConfig { + fake.loadbalancerV2Mutex.RLock() + defer fake.loadbalancerV2Mutex.RUnlock() + argsForCall := fake.loadbalancerV2ArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeOpenstackService) LoadbalancerV2Returns(result1 *gophercloud.ServiceClient, result2 error) { + fake.loadbalancerV2Mutex.Lock() + defer fake.loadbalancerV2Mutex.Unlock() + fake.LoadbalancerV2Stub = nil + fake.loadbalancerV2Returns = struct { + result1 *gophercloud.ServiceClient + result2 error + }{result1, result2} +} + +func (fake *FakeOpenstackService) LoadbalancerV2ReturnsOnCall(i int, result1 *gophercloud.ServiceClient, result2 error) { + fake.loadbalancerV2Mutex.Lock() + defer fake.loadbalancerV2Mutex.Unlock() + fake.LoadbalancerV2Stub = nil + if fake.loadbalancerV2ReturnsOnCall == nil { + fake.loadbalancerV2ReturnsOnCall = make(map[int]struct { + result1 *gophercloud.ServiceClient + result2 error + }) + } + fake.loadbalancerV2ReturnsOnCall[i] = struct { + result1 *gophercloud.ServiceClient + result2 error + }{result1, result2} +} + +func (fake *FakeOpenstackService) NetworkServiceV2(arg1 config.OpenstackConfig) (*gophercloud.ServiceClient, error) { + fake.networkServiceV2Mutex.Lock() + ret, specificReturn := fake.networkServiceV2ReturnsOnCall[len(fake.networkServiceV2ArgsForCall)] + fake.networkServiceV2ArgsForCall = append(fake.networkServiceV2ArgsForCall, struct { + arg1 config.OpenstackConfig + }{arg1}) + stub := fake.NetworkServiceV2Stub + fakeReturns := fake.networkServiceV2Returns + fake.recordInvocation("NetworkServiceV2", []interface{}{arg1}) + fake.networkServiceV2Mutex.Unlock() + if stub != nil { + return stub(arg1) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeOpenstackService) NetworkServiceV2CallCount() int { + fake.networkServiceV2Mutex.RLock() + defer fake.networkServiceV2Mutex.RUnlock() + return len(fake.networkServiceV2ArgsForCall) +} + +func (fake *FakeOpenstackService) NetworkServiceV2Calls(stub func(config.OpenstackConfig) (*gophercloud.ServiceClient, error)) { + fake.networkServiceV2Mutex.Lock() + defer fake.networkServiceV2Mutex.Unlock() + fake.NetworkServiceV2Stub = stub +} + +func (fake *FakeOpenstackService) NetworkServiceV2ArgsForCall(i int) config.OpenstackConfig { + fake.networkServiceV2Mutex.RLock() + defer fake.networkServiceV2Mutex.RUnlock() + argsForCall := fake.networkServiceV2ArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeOpenstackService) NetworkServiceV2Returns(result1 *gophercloud.ServiceClient, result2 error) { + fake.networkServiceV2Mutex.Lock() + defer fake.networkServiceV2Mutex.Unlock() + fake.NetworkServiceV2Stub = nil + fake.networkServiceV2Returns = struct { + result1 *gophercloud.ServiceClient + result2 error + }{result1, result2} +} + +func (fake *FakeOpenstackService) NetworkServiceV2ReturnsOnCall(i int, result1 *gophercloud.ServiceClient, result2 error) { + fake.networkServiceV2Mutex.Lock() + defer fake.networkServiceV2Mutex.Unlock() + fake.NetworkServiceV2Stub = nil + if fake.networkServiceV2ReturnsOnCall == nil { + fake.networkServiceV2ReturnsOnCall = make(map[int]struct { + result1 *gophercloud.ServiceClient + result2 error + }) + } + fake.networkServiceV2ReturnsOnCall[i] = struct { + result1 *gophercloud.ServiceClient + result2 error + }{result1, result2} +} + +func (fake *FakeOpenstackService) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.blockStorageV3Mutex.RLock() + defer fake.blockStorageV3Mutex.RUnlock() + fake.computeServiceV2Mutex.RLock() + defer fake.computeServiceV2Mutex.RUnlock() + fake.imageServiceV2Mutex.RLock() + defer fake.imageServiceV2Mutex.RUnlock() + fake.loadbalancerV2Mutex.RLock() + defer fake.loadbalancerV2Mutex.RUnlock() + fake.networkServiceV2Mutex.RLock() + defer fake.networkServiceV2Mutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *FakeOpenstackService) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ openstack.OpenstackService = new(FakeOpenstackService) diff --git a/src/openstack_cpi_golang/cpi/properties/calculate_vm_cloud_props.go b/src/openstack_cpi_golang/cpi/properties/calculate_vm_cloud_props.go new file mode 100644 index 00000000..ebbe5845 --- /dev/null +++ b/src/openstack_cpi_golang/cpi/properties/calculate_vm_cloud_props.go @@ -0,0 +1,3 @@ +package properties + +const OsOverheadInGb = 3 diff --git a/src/openstack_cpi_golang/cpi/properties/create_disk_cloud_props.go b/src/openstack_cpi_golang/cpi/properties/create_disk_cloud_props.go new file mode 100644 index 00000000..fb5ffdcf --- /dev/null +++ b/src/openstack_cpi_golang/cpi/properties/create_disk_cloud_props.go @@ -0,0 +1,5 @@ +package properties + +type CreateDisk struct { + VolumeType string `json:"type"` +} diff --git a/src/openstack_cpi_golang/cpi/properties/create_stemcell.go b/src/openstack_cpi_golang/cpi/properties/create_stemcell.go new file mode 100644 index 00000000..3e8092ab --- /dev/null +++ b/src/openstack_cpi_golang/cpi/properties/create_stemcell.go @@ -0,0 +1,19 @@ +package properties + +type CreateStemcell struct { + Version string `json:"version"` + ImageID string `json:"image_id"` + Name string `json:"name"` + DiskFormat string `json:"disk_format"` + ContainerFormat string `json:"container_format"` + OsType string `json:"os_type"` + OsDistro string `json:"os_distro"` + Architecture string `json:"architecture"` + AutoDiskConfig bool `json:"auto_disk_config"` + HwVifModel string `json:"hw_vif_model"` + Hypervisor string `json:"hypervisor"` + VmwareAdapterType string `json:"vmware_adaptertype"` + VmwareDiskType string `json:"vmware_disktype"` + VmwareLinkedClone string `json:"vmware_linked_clone"` + VmvareOsType string `json:"vmware_ostype"` +} diff --git a/src/openstack_cpi_golang/cpi/properties/create_vm_cloud_props.go b/src/openstack_cpi_golang/cpi/properties/create_vm_cloud_props.go new file mode 100644 index 00000000..755e077d --- /dev/null +++ b/src/openstack_cpi_golang/cpi/properties/create_vm_cloud_props.go @@ -0,0 +1,53 @@ +package properties + +import ( + "fmt" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/config" +) + +type CreateVM struct { + AllowedAddressPairs string `json:"allowed_address_pairs"` + AvailabilityZone string `json:"availability_zone"` + AvailabilityZones []string `json:"availability_zones"` + BootFromVolume *bool `json:"boot_from_volume,omitempty"` + EphemeralDisk string `json:"ephemeral_disk"` + InstanceType string `json:"instance_type"` + KeyName string `json:"key_name"` + LoadbalancerPools []LoadbalancerPool `json:"loadbalancer_pools"` + RootDisk Disk `json:"root_disk,omitempty"` + SchedulerHints string `json:"scheduler_hints"` + SecurityGroups []string `json:"security_groups"` + VRRPPortCheck *bool `json:"vrrp_port_check,omitempty"` +} + +type Disk struct { + Size int `json:"size"` +} + +type LoadbalancerPool struct { + Name string `json:"name"` + ProtocolPort int `json:"port"` + MonitoringPort *int `json:"monitoring_port,omitempty"` +} + +func (c CreateVM) Validate(opentackConfig config.OpenstackConfig) error { + + for _, pool := range c.LoadbalancerPools { + if pool.Name == "" { + return fmt.Errorf("load balancer pool defined without name") + } + if pool.ProtocolPort == 0 { + return fmt.Errorf("load balancer pool '%s' has no port definition", pool.Name) + } + } + + if c.AvailabilityZone != "" && len(c.AvailabilityZones) > 0 { + return fmt.Errorf("only one property of 'availability_zone' and 'availability_zones' can be configured") + } + + if len(c.AvailabilityZones) > 1 && !opentackConfig.IgnoreServerAvailabilityZone { + return fmt.Errorf("cannot use multiple azs without 'openstack.ignore_server_availability_zone' set to true") + } + return nil +} diff --git a/src/openstack_cpi_golang/cpi/properties/create_vm_cloud_props_test.go b/src/openstack_cpi_golang/cpi/properties/create_vm_cloud_props_test.go new file mode 100644 index 00000000..14fe1618 --- /dev/null +++ b/src/openstack_cpi_golang/cpi/properties/create_vm_cloud_props_test.go @@ -0,0 +1,62 @@ +package properties_test + +import ( + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/config" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/properties" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("CreateVM", func() { + + Context("Validate", func() { + + var openstackConfig config.OpenstackConfig + + BeforeEach(func() { + openstackConfig = config.OpenstackConfig{} + }) + + It("returns an error if a loadbalancer has no name", func() { + cloudProps := properties.CreateVM{ + LoadbalancerPools: []properties.LoadbalancerPool{{ProtocolPort: 1234}}, + } + + err := cloudProps.Validate(openstackConfig) + + Expect(err.Error()).To(Equal("load balancer pool defined without name")) + }) + + It("returns an error if a loadbalancer has no port", func() { + cloudProps := properties.CreateVM{ + LoadbalancerPools: []properties.LoadbalancerPool{{Name: "name"}}, + } + + err := cloudProps.Validate(openstackConfig) + + Expect(err.Error()).To(Equal("load balancer pool 'name' has no port definition")) + }) + + It("returns an error if 'availability_zone' and 'availability_zones' is configured", func() { + cloudProps := properties.CreateVM{ + AvailabilityZone: "az1", + AvailabilityZones: []string{"az1", "az2"}, + } + + err := cloudProps.Validate(openstackConfig) + + Expect(err.Error()).To(Equal("only one property of 'availability_zone' and 'availability_zones' can be configured")) + }) + + It("returns an error if 'availability_zones' are configured without ignore_server_availability_zone", func() { + cloudProps := properties.CreateVM{ + AvailabilityZones: []string{"az1", "az2"}, + } + + err := cloudProps.Validate(openstackConfig) + + Expect(err.Error()).To(Equal("cannot use multiple azs without 'openstack.ignore_server_availability_zone' set to true")) + }) + + }) +}) diff --git a/src/openstack_cpi_golang/cpi/properties/create_vm_user_data.go b/src/openstack_cpi_golang/cpi/properties/create_vm_user_data.go new file mode 100644 index 00000000..4a503e50 --- /dev/null +++ b/src/openstack_cpi_golang/cpi/properties/create_vm_user_data.go @@ -0,0 +1,50 @@ +package properties + +import "encoding/json" + +type UserData struct { + Server Server `json:"server"` + Networks map[string]UserdataNetwork `json:"networks"` + DNS DNS `json:"dns"` + VM VM `json:"vm"` + Disks Disks `json:"disks"` + AgentID string `json:"agent_id"` + Env json.RawMessage `json:"env"` + MBus string `json:"mbus"` +} + +type Server struct { + Name string `json:"name"` +} + +type OpenSSH struct { + PublicKey string `json:"public_key"` +} + +type UserdataNetwork struct { + Default []string `json:"default"` + DNS []string `json:"dns"` + IP string `json:"ip"` + Gateway string `json:"gateway,omitempty"` + Mac string `json:"mac,omitempty"` + Netmask string `json:"netmask,omitempty"` + Preconfigured *bool `json:"preconfigured,omitempty"` + Resolved *bool `json:"resolved,omitempty"` + Type string `json:"type"` + UseDHCP *bool `json:"use_dhcp,omitempty"` + CloudProps NetworkCloudProps `json:"cloud_properties"` +} + +type DNS struct { + Nameserver []string `json:"nameserver"` +} + +type VM struct { + Name string `json:"name"` +} + +type Disks struct { + System string `json:"system"` + Persistent struct{} `json:"persistent"` + Ephemeral string `json:"ephemeral"` +} diff --git a/src/openstack_cpi_golang/cpi/properties/network_configuration.go b/src/openstack_cpi_golang/cpi/properties/network_configuration.go new file mode 100644 index 00000000..7929207c --- /dev/null +++ b/src/openstack_cpi_golang/cpi/properties/network_configuration.go @@ -0,0 +1,50 @@ +package properties + +import ( + "github.com/gophercloud/gophercloud/openstack/networking/v2/ports" +) + +type NetworkConfig struct { + DefaultNetwork Network + ManualNetworks []Network + VIPNetwork *Network + DynamicNetwork *Network + SecurityGroups []string +} + +type Network struct { + Key string + Port ports.Port + Default []string `json:"default"` + DNS []string `json:"dns"` + IP string `json:"ip,omitempty"` + Gateway string `json:"gateway,omitempty"` + Netmask string `json:"netmask,omitempty"` + Type string `json:"type"` + CloudProps NetworkCloudProps `json:"cloud_properties"` + Mac string `json:"mac,omitempty"` +} + +func (n *Network) ConfigurePort(port ports.Port) { + n.Port = port + n.Mac = port.MACAddress +} + +type NetworkCloudProps struct { + NetID string `json:"net_id,omitempty"` + SecurityGroups []string `json:"security_groups,omitempty"` +} + +func (n *NetworkConfig) AllNetworks() []Network { + networks := n.ManualNetworks + + if n.DynamicNetwork != nil { + networks = append(networks, *n.DynamicNetwork) + } + + if n.VIPNetwork != nil { + networks = append(networks, *n.VIPNetwork) + } + + return networks +} diff --git a/src/openstack_cpi_golang/cpi/properties/network_configuration_test.go b/src/openstack_cpi_golang/cpi/properties/network_configuration_test.go new file mode 100644 index 00000000..28ed4474 --- /dev/null +++ b/src/openstack_cpi_golang/cpi/properties/network_configuration_test.go @@ -0,0 +1,53 @@ +package properties_test + +import ( + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/properties" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("NetworkConfig", func() { + + Context("AllNetworks", func() { + + var manualNetwork1 properties.Network + var manualNetwork2 properties.Network + var vipNetwork *properties.Network + var dynamicNetwork *properties.Network + + BeforeEach(func() { + manualNetwork1 = properties.Network{Type: "manual", Key: "1"} + manualNetwork2 = properties.Network{Type: "manual", Key: "2"} + vipNetwork = &properties.Network{Type: "vip", Key: "3"} + dynamicNetwork = &properties.Network{Type: "dynamic", Key: "4"} + }) + + It("returns a list of manual, dynamic and vip networks", func() { + networkConfig := properties.NetworkConfig{ + ManualNetworks: []properties.Network{manualNetwork1, manualNetwork2}, + VIPNetwork: vipNetwork, + DynamicNetwork: dynamicNetwork, + } + + Expect(networkConfig.AllNetworks()).To(ContainElements(manualNetwork1, manualNetwork2, *vipNetwork, *dynamicNetwork)) + }) + + It("skips adding dynamic network, if not defined", func() { + networkConfig := properties.NetworkConfig{ + ManualNetworks: []properties.Network{manualNetwork1, manualNetwork2}, + VIPNetwork: vipNetwork, + } + + Expect(networkConfig.AllNetworks()).To(ContainElements(manualNetwork1, manualNetwork2, *vipNetwork)) + }) + + It("skips adding vip network, if not defined", func() { + networkConfig := properties.NetworkConfig{ + ManualNetworks: []properties.Network{manualNetwork1, manualNetwork2}, + DynamicNetwork: dynamicNetwork, + } + + Expect(networkConfig.AllNetworks()).To(ContainElements(manualNetwork1, manualNetwork2, *dynamicNetwork)) + }) + }) +}) diff --git a/src/openstack_cpi_golang/cpi/properties/properties_suite_test.go b/src/openstack_cpi_golang/cpi/properties/properties_suite_test.go new file mode 100644 index 00000000..e4196cfc --- /dev/null +++ b/src/openstack_cpi_golang/cpi/properties/properties_suite_test.go @@ -0,0 +1,13 @@ +package properties_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestMethods(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Properties Suite") +} diff --git a/src/openstack_cpi_golang/cpi/properties/server_tags.go b/src/openstack_cpi_golang/cpi/properties/server_tags.go new file mode 100644 index 00000000..70033a53 --- /dev/null +++ b/src/openstack_cpi_golang/cpi/properties/server_tags.go @@ -0,0 +1,3 @@ +package properties + +type ServerMetadata map[string]interface{} diff --git a/src/openstack_cpi_golang/cpi/properties/user_data_builder.go b/src/openstack_cpi_golang/cpi/properties/user_data_builder.go new file mode 100644 index 00000000..7c149856 --- /dev/null +++ b/src/openstack_cpi_golang/cpi/properties/user_data_builder.go @@ -0,0 +1,91 @@ +package properties + +import ( + "encoding/json" + + "github.com/cloudfoundry/bosh-cpi-go/apiv1" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/config" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils" +) + +type userDataBuilder struct { + server Server + networks map[string]UserdataNetwork + dns DNS + vm VM + agentID string + ephemeralDiskSize int + env json.RawMessage + disks Disks + mbus string +} + +func NewUserDataBuilder() userDataBuilder { + return userDataBuilder{disks: Disks{System: "/dev/sda"}} +} + +func (u userDataBuilder) WithServer(server Server) userDataBuilder { + u.server = server + + return u +} + +func (u userDataBuilder) WithConfig(config config.CpiConfig) userDataBuilder { + u.mbus = config.Cloud.Properties.Agent.MBus + + return u +} + +func (u userDataBuilder) WithNetworks(networks map[string]UserdataNetwork) userDataBuilder { + u.networks = networks + + allDNSServers := make([]string, 0) + for _, network := range u.networks { + allDNSServers = append(allDNSServers, network.DNS...) + } + + u.dns = DNS{Nameserver: utils.UniqueArray(allDNSServers)} + + return u +} + +func (u userDataBuilder) WithVM(vm VM) userDataBuilder { + u.vm = vm + + return u +} + +func (u userDataBuilder) WithEphemeralDiskSize(diskSize int) userDataBuilder { + u.ephemeralDiskSize = diskSize + + return u +} + +func (u userDataBuilder) WithAgentID(agentID apiv1.AgentID) userDataBuilder { + u.agentID = agentID.AsString() + + return u +} + +func (u userDataBuilder) WithEnvironment(env json.RawMessage) userDataBuilder { + u.env = env + + return u +} + +func (u userDataBuilder) Build() UserData { + if u.ephemeralDiskSize > 0 { + u.disks.Ephemeral = "/dev/sdb" + } + + return UserData{ + Server: u.server, + Networks: u.networks, + DNS: u.dns, + VM: u.vm, + Disks: u.disks, + AgentID: u.agentID, + Env: u.env, + MBus: u.mbus, + } +} diff --git a/src/openstack_cpi_golang/cpi/properties/user_data_builder_test.go b/src/openstack_cpi_golang/cpi/properties/user_data_builder_test.go new file mode 100644 index 00000000..6ca525da --- /dev/null +++ b/src/openstack_cpi_golang/cpi/properties/user_data_builder_test.go @@ -0,0 +1,93 @@ +package properties_test + +import ( + "encoding/json" + + "github.com/cloudfoundry/bosh-cpi-go/apiv1" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/config" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/properties" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("userDataBuilder", func() { + + var _ = Context("WithServer", func() { + It("sets server data", func() { + userData := properties.NewUserDataBuilder().WithServer(properties.Server{Name: "the-name"}).Build() + + Expect(userData.Server).To(Equal(properties.Server{Name: "the-name"})) + }) + }) + + var _ = Context("WithServer", func() { + It("sets network data", func() { + userDataNetwork := map[string]properties.UserdataNetwork{} + userDataNetwork["bosh"] = properties.UserdataNetwork{IP: "1.1.1.1"} + userData := properties.NewUserDataBuilder().WithNetworks(userDataNetwork).Build() + + Expect(userData.Networks).To(Equal(userDataNetwork)) + }) + + It("it sets dns data", func() { + userDataNetwork := map[string]properties.UserdataNetwork{} + userDataNetwork["bosh"] = properties.UserdataNetwork{IP: "1.1.1.1"} + userData := properties.NewUserDataBuilder().WithNetworks(userDataNetwork).Build() + + Expect(userData.Networks).To(Equal(userDataNetwork)) + }) + }) + + var _ = Context("WithConfig", func() { + It("sets vm data", func() { + cpiConfig := config.CpiConfig{} + cpiConfig.Cloud.Properties.Agent.MBus = "the-mbus" + + userData := properties.NewUserDataBuilder().WithConfig(cpiConfig).Build() + + Expect(userData.MBus).To(Equal("the-mbus")) + }) + }) + + var _ = Context("WithVM", func() { + It("sets vm data", func() { + userData := properties.NewUserDataBuilder().WithVM(properties.VM{Name: "the-name"}).Build() + + Expect(userData.VM).To(Equal(properties.VM{Name: "the-name"})) + }) + }) + + var _ = Context("WithEphemeralDiskSize", func() { + It("sets ephemeral disk data", func() { + userData := properties.NewUserDataBuilder().WithEphemeralDiskSize(1).Build() + + Expect(userData.Disks).To(Equal(properties.Disks{System: "/dev/sda", Ephemeral: "/dev/sdb"})) + }) + + It("does not set ephemeral disk data if size is 0", func() { + userData := properties.NewUserDataBuilder().WithEphemeralDiskSize(0).Build() + + Expect(userData.Disks).To(Equal(properties.Disks{System: "/dev/sda"})) + }) + }) + + var _ = Context("WithAgentID", func() { + It("sets the agentID", func() { + userData := properties.NewUserDataBuilder().WithAgentID(apiv1.NewAgentID("the-agent-id")).Build() + + Expect(userData.AgentID).To(Equal("the-agent-id")) + }) + }) + + var _ = Context("WithEnvironment", func() { + It("sets the environment", func() { + jsonString := `{"test":{"a":"a"}}` + + userData := properties.NewUserDataBuilder().WithEnvironment([]byte(jsonString)).Build() + data, err := json.Marshal(userData) + + Expect(err).ToNot(HaveOccurred()) + Expect(string(data)).To(ContainSubstring(jsonString)) + }) + }) +}) diff --git a/src/openstack_cpi_golang/cpi/utils/env_var.go b/src/openstack_cpi_golang/cpi/utils/env_var.go new file mode 100644 index 00000000..75cb1d8d --- /dev/null +++ b/src/openstack_cpi_golang/cpi/utils/env_var.go @@ -0,0 +1,18 @@ +package utils + +import "os" + +//counterfeiter:generate . EnvVar +type EnvVar interface { + Get(key string) string +} + +type envVar struct{} + +func NewEnvVar() EnvVar { + return envVar{} +} + +func (envVar) Get(key string) string { + return os.Getenv(key) +} diff --git a/src/openstack_cpi_golang/cpi/utils/retry.go b/src/openstack_cpi_golang/cpi/utils/retry.go new file mode 100644 index 00000000..11f26e0c --- /dev/null +++ b/src/openstack_cpi_golang/cpi/utils/retry.go @@ -0,0 +1,74 @@ +package utils + +import ( + "context" + "errors" + "fmt" + "net" + "time" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/config" + "github.com/gophercloud/gophercloud" +) + +func RetryOnError(retryConfig config.RetryConfig, logger Logger) func( + ctx context.Context, + method string, + url string, + options *gophercloud.RequestOpts, + inError error, + failCount uint, +) error { + sleepDuration := time.Duration(retryConfig.SleepDuration) * time.Second + maxRetries := retryConfig.MaxAttempts + + return func(ctx context.Context, method string, url string, options *gophercloud.RequestOpts, inError error, failCount uint) error { + if failCount >= uint(maxRetries) { + return fmt.Errorf("max retry attempts (%d) reached, err: %w", failCount, inError) + } + + logger.Warn( + "retry on error", + fmt.Sprintf("attempt failed with error: %v", inError)) + + var responseCode gophercloud.ErrUnexpectedResponseCode + if errors.As(inError, &responseCode) { + if responseCode.Actual == 500 || responseCode.Actual == 503 { + logger.Warn( + "retry on error", + fmt.Sprintf("detected HTTP error %d, sleeping for %.0f seconds", responseCode.Actual, sleepDuration.Seconds())) + time.Sleep(sleepDuration) + return nil + } + } else if isTimeout(inError) { + logger.Warn( + "retry on error", + fmt.Sprintf("detected timeout, sleeping for %.0f seconds", sleepDuration.Seconds())) + time.Sleep(sleepDuration) + return nil + } else if isNetworkError(inError) { + logger.Warn( + "retry on error", + fmt.Sprintf("detected network error, sleeping for %.0f seconds", sleepDuration.Seconds())) + time.Sleep(sleepDuration) + return nil + } + + return inError + } +} + +func isTimeout(err error) bool { + var netErr net.Error + if errors.As(err, &netErr) { + return netErr.Timeout() + } + return false +} + +func isNetworkError(err error) bool { + var opError *net.OpError + result := errors.As(err, &opError) + + return result +} diff --git a/src/openstack_cpi_golang/cpi/utils/retry_test.go b/src/openstack_cpi_golang/cpi/utils/retry_test.go new file mode 100644 index 00000000..c8c381dc --- /dev/null +++ b/src/openstack_cpi_golang/cpi/utils/retry_test.go @@ -0,0 +1,103 @@ +package utils_test + +import ( + "errors" + "net" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/config" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils/utilsfakes" + "github.com/gophercloud/gophercloud" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +type MockNetError struct { + timeout bool + text string +} + +func (m MockNetError) Error() string { return m.text } +func (m MockNetError) Timeout() bool { return m.timeout } +func (m MockNetError) Temporary() bool { return !m.timeout } + +var _ = Describe("RetryOnError", func() { + var logger utilsfakes.FakeLogger + var retryConfig config.RetryConfig + + BeforeEach(func() { + retryConfig = config.RetryConfig{MaxAttempts: 10, SleepDuration: 0} + logger = utilsfakes.FakeLogger{} + }) + + It("returns an error if max retries is reached", func() { + err := utils.RetryOnError(retryConfig, &logger)(nil, "", "", nil, errors.New("boom"), 10) + Expect(err.Error()).To(Equal("max retry attempts (10) reached, err: boom")) + }) + + It("logs the current error", func() { + _ = utils.RetryOnError(retryConfig, &logger)(nil, "", "", nil, errors.New("boom"), 0) + + tag, msg, _ := logger.WarnArgsForCall(0) + Expect(tag).To(Equal("retry on error")) + Expect(msg).To(Equal("attempt failed with error: boom")) + }) + + It("raises received errors that should not be retried", func() { + err := utils.RetryOnError(retryConfig, &logger)(nil, "", "", nil, errors.New("boom"), 0) + + Expect(err.Error()).To(Equal("boom")) + }) + + It("retries on HTTP 500 error", func() { + testError := gophercloud.ErrUnexpectedResponseCode{ + Actual: 500, + } + + err := utils.RetryOnError(retryConfig, &logger)(nil, "", "", nil, testError, 0) + Expect(err).To(BeNil()) + + tag, msg, _ := logger.WarnArgsForCall(1) + Expect(tag).To(Equal("retry on error")) + Expect(msg).To(Equal("detected HTTP error 500, sleeping for 0 seconds")) + }) + + It("retries on HTTP 503 error", func() { + testError := gophercloud.ErrUnexpectedResponseCode{ + Actual: 503, + } + + err := utils.RetryOnError(retryConfig, &logger)(nil, "", "", nil, testError, 0) + Expect(err).To(BeNil()) + + tag, msg, _ := logger.WarnArgsForCall(1) + Expect(tag).To(Equal("retry on error")) + Expect(msg).To(Equal("detected HTTP error 503, sleeping for 0 seconds")) + }) + + It("retries on Network timeouts", func() { + netError := MockNetError{ + timeout: true, + text: "boom", + } + + err := utils.RetryOnError(retryConfig, &logger)(nil, "", "", nil, &netError, 0) + Expect(err).To(BeNil()) + + tag, msg, _ := logger.WarnArgsForCall(1) + Expect(tag).To(Equal("retry on error")) + Expect(msg).To(Equal("detected timeout, sleeping for 0 seconds")) + }) + + It("retries on generic network errors", func() { + opError := net.OpError{ + Op: "boom", + } + err := utils.RetryOnError(retryConfig, &logger)(nil, "", "", nil, &opError, 0) + Expect(err).To(BeNil()) + + tag, msg, _ := logger.WarnArgsForCall(1) + Expect(tag).To(Equal("retry on error")) + Expect(msg).To(Equal("detected network error, sleeping for 0 seconds")) + }) +}) diff --git a/src/openstack_cpi_golang/cpi/utils/service_clients.go b/src/openstack_cpi_golang/cpi/utils/service_clients.go new file mode 100644 index 00000000..5d24ff5d --- /dev/null +++ b/src/openstack_cpi_golang/cpi/utils/service_clients.go @@ -0,0 +1,24 @@ +package utils + +import ( + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/config" + "github.com/gophercloud/gophercloud" +) + +type ServiceClient *gophercloud.ServiceClient +type RetryableServiceClient *gophercloud.ServiceClient + +type ServiceClients struct { + ServiceClient ServiceClient + RetryableServiceClient RetryableServiceClient +} + +func NewServiceClients(serviceClient *gophercloud.ServiceClient, cpiConfig config.CpiConfig, logger Logger) ServiceClients { + retryableServiceClient := serviceClient + retryableServiceClient.RetryFunc = RetryOnError(cpiConfig.Properties().RetryConfig.Default(), logger) + + return ServiceClients{ + ServiceClient: serviceClient, + RetryableServiceClient: retryableServiceClient, + } +} diff --git a/src/openstack_cpi_golang/cpi/utils/service_clients_test.go b/src/openstack_cpi_golang/cpi/utils/service_clients_test.go new file mode 100644 index 00000000..9e069a66 --- /dev/null +++ b/src/openstack_cpi_golang/cpi/utils/service_clients_test.go @@ -0,0 +1,26 @@ +package utils_test + +import ( + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/config" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils/utilsfakes" + "github.com/gophercloud/gophercloud" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("ServiceClients", func() { + var utilsRetryableServiceClient utils.RetryableServiceClient + var utilsServiceClient utils.ServiceClient + + It("returns an error if max retries is reached", func() { + providerClient := gophercloud.ProviderClient{TokenID: "the_token"} + serviceClient := gophercloud.ServiceClient{ProviderClient: &providerClient} + cpiConfig := config.CpiConfig{} + logger := utilsfakes.FakeLogger{} + serviceClients := utils.NewServiceClients(&serviceClient, cpiConfig, &logger) + + Expect(serviceClients.ServiceClient).To(BeAssignableToTypeOf(utilsServiceClient)) + Expect(serviceClients.RetryableServiceClient).To(BeAssignableToTypeOf(utilsRetryableServiceClient)) + }) +}) diff --git a/src/openstack_cpi_golang/cpi/utils/unique_array.go b/src/openstack_cpi_golang/cpi/utils/unique_array.go new file mode 100644 index 00000000..5db5fce8 --- /dev/null +++ b/src/openstack_cpi_golang/cpi/utils/unique_array.go @@ -0,0 +1,14 @@ +package utils + +func UniqueArray(input []string) []string { + uniqueMap := make(map[string]bool) + + uniqueSlice := make([]string, 0) + for _, entry := range input { + if _, value := uniqueMap[entry]; !value { + uniqueMap[entry] = true + uniqueSlice = append(uniqueSlice, entry) + } + } + return uniqueSlice +} diff --git a/src/openstack_cpi_golang/cpi/utils/unique_array_test.go b/src/openstack_cpi_golang/cpi/utils/unique_array_test.go new file mode 100644 index 00000000..03b73335 --- /dev/null +++ b/src/openstack_cpi_golang/cpi/utils/unique_array_test.go @@ -0,0 +1,18 @@ +package utils_test + +import ( + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("UniqueArray", func() { + + It("returns an array with unique elements", func() { + Expect(utils.UniqueArray([]string{"a", "b", "b", "a"})).To(Equal([]string{"a", "b"})) + }) + + It("can ne invoked on an empty array", func() { + Expect(utils.UniqueArray([]string{})).To(Equal([]string{})) + }) +}) diff --git a/src/openstack_cpi_golang/cpi/utils/utils_suite_test.go b/src/openstack_cpi_golang/cpi/utils/utils_suite_test.go new file mode 100644 index 00000000..9335b3e4 --- /dev/null +++ b/src/openstack_cpi_golang/cpi/utils/utils_suite_test.go @@ -0,0 +1,13 @@ +package utils_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestMethods(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Utils Suite") +} diff --git a/src/openstack_cpi_golang/cpi/utils/utilsfakes/fake_env_var.go b/src/openstack_cpi_golang/cpi/utils/utilsfakes/fake_env_var.go new file mode 100644 index 00000000..cbcc26cb --- /dev/null +++ b/src/openstack_cpi_golang/cpi/utils/utilsfakes/fake_env_var.go @@ -0,0 +1,111 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package utilsfakes + +import ( + "sync" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils" +) + +type FakeEnvVar struct { + GetStub func(string) string + getMutex sync.RWMutex + getArgsForCall []struct { + arg1 string + } + getReturns struct { + result1 string + } + getReturnsOnCall map[int]struct { + result1 string + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *FakeEnvVar) Get(arg1 string) string { + fake.getMutex.Lock() + ret, specificReturn := fake.getReturnsOnCall[len(fake.getArgsForCall)] + fake.getArgsForCall = append(fake.getArgsForCall, struct { + arg1 string + }{arg1}) + stub := fake.GetStub + fakeReturns := fake.getReturns + fake.recordInvocation("Get", []interface{}{arg1}) + fake.getMutex.Unlock() + if stub != nil { + return stub(arg1) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeEnvVar) GetCallCount() int { + fake.getMutex.RLock() + defer fake.getMutex.RUnlock() + return len(fake.getArgsForCall) +} + +func (fake *FakeEnvVar) GetCalls(stub func(string) string) { + fake.getMutex.Lock() + defer fake.getMutex.Unlock() + fake.GetStub = stub +} + +func (fake *FakeEnvVar) GetArgsForCall(i int) string { + fake.getMutex.RLock() + defer fake.getMutex.RUnlock() + argsForCall := fake.getArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeEnvVar) GetReturns(result1 string) { + fake.getMutex.Lock() + defer fake.getMutex.Unlock() + fake.GetStub = nil + fake.getReturns = struct { + result1 string + }{result1} +} + +func (fake *FakeEnvVar) GetReturnsOnCall(i int, result1 string) { + fake.getMutex.Lock() + defer fake.getMutex.Unlock() + fake.GetStub = nil + if fake.getReturnsOnCall == nil { + fake.getReturnsOnCall = make(map[int]struct { + result1 string + }) + } + fake.getReturnsOnCall[i] = struct { + result1 string + }{result1} +} + +func (fake *FakeEnvVar) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.getMutex.RLock() + defer fake.getMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *FakeEnvVar) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ utils.EnvVar = new(FakeEnvVar) diff --git a/src/openstack_cpi_golang/cpi/utils/utilsfakes/fake_logger.go b/src/openstack_cpi_golang/cpi/utils/utilsfakes/fake_logger.go new file mode 100644 index 00000000..41fdd66a --- /dev/null +++ b/src/openstack_cpi_golang/cpi/utils/utilsfakes/fake_logger.go @@ -0,0 +1,314 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package utilsfakes + +import ( + "sync" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils" + "github.com/cloudfoundry/bosh-utils/logger" +) + +type FakeLogger struct { + DebugStub func(string, string, ...interface{}) + debugMutex sync.RWMutex + debugArgsForCall []struct { + arg1 string + arg2 string + arg3 []interface{} + } + ErrorStub func(string, string, ...interface{}) + errorMutex sync.RWMutex + errorArgsForCall []struct { + arg1 string + arg2 string + arg3 []interface{} + } + HandlePanicStub func(string) + handlePanicMutex sync.RWMutex + handlePanicArgsForCall []struct { + arg1 string + } + InfoStub func(string, string, ...interface{}) + infoMutex sync.RWMutex + infoArgsForCall []struct { + arg1 string + arg2 string + arg3 []interface{} + } + TargetLoggerStub func() logger.Logger + targetLoggerMutex sync.RWMutex + targetLoggerArgsForCall []struct { + } + targetLoggerReturns struct { + result1 logger.Logger + } + targetLoggerReturnsOnCall map[int]struct { + result1 logger.Logger + } + WarnStub func(string, string, ...interface{}) + warnMutex sync.RWMutex + warnArgsForCall []struct { + arg1 string + arg2 string + arg3 []interface{} + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *FakeLogger) Debug(arg1 string, arg2 string, arg3 ...interface{}) { + fake.debugMutex.Lock() + fake.debugArgsForCall = append(fake.debugArgsForCall, struct { + arg1 string + arg2 string + arg3 []interface{} + }{arg1, arg2, arg3}) + stub := fake.DebugStub + fake.recordInvocation("Debug", []interface{}{arg1, arg2, arg3}) + fake.debugMutex.Unlock() + if stub != nil { + fake.DebugStub(arg1, arg2, arg3...) + } +} + +func (fake *FakeLogger) DebugCallCount() int { + fake.debugMutex.RLock() + defer fake.debugMutex.RUnlock() + return len(fake.debugArgsForCall) +} + +func (fake *FakeLogger) DebugCalls(stub func(string, string, ...interface{})) { + fake.debugMutex.Lock() + defer fake.debugMutex.Unlock() + fake.DebugStub = stub +} + +func (fake *FakeLogger) DebugArgsForCall(i int) (string, string, []interface{}) { + fake.debugMutex.RLock() + defer fake.debugMutex.RUnlock() + argsForCall := fake.debugArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 +} + +func (fake *FakeLogger) Error(arg1 string, arg2 string, arg3 ...interface{}) { + fake.errorMutex.Lock() + fake.errorArgsForCall = append(fake.errorArgsForCall, struct { + arg1 string + arg2 string + arg3 []interface{} + }{arg1, arg2, arg3}) + stub := fake.ErrorStub + fake.recordInvocation("Error", []interface{}{arg1, arg2, arg3}) + fake.errorMutex.Unlock() + if stub != nil { + fake.ErrorStub(arg1, arg2, arg3...) + } +} + +func (fake *FakeLogger) ErrorCallCount() int { + fake.errorMutex.RLock() + defer fake.errorMutex.RUnlock() + return len(fake.errorArgsForCall) +} + +func (fake *FakeLogger) ErrorCalls(stub func(string, string, ...interface{})) { + fake.errorMutex.Lock() + defer fake.errorMutex.Unlock() + fake.ErrorStub = stub +} + +func (fake *FakeLogger) ErrorArgsForCall(i int) (string, string, []interface{}) { + fake.errorMutex.RLock() + defer fake.errorMutex.RUnlock() + argsForCall := fake.errorArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 +} + +func (fake *FakeLogger) HandlePanic(arg1 string) { + fake.handlePanicMutex.Lock() + fake.handlePanicArgsForCall = append(fake.handlePanicArgsForCall, struct { + arg1 string + }{arg1}) + stub := fake.HandlePanicStub + fake.recordInvocation("HandlePanic", []interface{}{arg1}) + fake.handlePanicMutex.Unlock() + if stub != nil { + fake.HandlePanicStub(arg1) + } +} + +func (fake *FakeLogger) HandlePanicCallCount() int { + fake.handlePanicMutex.RLock() + defer fake.handlePanicMutex.RUnlock() + return len(fake.handlePanicArgsForCall) +} + +func (fake *FakeLogger) HandlePanicCalls(stub func(string)) { + fake.handlePanicMutex.Lock() + defer fake.handlePanicMutex.Unlock() + fake.HandlePanicStub = stub +} + +func (fake *FakeLogger) HandlePanicArgsForCall(i int) string { + fake.handlePanicMutex.RLock() + defer fake.handlePanicMutex.RUnlock() + argsForCall := fake.handlePanicArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeLogger) Info(arg1 string, arg2 string, arg3 ...interface{}) { + fake.infoMutex.Lock() + fake.infoArgsForCall = append(fake.infoArgsForCall, struct { + arg1 string + arg2 string + arg3 []interface{} + }{arg1, arg2, arg3}) + stub := fake.InfoStub + fake.recordInvocation("Info", []interface{}{arg1, arg2, arg3}) + fake.infoMutex.Unlock() + if stub != nil { + fake.InfoStub(arg1, arg2, arg3...) + } +} + +func (fake *FakeLogger) InfoCallCount() int { + fake.infoMutex.RLock() + defer fake.infoMutex.RUnlock() + return len(fake.infoArgsForCall) +} + +func (fake *FakeLogger) InfoCalls(stub func(string, string, ...interface{})) { + fake.infoMutex.Lock() + defer fake.infoMutex.Unlock() + fake.InfoStub = stub +} + +func (fake *FakeLogger) InfoArgsForCall(i int) (string, string, []interface{}) { + fake.infoMutex.RLock() + defer fake.infoMutex.RUnlock() + argsForCall := fake.infoArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 +} + +func (fake *FakeLogger) TargetLogger() logger.Logger { + fake.targetLoggerMutex.Lock() + ret, specificReturn := fake.targetLoggerReturnsOnCall[len(fake.targetLoggerArgsForCall)] + fake.targetLoggerArgsForCall = append(fake.targetLoggerArgsForCall, struct { + }{}) + stub := fake.TargetLoggerStub + fakeReturns := fake.targetLoggerReturns + fake.recordInvocation("TargetLogger", []interface{}{}) + fake.targetLoggerMutex.Unlock() + if stub != nil { + return stub() + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeLogger) TargetLoggerCallCount() int { + fake.targetLoggerMutex.RLock() + defer fake.targetLoggerMutex.RUnlock() + return len(fake.targetLoggerArgsForCall) +} + +func (fake *FakeLogger) TargetLoggerCalls(stub func() logger.Logger) { + fake.targetLoggerMutex.Lock() + defer fake.targetLoggerMutex.Unlock() + fake.TargetLoggerStub = stub +} + +func (fake *FakeLogger) TargetLoggerReturns(result1 logger.Logger) { + fake.targetLoggerMutex.Lock() + defer fake.targetLoggerMutex.Unlock() + fake.TargetLoggerStub = nil + fake.targetLoggerReturns = struct { + result1 logger.Logger + }{result1} +} + +func (fake *FakeLogger) TargetLoggerReturnsOnCall(i int, result1 logger.Logger) { + fake.targetLoggerMutex.Lock() + defer fake.targetLoggerMutex.Unlock() + fake.TargetLoggerStub = nil + if fake.targetLoggerReturnsOnCall == nil { + fake.targetLoggerReturnsOnCall = make(map[int]struct { + result1 logger.Logger + }) + } + fake.targetLoggerReturnsOnCall[i] = struct { + result1 logger.Logger + }{result1} +} + +func (fake *FakeLogger) Warn(arg1 string, arg2 string, arg3 ...interface{}) { + fake.warnMutex.Lock() + fake.warnArgsForCall = append(fake.warnArgsForCall, struct { + arg1 string + arg2 string + arg3 []interface{} + }{arg1, arg2, arg3}) + stub := fake.WarnStub + fake.recordInvocation("Warn", []interface{}{arg1, arg2, arg3}) + fake.warnMutex.Unlock() + if stub != nil { + fake.WarnStub(arg1, arg2, arg3...) + } +} + +func (fake *FakeLogger) WarnCallCount() int { + fake.warnMutex.RLock() + defer fake.warnMutex.RUnlock() + return len(fake.warnArgsForCall) +} + +func (fake *FakeLogger) WarnCalls(stub func(string, string, ...interface{})) { + fake.warnMutex.Lock() + defer fake.warnMutex.Unlock() + fake.WarnStub = stub +} + +func (fake *FakeLogger) WarnArgsForCall(i int) (string, string, []interface{}) { + fake.warnMutex.RLock() + defer fake.warnMutex.RUnlock() + argsForCall := fake.warnArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 +} + +func (fake *FakeLogger) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.debugMutex.RLock() + defer fake.debugMutex.RUnlock() + fake.errorMutex.RLock() + defer fake.errorMutex.RUnlock() + fake.handlePanicMutex.RLock() + defer fake.handlePanicMutex.RUnlock() + fake.infoMutex.RLock() + defer fake.infoMutex.RUnlock() + fake.targetLoggerMutex.RLock() + defer fake.targetLoggerMutex.RUnlock() + fake.warnMutex.RLock() + defer fake.warnMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *FakeLogger) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ utils.Logger = new(FakeLogger) diff --git a/src/openstack_cpi_golang/cpi/volume/volume_facade.go b/src/openstack_cpi_golang/cpi/volume/volume_facade.go new file mode 100644 index 00000000..5a9429f6 --- /dev/null +++ b/src/openstack_cpi_golang/cpi/volume/volume_facade.go @@ -0,0 +1,73 @@ +package volume + +import ( + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils" + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/volumeactions" + "github.com/gophercloud/gophercloud/openstack/blockstorage/v3/snapshots" + "github.com/gophercloud/gophercloud/openstack/blockstorage/v3/volumes" +) + +//counterfeiter:generate . VolumeFacade +type VolumeFacade interface { + CreateVolume(client utils.ServiceClient, opts volumes.CreateOptsBuilder) (*volumes.Volume, error) + GetVolume(client utils.RetryableServiceClient, volumeID string) (*volumes.Volume, error) + DeleteVolume(client utils.RetryableServiceClient, volumeID string, opts volumes.DeleteOptsBuilder) error + ExtendVolumeSize(client utils.ServiceClient, volumeID string, opts volumeactions.ExtendSizeOptsBuilder) error + SetDiskMetadata(client utils.ServiceClient, volumeID string, opts volumes.UpdateOptsBuilder) error + CreateSnapshot(client *gophercloud.ServiceClient, opts snapshots.CreateOptsBuilder) (*snapshots.Snapshot, error) + DeleteSnapshot(client *gophercloud.ServiceClient, snapshotID string) error + UpdateMetaDataSnapShot(client *gophercloud.ServiceClient, snapshotID string, opts snapshots.UpdateMetadataOptsBuilder) (map[string]interface{}, error) + GetSnapshot(client utils.RetryableServiceClient, snapshotID string) (*snapshots.Snapshot, error) +} + +type volumeFacade struct{} + +func (v volumeFacade) CreateVolume(client utils.ServiceClient, opts volumes.CreateOptsBuilder) (*volumes.Volume, error) { + return volumes.Create(client, opts).Extract() +} + +func (v volumeFacade) GetVolume(client utils.RetryableServiceClient, volumeID string) (*volumes.Volume, error) { + return volumes.Get(client, volumeID).Extract() +} + +func (v volumeFacade) DeleteVolume(client utils.RetryableServiceClient, volumeID string, opts volumes.DeleteOptsBuilder) error { + err := volumes.Delete(client, volumeID, opts).ExtractErr() + if err != nil { + return err + } + return nil +} + +func (v volumeFacade) ExtendVolumeSize(client utils.ServiceClient, volumeID string, opts volumeactions.ExtendSizeOptsBuilder) error { + return volumeactions.ExtendSize(client, volumeID, opts).ExtractErr() +} + +func (v volumeFacade) SetDiskMetadata(client utils.ServiceClient, volumeID string, opts volumes.UpdateOptsBuilder) error { + _, err := volumes.Update(client, volumeID, opts).Extract() + return err +} + +func (v volumeFacade) CreateSnapshot(client *gophercloud.ServiceClient, opts snapshots.CreateOptsBuilder) (*snapshots.Snapshot, error) { + return snapshots.Create(client, opts).Extract() +} + +func (v volumeFacade) DeleteSnapshot(client *gophercloud.ServiceClient, snapshotID string) error { + return snapshots.Delete(client, snapshotID).ExtractErr() +} + +func (v volumeFacade) UpdateMetaDataSnapShot(client *gophercloud.ServiceClient, snapshotID string, opts snapshots.UpdateMetadataOptsBuilder) (map[string]interface{}, error) { + metaDataMap, err := snapshots.UpdateMetadata(client, snapshotID, opts).ExtractMetadata() + if err != nil { + return nil, err + } + return metaDataMap, nil +} + +func (v volumeFacade) GetSnapshot(client utils.RetryableServiceClient, snapshotID string) (*snapshots.Snapshot, error) { + return snapshots.Get(client, snapshotID).Extract() +} + +func NewVolumeFacade() volumeFacade { + return volumeFacade{} +} diff --git a/src/openstack_cpi_golang/cpi/volume/volume_service.go b/src/openstack_cpi_golang/cpi/volume/volume_service.go new file mode 100644 index 00000000..ac7519da --- /dev/null +++ b/src/openstack_cpi_golang/cpi/volume/volume_service.go @@ -0,0 +1,270 @@ +package volume + +import ( + "errors" + "fmt" + "time" + + "github.com/gophercloud/gophercloud/openstack/blockstorage/v3/snapshots" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/properties" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils" + "github.com/google/uuid" + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/volumeactions" + "github.com/gophercloud/gophercloud/openstack/blockstorage/v3/volumes" +) + +var VolumeServicePollingInterval = 10 * time.Second + +//counterfeiter:generate . VolumeService +type VolumeService interface { + CreateVolume( + size int, + cloudProps properties.CreateDisk, + az string, + ) (*volumes.Volume, error) + WaitForVolumeToBecomeStatus( + volumeID string, + timeout time.Duration, + status string, + ) error + GetVolume(volumeID string) (*volumes.Volume, error) + DeleteVolume(volumeId string) error + ExtendVolumeSize( + volumeID string, + size int, + ) error + SetDiskMetadata( + volumeID string, + metadata map[string]string, + ) error + CreateSnapshot( + volumeID string, + force bool, + name string, + description string, + metadata map[string]string, + ) (*snapshots.Snapshot, error) + DeleteSnapshot( + snapShotID string, + ) error + UpdateMetaDataSnapshot( + snapShotID string, + metadata map[string]interface{}, + ) (map[string]interface{}, error) + WaitForSnapshotToBecomeStatus( + snapShotID string, + timeout time.Duration, + status string, + ) error + GetSnapshot( + snapShotID string, + ) (*snapshots.Snapshot, error) +} + +type volumeService struct { + volumeFacade VolumeFacade + serviceClients utils.ServiceClients +} + +func NewVolumeService(serviceClients utils.ServiceClients, volumeFacade VolumeFacade) volumeService { + return volumeService{ + volumeFacade: volumeFacade, + serviceClients: serviceClients, + } +} + +func (v volumeService) CreateVolume( + size int, + cloudProps properties.CreateDisk, + az string, +) (*volumes.Volume, error) { + volumeType := cloudProps.VolumeType + + uuid, _ := uuid.NewRandom() + name := fmt.Sprintf("volume-%s", uuid) + createOpts := v.getVolumeCreateOpts(size, az, volumeType, name) + volume, err := v.volumeFacade.CreateVolume(v.serviceClients.ServiceClient, createOpts) + if err != nil { + return nil, fmt.Errorf("failed to create volume: %w", err) + } + + return volume, nil +} + +func (v volumeService) WaitForVolumeToBecomeStatus(volumeID string, timeout time.Duration, status string) error { + timeoutTimer := time.NewTimer(timeout) + var errDefault404 gophercloud.ErrDefault404 + + for { + select { + case <-timeoutTimer.C: + return fmt.Errorf("timeout while waiting for volume to become %s", status) + default: + volume, err := v.GetVolume(volumeID) + if err != nil { + if errors.As(err, &errDefault404) && status == "deleted" { + return nil + } + return err + } + + switch volume.Status { + case status: + return nil + case "error": + return fmt.Errorf("volume became error state while waiting to become %s", status) + } + + time.Sleep(VolumeServicePollingInterval) + } + } +} + +func (v volumeService) GetVolume(volumeID string) (*volumes.Volume, error) { + volume, err := v.volumeFacade.GetVolume(v.serviceClients.RetryableServiceClient, volumeID) + if err != nil { + return nil, fmt.Errorf("failed to retrieve volume information: %w", err) + } + return volume, nil +} + +func (v volumeService) ExtendVolumeSize(volumeID string, size int) error { + extendOpts := volumeactions.ExtendSizeOpts{ + NewSize: size, + } + + err := v.volumeFacade.ExtendVolumeSize(v.serviceClients.ServiceClient, volumeID, extendOpts) + if err != nil { + return fmt.Errorf("failed to extend volume size: %w", err) + } + return nil +} + +func (v volumeService) SetDiskMetadata(volumeID string, metadata map[string]string) error { + updateOpts := volumes.UpdateOpts{ + Metadata: metadata, + } + err := v.volumeFacade.SetDiskMetadata(v.serviceClients.ServiceClient, volumeID, updateOpts) + if err != nil { + return fmt.Errorf("failed to set disk metadata: %w", err) + } + return nil +} + +func (v volumeService) DeleteVolume(volumeID string) error { + deleteOpts := v.getVolumeDeleteOpts() + + err := v.volumeFacade.DeleteVolume(v.serviceClients.RetryableServiceClient, volumeID, deleteOpts) + if err != nil { + return fmt.Errorf("failed to delete volume: %w", err) + } + return nil +} + +func (v volumeService) CreateSnapshot( + volumeID string, + force bool, + name string, + description string, + metadata map[string]string, +) (*snapshots.Snapshot, error) { + + snapshot, err := v.volumeFacade.CreateSnapshot( + v.serviceClients.ServiceClient, + snapshots.CreateOpts{ + VolumeID: volumeID, + Force: force, + Name: name, + Description: description, + Metadata: metadata}, + ) + if err != nil { + return nil, fmt.Errorf("failed to create snapshot: %w", err) + } + + return snapshot, nil +} + +func (v volumeService) DeleteSnapshot(snapShotID string) error { + err := v.volumeFacade.DeleteSnapshot(v.serviceClients.ServiceClient, snapShotID) + if err != nil { + return fmt.Errorf("failed to delete snapshot: %w", err) + } + return nil +} + +func (v volumeService) UpdateMetaDataSnapshot( + snapShotID string, + metadata map[string]interface{}, +) (map[string]interface{}, error) { + + metaData, err := v.volumeFacade.UpdateMetaDataSnapShot( + v.serviceClients.ServiceClient, + snapShotID, + snapshots.UpdateMetadataOpts{ + Metadata: metadata}, + ) + if err != nil { + return nil, fmt.Errorf("failed to update metadata snapshot: %w", err) + } + + return metaData, nil +} + +func (v volumeService) GetSnapshot(snapShotID string) (*snapshots.Snapshot, error) { + snapshot, err := v.volumeFacade.GetSnapshot(v.serviceClients.RetryableServiceClient, snapShotID) + if err != nil { + return nil, fmt.Errorf("failed to retrieve snapshot information: %w", err) + } + return snapshot, nil +} + +func (v volumeService) WaitForSnapshotToBecomeStatus(snapShotID string, timeout time.Duration, status string) error { + timeoutTimer := time.NewTimer(timeout) + var errDefault404 gophercloud.ErrDefault404 + + for { + select { + case <-timeoutTimer.C: + return fmt.Errorf("timeout while waiting for snapshot to become %s", status) + default: + snapshot, err := v.GetSnapshot(snapShotID) + if err != nil { + if errors.As(err, &errDefault404) && status == "deleted" { + return nil + } + return err + } + + switch snapshot.Status { + case status: + return nil + case "error": + return fmt.Errorf("snapshot became error state while waiting to become %s", status) + case "failed": + return fmt.Errorf("snapshot became error state while waiting to become %s", status) + case "killed": + return fmt.Errorf("snapshot became error state while waiting to become %s", status) + } + + time.Sleep(VolumeServicePollingInterval) + } + } +} + +func (v volumeService) getVolumeCreateOpts(size int, availabilityZone string, volumeType string, name string) volumes.CreateOptsBuilder { + createOpts := volumes.CreateOpts{ + Size: size, + AvailabilityZone: availabilityZone, + VolumeType: volumeType, + Name: name, + } + return createOpts +} + +func (v volumeService) getVolumeDeleteOpts() volumes.DeleteOptsBuilder { + deleteOpts := volumes.DeleteOpts{} + return deleteOpts +} diff --git a/src/openstack_cpi_golang/cpi/volume/volume_service_builder.go b/src/openstack_cpi_golang/cpi/volume/volume_service_builder.go new file mode 100644 index 00000000..96b94fbd --- /dev/null +++ b/src/openstack_cpi_golang/cpi/volume/volume_service_builder.go @@ -0,0 +1,39 @@ +package volume + +import ( + "fmt" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/config" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/openstack" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils" +) + +//counterfeiter:generate . VolumeServiceBuilder +type VolumeServiceBuilder interface { + Build() (VolumeService, error) +} + +type volumeServiceBuilder struct { + openstackService openstack.OpenstackService + cpiConfig config.CpiConfig + logger utils.Logger +} + +func NewVolumeServiceBuilder(openstackService openstack.OpenstackService, cpiConfig config.CpiConfig, logger utils.Logger) volumeServiceBuilder { + return volumeServiceBuilder{ + openstackService: openstackService, + cpiConfig: cpiConfig, + logger: logger, + } +} + +func (v volumeServiceBuilder) Build() (VolumeService, error) { + serviceClient, err := v.openstackService.BlockStorageV3(v.cpiConfig.OpenStackConfig()) + if err != nil { + return nil, fmt.Errorf("failed to retrieve volume service client: %w", err) + } + + serviceClients := utils.NewServiceClients(serviceClient, v.cpiConfig, v.logger) + volumeFacade := NewVolumeFacade() + return NewVolumeService(serviceClients, volumeFacade), nil +} diff --git a/src/openstack_cpi_golang/cpi/volume/volume_service_builder_test.go b/src/openstack_cpi_golang/cpi/volume/volume_service_builder_test.go new file mode 100644 index 00000000..168e0902 --- /dev/null +++ b/src/openstack_cpi_golang/cpi/volume/volume_service_builder_test.go @@ -0,0 +1,55 @@ +package volume_test + +import ( + "errors" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/config" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/openstack/openstackfakes" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/volume" + "github.com/gophercloud/gophercloud" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils/utilsfakes" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("VolumeServiceBuilder", func() { + var openstackService openstackfakes.FakeOpenstackService + var logger utilsfakes.FakeLogger + var volumeServiceBuilder volume.VolumeServiceBuilder + + BeforeEach(func() { + openstackService = openstackfakes.FakeOpenstackService{} + logger = utilsfakes.FakeLogger{} + cpiConfig := config.CpiConfig{} + cpiConfig.Cloud.Properties.RetryConfig = config.RetryConfigMap{} + + volumeServiceBuilder = volume.NewVolumeServiceBuilder( + &openstackService, + cpiConfig, + &logger, + ) + }) + + Context("Build", func() { + It("returns a volume service", func() { + providerClient := gophercloud.ProviderClient{TokenID: "the_token"} + serviceClient := gophercloud.ServiceClient{ProviderClient: &providerClient} + openstackService.BlockStorageV3Returns(&serviceClient, nil) + + volumeService, err := volumeServiceBuilder.Build() + + Expect(err).ToNot(HaveOccurred()) + Expect(volumeService).To(Not(BeNil())) + }) + + It("returns an error if the compute service client cannot be retrieved", func() { + openstackService.BlockStorageV3Returns(nil, errors.New("boom")) + + volumeService, err := volumeServiceBuilder.Build() + + Expect(err.Error()).To(Equal("failed to retrieve volume service client: boom")) + Expect(volumeService).To(BeNil()) + }) + }) +}) diff --git a/src/openstack_cpi_golang/cpi/volume/volume_service_test.go b/src/openstack_cpi_golang/cpi/volume/volume_service_test.go new file mode 100644 index 00000000..915428b4 --- /dev/null +++ b/src/openstack_cpi_golang/cpi/volume/volume_service_test.go @@ -0,0 +1,209 @@ +package volume_test + +import ( + "errors" + "time" + + "github.com/gophercloud/gophercloud/openstack/blockstorage/v3/snapshots" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/properties" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/volume" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/volume/volumefakes" + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/blockstorage/v3/volumes" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("VolumeService", func() { + var serviceClient gophercloud.ServiceClient + var retryableServiceClient gophercloud.ServiceClient + var serviceClients utils.ServiceClients + var volumeFacade volumefakes.FakeVolumeFacade + var defaultCloudConfig properties.CreateDisk + var volumeService volume.VolumeService + + BeforeEach(func() { + serviceClient = gophercloud.ServiceClient{} + retryableServiceClient = gophercloud.ServiceClient{} + serviceClients = utils.ServiceClients{ServiceClient: &serviceClient, RetryableServiceClient: &retryableServiceClient} + volumeFacade = volumefakes.FakeVolumeFacade{} + + volumeService = volume.NewVolumeService(serviceClients, &volumeFacade) + volume.VolumeServicePollingInterval = 0 + volumeFacade.CreateVolumeReturns(&volumes.Volume{ID: "123-456"}, nil) + defaultCloudConfig = properties.CreateDisk{VolumeType: "the_volume_type"} + }) + + Context("CreateVolume", func() { + + It("returns error if volume was failed to be created", func() { + volumeFacade.CreateVolumeReturns(&volumes.Volume{ID: "123-456", Status: "available"}, errors.New("boom")) + + _, err := volumeService.CreateVolume(1, defaultCloudConfig, "z1") + + Expect(err.Error()).To(Equal("failed to create volume: boom")) + }) + + It("returns an available volume", func() { + volumeFacade.CreateVolumeReturns(&volumes.Volume{ID: "123-456", Status: "available"}, nil) + + volume, err := volumeService.CreateVolume(1, defaultCloudConfig, "z1") + + Expect(err).ToNot(HaveOccurred()) + Expect(volume).ToNot(BeNil()) + }) + }) + + Context("WaitForVolumeToBecomeStatus", func() { + It("returns error if volume was failed to become available", func() { + volumeFacade.GetVolumeReturns(&volumes.Volume{ID: "123-456", Status: "error"}, nil) + + err := volumeService.WaitForVolumeToBecomeStatus("123-456", 1*time.Second, "some_target_status") + + Expect(err.Error()).To(Equal("volume became error state while waiting to become some_target_status")) + }) + + It("returns an available volume", func() { + volumeFacade.GetVolumeReturnsOnCall(0, &volumes.Volume{ID: "123-456", Status: "creating"}, nil) + volumeFacade.GetVolumeReturnsOnCall(1, &volumes.Volume{ID: "123-456", Status: "some_target_status"}, nil) + + err := volumeService.WaitForVolumeToBecomeStatus("123-456", 1*time.Second, "some_target_status") + + Expect(volumeFacade.GetVolumeCallCount()).To(Equal(2)) + Expect(err).ToNot(HaveOccurred()) + }) + + It("times out while waiting for volume to become some_target_status", func() { + volumeFacade.GetVolumeReturns(&volumes.Volume{ID: "123-456", Status: "creating"}, nil) + + err := volumeService.WaitForVolumeToBecomeStatus("123-456", 1, "some_target_status") + + Expect(err.Error()).To(Equal("timeout while waiting for volume to become some_target_status")) + }) + + It("returns an error if it cannot get the volume", func() { + volumeFacade.GetVolumeReturns(&volumes.Volume{}, errors.New("boom")) + + err := volumeService.WaitForVolumeToBecomeStatus("123-456", 1, "some_target_status") + + Expect(volumeFacade.GetVolumeCallCount()).To(Equal(1)) + Expect(err.Error()).To(Equal("failed to retrieve volume information: boom")) + }) + }) + + Context("DeleteVolume", func() { + + It("returns error if volume was failed to be deleted", func() { + volumeFacade.DeleteVolumeReturns(errors.New("boom")) + + err := volumeService.DeleteVolume("some_disk_cid") + + Expect(err.Error()).To(Equal("failed to delete volume: boom")) + }) + + It("returns nil if deletion of volume was successful", func() { + volumeFacade.DeleteVolumeReturns(nil) + + err := volumeService.DeleteVolume("some_disk_cid") + + Expect(err).ToNot(HaveOccurred()) + }) + }) + + Context("CreateSnapshot", func() { + It("returns an error if snapshot creation fails", func() { + volumeFacade.CreateSnapshotReturns(nil, errors.New("boom")) + _, err := volumeService.CreateSnapshot( + "123-456", + true, + "test-snapshot", + "test-snapshot-description", + map[string]string{}) + + Expect(err.Error()).To(ContainSubstring("failed to create snapshot: boom")) + }) + }) + + Context("UpdateMetaDataSnapshot", func() { + It("returns an error if snapshot metadata Update fails", func() { + volumeFacade.UpdateMetaDataSnapShotReturns(nil, errors.New("boom")) + _, err := volumeService.UpdateMetaDataSnapshot( + "123-456", + map[string]interface{}{}, + ) + + Expect(err.Error()).To(ContainSubstring("failed to update metadata snapshot: boom")) + }) + }) + + Context("GetSnapshot", func() { + var snapshot snapshots.Snapshot + + BeforeEach(func() { + snapshot = snapshots.Snapshot{ + ID: "123-456", + } + volumeFacade.GetSnapshotReturns(&snapshot, nil) + + }) + + It("returns snapshot", func() { + snapShotResult, err := volumeService.GetSnapshot( + "123-456", + ) + + Expect(err).ToNot(HaveOccurred()) + Expect(snapShotResult.ID).To(Equal(snapshot.ID)) + }) + + It("returns an error if snapshot retrieval fail", func() { + volumeFacade.GetSnapshotReturns(nil, errors.New("boom")) + + _, err := volumeService.GetSnapshot( + "123-456", + ) + + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal("failed to retrieve snapshot information: boom")) + }) + }) + + Context("WaitForSnapshotToBecomeStatus", func() { + It("returns error if snapshot was failed to become available", func() { + volumeFacade.GetSnapshotReturns(&snapshots.Snapshot{ID: "123-456", Status: "error"}, nil) + + err := volumeService.WaitForSnapshotToBecomeStatus("123-456", 1*time.Second, "some_target_status") + + Expect(err.Error()).To(Equal("snapshot became error state while waiting to become some_target_status")) + }) + + It("returns an available volume", func() { + volumeFacade.GetSnapshotReturnsOnCall(0, &snapshots.Snapshot{ID: "123-456", Status: "creating"}, nil) + volumeFacade.GetSnapshotReturnsOnCall(1, &snapshots.Snapshot{ID: "123-456", Status: "some_target_status"}, nil) + + err := volumeService.WaitForSnapshotToBecomeStatus("123-456", 1*time.Second, "some_target_status") + + Expect(volumeFacade.GetSnapshotCallCount()).To(Equal(2)) + Expect(err).ToNot(HaveOccurred()) + }) + + It("times out while waiting for snapshot to become some_target_status", func() { + volumeFacade.GetSnapshotReturns(&snapshots.Snapshot{ID: "123-456", Status: "creating"}, nil) + + err := volumeService.WaitForSnapshotToBecomeStatus("123-456", 1, "some_target_status") + + Expect(err.Error()).To(Equal("timeout while waiting for snapshot to become some_target_status")) + }) + + It("returns an error if it cannot get the snapshot", func() { + volumeFacade.GetSnapshotReturns(&snapshots.Snapshot{}, errors.New("boom")) + + err := volumeService.WaitForSnapshotToBecomeStatus("123-456", 1, "some_target_status") + + Expect(volumeFacade.GetSnapshotCallCount()).To(Equal(1)) + Expect(err.Error()).To(Equal("failed to retrieve snapshot information: boom")) + }) + }) +}) diff --git a/src/openstack_cpi_golang/cpi/volume/volume_suite_test.go b/src/openstack_cpi_golang/cpi/volume/volume_suite_test.go new file mode 100644 index 00000000..22eb9d5d --- /dev/null +++ b/src/openstack_cpi_golang/cpi/volume/volume_suite_test.go @@ -0,0 +1,13 @@ +package volume_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestMethods(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Volume Suite") +} diff --git a/src/openstack_cpi_golang/cpi/volume/volumefakes/fake_volume_facade.go b/src/openstack_cpi_golang/cpi/volume/volumefakes/fake_volume_facade.go new file mode 100644 index 00000000..96f4c84e --- /dev/null +++ b/src/openstack_cpi_golang/cpi/volume/volumefakes/fake_volume_facade.go @@ -0,0 +1,759 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package volumefakes + +import ( + "sync" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/utils" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/volume" + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/volumeactions" + "github.com/gophercloud/gophercloud/openstack/blockstorage/v3/snapshots" + "github.com/gophercloud/gophercloud/openstack/blockstorage/v3/volumes" +) + +type FakeVolumeFacade struct { + CreateSnapshotStub func(*gophercloud.ServiceClient, snapshots.CreateOptsBuilder) (*snapshots.Snapshot, error) + createSnapshotMutex sync.RWMutex + createSnapshotArgsForCall []struct { + arg1 *gophercloud.ServiceClient + arg2 snapshots.CreateOptsBuilder + } + createSnapshotReturns struct { + result1 *snapshots.Snapshot + result2 error + } + createSnapshotReturnsOnCall map[int]struct { + result1 *snapshots.Snapshot + result2 error + } + CreateVolumeStub func(utils.ServiceClient, volumes.CreateOptsBuilder) (*volumes.Volume, error) + createVolumeMutex sync.RWMutex + createVolumeArgsForCall []struct { + arg1 utils.ServiceClient + arg2 volumes.CreateOptsBuilder + } + createVolumeReturns struct { + result1 *volumes.Volume + result2 error + } + createVolumeReturnsOnCall map[int]struct { + result1 *volumes.Volume + result2 error + } + DeleteSnapshotStub func(*gophercloud.ServiceClient, string) error + deleteSnapshotMutex sync.RWMutex + deleteSnapshotArgsForCall []struct { + arg1 *gophercloud.ServiceClient + arg2 string + } + deleteSnapshotReturns struct { + result1 error + } + deleteSnapshotReturnsOnCall map[int]struct { + result1 error + } + DeleteVolumeStub func(utils.RetryableServiceClient, string, volumes.DeleteOptsBuilder) error + deleteVolumeMutex sync.RWMutex + deleteVolumeArgsForCall []struct { + arg1 utils.RetryableServiceClient + arg2 string + arg3 volumes.DeleteOptsBuilder + } + deleteVolumeReturns struct { + result1 error + } + deleteVolumeReturnsOnCall map[int]struct { + result1 error + } + ExtendVolumeSizeStub func(utils.ServiceClient, string, volumeactions.ExtendSizeOptsBuilder) error + extendVolumeSizeMutex sync.RWMutex + extendVolumeSizeArgsForCall []struct { + arg1 utils.ServiceClient + arg2 string + arg3 volumeactions.ExtendSizeOptsBuilder + } + extendVolumeSizeReturns struct { + result1 error + } + extendVolumeSizeReturnsOnCall map[int]struct { + result1 error + } + GetSnapshotStub func(utils.RetryableServiceClient, string) (*snapshots.Snapshot, error) + getSnapshotMutex sync.RWMutex + getSnapshotArgsForCall []struct { + arg1 utils.RetryableServiceClient + arg2 string + } + getSnapshotReturns struct { + result1 *snapshots.Snapshot + result2 error + } + getSnapshotReturnsOnCall map[int]struct { + result1 *snapshots.Snapshot + result2 error + } + GetVolumeStub func(utils.RetryableServiceClient, string) (*volumes.Volume, error) + getVolumeMutex sync.RWMutex + getVolumeArgsForCall []struct { + arg1 utils.RetryableServiceClient + arg2 string + } + getVolumeReturns struct { + result1 *volumes.Volume + result2 error + } + getVolumeReturnsOnCall map[int]struct { + result1 *volumes.Volume + result2 error + } + SetDiskMetadataStub func(utils.ServiceClient, string, volumes.UpdateOptsBuilder) error + setDiskMetadataMutex sync.RWMutex + setDiskMetadataArgsForCall []struct { + arg1 utils.ServiceClient + arg2 string + arg3 volumes.UpdateOptsBuilder + } + setDiskMetadataReturns struct { + result1 error + } + setDiskMetadataReturnsOnCall map[int]struct { + result1 error + } + UpdateMetaDataSnapShotStub func(*gophercloud.ServiceClient, string, snapshots.UpdateMetadataOptsBuilder) (map[string]interface{}, error) + updateMetaDataSnapShotMutex sync.RWMutex + updateMetaDataSnapShotArgsForCall []struct { + arg1 *gophercloud.ServiceClient + arg2 string + arg3 snapshots.UpdateMetadataOptsBuilder + } + updateMetaDataSnapShotReturns struct { + result1 map[string]interface{} + result2 error + } + updateMetaDataSnapShotReturnsOnCall map[int]struct { + result1 map[string]interface{} + result2 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *FakeVolumeFacade) CreateSnapshot(arg1 *gophercloud.ServiceClient, arg2 snapshots.CreateOptsBuilder) (*snapshots.Snapshot, error) { + fake.createSnapshotMutex.Lock() + ret, specificReturn := fake.createSnapshotReturnsOnCall[len(fake.createSnapshotArgsForCall)] + fake.createSnapshotArgsForCall = append(fake.createSnapshotArgsForCall, struct { + arg1 *gophercloud.ServiceClient + arg2 snapshots.CreateOptsBuilder + }{arg1, arg2}) + stub := fake.CreateSnapshotStub + fakeReturns := fake.createSnapshotReturns + fake.recordInvocation("CreateSnapshot", []interface{}{arg1, arg2}) + fake.createSnapshotMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeVolumeFacade) CreateSnapshotCallCount() int { + fake.createSnapshotMutex.RLock() + defer fake.createSnapshotMutex.RUnlock() + return len(fake.createSnapshotArgsForCall) +} + +func (fake *FakeVolumeFacade) CreateSnapshotCalls(stub func(*gophercloud.ServiceClient, snapshots.CreateOptsBuilder) (*snapshots.Snapshot, error)) { + fake.createSnapshotMutex.Lock() + defer fake.createSnapshotMutex.Unlock() + fake.CreateSnapshotStub = stub +} + +func (fake *FakeVolumeFacade) CreateSnapshotArgsForCall(i int) (*gophercloud.ServiceClient, snapshots.CreateOptsBuilder) { + fake.createSnapshotMutex.RLock() + defer fake.createSnapshotMutex.RUnlock() + argsForCall := fake.createSnapshotArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeVolumeFacade) CreateSnapshotReturns(result1 *snapshots.Snapshot, result2 error) { + fake.createSnapshotMutex.Lock() + defer fake.createSnapshotMutex.Unlock() + fake.CreateSnapshotStub = nil + fake.createSnapshotReturns = struct { + result1 *snapshots.Snapshot + result2 error + }{result1, result2} +} + +func (fake *FakeVolumeFacade) CreateSnapshotReturnsOnCall(i int, result1 *snapshots.Snapshot, result2 error) { + fake.createSnapshotMutex.Lock() + defer fake.createSnapshotMutex.Unlock() + fake.CreateSnapshotStub = nil + if fake.createSnapshotReturnsOnCall == nil { + fake.createSnapshotReturnsOnCall = make(map[int]struct { + result1 *snapshots.Snapshot + result2 error + }) + } + fake.createSnapshotReturnsOnCall[i] = struct { + result1 *snapshots.Snapshot + result2 error + }{result1, result2} +} + +func (fake *FakeVolumeFacade) CreateVolume(arg1 utils.ServiceClient, arg2 volumes.CreateOptsBuilder) (*volumes.Volume, error) { + fake.createVolumeMutex.Lock() + ret, specificReturn := fake.createVolumeReturnsOnCall[len(fake.createVolumeArgsForCall)] + fake.createVolumeArgsForCall = append(fake.createVolumeArgsForCall, struct { + arg1 utils.ServiceClient + arg2 volumes.CreateOptsBuilder + }{arg1, arg2}) + stub := fake.CreateVolumeStub + fakeReturns := fake.createVolumeReturns + fake.recordInvocation("CreateVolume", []interface{}{arg1, arg2}) + fake.createVolumeMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeVolumeFacade) CreateVolumeCallCount() int { + fake.createVolumeMutex.RLock() + defer fake.createVolumeMutex.RUnlock() + return len(fake.createVolumeArgsForCall) +} + +func (fake *FakeVolumeFacade) CreateVolumeCalls(stub func(utils.ServiceClient, volumes.CreateOptsBuilder) (*volumes.Volume, error)) { + fake.createVolumeMutex.Lock() + defer fake.createVolumeMutex.Unlock() + fake.CreateVolumeStub = stub +} + +func (fake *FakeVolumeFacade) CreateVolumeArgsForCall(i int) (utils.ServiceClient, volumes.CreateOptsBuilder) { + fake.createVolumeMutex.RLock() + defer fake.createVolumeMutex.RUnlock() + argsForCall := fake.createVolumeArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeVolumeFacade) CreateVolumeReturns(result1 *volumes.Volume, result2 error) { + fake.createVolumeMutex.Lock() + defer fake.createVolumeMutex.Unlock() + fake.CreateVolumeStub = nil + fake.createVolumeReturns = struct { + result1 *volumes.Volume + result2 error + }{result1, result2} +} + +func (fake *FakeVolumeFacade) CreateVolumeReturnsOnCall(i int, result1 *volumes.Volume, result2 error) { + fake.createVolumeMutex.Lock() + defer fake.createVolumeMutex.Unlock() + fake.CreateVolumeStub = nil + if fake.createVolumeReturnsOnCall == nil { + fake.createVolumeReturnsOnCall = make(map[int]struct { + result1 *volumes.Volume + result2 error + }) + } + fake.createVolumeReturnsOnCall[i] = struct { + result1 *volumes.Volume + result2 error + }{result1, result2} +} + +func (fake *FakeVolumeFacade) DeleteSnapshot(arg1 *gophercloud.ServiceClient, arg2 string) error { + fake.deleteSnapshotMutex.Lock() + ret, specificReturn := fake.deleteSnapshotReturnsOnCall[len(fake.deleteSnapshotArgsForCall)] + fake.deleteSnapshotArgsForCall = append(fake.deleteSnapshotArgsForCall, struct { + arg1 *gophercloud.ServiceClient + arg2 string + }{arg1, arg2}) + stub := fake.DeleteSnapshotStub + fakeReturns := fake.deleteSnapshotReturns + fake.recordInvocation("DeleteSnapshot", []interface{}{arg1, arg2}) + fake.deleteSnapshotMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeVolumeFacade) DeleteSnapshotCallCount() int { + fake.deleteSnapshotMutex.RLock() + defer fake.deleteSnapshotMutex.RUnlock() + return len(fake.deleteSnapshotArgsForCall) +} + +func (fake *FakeVolumeFacade) DeleteSnapshotCalls(stub func(*gophercloud.ServiceClient, string) error) { + fake.deleteSnapshotMutex.Lock() + defer fake.deleteSnapshotMutex.Unlock() + fake.DeleteSnapshotStub = stub +} + +func (fake *FakeVolumeFacade) DeleteSnapshotArgsForCall(i int) (*gophercloud.ServiceClient, string) { + fake.deleteSnapshotMutex.RLock() + defer fake.deleteSnapshotMutex.RUnlock() + argsForCall := fake.deleteSnapshotArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeVolumeFacade) DeleteSnapshotReturns(result1 error) { + fake.deleteSnapshotMutex.Lock() + defer fake.deleteSnapshotMutex.Unlock() + fake.DeleteSnapshotStub = nil + fake.deleteSnapshotReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeVolumeFacade) DeleteSnapshotReturnsOnCall(i int, result1 error) { + fake.deleteSnapshotMutex.Lock() + defer fake.deleteSnapshotMutex.Unlock() + fake.DeleteSnapshotStub = nil + if fake.deleteSnapshotReturnsOnCall == nil { + fake.deleteSnapshotReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.deleteSnapshotReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *FakeVolumeFacade) DeleteVolume(arg1 utils.RetryableServiceClient, arg2 string, arg3 volumes.DeleteOptsBuilder) error { + fake.deleteVolumeMutex.Lock() + ret, specificReturn := fake.deleteVolumeReturnsOnCall[len(fake.deleteVolumeArgsForCall)] + fake.deleteVolumeArgsForCall = append(fake.deleteVolumeArgsForCall, struct { + arg1 utils.RetryableServiceClient + arg2 string + arg3 volumes.DeleteOptsBuilder + }{arg1, arg2, arg3}) + stub := fake.DeleteVolumeStub + fakeReturns := fake.deleteVolumeReturns + fake.recordInvocation("DeleteVolume", []interface{}{arg1, arg2, arg3}) + fake.deleteVolumeMutex.Unlock() + if stub != nil { + return stub(arg1, arg2, arg3) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeVolumeFacade) DeleteVolumeCallCount() int { + fake.deleteVolumeMutex.RLock() + defer fake.deleteVolumeMutex.RUnlock() + return len(fake.deleteVolumeArgsForCall) +} + +func (fake *FakeVolumeFacade) DeleteVolumeCalls(stub func(utils.RetryableServiceClient, string, volumes.DeleteOptsBuilder) error) { + fake.deleteVolumeMutex.Lock() + defer fake.deleteVolumeMutex.Unlock() + fake.DeleteVolumeStub = stub +} + +func (fake *FakeVolumeFacade) DeleteVolumeArgsForCall(i int) (utils.RetryableServiceClient, string, volumes.DeleteOptsBuilder) { + fake.deleteVolumeMutex.RLock() + defer fake.deleteVolumeMutex.RUnlock() + argsForCall := fake.deleteVolumeArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 +} + +func (fake *FakeVolumeFacade) DeleteVolumeReturns(result1 error) { + fake.deleteVolumeMutex.Lock() + defer fake.deleteVolumeMutex.Unlock() + fake.DeleteVolumeStub = nil + fake.deleteVolumeReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeVolumeFacade) DeleteVolumeReturnsOnCall(i int, result1 error) { + fake.deleteVolumeMutex.Lock() + defer fake.deleteVolumeMutex.Unlock() + fake.DeleteVolumeStub = nil + if fake.deleteVolumeReturnsOnCall == nil { + fake.deleteVolumeReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.deleteVolumeReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *FakeVolumeFacade) ExtendVolumeSize(arg1 utils.ServiceClient, arg2 string, arg3 volumeactions.ExtendSizeOptsBuilder) error { + fake.extendVolumeSizeMutex.Lock() + ret, specificReturn := fake.extendVolumeSizeReturnsOnCall[len(fake.extendVolumeSizeArgsForCall)] + fake.extendVolumeSizeArgsForCall = append(fake.extendVolumeSizeArgsForCall, struct { + arg1 utils.ServiceClient + arg2 string + arg3 volumeactions.ExtendSizeOptsBuilder + }{arg1, arg2, arg3}) + stub := fake.ExtendVolumeSizeStub + fakeReturns := fake.extendVolumeSizeReturns + fake.recordInvocation("ExtendVolumeSize", []interface{}{arg1, arg2, arg3}) + fake.extendVolumeSizeMutex.Unlock() + if stub != nil { + return stub(arg1, arg2, arg3) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeVolumeFacade) ExtendVolumeSizeCallCount() int { + fake.extendVolumeSizeMutex.RLock() + defer fake.extendVolumeSizeMutex.RUnlock() + return len(fake.extendVolumeSizeArgsForCall) +} + +func (fake *FakeVolumeFacade) ExtendVolumeSizeCalls(stub func(utils.ServiceClient, string, volumeactions.ExtendSizeOptsBuilder) error) { + fake.extendVolumeSizeMutex.Lock() + defer fake.extendVolumeSizeMutex.Unlock() + fake.ExtendVolumeSizeStub = stub +} + +func (fake *FakeVolumeFacade) ExtendVolumeSizeArgsForCall(i int) (utils.ServiceClient, string, volumeactions.ExtendSizeOptsBuilder) { + fake.extendVolumeSizeMutex.RLock() + defer fake.extendVolumeSizeMutex.RUnlock() + argsForCall := fake.extendVolumeSizeArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 +} + +func (fake *FakeVolumeFacade) ExtendVolumeSizeReturns(result1 error) { + fake.extendVolumeSizeMutex.Lock() + defer fake.extendVolumeSizeMutex.Unlock() + fake.ExtendVolumeSizeStub = nil + fake.extendVolumeSizeReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeVolumeFacade) ExtendVolumeSizeReturnsOnCall(i int, result1 error) { + fake.extendVolumeSizeMutex.Lock() + defer fake.extendVolumeSizeMutex.Unlock() + fake.ExtendVolumeSizeStub = nil + if fake.extendVolumeSizeReturnsOnCall == nil { + fake.extendVolumeSizeReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.extendVolumeSizeReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *FakeVolumeFacade) GetSnapshot(arg1 utils.RetryableServiceClient, arg2 string) (*snapshots.Snapshot, error) { + fake.getSnapshotMutex.Lock() + ret, specificReturn := fake.getSnapshotReturnsOnCall[len(fake.getSnapshotArgsForCall)] + fake.getSnapshotArgsForCall = append(fake.getSnapshotArgsForCall, struct { + arg1 utils.RetryableServiceClient + arg2 string + }{arg1, arg2}) + stub := fake.GetSnapshotStub + fakeReturns := fake.getSnapshotReturns + fake.recordInvocation("GetSnapshot", []interface{}{arg1, arg2}) + fake.getSnapshotMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeVolumeFacade) GetSnapshotCallCount() int { + fake.getSnapshotMutex.RLock() + defer fake.getSnapshotMutex.RUnlock() + return len(fake.getSnapshotArgsForCall) +} + +func (fake *FakeVolumeFacade) GetSnapshotCalls(stub func(utils.RetryableServiceClient, string) (*snapshots.Snapshot, error)) { + fake.getSnapshotMutex.Lock() + defer fake.getSnapshotMutex.Unlock() + fake.GetSnapshotStub = stub +} + +func (fake *FakeVolumeFacade) GetSnapshotArgsForCall(i int) (utils.RetryableServiceClient, string) { + fake.getSnapshotMutex.RLock() + defer fake.getSnapshotMutex.RUnlock() + argsForCall := fake.getSnapshotArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeVolumeFacade) GetSnapshotReturns(result1 *snapshots.Snapshot, result2 error) { + fake.getSnapshotMutex.Lock() + defer fake.getSnapshotMutex.Unlock() + fake.GetSnapshotStub = nil + fake.getSnapshotReturns = struct { + result1 *snapshots.Snapshot + result2 error + }{result1, result2} +} + +func (fake *FakeVolumeFacade) GetSnapshotReturnsOnCall(i int, result1 *snapshots.Snapshot, result2 error) { + fake.getSnapshotMutex.Lock() + defer fake.getSnapshotMutex.Unlock() + fake.GetSnapshotStub = nil + if fake.getSnapshotReturnsOnCall == nil { + fake.getSnapshotReturnsOnCall = make(map[int]struct { + result1 *snapshots.Snapshot + result2 error + }) + } + fake.getSnapshotReturnsOnCall[i] = struct { + result1 *snapshots.Snapshot + result2 error + }{result1, result2} +} + +func (fake *FakeVolumeFacade) GetVolume(arg1 utils.RetryableServiceClient, arg2 string) (*volumes.Volume, error) { + fake.getVolumeMutex.Lock() + ret, specificReturn := fake.getVolumeReturnsOnCall[len(fake.getVolumeArgsForCall)] + fake.getVolumeArgsForCall = append(fake.getVolumeArgsForCall, struct { + arg1 utils.RetryableServiceClient + arg2 string + }{arg1, arg2}) + stub := fake.GetVolumeStub + fakeReturns := fake.getVolumeReturns + fake.recordInvocation("GetVolume", []interface{}{arg1, arg2}) + fake.getVolumeMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeVolumeFacade) GetVolumeCallCount() int { + fake.getVolumeMutex.RLock() + defer fake.getVolumeMutex.RUnlock() + return len(fake.getVolumeArgsForCall) +} + +func (fake *FakeVolumeFacade) GetVolumeCalls(stub func(utils.RetryableServiceClient, string) (*volumes.Volume, error)) { + fake.getVolumeMutex.Lock() + defer fake.getVolumeMutex.Unlock() + fake.GetVolumeStub = stub +} + +func (fake *FakeVolumeFacade) GetVolumeArgsForCall(i int) (utils.RetryableServiceClient, string) { + fake.getVolumeMutex.RLock() + defer fake.getVolumeMutex.RUnlock() + argsForCall := fake.getVolumeArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeVolumeFacade) GetVolumeReturns(result1 *volumes.Volume, result2 error) { + fake.getVolumeMutex.Lock() + defer fake.getVolumeMutex.Unlock() + fake.GetVolumeStub = nil + fake.getVolumeReturns = struct { + result1 *volumes.Volume + result2 error + }{result1, result2} +} + +func (fake *FakeVolumeFacade) GetVolumeReturnsOnCall(i int, result1 *volumes.Volume, result2 error) { + fake.getVolumeMutex.Lock() + defer fake.getVolumeMutex.Unlock() + fake.GetVolumeStub = nil + if fake.getVolumeReturnsOnCall == nil { + fake.getVolumeReturnsOnCall = make(map[int]struct { + result1 *volumes.Volume + result2 error + }) + } + fake.getVolumeReturnsOnCall[i] = struct { + result1 *volumes.Volume + result2 error + }{result1, result2} +} + +func (fake *FakeVolumeFacade) SetDiskMetadata(arg1 utils.ServiceClient, arg2 string, arg3 volumes.UpdateOptsBuilder) error { + fake.setDiskMetadataMutex.Lock() + ret, specificReturn := fake.setDiskMetadataReturnsOnCall[len(fake.setDiskMetadataArgsForCall)] + fake.setDiskMetadataArgsForCall = append(fake.setDiskMetadataArgsForCall, struct { + arg1 utils.ServiceClient + arg2 string + arg3 volumes.UpdateOptsBuilder + }{arg1, arg2, arg3}) + stub := fake.SetDiskMetadataStub + fakeReturns := fake.setDiskMetadataReturns + fake.recordInvocation("SetDiskMetadata", []interface{}{arg1, arg2, arg3}) + fake.setDiskMetadataMutex.Unlock() + if stub != nil { + return stub(arg1, arg2, arg3) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeVolumeFacade) SetDiskMetadataCallCount() int { + fake.setDiskMetadataMutex.RLock() + defer fake.setDiskMetadataMutex.RUnlock() + return len(fake.setDiskMetadataArgsForCall) +} + +func (fake *FakeVolumeFacade) SetDiskMetadataCalls(stub func(utils.ServiceClient, string, volumes.UpdateOptsBuilder) error) { + fake.setDiskMetadataMutex.Lock() + defer fake.setDiskMetadataMutex.Unlock() + fake.SetDiskMetadataStub = stub +} + +func (fake *FakeVolumeFacade) SetDiskMetadataArgsForCall(i int) (utils.ServiceClient, string, volumes.UpdateOptsBuilder) { + fake.setDiskMetadataMutex.RLock() + defer fake.setDiskMetadataMutex.RUnlock() + argsForCall := fake.setDiskMetadataArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 +} + +func (fake *FakeVolumeFacade) SetDiskMetadataReturns(result1 error) { + fake.setDiskMetadataMutex.Lock() + defer fake.setDiskMetadataMutex.Unlock() + fake.SetDiskMetadataStub = nil + fake.setDiskMetadataReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeVolumeFacade) SetDiskMetadataReturnsOnCall(i int, result1 error) { + fake.setDiskMetadataMutex.Lock() + defer fake.setDiskMetadataMutex.Unlock() + fake.SetDiskMetadataStub = nil + if fake.setDiskMetadataReturnsOnCall == nil { + fake.setDiskMetadataReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.setDiskMetadataReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *FakeVolumeFacade) UpdateMetaDataSnapShot(arg1 *gophercloud.ServiceClient, arg2 string, arg3 snapshots.UpdateMetadataOptsBuilder) (map[string]interface{}, error) { + fake.updateMetaDataSnapShotMutex.Lock() + ret, specificReturn := fake.updateMetaDataSnapShotReturnsOnCall[len(fake.updateMetaDataSnapShotArgsForCall)] + fake.updateMetaDataSnapShotArgsForCall = append(fake.updateMetaDataSnapShotArgsForCall, struct { + arg1 *gophercloud.ServiceClient + arg2 string + arg3 snapshots.UpdateMetadataOptsBuilder + }{arg1, arg2, arg3}) + stub := fake.UpdateMetaDataSnapShotStub + fakeReturns := fake.updateMetaDataSnapShotReturns + fake.recordInvocation("UpdateMetaDataSnapShot", []interface{}{arg1, arg2, arg3}) + fake.updateMetaDataSnapShotMutex.Unlock() + if stub != nil { + return stub(arg1, arg2, arg3) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeVolumeFacade) UpdateMetaDataSnapShotCallCount() int { + fake.updateMetaDataSnapShotMutex.RLock() + defer fake.updateMetaDataSnapShotMutex.RUnlock() + return len(fake.updateMetaDataSnapShotArgsForCall) +} + +func (fake *FakeVolumeFacade) UpdateMetaDataSnapShotCalls(stub func(*gophercloud.ServiceClient, string, snapshots.UpdateMetadataOptsBuilder) (map[string]interface{}, error)) { + fake.updateMetaDataSnapShotMutex.Lock() + defer fake.updateMetaDataSnapShotMutex.Unlock() + fake.UpdateMetaDataSnapShotStub = stub +} + +func (fake *FakeVolumeFacade) UpdateMetaDataSnapShotArgsForCall(i int) (*gophercloud.ServiceClient, string, snapshots.UpdateMetadataOptsBuilder) { + fake.updateMetaDataSnapShotMutex.RLock() + defer fake.updateMetaDataSnapShotMutex.RUnlock() + argsForCall := fake.updateMetaDataSnapShotArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 +} + +func (fake *FakeVolumeFacade) UpdateMetaDataSnapShotReturns(result1 map[string]interface{}, result2 error) { + fake.updateMetaDataSnapShotMutex.Lock() + defer fake.updateMetaDataSnapShotMutex.Unlock() + fake.UpdateMetaDataSnapShotStub = nil + fake.updateMetaDataSnapShotReturns = struct { + result1 map[string]interface{} + result2 error + }{result1, result2} +} + +func (fake *FakeVolumeFacade) UpdateMetaDataSnapShotReturnsOnCall(i int, result1 map[string]interface{}, result2 error) { + fake.updateMetaDataSnapShotMutex.Lock() + defer fake.updateMetaDataSnapShotMutex.Unlock() + fake.UpdateMetaDataSnapShotStub = nil + if fake.updateMetaDataSnapShotReturnsOnCall == nil { + fake.updateMetaDataSnapShotReturnsOnCall = make(map[int]struct { + result1 map[string]interface{} + result2 error + }) + } + fake.updateMetaDataSnapShotReturnsOnCall[i] = struct { + result1 map[string]interface{} + result2 error + }{result1, result2} +} + +func (fake *FakeVolumeFacade) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.createSnapshotMutex.RLock() + defer fake.createSnapshotMutex.RUnlock() + fake.createVolumeMutex.RLock() + defer fake.createVolumeMutex.RUnlock() + fake.deleteSnapshotMutex.RLock() + defer fake.deleteSnapshotMutex.RUnlock() + fake.deleteVolumeMutex.RLock() + defer fake.deleteVolumeMutex.RUnlock() + fake.extendVolumeSizeMutex.RLock() + defer fake.extendVolumeSizeMutex.RUnlock() + fake.getSnapshotMutex.RLock() + defer fake.getSnapshotMutex.RUnlock() + fake.getVolumeMutex.RLock() + defer fake.getVolumeMutex.RUnlock() + fake.setDiskMetadataMutex.RLock() + defer fake.setDiskMetadataMutex.RUnlock() + fake.updateMetaDataSnapShotMutex.RLock() + defer fake.updateMetaDataSnapShotMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *FakeVolumeFacade) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ volume.VolumeFacade = new(FakeVolumeFacade) diff --git a/src/openstack_cpi_golang/cpi/volume/volumefakes/fake_volume_service.go b/src/openstack_cpi_golang/cpi/volume/volumefakes/fake_volume_service.go new file mode 100644 index 00000000..86e794aa --- /dev/null +++ b/src/openstack_cpi_golang/cpi/volume/volumefakes/fake_volume_service.go @@ -0,0 +1,906 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package volumefakes + +import ( + "sync" + "time" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/properties" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/volume" + "github.com/gophercloud/gophercloud/openstack/blockstorage/v3/snapshots" + "github.com/gophercloud/gophercloud/openstack/blockstorage/v3/volumes" +) + +type FakeVolumeService struct { + CreateSnapshotStub func(string, bool, string, string, map[string]string) (*snapshots.Snapshot, error) + createSnapshotMutex sync.RWMutex + createSnapshotArgsForCall []struct { + arg1 string + arg2 bool + arg3 string + arg4 string + arg5 map[string]string + } + createSnapshotReturns struct { + result1 *snapshots.Snapshot + result2 error + } + createSnapshotReturnsOnCall map[int]struct { + result1 *snapshots.Snapshot + result2 error + } + CreateVolumeStub func(int, properties.CreateDisk, string) (*volumes.Volume, error) + createVolumeMutex sync.RWMutex + createVolumeArgsForCall []struct { + arg1 int + arg2 properties.CreateDisk + arg3 string + } + createVolumeReturns struct { + result1 *volumes.Volume + result2 error + } + createVolumeReturnsOnCall map[int]struct { + result1 *volumes.Volume + result2 error + } + DeleteSnapshotStub func(string) error + deleteSnapshotMutex sync.RWMutex + deleteSnapshotArgsForCall []struct { + arg1 string + } + deleteSnapshotReturns struct { + result1 error + } + deleteSnapshotReturnsOnCall map[int]struct { + result1 error + } + DeleteVolumeStub func(string) error + deleteVolumeMutex sync.RWMutex + deleteVolumeArgsForCall []struct { + arg1 string + } + deleteVolumeReturns struct { + result1 error + } + deleteVolumeReturnsOnCall map[int]struct { + result1 error + } + ExtendVolumeSizeStub func(string, int) error + extendVolumeSizeMutex sync.RWMutex + extendVolumeSizeArgsForCall []struct { + arg1 string + arg2 int + } + extendVolumeSizeReturns struct { + result1 error + } + extendVolumeSizeReturnsOnCall map[int]struct { + result1 error + } + GetSnapshotStub func(string) (*snapshots.Snapshot, error) + getSnapshotMutex sync.RWMutex + getSnapshotArgsForCall []struct { + arg1 string + } + getSnapshotReturns struct { + result1 *snapshots.Snapshot + result2 error + } + getSnapshotReturnsOnCall map[int]struct { + result1 *snapshots.Snapshot + result2 error + } + GetVolumeStub func(string) (*volumes.Volume, error) + getVolumeMutex sync.RWMutex + getVolumeArgsForCall []struct { + arg1 string + } + getVolumeReturns struct { + result1 *volumes.Volume + result2 error + } + getVolumeReturnsOnCall map[int]struct { + result1 *volumes.Volume + result2 error + } + SetDiskMetadataStub func(string, map[string]string) error + setDiskMetadataMutex sync.RWMutex + setDiskMetadataArgsForCall []struct { + arg1 string + arg2 map[string]string + } + setDiskMetadataReturns struct { + result1 error + } + setDiskMetadataReturnsOnCall map[int]struct { + result1 error + } + UpdateMetaDataSnapshotStub func(string, map[string]interface{}) (map[string]interface{}, error) + updateMetaDataSnapshotMutex sync.RWMutex + updateMetaDataSnapshotArgsForCall []struct { + arg1 string + arg2 map[string]interface{} + } + updateMetaDataSnapshotReturns struct { + result1 map[string]interface{} + result2 error + } + updateMetaDataSnapshotReturnsOnCall map[int]struct { + result1 map[string]interface{} + result2 error + } + WaitForSnapshotToBecomeStatusStub func(string, time.Duration, string) error + waitForSnapshotToBecomeStatusMutex sync.RWMutex + waitForSnapshotToBecomeStatusArgsForCall []struct { + arg1 string + arg2 time.Duration + arg3 string + } + waitForSnapshotToBecomeStatusReturns struct { + result1 error + } + waitForSnapshotToBecomeStatusReturnsOnCall map[int]struct { + result1 error + } + WaitForVolumeToBecomeStatusStub func(string, time.Duration, string) error + waitForVolumeToBecomeStatusMutex sync.RWMutex + waitForVolumeToBecomeStatusArgsForCall []struct { + arg1 string + arg2 time.Duration + arg3 string + } + waitForVolumeToBecomeStatusReturns struct { + result1 error + } + waitForVolumeToBecomeStatusReturnsOnCall map[int]struct { + result1 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *FakeVolumeService) CreateSnapshot(arg1 string, arg2 bool, arg3 string, arg4 string, arg5 map[string]string) (*snapshots.Snapshot, error) { + fake.createSnapshotMutex.Lock() + ret, specificReturn := fake.createSnapshotReturnsOnCall[len(fake.createSnapshotArgsForCall)] + fake.createSnapshotArgsForCall = append(fake.createSnapshotArgsForCall, struct { + arg1 string + arg2 bool + arg3 string + arg4 string + arg5 map[string]string + }{arg1, arg2, arg3, arg4, arg5}) + stub := fake.CreateSnapshotStub + fakeReturns := fake.createSnapshotReturns + fake.recordInvocation("CreateSnapshot", []interface{}{arg1, arg2, arg3, arg4, arg5}) + fake.createSnapshotMutex.Unlock() + if stub != nil { + return stub(arg1, arg2, arg3, arg4, arg5) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeVolumeService) CreateSnapshotCallCount() int { + fake.createSnapshotMutex.RLock() + defer fake.createSnapshotMutex.RUnlock() + return len(fake.createSnapshotArgsForCall) +} + +func (fake *FakeVolumeService) CreateSnapshotCalls(stub func(string, bool, string, string, map[string]string) (*snapshots.Snapshot, error)) { + fake.createSnapshotMutex.Lock() + defer fake.createSnapshotMutex.Unlock() + fake.CreateSnapshotStub = stub +} + +func (fake *FakeVolumeService) CreateSnapshotArgsForCall(i int) (string, bool, string, string, map[string]string) { + fake.createSnapshotMutex.RLock() + defer fake.createSnapshotMutex.RUnlock() + argsForCall := fake.createSnapshotArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4, argsForCall.arg5 +} + +func (fake *FakeVolumeService) CreateSnapshotReturns(result1 *snapshots.Snapshot, result2 error) { + fake.createSnapshotMutex.Lock() + defer fake.createSnapshotMutex.Unlock() + fake.CreateSnapshotStub = nil + fake.createSnapshotReturns = struct { + result1 *snapshots.Snapshot + result2 error + }{result1, result2} +} + +func (fake *FakeVolumeService) CreateSnapshotReturnsOnCall(i int, result1 *snapshots.Snapshot, result2 error) { + fake.createSnapshotMutex.Lock() + defer fake.createSnapshotMutex.Unlock() + fake.CreateSnapshotStub = nil + if fake.createSnapshotReturnsOnCall == nil { + fake.createSnapshotReturnsOnCall = make(map[int]struct { + result1 *snapshots.Snapshot + result2 error + }) + } + fake.createSnapshotReturnsOnCall[i] = struct { + result1 *snapshots.Snapshot + result2 error + }{result1, result2} +} + +func (fake *FakeVolumeService) CreateVolume(arg1 int, arg2 properties.CreateDisk, arg3 string) (*volumes.Volume, error) { + fake.createVolumeMutex.Lock() + ret, specificReturn := fake.createVolumeReturnsOnCall[len(fake.createVolumeArgsForCall)] + fake.createVolumeArgsForCall = append(fake.createVolumeArgsForCall, struct { + arg1 int + arg2 properties.CreateDisk + arg3 string + }{arg1, arg2, arg3}) + stub := fake.CreateVolumeStub + fakeReturns := fake.createVolumeReturns + fake.recordInvocation("CreateVolume", []interface{}{arg1, arg2, arg3}) + fake.createVolumeMutex.Unlock() + if stub != nil { + return stub(arg1, arg2, arg3) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeVolumeService) CreateVolumeCallCount() int { + fake.createVolumeMutex.RLock() + defer fake.createVolumeMutex.RUnlock() + return len(fake.createVolumeArgsForCall) +} + +func (fake *FakeVolumeService) CreateVolumeCalls(stub func(int, properties.CreateDisk, string) (*volumes.Volume, error)) { + fake.createVolumeMutex.Lock() + defer fake.createVolumeMutex.Unlock() + fake.CreateVolumeStub = stub +} + +func (fake *FakeVolumeService) CreateVolumeArgsForCall(i int) (int, properties.CreateDisk, string) { + fake.createVolumeMutex.RLock() + defer fake.createVolumeMutex.RUnlock() + argsForCall := fake.createVolumeArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 +} + +func (fake *FakeVolumeService) CreateVolumeReturns(result1 *volumes.Volume, result2 error) { + fake.createVolumeMutex.Lock() + defer fake.createVolumeMutex.Unlock() + fake.CreateVolumeStub = nil + fake.createVolumeReturns = struct { + result1 *volumes.Volume + result2 error + }{result1, result2} +} + +func (fake *FakeVolumeService) CreateVolumeReturnsOnCall(i int, result1 *volumes.Volume, result2 error) { + fake.createVolumeMutex.Lock() + defer fake.createVolumeMutex.Unlock() + fake.CreateVolumeStub = nil + if fake.createVolumeReturnsOnCall == nil { + fake.createVolumeReturnsOnCall = make(map[int]struct { + result1 *volumes.Volume + result2 error + }) + } + fake.createVolumeReturnsOnCall[i] = struct { + result1 *volumes.Volume + result2 error + }{result1, result2} +} + +func (fake *FakeVolumeService) DeleteSnapshot(arg1 string) error { + fake.deleteSnapshotMutex.Lock() + ret, specificReturn := fake.deleteSnapshotReturnsOnCall[len(fake.deleteSnapshotArgsForCall)] + fake.deleteSnapshotArgsForCall = append(fake.deleteSnapshotArgsForCall, struct { + arg1 string + }{arg1}) + stub := fake.DeleteSnapshotStub + fakeReturns := fake.deleteSnapshotReturns + fake.recordInvocation("DeleteSnapshot", []interface{}{arg1}) + fake.deleteSnapshotMutex.Unlock() + if stub != nil { + return stub(arg1) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeVolumeService) DeleteSnapshotCallCount() int { + fake.deleteSnapshotMutex.RLock() + defer fake.deleteSnapshotMutex.RUnlock() + return len(fake.deleteSnapshotArgsForCall) +} + +func (fake *FakeVolumeService) DeleteSnapshotCalls(stub func(string) error) { + fake.deleteSnapshotMutex.Lock() + defer fake.deleteSnapshotMutex.Unlock() + fake.DeleteSnapshotStub = stub +} + +func (fake *FakeVolumeService) DeleteSnapshotArgsForCall(i int) string { + fake.deleteSnapshotMutex.RLock() + defer fake.deleteSnapshotMutex.RUnlock() + argsForCall := fake.deleteSnapshotArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeVolumeService) DeleteSnapshotReturns(result1 error) { + fake.deleteSnapshotMutex.Lock() + defer fake.deleteSnapshotMutex.Unlock() + fake.DeleteSnapshotStub = nil + fake.deleteSnapshotReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeVolumeService) DeleteSnapshotReturnsOnCall(i int, result1 error) { + fake.deleteSnapshotMutex.Lock() + defer fake.deleteSnapshotMutex.Unlock() + fake.DeleteSnapshotStub = nil + if fake.deleteSnapshotReturnsOnCall == nil { + fake.deleteSnapshotReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.deleteSnapshotReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *FakeVolumeService) DeleteVolume(arg1 string) error { + fake.deleteVolumeMutex.Lock() + ret, specificReturn := fake.deleteVolumeReturnsOnCall[len(fake.deleteVolumeArgsForCall)] + fake.deleteVolumeArgsForCall = append(fake.deleteVolumeArgsForCall, struct { + arg1 string + }{arg1}) + stub := fake.DeleteVolumeStub + fakeReturns := fake.deleteVolumeReturns + fake.recordInvocation("DeleteVolume", []interface{}{arg1}) + fake.deleteVolumeMutex.Unlock() + if stub != nil { + return stub(arg1) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeVolumeService) DeleteVolumeCallCount() int { + fake.deleteVolumeMutex.RLock() + defer fake.deleteVolumeMutex.RUnlock() + return len(fake.deleteVolumeArgsForCall) +} + +func (fake *FakeVolumeService) DeleteVolumeCalls(stub func(string) error) { + fake.deleteVolumeMutex.Lock() + defer fake.deleteVolumeMutex.Unlock() + fake.DeleteVolumeStub = stub +} + +func (fake *FakeVolumeService) DeleteVolumeArgsForCall(i int) string { + fake.deleteVolumeMutex.RLock() + defer fake.deleteVolumeMutex.RUnlock() + argsForCall := fake.deleteVolumeArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeVolumeService) DeleteVolumeReturns(result1 error) { + fake.deleteVolumeMutex.Lock() + defer fake.deleteVolumeMutex.Unlock() + fake.DeleteVolumeStub = nil + fake.deleteVolumeReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeVolumeService) DeleteVolumeReturnsOnCall(i int, result1 error) { + fake.deleteVolumeMutex.Lock() + defer fake.deleteVolumeMutex.Unlock() + fake.DeleteVolumeStub = nil + if fake.deleteVolumeReturnsOnCall == nil { + fake.deleteVolumeReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.deleteVolumeReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *FakeVolumeService) ExtendVolumeSize(arg1 string, arg2 int) error { + fake.extendVolumeSizeMutex.Lock() + ret, specificReturn := fake.extendVolumeSizeReturnsOnCall[len(fake.extendVolumeSizeArgsForCall)] + fake.extendVolumeSizeArgsForCall = append(fake.extendVolumeSizeArgsForCall, struct { + arg1 string + arg2 int + }{arg1, arg2}) + stub := fake.ExtendVolumeSizeStub + fakeReturns := fake.extendVolumeSizeReturns + fake.recordInvocation("ExtendVolumeSize", []interface{}{arg1, arg2}) + fake.extendVolumeSizeMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeVolumeService) ExtendVolumeSizeCallCount() int { + fake.extendVolumeSizeMutex.RLock() + defer fake.extendVolumeSizeMutex.RUnlock() + return len(fake.extendVolumeSizeArgsForCall) +} + +func (fake *FakeVolumeService) ExtendVolumeSizeCalls(stub func(string, int) error) { + fake.extendVolumeSizeMutex.Lock() + defer fake.extendVolumeSizeMutex.Unlock() + fake.ExtendVolumeSizeStub = stub +} + +func (fake *FakeVolumeService) ExtendVolumeSizeArgsForCall(i int) (string, int) { + fake.extendVolumeSizeMutex.RLock() + defer fake.extendVolumeSizeMutex.RUnlock() + argsForCall := fake.extendVolumeSizeArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeVolumeService) ExtendVolumeSizeReturns(result1 error) { + fake.extendVolumeSizeMutex.Lock() + defer fake.extendVolumeSizeMutex.Unlock() + fake.ExtendVolumeSizeStub = nil + fake.extendVolumeSizeReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeVolumeService) ExtendVolumeSizeReturnsOnCall(i int, result1 error) { + fake.extendVolumeSizeMutex.Lock() + defer fake.extendVolumeSizeMutex.Unlock() + fake.ExtendVolumeSizeStub = nil + if fake.extendVolumeSizeReturnsOnCall == nil { + fake.extendVolumeSizeReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.extendVolumeSizeReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *FakeVolumeService) GetSnapshot(arg1 string) (*snapshots.Snapshot, error) { + fake.getSnapshotMutex.Lock() + ret, specificReturn := fake.getSnapshotReturnsOnCall[len(fake.getSnapshotArgsForCall)] + fake.getSnapshotArgsForCall = append(fake.getSnapshotArgsForCall, struct { + arg1 string + }{arg1}) + stub := fake.GetSnapshotStub + fakeReturns := fake.getSnapshotReturns + fake.recordInvocation("GetSnapshot", []interface{}{arg1}) + fake.getSnapshotMutex.Unlock() + if stub != nil { + return stub(arg1) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeVolumeService) GetSnapshotCallCount() int { + fake.getSnapshotMutex.RLock() + defer fake.getSnapshotMutex.RUnlock() + return len(fake.getSnapshotArgsForCall) +} + +func (fake *FakeVolumeService) GetSnapshotCalls(stub func(string) (*snapshots.Snapshot, error)) { + fake.getSnapshotMutex.Lock() + defer fake.getSnapshotMutex.Unlock() + fake.GetSnapshotStub = stub +} + +func (fake *FakeVolumeService) GetSnapshotArgsForCall(i int) string { + fake.getSnapshotMutex.RLock() + defer fake.getSnapshotMutex.RUnlock() + argsForCall := fake.getSnapshotArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeVolumeService) GetSnapshotReturns(result1 *snapshots.Snapshot, result2 error) { + fake.getSnapshotMutex.Lock() + defer fake.getSnapshotMutex.Unlock() + fake.GetSnapshotStub = nil + fake.getSnapshotReturns = struct { + result1 *snapshots.Snapshot + result2 error + }{result1, result2} +} + +func (fake *FakeVolumeService) GetSnapshotReturnsOnCall(i int, result1 *snapshots.Snapshot, result2 error) { + fake.getSnapshotMutex.Lock() + defer fake.getSnapshotMutex.Unlock() + fake.GetSnapshotStub = nil + if fake.getSnapshotReturnsOnCall == nil { + fake.getSnapshotReturnsOnCall = make(map[int]struct { + result1 *snapshots.Snapshot + result2 error + }) + } + fake.getSnapshotReturnsOnCall[i] = struct { + result1 *snapshots.Snapshot + result2 error + }{result1, result2} +} + +func (fake *FakeVolumeService) GetVolume(arg1 string) (*volumes.Volume, error) { + fake.getVolumeMutex.Lock() + ret, specificReturn := fake.getVolumeReturnsOnCall[len(fake.getVolumeArgsForCall)] + fake.getVolumeArgsForCall = append(fake.getVolumeArgsForCall, struct { + arg1 string + }{arg1}) + stub := fake.GetVolumeStub + fakeReturns := fake.getVolumeReturns + fake.recordInvocation("GetVolume", []interface{}{arg1}) + fake.getVolumeMutex.Unlock() + if stub != nil { + return stub(arg1) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeVolumeService) GetVolumeCallCount() int { + fake.getVolumeMutex.RLock() + defer fake.getVolumeMutex.RUnlock() + return len(fake.getVolumeArgsForCall) +} + +func (fake *FakeVolumeService) GetVolumeCalls(stub func(string) (*volumes.Volume, error)) { + fake.getVolumeMutex.Lock() + defer fake.getVolumeMutex.Unlock() + fake.GetVolumeStub = stub +} + +func (fake *FakeVolumeService) GetVolumeArgsForCall(i int) string { + fake.getVolumeMutex.RLock() + defer fake.getVolumeMutex.RUnlock() + argsForCall := fake.getVolumeArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeVolumeService) GetVolumeReturns(result1 *volumes.Volume, result2 error) { + fake.getVolumeMutex.Lock() + defer fake.getVolumeMutex.Unlock() + fake.GetVolumeStub = nil + fake.getVolumeReturns = struct { + result1 *volumes.Volume + result2 error + }{result1, result2} +} + +func (fake *FakeVolumeService) GetVolumeReturnsOnCall(i int, result1 *volumes.Volume, result2 error) { + fake.getVolumeMutex.Lock() + defer fake.getVolumeMutex.Unlock() + fake.GetVolumeStub = nil + if fake.getVolumeReturnsOnCall == nil { + fake.getVolumeReturnsOnCall = make(map[int]struct { + result1 *volumes.Volume + result2 error + }) + } + fake.getVolumeReturnsOnCall[i] = struct { + result1 *volumes.Volume + result2 error + }{result1, result2} +} + +func (fake *FakeVolumeService) SetDiskMetadata(arg1 string, arg2 map[string]string) error { + fake.setDiskMetadataMutex.Lock() + ret, specificReturn := fake.setDiskMetadataReturnsOnCall[len(fake.setDiskMetadataArgsForCall)] + fake.setDiskMetadataArgsForCall = append(fake.setDiskMetadataArgsForCall, struct { + arg1 string + arg2 map[string]string + }{arg1, arg2}) + stub := fake.SetDiskMetadataStub + fakeReturns := fake.setDiskMetadataReturns + fake.recordInvocation("SetDiskMetadata", []interface{}{arg1, arg2}) + fake.setDiskMetadataMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeVolumeService) SetDiskMetadataCallCount() int { + fake.setDiskMetadataMutex.RLock() + defer fake.setDiskMetadataMutex.RUnlock() + return len(fake.setDiskMetadataArgsForCall) +} + +func (fake *FakeVolumeService) SetDiskMetadataCalls(stub func(string, map[string]string) error) { + fake.setDiskMetadataMutex.Lock() + defer fake.setDiskMetadataMutex.Unlock() + fake.SetDiskMetadataStub = stub +} + +func (fake *FakeVolumeService) SetDiskMetadataArgsForCall(i int) (string, map[string]string) { + fake.setDiskMetadataMutex.RLock() + defer fake.setDiskMetadataMutex.RUnlock() + argsForCall := fake.setDiskMetadataArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeVolumeService) SetDiskMetadataReturns(result1 error) { + fake.setDiskMetadataMutex.Lock() + defer fake.setDiskMetadataMutex.Unlock() + fake.SetDiskMetadataStub = nil + fake.setDiskMetadataReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeVolumeService) SetDiskMetadataReturnsOnCall(i int, result1 error) { + fake.setDiskMetadataMutex.Lock() + defer fake.setDiskMetadataMutex.Unlock() + fake.SetDiskMetadataStub = nil + if fake.setDiskMetadataReturnsOnCall == nil { + fake.setDiskMetadataReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.setDiskMetadataReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *FakeVolumeService) UpdateMetaDataSnapshot(arg1 string, arg2 map[string]interface{}) (map[string]interface{}, error) { + fake.updateMetaDataSnapshotMutex.Lock() + ret, specificReturn := fake.updateMetaDataSnapshotReturnsOnCall[len(fake.updateMetaDataSnapshotArgsForCall)] + fake.updateMetaDataSnapshotArgsForCall = append(fake.updateMetaDataSnapshotArgsForCall, struct { + arg1 string + arg2 map[string]interface{} + }{arg1, arg2}) + stub := fake.UpdateMetaDataSnapshotStub + fakeReturns := fake.updateMetaDataSnapshotReturns + fake.recordInvocation("UpdateMetaDataSnapshot", []interface{}{arg1, arg2}) + fake.updateMetaDataSnapshotMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeVolumeService) UpdateMetaDataSnapshotCallCount() int { + fake.updateMetaDataSnapshotMutex.RLock() + defer fake.updateMetaDataSnapshotMutex.RUnlock() + return len(fake.updateMetaDataSnapshotArgsForCall) +} + +func (fake *FakeVolumeService) UpdateMetaDataSnapshotCalls(stub func(string, map[string]interface{}) (map[string]interface{}, error)) { + fake.updateMetaDataSnapshotMutex.Lock() + defer fake.updateMetaDataSnapshotMutex.Unlock() + fake.UpdateMetaDataSnapshotStub = stub +} + +func (fake *FakeVolumeService) UpdateMetaDataSnapshotArgsForCall(i int) (string, map[string]interface{}) { + fake.updateMetaDataSnapshotMutex.RLock() + defer fake.updateMetaDataSnapshotMutex.RUnlock() + argsForCall := fake.updateMetaDataSnapshotArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeVolumeService) UpdateMetaDataSnapshotReturns(result1 map[string]interface{}, result2 error) { + fake.updateMetaDataSnapshotMutex.Lock() + defer fake.updateMetaDataSnapshotMutex.Unlock() + fake.UpdateMetaDataSnapshotStub = nil + fake.updateMetaDataSnapshotReturns = struct { + result1 map[string]interface{} + result2 error + }{result1, result2} +} + +func (fake *FakeVolumeService) UpdateMetaDataSnapshotReturnsOnCall(i int, result1 map[string]interface{}, result2 error) { + fake.updateMetaDataSnapshotMutex.Lock() + defer fake.updateMetaDataSnapshotMutex.Unlock() + fake.UpdateMetaDataSnapshotStub = nil + if fake.updateMetaDataSnapshotReturnsOnCall == nil { + fake.updateMetaDataSnapshotReturnsOnCall = make(map[int]struct { + result1 map[string]interface{} + result2 error + }) + } + fake.updateMetaDataSnapshotReturnsOnCall[i] = struct { + result1 map[string]interface{} + result2 error + }{result1, result2} +} + +func (fake *FakeVolumeService) WaitForSnapshotToBecomeStatus(arg1 string, arg2 time.Duration, arg3 string) error { + fake.waitForSnapshotToBecomeStatusMutex.Lock() + ret, specificReturn := fake.waitForSnapshotToBecomeStatusReturnsOnCall[len(fake.waitForSnapshotToBecomeStatusArgsForCall)] + fake.waitForSnapshotToBecomeStatusArgsForCall = append(fake.waitForSnapshotToBecomeStatusArgsForCall, struct { + arg1 string + arg2 time.Duration + arg3 string + }{arg1, arg2, arg3}) + stub := fake.WaitForSnapshotToBecomeStatusStub + fakeReturns := fake.waitForSnapshotToBecomeStatusReturns + fake.recordInvocation("WaitForSnapshotToBecomeStatus", []interface{}{arg1, arg2, arg3}) + fake.waitForSnapshotToBecomeStatusMutex.Unlock() + if stub != nil { + return stub(arg1, arg2, arg3) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeVolumeService) WaitForSnapshotToBecomeStatusCallCount() int { + fake.waitForSnapshotToBecomeStatusMutex.RLock() + defer fake.waitForSnapshotToBecomeStatusMutex.RUnlock() + return len(fake.waitForSnapshotToBecomeStatusArgsForCall) +} + +func (fake *FakeVolumeService) WaitForSnapshotToBecomeStatusCalls(stub func(string, time.Duration, string) error) { + fake.waitForSnapshotToBecomeStatusMutex.Lock() + defer fake.waitForSnapshotToBecomeStatusMutex.Unlock() + fake.WaitForSnapshotToBecomeStatusStub = stub +} + +func (fake *FakeVolumeService) WaitForSnapshotToBecomeStatusArgsForCall(i int) (string, time.Duration, string) { + fake.waitForSnapshotToBecomeStatusMutex.RLock() + defer fake.waitForSnapshotToBecomeStatusMutex.RUnlock() + argsForCall := fake.waitForSnapshotToBecomeStatusArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 +} + +func (fake *FakeVolumeService) WaitForSnapshotToBecomeStatusReturns(result1 error) { + fake.waitForSnapshotToBecomeStatusMutex.Lock() + defer fake.waitForSnapshotToBecomeStatusMutex.Unlock() + fake.WaitForSnapshotToBecomeStatusStub = nil + fake.waitForSnapshotToBecomeStatusReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeVolumeService) WaitForSnapshotToBecomeStatusReturnsOnCall(i int, result1 error) { + fake.waitForSnapshotToBecomeStatusMutex.Lock() + defer fake.waitForSnapshotToBecomeStatusMutex.Unlock() + fake.WaitForSnapshotToBecomeStatusStub = nil + if fake.waitForSnapshotToBecomeStatusReturnsOnCall == nil { + fake.waitForSnapshotToBecomeStatusReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.waitForSnapshotToBecomeStatusReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *FakeVolumeService) WaitForVolumeToBecomeStatus(arg1 string, arg2 time.Duration, arg3 string) error { + fake.waitForVolumeToBecomeStatusMutex.Lock() + ret, specificReturn := fake.waitForVolumeToBecomeStatusReturnsOnCall[len(fake.waitForVolumeToBecomeStatusArgsForCall)] + fake.waitForVolumeToBecomeStatusArgsForCall = append(fake.waitForVolumeToBecomeStatusArgsForCall, struct { + arg1 string + arg2 time.Duration + arg3 string + }{arg1, arg2, arg3}) + stub := fake.WaitForVolumeToBecomeStatusStub + fakeReturns := fake.waitForVolumeToBecomeStatusReturns + fake.recordInvocation("WaitForVolumeToBecomeStatus", []interface{}{arg1, arg2, arg3}) + fake.waitForVolumeToBecomeStatusMutex.Unlock() + if stub != nil { + return stub(arg1, arg2, arg3) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeVolumeService) WaitForVolumeToBecomeStatusCallCount() int { + fake.waitForVolumeToBecomeStatusMutex.RLock() + defer fake.waitForVolumeToBecomeStatusMutex.RUnlock() + return len(fake.waitForVolumeToBecomeStatusArgsForCall) +} + +func (fake *FakeVolumeService) WaitForVolumeToBecomeStatusCalls(stub func(string, time.Duration, string) error) { + fake.waitForVolumeToBecomeStatusMutex.Lock() + defer fake.waitForVolumeToBecomeStatusMutex.Unlock() + fake.WaitForVolumeToBecomeStatusStub = stub +} + +func (fake *FakeVolumeService) WaitForVolumeToBecomeStatusArgsForCall(i int) (string, time.Duration, string) { + fake.waitForVolumeToBecomeStatusMutex.RLock() + defer fake.waitForVolumeToBecomeStatusMutex.RUnlock() + argsForCall := fake.waitForVolumeToBecomeStatusArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 +} + +func (fake *FakeVolumeService) WaitForVolumeToBecomeStatusReturns(result1 error) { + fake.waitForVolumeToBecomeStatusMutex.Lock() + defer fake.waitForVolumeToBecomeStatusMutex.Unlock() + fake.WaitForVolumeToBecomeStatusStub = nil + fake.waitForVolumeToBecomeStatusReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeVolumeService) WaitForVolumeToBecomeStatusReturnsOnCall(i int, result1 error) { + fake.waitForVolumeToBecomeStatusMutex.Lock() + defer fake.waitForVolumeToBecomeStatusMutex.Unlock() + fake.WaitForVolumeToBecomeStatusStub = nil + if fake.waitForVolumeToBecomeStatusReturnsOnCall == nil { + fake.waitForVolumeToBecomeStatusReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.waitForVolumeToBecomeStatusReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *FakeVolumeService) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.createSnapshotMutex.RLock() + defer fake.createSnapshotMutex.RUnlock() + fake.createVolumeMutex.RLock() + defer fake.createVolumeMutex.RUnlock() + fake.deleteSnapshotMutex.RLock() + defer fake.deleteSnapshotMutex.RUnlock() + fake.deleteVolumeMutex.RLock() + defer fake.deleteVolumeMutex.RUnlock() + fake.extendVolumeSizeMutex.RLock() + defer fake.extendVolumeSizeMutex.RUnlock() + fake.getSnapshotMutex.RLock() + defer fake.getSnapshotMutex.RUnlock() + fake.getVolumeMutex.RLock() + defer fake.getVolumeMutex.RUnlock() + fake.setDiskMetadataMutex.RLock() + defer fake.setDiskMetadataMutex.RUnlock() + fake.updateMetaDataSnapshotMutex.RLock() + defer fake.updateMetaDataSnapshotMutex.RUnlock() + fake.waitForSnapshotToBecomeStatusMutex.RLock() + defer fake.waitForSnapshotToBecomeStatusMutex.RUnlock() + fake.waitForVolumeToBecomeStatusMutex.RLock() + defer fake.waitForVolumeToBecomeStatusMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *FakeVolumeService) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ volume.VolumeService = new(FakeVolumeService) diff --git a/src/openstack_cpi_golang/cpi/volume/volumefakes/fake_volume_service_builder.go b/src/openstack_cpi_golang/cpi/volume/volumefakes/fake_volume_service_builder.go new file mode 100644 index 00000000..b7af3a6b --- /dev/null +++ b/src/openstack_cpi_golang/cpi/volume/volumefakes/fake_volume_service_builder.go @@ -0,0 +1,107 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package volumefakes + +import ( + "sync" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/volume" +) + +type FakeVolumeServiceBuilder struct { + BuildStub func() (volume.VolumeService, error) + buildMutex sync.RWMutex + buildArgsForCall []struct { + } + buildReturns struct { + result1 volume.VolumeService + result2 error + } + buildReturnsOnCall map[int]struct { + result1 volume.VolumeService + result2 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *FakeVolumeServiceBuilder) Build() (volume.VolumeService, error) { + fake.buildMutex.Lock() + ret, specificReturn := fake.buildReturnsOnCall[len(fake.buildArgsForCall)] + fake.buildArgsForCall = append(fake.buildArgsForCall, struct { + }{}) + stub := fake.BuildStub + fakeReturns := fake.buildReturns + fake.recordInvocation("Build", []interface{}{}) + fake.buildMutex.Unlock() + if stub != nil { + return stub() + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeVolumeServiceBuilder) BuildCallCount() int { + fake.buildMutex.RLock() + defer fake.buildMutex.RUnlock() + return len(fake.buildArgsForCall) +} + +func (fake *FakeVolumeServiceBuilder) BuildCalls(stub func() (volume.VolumeService, error)) { + fake.buildMutex.Lock() + defer fake.buildMutex.Unlock() + fake.BuildStub = stub +} + +func (fake *FakeVolumeServiceBuilder) BuildReturns(result1 volume.VolumeService, result2 error) { + fake.buildMutex.Lock() + defer fake.buildMutex.Unlock() + fake.BuildStub = nil + fake.buildReturns = struct { + result1 volume.VolumeService + result2 error + }{result1, result2} +} + +func (fake *FakeVolumeServiceBuilder) BuildReturnsOnCall(i int, result1 volume.VolumeService, result2 error) { + fake.buildMutex.Lock() + defer fake.buildMutex.Unlock() + fake.BuildStub = nil + if fake.buildReturnsOnCall == nil { + fake.buildReturnsOnCall = make(map[int]struct { + result1 volume.VolumeService + result2 error + }) + } + fake.buildReturnsOnCall[i] = struct { + result1 volume.VolumeService + result2 error + }{result1, result2} +} + +func (fake *FakeVolumeServiceBuilder) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.buildMutex.RLock() + defer fake.buildMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *FakeVolumeServiceBuilder) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ volume.VolumeServiceBuilder = new(FakeVolumeServiceBuilder) diff --git a/src/openstack_cpi_golang/go.mod b/src/openstack_cpi_golang/go.mod index 2bbb7ea2..79df388e 100644 --- a/src/openstack_cpi_golang/go.mod +++ b/src/openstack_cpi_golang/go.mod @@ -7,6 +7,7 @@ toolchain go1.21.3 require ( github.com/cloudfoundry/bosh-cpi-go v0.0.0-20240224100157-0922490cd354 github.com/cloudfoundry/bosh-utils v0.0.446 + github.com/google/uuid v1.6.0 github.com/gophercloud/gophercloud v1.12.0 github.com/maxbrunsfeld/counterfeiter/v6 v6.8.1 github.com/onsi/ginkgo/v2 v2.17.1 diff --git a/src/openstack_cpi_golang/go.sum b/src/openstack_cpi_golang/go.sum index 8f1e4e76..9b16d50d 100644 --- a/src/openstack_cpi_golang/go.sum +++ b/src/openstack_cpi_golang/go.sum @@ -15,6 +15,8 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/pprof v0.0.0-20240207164012-fb44976bdcd5 h1:E/LAvt58di64hlYjx7AsNS6C/ysHWYo+2qPCZKTQhRo= github.com/google/pprof v0.0.0-20240207164012-fb44976bdcd5/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gophercloud/gophercloud v1.12.0 h1:Jrz16vPAL93l80q16fp8NplrTCp93y7rZh2P3Q4Yq7g= github.com/gophercloud/gophercloud v1.12.0/go.mod h1:aAVqcocTSXh2vYFZ1JTvx4EQmfgzxRcNupUfxZbBNDM= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= diff --git a/src/openstack_cpi_golang/integration/attach_disk_test.go b/src/openstack_cpi_golang/integration/attach_disk_test.go new file mode 100644 index 00000000..33a96977 --- /dev/null +++ b/src/openstack_cpi_golang/integration/attach_disk_test.go @@ -0,0 +1,143 @@ +package integration_test + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi" + "github.com/gophercloud/gophercloud/openstack/blockstorage/v3/volumes" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("AttachDiskMethod Integration Tests", func() { + + var getVolumeCallCount int + var volumeStatus string + var attachments []volumes.Attachment + + BeforeEach(func() { + SetupHTTP() + MockAuthentication() + getVolumeCallCount = 1 + Mux.HandleFunc("/v3/volumes/volume-id-ok", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + if getVolumeCallCount == 1 { + volumeStatus = "available" + } else { + volumeStatus = "in-use" + attachments = []volumes.Attachment{ + { + ServerID: "server-id-ok", + Device: "/dev/sdb", + }, + } + } + responsePayload := map[string]interface{}{ + "id": "volume-id-ok", + "volume": map[string]interface{}{ + "id": "volume-id-ok", + "status": volumeStatus, + "attachments": attachments, + }, + } + response, _ := json.Marshal(responsePayload) + fmt.Fprint(w, string(response)) + getVolumeCallCount++ + default: + w.WriteHeader(http.StatusNotImplemented) + } + }) + Mux.HandleFunc("/v2.1/servers/server-id-ok", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + payload := `{ + "server": { + "id": "server-id-ok", + "status": "ACTIVE" + } + }` + fmt.Fprint(w, payload) + default: + w.WriteHeader(http.StatusNotImplemented) + } + }) + Mux.HandleFunc("/v2.1/servers/server-id-ok/os-volume_attachments", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodPost: + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + payload := `{ + "volumeAttachment": { + "device": "/dev/sdb", + "id": "attachment-id", + "serverId": "server-id-ok", + "volumeId": "volume-id-ok" + } + }` + fmt.Fprint(w, payload) + case http.MethodGet: + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + payload := `{ "volumeAttachments": [ { + "device": "/dev/sdb", + "id": "attachment-id", + "serverId": "server-id-ok", + "volumeId": "volume-id-ok" + }]}` + fmt.Fprint(w, payload) + //log.Printf("Mux: Payload: %s\n", payload) + default: + w.WriteHeader(http.StatusNotImplemented) + } + }) + }) + + AfterEach(func() { + TeardownHTTP() + }) + + Context("success case: AttachDisk", func() { + + It("attaches a new volume V1", func() { + writeJsonParamToStdIn(`{ + "method":"attach_disk", + "arguments": [ + "server-id-ok", + "volume-id-ok" + ], + "context": {}, + "api_version": 1 + }`) + + err := cpi.Execute(getDefaultConfig(Endpoint()), logger) + Expect(err).ShouldNot(HaveOccurred()) + stdOutWriter.Close() + actual := <-outChannel + Expect(actual).To(ContainSubstring(`"result":null,"error":null`)) + }) + + It("attaches a new volume V2", func() { + writeJsonParamToStdIn(`{ + "method":"attach_disk", + "arguments": [ + "server-id-ok", + "volume-id-ok" + ], + "context": {}, + "api_version": 2 + }`) + err := cpi.Execute(getDefaultConfig(Endpoint()), logger) + Expect(err).ShouldNot(HaveOccurred()) + stdOutWriter.Close() + actual := <-outChannel + Expect(actual).To(ContainSubstring(`{"result":"/dev/sdb","error":null,"log":""}`)) + }) + }) +}) diff --git a/src/openstack_cpi_golang/integration/calculate_vm_cloud_properties_test.go b/src/openstack_cpi_golang/integration/calculate_vm_cloud_properties_test.go new file mode 100644 index 00000000..696da229 --- /dev/null +++ b/src/openstack_cpi_golang/integration/calculate_vm_cloud_properties_test.go @@ -0,0 +1,116 @@ +package integration_test + +import ( + "fmt" + "net/http" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Calculate VM cloud properties", func() { + + BeforeEach(func() { + SetupHTTP() + + MockAuthentication() + + Mux.HandleFunc("/v2.1/flavors/detail", func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, `{ + "flavors": [ + { + "disk": 1, + "id": "1", + "vcpus": 1, + "name": "m1.tiny", + "ram": 512 + }, + { + "disk": 64, + "id": "138", + "vcpus": 2, + "name": "m_c2_m16", + "ram": 16368 + }, + { + "disk": 64, + "id": "50", + "vcpus": 4, + "name": "g_c4_m16", + "ram": 16368 + } + ] + }`) + }) + }) + + AfterEach(func() { + TeardownHTTP() + }) + + It("calculate vm cloud properties without bootFromVolume", func() { + writeJsonParamToStdIn(`{ + "method": "calculate_vm_cloud_properties", + "arguments": [ + { + "cpu": 1, + "ram": 8192, + "ephemeral_disk_size": 4096 + } + ], + "api_version": 2 + }`) + + err := cpi.Execute(getDefaultConfig(Endpoint()), logger) + Expect(err).ShouldNot(HaveOccurred()) + + stdOutWriter.Close() + actual := <-outChannel + Expect(actual).To(ContainSubstring(`"result":{"instance_type":"m_c2_m16"},"error":null`)) + }) + + It("calculate vm cloud properties with bootFromVolume", func() { + writeJsonParamToStdIn(`{ + "method": "calculate_vm_cloud_properties", + "arguments": [ + { + "cpu": 1, + "ram": 8192, + "ephemeral_disk_size": 4194304 + } + ], + "api_version": 2 + }`) + + err := cpi.Execute(getBootFromVolumeConfig(Endpoint()), logger) + Expect(err).ShouldNot(HaveOccurred()) + + stdOutWriter.Close() + actual := <-outChannel + Expect(actual).To(ContainSubstring(`"result":{"instance_type":"m_c2_m16","root_disk":{"size":"4099.0"}},"error":null`)) + }) + + It("fails if no flavor can fulfill the requirement", func() { + writeJsonParamToStdIn(`{ + "method": "calculate_vm_cloud_properties", + "arguments": [ + { + "cpu": 10, + "ram": 8192, + "ephemeral_disk_size": 4096 + } + ], + "api_version": 2 + }`) + + err := cpi.Execute(getDefaultConfig(Endpoint()), logger) + Expect(err).ShouldNot(HaveOccurred()) + + stdOutWriter.Close() + Expect(<-outChannel).To(ContainSubstring("Unable to meet requested VM requirements: 10 CPU, 8192 MB RAM, 4 GB Disk.")) + }) +}) diff --git a/src/openstack_cpi_golang/integration/create_disk_test.go b/src/openstack_cpi_golang/integration/create_disk_test.go new file mode 100644 index 00000000..cd6581ee --- /dev/null +++ b/src/openstack_cpi_golang/integration/create_disk_test.go @@ -0,0 +1,119 @@ +package integration_test + +import ( + "fmt" + "net/http" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Create Disk", func() { + callCount := 0 + + BeforeEach(func() { + SetupHTTP() + + MockAuthentication() + + Mux.HandleFunc("/v3/volumes", func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "application/json") + + if r.Method == "POST" { + w.WriteHeader(http.StatusAccepted) + + fmt.Fprintf(w, `{ + "volume": { + "id": "2b955850-f177-45f7-9f49-ecb2c256d161" + } + }`) + } + }) + + Mux.HandleFunc("/v2.1/servers/server-id", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, `{ + "server": { + "id": "server-id", + "OS-EXT-AZ:availability_zone": "us-west" + } + }`) + } + }) + + Mux.HandleFunc("/v3/volumes/2b955850-f177-45f7-9f49-ecb2c256d161", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + callCount++ + if callCount == 1 { + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, `{ + "volume": { + "id": "2b955850-f177-45f7-9f49-ecb2c256d161", + "status": "available" + } + }`) + } else { + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, `{ + "volume": { + "id": "2b955850-f177-45f7-9f49-ecb2c256d161", + "status": "error" + } + }`) + } + } + }) + }) + + AfterEach(func() { + TeardownHTTP() + }) + + It("creates a volume", func() { + writeJsonParamToStdIn(`{ + "method": "create_disk", + "arguments": [ + 4096, + { + "type": "vmware" + }, + "server-id" + ], + "api_version": 2 + }`) + + err := cpi.Execute(getDefaultConfig(Endpoint()), logger) + Expect(err).ShouldNot(HaveOccurred()) + + stdOutWriter.Close() + actual := <-outChannel + Expect(actual).To(ContainSubstring(`"result":"2b955850-f177-45f7-9f49-ecb2c256d161","error":null`)) + }) + + Context("when the volume creation fails", func() { + It("fails when creating a volume", func() { + writeJsonParamToStdIn(`{ + "method": "create_disk", + "arguments": [ + 4096, + { + "type": "vmware" + }, + "server-id" + ], + "api_version": 2 + }`) + + err := cpi.Execute(getDefaultConfig(Endpoint()), logger) + Expect(err).ShouldNot(HaveOccurred()) + + stdOutWriter.Close() + actual := <-outChannel + Expect(actual).To(ContainSubstring(`create disk: volume became error state while waiting to become available`)) + }) + }) +}) diff --git a/src/openstack_cpi_golang/integration/create_stemcell_test.go b/src/openstack_cpi_golang/integration/create_stemcell_test.go new file mode 100644 index 00000000..f1394cf8 --- /dev/null +++ b/src/openstack_cpi_golang/integration/create_stemcell_test.go @@ -0,0 +1,147 @@ +package integration_test + +import ( + "fmt" + "net/http" + "sync/atomic" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/config" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Create Stemcell", func() { + var count int64 + + BeforeEach(func() { + SetupHTTP() + + MockAuthentication() + }) + + AfterEach(func() { + TeardownHTTP() + }) + + It("create a heavy stemcell image", func() { + Mux.HandleFunc("/v2/images", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, `{ + "status": "queued", + "visibility": "private", + "id": "b2173dd3-7ad6-4362-baa6-a68bce3565cb", + "file": "/v2/images/b2173dd3-7ad6-4362-baa6-a68bce3565cb/file", + "schema": "/v2/schemas/image" + }`) + }) + + Mux.HandleFunc("/v2/images/b2173dd3-7ad6-4362-baa6-a68bce3565cb/file", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNoContent) + }) + + writeJsonParamToStdIn(`{ + "method":"create_stemcell", + "arguments":[ + "./testdata/image", + { + "disk":5120,"disk_format": + "vmdk","container_format":"bare", + "architecture":"x86_64", + "vmware_ostype":"ubuntu64Guest" + } + ] + }`) + + err := cpi.Execute(getDefaultConfig(Endpoint()), logger) + Expect(err).ShouldNot(HaveOccurred()) + + stdOutWriter.Close() + Expect(<-outChannel).To(ContainSubstring(`"result":"b2173dd3-7ad6-4362-baa6-a68bce3565cb"`)) + }) + + It("create a light stemcell image", func() { + Mux.HandleFunc("/v2/images/b2173dd3-7ad6-4362-baa6-a68bce3565cb", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, `{ + "status": "active", + "visibility": "private", + "id": "b2173dd3-7ad6-4362-baa6-a68bce3565cb", + "file": "/v2/images/b2173dd3-7ad6-4362-baa6-a68bce3565cb/file", + "schema": "/v2/schemas/image" + }`) + }) + + writeJsonParamToStdIn(`{ + "method":"create_stemcell", + "arguments":[ + "./testdata/image", + { + "image_id":"b2173dd3-7ad6-4362-baa6-a68bce3565cb", + "disk":5120,"disk_format": + "vmdk","container_format":"bare", + "architecture":"x86_64", + "vmware_ostype":"ubuntu64Guest" + } + ] + }`) + + err := cpi.Execute(getDefaultConfig(Endpoint()), logger) + Expect(err).ShouldNot(HaveOccurred()) + + stdOutWriter.Close() + Expect(<-outChannel).To(ContainSubstring(`"result":"b2173dd3-7ad6-4362-baa6-a68bce3565cb"`)) + }) + + It("retries the light stemcell creation", func() { + Mux.HandleFunc("/v2/images/b2173dd3-7ad6-4362-baa6-a68bce3565cb", func(w http.ResponseWriter, r *http.Request) { + + if atomic.LoadInt64(&count) == 0 { + // fail on first request + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprintf(w, `{}`) + + atomic.AddInt64(&count, 1) + } else { + // succeed on second request + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, `{ + "status": "active", + "visibility": "private", + "id": "b2173dd3-7ad6-4362-baa6-a68bce3565cb", + "file": "/v2/images/b2173dd3-7ad6-4362-baa6-a68bce3565cb/file", + "schema": "/v2/schemas/image" + }`) + } + }) + + writeJsonParamToStdIn(`{ + "method":"create_stemcell", + "arguments":[ + "./testdata/image", + { + "image_id":"b2173dd3-7ad6-4362-baa6-a68bce3565cb", + "disk":5120,"disk_format": + "vmdk","container_format":"bare", + "architecture":"x86_64", + "vmware_ostype":"ubuntu64Guest" + } + ] + }`) + + cpiConfig := getDefaultConfig(Endpoint()) + cpiConfig.Cloud.Properties.RetryConfig = config.RetryConfigMap{ + "default": { + MaxAttempts: 10, + SleepDuration: 0, + }, + } + err := cpi.Execute(cpiConfig, logger) + Expect(err).ShouldNot(HaveOccurred()) + + stdOutWriter.Close() + Expect(<-outChannel).To(ContainSubstring(`"result":"b2173dd3-7ad6-4362-baa6-a68bce3565cb"`)) + }) +}) diff --git a/src/openstack_cpi_golang/integration/create_vm_test.go b/src/openstack_cpi_golang/integration/create_vm_test.go new file mode 100644 index 00000000..e067a3c0 --- /dev/null +++ b/src/openstack_cpi_golang/integration/create_vm_test.go @@ -0,0 +1,1910 @@ +package integration_test + +import ( + "fmt" + "net/http" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/compute" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/config" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/loadbalancer" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Create VM", func() { + + BeforeEach(func() { + SetupHTTP() + + MockAuthentication() + + loadbalancer.LoadbalancerServicePollingInterval = 0 + compute.ComputeServicePollingInterval = 0 + + Mux.HandleFunc("/v2/images/5bba0da5-dfb3-49d8-a005-d799507518f7", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, `{ + "status": "active", + "visibility": "private", + "id": "b2173dd3-7ad6-4362-baa6-a68bce3565cb", + "file": "/v2/images/b2173dd3-7ad6-4362-baa6-a68bce3565cb/file", + "schema": "/v2/schemas/image" + }`) + } + }) + + Mux.HandleFunc("/v2.0/security-groups/0c8a5d1a-8922-4d65-a0b2-dd78ab869e04", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, `{ + "security_group": { + "id": "85cc3048-abc3-43cc-89b3-377341426ac5" + } + }`) + } + }) + + Mux.HandleFunc("/v2.0/security-groups/bosh_acceptance_tests", func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusNotFound) + }) + + Mux.HandleFunc("/v2.0/security-groups", func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, `{ + "security_groups": [ + { + "id": "191e4194-1b33-4886-8b2e-4a4e5de3f9ff", + "name": "bosh_acceptance_tests" + } + ] + }`) + }) + + Mux.HandleFunc("/v2.1/os-keypairs/default_key_name", func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, `{ + "keypair": { + "name": "default_key_name", + "id": 1 + } + }`) + }) + + Mux.HandleFunc("/v2.0/subnets", func(w http.ResponseWriter, r *http.Request) { + if r.URL.Query().Get("network_id") != "fbe64fb7-b47c-4fd1-b158-9411d5c3ebf3" { + w.WriteHeader(http.StatusNotFound) + return + } + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, `{ + "subnets": [ + { + "cidr": "10.0.11.0/24", + "id": "08eae331-0402-425a-923c-34f7cfe39c1b" + } + ] + }`) + + }) + }) + + AfterEach(func() { + TeardownHTTP() + }) + + Context("when a vm is not created", func() { + Context("when getting an image", func() { + It("fails if an image does not exist", func() { + Mux.HandleFunc("/v2/images/not-existing-image-id", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + }) + + writeJsonParamToStdIn(`{ + "method": "create_vm", + "arguments": [ + "a694d798-0b41-4255-9c8e-b282cd504a52", "not-existing-image-id", + {}, {}, [], {} + ], + "api_version": 2 + }`) + + err := cpi.Execute(getDefaultConfig(Endpoint()), logger) + Expect(err).ShouldNot(HaveOccurred()) + + stdOutWriter.Close() + Expect(<-outChannel).To(ContainSubstring("could not find the image 'not-existing-image-id' in OpenStack")) + }) + + It("fails if an image is not active", func() { + Mux.HandleFunc("/v2/images/not-active-image-id", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, `{ + "status": "deleted" + }`) + }) + + writeJsonParamToStdIn(`{ + "method": "create_vm", + "arguments": [ + "a694d798-0b41-4255-9c8e-b282cd504a52", "not-active-image-id", + {}, {}, [], {} + ], + "api_version": 2 + }`) + + err := cpi.Execute(getDefaultConfig(Endpoint()), logger) + Expect(err).ShouldNot(HaveOccurred()) + + stdOutWriter.Close() + Expect(<-outChannel).To(ContainSubstring("image 'not-active-image-id' is not in active state, it is in state: deleted")) + }) + }) + + Context("when getting network configuration", func() { + It("fails if a manual network has no net_id", func() { + writeJsonParamToStdIn(`{ + "method": "create_vm", + "arguments": [ + "a694d798-0b41-4255-9c8e-b282cd504a52", "5bba0da5-dfb3-49d8-a005-d799507518f7", {}, + { + "bosh": { + "type": "manual", + "cloud_properties": { + "availability_zone":"z1" + } + } + }, + [], {} + ], + "api_version": 2 + }`) + + err := cpi.Execute(getDefaultConfig(Endpoint()), logger) + Expect(err).ShouldNot(HaveOccurred()) + + stdOutWriter.Close() + Expect(<-outChannel).To(ContainSubstring("failed to create network config: invalid manual network configuration: manual network must have a net_id")) + }) + + It("fails if multiple manual networks is used with 'openstack.use_dhcp=true'", func() { + writeJsonParamToStdIn(`{ + "method": "create_vm", + "arguments": [ + "a694d798-0b41-4255-9c8e-b282cd504a52", "5bba0da5-dfb3-49d8-a005-d799507518f7", {}, + { + "bosh": { + "type": "manual", + "cloud_properties": { + "net_id": "fbe64fb7-b47c-4fd1-b158-9411d5c3ebf3" + } + }, + "another_network": { + "type": "manual", + "cloud_properties": { + "net_id": "fbe64fb7-b47c-4fd1-b158-9411d5c3ebf4" + } + } + }, + [], {} + ], + "api_version": 2 + }`) + + defaultConfig = getDefaultConfig(Endpoint()) + defaultConfig.Cloud.Properties.Openstack.UseDHCP = true + err := cpi.Execute(defaultConfig, logger) + Expect(err).ShouldNot(HaveOccurred()) + + stdOutWriter.Close() + Expect(<-outChannel).To(ContainSubstring("multiple manual networks can only be used with")) + }) + + It("fails if multiple manual networks is used with an non-empty ConfigDrive", func() { + writeJsonParamToStdIn(`{ + "method": "create_vm", + "arguments": [ + "a694d798-0b41-4255-9c8e-b282cd504a52", "5bba0da5-dfb3-49d8-a005-d799507518f7", {}, + { + "bosh": { + "type": "manual", + "cloud_properties": { + "net_id": "fbe64fb7-b47c-4fd1-b158-9411d5c3ebf3" + } + }, + "another_network": { + "type": "manual", + "cloud_properties": { + "net_id": "fbe64fb7-b47c-4fd1-b158-9411d5c3ebf4" + } + } + }, + [], {} + ], + "api_version": 2 + }`) + + defaultConfig = getDefaultConfig(Endpoint()) + defaultConfig.Cloud.Properties.Openstack.ConfigDrive = "cdrom" + err := cpi.Execute(defaultConfig, logger) + Expect(err).ShouldNot(HaveOccurred()) + + stdOutWriter.Close() + Expect(<-outChannel).To(ContainSubstring("multiple manual networks can only be used with")) + }) + + It("fails when configuring multiple dynamic networks", func() { + writeJsonParamToStdIn(`{ + "method": "create_vm", + "arguments": [ + "a694d798-0b41-4255-9c8e-b282cd504a52", "5bba0da5-dfb3-49d8-a005-d799507518f7", {}, + { + "bosh": { + "type": "dynamic", + "cloud_properties": { + "net_id": "fbe64fb7-b47c-4fd1-b158-9411d5c3ebf3" + } + }, + "another_network": { + "type": "dynamic", + "cloud_properties": { + "net_id": "fbe64fb7-b47c-4fd1-b158-9411d5c3ebf4" + } + } + }, + [], {} + ], + "api_version": 2 + }`) + + defaultConfig = getDefaultConfig(Endpoint()) + defaultConfig.Cloud.Properties.Openstack.ConfigDrive = "cdrom" + err := cpi.Execute(defaultConfig, logger) + Expect(err).ShouldNot(HaveOccurred()) + + stdOutWriter.Close() + Expect(<-outChannel).To(ContainSubstring("only one dynamic should be defined per instance")) + }) + + It("fails when configuring multiple vip networks", func() { + writeJsonParamToStdIn(`{ + "method": "create_vm", + "arguments": [ + "a694d798-0b41-4255-9c8e-b282cd504a52", "5bba0da5-dfb3-49d8-a005-d799507518f7", {}, + { + "bosh": { + "type": "vip", + "cloud_properties": { + "net_id": "fbe64fb7-b47c-4fd1-b158-9411d5c3ebf3" + } + }, + "another_network": { + "type": "vip", + "cloud_properties": { + "net_id": "fbe64fb7-b47c-4fd1-b158-9411d5c3ebf4" + } + } + }, + [], {} + ], + "api_version": 2 + }`) + + defaultConfig = getDefaultConfig(Endpoint()) + defaultConfig.Cloud.Properties.Openstack.ConfigDrive = "cdrom" + err := cpi.Execute(defaultConfig, logger) + Expect(err).ShouldNot(HaveOccurred()) + + stdOutWriter.Close() + Expect(<-outChannel).To(ContainSubstring("only one vip should be defined per instance")) + }) + + It("fails when a network has no net_id", func() { + writeJsonParamToStdIn(`{ + "method": "create_vm", + "arguments": [ + "a694d798-0b41-4255-9c8e-b282cd504a52", "5bba0da5-dfb3-49d8-a005-d799507518f7", {}, + { + "bosh": { + "type": "manual", + "cloud_properties": { + "availability_zone":"z1" + } + } + }, + [], {} + ], + "api_version": 2 + }`) + + err := cpi.Execute(getDefaultConfig(Endpoint()), logger) + Expect(err).ShouldNot(HaveOccurred()) + + stdOutWriter.Close() + Expect(<-outChannel).To(ContainSubstring("failed to create network config: invalid manual network configuration: manual network must have a net_id")) + }) + + It("fails if a security rule cannot be resolved", func() { + writeJsonParamToStdIn(`{ + "method": "create_vm", + "arguments": [ + "a694d798-0b41-4255-9c8e-b282cd504a52", "5bba0da5-dfb3-49d8-a005-d799507518f7", {}, + { + "bosh": { + "type": "manual", + "ip": "10.0.11.16", + "netmask": "255.255.255.0", + "cloud_properties": { + "availability_zone":"z1", + "net_id": "fbe64fb7-b47c-4fd1-b158-9411d5c3ebf3", + "security_groups": [ + "not-exiting-group" + ] + }, + "default": [ + "dns", + "gateway" + ], + "gateway": "10.0.11.1" + } + }, + [], {} + ], + "api_version": 2 + }`) + + err := cpi.Execute(getDefaultConfig(Endpoint()), logger) + Expect(err).ShouldNot(HaveOccurred()) + + stdOutWriter.Close() + Expect(<-outChannel).To(ContainSubstring("failed to resolve security group: could not resolve security group 'not-exiting-group'")) + }) + }) + + Context("when creating ports for manual networks", func() { + It("handles conflicting ports and fails if port can not be created", func() { + Mux.HandleFunc("/v2.0/ports", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, `{ + "port": { + "id": "65c0ee9f-d634-4522-8954-51021b570b0d", + "status": "DOWN" + } + }`) + + case http.MethodPost: + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusForbidden) + + fmt.Fprintf(w, `{}`) + } + }) + + Mux.HandleFunc("/v2.0/ports/65c0ee9f-d634-4522-8954-51021b570b0d", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodDelete: + w.WriteHeader(http.StatusNoContent) + + fmt.Fprintf(w, `{}`) + } + }) + + writeJsonParamToStdIn(`{ + "method": "create_vm", + "arguments": [ + "a694d798-0b41-4255-9c8e-b282cd504a52", + "5bba0da5-dfb3-49d8-a005-d799507518f7", + { + "instance_type": "m1.tiny", + "key_name": "default_key_name", + "availability_zones": ["z1"] + }, + { + "bosh": { + "type": "manual", + "ip": "10.0.11.16", + "netmask": "255.255.255.0", + "cloud_properties": { + "availability_zone": "z1", + "net_id": "fbe64fb7-b47c-4fd1-b158-9411d5c3ebf3", + "security_groups": [ + "0c8a5d1a-8922-4d65-a0b2-dd78ab869e04", + "bosh_acceptance_tests" + ] + }, + "default": [ + "dns", + "gateway" + ], + "gateway": "10.0.11.1" + } + }, + [], + {} + ], + "api_version": 2 + }`) + + err := cpi.Execute(getDefaultConfig(Endpoint()), logger) + Expect(err).ShouldNot(HaveOccurred()) + + stdOutWriter.Close() + Expect(<-outChannel).To(ContainSubstring("failed to recreate port on network 'fbe64fb7-b47c-4fd1-b158-9411d5c3ebf3' for ip '10.0.11.16' Request forbidden")) + }) + + It("fails if allowed_address_pairs are not empty and vrrp_port_check is set to true but no port exists", func() { + Mux.HandleFunc("/v2.0/ports", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, `{"ports": []}`) + + case http.MethodPost: + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, `{ + "port": { + "id": "65c0ee9f-d634-4522-8954-51021b570b0d", + "status": "ACTIVE" + } + }`) + } + }) + + writeJsonParamToStdIn(`{ + "method": "create_vm", + "arguments": [ + "a694d798-0b41-4255-9c8e-b282cd504a52", + "5bba0da5-dfb3-49d8-a005-d799507518f7", + { + "instance_type": "m1.tiny", + "key_name": "default_key_name", + "availability_zones": ["z1"], + "allowed_address_pairs": "198.51.100.221", + "vrrp_port_check": true + }, + { + "bosh": { + "type": "manual", + "ip": "10.0.11.16", + "netmask": "255.255.255.0", + "cloud_properties": { + "availability_zone": "z1", + "net_id": "fbe64fb7-b47c-4fd1-b158-9411d5c3ebf3", + "security_groups": [ + "0c8a5d1a-8922-4d65-a0b2-dd78ab869e04", + "bosh_acceptance_tests" + ] + }, + "default": [ + "dns", + "gateway" + ], + "gateway": "10.0.11.1" + } + }, + [], + {} + ], + "api_version": 2 + }`) + + err := cpi.Execute(getDefaultConfig(Endpoint()), logger) + Expect(err).ShouldNot(HaveOccurred()) + + stdOutWriter.Close() + Expect(<-outChannel).To(ContainSubstring("failed create network opts: configured VRRP port with ip '198.51.100.221' does not exist")) + }) + }) + + Context("when creating a server", func() { + BeforeEach(func() { + Mux.HandleFunc("/v2.1/servers", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + w.WriteHeader(http.StatusAccepted) + + fmt.Fprintf(w, `{ + "server": { + "id": "f5dc173b-6804-445a-a6d8-c705dad5b5eb", + "status": "ACTIVE" + } + }`) + case http.MethodPost: + w.WriteHeader(http.StatusBadRequest) + } + }) + + Mux.HandleFunc("/v2.0/ports", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, `{"ports": []}`) + + case http.MethodPost: + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, `{ + "port": { + "id": "65c0ee9f-d634-4522-8954-51021b570b0d", + "status": "ACTIVE" + } + }`) + } + }) + }) + + Context("when flavor disk is not zero", func() { + BeforeEach(func() { + Mux.HandleFunc("/v2.1/flavors/detail", func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, `{ + "flavors": [ + { + "disk": 1, + "id": "1", + "name": "m1.tiny", + "ram": 512 + } + ] + }`) + }) + }) + + It("fails if a wrong flavorName is given", func() { + writeJsonParamToStdIn(`{ + "method": "create_vm", + "arguments": [ + "a694d798-0b41-4255-9c8e-b282cd504a52", + "5bba0da5-dfb3-49d8-a005-d799507518f7", + { + "instance_type": "wrong_flavor", + "availability_zones": ["z1"] + }, + { + "bosh": { + "type": "manual", + "ip": "10.0.11.16", + "netmask": "255.255.255.0", + "cloud_properties": { + "availability_zone": "z1", + "net_id": "fbe64fb7-b47c-4fd1-b158-9411d5c3ebf3", + "security_groups": [ + "0c8a5d1a-8922-4d65-a0b2-dd78ab869e04" + ] + }, + "default": [ + "dns", + "gateway" + ], + "gateway": "10.0.11.1" + } + }, + [], + {} + ], + "api_version": 2 + }`) + + err := cpi.Execute(getDefaultConfig(Endpoint()), logger) + Expect(err).ShouldNot(HaveOccurred()) + + stdOutWriter.Close() + Expect(<-outChannel).To(ContainSubstring("failed to resolve flavor of instance type 'wrong_flavor': flavor for instance type 'wrong_flavor' not found")) + }) + + It("fails if a key pair name cannot be resolved", func() { + Mux.HandleFunc("/v2.1/os-keypairs/unknown_key_name", func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusNotFound) + }) + + writeJsonParamToStdIn(`{ + "method": "create_vm", + "arguments": [ + "a694d798-0b41-4255-9c8e-b282cd504a52", + "5bba0da5-dfb3-49d8-a005-d799507518f7", + { + "instance_type": "m1.tiny", + "availability_zones": ["z1"] + }, + { + "bosh": { + "type": "manual", + "ip": "10.0.11.16", + "netmask": "255.255.255.0", + "cloud_properties": { + "availability_zone":"z1", + "net_id": "fbe64fb7-b47c-4fd1-b158-9411d5c3ebf3", + "security_groups": [ + "0c8a5d1a-8922-4d65-a0b2-dd78ab869e04" + ] + }, + "default": [ + "dns", + "gateway" + ], + "gateway": "10.0.11.1" + } + }, + [], + {} + ], + "api_version": 2 + }`) + + defaultConfig = getDefaultConfig(Endpoint()) + defaultConfig.Cloud.Properties.Openstack.DefaultKeyName = "unknown_key_name" + + err := cpi.Execute(defaultConfig, logger) + Expect(err).ShouldNot(HaveOccurred()) + + stdOutWriter.Close() + Expect(<-outChannel).To(ContainSubstring("failed to resolve keypair: failed to retrieve 'unknown_key_name'")) + }) + + It("fails if create server raises an error in all availability zones", func() { + writeJsonParamToStdIn(`{ + "method": "create_vm", + "arguments": [ + "a694d798-0b41-4255-9c8e-b282cd504a52", + "5bba0da5-dfb3-49d8-a005-d799507518f7", + { + "instance_type": "m1.tiny", + "key_name": "default_key_name", + "availability_zones": ["z1", "z2"] + }, + { + "bosh": { + "type": "manual", + "ip": "10.0.11.16", + "netmask": "255.255.255.0", + "cloud_properties": { + "availability_zone": "z1", + "net_id": "fbe64fb7-b47c-4fd1-b158-9411d5c3ebf3", + "security_groups": [ + "0c8a5d1a-8922-4d65-a0b2-dd78ab869e04", + "bosh_acceptance_tests" + ] + }, + "default": [ + "dns", + "gateway" + ], + "gateway": "10.0.11.1" + } + }, + [], + {} + ], + "api_version": 2 + }`) + + defaultConfig = getDefaultConfig(Endpoint()) + defaultConfig.Cloud.Properties.Openstack.IgnoreServerAvailabilityZone = true + + err := cpi.Execute(defaultConfig, logger) + Expect(err).ShouldNot(HaveOccurred()) + + stdOutWriter.Close() + Expect(<-outChannel).To(ContainSubstring("failed to create server in availability zone")) + }) + }) + + Context("when flavor disk is zero", func() { + BeforeEach(func() { + Mux.HandleFunc("/v2.1/flavors/detail", func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, `{ + "flavors": [ + { + "disk": 0, + "id": "1", + "name": "m1.tiny", + "ram": 512 + } + ] + }`) + }) + }) + + It("fails if root disk size and flavor disk size are 0", func() { + + writeJsonParamToStdIn(`{ + "method": "create_vm", + "arguments": [ + "a694d798-0b41-4255-9c8e-b282cd504a52", + "5bba0da5-dfb3-49d8-a005-d799507518f7", + { + "instance_type": "m1.tiny", + "key_name": "default_key_name", + "availability_zones": ["z1"] + }, + { + "bosh": { + "type": "manual", + "ip": "10.0.11.16", + "netmask": "255.255.255.0", + "cloud_properties": { + "availability_zone": "z1", + "net_id": "fbe64fb7-b47c-4fd1-b158-9411d5c3ebf3", + "security_groups": [ + "0c8a5d1a-8922-4d65-a0b2-dd78ab869e04", + "bosh_acceptance_tests" + ] + }, + "default": [ + "dns", + "gateway" + ], + "gateway": "10.0.11.1" + } + }, + [], + {} + ], + "api_version": 2 + }`) + + defaultConfig.Cloud.Properties.Openstack = config.OpenstackConfig{ + AuthURL: Endpoint(), + Username: "admin", + APIKey: "admin", + DomainName: "domain", + Tenant: "tenant", + Region: "region", + DefaultKeyName: "unknown_key_name", + StemcellPubliclyVisible: true, + } + + err := cpi.Execute(defaultConfig, logger) + Expect(err).ShouldNot(HaveOccurred()) + + stdOutWriter.Close() + Expect(<-outChannel).To(ContainSubstring("flavor '1' has a root disk size of 0. Either pick a different flavor or define root_disk.size in your VM cloud_properties")) + }) + }) + }) + }) + + Context("when a vm is created", func() { + BeforeEach(func() { + Mux.HandleFunc("/v2.1/servers", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + w.WriteHeader(http.StatusAccepted) + + fmt.Fprintf(w, `{ + "server": { + "id": "f5dc173b-6804-445a-a6d8-c705dad5b5eb", + "status": "ACTIVE" + } + }`) + case http.MethodPost: + w.WriteHeader(http.StatusCreated) + fmt.Fprintf(w, `{ + "server": { + "id": "f5dc173b-6804-445a-a6d8-c705dad5b5eb" + } + }`) + } + }) + + Mux.HandleFunc("/v2.1/flavors/detail", func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, `{ + "flavors": [ + { + "disk": 1, + "id": "1", + "name": "m1.tiny", + "ram": 512 + } + ] + }`) + }) + + Mux.HandleFunc("/v2.0/ports", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, `{ + "ports": [ + { + "id": "65c0ee9f-d634-4522-8954-51021b570b0d", + "status": "ACTIVE" + } + ] + }`) + + case http.MethodPost: + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, `{ + "port": { + "id": "65c0ee9f-d634-4522-8954-51021b570b0d", + "status": "ACTIVE" + } + }`) + } + }) + }) + + Context("when in status ACTIVE", func() { + BeforeEach(func() { + Mux.HandleFunc("/v2.1/servers/f5dc173b-6804-445a-a6d8-c705dad5b5eb", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, `{ + "server": { + "id": "f5dc173b-6804-445a-a6d8-c705dad5b5eb", + "status": "ACTIVE" + } + }`) + } + }) + }) + + Context("when it succeeds", func() { + It("Creates a VM with the configured resources", func() { + writeJsonParamToStdIn(`{ + "method": "create_vm", + "arguments": [ + "a694d798-0b41-4255-9c8e-b282cd504a52", + "5bba0da5-dfb3-49d8-a005-d799507518f7", + { + "instance_type": "m1.tiny", + "key_name": "default_key_name", + "availability_zones": ["z1"] + }, + { + "bosh": { + "type": "manual", + "ip": "10.0.11.16", + "netmask": "255.255.255.0", + "cloud_properties": { + "availability_zone": "z1", + "net_id": "fbe64fb7-b47c-4fd1-b158-9411d5c3ebf3", + "security_groups": [ + "0c8a5d1a-8922-4d65-a0b2-dd78ab869e04", + "bosh_acceptance_tests" + ] + }, + "default": [ + "dns", + "gateway" + ], + "gateway": "10.0.11.1" + } + }, + [], + {} + ], + "api_version": 2 + }`) + + err := cpi.Execute(getDefaultConfig(Endpoint()), logger) + Expect(err).ShouldNot(HaveOccurred()) + + stdOutWriter.Close() + Expect(<-outChannel).To(ContainSubstring(`"result":["f5dc173b-6804-445a-a6d8-c705dad5b5eb",{"bosh":{"type":"manual","ip":"10.0.11.16","netmask":"255.255.255.0","gateway":"10.0.11.1","dns":null,"default":["dns","gateway"],"routes":null,"cloud_properties":{"availability_zone":"z1","net_id":"fbe64fb7-b47c-4fd1-b158-9411d5c3ebf3","security_groups":["0c8a5d1a-8922-4d65-a0b2-dd78ab869e04","bosh_acceptance_tests"]}}}],"error":null`)) + }) + }) + + Context("when validating Cloud Properties", func() { + It("succeeds if multiple availability zones are defined and setting ignore_server_availability_zone to true", func() { + writeJsonParamToStdIn(`{ + "method": "create_vm", + "arguments": [ + "a694d798-0b41-4255-9c8e-b282cd504a52", + "5bba0da5-dfb3-49d8-a005-d799507518f7", + { + "instance_type": "m1.tiny", + "key_name": "default_key_name", + "availability_zones": ["z1", "z2"] + }, + { + "bosh": { + "type": "manual", + "ip": "10.0.11.16", + "netmask": "255.255.255.0", + "cloud_properties": { + "availability_zone": "z1", + "net_id": "fbe64fb7-b47c-4fd1-b158-9411d5c3ebf3", + "security_groups": [ + "0c8a5d1a-8922-4d65-a0b2-dd78ab869e04", + "bosh_acceptance_tests" + ] + }, + "default": [ + "dns", + "gateway" + ], + "gateway": "10.0.11.1" + } + }, + [], + {} + ], + "api_version": 2 + }`) + + defaultConfig = getDefaultConfig(Endpoint()) + defaultConfig.Cloud.Properties.Openstack.IgnoreServerAvailabilityZone = true + err := cpi.Execute(defaultConfig, logger) + Expect(err).ShouldNot(HaveOccurred()) + + stdOutWriter.Close() + Expect(<-outChannel).To(ContainSubstring(`"result":["f5dc173b-6804-445a-a6d8-c705dad5b5eb",{"bosh":{"type":"manual","ip":"10.0.11.16","netmask":"255.255.255.0","gateway":"10.0.11.1","dns":null,"default":["dns","gateway"],"routes":null,"cloud_properties":{"availability_zone":"z1","net_id":"fbe64fb7-b47c-4fd1-b158-9411d5c3ebf3","security_groups":["0c8a5d1a-8922-4d65-a0b2-dd78ab869e04","bosh_acceptance_tests"]}}}],"error":null`)) + }) + + It("fails if load balancer pool defined without a name", func() { + writeJsonParamToStdIn(`{ + "method": "create_vm", + "arguments": [ + "a694d798-0b41-4255-9c8e-b282cd504a52", "5bba0da5-dfb3-49d8-a005-d799507518f7", + { + "instance_type": "m1.tiny", + "availability_zones": ["z1"], + "loadbalancer_pools": [ { "name": "", "ProtocolPort": 80 } ] + }, + {}, [], {} + ], + "api_version": 2 + }`) + + err := cpi.Execute(getDefaultConfig(Endpoint()), logger) + Expect(err).ShouldNot(HaveOccurred()) + + stdOutWriter.Close() + Expect(<-outChannel).To(ContainSubstring("failed to validate cloud properties: load balancer pool defined without name")) + }) + + It("fails if load balancer pool defined without a valid ProtocolPort", func() { + writeJsonParamToStdIn(`{ + "method": "create_vm", + "arguments": [ + "a694d798-0b41-4255-9c8e-b282cd504a52", "5bba0da5-dfb3-49d8-a005-d799507518f7", + { + "instance_type": "m1.tiny", + "availability_zones": ["z1"], + "loadbalancer_pools": [ { "name": "myPool", "ProtocolPort": 0 } ] + }, + {}, [], {} + ], + "api_version": 2 + }`) + + err := cpi.Execute(getDefaultConfig(Endpoint()), logger) + Expect(err).ShouldNot(HaveOccurred()) + + stdOutWriter.Close() + Expect(<-outChannel).To(ContainSubstring("load balancer pool 'myPool' has no port definition")) + }) + + It("fails if multiple availability zone properties are defined", func() { + writeJsonParamToStdIn(`{ + "method": "create_vm", + "arguments": [ + "a694d798-0b41-4255-9c8e-b282cd504a52", "5bba0da5-dfb3-49d8-a005-d799507518f7", + { + "availability_zones": ["z1"], + "availability_zone": "z2" + }, + {}, [], {} + ], + "api_version": 2 + }`) + + err := cpi.Execute(getDefaultConfig(Endpoint()), logger) + Expect(err).ShouldNot(HaveOccurred()) + + stdOutWriter.Close() + Expect(<-outChannel).To(ContainSubstring("only one property of 'availability_zone' and 'availability_zones' can be configured")) + }) + + It("fails if multiple availability zones are defined without setting ignore_server_availability_zone to true", func() { + writeJsonParamToStdIn(`{ + "method": "create_vm", + "arguments": [ + "a694d798-0b41-4255-9c8e-b282cd504a52", "5bba0da5-dfb3-49d8-a005-d799507518f7", + { + "availability_zones": ["z1", "z2"] + }, + {}, [], {} + ], + "api_version": 2 + }`) + + err := cpi.Execute(getDefaultConfig(Endpoint()), logger) + Expect(err).ShouldNot(HaveOccurred()) + + stdOutWriter.Close() + Expect(<-outChannel).To(ContainSubstring("cannot use multiple azs without 'openstack.ignore_server_availability_zone' set to true")) + }) + }) + + Context("when configuring VIP Networks", func() { + It("configures a VIP Network", func() { + Mux.HandleFunc("/v2.0/floatingips", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + floatingip := r.URL.Query().Get("floating_ip_address") + + if floatingip == "10.0.11.16" { + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, `{ + "floatingips": [ + { + "id": "floating_ip_id_1" + } + ] + }`) + } + } + }) + + Mux.HandleFunc("/v2.0/floatingips/floating_ip_id_1", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodPut: + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, `{ + "floatingips": [ + { + "id": "floating_ip_id_1" + } + ] + }`) + } + }) + + writeJsonParamToStdIn(`{ + "method": "create_vm", + "arguments": [ + "a694d798-0b41-4255-9c8e-b282cd504a52", + "5bba0da5-dfb3-49d8-a005-d799507518f7", + { + "instance_type": "m1.tiny", + "key_name": "default_key_name", + "availability_zones": ["z1"] + }, + { + "bosh": { + "type": "vip", + "ip": "10.0.11.16", + "netmask": "255.255.255.0", + "cloud_properties": { + "availability_zone": "z1", + "net_id": "fbe64fb7-b47c-4fd1-b158-9411d5c3ebf3", + "security_groups": [ + "0c8a5d1a-8922-4d65-a0b2-dd78ab869e04", + "bosh_acceptance_tests" + ] + }, + "default": [ + "dns", + "gateway" + ], + "gateway": "10.0.11.1" + } + }, + [], + {} + ], + "api_version": 2 + }`) + + err := cpi.Execute(getDefaultConfig(Endpoint()), logger) + Expect(err).ShouldNot(HaveOccurred()) + + stdOutWriter.Close() + Expect(<-outChannel).To(ContainSubstring(`"result":["f5dc173b-6804-445a-a6d8-c705dad5b5eb",{"bosh":{"type":"vip","ip":"10.0.11.16","netmask":"255.255.255.0","gateway":"10.0.11.1","dns":null,"default":["dns","gateway"],"routes":null,"cloud_properties":{"availability_zone":"z1","net_id":"fbe64fb7-b47c-4fd1-b158-9411d5c3ebf3","security_groups":["0c8a5d1a-8922-4d65-a0b2-dd78ab869e04","bosh_acceptance_tests"]}}}],"error":null`)) + }) + + It("fails if a floating ip can not be associated", func() { + Mux.HandleFunc("/v2.0/floatingips", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + floatingip := r.URL.Query().Get("floating_ip_address") + + if floatingip == "10.0.11.16" { + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, `{ + "floatingips": [ + { + "id": "floating_ip_id_1" + } + ] + }`) + } + } + }) + + Mux.HandleFunc("/v2.0/floatingips/floating_ip_id_1", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodPut: + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + } + }) + + writeJsonParamToStdIn(`{ + "method": "create_vm", + "arguments": [ + "a694d798-0b41-4255-9c8e-b282cd504a52", + "5bba0da5-dfb3-49d8-a005-d799507518f7", + { + "instance_type": "m1.tiny", + "key_name": "default_key_name", + "availability_zones": ["z1"] + }, + { + "bosh": { + "type": "vip", + "ip": "10.0.11.16", + "netmask": "255.255.255.0", + "cloud_properties": { + "availability_zone": "z1", + "net_id": "fbe64fb7-b47c-4fd1-b158-9411d5c3ebf3", + "security_groups": [ + "0c8a5d1a-8922-4d65-a0b2-dd78ab869e04", + "bosh_acceptance_tests" + ] + }, + "default": [ + "dns", + "gateway" + ], + "gateway": "10.0.11.1" + } + }, + [], + {} + ], + "api_version": 2 + }`) + + err := cpi.Execute(getDefaultConfig(Endpoint()), logger) + Expect(err).ShouldNot(HaveOccurred()) + + stdOutWriter.Close() + Expect(<-outChannel).To(ContainSubstring(`failed to associate floating ip to port`)) + }) + + It("fails if no floating ips are allocated", func() { + Mux.HandleFunc("/v2.0/floatingips", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, `{ + "floatingips": [] + }`) + + } + }) + + writeJsonParamToStdIn(`{ + "method": "create_vm", + "arguments": [ + "a694d798-0b41-4255-9c8e-b282cd504a52", + "5bba0da5-dfb3-49d8-a005-d799507518f7", + { + "instance_type": "m1.tiny", + "key_name": "default_key_name", + "availability_zones": ["z1"] + }, + { + "bosh": { + "type": "vip", + "ip": "10.0.11.16", + "netmask": "255.255.255.0", + "cloud_properties": { + "availability_zone": "z1", + "net_id": "fbe64fb7-b47c-4fd1-b158-9411d5c3ebf3", + "security_groups": [ + "0c8a5d1a-8922-4d65-a0b2-dd78ab869e04", + "bosh_acceptance_tests" + ] + }, + "default": [ + "dns", + "gateway" + ], + "gateway": "10.0.11.1" + } + }, + [], + {} + ], + "api_version": 2 + }`) + + err := cpi.Execute(getDefaultConfig(Endpoint()), logger) + Expect(err).ShouldNot(HaveOccurred()) + + stdOutWriter.Close() + Expect(<-outChannel).To(ContainSubstring(`failed to get floating IP: floating IP 10.0.11.16 not allocated`)) + }) + }) + + Context("when configuring load balancer pools", func() { + It("create pool members for provided load balancer pools", func() { + Mux.HandleFunc("/v2.0/lbaas/pools", func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet || r.URL.Query().Get("name") != "myPool" { + w.WriteHeader(http.StatusNotFound) + return + } + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, `{ + "pools": [ + { + "id": "pool_id_1", + "name": "myPool", + "loadbalancers": [{"id": "the-lb-id"}] + } + ] + }`) + + }) + + Mux.HandleFunc("/v2.0/lbaas/loadbalancers/the-lb-id", func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + w.WriteHeader(http.StatusNotFound) + return + } + + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, `{ + "loadbalancer": { + "id": "the-lb-id", + "provisioning_status": "ACTIVE" + } + }`) + }) + + Mux.HandleFunc("/v2.0/lbaas/pools/pool_id_1/members", func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + w.WriteHeader(http.StatusNotFound) + return + } + + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, `{ + "member": { + "id": "member_id_1", + "name": "myPoolMember", + "provisioning_status": "PENDING_CREATE" + } + }`) + }) + + Mux.HandleFunc("/v2.1/servers/f5dc173b-6804-445a-a6d8-c705dad5b5eb/metadata", func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + w.WriteHeader(http.StatusNotFound) + return + } + + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, `{"meta": {}}`) + }) + + writeJsonParamToStdIn(`{ + "method": "create_vm", + "arguments": [ + "a694d798-0b41-4255-9c8e-b282cd504a52", + "5bba0da5-dfb3-49d8-a005-d799507518f7", + { + "instance_type": "m1.tiny", + "key_name": "default_key_name", + "availability_zones": ["z1"], + "loadbalancer_pools": [ + { "name": "myPool", "port": 80 } + ] + }, + { + "bosh": { + "type": "manual", + "ip": "10.0.11.16", + "netmask": "255.255.255.0", + "cloud_properties": { + "availability_zone": "z1", + "net_id": "fbe64fb7-b47c-4fd1-b158-9411d5c3ebf3", + "security_groups": [ + "0c8a5d1a-8922-4d65-a0b2-dd78ab869e04", + "bosh_acceptance_tests" + ] + }, + "default": [ + "dns", + "gateway" + ], + "gateway": "10.0.11.1" + } + }, + [], + {} + ], + "api_version": 2 + }`) + + err := cpi.Execute(getDefaultConfig(Endpoint()), logger) + Expect(err).ShouldNot(HaveOccurred()) + + stdOutWriter.Close() + Expect(<-outChannel).To(ContainSubstring(`"result":["f5dc173b-6804-445a-a6d8-c705dad5b5eb",{"bosh":{"type":"manual","ip":"10.0.11.16","netmask":"255.255.255.0","gateway":"10.0.11.1","dns":null,"default":["dns","gateway"],"routes":null,"cloud_properties":{"availability_zone":"z1","net_id":"fbe64fb7-b47c-4fd1-b158-9411d5c3ebf3","security_groups":["0c8a5d1a-8922-4d65-a0b2-dd78ab869e04","bosh_acceptance_tests"]}}}],"error":null`)) + }) + + It("fails if it can not retrieve load balancer pools", func() { + writeJsonParamToStdIn(`{ + "method": "create_vm", + "arguments": [ + "a694d798-0b41-4255-9c8e-b282cd504a52", + "5bba0da5-dfb3-49d8-a005-d799507518f7", + { + "instance_type": "m1.tiny", + "key_name": "default_key_name", + "availability_zones": ["z1"], + "loadbalancer_pools": [ + { "name": "myPool", "port": 80 } + ] + }, + { + "bosh": { + "type": "manual", + "ip": "10.0.11.16", + "netmask": "255.255.255.0", + "cloud_properties": { + "availability_zone": "z1", + "net_id": "fbe64fb7-b47c-4fd1-b158-9411d5c3ebf3", + "security_groups": [ + "0c8a5d1a-8922-4d65-a0b2-dd78ab869e04", + "bosh_acceptance_tests" + ] + }, + "default": [ + "dns", + "gateway" + ], + "gateway": "10.0.11.1" + } + }, + [], + {} + ], + "api_version": 2 + }`) + + err := cpi.Execute(getDefaultConfig(Endpoint()), logger) + Expect(err).ShouldNot(HaveOccurred()) + + stdOutWriter.Close() + Expect(<-outChannel).To(ContainSubstring(`failed to get pool ID of pool 'myPool': failed to list loadbalancer pools`)) + }) + + It("fails if it can not retrieve subnet ids", func() { + Mux.HandleFunc("/v2.0/lbaas/pools", func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet || r.URL.Query().Get("name") != "myPool" { + w.WriteHeader(http.StatusNotFound) + return + } + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, `{ + "pools": [ + { + "id": "pool_id_1", + "name": "myPool" + } + ] + }`) + + }) + + writeJsonParamToStdIn(`{ + "method": "create_vm", + "arguments": [ + "a694d798-0b41-4255-9c8e-b282cd504a52", + "5bba0da5-dfb3-49d8-a005-d799507518f7", + { + "instance_type": "m1.tiny", + "key_name": "default_key_name", + "availability_zones": ["z1"], + "loadbalancer_pools": [ + { "name": "myPool", "port": 80 } + ] + }, + { + "bosh": { + "type": "manual", + "ip": "10.0.11.16", + "netmask": "255.255.255.0", + "cloud_properties": { + "availability_zone": "z1", + "net_id": "wrong-subnet-id", + "security_groups": [ + "0c8a5d1a-8922-4d65-a0b2-dd78ab869e04", + "bosh_acceptance_tests" + ] + }, + "default": [ + "dns", + "gateway" + ], + "gateway": "10.0.11.1" + } + }, + [], + {} + ], + "api_version": 2 + }`) + + err := cpi.Execute(getDefaultConfig(Endpoint()), logger) + Expect(err).ShouldNot(HaveOccurred()) + + stdOutWriter.Close() + Expect(<-outChannel).To(ContainSubstring(`failed to create port: failed create network opts: failed to get subnet: failed to list subnets`)) + }) + + It("fails if it can not retrieve pool", func() { + Mux.HandleFunc("/v2.0/lbaas/pools", func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet || r.URL.Query().Get("name") != "myPool" { + w.WriteHeader(http.StatusNotFound) + return + } + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, `{ + "pools": [ + { + "id": "pool_id_1", + "name": "myPool" + } + ] + }`) + + }) + + writeJsonParamToStdIn(`{ + "method": "create_vm", + "arguments": [ + "a694d798-0b41-4255-9c8e-b282cd504a52", + "5bba0da5-dfb3-49d8-a005-d799507518f7", + { + "instance_type": "m1.tiny", + "key_name": "default_key_name", + "availability_zones": ["z1"], + "loadbalancer_pools": [ + { "name": "myPool", "port": 80 } + ] + }, + { + "bosh": { + "type": "manual", + "ip": "10.0.11.16", + "netmask": "255.255.255.0", + "cloud_properties": { + "availability_zone": "z1", + "net_id": "fbe64fb7-b47c-4fd1-b158-9411d5c3ebf3", + "security_groups": [ + "0c8a5d1a-8922-4d65-a0b2-dd78ab869e04", + "bosh_acceptance_tests" + ] + }, + "default": [ + "dns", + "gateway" + ], + "gateway": "10.0.11.1" + } + }, + [], + {} + ], + "api_version": 2 + }`) + + err := cpi.Execute(getDefaultConfig(Endpoint()), logger) + Expect(err).ShouldNot(HaveOccurred()) + + stdOutWriter.Close() + Expect(<-outChannel).To(ContainSubstring(`no load balancers or listeners associated with pool 'pool_id_1'`)) + }) + + It("times out if pool does not become ACTIVE", func() { + Mux.HandleFunc("/v2.0/lbaas/pools", func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet || r.URL.Query().Get("name") != "myPool" { + w.WriteHeader(http.StatusNotFound) + return + } + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, `{ + "pools": [ + { + "id": "pool_id_1", + "name": "myPool", + "loadbalancers": [{"id": "the-lb-id"}] + } + ] + }`) + + }) + + Mux.HandleFunc("/v2.0/lbaas/loadbalancers/the-lb-id", func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + w.WriteHeader(http.StatusNotFound) + return + } + + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, `{ + "loadbalancer": { + "id": "the-lb-id", + "provisioning_status": "PENDING_UPDATE" + } + }`) + }) + + writeJsonParamToStdIn(`{ + "method": "create_vm", + "arguments": [ + "a694d798-0b41-4255-9c8e-b282cd504a52", + "5bba0da5-dfb3-49d8-a005-d799507518f7", + { + "instance_type": "m1.tiny", + "key_name": "default_key_name", + "availability_zones": ["z1"], + "loadbalancer_pools": [ + { "name": "myPool", "port": 80 } + ] + }, + { + "bosh": { + "type": "manual", + "ip": "10.0.11.16", + "netmask": "255.255.255.0", + "cloud_properties": { + "availability_zone": "z1", + "net_id": "fbe64fb7-b47c-4fd1-b158-9411d5c3ebf3", + "security_groups": [ + "0c8a5d1a-8922-4d65-a0b2-dd78ab869e04", + "bosh_acceptance_tests" + ] + }, + "default": [ + "dns", + "gateway" + ], + "gateway": "10.0.11.1" + } + }, + [], + {} + ], + "api_version": 2 + }`) + + err := cpi.Execute(getDefaultConfig(Endpoint()), logger) + Expect(err).ShouldNot(HaveOccurred()) + + stdOutWriter.Close() + Expect(<-outChannel).To(ContainSubstring(`timeout while waiting for loadbalancer 'the-lb-id' to become active`)) + }) + + It("fails if it can not set server metadata", func() { + Mux.HandleFunc("/v2.0/lbaas/pools", func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet || r.URL.Query().Get("name") != "myPool" { + w.WriteHeader(http.StatusNotFound) + return + } + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, `{ + "pools": [ + { + "id": "pool_id_1", + "name": "myPool", + "loadbalancers": [{"id": "the-lb-id"}] + } + ] + }`) + + }) + + Mux.HandleFunc("/v2.0/lbaas/loadbalancers/the-lb-id", func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + w.WriteHeader(http.StatusNotFound) + return + } + + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, `{ + "loadbalancer": { + "id": "the-lb-id", + "provisioning_status": "ACTIVE" + } + }`) + }) + + Mux.HandleFunc("/v2.0/lbaas/pools/pool_id_1", func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + w.WriteHeader(http.StatusNotFound) + return + } + + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, `{ + "pool": { + "id": "pool_id_1", + "name": "myPool", + "provisioning_status": "ACTIVE" + } + }`) + }) + + Mux.HandleFunc("/v2.0/lbaas/pools/pool_id_1/members", func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + w.WriteHeader(http.StatusNotFound) + return + } + + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, `{ + "member": { + "id": "member_id_1", + "name": "myPoolMember", + "provisioning_status": "PENDING_CREATE" + } + }`) + }) + + Mux.HandleFunc("/v2.1/servers/f5dc173b-6804-445a-a6d8-c705dad5b5eb/metadata/lbaas_pool_1", func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPut { + w.WriteHeader(http.StatusNotFound) + return + } + + w.WriteHeader(http.StatusBadRequest) + + fmt.Fprintf(w, `{"meta": {}}`) + }) + + writeJsonParamToStdIn(`{ + "method": "create_vm", + "arguments": [ + "a694d798-0b41-4255-9c8e-b282cd504a52", + "5bba0da5-dfb3-49d8-a005-d799507518f7", + { + "instance_type": "m1.tiny", + "key_name": "default_key_name", + "availability_zones": ["z1"], + "loadbalancer_pools": [ + { "name": "myPool", "port": 80 } + ] + }, + { + "bosh": { + "type": "manual", + "ip": "10.0.11.16", + "netmask": "255.255.255.0", + "cloud_properties": { + "availability_zone": "z1", + "net_id": "fbe64fb7-b47c-4fd1-b158-9411d5c3ebf3", + "security_groups": [ + "0c8a5d1a-8922-4d65-a0b2-dd78ab869e04", + "bosh_acceptance_tests" + ] + }, + "default": [ + "dns", + "gateway" + ], + "gateway": "10.0.11.1" + } + }, + [], + {} + ], + "api_version": 2 + }`) + + err := cpi.Execute(getDefaultConfig(Endpoint()), logger) + Expect(err).ShouldNot(HaveOccurred()) + + stdOutWriter.Close() + Expect(<-outChannel).To(ContainSubstring(`failed to update metadata for server 'f5dc173b-6804-445a-a6d8-c705dad5b5eb' with error:`)) + }) + }) + + }) + + Context("when vm status stays BUILD", func() { + BeforeEach(func() { + Mux.HandleFunc("/v2.1/servers/f5dc173b-6804-445a-a6d8-c705dad5b5eb", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, `{ + "server": { + "id": "f5dc173b-6804-445a-a6d8-c705dad5b5eb", + "status": "BUILD" + } + }`) + } + }) + }) + + It("times out when status ACTIVE is not reached", func() { + writeJsonParamToStdIn(`{ + "method": "create_vm", + "arguments": [ + "a694d798-0b41-4255-9c8e-b282cd504a52", + "5bba0da5-dfb3-49d8-a005-d799507518f7", + { + "instance_type": "m1.tiny", + "key_name": "default_key_name", + "availability_zones": ["z1"] + }, + { + "bosh": { + "type": "manual", + "ip": "10.0.11.16", + "netmask": "255.255.255.0", + "cloud_properties": { + "availability_zone": "z1", + "net_id": "fbe64fb7-b47c-4fd1-b158-9411d5c3ebf3", + "security_groups": [ + "0c8a5d1a-8922-4d65-a0b2-dd78ab869e04", + "bosh_acceptance_tests" + ] + }, + "default": [ + "dns", + "gateway" + ], + "gateway": "10.0.11.1" + } + }, + [], + {} + ], + "api_version": 2 + }`) + + defaultConfig = getDefaultConfig(Endpoint()) + defaultConfig.Cloud.Properties.Openstack.StateTimeOut = 1 + + err := cpi.Execute(defaultConfig, logger) + Expect(err).ShouldNot(HaveOccurred()) + + stdOutWriter.Close() + Expect(<-outChannel).To(ContainSubstring(`timeout while waiting for server to become active`)) + }) + }) + + Context("when vm status goes to ERROR", func() { + BeforeEach(func() { + Mux.HandleFunc("/v2.1/servers/f5dc173b-6804-445a-a6d8-c705dad5b5eb", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, `{ + "server": { + "id": "f5dc173b-6804-445a-a6d8-c705dad5b5eb", + "status": "ERROR" + } + }`) + } + }) + }) + + It("fails if server status became ERROR", func() { + writeJsonParamToStdIn(`{ + "method": "create_vm", + "arguments": [ + "a694d798-0b41-4255-9c8e-b282cd504a52", + "5bba0da5-dfb3-49d8-a005-d799507518f7", + { + "instance_type": "m1.tiny", + "key_name": "default_key_name", + "availability_zones": ["z1"] + }, + { + "bosh": { + "type": "manual", + "ip": "10.0.11.16", + "netmask": "255.255.255.0", + "cloud_properties": { + "availability_zone": "z1", + "net_id": "fbe64fb7-b47c-4fd1-b158-9411d5c3ebf3", + "security_groups": [ + "0c8a5d1a-8922-4d65-a0b2-dd78ab869e04", + "bosh_acceptance_tests" + ] + }, + "default": [ + "dns", + "gateway" + ], + "gateway": "10.0.11.1" + } + }, + [], + {} + ], + "api_version": 2 + }`) + + defaultConfig = getDefaultConfig(Endpoint()) + defaultConfig.Cloud.Properties.Openstack.StateTimeOut = 1 + + err := cpi.Execute(defaultConfig, logger) + Expect(err).ShouldNot(HaveOccurred()) + + stdOutWriter.Close() + Expect(<-outChannel).To(ContainSubstring(`server became ERROR state while waiting to become ACTIVE`)) + }) + }) + }) + +}) diff --git a/src/openstack_cpi_golang/integration/delete_disk_test.go b/src/openstack_cpi_golang/integration/delete_disk_test.go new file mode 100644 index 00000000..4349f682 --- /dev/null +++ b/src/openstack_cpi_golang/integration/delete_disk_test.go @@ -0,0 +1,84 @@ +package integration_test + +import ( + "fmt" + "net/http" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Delete Disk", func() { + var callCount = 0 + + BeforeEach(func() { + SetupHTTP() + + MockAuthentication() + + Mux.HandleFunc("/v3/volumes/volume_id", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + callCount++ + if callCount == 1 { + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, `{ + "volume": { + "id": "volume_id", + "status": "available" + } + }`) + } else { + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, `{ + "volume": { + "id": "volume_id", + "status": "deleted" + } + }`) + } + case http.MethodDelete: + w.WriteHeader(http.StatusAccepted) + } + }) + }) + + AfterEach(func() { + TeardownHTTP() + }) + + It("deletes a volume", func() { + writeJsonParamToStdIn(`{ + "method": "delete_disk", + "arguments": [ + "volume_id" + ], + "api_version": 2 + }`) + + err := cpi.Execute(getDefaultConfig(Endpoint()), logger) + Expect(err).ShouldNot(HaveOccurred()) + + stdOutWriter.Close() + Expect(<-outChannel).To(ContainSubstring(`"result":null,"error":null`)) + }) + + Context("when the volume deletion fails", func() { + It("fails when deleting a volume", func() { + writeJsonParamToStdIn(`{ + "method": "delete_disk", + "arguments": [ + "volume_id" + ], + "api_version": 2 + }`) + + err := cpi.Execute(getDefaultConfig(Endpoint()), logger) + Expect(err).ShouldNot(HaveOccurred()) + + stdOutWriter.Close() + Expect(<-outChannel).To(ContainSubstring(`cannot delete volume volume_id, state is deleted`)) + }) + }) +}) diff --git a/src/openstack_cpi_golang/integration/delete_snapshot_test.go b/src/openstack_cpi_golang/integration/delete_snapshot_test.go new file mode 100644 index 00000000..3cfc20af --- /dev/null +++ b/src/openstack_cpi_golang/integration/delete_snapshot_test.go @@ -0,0 +1,111 @@ +package integration_test + +import ( + "fmt" + "net/http" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("DELETE SNAPSHOT", func() { + + BeforeEach(func() { + SetupHTTP() + MockAuthentication() + }) + + AfterEach(func() { + TeardownHTTP() + }) + + Context("Positive cases: ", func() { + + BeforeEach(func() { + Mux.HandleFunc("/v3/snapshots/snapshot-id", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodDelete: + w.WriteHeader(http.StatusAccepted) + case http.MethodGet: + w.WriteHeader(http.StatusNotFound) + default: + w.WriteHeader(http.StatusNotImplemented) + } + }) + }) + + It("deletes a snapshot successfully", func() { + writeJsonParamToStdIn(`{ + "method":"delete_snapshot", + "arguments": ["snapshot-id"] + }`) + config := getDefaultConfig(Endpoint()) + err := cpi.Execute(config, logger) + Expect(err).ShouldNot(HaveOccurred()) + + _ = stdOutWriter.Close() + Expect(<-outChannel).To(ContainSubstring(`"result":null,"error":null`)) + }) + }) + + Context("Failure in DeleteSnapshot: ", func() { + + BeforeEach(func() { + Mux.HandleFunc("/v3/snapshots/snapshot-id", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodDelete: + w.WriteHeader(http.StatusBadRequest) // Return 400 to simulate failure + default: + w.WriteHeader(http.StatusNotImplemented) + } + }) + }) + + It("returns error if delete snapshot fails", func() { + writeJsonParamToStdIn(`{ + "method":"delete_snapshot", + "arguments": ["snapshot-id"] + }`) + config := getDefaultConfig(Endpoint()) + err := cpi.Execute(config, logger) + Expect(err).ShouldNot(HaveOccurred()) + + _ = stdOutWriter.Close() + Expect(<-outChannel).To(ContainSubstring(`"message":"deleteSnapshot: Failed to delete snapshot ID snapshot-id`)) + }) + }) + + Context("Failure in WaitForSnapshotToBecomeStatus: ", func() { + + BeforeEach(func() { + Mux.HandleFunc("/v3/snapshots/snapshot-id", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodDelete: + w.WriteHeader(http.StatusAccepted) + case http.MethodGet: + w.WriteHeader(http.StatusOK) + _, _ = fmt.Fprintf(w, `{ + "snapshot": { + "id": "snapshot-id", + "status": "available" + } + }`) + } + }) + }) + + It("returns error if wait for snapshot to be deleted fails", func() { + writeJsonParamToStdIn(`{ + "method":"delete_snapshot", + "arguments": ["snapshot-id"] + }`) + config := getDefaultConfig(Endpoint()) + err := cpi.Execute(config, logger) + Expect(err).ShouldNot(HaveOccurred()) + + _ = stdOutWriter.Close() + Expect(<-outChannel).To(ContainSubstring(`"message":"deleteSnapshot: Failed while waiting for snapshot ID snapshot-id to be deleted`)) + }) + }) +}) diff --git a/src/openstack_cpi_golang/integration/delete_stemcell_test.go b/src/openstack_cpi_golang/integration/delete_stemcell_test.go new file mode 100644 index 00000000..5fe66cad --- /dev/null +++ b/src/openstack_cpi_golang/integration/delete_stemcell_test.go @@ -0,0 +1,40 @@ +package integration_test + +import ( + "net/http" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("DELETE STEMCELL", func() { + BeforeEach(func() { + SetupHTTP() + + MockAuthentication() + }) + + AfterEach(func() { + TeardownHTTP() + }) + + It("delete the stemcell image", func() { + Mux.HandleFunc("/v2/images/b2173dd3-7ad6-4362-baa6-a68bce3565cb", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusAccepted) + }) + + writeJsonParamToStdIn(`{ + "method":"delete_stemcell", + "arguments":[ + "b2173dd3-7ad6-4362-baa6-a68bce3565cb" + ] + }`) + + err := cpi.Execute(getDefaultConfig(Endpoint()), logger) + Expect(err).ShouldNot(HaveOccurred()) + + stdOutWriter.Close() + Expect(<-outChannel).To(ContainSubstring(`"error":null`)) + }) +}) diff --git a/src/openstack_cpi_golang/integration/delete_vm_test.go b/src/openstack_cpi_golang/integration/delete_vm_test.go new file mode 100644 index 00000000..af559da1 --- /dev/null +++ b/src/openstack_cpi_golang/integration/delete_vm_test.go @@ -0,0 +1,384 @@ +package integration_test + +import ( + "fmt" + "net/http" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/compute" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/loadbalancer" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Delete VM", func() { + var getServerCount = 0 + + var defaultServerResponse = func(r *http.Request, w http.ResponseWriter, serverId string) { + switch r.Method { + case http.MethodDelete: + w.WriteHeader(http.StatusNoContent) + fmt.Fprintf(w, `{}`) + + case http.MethodGet: + getServerCount++ + switchCase := getServerCount % 2 + + w.WriteHeader(http.StatusOK) + + switch switchCase { + case 1: + fmt.Fprintf(w, `{ + "server": { + "id": "%s", + "status": "ACTIVE" + } + }`, serverId) + case 0: + fmt.Fprintf(w, `{ + "server": { + "id": "%s", + "status": "DELETED" + } + }`, serverId) + } + } + } + + BeforeEach(func() { + loadbalancer.LoadbalancerServicePollingInterval = 0 + compute.ComputeServicePollingInterval = 0 + + SetupHTTP() + + MockAuthentication() + + Mux.HandleFunc("/v2.0/ports/1", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodDelete: + w.WriteHeader(http.StatusNoContent) + fmt.Fprintf(w, `{}`) + } + }) + + Mux.HandleFunc("/v2.0/ports/wrong-port-id", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodDelete: + w.WriteHeader(http.StatusNotFound) + fmt.Fprintf(w, `{}`) + } + }) + + Mux.HandleFunc("/v2.0/ports", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodDelete: + w.WriteHeader(http.StatusNoContent) + fmt.Fprintf(w, `{}`) + + case http.MethodGet: + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + deviceID := r.URL.Query().Get("device_id") + if deviceID != "1" && deviceID != "server_id_wrong_port" { + fmt.Fprintf(w, `{ + "ports": [] + }`) + } + + if deviceID == "1" { + fmt.Fprintf(w, `{ + "ports": [ + { + "device_id": "1", + "id": "1" + } + ] + }`) + } else if deviceID == "server_id_wrong_port" { + fmt.Fprintf(w, `{ + "ports": [ + { + "device_id": "server_id_wrong_port", + "id": "1" + }, + { + "device_id": "server_id_wrong_port", + "id": "wrong-port-id" + } + ] + }`) + } + } + }) + + Mux.HandleFunc("/v2.1/servers/1", func(w http.ResponseWriter, r *http.Request) { + defaultServerResponse(r, w, "1") + }) + + Mux.HandleFunc("/v2.1/servers/1/metadata", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, `{ + "metadata": { + "foo": "foo_value", + "lbaas_pool_1": "pool_id_1/member_id_1", + "lbaas_pool_2": "pool_id_1/member_id_not_existing" + } + }`) + } + }) + + Mux.HandleFunc("/v2.1/servers/server_id_wrong_port", func(w http.ResponseWriter, r *http.Request) { + defaultServerResponse(r, w, "server_id_wrong_port") + }) + + Mux.HandleFunc("/v2.1/servers/server_id_poolmember_deletion_error", func(w http.ResponseWriter, r *http.Request) { + defaultServerResponse(r, w, "server_id_poolmember_deletion_error") + }) + + Mux.HandleFunc("/v2.1/servers/server_id_poolmember_deletion_error/metadata", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, `{ + "metadata": { + "lbaas_pool_1": "pool_id_1/member_id_1", + "lbaas_pool_3": "pool_id_1/member_id_error" + } + }`) + } + }) + + Mux.HandleFunc("/v2.1/servers/server_id_deleted_inbetween", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodDelete: + w.WriteHeader(http.StatusNotFound) + fmt.Fprintf(w, `{}`) + + case http.MethodGet: + getServerCount++ + switchCase := getServerCount % 2 + + switch switchCase { + case 1: + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, `{ + "server": { + "id": "1", + "status": "ACTIVE" + } + }`) + case 0: + w.WriteHeader(http.StatusNotFound) + fmt.Fprintf(w, `{}`) + } + } + }) + + Mux.HandleFunc("/v2.1/servers/server_id_timeout_lbas", func(w http.ResponseWriter, r *http.Request) { + defaultServerResponse(r, w, "server_id_timeout_lbas") + }) + + Mux.HandleFunc("/v2.1/servers/server_id_timeout_lbas/metadata", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, `{ + "metadata": { + "foo": "foo_value", + "lbaas_pool_id_timeout": "pool_id_timeout/member_id_1" + } + }`) + } + }) + + Mux.HandleFunc("/v2.1/servers/wrong-vm-id", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + w.WriteHeader(http.StatusNotFound) + fmt.Fprintf(w, `{}`) + } + }) + + Mux.HandleFunc("/v2.0/lbaas/pools/pool_id_1", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, `{ + "pool": { + "id": "pool_id_1", + "loadbalancers": [{"id": "lbas_id_1"}], + "listeners": [{"id": "listener_id_1"}] + } + }`) + } + }) + + Mux.HandleFunc("/v2.0/lbaas/pools/pool_id_timeout", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, `{ + "pool": { + "id": "pool_id_timeout", + "loadbalancers": [{"id": "lbas_id_timeout"}] + } + }`) + } + }) + + Mux.HandleFunc("/v2.0/lbaas/pools/pool_id_1/members/member_id_1", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodDelete: + w.WriteHeader(http.StatusNoContent) + + fmt.Fprintf(w, `{}`) + } + }) + + Mux.HandleFunc("/v2.0/lbaas/pools/pool_id_1/members/member_id_not_existing", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodDelete: + w.WriteHeader(http.StatusNotFound) + + fmt.Fprintf(w, `{}`) + } + }) + + Mux.HandleFunc("/v2.0/lbaas/pools/pool_id_1/members/member_id_error", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodDelete: + w.WriteHeader(http.StatusInternalServerError) + + fmt.Fprintf(w, `{}`) + } + }) + + Mux.HandleFunc("/v2.0/lbaas/loadbalancers/lbas_id_1", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, `{ + "loadbalancer": { + "id": "lbas_id_1", + "provisioning_status": "ACTIVE" + } + }`) + } + }) + + Mux.HandleFunc("/v2.0/lbaas/loadbalancers/lbas_id_timeout", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, `{ + "loadbalancer": { + "id": "lbas_id_timeout", + "provisioning_status": "PENDING_UPDATE" + } + }`) + } + }) + }) + + AfterEach(func() { + TeardownHTTP() + }) + + It("deletes a vm", func() { + writeJsonParamToStdIn(`{ + "method": "delete_vm", + "arguments": ["1"], + "api_version": 2 + }`) + + err := cpi.Execute(getDefaultConfig(Endpoint()), logger) + Expect(err).ShouldNot(HaveOccurred()) + + stdOutWriter.Close() + Expect(<-outChannel).To(ContainSubstring(`"result":null,"error":null`)) + }) + + It("times out waiting for the load balancer to become ACTIVE", func() { + writeJsonParamToStdIn(`{ + "method": "delete_vm", + "arguments": ["server_id_timeout_lbas"], + "api_version": 2 + }`) + + cpiConfig := getDefaultConfig(Endpoint()) + + err := cpi.Execute(cpiConfig, logger) + Expect(err).ShouldNot(HaveOccurred()) + + stdOutWriter.Close() + Expect(<-outChannel).To(ContainSubstring(`message":"delete_vm: failed while waiting for loadbalancer to become active: timeout while waiting for loadbalancer 'lbas_id_timeout'`)) + }) + + It("does not fail when deleting not-existing vm", func() { + writeJsonParamToStdIn(`{ + "method": "delete_vm", + "arguments": ["wrong-vm-id"], + "api_version": 2 + }`) + + err := cpi.Execute(getDefaultConfig(Endpoint()), logger) + Expect(err).ShouldNot(HaveOccurred()) + + stdOutWriter.Close() + Expect(<-outChannel).To(ContainSubstring(`"result":null,"error":null`)) + }) + + It("does not fail when deleting a vm with not-existing port", func() { + writeJsonParamToStdIn(`{ + "method": "delete_vm", + "arguments": ["server_id_wrong_port"], + "api_version": 2 + }`) + + err := cpi.Execute(getDefaultConfig(Endpoint()), logger) + Expect(err).ShouldNot(HaveOccurred()) + + stdOutWriter.Close() + Expect(<-outChannel).To(ContainSubstring(`"result":null,"error":null`)) + }) + + It("does not fail when deleting a vm which no longer exists", func() { + writeJsonParamToStdIn(`{ + "method": "delete_vm", + "arguments": ["server_id_deleted_inbetween"], + "api_version": 2 + }`) + + err := cpi.Execute(getDefaultConfig(Endpoint()), logger) + Expect(err).ShouldNot(HaveOccurred()) + + stdOutWriter.Close() + Expect(<-outChannel).To(ContainSubstring(`"result":null,"error":null`)) + }) + + It("raises an error if it fails to delete pool member", func() { + writeJsonParamToStdIn(`{ + "method": "delete_vm", + "arguments": ["server_id_poolmember_deletion_error"], + "api_version": 2 + }`) + + cpiConfig := getDefaultConfig(Endpoint()) + + err := cpi.Execute(cpiConfig, logger) + Expect(err).ShouldNot(HaveOccurred()) + + stdOutWriter.Close() + Expect(<-outChannel).To(ContainSubstring(`message":"delete_vm: failed to delete pool member: max retry attempts (10) reached, err: Internal Server Error`)) + }) +}) diff --git a/src/openstack_cpi_golang/integration/detach_disk_test.go b/src/openstack_cpi_golang/integration/detach_disk_test.go new file mode 100644 index 00000000..82cc6362 --- /dev/null +++ b/src/openstack_cpi_golang/integration/detach_disk_test.go @@ -0,0 +1,161 @@ +package integration_test + +import ( + "fmt" + "net/http" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/config" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("DetachDiskMethod Integration Tests", func() { + + detachResultSuccess := true + + BeforeEach(func() { + SetupHTTP() + MockAuthentication() + + Mux.HandleFunc("/v2.1/servers/server-id-ok", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, `{ + "server": { + "id": "server-id-ok", + "status": "ACTIVE" + } + }`) + default: + w.WriteHeader(http.StatusNotImplemented) + } + }) + Mux.HandleFunc("/v2.1/servers/server-id-ok/os-volume_attachments", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + payload := `{ "volumeAttachments": [ + { + "id": "attachment-id-1", + "device": "/dev/sdb", + "volumeId": "volume-id-already-1", + "serverId": "server-id-ok", + "tag": "tag-1", + "delete_on_termination": false + }, + { + "id": "attachment-id-2", + "device": "/dev/sdc", + "volumeId": "volume-id-already-2", + "serverId": "server-id-ok", + "tag": "tag-2", + "delete_on_termination": false + }]}` + fmt.Fprint(w, payload) + default: + w.WriteHeader(http.StatusNotImplemented) + } + }) + Mux.HandleFunc("/v2.1/servers/server-id-ok/os-volume_attachments/volume-id-already-1", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodDelete: + if detachResultSuccess == true { + w.WriteHeader(http.StatusAccepted) + } else { + w.WriteHeader(http.StatusInternalServerError) + } + default: + w.WriteHeader(http.StatusNotImplemented) + } + }) + Mux.HandleFunc("/v3/volumes/volume-id-already-1", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, `{ + "volume": { + "id": "volume-id-already-1", + "status": "available" + } + }`) + default: + w.WriteHeader(http.StatusNotImplemented) + } + }) + }) + + AfterEach(func() { + TeardownHTTP() + }) + + Context("success: DetachDisk", func() { + + It("volume already attached", func() { + writeJsonParamToStdIn(`{ + "method":"detach_disk", + "arguments": [ + "server-id-ok", + "volume-id-ok" + ], + "context": {}, + "api_version": 2 + }`) + err := cpi.Execute(getDefaultConfig(Endpoint()), logger) + Expect(err).ShouldNot(HaveOccurred()) + stdOutWriter.Close() + actual := <-outChannel + Expect(actual).To(ContainSubstring(`"result":null,"error":null`)) + }) + + It("volume gets detached", func() { + writeJsonParamToStdIn(`{ + "method":"detach_disk", + "arguments": [ + "server-id-ok", + "volume-id-already-1" + ], + "context": {}, + "api_version": 2 + }`) + err := cpi.Execute(getDefaultConfig(Endpoint()), logger) + Expect(err).ShouldNot(HaveOccurred()) + stdOutWriter.Close() + actual := <-outChannel + Expect(actual).To(ContainSubstring(`"result":null,"error":null`)) + }) + }) + + Context("failure: DetachDisk", func() { + + It("volume detach failed", func() { + detachResultSuccess = false + writeJsonParamToStdIn(`{ + "method":"detach_disk", + "arguments": [ + "server-id-ok", + "volume-id-already-1" + ], + "context": {}, + "api_version": 2 + }`) + currentConfig := getDefaultConfig(Endpoint()) + currentConfig.Cloud.Properties.RetryConfig = config.RetryConfigMap{ + "default": config.RetryConfig{ + MaxAttempts: 0, + SleepDuration: 0, + }, + } + err := cpi.Execute(currentConfig, logger) + Expect(err).ShouldNot(HaveOccurred()) + stdOutWriter.Close() + actual := <-outChannel + Expect(actual).To(ContainSubstring("Internal Server Error")) + }) + + }) +}) diff --git a/src/openstack_cpi_golang/integration/get_disks_test.go b/src/openstack_cpi_golang/integration/get_disks_test.go new file mode 100644 index 00000000..06400b3c --- /dev/null +++ b/src/openstack_cpi_golang/integration/get_disks_test.go @@ -0,0 +1,148 @@ +package integration_test + +import ( + "fmt" + "net/http" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("GetDisksMethod Integration Tests", func() { + + BeforeEach(func() { + SetupHTTP() + MockAuthentication() + + Mux.HandleFunc("/v2.1/servers/server-id-ok", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, `{ + "server": { + "id": "server-id-ok", + "status": "ACTIVE" + } + }`) + default: + w.WriteHeader(http.StatusNotImplemented) + } + }) + Mux.HandleFunc("/v2.1/servers/server-id-ok-no-disks", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, `{ + "server": { + "id": "server-id-ok-no-disks", + "status": "ACTIVE" + } + }`) + default: + w.WriteHeader(http.StatusNotImplemented) + } + }) + Mux.HandleFunc("/v2.1/servers/server-id-not-ok", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + w.WriteHeader(http.StatusNotFound) + default: + w.WriteHeader(http.StatusNotImplemented) + } + }) + Mux.HandleFunc("/v2.1/servers/server-id-ok/os-volume_attachments", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + payload := `{ "volumeAttachments": [ + { + "id": "attachment-id-1", + "device": "/dev/sdb", + "volumeId": "volume-id-already-1", + "serverId": "server-id-ok", + "tag": "tag-1", + "delete_on_termination": false + }, + { + "id": "attachment-id-2", + "device": "/dev/sdc", + "volumeId": "volume-id-already-2", + "serverId": "server-id-ok", + "tag": "tag-2", + "delete_on_termination": false + }]}` + fmt.Fprint(w, payload) + default: + w.WriteHeader(http.StatusNotImplemented) + } + }) + + Mux.HandleFunc("/v2.1/servers/server-id-ok-no-disks/os-volume_attachments", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + payload := `{ "volumeAttachments": []}` + fmt.Fprint(w, payload) + default: + w.WriteHeader(http.StatusNotImplemented) + } + }) + }) + + AfterEach(func() { + TeardownHTTP() + }) + + It("lists the volumes already attached", func() { + writeJsonParamToStdIn(`{ + "method":"get_disks", + "arguments": [ + "server-id-ok" + ], + "context": {}, + "api_version": 2 + }`) + err := cpi.Execute(getDefaultConfig(Endpoint()), logger) + Expect(err).ShouldNot(HaveOccurred()) + stdOutWriter.Close() + actual := <-outChannel + Expect(actual).To(ContainSubstring(`"result":["volume-id-already-1","volume-id-already-2"],"error":null,"log":""`)) + }) + + It("server not found", func() { + writeJsonParamToStdIn(`{ + "method":"get_disks", + "arguments": [ + "server-id-not-ok" + ], + "context": {}, + "api_version": 2 + }`) + err := cpi.Execute(getDefaultConfig(Endpoint()), logger) + Expect(err).ShouldNot(HaveOccurred()) + stdOutWriter.Close() + actual := <-outChannel + Expect(actual).To(ContainSubstring(`"message":"get_disks: Failed to get VM server-id-not-ok: failed to retrieve server information: Resource not found`)) + }) + + It("no disks attached to the server", func() { + writeJsonParamToStdIn(`{ + "method":"get_disks", + "arguments": [ + "server-id-ok-no-disks" + ], + "context": {}, + "api_version": 2 + }`) + err := cpi.Execute(getDefaultConfig(Endpoint()), logger) + Expect(err).ShouldNot(HaveOccurred()) + stdOutWriter.Close() + actual := <-outChannel + Expect(actual).To(ContainSubstring(`"result":[],"error":null`)) + }) +}) diff --git a/src/openstack_cpi_golang/integration/has_disk_test.go b/src/openstack_cpi_golang/integration/has_disk_test.go new file mode 100644 index 00000000..eca89d59 --- /dev/null +++ b/src/openstack_cpi_golang/integration/has_disk_test.go @@ -0,0 +1,70 @@ +package integration_test + +import ( + "fmt" + "net/http" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Has Disk", func() { + BeforeEach(func() { + SetupHTTP() + + MockAuthentication() + + Mux.HandleFunc("/v3/volumes/volume_id_exists", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, `{ + "volume": { + "id": "volume-id" + } + }`) + } + }) + + Mux.HandleFunc("/v3/volumes/volume_id_does_not_exist", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + w.WriteHeader(http.StatusNotFound) + } + }) + }) + + AfterEach(func() { + TeardownHTTP() + }) + + It("returns true if the volume exists", func() { + writeJsonParamToStdIn(`{ + "method":"has_disk", + "arguments": ["volume_id_exists"], + "api_version": 2 + }`) + + err := cpi.Execute(getDefaultConfig(Endpoint()), logger) + Expect(err).ShouldNot(HaveOccurred()) + + stdOutWriter.Close() + Expect(<-outChannel).To(ContainSubstring(`"result":true,"error":null`)) + }) + + It("returns false if the volume does not exist", func() { + writeJsonParamToStdIn(`{ + "method":"has_disk", + "arguments": ["volume_id_does_not_exist"], + "api_version": 2 + }`) + + err := cpi.Execute(getDefaultConfig(Endpoint()), logger) + Expect(err).ShouldNot(HaveOccurred()) + + stdOutWriter.Close() + Expect(<-outChannel).To(ContainSubstring(`"result":false,"error":null`)) + }) + +}) diff --git a/src/openstack_cpi_golang/integration/has_vm_test.go b/src/openstack_cpi_golang/integration/has_vm_test.go new file mode 100644 index 00000000..3672f14d --- /dev/null +++ b/src/openstack_cpi_golang/integration/has_vm_test.go @@ -0,0 +1,158 @@ +package integration_test + +import ( + "fmt" + "net/http" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi" + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi/config" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("HAS VM", func() { + BeforeEach(func() { + SetupHTTP() + + MockAuthentication() + + Mux.HandleFunc("/v2.1/servers/active-server-id", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, `{ + "server": { + "id": "active-server-id", + "status": "ACTIVE" + } + }`) + } + }) + + Mux.HandleFunc("/v2.1/servers/deleted-server-id", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, `{ + "server": { + "id": "deleted-server-id", + "status": "DELETED" + } + }`) + } + }) + + Mux.HandleFunc("/v2.1/servers/terminated-server-id", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, `{ + "server": { + "id": "terminated-server-id", + "status": "TERMINATED" + } + }`) + } + }) + + Mux.HandleFunc("/v2.1/servers/wrong-vm-id", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + w.WriteHeader(http.StatusNotFound) + fmt.Fprintf(w, `{}`) + } + }) + + Mux.HandleFunc("/v2.1/servers/error-vm-id", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprintf(w, `{}`) + } + }) + + }) + + AfterEach(func() { + TeardownHTTP() + }) + + It("returns true if the server exists and is ACTIVE", func() { + writeJsonParamToStdIn(`{ + "method":"has_vm", + "arguments": ["active-server-id"], + "api_version": 2 + }`) + + err := cpi.Execute(getDefaultConfig(Endpoint()), logger) + Expect(err).ShouldNot(HaveOccurred()) + + stdOutWriter.Close() + Expect(<-outChannel).To(ContainSubstring(`"result":true,"error":null`)) + }) + + It("returns false if the server exists and is DELETED", func() { + writeJsonParamToStdIn(`{ + "method":"has_vm", + "arguments": ["deleted-server-id"], + "api_version": 2 + }`) + + err := cpi.Execute(getDefaultConfig(Endpoint()), logger) + Expect(err).ShouldNot(HaveOccurred()) + + stdOutWriter.Close() + Expect(<-outChannel).To(ContainSubstring(`"result":false,"error":null`)) + }) + + It("returns false if the server exists and is TERMINATED", func() { + writeJsonParamToStdIn(`{ + "method":"has_vm", + "arguments": ["terminated-server-id"], + "api_version": 2 + }`) + + err := cpi.Execute(getDefaultConfig(Endpoint()), logger) + Expect(err).ShouldNot(HaveOccurred()) + + stdOutWriter.Close() + Expect(<-outChannel).To(ContainSubstring(`"result":false,"error":null`)) + }) + + It("returns false if the server does not exist", func() { + writeJsonParamToStdIn(`{ + "method":"has_vm", + "arguments": ["wrong-vm-id"], + "api_version": 2 + }`) + + err := cpi.Execute(getDefaultConfig(Endpoint()), logger) + Expect(err).ShouldNot(HaveOccurred()) + + stdOutWriter.Close() + Expect(<-outChannel).To(ContainSubstring(`"result":false,"error":null`)) + }) + + It("returns false and raises an error if server retrieval fails", func() { + writeJsonParamToStdIn(`{ + "method":"has_vm", + "arguments": ["error-vm-id"], + "api_version": 2 + }`) + + cpiConfig := getDefaultConfig(Endpoint()) + cpiConfig.Cloud.Properties.RetryConfig = config.RetryConfigMap{ + "default": config.RetryConfig{ + MaxAttempts: 10, + SleepDuration: 0, + }, + } + + err := cpi.Execute(cpiConfig, logger) + Expect(err).ShouldNot(HaveOccurred()) + + stdOutWriter.Close() + Expect(<-outChannel).To(ContainSubstring(`message":"has_vm: failed to retrieve server information: max retry attempts (10) reached, err: Internal Server Error`)) + }) + +}) diff --git a/src/openstack_cpi_golang/integration/integration_suite_test.go b/src/openstack_cpi_golang/integration/integration_suite_test.go index 4f5528eb..e5edeaef 100644 --- a/src/openstack_cpi_golang/integration/integration_suite_test.go +++ b/src/openstack_cpi_golang/integration/integration_suite_test.go @@ -24,7 +24,7 @@ func TestIntegration(t *testing.T) { } var defaultConfig config.CpiConfig -var bootFromVolumeConfig config.CpiConfig //nolint:unused // To be used in subsequent PR. +var bootFromVolumeConfig config.CpiConfig var logger = utils.NewLogger(boshlog.NewWriterLogger(boshlog.LevelDebug, os.Stderr)) var Mux *http.ServeMux var Server *httptest.Server @@ -68,7 +68,6 @@ func getDefaultConfig(url string) config.CpiConfig { return defaultConfig } -// nolint:unused // To be used in subsequent PR. func getBootFromVolumeConfig(url string) config.CpiConfig { bootFromVolumeConfig.Cloud.Properties.Openstack = config.OpenstackConfig{ AuthURL: url, @@ -108,7 +107,7 @@ func MockAuthentication() { Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: - fmt.Fprintf(w, `{ + _, _ = fmt.Fprintf(w, `{ "versions": {"values": [ {"status": "stable","id": "v3.0","links": [{ "href": "%s", "rel": "self" }]}, {"status": "stable","id": "v2.0","links": [{ "href": "%s", "rel": "self" }]} @@ -123,7 +122,7 @@ func MockAuthentication() { w.Header().Add("X-Subject-Token", "0123456789") w.WriteHeader(http.StatusCreated) - fmt.Fprintf(w, `{ + _, _ = fmt.Fprintf(w, `{ "token": { "expires_at": "2013-02-02T18:30:59.000000Z", "catalog": [{ @@ -174,14 +173,8 @@ func writeJsonParamToStdIn(json string) { os.Stdin = reader go func() { - _, err := writer.WriteString(json) - if err != nil { - return - } - err = writer.Close() - if err != nil { - return - } + _, _ = writer.WriteString(json) + _ = writer.Close() }() } @@ -192,10 +185,7 @@ func setupReadableStdOut() (chan string, *os.File) { // copy the output in a separate goroutine so reading from the pipe doesn't block indefinitely go func() { var buf bytes.Buffer - _, err := io.Copy(&buf, reader) - if err != nil { - return - } + _, _ = io.Copy(&buf, reader) outChannel <- buf.String() }() diff --git a/src/openstack_cpi_golang/integration/reboot_vm_test.go b/src/openstack_cpi_golang/integration/reboot_vm_test.go new file mode 100644 index 00000000..6d87b9a2 --- /dev/null +++ b/src/openstack_cpi_golang/integration/reboot_vm_test.go @@ -0,0 +1,249 @@ +package integration_test + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("REBOOT VM", func() { + var getServerCount = 0 + + BeforeEach(func() { + SetupHTTP() + + Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + _, _ = fmt.Fprintf(w, `{ + "versions": {"values": [ + {"status": "stable","id": "v3.0","links": [{ "href": "%s", "rel": "self" }]}, + {"status": "stable","id": "v2.0","links": [{ "href": "%s", "rel": "self" }]} + ]} + }`, Endpoint()+"/v3", Endpoint()+"/v2.0") + } + }) + + Mux.HandleFunc("/v3/auth/tokens", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodPost: + w.Header().Add("X-Subject-Token", "0123456789") + w.WriteHeader(http.StatusCreated) + + _, _ = fmt.Fprintf(w, `{ + "token": { + "expires_at": "2013-02-02T18:30:59.000000Z", + "catalog": [{ + "endpoints": [ + {"id": "1", "interface": "public", "region": "RegionOne", "url": "%s/v2.1"}, + {"id": "2", "interface": "admin", "region": "RegionOne", "url": "%s/v2.1"}, + {"id": "3", "interface": "internal", "region": "RegionOne", "url": "%s/v2.1"} + ], + "type": "compute", + "name": "nova" + },{ + "endpoints": [ + {"id": "1", "interface": "public", "region": "RegionOne", "url": "%s/"}, + {"id": "2", "interface": "admin", "region": "RegionOne", "url": "%s/"}, + {"id": "3", "interface": "internal", "region": "RegionOne", "url": "%s/"} + ], + "type": "network", + "name": "neutron" + },{ + "endpoints": [{"url": "%s/","interface": "public","region": "RegionOne"}], + "type": "image", + "name": "glance" + },{ + "endpoints": [ + { "id": "1", "interface": "public", "region": "RegionOne", "url": "%s/v2.0"}, + { "id": "2", "interface": "admin", "region": "RegionOne", "url": "%s/v2.0"}, + { "id": "3", "interface": "internal","region": "RegionOne", "url": "%s/v2.0"} + ], + "type": "load-balancer", + "name": "octavia" + }] + } + }`, Endpoint(), Endpoint(), Endpoint(), Endpoint(), Endpoint(), Endpoint(), Endpoint(), Endpoint(), Endpoint(), Endpoint()) + } + }) + + Mux.HandleFunc("/v2.1/servers/active-server-id", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + w.WriteHeader(http.StatusOK) + _, _ = fmt.Fprintf(w, `{ + "server": { + "id": "active-server-id", + "status": "ACTIVE" + } + }`) + } + }) + + Mux.HandleFunc("/v2.1/servers/wrong-vm-id", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + w.WriteHeader(http.StatusNotFound) + _, _ = fmt.Fprintf(w, `{}`) + } + }) + + Mux.HandleFunc("/v2.1/servers/error-reboot-server-id", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + w.WriteHeader(http.StatusOK) + _, _ = fmt.Fprintf(w, `{ + "server": { + "id": "error-reboot-server-id", + "status": "ACTIVE" + } + }`) + } + }) + + Mux.HandleFunc("/v2.1/servers/error-server-state-id", func(w http.ResponseWriter, r *http.Request) { + getServerCount++ + switchCase := getServerCount % 2 + + w.WriteHeader(http.StatusOK) + + switch switchCase { + case 1: + _, _ = fmt.Fprintf(w, `{ + "server": { + "id": "error-server-state-id", + "status": "ACTIVE" + } + }`) + case 0: + _, _ = fmt.Fprintf(w, `{ + "server": { + "id": "error-server-state-id", + "status": "ERROR" + } + }`) + } + }) + + Mux.HandleFunc("/v2.1/servers/active-server-id/action", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodPost: + var result map[string]interface{} + body, _ := io.ReadAll(r.Body) + _ = json.Unmarshal(body, &result) + + cpiRebootMethod := result["reboot"].(map[string]interface{}) + if cpiRebootMethod["type"].(string) == "SOFT" { + w.WriteHeader(http.StatusAccepted) + _, _ = fmt.Fprintf(w, `{ }`) + } + } + }) + + Mux.HandleFunc("/v2.1/servers/error-reboot-server-id/action", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodPost: + var result map[string]interface{} + body, _ := io.ReadAll(r.Body) + _ = json.Unmarshal(body, &result) + + cpiRebootMethod := result["reboot"].(map[string]interface{}) + if cpiRebootMethod["type"].(string) == "SOFT" { + w.WriteHeader(http.StatusNotFound) + _, _ = fmt.Fprintf(w, `{}`) + } + } + }) + + Mux.HandleFunc("/v2.1/servers/error-server-state-id/action", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodPost: + var result map[string]interface{} + body, _ := io.ReadAll(r.Body) + _ = json.Unmarshal(body, &result) + + cpiRebootMethod := result["reboot"].(map[string]interface{}) + if cpiRebootMethod["type"].(string) == "SOFT" { + w.WriteHeader(http.StatusAccepted) + _, _ = fmt.Fprintf(w, `{ }`) + } + } + }) + + }) + + AfterEach(func() { + TeardownHTTP() + }) + + It("Reboots a server", func() { + writeJsonParamToStdIn(`{ + "method":"reboot_vm", + "arguments": ["active-server-id"], + "api_version": 2 + }`) + + cpiConfig := getDefaultConfig(Endpoint()) + cpiConfig.Cloud.Properties.Openstack.StateTimeOut = 1 + + err := cpi.Execute(cpiConfig, logger) + Expect(err).ShouldNot(HaveOccurred()) + + _ = stdOutWriter.Close() + Expect(<-outChannel).To(ContainSubstring(`"result":"","error":null`)) + }) + + It("Fails if a server is not found", func() { + writeJsonParamToStdIn(`{ + "method":"reboot_vm", + "arguments": ["wrong-vm-id"], + "api_version": 2 + }`) + + err := cpi.Execute(getDefaultConfig(Endpoint()), logger) + Expect(err).ShouldNot(HaveOccurred()) + + _ = stdOutWriter.Close() + Expect(<-outChannel).To( + ContainSubstring(`reboot_vm: failed to retrieve server information: Resource not found`), + ) + }) + + It("Fails if rebooting server raises an error", func() { + writeJsonParamToStdIn(`{ + "method":"reboot_vm", + "arguments": ["error-reboot-server-id"], + "api_version": 2 + }`) + + err := cpi.Execute(getDefaultConfig(Endpoint()), logger) + Expect(err).ShouldNot(HaveOccurred()) + + _ = stdOutWriter.Close() + Expect(<-outChannel).To( + ContainSubstring(`reboot_vm: failed to reboot server: Resource not found`), + ) + }) + + It("Fails if rebooting server results in an erroneous server state", func() { + writeJsonParamToStdIn(`{ + "method":"reboot_vm", + "arguments": ["error-server-state-id"], + "api_version": 2 + }`) + + err := cpi.Execute(getDefaultConfig(Endpoint()), logger) + Expect(err).ShouldNot(HaveOccurred()) + + _ = stdOutWriter.Close() + Expect(<-outChannel).To( + ContainSubstring(`reboot_vm: compute_service: server became ERROR state while waiting to become ACTIVE"`), + ) + }) + +}) diff --git a/src/openstack_cpi_golang/integration/resize_disk_test.go b/src/openstack_cpi_golang/integration/resize_disk_test.go new file mode 100644 index 00000000..3cefda41 --- /dev/null +++ b/src/openstack_cpi_golang/integration/resize_disk_test.go @@ -0,0 +1,260 @@ +package integration_test + +import ( + "fmt" + "net/http" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("RESIZE DISK", func() { + + BeforeEach(func() { + SetupHTTP() + MockAuthentication() + }) + + AfterEach(func() { + TeardownHTTP() + }) + + Context("Failure in GetVolume: ", func() { + + BeforeEach(func() { + Mux.HandleFunc("/v3/volumes/disk-id", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + w.WriteHeader(http.StatusNotFound) + } + }) + }) + + It("returns error if getter volume fails ", func() { + writeJsonParamToStdIn(`{ + "method":"resize_disk", + "arguments": [ + "disk-id", + 5000 + ], + "api_version": 2 + }`) + + err := cpi.Execute(getDefaultConfig(Endpoint()), logger) + Expect(err).ShouldNot(HaveOccurred()) + + stdOutWriter.Close() + Expect(<-outChannel).To(ContainSubstring(`cannot resize volume because volume with id`)) + + }) + }) + + Context("Failure in Switch Case: ", func() { + + BeforeEach(func() { + Mux.HandleFunc("/v3/volumes/disk-id", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, `{ + "volume": { + "id": "disk-id", + "size": 5, + "attachments": [{"AttachmentID": "test-attachment-id"}] + } + }`) + } + }) + }) + + It("returns nil if current volumesize = new volumesize", func() { + writeJsonParamToStdIn(`{ + "method":"resize_disk", + "arguments": [ + "disk-id", + 5000 + ], + "api_version": 2 + }`) + + err := cpi.Execute(getDefaultConfig(Endpoint()), logger) + Expect(err).ShouldNot(HaveOccurred()) + + stdOutWriter.Close() + Expect(<-outChannel).To(ContainSubstring(`{"result":null,"error":null,"log":""}`)) + }) + + It("returns error if current volumesize > new volumesize ", func() { + writeJsonParamToStdIn(`{ + "method":"resize_disk", + "arguments": [ + "disk-id", + 4000 + ], + "api_version": 2 + }`) + + err := cpi.Execute(getDefaultConfig(Endpoint()), logger) + Expect(err).ShouldNot(HaveOccurred()) + + stdOutWriter.Close() + Expect(<-outChannel).To(ContainSubstring(`cannot resize volume to a smaller size from`)) + }) + + It("returns error if volumeattachments != nil", func() { + writeJsonParamToStdIn(`{ + "method":"resize_disk", + "arguments": [ + "disk-id", + 6000 + ], + "api_version": 2 + }`) + + err := cpi.Execute(getDefaultConfig(Endpoint()), logger) + Expect(err).ShouldNot(HaveOccurred()) + + stdOutWriter.Close() + Expect(<-outChannel).To(ContainSubstring(`cannot resize volume disk-id due to attachments`)) + }) + }) + + Context("Failure in ExtendVolumeSize: ", func() { + + BeforeEach(func() { + Mux.HandleFunc("/v3/volumes/disk-id", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, `{ + "volume": { + "id": "disk-id", + "size": 4, + "attachments": null + } + }`) + } + }) + + Mux.HandleFunc("/v3/volumes/disk-id/action", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodPost: + w.WriteHeader(http.StatusNotFound) + } + }) + }) + + It("returns error if volumeattachments != nil", func() { + writeJsonParamToStdIn(`{ + "method":"resize_disk", + "arguments": [ + "disk-id", + 5000 + ], + "api_version": 2 + }`) + + err := cpi.Execute(getDefaultConfig(Endpoint()), logger) + Expect(err).ShouldNot(HaveOccurred()) + + stdOutWriter.Close() + Expect(<-outChannel).To(ContainSubstring(`failed to resize volume`)) + }) + }) + + Context("Failure in WaitForVolumeToBecomeStatus: ", func() { + + BeforeEach(func() { + Mux.HandleFunc("/v3/volumes/disk-id", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, `{ + "volume": { + "id": "disk-id", + "size": 4, + "status": "error", + "attachments": null + } + }`) + } + }) + + Mux.HandleFunc("/v3/volumes/disk-id/action", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodPost: + w.WriteHeader(http.StatusAccepted) + } + }) + }) + + It("returns error while waiting for resizing", func() { + writeJsonParamToStdIn(`{ + "method":"resize_disk", + "arguments": [ + "disk-id", + 5000 + ], + "api_version": 2 + }`) + + config := getDefaultConfig(Endpoint()) + config.Cloud.Properties.Openstack.StateTimeOut = 0 + + err := cpi.Execute(config, logger) + Expect(err).ShouldNot(HaveOccurred()) + + stdOutWriter.Close() + Expect(<-outChannel).To(ContainSubstring(`failed while waiting on resizing volume`)) + }) + }) + + Context("Positive Case: ", func() { + + BeforeEach(func() { + Mux.HandleFunc("/v3/volumes/disk-id", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, `{ + "volume": { + "id": "disk-id", + "size": 4, + "status": "available", + "attachments": null + } + }`) + } + }) + + Mux.HandleFunc("/v3/volumes/disk-id/action", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodPost: + w.WriteHeader(http.StatusAccepted) + } + }) + }) + + It("returns nil for successful run", func() { + writeJsonParamToStdIn(`{ + "method":"resize_disk", + "arguments": [ + "disk-id", + 5000 + ], + "api_version": 2 + }`) + + config := getDefaultConfig(Endpoint()) + config.Cloud.Properties.Openstack.StateTimeOut = 0 + + err := cpi.Execute(config, logger) + Expect(err).ShouldNot(HaveOccurred()) + + stdOutWriter.Close() + Expect(<-outChannel).To(ContainSubstring(`{"result":null,"error":null,"log":""}`)) + }) + }) + +}) diff --git a/src/openstack_cpi_golang/integration/set_disk_metadata_test.go b/src/openstack_cpi_golang/integration/set_disk_metadata_test.go new file mode 100644 index 00000000..3ca862f0 --- /dev/null +++ b/src/openstack_cpi_golang/integration/set_disk_metadata_test.go @@ -0,0 +1,117 @@ +package integration_test + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("SetDiskMetadataMethod Integration Tests", func() { + + setMetadataIsSuccessful := true + + BeforeEach(func() { + SetupHTTP() + MockAuthentication() + Mux.HandleFunc("/v3/volumes/volume-id-ok", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + metadata := map[string]string{ + "key1": "value1", + } + responsePayload := map[string]interface{}{ + "id": "volume-id-ok", + "metadata": metadata, + } + response, _ := json.Marshal(responsePayload) + fmt.Fprint(w, string(response)) + case http.MethodPut: + if setMetadataIsSuccessful { + w.WriteHeader(http.StatusOK) + responsePayload := map[string]interface{}{ + "id": "volume-id-ok", + "metadata": map[string]interface{}{ + "key2": "value2", + }, + } + response, _ := json.Marshal(responsePayload) + fmt.Fprint(w, string(response)) + } else { + w.WriteHeader(http.StatusNotFound) + } + default: + w.WriteHeader(http.StatusNotImplemented) + } + }) + }) + + AfterEach(func() { + TeardownHTTP() + }) + + Context("success case: SetDiskMetadata", func() { + + It("sets disk metadata", func() { + setMetadataIsSuccessful = true + writeJsonParamToStdIn(`{ + "method":"set_disk_metadata", + "arguments": [ + "volume-id-ok", + { + "key2": "value2" + } + ], + "context": {}, + "api_version": 2 + }`) + err := cpi.Execute(getDefaultConfig(Endpoint()), logger) + Expect(err).ShouldNot(HaveOccurred()) + stdOutWriter.Close() + actual := <-outChannel + fmt.Printf("actual: %s\n", actual) + var response map[string]interface{} + err = json.Unmarshal([]byte(actual), &response) + Expect(err).ShouldNot(HaveOccurred()) + Expect(response).To(HaveKey("result")) + Expect(response["result"]).To(BeNil()) + Expect(response).To(HaveKey("error")) + Expect(response["error"]).To(BeNil()) + }) + }) + + Context("fail case: SetDiskMetadata", func() { + + It("fails to set disk metadata", func() { + setMetadataIsSuccessful = false + writeJsonParamToStdIn(`{ + "method":"set_disk_metadata", + "arguments": [ + "volume-id-ok", + { + "key2": "value2" + } + ], + "context": {}, + "api_version": 2 + }`) + err := cpi.Execute(getDefaultConfig(Endpoint()), logger) + Expect(err).ShouldNot(HaveOccurred()) + stdOutWriter.Close() + actual := <-outChannel + fmt.Printf("actual: %s\n", actual) + var response map[string]interface{} + err = json.Unmarshal([]byte(actual), &response) + Expect(err).ShouldNot(HaveOccurred()) + Expect(response).To(HaveKey("result")) + Expect(response["result"]).To(BeNil()) + Expect(response).To(HaveKey("error")) + Expect(response["error"]).NotTo(BeNil()) + }) + }) +}) diff --git a/src/openstack_cpi_golang/integration/set_vm_metadata_test.go b/src/openstack_cpi_golang/integration/set_vm_metadata_test.go new file mode 100644 index 00000000..39002233 --- /dev/null +++ b/src/openstack_cpi_golang/integration/set_vm_metadata_test.go @@ -0,0 +1,578 @@ +package integration_test + +import ( + "fmt" + "net/http" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("SET VM METADATA", func() { + + BeforeEach(func() { + SetupHTTP() + + Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + fmt.Fprintf(w, `{ + "versions": {"values": [ + {"status": "stable","id": "v3.0","links": [{ "href": "%s", "rel": "self" }]}, + {"status": "stable","id": "v2.0","links": [{ "href": "%s", "rel": "self" }]} + ]} + }`, Endpoint()+"/v3", Endpoint()+"/v2.0") + } + }) + + Mux.HandleFunc("/v3/auth/tokens", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodPost: + w.Header().Add("X-Subject-Token", "0123456789") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, `{ + "token": { + "expires_at": "2013-02-02T18:30:59.000000Z", + "catalog": [{ + "endpoints": [ + {"id": "1", "interface": "public", "region": "RegionOne", "url": "%s/v2.1"}, + {"id": "2", "interface": "admin", "region": "RegionOne", "url": "%s/v2.1"}, + {"id": "3", "interface": "internal", "region": "RegionOne", "url": "%s/v2.1"} + ], + "type": "compute", + "name": "nova" + }] + } + }`, Endpoint(), Endpoint(), Endpoint()) + } + }) + + }) + + AfterEach(func() { + TeardownHTTP() + }) + + Context("Positive cases: ", func() { + + BeforeEach(func() { + Mux.HandleFunc("/v2.1/servers/server-id", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, `{ + "server": { + "id": "server-id", + "name": "old-name", + "status": "ACTIVE" + } + }`) + case http.MethodPut: + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, `{ + "server": { + "id": "server-id", + "name": "old-name", + "status": "ACTIVE" + } + }`) + + } + }) + + Mux.HandleFunc("/v2.1/servers/server-id/metadata", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, `{ + "server": { + "id": "server-id", + "name": "old-name", + "status": "ACTIVE" + } + }`) + case http.MethodPost: + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, `{ + "server": { + "id": "server-id", + "name": "old-name", + "status": "ACTIVE" + } + }`) + } + }) + + Mux.HandleFunc("/v2.1/servers/server-id/metadata/name", func(w http.ResponseWriter, r *http.Request) { + + fmt.Printf("---Called: Method: %s URI: %s ", r.Method, r.URL) + + switch r.Method { + case http.MethodPut: + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, `{ + "server": { + "id": "server-id", + "name": "old-name", + "status": "ACTIVE" + } + }`) + } + }) + + Mux.HandleFunc("/v2.1/servers/server-id/metadata/job", func(w http.ResponseWriter, r *http.Request) { + + fmt.Printf("---Called: Method: %s URI: %s ", r.Method, r.URL) + + switch r.Method { + case http.MethodPut: + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, `{ + "server": { + "id": "server-id", + "name": "old-name", + "status": "ACTIVE" + } + }`) + } + }) + + Mux.HandleFunc("/v2.1/servers/server-id/metadata/index", func(w http.ResponseWriter, r *http.Request) { + + fmt.Printf("---Called: Method: %s URI: %s ", r.Method, r.URL) + + switch r.Method { + case http.MethodPut: + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, `{ + "server": { + "id": "server-id", + "name": "old-name", + "status": "ACTIVE" + } + }`) + } + }) + + Mux.HandleFunc("/v2.1/servers/server-id/metadata/compiling", func(w http.ResponseWriter, r *http.Request) { + + fmt.Printf("---Called: Method: %s URI: %s ", r.Method, r.URL) + + switch r.Method { + case http.MethodPut: + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, `{ + "server": { + "id": "server-id", + "name": "old-name", + "status": "ACTIVE" + } + }`) + } + }) + }) + + It("sets a new servername successfully base on importing parameter 'name'", func() { + writeJsonParamToStdIn(`{ + "method":"set_vm_metadata", + "arguments": [ + "server-id", + { + "name": "new-name" + } + ], + "api_version": 2 + + }`) + + config := getDefaultConfig(Endpoint()) + config.Cloud.Properties.Openstack.HumanReadableVMNames = true + config.Cloud.Properties.Openstack.VM.Stemcell.APIVersion = 2 + + err := cpi.Execute(config, logger) + Expect(err).ShouldNot(HaveOccurred()) + + stdOutWriter.Close() + Expect(<-outChannel).To(ContainSubstring(`"result":null,"error":null`)) + }) + + It("sets a new servername successfully base on importing parameter 'job/index'", func() { + writeJsonParamToStdIn(`{ + "method":"set_vm_metadata", + "arguments": [ + "server-id", + { + "job": "new-job", + "index": "new-index" + } + ], + "api_version": 2 + + }`) + + config := getDefaultConfig(Endpoint()) + config.Cloud.Properties.Openstack.HumanReadableVMNames = true + config.Cloud.Properties.Openstack.VM.Stemcell.APIVersion = 2 + + err := cpi.Execute(config, logger) + Expect(err).ShouldNot(HaveOccurred()) + + stdOutWriter.Close() + Expect(<-outChannel).To(ContainSubstring(`"result":null,"error":null`)) + }) + + It("sets a new servername successfully base on importing parameter 'compiling'", func() { + writeJsonParamToStdIn(`{ + "method":"set_vm_metadata", + "arguments": [ + "server-id", + { + "compiling": "new-compiling" + } + ], + "api_version": 2 + + }`) + + config := getDefaultConfig(Endpoint()) + config.Cloud.Properties.Openstack.HumanReadableVMNames = true + config.Cloud.Properties.Openstack.VM.Stemcell.APIVersion = 2 + + err := cpi.Execute(config, logger) + Expect(err).ShouldNot(HaveOccurred()) + + stdOutWriter.Close() + Expect(<-outChannel).To(ContainSubstring(`"result":null,"error":null`)) + }) + + It("sets a new servername successfully base on importing parameter 'name' regardless of job/index/compiling", func() { + writeJsonParamToStdIn(`{ + "method":"set_vm_metadata", + "arguments": [ + "server-id", + { + "name": "new-name", + "job": "new-job", + "index": "new-index", + "compiling": "new-compiling" + } + ], + "api_version": 2 + }`) + + config := getDefaultConfig(Endpoint()) + config.Cloud.Properties.Openstack.HumanReadableVMNames = true + config.Cloud.Properties.Openstack.VM.Stemcell.APIVersion = 2 + + err := cpi.Execute(config, logger) + Expect(err).ShouldNot(HaveOccurred()) + + stdOutWriter.Close() + Expect(<-outChannel).To(ContainSubstring(`"result":null,"error":null`)) + }) + + It("removes a key when one key map has null value", func() { + writeJsonParamToStdIn(`{ + "method":"set_vm_metadata", + "arguments": [ + "server-id", + { + "name": "new-name", + "test": null + } + ], + "api_version": 2 + + }`) + + config := getDefaultConfig(Endpoint()) + config.Cloud.Properties.Openstack.HumanReadableVMNames = true + config.Cloud.Properties.Openstack.VM.Stemcell.APIVersion = 2 + + err := cpi.Execute(config, logger) + Expect(err).ShouldNot(HaveOccurred()) + + stdOutWriter.Close() + Expect(<-outChannel).To(ContainSubstring(`"result":null,"error":null`)) + }) + }) + + Context("Failure in GetMetadata: ", func() { + + BeforeEach(func() { + Mux.HandleFunc("/v2.1/servers/server", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, `{ + "server": { + "ID": "get_meta_not_found", + "name": "name" + } + }`) + } + }) + + Mux.HandleFunc("/v2.1/servers/get_meta_not_found/metadata/Name", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodPut: + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, `{ + "server": { + "ID": "get_meta_not_found", + "name": "Name" + } + }`) + } + }) + + Mux.HandleFunc("/v2.1/servers/get_meta_not_found/metadata/ID", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodPut: + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, `{ + "server": { + "ID": "get_meta_not_found", + "name": "Name" + } + }`) + } + }) + + Mux.HandleFunc("/v2.1/servers/server/metadata", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + w.WriteHeader(http.StatusUnauthorized) + fmt.Fprintf(w, `{ + "server": { + "ID": "get_meta_not_found", + "name": "name" + } + }`) + } + }) + + }) + + It("returns error if getter server meta fails ", func() { + writeJsonParamToStdIn(`{ + "method":"set_vm_metadata", + "arguments": [ + "server", + { + "ID": "get_meta_not_found", + "Name": "new-name" + } + ], + "api_version": 2 + }`) + + err := cpi.Execute(getDefaultConfig(Endpoint()), logger) + Expect(err).ShouldNot(HaveOccurred()) + + stdOutWriter.Close() + Expect(<-outChannel).To(ContainSubstring(`"message":"failed to get Metadata`)) + + }) + }) + + Context("Failure in DeleteServerMetaData: ", func() { + + BeforeEach(func() { + + Mux.HandleFunc("/v2.1/servers/server/metadata", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, `{ + "metadata": { + "foo": "foo_value", + "lbaas_pool_1": "pool_id_1/member_id_1", + "lbaas_pool_2": "pool_id_2/member_id_not_existing", + "test": "test" + } + }`) + + case http.MethodPost: + w.WriteHeader(http.StatusBadRequest) + fmt.Fprintf(w, `{ + "server": { + "ID": "get_meta", + "name": "name" + } + }`) + } + }) + + }) + + It("returns error if delete server meta fails", func() { + writeJsonParamToStdIn(`{ + "method":"set_vm_metadata", + "arguments": [ + "server", + { + "id": "delete_meta_not_possible", + "name": "new-name", + "test": "test" + } + ], + "api_version": 2 + }`) + + err := cpi.Execute(getDefaultConfig(Endpoint()), logger) + Expect(err).ShouldNot(HaveOccurred()) + + stdOutWriter.Close() + Expect(<-outChannel).To(ContainSubstring(`"failed to delete Metadata`)) + + }) + + }) + + Context("Failure in UpdateServerMetadata: ", func() { + + BeforeEach(func() { + + Mux.HandleFunc("/v2.1/servers/server/metadata", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, `{ + "metadata": { + "foo": "foo_value", + "lbaas_pool_1": "pool_id_1/member_id_1", + "lbaas_pool_2": "pool_id_2/member_id_not_existing", + "test": "test" + } + }`) + + case http.MethodPost: + w.WriteHeader(http.StatusBadRequest) + } + }) + + }) + + It("returns error if delete server meta fails", func() { + writeJsonParamToStdIn(`{ + "method":"set_vm_metadata", + "arguments": [ + "server", + { + "name": "new-name", + "id": "update_failed" + } + ], + "api_version": 2 + + }`) + + config := getDefaultConfig(Endpoint()) + config.Cloud.Properties.Openstack.HumanReadableVMNames = true + config.Cloud.Properties.Openstack.VM.Stemcell.APIVersion = 2 + + err := cpi.Execute(config, logger) + Expect(err).ShouldNot(HaveOccurred()) + + stdOutWriter.Close() + Expect(<-outChannel).To(ContainSubstring(`"message":"failed to update`)) + }) + + }) + + Context("Failure in UpdateServer: ", func() { + + BeforeEach(func() { + Mux.HandleFunc("/v2.1/servers/server", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, `{ + "server": { + "id": "update_failed", + "name": "name" + } + }`) + } + }) + + Mux.HandleFunc("/v2.1/servers/update_failed/metadata/name", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodPut: + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, `{ + "server": { + "ID": "update_failed", + "name": "Name" + } + }`) + } + }) + + Mux.HandleFunc("/v2.1/servers/update_failed/metadata/id", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodPut: + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, `{ + "server": { + "id": "get_meta_not_found", + "name": "Name" + } + }`) + } + }) + + Mux.HandleFunc("/v2.1/servers/server/metadata", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, `{ + "server": { + "id": "update_failed", + "name": "name" + } + }`) + case http.MethodPost: + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, `{ + "server": { + "id": "update_failed", + "name": "name" + } + }`) + } + }) + + }) + + It("failed to update server name with importing 'name'", func() { + writeJsonParamToStdIn(`{ + "method":"set_vm_metadata", + "arguments": [ + "server", + { + "name": "new-name", + "id": "update_failed" + } + ], + "api_version": 2 + + }`) + + config := getDefaultConfig(Endpoint()) + config.Cloud.Properties.Openstack.HumanReadableVMNames = true + config.Cloud.Properties.Openstack.VM.Stemcell.APIVersion = 2 + + err := cpi.Execute(config, logger) + Expect(err).ShouldNot(HaveOccurred()) + + stdOutWriter.Close() + Expect(<-outChannel).To(ContainSubstring(`"message":"failed to update`)) + }) + + }) + +}) diff --git a/src/openstack_cpi_golang/integration/snapshot_disk_test.go b/src/openstack_cpi_golang/integration/snapshot_disk_test.go new file mode 100644 index 00000000..e50c86bb --- /dev/null +++ b/src/openstack_cpi_golang/integration/snapshot_disk_test.go @@ -0,0 +1,320 @@ +package integration_test + +import ( + "fmt" + "net/http" + + "github.com/cloudfoundry/bosh-openstack-cpi-release/src/openstack_cpi_golang/cpi" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("SNAPSHOT DISK", func() { + + BeforeEach(func() { + SetupHTTP() + MockAuthentication() + }) + + AfterEach(func() { + TeardownHTTP() + }) + + Context("Positive cases: ", func() { + + BeforeEach(func() { + Mux.HandleFunc("/v3/volumes/volume-id", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + w.WriteHeader(http.StatusOK) + _, _ = fmt.Fprintf(w, `{ + "volume": { + "id": "volume-id", + "name": "old-name", + "status": "ACTIVE", + "attachments": [{ + "device": "dev1/dev2/dev3", + "server_id": "server-id", + "volume_id": "volume-id" + }] + } + }`) + } + }) + + Mux.HandleFunc("/v3/snapshots", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodPost: + w.WriteHeader(http.StatusAccepted) + _, _ = fmt.Fprintf(w, `{ + "snapshot": { + "id": "snapshot-id", + "force": true, + "name": "snapshot-name", + "description": "deployment/job/1/dev3", + "metadata": { + "deployment": "deployment", + "job": "job", + "index": "1", + "test": "test", + "director_name": "director_name", + "instance_id": "instance_id" + } + } + }`) + } + }) + + Mux.HandleFunc("/v3/snapshots/snapshot-id", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + w.WriteHeader(http.StatusOK) + _, _ = fmt.Fprintf(w, `{ + "snapshot": { + "id": "snapshot-id", + "name": "snapshot-name", + "description": "deployment/job/1/dev3", + "STATUS": "available", + "metadata": { + "deployment": "deployment", + "job": "job", + "index": "1", + "test": "test", + "director_name": "director_name", + "instance_id": "instance_id" + } + } + }`) + } + }) + + Mux.HandleFunc("/v3/snapshots/snapshot-id/metadata", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodPut: + w.WriteHeader(http.StatusOK) + _, _ = fmt.Fprintf(w, `{ + "snapShotID": "snapshot-id", + "metadata": { + "deployment": "deployment", + "test": "test", + "instance_id": "instance_id", + "director": "director_name", + "instance_index": "1", + "instance_name": "job/instance_id" + } + }`) + } + }) + }) + + It("creates a new snapshot successfully", func() { + writeJsonParamToStdIn(`{ + "method":"snapshot_disk", + "arguments": [ + "volume-id", + { + "deployment": "deployment", + "job": "job", + "index": 1, + "test": "test", + "director_name": "director_name", + "instance_id": "instance_id" + } + ] + }`) + config := getDefaultConfig(Endpoint()) + err := cpi.Execute(config, logger) + Expect(err).ShouldNot(HaveOccurred()) + + _ = stdOutWriter.Close() + Expect(<-outChannel).To(ContainSubstring(`"result":"snapshot-id","error":null`)) + }) + }) + + Context("Failure in GetVolume: ", func() { + + BeforeEach(func() { + + Mux.HandleFunc("/v3/volumes/volume-id", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + w.WriteHeader(http.StatusUnauthorized) + _, _ = fmt.Fprintf(w, `{ + "volume": { + "id": "volume-id", + "name": "old-name", + "status": "ACTIVE", + "attachments": [{ + "device": "dev1/dev2/dev3", + "server_id": "server-id", + "volume_id": "volume-id" + }] + } + }`) + } + }) + + }) + + It("returns error if get volume fails", func() { + writeJsonParamToStdIn(`{ + "method":"snapshot_disk", + "arguments": [ + "volume-id", + { + "deployment": "deployment", + "job": "job", + "index": 1, + "test": "test", + "director_name": "director_name", + "instance_id": "instance_id" + } + ] + }`) + config := getDefaultConfig(Endpoint()) + err := cpi.Execute(config, logger) + Expect(err).ShouldNot(HaveOccurred()) + + _ = stdOutWriter.Close() + Expect(<-outChannel).To(ContainSubstring(`"message":"snapShotDisk: Failed to get volume ID`)) + }) + + }) + + Context("Failure in CreateSnapshot: ", func() { + + BeforeEach(func() { + + Mux.HandleFunc("/v3/volumes/volume-id", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + w.WriteHeader(http.StatusOK) + _, _ = fmt.Fprintf(w, `{ + "volume": { + "id": "volume-id", + "name": "old-name", + "status": "ACTIVE", + "attachments": [{ + "device": "dev1/dev2/dev3", + "server_id": "server-id", + "volume_id": "volume-id" + }] + } + }`) + } + }) + + Mux.HandleFunc("/v3/snapshots", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodPost: + w.WriteHeader(http.StatusBadRequest) + } + }) + + }) + + It("returns error if create snapshot fails", func() { + writeJsonParamToStdIn(`{ + "method":"snapshot_disk", + "arguments": [ + "volume-id", + { + "deployment": "deployment", + "job": "job", + "index": 1, + "test": "test", + "director_name": "director_name", + "instance_id": "instance_id" + } + ] + }`) + config := getDefaultConfig(Endpoint()) + err := cpi.Execute(config, logger) + Expect(err).ShouldNot(HaveOccurred()) + + _ = stdOutWriter.Close() + Expect(<-outChannel).To(ContainSubstring(`"message":"snapShotDisk: Failed to create snapshot snapshot-`)) + }) + + }) + + Context("Failure in WaitForSnapshotToBecomeStatus: ", func() { + + BeforeEach(func() { + + Mux.HandleFunc("/v3/volumes/volume-id", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + w.WriteHeader(http.StatusOK) + _, _ = fmt.Fprintf(w, `{ + "volume": { + "id": "volume-id", + "name": "old-name", + "status": "ACTIVE", + "attachments": [{ + "device": "dev1/dev2/dev3", + "server_id": "server-id", + "volume_id": "volume-id" + }] + } + }`) + } + }) + + Mux.HandleFunc("/v3/snapshots", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodPost: + w.WriteHeader(http.StatusAccepted) + _, _ = fmt.Fprintf(w, `{ + "snapshot": { + "id": "snapshot-id", + "force": true, + "name": "snapshot-name", + "description": "deployment/job/1/dev3", + "metadata": { + "deployment": "deployment", + "job": "job", + "index": "1", + "test": "test", + "director_name": "director_name", + "instance_id": "instance_id" + } + } + }`) + } + }) + + Mux.HandleFunc("/v3/snapshots/snapshot-id", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + w.WriteHeader(http.StatusUnauthorized) + } + }) + }) + + It("returns error if wait for snapshot fails", func() { + writeJsonParamToStdIn(`{ + "method":"snapshot_disk", + "arguments": [ + "volume-id", + { + "deployment": "deployment", + "job": "job", + "index": 1, + "test": "test", + "director_name": "director_name", + "instance_id": "instance_id" + } + ] + }`) + config := getDefaultConfig(Endpoint()) + err := cpi.Execute(config, logger) + Expect(err).ShouldNot(HaveOccurred()) + + _ = stdOutWriter.Close() + Expect(<-outChannel).To(ContainSubstring(`"message":"snapShotDisk: Failed while waiting for creating snapshot snapshot-`)) + }) + + }) + +}) diff --git a/src/openstack_cpi_golang/integration/testdata/image b/src/openstack_cpi_golang/integration/testdata/image new file mode 100644 index 0000000000000000000000000000000000000000..a40e6f7ab09c8217960db3303cbbe4a31766b9bb GIT binary patch literal 352 zcmV-m0iXUKiwFSSsw-vy1JjEy%Fi#+%gjw@pcybQFfcPQQ2^2AW~N};zzD)OfUu1f z3{A`ojm(Wq%uUP{41g?iGX@0%+BpdUN{dSpi-1mXF)%SPG&aVm4yY_G4UahxuPDIj zp#oUN*cq4@7GC?|K05Lb35=Ml$ zXmWn8USdH(PO4r(QGQu!USeKyDoDYdyqRiCfAhVef+t4-E)bAd;gOn{l3JusyK;bF z{x>v0%l{^(MkWjj71VbMCK%!Q-`GGQw2Ll6rX1%_Ov<_bQZ zPL4s&9-d*YdKHN!B}IDJva79QprfO)yIGW1VT8F$X*rjPr9v=JlTRc*O-Om1YoNnn yqypO4{)F>CFu$YM|E8vRB!U=)mk!36+$Gq(N!5C8yIfs%Cq literal 0 HcmV?d00001 diff --git a/src/openstack_cpi_golang/main.go b/src/openstack_cpi_golang/main.go index 8bb19b3c..8539ab22 100644 --- a/src/openstack_cpi_golang/main.go +++ b/src/openstack_cpi_golang/main.go @@ -12,6 +12,7 @@ import ( var ( configPathOpt = flag.String("configFile", "", "Path to configuration file") + caCertPathOpt = flag.String("caCert", "", "Path to ca cert file") //nolint:unused ) func main() { diff --git a/src/openstack_cpi_golang/vendor/github.com/google/uuid/CHANGELOG.md b/src/openstack_cpi_golang/vendor/github.com/google/uuid/CHANGELOG.md new file mode 100644 index 00000000..7ec5ac7e --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/google/uuid/CHANGELOG.md @@ -0,0 +1,41 @@ +# Changelog + +## [1.6.0](https://github.com/google/uuid/compare/v1.5.0...v1.6.0) (2024-01-16) + + +### Features + +* add Max UUID constant ([#149](https://github.com/google/uuid/issues/149)) ([c58770e](https://github.com/google/uuid/commit/c58770eb495f55fe2ced6284f93c5158a62e53e3)) + + +### Bug Fixes + +* fix typo in version 7 uuid documentation ([#153](https://github.com/google/uuid/issues/153)) ([016b199](https://github.com/google/uuid/commit/016b199544692f745ffc8867b914129ecb47ef06)) +* Monotonicity in UUIDv7 ([#150](https://github.com/google/uuid/issues/150)) ([a2b2b32](https://github.com/google/uuid/commit/a2b2b32373ff0b1a312b7fdf6d38a977099698a6)) + +## [1.5.0](https://github.com/google/uuid/compare/v1.4.0...v1.5.0) (2023-12-12) + + +### Features + +* Validate UUID without creating new UUID ([#141](https://github.com/google/uuid/issues/141)) ([9ee7366](https://github.com/google/uuid/commit/9ee7366e66c9ad96bab89139418a713dc584ae29)) + +## [1.4.0](https://github.com/google/uuid/compare/v1.3.1...v1.4.0) (2023-10-26) + + +### Features + +* UUIDs slice type with Strings() convenience method ([#133](https://github.com/google/uuid/issues/133)) ([cd5fbbd](https://github.com/google/uuid/commit/cd5fbbdd02f3e3467ac18940e07e062be1f864b4)) + +### Fixes + +* Clarify that Parse's job is to parse but not necessarily validate strings. (Documents current behavior) + +## [1.3.1](https://github.com/google/uuid/compare/v1.3.0...v1.3.1) (2023-08-18) + + +### Bug Fixes + +* Use .EqualFold() to parse urn prefixed UUIDs ([#118](https://github.com/google/uuid/issues/118)) ([574e687](https://github.com/google/uuid/commit/574e6874943741fb99d41764c705173ada5293f0)) + +## Changelog diff --git a/src/openstack_cpi_golang/vendor/github.com/google/uuid/CONTRIBUTING.md b/src/openstack_cpi_golang/vendor/github.com/google/uuid/CONTRIBUTING.md new file mode 100644 index 00000000..a502fdc5 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/google/uuid/CONTRIBUTING.md @@ -0,0 +1,26 @@ +# How to contribute + +We definitely welcome patches and contribution to this project! + +### Tips + +Commits must be formatted according to the [Conventional Commits Specification](https://www.conventionalcommits.org). + +Always try to include a test case! If it is not possible or not necessary, +please explain why in the pull request description. + +### Releasing + +Commits that would precipitate a SemVer change, as described in the Conventional +Commits Specification, will trigger [`release-please`](https://github.com/google-github-actions/release-please-action) +to create a release candidate pull request. Once submitted, `release-please` +will create a release. + +For tips on how to work with `release-please`, see its documentation. + +### Legal requirements + +In order to protect both you and ourselves, you will need to sign the +[Contributor License Agreement](https://cla.developers.google.com/clas). + +You may have already signed it for other Google projects. diff --git a/src/openstack_cpi_golang/vendor/github.com/google/uuid/CONTRIBUTORS b/src/openstack_cpi_golang/vendor/github.com/google/uuid/CONTRIBUTORS new file mode 100644 index 00000000..b4bb97f6 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/google/uuid/CONTRIBUTORS @@ -0,0 +1,9 @@ +Paul Borman +bmatsuo +shawnps +theory +jboverfelt +dsymonds +cd1 +wallclockbuilder +dansouza diff --git a/src/openstack_cpi_golang/vendor/github.com/google/uuid/LICENSE b/src/openstack_cpi_golang/vendor/github.com/google/uuid/LICENSE new file mode 100644 index 00000000..5dc68268 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/google/uuid/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2009,2014 Google Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/src/openstack_cpi_golang/vendor/github.com/google/uuid/README.md b/src/openstack_cpi_golang/vendor/github.com/google/uuid/README.md new file mode 100644 index 00000000..3e9a6188 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/google/uuid/README.md @@ -0,0 +1,21 @@ +# uuid +The uuid package generates and inspects UUIDs based on +[RFC 4122](https://datatracker.ietf.org/doc/html/rfc4122) +and DCE 1.1: Authentication and Security Services. + +This package is based on the github.com/pborman/uuid package (previously named +code.google.com/p/go-uuid). It differs from these earlier packages in that +a UUID is a 16 byte array rather than a byte slice. One loss due to this +change is the ability to represent an invalid UUID (vs a NIL UUID). + +###### Install +```sh +go get github.com/google/uuid +``` + +###### Documentation +[![Go Reference](https://pkg.go.dev/badge/github.com/google/uuid.svg)](https://pkg.go.dev/github.com/google/uuid) + +Full `go doc` style documentation for the package can be viewed online without +installing this package by using the GoDoc site here: +http://pkg.go.dev/github.com/google/uuid diff --git a/src/openstack_cpi_golang/vendor/github.com/google/uuid/dce.go b/src/openstack_cpi_golang/vendor/github.com/google/uuid/dce.go new file mode 100644 index 00000000..fa820b9d --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/google/uuid/dce.go @@ -0,0 +1,80 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "encoding/binary" + "fmt" + "os" +) + +// A Domain represents a Version 2 domain +type Domain byte + +// Domain constants for DCE Security (Version 2) UUIDs. +const ( + Person = Domain(0) + Group = Domain(1) + Org = Domain(2) +) + +// NewDCESecurity returns a DCE Security (Version 2) UUID. +// +// The domain should be one of Person, Group or Org. +// On a POSIX system the id should be the users UID for the Person +// domain and the users GID for the Group. The meaning of id for +// the domain Org or on non-POSIX systems is site defined. +// +// For a given domain/id pair the same token may be returned for up to +// 7 minutes and 10 seconds. +func NewDCESecurity(domain Domain, id uint32) (UUID, error) { + uuid, err := NewUUID() + if err == nil { + uuid[6] = (uuid[6] & 0x0f) | 0x20 // Version 2 + uuid[9] = byte(domain) + binary.BigEndian.PutUint32(uuid[0:], id) + } + return uuid, err +} + +// NewDCEPerson returns a DCE Security (Version 2) UUID in the person +// domain with the id returned by os.Getuid. +// +// NewDCESecurity(Person, uint32(os.Getuid())) +func NewDCEPerson() (UUID, error) { + return NewDCESecurity(Person, uint32(os.Getuid())) +} + +// NewDCEGroup returns a DCE Security (Version 2) UUID in the group +// domain with the id returned by os.Getgid. +// +// NewDCESecurity(Group, uint32(os.Getgid())) +func NewDCEGroup() (UUID, error) { + return NewDCESecurity(Group, uint32(os.Getgid())) +} + +// Domain returns the domain for a Version 2 UUID. Domains are only defined +// for Version 2 UUIDs. +func (uuid UUID) Domain() Domain { + return Domain(uuid[9]) +} + +// ID returns the id for a Version 2 UUID. IDs are only defined for Version 2 +// UUIDs. +func (uuid UUID) ID() uint32 { + return binary.BigEndian.Uint32(uuid[0:4]) +} + +func (d Domain) String() string { + switch d { + case Person: + return "Person" + case Group: + return "Group" + case Org: + return "Org" + } + return fmt.Sprintf("Domain%d", int(d)) +} diff --git a/src/openstack_cpi_golang/vendor/github.com/google/uuid/doc.go b/src/openstack_cpi_golang/vendor/github.com/google/uuid/doc.go new file mode 100644 index 00000000..5b8a4b9a --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/google/uuid/doc.go @@ -0,0 +1,12 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package uuid generates and inspects UUIDs. +// +// UUIDs are based on RFC 4122 and DCE 1.1: Authentication and Security +// Services. +// +// A UUID is a 16 byte (128 bit) array. UUIDs may be used as keys to +// maps or compared directly. +package uuid diff --git a/src/openstack_cpi_golang/vendor/github.com/google/uuid/hash.go b/src/openstack_cpi_golang/vendor/github.com/google/uuid/hash.go new file mode 100644 index 00000000..dc60082d --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/google/uuid/hash.go @@ -0,0 +1,59 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "crypto/md5" + "crypto/sha1" + "hash" +) + +// Well known namespace IDs and UUIDs +var ( + NameSpaceDNS = Must(Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c8")) + NameSpaceURL = Must(Parse("6ba7b811-9dad-11d1-80b4-00c04fd430c8")) + NameSpaceOID = Must(Parse("6ba7b812-9dad-11d1-80b4-00c04fd430c8")) + NameSpaceX500 = Must(Parse("6ba7b814-9dad-11d1-80b4-00c04fd430c8")) + Nil UUID // empty UUID, all zeros + + // The Max UUID is special form of UUID that is specified to have all 128 bits set to 1. + Max = UUID{ + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + } +) + +// NewHash returns a new UUID derived from the hash of space concatenated with +// data generated by h. The hash should be at least 16 byte in length. The +// first 16 bytes of the hash are used to form the UUID. The version of the +// UUID will be the lower 4 bits of version. NewHash is used to implement +// NewMD5 and NewSHA1. +func NewHash(h hash.Hash, space UUID, data []byte, version int) UUID { + h.Reset() + h.Write(space[:]) //nolint:errcheck + h.Write(data) //nolint:errcheck + s := h.Sum(nil) + var uuid UUID + copy(uuid[:], s) + uuid[6] = (uuid[6] & 0x0f) | uint8((version&0xf)<<4) + uuid[8] = (uuid[8] & 0x3f) | 0x80 // RFC 4122 variant + return uuid +} + +// NewMD5 returns a new MD5 (Version 3) UUID based on the +// supplied name space and data. It is the same as calling: +// +// NewHash(md5.New(), space, data, 3) +func NewMD5(space UUID, data []byte) UUID { + return NewHash(md5.New(), space, data, 3) +} + +// NewSHA1 returns a new SHA1 (Version 5) UUID based on the +// supplied name space and data. It is the same as calling: +// +// NewHash(sha1.New(), space, data, 5) +func NewSHA1(space UUID, data []byte) UUID { + return NewHash(sha1.New(), space, data, 5) +} diff --git a/src/openstack_cpi_golang/vendor/github.com/google/uuid/marshal.go b/src/openstack_cpi_golang/vendor/github.com/google/uuid/marshal.go new file mode 100644 index 00000000..14bd3407 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/google/uuid/marshal.go @@ -0,0 +1,38 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import "fmt" + +// MarshalText implements encoding.TextMarshaler. +func (uuid UUID) MarshalText() ([]byte, error) { + var js [36]byte + encodeHex(js[:], uuid) + return js[:], nil +} + +// UnmarshalText implements encoding.TextUnmarshaler. +func (uuid *UUID) UnmarshalText(data []byte) error { + id, err := ParseBytes(data) + if err != nil { + return err + } + *uuid = id + return nil +} + +// MarshalBinary implements encoding.BinaryMarshaler. +func (uuid UUID) MarshalBinary() ([]byte, error) { + return uuid[:], nil +} + +// UnmarshalBinary implements encoding.BinaryUnmarshaler. +func (uuid *UUID) UnmarshalBinary(data []byte) error { + if len(data) != 16 { + return fmt.Errorf("invalid UUID (got %d bytes)", len(data)) + } + copy(uuid[:], data) + return nil +} diff --git a/src/openstack_cpi_golang/vendor/github.com/google/uuid/node.go b/src/openstack_cpi_golang/vendor/github.com/google/uuid/node.go new file mode 100644 index 00000000..d651a2b0 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/google/uuid/node.go @@ -0,0 +1,90 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "sync" +) + +var ( + nodeMu sync.Mutex + ifname string // name of interface being used + nodeID [6]byte // hardware for version 1 UUIDs + zeroID [6]byte // nodeID with only 0's +) + +// NodeInterface returns the name of the interface from which the NodeID was +// derived. The interface "user" is returned if the NodeID was set by +// SetNodeID. +func NodeInterface() string { + defer nodeMu.Unlock() + nodeMu.Lock() + return ifname +} + +// SetNodeInterface selects the hardware address to be used for Version 1 UUIDs. +// If name is "" then the first usable interface found will be used or a random +// Node ID will be generated. If a named interface cannot be found then false +// is returned. +// +// SetNodeInterface never fails when name is "". +func SetNodeInterface(name string) bool { + defer nodeMu.Unlock() + nodeMu.Lock() + return setNodeInterface(name) +} + +func setNodeInterface(name string) bool { + iname, addr := getHardwareInterface(name) // null implementation for js + if iname != "" && addr != nil { + ifname = iname + copy(nodeID[:], addr) + return true + } + + // We found no interfaces with a valid hardware address. If name + // does not specify a specific interface generate a random Node ID + // (section 4.1.6) + if name == "" { + ifname = "random" + randomBits(nodeID[:]) + return true + } + return false +} + +// NodeID returns a slice of a copy of the current Node ID, setting the Node ID +// if not already set. +func NodeID() []byte { + defer nodeMu.Unlock() + nodeMu.Lock() + if nodeID == zeroID { + setNodeInterface("") + } + nid := nodeID + return nid[:] +} + +// SetNodeID sets the Node ID to be used for Version 1 UUIDs. The first 6 bytes +// of id are used. If id is less than 6 bytes then false is returned and the +// Node ID is not set. +func SetNodeID(id []byte) bool { + if len(id) < 6 { + return false + } + defer nodeMu.Unlock() + nodeMu.Lock() + copy(nodeID[:], id) + ifname = "user" + return true +} + +// NodeID returns the 6 byte node id encoded in uuid. It returns nil if uuid is +// not valid. The NodeID is only well defined for version 1 and 2 UUIDs. +func (uuid UUID) NodeID() []byte { + var node [6]byte + copy(node[:], uuid[10:]) + return node[:] +} diff --git a/src/openstack_cpi_golang/vendor/github.com/google/uuid/node_js.go b/src/openstack_cpi_golang/vendor/github.com/google/uuid/node_js.go new file mode 100644 index 00000000..b2a0bc87 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/google/uuid/node_js.go @@ -0,0 +1,12 @@ +// Copyright 2017 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build js + +package uuid + +// getHardwareInterface returns nil values for the JS version of the code. +// This removes the "net" dependency, because it is not used in the browser. +// Using the "net" library inflates the size of the transpiled JS code by 673k bytes. +func getHardwareInterface(name string) (string, []byte) { return "", nil } diff --git a/src/openstack_cpi_golang/vendor/github.com/google/uuid/node_net.go b/src/openstack_cpi_golang/vendor/github.com/google/uuid/node_net.go new file mode 100644 index 00000000..0cbbcddb --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/google/uuid/node_net.go @@ -0,0 +1,33 @@ +// Copyright 2017 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build !js + +package uuid + +import "net" + +var interfaces []net.Interface // cached list of interfaces + +// getHardwareInterface returns the name and hardware address of interface name. +// If name is "" then the name and hardware address of one of the system's +// interfaces is returned. If no interfaces are found (name does not exist or +// there are no interfaces) then "", nil is returned. +// +// Only addresses of at least 6 bytes are returned. +func getHardwareInterface(name string) (string, []byte) { + if interfaces == nil { + var err error + interfaces, err = net.Interfaces() + if err != nil { + return "", nil + } + } + for _, ifs := range interfaces { + if len(ifs.HardwareAddr) >= 6 && (name == "" || name == ifs.Name) { + return ifs.Name, ifs.HardwareAddr + } + } + return "", nil +} diff --git a/src/openstack_cpi_golang/vendor/github.com/google/uuid/null.go b/src/openstack_cpi_golang/vendor/github.com/google/uuid/null.go new file mode 100644 index 00000000..d7fcbf28 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/google/uuid/null.go @@ -0,0 +1,118 @@ +// Copyright 2021 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "bytes" + "database/sql/driver" + "encoding/json" + "fmt" +) + +var jsonNull = []byte("null") + +// NullUUID represents a UUID that may be null. +// NullUUID implements the SQL driver.Scanner interface so +// it can be used as a scan destination: +// +// var u uuid.NullUUID +// err := db.QueryRow("SELECT name FROM foo WHERE id=?", id).Scan(&u) +// ... +// if u.Valid { +// // use u.UUID +// } else { +// // NULL value +// } +// +type NullUUID struct { + UUID UUID + Valid bool // Valid is true if UUID is not NULL +} + +// Scan implements the SQL driver.Scanner interface. +func (nu *NullUUID) Scan(value interface{}) error { + if value == nil { + nu.UUID, nu.Valid = Nil, false + return nil + } + + err := nu.UUID.Scan(value) + if err != nil { + nu.Valid = false + return err + } + + nu.Valid = true + return nil +} + +// Value implements the driver Valuer interface. +func (nu NullUUID) Value() (driver.Value, error) { + if !nu.Valid { + return nil, nil + } + // Delegate to UUID Value function + return nu.UUID.Value() +} + +// MarshalBinary implements encoding.BinaryMarshaler. +func (nu NullUUID) MarshalBinary() ([]byte, error) { + if nu.Valid { + return nu.UUID[:], nil + } + + return []byte(nil), nil +} + +// UnmarshalBinary implements encoding.BinaryUnmarshaler. +func (nu *NullUUID) UnmarshalBinary(data []byte) error { + if len(data) != 16 { + return fmt.Errorf("invalid UUID (got %d bytes)", len(data)) + } + copy(nu.UUID[:], data) + nu.Valid = true + return nil +} + +// MarshalText implements encoding.TextMarshaler. +func (nu NullUUID) MarshalText() ([]byte, error) { + if nu.Valid { + return nu.UUID.MarshalText() + } + + return jsonNull, nil +} + +// UnmarshalText implements encoding.TextUnmarshaler. +func (nu *NullUUID) UnmarshalText(data []byte) error { + id, err := ParseBytes(data) + if err != nil { + nu.Valid = false + return err + } + nu.UUID = id + nu.Valid = true + return nil +} + +// MarshalJSON implements json.Marshaler. +func (nu NullUUID) MarshalJSON() ([]byte, error) { + if nu.Valid { + return json.Marshal(nu.UUID) + } + + return jsonNull, nil +} + +// UnmarshalJSON implements json.Unmarshaler. +func (nu *NullUUID) UnmarshalJSON(data []byte) error { + if bytes.Equal(data, jsonNull) { + *nu = NullUUID{} + return nil // valid null UUID + } + err := json.Unmarshal(data, &nu.UUID) + nu.Valid = err == nil + return err +} diff --git a/src/openstack_cpi_golang/vendor/github.com/google/uuid/sql.go b/src/openstack_cpi_golang/vendor/github.com/google/uuid/sql.go new file mode 100644 index 00000000..2e02ec06 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/google/uuid/sql.go @@ -0,0 +1,59 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "database/sql/driver" + "fmt" +) + +// Scan implements sql.Scanner so UUIDs can be read from databases transparently. +// Currently, database types that map to string and []byte are supported. Please +// consult database-specific driver documentation for matching types. +func (uuid *UUID) Scan(src interface{}) error { + switch src := src.(type) { + case nil: + return nil + + case string: + // if an empty UUID comes from a table, we return a null UUID + if src == "" { + return nil + } + + // see Parse for required string format + u, err := Parse(src) + if err != nil { + return fmt.Errorf("Scan: %v", err) + } + + *uuid = u + + case []byte: + // if an empty UUID comes from a table, we return a null UUID + if len(src) == 0 { + return nil + } + + // assumes a simple slice of bytes if 16 bytes + // otherwise attempts to parse + if len(src) != 16 { + return uuid.Scan(string(src)) + } + copy((*uuid)[:], src) + + default: + return fmt.Errorf("Scan: unable to scan type %T into UUID", src) + } + + return nil +} + +// Value implements sql.Valuer so that UUIDs can be written to databases +// transparently. Currently, UUIDs map to strings. Please consult +// database-specific driver documentation for matching types. +func (uuid UUID) Value() (driver.Value, error) { + return uuid.String(), nil +} diff --git a/src/openstack_cpi_golang/vendor/github.com/google/uuid/time.go b/src/openstack_cpi_golang/vendor/github.com/google/uuid/time.go new file mode 100644 index 00000000..c3511292 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/google/uuid/time.go @@ -0,0 +1,134 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "encoding/binary" + "sync" + "time" +) + +// A Time represents a time as the number of 100's of nanoseconds since 15 Oct +// 1582. +type Time int64 + +const ( + lillian = 2299160 // Julian day of 15 Oct 1582 + unix = 2440587 // Julian day of 1 Jan 1970 + epoch = unix - lillian // Days between epochs + g1582 = epoch * 86400 // seconds between epochs + g1582ns100 = g1582 * 10000000 // 100s of a nanoseconds between epochs +) + +var ( + timeMu sync.Mutex + lasttime uint64 // last time we returned + clockSeq uint16 // clock sequence for this run + + timeNow = time.Now // for testing +) + +// UnixTime converts t the number of seconds and nanoseconds using the Unix +// epoch of 1 Jan 1970. +func (t Time) UnixTime() (sec, nsec int64) { + sec = int64(t - g1582ns100) + nsec = (sec % 10000000) * 100 + sec /= 10000000 + return sec, nsec +} + +// GetTime returns the current Time (100s of nanoseconds since 15 Oct 1582) and +// clock sequence as well as adjusting the clock sequence as needed. An error +// is returned if the current time cannot be determined. +func GetTime() (Time, uint16, error) { + defer timeMu.Unlock() + timeMu.Lock() + return getTime() +} + +func getTime() (Time, uint16, error) { + t := timeNow() + + // If we don't have a clock sequence already, set one. + if clockSeq == 0 { + setClockSequence(-1) + } + now := uint64(t.UnixNano()/100) + g1582ns100 + + // If time has gone backwards with this clock sequence then we + // increment the clock sequence + if now <= lasttime { + clockSeq = ((clockSeq + 1) & 0x3fff) | 0x8000 + } + lasttime = now + return Time(now), clockSeq, nil +} + +// ClockSequence returns the current clock sequence, generating one if not +// already set. The clock sequence is only used for Version 1 UUIDs. +// +// The uuid package does not use global static storage for the clock sequence or +// the last time a UUID was generated. Unless SetClockSequence is used, a new +// random clock sequence is generated the first time a clock sequence is +// requested by ClockSequence, GetTime, or NewUUID. (section 4.2.1.1) +func ClockSequence() int { + defer timeMu.Unlock() + timeMu.Lock() + return clockSequence() +} + +func clockSequence() int { + if clockSeq == 0 { + setClockSequence(-1) + } + return int(clockSeq & 0x3fff) +} + +// SetClockSequence sets the clock sequence to the lower 14 bits of seq. Setting to +// -1 causes a new sequence to be generated. +func SetClockSequence(seq int) { + defer timeMu.Unlock() + timeMu.Lock() + setClockSequence(seq) +} + +func setClockSequence(seq int) { + if seq == -1 { + var b [2]byte + randomBits(b[:]) // clock sequence + seq = int(b[0])<<8 | int(b[1]) + } + oldSeq := clockSeq + clockSeq = uint16(seq&0x3fff) | 0x8000 // Set our variant + if oldSeq != clockSeq { + lasttime = 0 + } +} + +// Time returns the time in 100s of nanoseconds since 15 Oct 1582 encoded in +// uuid. The time is only defined for version 1, 2, 6 and 7 UUIDs. +func (uuid UUID) Time() Time { + var t Time + switch uuid.Version() { + case 6: + time := binary.BigEndian.Uint64(uuid[:8]) // Ignore uuid[6] version b0110 + t = Time(time) + case 7: + time := binary.BigEndian.Uint64(uuid[:8]) + t = Time((time>>16)*10000 + g1582ns100) + default: // forward compatible + time := int64(binary.BigEndian.Uint32(uuid[0:4])) + time |= int64(binary.BigEndian.Uint16(uuid[4:6])) << 32 + time |= int64(binary.BigEndian.Uint16(uuid[6:8])&0xfff) << 48 + t = Time(time) + } + return t +} + +// ClockSequence returns the clock sequence encoded in uuid. +// The clock sequence is only well defined for version 1 and 2 UUIDs. +func (uuid UUID) ClockSequence() int { + return int(binary.BigEndian.Uint16(uuid[8:10])) & 0x3fff +} diff --git a/src/openstack_cpi_golang/vendor/github.com/google/uuid/util.go b/src/openstack_cpi_golang/vendor/github.com/google/uuid/util.go new file mode 100644 index 00000000..5ea6c737 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/google/uuid/util.go @@ -0,0 +1,43 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "io" +) + +// randomBits completely fills slice b with random data. +func randomBits(b []byte) { + if _, err := io.ReadFull(rander, b); err != nil { + panic(err.Error()) // rand should never fail + } +} + +// xvalues returns the value of a byte as a hexadecimal digit or 255. +var xvalues = [256]byte{ + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 255, 255, 255, 255, 255, 255, + 255, 10, 11, 12, 13, 14, 15, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 10, 11, 12, 13, 14, 15, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, +} + +// xtob converts hex characters x1 and x2 into a byte. +func xtob(x1, x2 byte) (byte, bool) { + b1 := xvalues[x1] + b2 := xvalues[x2] + return (b1 << 4) | b2, b1 != 255 && b2 != 255 +} diff --git a/src/openstack_cpi_golang/vendor/github.com/google/uuid/uuid.go b/src/openstack_cpi_golang/vendor/github.com/google/uuid/uuid.go new file mode 100644 index 00000000..5232b486 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/google/uuid/uuid.go @@ -0,0 +1,365 @@ +// Copyright 2018 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "bytes" + "crypto/rand" + "encoding/hex" + "errors" + "fmt" + "io" + "strings" + "sync" +) + +// A UUID is a 128 bit (16 byte) Universal Unique IDentifier as defined in RFC +// 4122. +type UUID [16]byte + +// A Version represents a UUID's version. +type Version byte + +// A Variant represents a UUID's variant. +type Variant byte + +// Constants returned by Variant. +const ( + Invalid = Variant(iota) // Invalid UUID + RFC4122 // The variant specified in RFC4122 + Reserved // Reserved, NCS backward compatibility. + Microsoft // Reserved, Microsoft Corporation backward compatibility. + Future // Reserved for future definition. +) + +const randPoolSize = 16 * 16 + +var ( + rander = rand.Reader // random function + poolEnabled = false + poolMu sync.Mutex + poolPos = randPoolSize // protected with poolMu + pool [randPoolSize]byte // protected with poolMu +) + +type invalidLengthError struct{ len int } + +func (err invalidLengthError) Error() string { + return fmt.Sprintf("invalid UUID length: %d", err.len) +} + +// IsInvalidLengthError is matcher function for custom error invalidLengthError +func IsInvalidLengthError(err error) bool { + _, ok := err.(invalidLengthError) + return ok +} + +// Parse decodes s into a UUID or returns an error if it cannot be parsed. Both +// the standard UUID forms defined in RFC 4122 +// (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx and +// urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx) are decoded. In addition, +// Parse accepts non-standard strings such as the raw hex encoding +// xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx and 38 byte "Microsoft style" encodings, +// e.g. {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}. Only the middle 36 bytes are +// examined in the latter case. Parse should not be used to validate strings as +// it parses non-standard encodings as indicated above. +func Parse(s string) (UUID, error) { + var uuid UUID + switch len(s) { + // xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + case 36: + + // urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + case 36 + 9: + if !strings.EqualFold(s[:9], "urn:uuid:") { + return uuid, fmt.Errorf("invalid urn prefix: %q", s[:9]) + } + s = s[9:] + + // {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx} + case 36 + 2: + s = s[1:] + + // xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + case 32: + var ok bool + for i := range uuid { + uuid[i], ok = xtob(s[i*2], s[i*2+1]) + if !ok { + return uuid, errors.New("invalid UUID format") + } + } + return uuid, nil + default: + return uuid, invalidLengthError{len(s)} + } + // s is now at least 36 bytes long + // it must be of the form xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + if s[8] != '-' || s[13] != '-' || s[18] != '-' || s[23] != '-' { + return uuid, errors.New("invalid UUID format") + } + for i, x := range [16]int{ + 0, 2, 4, 6, + 9, 11, + 14, 16, + 19, 21, + 24, 26, 28, 30, 32, 34, + } { + v, ok := xtob(s[x], s[x+1]) + if !ok { + return uuid, errors.New("invalid UUID format") + } + uuid[i] = v + } + return uuid, nil +} + +// ParseBytes is like Parse, except it parses a byte slice instead of a string. +func ParseBytes(b []byte) (UUID, error) { + var uuid UUID + switch len(b) { + case 36: // xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + case 36 + 9: // urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + if !bytes.EqualFold(b[:9], []byte("urn:uuid:")) { + return uuid, fmt.Errorf("invalid urn prefix: %q", b[:9]) + } + b = b[9:] + case 36 + 2: // {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx} + b = b[1:] + case 32: // xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + var ok bool + for i := 0; i < 32; i += 2 { + uuid[i/2], ok = xtob(b[i], b[i+1]) + if !ok { + return uuid, errors.New("invalid UUID format") + } + } + return uuid, nil + default: + return uuid, invalidLengthError{len(b)} + } + // s is now at least 36 bytes long + // it must be of the form xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + if b[8] != '-' || b[13] != '-' || b[18] != '-' || b[23] != '-' { + return uuid, errors.New("invalid UUID format") + } + for i, x := range [16]int{ + 0, 2, 4, 6, + 9, 11, + 14, 16, + 19, 21, + 24, 26, 28, 30, 32, 34, + } { + v, ok := xtob(b[x], b[x+1]) + if !ok { + return uuid, errors.New("invalid UUID format") + } + uuid[i] = v + } + return uuid, nil +} + +// MustParse is like Parse but panics if the string cannot be parsed. +// It simplifies safe initialization of global variables holding compiled UUIDs. +func MustParse(s string) UUID { + uuid, err := Parse(s) + if err != nil { + panic(`uuid: Parse(` + s + `): ` + err.Error()) + } + return uuid +} + +// FromBytes creates a new UUID from a byte slice. Returns an error if the slice +// does not have a length of 16. The bytes are copied from the slice. +func FromBytes(b []byte) (uuid UUID, err error) { + err = uuid.UnmarshalBinary(b) + return uuid, err +} + +// Must returns uuid if err is nil and panics otherwise. +func Must(uuid UUID, err error) UUID { + if err != nil { + panic(err) + } + return uuid +} + +// Validate returns an error if s is not a properly formatted UUID in one of the following formats: +// xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx +// urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx +// xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +// {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx} +// It returns an error if the format is invalid, otherwise nil. +func Validate(s string) error { + switch len(s) { + // Standard UUID format + case 36: + + // UUID with "urn:uuid:" prefix + case 36 + 9: + if !strings.EqualFold(s[:9], "urn:uuid:") { + return fmt.Errorf("invalid urn prefix: %q", s[:9]) + } + s = s[9:] + + // UUID enclosed in braces + case 36 + 2: + if s[0] != '{' || s[len(s)-1] != '}' { + return fmt.Errorf("invalid bracketed UUID format") + } + s = s[1 : len(s)-1] + + // UUID without hyphens + case 32: + for i := 0; i < len(s); i += 2 { + _, ok := xtob(s[i], s[i+1]) + if !ok { + return errors.New("invalid UUID format") + } + } + + default: + return invalidLengthError{len(s)} + } + + // Check for standard UUID format + if len(s) == 36 { + if s[8] != '-' || s[13] != '-' || s[18] != '-' || s[23] != '-' { + return errors.New("invalid UUID format") + } + for _, x := range []int{0, 2, 4, 6, 9, 11, 14, 16, 19, 21, 24, 26, 28, 30, 32, 34} { + if _, ok := xtob(s[x], s[x+1]); !ok { + return errors.New("invalid UUID format") + } + } + } + + return nil +} + +// String returns the string form of uuid, xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx +// , or "" if uuid is invalid. +func (uuid UUID) String() string { + var buf [36]byte + encodeHex(buf[:], uuid) + return string(buf[:]) +} + +// URN returns the RFC 2141 URN form of uuid, +// urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx, or "" if uuid is invalid. +func (uuid UUID) URN() string { + var buf [36 + 9]byte + copy(buf[:], "urn:uuid:") + encodeHex(buf[9:], uuid) + return string(buf[:]) +} + +func encodeHex(dst []byte, uuid UUID) { + hex.Encode(dst, uuid[:4]) + dst[8] = '-' + hex.Encode(dst[9:13], uuid[4:6]) + dst[13] = '-' + hex.Encode(dst[14:18], uuid[6:8]) + dst[18] = '-' + hex.Encode(dst[19:23], uuid[8:10]) + dst[23] = '-' + hex.Encode(dst[24:], uuid[10:]) +} + +// Variant returns the variant encoded in uuid. +func (uuid UUID) Variant() Variant { + switch { + case (uuid[8] & 0xc0) == 0x80: + return RFC4122 + case (uuid[8] & 0xe0) == 0xc0: + return Microsoft + case (uuid[8] & 0xe0) == 0xe0: + return Future + default: + return Reserved + } +} + +// Version returns the version of uuid. +func (uuid UUID) Version() Version { + return Version(uuid[6] >> 4) +} + +func (v Version) String() string { + if v > 15 { + return fmt.Sprintf("BAD_VERSION_%d", v) + } + return fmt.Sprintf("VERSION_%d", v) +} + +func (v Variant) String() string { + switch v { + case RFC4122: + return "RFC4122" + case Reserved: + return "Reserved" + case Microsoft: + return "Microsoft" + case Future: + return "Future" + case Invalid: + return "Invalid" + } + return fmt.Sprintf("BadVariant%d", int(v)) +} + +// SetRand sets the random number generator to r, which implements io.Reader. +// If r.Read returns an error when the package requests random data then +// a panic will be issued. +// +// Calling SetRand with nil sets the random number generator to the default +// generator. +func SetRand(r io.Reader) { + if r == nil { + rander = rand.Reader + return + } + rander = r +} + +// EnableRandPool enables internal randomness pool used for Random +// (Version 4) UUID generation. The pool contains random bytes read from +// the random number generator on demand in batches. Enabling the pool +// may improve the UUID generation throughput significantly. +// +// Since the pool is stored on the Go heap, this feature may be a bad fit +// for security sensitive applications. +// +// Both EnableRandPool and DisableRandPool are not thread-safe and should +// only be called when there is no possibility that New or any other +// UUID Version 4 generation function will be called concurrently. +func EnableRandPool() { + poolEnabled = true +} + +// DisableRandPool disables the randomness pool if it was previously +// enabled with EnableRandPool. +// +// Both EnableRandPool and DisableRandPool are not thread-safe and should +// only be called when there is no possibility that New or any other +// UUID Version 4 generation function will be called concurrently. +func DisableRandPool() { + poolEnabled = false + defer poolMu.Unlock() + poolMu.Lock() + poolPos = randPoolSize +} + +// UUIDs is a slice of UUID types. +type UUIDs []UUID + +// Strings returns a string slice containing the string form of each UUID in uuids. +func (uuids UUIDs) Strings() []string { + var uuidStrs = make([]string, len(uuids)) + for i, uuid := range uuids { + uuidStrs[i] = uuid.String() + } + return uuidStrs +} diff --git a/src/openstack_cpi_golang/vendor/github.com/google/uuid/version1.go b/src/openstack_cpi_golang/vendor/github.com/google/uuid/version1.go new file mode 100644 index 00000000..46310962 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/google/uuid/version1.go @@ -0,0 +1,44 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "encoding/binary" +) + +// NewUUID returns a Version 1 UUID based on the current NodeID and clock +// sequence, and the current time. If the NodeID has not been set by SetNodeID +// or SetNodeInterface then it will be set automatically. If the NodeID cannot +// be set NewUUID returns nil. If clock sequence has not been set by +// SetClockSequence then it will be set automatically. If GetTime fails to +// return the current NewUUID returns nil and an error. +// +// In most cases, New should be used. +func NewUUID() (UUID, error) { + var uuid UUID + now, seq, err := GetTime() + if err != nil { + return uuid, err + } + + timeLow := uint32(now & 0xffffffff) + timeMid := uint16((now >> 32) & 0xffff) + timeHi := uint16((now >> 48) & 0x0fff) + timeHi |= 0x1000 // Version 1 + + binary.BigEndian.PutUint32(uuid[0:], timeLow) + binary.BigEndian.PutUint16(uuid[4:], timeMid) + binary.BigEndian.PutUint16(uuid[6:], timeHi) + binary.BigEndian.PutUint16(uuid[8:], seq) + + nodeMu.Lock() + if nodeID == zeroID { + setNodeInterface("") + } + copy(uuid[10:], nodeID[:]) + nodeMu.Unlock() + + return uuid, nil +} diff --git a/src/openstack_cpi_golang/vendor/github.com/google/uuid/version4.go b/src/openstack_cpi_golang/vendor/github.com/google/uuid/version4.go new file mode 100644 index 00000000..7697802e --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/google/uuid/version4.go @@ -0,0 +1,76 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import "io" + +// New creates a new random UUID or panics. New is equivalent to +// the expression +// +// uuid.Must(uuid.NewRandom()) +func New() UUID { + return Must(NewRandom()) +} + +// NewString creates a new random UUID and returns it as a string or panics. +// NewString is equivalent to the expression +// +// uuid.New().String() +func NewString() string { + return Must(NewRandom()).String() +} + +// NewRandom returns a Random (Version 4) UUID. +// +// The strength of the UUIDs is based on the strength of the crypto/rand +// package. +// +// Uses the randomness pool if it was enabled with EnableRandPool. +// +// A note about uniqueness derived from the UUID Wikipedia entry: +// +// Randomly generated UUIDs have 122 random bits. One's annual risk of being +// hit by a meteorite is estimated to be one chance in 17 billion, that +// means the probability is about 0.00000000006 (6 × 10−11), +// equivalent to the odds of creating a few tens of trillions of UUIDs in a +// year and having one duplicate. +func NewRandom() (UUID, error) { + if !poolEnabled { + return NewRandomFromReader(rander) + } + return newRandomFromPool() +} + +// NewRandomFromReader returns a UUID based on bytes read from a given io.Reader. +func NewRandomFromReader(r io.Reader) (UUID, error) { + var uuid UUID + _, err := io.ReadFull(r, uuid[:]) + if err != nil { + return Nil, err + } + uuid[6] = (uuid[6] & 0x0f) | 0x40 // Version 4 + uuid[8] = (uuid[8] & 0x3f) | 0x80 // Variant is 10 + return uuid, nil +} + +func newRandomFromPool() (UUID, error) { + var uuid UUID + poolMu.Lock() + if poolPos == randPoolSize { + _, err := io.ReadFull(rander, pool[:]) + if err != nil { + poolMu.Unlock() + return Nil, err + } + poolPos = 0 + } + copy(uuid[:], pool[poolPos:(poolPos+16)]) + poolPos += 16 + poolMu.Unlock() + + uuid[6] = (uuid[6] & 0x0f) | 0x40 // Version 4 + uuid[8] = (uuid[8] & 0x3f) | 0x80 // Variant is 10 + return uuid, nil +} diff --git a/src/openstack_cpi_golang/vendor/github.com/google/uuid/version6.go b/src/openstack_cpi_golang/vendor/github.com/google/uuid/version6.go new file mode 100644 index 00000000..339a959a --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/google/uuid/version6.go @@ -0,0 +1,56 @@ +// Copyright 2023 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import "encoding/binary" + +// UUID version 6 is a field-compatible version of UUIDv1, reordered for improved DB locality. +// It is expected that UUIDv6 will primarily be used in contexts where there are existing v1 UUIDs. +// Systems that do not involve legacy UUIDv1 SHOULD consider using UUIDv7 instead. +// +// see https://datatracker.ietf.org/doc/html/draft-peabody-dispatch-new-uuid-format-03#uuidv6 +// +// NewV6 returns a Version 6 UUID based on the current NodeID and clock +// sequence, and the current time. If the NodeID has not been set by SetNodeID +// or SetNodeInterface then it will be set automatically. If the NodeID cannot +// be set NewV6 set NodeID is random bits automatically . If clock sequence has not been set by +// SetClockSequence then it will be set automatically. If GetTime fails to +// return the current NewV6 returns Nil and an error. +func NewV6() (UUID, error) { + var uuid UUID + now, seq, err := GetTime() + if err != nil { + return uuid, err + } + + /* + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | time_high | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | time_mid | time_low_and_version | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + |clk_seq_hi_res | clk_seq_low | node (0-1) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | node (2-5) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + */ + + binary.BigEndian.PutUint64(uuid[0:], uint64(now)) + binary.BigEndian.PutUint16(uuid[8:], seq) + + uuid[6] = 0x60 | (uuid[6] & 0x0F) + uuid[8] = 0x80 | (uuid[8] & 0x3F) + + nodeMu.Lock() + if nodeID == zeroID { + setNodeInterface("") + } + copy(uuid[10:], nodeID[:]) + nodeMu.Unlock() + + return uuid, nil +} diff --git a/src/openstack_cpi_golang/vendor/github.com/google/uuid/version7.go b/src/openstack_cpi_golang/vendor/github.com/google/uuid/version7.go new file mode 100644 index 00000000..3167b643 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/google/uuid/version7.go @@ -0,0 +1,104 @@ +// Copyright 2023 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "io" +) + +// UUID version 7 features a time-ordered value field derived from the widely +// implemented and well known Unix Epoch timestamp source, +// the number of milliseconds seconds since midnight 1 Jan 1970 UTC, leap seconds excluded. +// As well as improved entropy characteristics over versions 1 or 6. +// +// see https://datatracker.ietf.org/doc/html/draft-peabody-dispatch-new-uuid-format-03#name-uuid-version-7 +// +// Implementations SHOULD utilize UUID version 7 over UUID version 1 and 6 if possible. +// +// NewV7 returns a Version 7 UUID based on the current time(Unix Epoch). +// Uses the randomness pool if it was enabled with EnableRandPool. +// On error, NewV7 returns Nil and an error +func NewV7() (UUID, error) { + uuid, err := NewRandom() + if err != nil { + return uuid, err + } + makeV7(uuid[:]) + return uuid, nil +} + +// NewV7FromReader returns a Version 7 UUID based on the current time(Unix Epoch). +// it use NewRandomFromReader fill random bits. +// On error, NewV7FromReader returns Nil and an error. +func NewV7FromReader(r io.Reader) (UUID, error) { + uuid, err := NewRandomFromReader(r) + if err != nil { + return uuid, err + } + + makeV7(uuid[:]) + return uuid, nil +} + +// makeV7 fill 48 bits time (uuid[0] - uuid[5]), set version b0111 (uuid[6]) +// uuid[8] already has the right version number (Variant is 10) +// see function NewV7 and NewV7FromReader +func makeV7(uuid []byte) { + /* + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | unix_ts_ms | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | unix_ts_ms | ver | rand_a (12 bit seq) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + |var| rand_b | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | rand_b | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + */ + _ = uuid[15] // bounds check + + t, s := getV7Time() + + uuid[0] = byte(t >> 40) + uuid[1] = byte(t >> 32) + uuid[2] = byte(t >> 24) + uuid[3] = byte(t >> 16) + uuid[4] = byte(t >> 8) + uuid[5] = byte(t) + + uuid[6] = 0x70 | (0x0F & byte(s>>8)) + uuid[7] = byte(s) +} + +// lastV7time is the last time we returned stored as: +// +// 52 bits of time in milliseconds since epoch +// 12 bits of (fractional nanoseconds) >> 8 +var lastV7time int64 + +const nanoPerMilli = 1000000 + +// getV7Time returns the time in milliseconds and nanoseconds / 256. +// The returned (milli << 12 + seq) is guarenteed to be greater than +// (milli << 12 + seq) returned by any previous call to getV7Time. +func getV7Time() (milli, seq int64) { + timeMu.Lock() + defer timeMu.Unlock() + + nano := timeNow().UnixNano() + milli = nano / nanoPerMilli + // Sequence number is between 0 and 3906 (nanoPerMilli>>8) + seq = (nano - milli*nanoPerMilli) >> 8 + now := milli<<12 + seq + if now <= lastV7time { + now = lastV7time + 1 + milli = now >> 12 + seq = now & 0xfff + } + lastV7time = now + return milli, seq +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/auth_env.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/auth_env.go new file mode 100644 index 00000000..7c6d06f0 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/auth_env.go @@ -0,0 +1,137 @@ +package openstack + +import ( + "os" + + "github.com/gophercloud/gophercloud" +) + +var nilOptions = gophercloud.AuthOptions{} + +/* +AuthOptionsFromEnv fills out an identity.AuthOptions structure with the +settings found on the various OpenStack OS_* environment variables. + +The following variables provide sources of truth: OS_AUTH_URL, OS_USERNAME, +OS_PASSWORD and OS_PROJECT_ID. + +Of these, OS_USERNAME, OS_PASSWORD, and OS_AUTH_URL must have settings, +or an error will result. OS_PROJECT_ID, is optional. + +OS_TENANT_ID and OS_TENANT_NAME are deprecated forms of OS_PROJECT_ID and +OS_PROJECT_NAME and the latter are expected against a v3 auth api. + +If OS_PROJECT_ID and OS_PROJECT_NAME are set, they will still be referred +as "tenant" in Gophercloud. + +If OS_PROJECT_NAME is set, it requires OS_PROJECT_ID to be set as well to +handle projects not on the default domain. + +To use this function, first set the OS_* environment variables (for example, +by sourcing an `openrc` file), then: + + opts, err := openstack.AuthOptionsFromEnv() + provider, err := openstack.AuthenticatedClient(opts) +*/ +func AuthOptionsFromEnv() (gophercloud.AuthOptions, error) { + authURL := os.Getenv("OS_AUTH_URL") + username := os.Getenv("OS_USERNAME") + userID := os.Getenv("OS_USERID") + password := os.Getenv("OS_PASSWORD") + passcode := os.Getenv("OS_PASSCODE") + tenantID := os.Getenv("OS_TENANT_ID") + tenantName := os.Getenv("OS_TENANT_NAME") + domainID := os.Getenv("OS_DOMAIN_ID") + domainName := os.Getenv("OS_DOMAIN_NAME") + applicationCredentialID := os.Getenv("OS_APPLICATION_CREDENTIAL_ID") + applicationCredentialName := os.Getenv("OS_APPLICATION_CREDENTIAL_NAME") + applicationCredentialSecret := os.Getenv("OS_APPLICATION_CREDENTIAL_SECRET") + systemScope := os.Getenv("OS_SYSTEM_SCOPE") + + // If OS_PROJECT_ID is set, overwrite tenantID with the value. + if v := os.Getenv("OS_PROJECT_ID"); v != "" { + tenantID = v + } + + // If OS_PROJECT_NAME is set, overwrite tenantName with the value. + if v := os.Getenv("OS_PROJECT_NAME"); v != "" { + tenantName = v + } + + if authURL == "" { + err := gophercloud.ErrMissingEnvironmentVariable{ + EnvironmentVariable: "OS_AUTH_URL", + } + return nilOptions, err + } + + if userID == "" && username == "" { + // Empty username and userID could be ignored, when applicationCredentialID and applicationCredentialSecret are set + if applicationCredentialID == "" && applicationCredentialSecret == "" { + err := gophercloud.ErrMissingAnyoneOfEnvironmentVariables{ + EnvironmentVariables: []string{"OS_USERID", "OS_USERNAME"}, + } + return nilOptions, err + } + } + + if password == "" && passcode == "" && applicationCredentialID == "" && applicationCredentialName == "" { + err := gophercloud.ErrMissingEnvironmentVariable{ + // silently ignore TOTP passcode warning, since it is not a common auth method + EnvironmentVariable: "OS_PASSWORD", + } + return nilOptions, err + } + + if (applicationCredentialID != "" || applicationCredentialName != "") && applicationCredentialSecret == "" { + err := gophercloud.ErrMissingEnvironmentVariable{ + EnvironmentVariable: "OS_APPLICATION_CREDENTIAL_SECRET", + } + return nilOptions, err + } + + if domainID == "" && domainName == "" && tenantID == "" && tenantName != "" { + err := gophercloud.ErrMissingEnvironmentVariable{ + EnvironmentVariable: "OS_PROJECT_ID", + } + return nilOptions, err + } + + if applicationCredentialID == "" && applicationCredentialName != "" && applicationCredentialSecret != "" { + if userID == "" && username == "" { + return nilOptions, gophercloud.ErrMissingAnyoneOfEnvironmentVariables{ + EnvironmentVariables: []string{"OS_USERID", "OS_USERNAME"}, + } + } + if username != "" && domainID == "" && domainName == "" { + return nilOptions, gophercloud.ErrMissingAnyoneOfEnvironmentVariables{ + EnvironmentVariables: []string{"OS_DOMAIN_ID", "OS_DOMAIN_NAME"}, + } + } + } + + var scope *gophercloud.AuthScope + if systemScope == "all" { + scope = &gophercloud.AuthScope{ + System: true, + } + } + + ao := gophercloud.AuthOptions{ + IdentityEndpoint: authURL, + UserID: userID, + Username: username, + Password: password, + Passcode: passcode, + TenantID: tenantID, + TenantName: tenantName, + DomainID: domainID, + DomainName: domainName, + ApplicationCredentialID: applicationCredentialID, + ApplicationCredentialName: applicationCredentialName, + ApplicationCredentialSecret: applicationCredentialSecret, + Scope: scope, + } + + return ao, nil +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/volumeactions/doc.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/volumeactions/doc.go new file mode 100644 index 00000000..34db834f --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/volumeactions/doc.go @@ -0,0 +1,108 @@ +/* +Package volumeactions provides information and interaction with volumes in the +OpenStack Block Storage service. A volume is a detachable block storage +device, akin to a USB hard drive. + +Example of Attaching a Volume to an Instance + + attachOpts := volumeactions.AttachOpts{ + MountPoint: "/mnt", + Mode: "rw", + InstanceUUID: server.ID, + } + + err := volumeactions.Attach(client, volume.ID, attachOpts).ExtractErr() + if err != nil { + panic(err) + } + + detachOpts := volumeactions.DetachOpts{ + AttachmentID: volume.Attachments[0].AttachmentID, + } + + err = volumeactions.Detach(client, volume.ID, detachOpts).ExtractErr() + if err != nil { + panic(err) + } + +Example of Creating an Image from a Volume + + uploadImageOpts := volumeactions.UploadImageOpts{ + ImageName: "my_vol", + Force: true, + } + + volumeImage, err := volumeactions.UploadImage(client, volume.ID, uploadImageOpts).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", volumeImage) + +Example of Extending a Volume's Size + + extendOpts := volumeactions.ExtendSizeOpts{ + NewSize: 100, + } + + err := volumeactions.ExtendSize(client, volume.ID, extendOpts).ExtractErr() + if err != nil { + panic(err) + } + +Example of Initializing a Volume Connection + + connectOpts := &volumeactions.InitializeConnectionOpts{ + IP: "127.0.0.1", + Host: "stack", + Initiator: "iqn.1994-05.com.redhat:17cf566367d2", + Multipath: gophercloud.Disabled, + Platform: "x86_64", + OSType: "linux2", + } + + connectionInfo, err := volumeactions.InitializeConnection(client, volume.ID, connectOpts).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", connectionInfo["data"]) + + terminateOpts := &volumeactions.InitializeConnectionOpts{ + IP: "127.0.0.1", + Host: "stack", + Initiator: "iqn.1994-05.com.redhat:17cf566367d2", + Multipath: gophercloud.Disabled, + Platform: "x86_64", + OSType: "linux2", + } + + err = volumeactions.TerminateConnection(client, volume.ID, terminateOpts).ExtractErr() + if err != nil { + panic(err) + } + +Example of Setting a Volume's Bootable status + + options := volumeactions.BootableOpts{ + Bootable: true, + } + + err := volumeactions.SetBootable(client, volume.ID, options).ExtractErr() + if err != nil { + panic(err) + } + +Example of Changing Type of a Volume + + changeTypeOpts := volumeactions.ChangeTypeOpts{ + NewType: "ssd", + MigrationPolicy: volumeactions.MigrationPolicyOnDemand, + } + + err = volumeactions.ChangeType(client, volumeID, changeTypeOpts).ExtractErr() + if err != nil { + panic(err) + } +*/ +package volumeactions diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/volumeactions/requests.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/volumeactions/requests.go new file mode 100644 index 00000000..03fb724a --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/volumeactions/requests.go @@ -0,0 +1,460 @@ +package volumeactions + +import ( + "github.com/gophercloud/gophercloud" +) + +// AttachOptsBuilder allows extensions to add additional parameters to the +// Attach request. +type AttachOptsBuilder interface { + ToVolumeAttachMap() (map[string]interface{}, error) +} + +// AttachMode describes the attachment mode for volumes. +type AttachMode string + +// These constants determine how a volume is attached. +const ( + ReadOnly AttachMode = "ro" + ReadWrite AttachMode = "rw" +) + +// AttachOpts contains options for attaching a Volume. +type AttachOpts struct { + // The mountpoint of this volume. + MountPoint string `json:"mountpoint,omitempty"` + + // The nova instance ID, can't set simultaneously with HostName. + InstanceUUID string `json:"instance_uuid,omitempty"` + + // The hostname of baremetal host, can't set simultaneously with InstanceUUID. + HostName string `json:"host_name,omitempty"` + + // Mount mode of this volume. + Mode AttachMode `json:"mode,omitempty"` +} + +// ToVolumeAttachMap assembles a request body based on the contents of a +// AttachOpts. +func (opts AttachOpts) ToVolumeAttachMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "os-attach") +} + +// Attach will attach a volume based on the values in AttachOpts. +func Attach(client *gophercloud.ServiceClient, id string, opts AttachOptsBuilder) (r AttachResult) { + b, err := opts.ToVolumeAttachMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(actionURL(client, id), b, nil, &gophercloud.RequestOpts{ + OkCodes: []int{202}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// BeginDetaching will mark the volume as detaching. +func BeginDetaching(client *gophercloud.ServiceClient, id string) (r BeginDetachingResult) { + b := map[string]interface{}{"os-begin_detaching": make(map[string]interface{})} + resp, err := client.Post(actionURL(client, id), b, nil, &gophercloud.RequestOpts{ + OkCodes: []int{202}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// DetachOptsBuilder allows extensions to add additional parameters to the +// Detach request. +type DetachOptsBuilder interface { + ToVolumeDetachMap() (map[string]interface{}, error) +} + +// DetachOpts contains options for detaching a Volume. +type DetachOpts struct { + // AttachmentID is the ID of the attachment between a volume and instance. + AttachmentID string `json:"attachment_id,omitempty"` +} + +// ToVolumeDetachMap assembles a request body based on the contents of a +// DetachOpts. +func (opts DetachOpts) ToVolumeDetachMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "os-detach") +} + +// Detach will detach a volume based on volume ID. +func Detach(client *gophercloud.ServiceClient, id string, opts DetachOptsBuilder) (r DetachResult) { + b, err := opts.ToVolumeDetachMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(actionURL(client, id), b, nil, &gophercloud.RequestOpts{ + OkCodes: []int{202}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Reserve will reserve a volume based on volume ID. +func Reserve(client *gophercloud.ServiceClient, id string) (r ReserveResult) { + b := map[string]interface{}{"os-reserve": make(map[string]interface{})} + resp, err := client.Post(actionURL(client, id), b, nil, &gophercloud.RequestOpts{ + OkCodes: []int{200, 201, 202}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Unreserve will unreserve a volume based on volume ID. +func Unreserve(client *gophercloud.ServiceClient, id string) (r UnreserveResult) { + b := map[string]interface{}{"os-unreserve": make(map[string]interface{})} + resp, err := client.Post(actionURL(client, id), b, nil, &gophercloud.RequestOpts{ + OkCodes: []int{200, 201, 202}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// InitializeConnectionOptsBuilder allows extensions to add additional parameters to the +// InitializeConnection request. +type InitializeConnectionOptsBuilder interface { + ToVolumeInitializeConnectionMap() (map[string]interface{}, error) +} + +// InitializeConnectionOpts hosts options for InitializeConnection. +// The fields are specific to the storage driver in use and the destination +// attachment. +type InitializeConnectionOpts struct { + IP string `json:"ip,omitempty"` + Host string `json:"host,omitempty"` + Initiator string `json:"initiator,omitempty"` + Wwpns []string `json:"wwpns,omitempty"` + Wwnns string `json:"wwnns,omitempty"` + Multipath *bool `json:"multipath,omitempty"` + Platform string `json:"platform,omitempty"` + OSType string `json:"os_type,omitempty"` +} + +// ToVolumeInitializeConnectionMap assembles a request body based on the contents of a +// InitializeConnectionOpts. +func (opts InitializeConnectionOpts) ToVolumeInitializeConnectionMap() (map[string]interface{}, error) { + b, err := gophercloud.BuildRequestBody(opts, "connector") + return map[string]interface{}{"os-initialize_connection": b}, err +} + +// InitializeConnection initializes an iSCSI connection by volume ID. +func InitializeConnection(client *gophercloud.ServiceClient, id string, opts InitializeConnectionOptsBuilder) (r InitializeConnectionResult) { + b, err := opts.ToVolumeInitializeConnectionMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(actionURL(client, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 201, 202}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// TerminateConnectionOptsBuilder allows extensions to add additional parameters to the +// TerminateConnection request. +type TerminateConnectionOptsBuilder interface { + ToVolumeTerminateConnectionMap() (map[string]interface{}, error) +} + +// TerminateConnectionOpts hosts options for TerminateConnection. +type TerminateConnectionOpts struct { + IP string `json:"ip,omitempty"` + Host string `json:"host,omitempty"` + Initiator string `json:"initiator,omitempty"` + Wwpns []string `json:"wwpns,omitempty"` + Wwnns string `json:"wwnns,omitempty"` + Multipath *bool `json:"multipath,omitempty"` + Platform string `json:"platform,omitempty"` + OSType string `json:"os_type,omitempty"` +} + +// ToVolumeTerminateConnectionMap assembles a request body based on the contents of a +// TerminateConnectionOpts. +func (opts TerminateConnectionOpts) ToVolumeTerminateConnectionMap() (map[string]interface{}, error) { + b, err := gophercloud.BuildRequestBody(opts, "connector") + return map[string]interface{}{"os-terminate_connection": b}, err +} + +// TerminateConnection terminates an iSCSI connection by volume ID. +func TerminateConnection(client *gophercloud.ServiceClient, id string, opts TerminateConnectionOptsBuilder) (r TerminateConnectionResult) { + b, err := opts.ToVolumeTerminateConnectionMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(actionURL(client, id), b, nil, &gophercloud.RequestOpts{ + OkCodes: []int{202}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// ExtendSizeOptsBuilder allows extensions to add additional parameters to the +// ExtendSize request. +type ExtendSizeOptsBuilder interface { + ToVolumeExtendSizeMap() (map[string]interface{}, error) +} + +// ExtendSizeOpts contains options for extending the size of an existing Volume. +// This object is passed to the volumes.ExtendSize function. +type ExtendSizeOpts struct { + // NewSize is the new size of the volume, in GB. + NewSize int `json:"new_size" required:"true"` +} + +// ToVolumeExtendSizeMap assembles a request body based on the contents of an +// ExtendSizeOpts. +func (opts ExtendSizeOpts) ToVolumeExtendSizeMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "os-extend") +} + +// ExtendSize will extend the size of the volume based on the provided information. +// This operation does not return a response body. +func ExtendSize(client *gophercloud.ServiceClient, id string, opts ExtendSizeOptsBuilder) (r ExtendSizeResult) { + b, err := opts.ToVolumeExtendSizeMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(actionURL(client, id), b, nil, &gophercloud.RequestOpts{ + OkCodes: []int{202}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// UploadImageOptsBuilder allows extensions to add additional parameters to the +// UploadImage request. +type UploadImageOptsBuilder interface { + ToVolumeUploadImageMap() (map[string]interface{}, error) +} + +// UploadImageOpts contains options for uploading a Volume to image storage. +type UploadImageOpts struct { + // Container format, may be bare, ofv, ova, etc. + ContainerFormat string `json:"container_format,omitempty"` + + // Disk format, may be raw, qcow2, vhd, vdi, vmdk, etc. + DiskFormat string `json:"disk_format,omitempty"` + + // The name of image that will be stored in glance. + ImageName string `json:"image_name,omitempty"` + + // Force image creation, usable if volume attached to instance. + Force bool `json:"force,omitempty"` + + // Visibility defines who can see/use the image. + // supported since 3.1 microversion + Visibility string `json:"visibility,omitempty"` + + // whether the image is not deletable. + // supported since 3.1 microversion + Protected bool `json:"protected,omitempty"` +} + +// ToVolumeUploadImageMap assembles a request body based on the contents of a +// UploadImageOpts. +func (opts UploadImageOpts) ToVolumeUploadImageMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "os-volume_upload_image") +} + +// UploadImage will upload an image based on the values in UploadImageOptsBuilder. +func UploadImage(client *gophercloud.ServiceClient, id string, opts UploadImageOptsBuilder) (r UploadImageResult) { + b, err := opts.ToVolumeUploadImageMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(actionURL(client, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{202}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// ForceDelete will delete the volume regardless of state. +func ForceDelete(client *gophercloud.ServiceClient, id string) (r ForceDeleteResult) { + resp, err := client.Post(actionURL(client, id), map[string]interface{}{"os-force_delete": ""}, nil, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// ImageMetadataOptsBuilder allows extensions to add additional parameters to the +// ImageMetadataRequest request. +type ImageMetadataOptsBuilder interface { + ToImageMetadataMap() (map[string]interface{}, error) +} + +// ImageMetadataOpts contains options for setting image metadata to a volume. +type ImageMetadataOpts struct { + // The image metadata to add to the volume as a set of metadata key and value pairs. + Metadata map[string]string `json:"metadata"` +} + +// ToImageMetadataMap assembles a request body based on the contents of a +// ImageMetadataOpts. +func (opts ImageMetadataOpts) ToImageMetadataMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "os-set_image_metadata") +} + +// SetImageMetadata will set image metadata on a volume based on the values in ImageMetadataOptsBuilder. +func SetImageMetadata(client *gophercloud.ServiceClient, id string, opts ImageMetadataOptsBuilder) (r SetImageMetadataResult) { + b, err := opts.ToImageMetadataMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(actionURL(client, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// BootableOpts contains options for setting bootable status to a volume. +type BootableOpts struct { + // Enables or disables the bootable attribute. You can boot an instance from a bootable volume. + Bootable bool `json:"bootable"` +} + +// ToBootableMap assembles a request body based on the contents of a +// BootableOpts. +func (opts BootableOpts) ToBootableMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "os-set_bootable") +} + +// SetBootable will set bootable status on a volume based on the values in BootableOpts +func SetBootable(client *gophercloud.ServiceClient, id string, opts BootableOpts) (r SetBootableResult) { + b, err := opts.ToBootableMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(actionURL(client, id), b, nil, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// MigrationPolicy type represents a migration_policy when changing types. +type MigrationPolicy string + +// Supported attributes for MigrationPolicy attribute for changeType operations. +const ( + MigrationPolicyNever MigrationPolicy = "never" + MigrationPolicyOnDemand MigrationPolicy = "on-demand" +) + +// ChangeTypeOptsBuilder allows extensions to add additional parameters to the +// ChangeType request. +type ChangeTypeOptsBuilder interface { + ToVolumeChangeTypeMap() (map[string]interface{}, error) +} + +// ChangeTypeOpts contains options for changing the type of an existing Volume. +// This object is passed to the volumes.ChangeType function. +type ChangeTypeOpts struct { + // NewType is the name of the new volume type of the volume. + NewType string `json:"new_type" required:"true"` + + // MigrationPolicy specifies if the volume should be migrated when it is + // re-typed. Possible values are "on-demand" or "never". If not specified, + // the default is "never". + MigrationPolicy MigrationPolicy `json:"migration_policy,omitempty"` +} + +// ToVolumeChangeTypeMap assembles a request body based on the contents of an +// ChangeTypeOpts. +func (opts ChangeTypeOpts) ToVolumeChangeTypeMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "os-retype") +} + +// ChangeType will change the volume type of the volume based on the provided information. +// This operation does not return a response body. +func ChangeType(client *gophercloud.ServiceClient, id string, opts ChangeTypeOptsBuilder) (r ChangeTypeResult) { + b, err := opts.ToVolumeChangeTypeMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(actionURL(client, id), b, nil, &gophercloud.RequestOpts{ + OkCodes: []int{202}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// ReImageOpts contains options for Re-image a volume. +type ReImageOpts struct { + // New image id + ImageID string `json:"image_id"` + // set true to re-image volumes in reserved state + ReImageReserved bool `json:"reimage_reserved"` +} + +// ToReImageMap assembles a request body based on the contents of a ReImageOpts. +func (opts ReImageOpts) ToReImageMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "os-reimage") +} + +// ReImage will re-image a volume based on the values in ReImageOpts +func ReImage(client *gophercloud.ServiceClient, id string, opts ReImageOpts) (r ReImageResult) { + b, err := opts.ToReImageMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(actionURL(client, id), b, nil, &gophercloud.RequestOpts{ + OkCodes: []int{202}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// ResetStatusOptsBuilder allows extensions to add additional parameters to the +// ResetStatus request. +type ResetStatusOptsBuilder interface { + ToResetStatusMap() (map[string]interface{}, error) +} + +// ResetStatusOpts contains options for resetting a Volume status. +// For more information about these parameters, please, refer to the Block Storage API V3, +// Volume Actions, ResetStatus volume documentation. +type ResetStatusOpts struct { + // Status is a volume status to reset to. + Status string `json:"status"` + // MigrationStatus is a volume migration status to reset to. + MigrationStatus string `json:"migration_status,omitempty"` + // AttachStatus is a volume attach status to reset to. + AttachStatus string `json:"attach_status,omitempty"` +} + +// ToResetStatusMap assembles a request body based on the contents of a +// ResetStatusOpts. +func (opts ResetStatusOpts) ToResetStatusMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "os-reset_status") +} + +// ResetStatus will reset the existing volume status. ResetStatusResult contains only the error. +// To extract it, call the ExtractErr method on the ResetStatusResult. +func ResetStatus(client *gophercloud.ServiceClient, id string, opts ResetStatusOptsBuilder) (r ResetStatusResult) { + b, err := opts.ToResetStatusMap() + if err != nil { + r.Err = err + return + } + + resp, err := client.Post(actionURL(client, id), b, nil, &gophercloud.RequestOpts{ + OkCodes: []int{202}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/volumeactions/results.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/volumeactions/results.go new file mode 100644 index 00000000..34f64e18 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/volumeactions/results.go @@ -0,0 +1,226 @@ +package volumeactions + +import ( + "encoding/json" + "time" + + "github.com/gophercloud/gophercloud" +) + +// AttachResult contains the response body and error from an Attach request. +type AttachResult struct { + gophercloud.ErrResult +} + +// BeginDetachingResult contains the response body and error from a BeginDetach +// request. +type BeginDetachingResult struct { + gophercloud.ErrResult +} + +// DetachResult contains the response body and error from a Detach request. +type DetachResult struct { + gophercloud.ErrResult +} + +// UploadImageResult contains the response body and error from an UploadImage +// request. +type UploadImageResult struct { + gophercloud.Result +} + +// SetImageMetadataResult contains the response body and error from an SetImageMetadata +// request. +type SetImageMetadataResult struct { + gophercloud.ErrResult +} + +// SetBootableResult contains the response body and error from a SetBootable +// request. +type SetBootableResult struct { + gophercloud.ErrResult +} + +// ReserveResult contains the response body and error from a Reserve request. +type ReserveResult struct { + gophercloud.ErrResult +} + +// UnreserveResult contains the response body and error from an Unreserve +// request. +type UnreserveResult struct { + gophercloud.ErrResult +} + +// TerminateConnectionResult contains the response body and error from a +// TerminateConnection request. +type TerminateConnectionResult struct { + gophercloud.ErrResult +} + +// InitializeConnectionResult contains the response body and error from an +// InitializeConnection request. +type InitializeConnectionResult struct { + gophercloud.Result +} + +// ExtendSizeResult contains the response body and error from an ExtendSize request. +type ExtendSizeResult struct { + gophercloud.ErrResult +} + +// Extract will get the connection information out of the +// InitializeConnectionResult object. +// +// This will be a generic map[string]interface{} and the results will be +// dependent on the type of connection made. +func (r InitializeConnectionResult) Extract() (map[string]interface{}, error) { + var s struct { + ConnectionInfo map[string]interface{} `json:"connection_info"` + } + err := r.ExtractInto(&s) + return s.ConnectionInfo, err +} + +// ImageVolumeType contains volume type information obtained from UploadImage +// action. +type ImageVolumeType struct { + // The ID of a volume type. + ID string `json:"id"` + + // Human-readable display name for the volume type. + Name string `json:"name"` + + // Human-readable description for the volume type. + Description string `json:"display_description"` + + // Flag for public access. + IsPublic bool `json:"is_public"` + + // Extra specifications for volume type. + ExtraSpecs map[string]interface{} `json:"extra_specs"` + + // ID of quality of service specs. + QosSpecsID string `json:"qos_specs_id"` + + // Flag for deletion status of volume type. + Deleted bool `json:"deleted"` + + // The date when volume type was deleted. + DeletedAt time.Time `json:"-"` + + // The date when volume type was created. + CreatedAt time.Time `json:"-"` + + // The date when this volume was last updated. + UpdatedAt time.Time `json:"-"` +} + +func (r *ImageVolumeType) UnmarshalJSON(b []byte) error { + type tmp ImageVolumeType + var s struct { + tmp + CreatedAt gophercloud.JSONRFC3339MilliNoZ `json:"created_at"` + UpdatedAt gophercloud.JSONRFC3339MilliNoZ `json:"updated_at"` + DeletedAt gophercloud.JSONRFC3339MilliNoZ `json:"deleted_at"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = ImageVolumeType(s.tmp) + + r.CreatedAt = time.Time(s.CreatedAt) + r.UpdatedAt = time.Time(s.UpdatedAt) + r.DeletedAt = time.Time(s.DeletedAt) + + return err +} + +// VolumeImage contains information about volume uploaded to an image service. +type VolumeImage struct { + // The ID of a volume an image is created from. + VolumeID string `json:"id"` + + // Container format, may be bare, ofv, ova, etc. + ContainerFormat string `json:"container_format"` + + // Disk format, may be raw, qcow2, vhd, vdi, vmdk, etc. + DiskFormat string `json:"disk_format"` + + // Human-readable description for the volume. + Description string `json:"display_description"` + + // The ID of the created image. + ImageID string `json:"image_id"` + + // Human-readable display name for the image. + ImageName string `json:"image_name"` + + // Size of the volume in GB. + Size int `json:"size"` + + // Current status of the volume. + Status string `json:"status"` + + // Visibility defines who can see/use the image. + // supported since 3.1 microversion + Visibility string `json:"visibility"` + + // whether the image is not deletable. + // supported since 3.1 microversion + Protected bool `json:"protected"` + + // The date when this volume was last updated. + UpdatedAt time.Time `json:"-"` + + // Volume type object of used volume. + VolumeType ImageVolumeType `json:"volume_type"` +} + +func (r *VolumeImage) UnmarshalJSON(b []byte) error { + type tmp VolumeImage + var s struct { + tmp + UpdatedAt gophercloud.JSONRFC3339MilliNoZ `json:"updated_at"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = VolumeImage(s.tmp) + + r.UpdatedAt = time.Time(s.UpdatedAt) + + return err +} + +// Extract will get an object with info about the uploaded image out of the +// UploadImageResult object. +func (r UploadImageResult) Extract() (VolumeImage, error) { + var s struct { + VolumeImage VolumeImage `json:"os-volume_upload_image"` + } + err := r.ExtractInto(&s) + return s.VolumeImage, err +} + +// ForceDeleteResult contains the response body and error from a ForceDelete request. +type ForceDeleteResult struct { + gophercloud.ErrResult +} + +// ChangeTypeResult contains the response body and error from an ChangeType request. +type ChangeTypeResult struct { + gophercloud.ErrResult +} + +// ReImageResult contains the response body and error from a ReImage request. +type ReImageResult struct { + gophercloud.ErrResult +} + +// ResetStatusResult contains the response error from a ResetStatus request. +type ResetStatusResult struct { + gophercloud.ErrResult +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/volumeactions/urls.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/volumeactions/urls.go new file mode 100644 index 00000000..20486ed7 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/volumeactions/urls.go @@ -0,0 +1,7 @@ +package volumeactions + +import "github.com/gophercloud/gophercloud" + +func actionURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("volumes", id, "action") +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v3/snapshots/doc.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v3/snapshots/doc.go new file mode 100644 index 00000000..702094c3 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v3/snapshots/doc.go @@ -0,0 +1,61 @@ +/* +Package snapshots provides information and interaction with snapshots in the +OpenStack Block Storage service. A snapshot is a point in time copy of the +data contained in an external storage volume, and can be controlled +programmatically. + +Example to list Snapshots + + allPages, err := snapshots.List(client, snapshots.ListOpts{}).AllPages() + if err != nil{ + panic(err) + } + snapshots, err := snapshots.ExtractSnapshots(allPages) + if err != nil{ + panic(err) + } + for _,s := range snapshots{ + fmt.Println(s) + } + +Example to get a Snapshot + + snapshotID := "4a584cae-e4ce-429b-9154-d4c9eb8fda4c" + snapshot, err := snapshots.Get(client, snapshotID).Extract() + if err != nil{ + panic(err) + } + fmt.Println(snapshot) + +Example to create a Snapshot + + snapshot, err := snapshots.Create(client, snapshots.CreateOpts{ + Name:"snapshot_001", + VolumeID:"5aa119a8-d25b-45a7-8d1b-88e127885635", + }).Extract() + if err != nil{ + panic(err) + } + fmt.Println(snapshot) + +Example to delete a Snapshot + + snapshotID := "4a584cae-e4ce-429b-9154-d4c9eb8fda4c" + err := snapshots.Delete(client, snapshotID).ExtractErr() + if err != nil{ + panic(err) + } + +Example to update a Snapshot + + snapshotID := "4a584cae-e4ce-429b-9154-d4c9eb8fda4c" + snapshot, err = snapshots.Update(client, snapshotID, snapshots.UpdateOpts{ + Name: "snapshot_002", + Description:"description_002", + }).Extract() + if err != nil{ + panic(err) + } + fmt.Println(snapshot) +*/ +package snapshots diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v3/snapshots/requests.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v3/snapshots/requests.go new file mode 100644 index 00000000..9bbec339 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v3/snapshots/requests.go @@ -0,0 +1,278 @@ +package snapshots + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToSnapshotCreateMap() (map[string]interface{}, error) +} + +// CreateOpts contains options for creating a Snapshot. This object is passed to +// the snapshots.Create function. For more information about these parameters, +// see the Snapshot object. +type CreateOpts struct { + VolumeID string `json:"volume_id" required:"true"` + Force bool `json:"force,omitempty"` + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + Metadata map[string]string `json:"metadata,omitempty"` +} + +// ToSnapshotCreateMap assembles a request body based on the contents of a +// CreateOpts. +func (opts CreateOpts) ToSnapshotCreateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "snapshot") +} + +// Create will create a new Snapshot based on the values in CreateOpts. To +// extract the Snapshot object from the response, call the Extract method on the +// CreateResult. +func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToSnapshotCreateMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(createURL(client), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{202}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Delete will delete the existing Snapshot with the provided ID. +func Delete(client *gophercloud.ServiceClient, id string) (r DeleteResult) { + resp, err := client.Delete(deleteURL(client, id), nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Get retrieves the Snapshot with the provided ID. To extract the Snapshot +// object from the response, call the Extract method on the GetResult. +func Get(client *gophercloud.ServiceClient, id string) (r GetResult) { + resp, err := client.Get(getURL(client, id), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// ListOptsBuilder allows extensions to add additional parameters to the List +// request. +type ListOptsBuilder interface { + ToSnapshotListQuery() (string, error) +} + +// ListOpts holds options for listing Snapshots. It is passed to the snapshots.List +// function. +type ListOpts struct { + // AllTenants will retrieve snapshots of all tenants/projects. + AllTenants bool `q:"all_tenants"` + + // Name will filter by the specified snapshot name. + Name string `q:"name"` + + // Status will filter by the specified status. + Status string `q:"status"` + + // TenantID will filter by a specific tenant/project ID. + // Setting AllTenants is required to use this. + TenantID string `q:"project_id"` + + // VolumeID will filter by a specified volume ID. + VolumeID string `q:"volume_id"` + + // Comma-separated list of sort keys and optional sort directions in the + // form of [:]. + Sort string `q:"sort"` + + // Requests a page size of items. + Limit int `q:"limit"` + + // Used in conjunction with limit to return a slice of items. + Offset int `q:"offset"` + + // The ID of the last-seen item. + Marker string `q:"marker"` +} + +// ToSnapshotListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToSnapshotListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns Snapshots optionally limited by the conditions provided in +// ListOpts. +func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToSnapshotListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return SnapshotPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the +// Update request. +type UpdateOptsBuilder interface { + ToSnapshotUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts contain options for updating an existing Snapshot. This object is passed +// to the snapshots.Update function. For more information about the parameters, see +// the Snapshot object. +type UpdateOpts struct { + Name *string `json:"name,omitempty"` + Description *string `json:"description,omitempty"` +} + +// ToSnapshotUpdateMap assembles a request body based on the contents of an +// UpdateOpts. +func (opts UpdateOpts) ToSnapshotUpdateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "snapshot") +} + +// Update will update the Snapshot with provided information. To extract the updated +// Snapshot from the response, call the Extract method on the UpdateResult. +func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToSnapshotUpdateMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Put(updateURL(client, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// UpdateMetadataOptsBuilder allows extensions to add additional parameters to +// the Update request. +type UpdateMetadataOptsBuilder interface { + ToSnapshotUpdateMetadataMap() (map[string]interface{}, error) +} + +// UpdateMetadataOpts contain options for updating an existing Snapshot. This +// object is passed to the snapshots.Update function. For more information +// about the parameters, see the Snapshot object. +type UpdateMetadataOpts struct { + Metadata map[string]interface{} `json:"metadata,omitempty"` +} + +// ToSnapshotUpdateMetadataMap assembles a request body based on the contents of +// an UpdateMetadataOpts. +func (opts UpdateMetadataOpts) ToSnapshotUpdateMetadataMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "") +} + +// UpdateMetadata will update the Snapshot with provided information. To +// extract the updated Snapshot from the response, call the ExtractMetadata +// method on the UpdateMetadataResult. +func UpdateMetadata(client *gophercloud.ServiceClient, id string, opts UpdateMetadataOptsBuilder) (r UpdateMetadataResult) { + b, err := opts.ToSnapshotUpdateMetadataMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Put(updateMetadataURL(client, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// ResetStatusOptsBuilder allows extensions to add additional parameters to the +// ResetStatus request. +type ResetStatusOptsBuilder interface { + ToSnapshotResetStatusMap() (map[string]interface{}, error) +} + +// ResetStatusOpts contains options for resetting a Snapshot status. +// For more information about these parameters, please, refer to the Block Storage API V3, +// Snapshot Actions, ResetStatus snapshot documentation. +type ResetStatusOpts struct { + // Status is a snapshot status to reset to. + Status string `json:"status"` +} + +// ToSnapshotResetStatusMap assembles a request body based on the contents of a +// ResetStatusOpts. +func (opts ResetStatusOpts) ToSnapshotResetStatusMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "os-reset_status") +} + +// ResetStatus will reset the existing snapshot status. ResetStatusResult contains only the error. +// To extract it, call the ExtractErr method on the ResetStatusResult. +func ResetStatus(client *gophercloud.ServiceClient, id string, opts ResetStatusOptsBuilder) (r ResetStatusResult) { + b, err := opts.ToSnapshotResetStatusMap() + if err != nil { + r.Err = err + return + } + + resp, err := client.Post(resetStatusURL(client, id), b, nil, &gophercloud.RequestOpts{ + OkCodes: []int{202}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// UpdateStatusOptsBuilder allows extensions to add additional parameters to the +// UpdateStatus request. +type UpdateStatusOptsBuilder interface { + ToSnapshotUpdateStatusMap() (map[string]interface{}, error) +} + +// UpdateStatusOpts contains options for resetting a Snapshot status. +// For more information about these parameters, please, refer to the Block Storage API V3, +// Snapshot Actions, UpdateStatus snapshot documentation. +type UpdateStatusOpts struct { + // Status is a snapshot status to update to. + Status string `json:"status"` + // A progress percentage value for snapshot build progress. + Progress string `json:"progress,omitempty"` +} + +// ToSnapshotUpdateStatusMap assembles a request body based on the contents of a +// UpdateStatusOpts. +func (opts UpdateStatusOpts) ToSnapshotUpdateStatusMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "os-update_snapshot_status") +} + +// UpdateStatus will reset the existing snapshot status. UpdateStatusResult contains only the error. +// To extract it, call the ExtractErr method on the UpdateStatusResult. +func UpdateStatus(client *gophercloud.ServiceClient, id string, opts UpdateStatusOptsBuilder) (r UpdateStatusResult) { + b, err := opts.ToSnapshotUpdateStatusMap() + if err != nil { + r.Err = err + return + } + + resp, err := client.Post(resetStatusURL(client, id), b, nil, &gophercloud.RequestOpts{ + OkCodes: []int{202}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// ForceDelete will delete the existing snapshot in any state. ForceDeleteResult contains only the error. +// To extract it, call the ExtractErr method on the ForceDeleteResult. +func ForceDelete(client *gophercloud.ServiceClient, id string) (r ForceDeleteResult) { + b := map[string]interface{}{ + "os-force_delete": struct{}{}, + } + resp, err := client.Post(forceDeleteURL(client, id), b, nil, &gophercloud.RequestOpts{ + OkCodes: []int{202}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v3/snapshots/results.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v3/snapshots/results.go new file mode 100644 index 00000000..23c7a506 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v3/snapshots/results.go @@ -0,0 +1,158 @@ +package snapshots + +import ( + "encoding/json" + "time" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// Snapshot contains all the information associated with a Cinder Snapshot. +type Snapshot struct { + // Unique identifier. + ID string `json:"id"` + + // Date created. + CreatedAt time.Time `json:"-"` + + // Date updated. + UpdatedAt time.Time `json:"-"` + + // Display name. + Name string `json:"name"` + + // Display description. + Description string `json:"description"` + + // ID of the Volume from which this Snapshot was created. + VolumeID string `json:"volume_id"` + + // Currect status of the Snapshot. + Status string `json:"status"` + + // Size of the Snapshot, in GB. + Size int `json:"size"` + + // User-defined key-value pairs. + Metadata map[string]string `json:"metadata"` +} + +// CreateResult contains the response body and error from a Create request. +type CreateResult struct { + commonResult +} + +// GetResult contains the response body and error from a Get request. +type GetResult struct { + commonResult +} + +// DeleteResult contains the response body and error from a Delete request. +type DeleteResult struct { + gophercloud.ErrResult +} + +// UpdateResult contains the response body and error from an Update request. +type UpdateResult struct { + commonResult +} + +// SnapshotPage is a pagination.Pager that is returned from a call to the List function. +type SnapshotPage struct { + pagination.LinkedPageBase +} + +// UnmarshalJSON converts our JSON API response into our snapshot struct +func (r *Snapshot) UnmarshalJSON(b []byte) error { + type tmp Snapshot + var s struct { + tmp + CreatedAt gophercloud.JSONRFC3339MilliNoZ `json:"created_at"` + UpdatedAt gophercloud.JSONRFC3339MilliNoZ `json:"updated_at"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = Snapshot(s.tmp) + + r.CreatedAt = time.Time(s.CreatedAt) + r.UpdatedAt = time.Time(s.UpdatedAt) + + return err +} + +// IsEmpty returns true if a SnapshotPage contains no Snapshots. +func (r SnapshotPage) IsEmpty() (bool, error) { + if r.StatusCode == 204 { + return true, nil + } + + volumes, err := ExtractSnapshots(r) + return len(volumes) == 0, err +} + +// NextPageURL uses the response's embedded link reference to navigate to the +// next page of results. +func (r SnapshotPage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"snapshots_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// ExtractSnapshots extracts and returns Snapshots. It is used while iterating over a snapshots.List call. +func ExtractSnapshots(r pagination.Page) ([]Snapshot, error) { + var s struct { + Snapshots []Snapshot `json:"snapshots"` + } + err := (r.(SnapshotPage)).ExtractInto(&s) + return s.Snapshots, err +} + +// UpdateMetadataResult contains the response body and error from an UpdateMetadata request. +type UpdateMetadataResult struct { + commonResult +} + +// ExtractMetadata returns the metadata from a response from snapshots.UpdateMetadata. +func (r UpdateMetadataResult) ExtractMetadata() (map[string]interface{}, error) { + if r.Err != nil { + return nil, r.Err + } + m := r.Body.(map[string]interface{})["metadata"] + return m.(map[string]interface{}), nil +} + +type commonResult struct { + gophercloud.Result +} + +// Extract will get the Snapshot object out of the commonResult object. +func (r commonResult) Extract() (*Snapshot, error) { + var s struct { + Snapshot *Snapshot `json:"snapshot"` + } + err := r.ExtractInto(&s) + return s.Snapshot, err +} + +// ResetStatusResult contains the response error from a ResetStatus request. +type ResetStatusResult struct { + gophercloud.ErrResult +} + +// UpdateStatusResult contains the response error from an UpdateStatus request. +type UpdateStatusResult struct { + gophercloud.ErrResult +} + +// ForceDeleteResult contains the response error from a ForceDelete request. +type ForceDeleteResult struct { + gophercloud.ErrResult +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v3/snapshots/urls.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v3/snapshots/urls.go new file mode 100644 index 00000000..605b3cf5 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v3/snapshots/urls.go @@ -0,0 +1,43 @@ +package snapshots + +import "github.com/gophercloud/gophercloud" + +func createURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("snapshots") +} + +func deleteURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("snapshots", id) +} + +func getURL(c *gophercloud.ServiceClient, id string) string { + return deleteURL(c, id) +} + +func listURL(c *gophercloud.ServiceClient) string { + return createURL(c) +} + +func updateURL(c *gophercloud.ServiceClient, id string) string { + return deleteURL(c, id) +} + +func metadataURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("snapshots", id, "metadata") +} + +func updateMetadataURL(c *gophercloud.ServiceClient, id string) string { + return metadataURL(c, id) +} + +func resetStatusURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("snapshots", id, "action") +} + +func updateStatusURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("snapshots", id, "action") +} + +func forceDeleteURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("snapshots", id, "action") +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v3/snapshots/util.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v3/snapshots/util.go new file mode 100644 index 00000000..40fbb827 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v3/snapshots/util.go @@ -0,0 +1,22 @@ +package snapshots + +import ( + "github.com/gophercloud/gophercloud" +) + +// WaitForStatus will continually poll the resource, checking for a particular +// status. It will do this for the amount of seconds defined. +func WaitForStatus(c *gophercloud.ServiceClient, id, status string, secs int) error { + return gophercloud.WaitFor(secs, func() (bool, error) { + current, err := Get(c, id).Extract() + if err != nil { + return false, err + } + + if current.Status == status { + return true, nil + } + + return false, nil + }) +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v3/volumes/doc.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v3/volumes/doc.go new file mode 100644 index 00000000..0b834852 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v3/volumes/doc.go @@ -0,0 +1,23 @@ +/* +Package volumes provides information and interaction with volumes in the +OpenStack Block Storage service. A volume is a detachable block storage +device, akin to a USB hard drive. It can only be attached to one instance at +a time. + +Example to create a Volume from a Backup + + backupID := "20c792f0-bb03-434f-b653-06ef238e337e" + options := volumes.CreateOpts{ + Name: "vol-001", + BackupID: &backupID, + } + + client.Microversion = "3.47" + volume, err := volumes.Create(client, options).Extract() + if err != nil { + panic(err) + } + + fmt.Println(volume) +*/ +package volumes diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v3/volumes/requests.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v3/volumes/requests.go new file mode 100644 index 00000000..f6063c59 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v3/volumes/requests.go @@ -0,0 +1,208 @@ +package volumes + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToVolumeCreateMap() (map[string]interface{}, error) +} + +// CreateOpts contains options for creating a Volume. This object is passed to +// the volumes.Create function. For more information about these parameters, +// see the Volume object. +type CreateOpts struct { + // The size of the volume, in GB + Size int `json:"size,omitempty"` + // The availability zone + AvailabilityZone string `json:"availability_zone,omitempty"` + // ConsistencyGroupID is the ID of a consistency group + ConsistencyGroupID string `json:"consistencygroup_id,omitempty"` + // The volume description + Description string `json:"description,omitempty"` + // One or more metadata key and value pairs to associate with the volume + Metadata map[string]string `json:"metadata,omitempty"` + // The volume name + Name string `json:"name,omitempty"` + // the ID of the existing volume snapshot + SnapshotID string `json:"snapshot_id,omitempty"` + // SourceReplica is a UUID of an existing volume to replicate with + SourceReplica string `json:"source_replica,omitempty"` + // the ID of the existing volume + SourceVolID string `json:"source_volid,omitempty"` + // The ID of the image from which you want to create the volume. + // Required to create a bootable volume. + ImageID string `json:"imageRef,omitempty"` + // Specifies the backup ID, from which you want to create the volume. + // Create a volume from a backup is supported since 3.47 microversion + BackupID string `json:"backup_id,omitempty"` + // The associated volume type + VolumeType string `json:"volume_type,omitempty"` + // Multiattach denotes if the volume is multi-attach capable. + Multiattach bool `json:"multiattach,omitempty"` +} + +// ToVolumeCreateMap assembles a request body based on the contents of a +// CreateOpts. +func (opts CreateOpts) ToVolumeCreateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "volume") +} + +// Create will create a new Volume based on the values in CreateOpts. To extract +// the Volume object from the response, call the Extract method on the +// CreateResult. +func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToVolumeCreateMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(createURL(client), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{202}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// DeleteOptsBuilder allows extensions to add additional parameters to the +// Delete request. +type DeleteOptsBuilder interface { + ToVolumeDeleteQuery() (string, error) +} + +// DeleteOpts contains options for deleting a Volume. This object is passed to +// the volumes.Delete function. +type DeleteOpts struct { + // Delete all snapshots of this volume as well. + Cascade bool `q:"cascade"` +} + +// ToLoadBalancerDeleteQuery formats a DeleteOpts into a query string. +func (opts DeleteOpts) ToVolumeDeleteQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// Delete will delete the existing Volume with the provided ID. +func Delete(client *gophercloud.ServiceClient, id string, opts DeleteOptsBuilder) (r DeleteResult) { + url := deleteURL(client, id) + if opts != nil { + query, err := opts.ToVolumeDeleteQuery() + if err != nil { + r.Err = err + return + } + url += query + } + resp, err := client.Delete(url, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Get retrieves the Volume with the provided ID. To extract the Volume object +// from the response, call the Extract method on the GetResult. +func Get(client *gophercloud.ServiceClient, id string) (r GetResult) { + resp, err := client.Get(getURL(client, id), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// ListOptsBuilder allows extensions to add additional parameters to the List +// request. +type ListOptsBuilder interface { + ToVolumeListQuery() (string, error) +} + +// ListOpts holds options for listing Volumes. It is passed to the volumes.List +// function. +type ListOpts struct { + // AllTenants will retrieve volumes of all tenants/projects. + AllTenants bool `q:"all_tenants"` + + // Metadata will filter results based on specified metadata. + Metadata map[string]string `q:"metadata"` + + // Name will filter by the specified volume name. + Name string `q:"name"` + + // Status will filter by the specified status. + Status string `q:"status"` + + // TenantID will filter by a specific tenant/project ID. + // Setting AllTenants is required for this. + TenantID string `q:"project_id"` + + // Comma-separated list of sort keys and optional sort directions in the + // form of [:]. + Sort string `q:"sort"` + + // Requests a page size of items. + Limit int `q:"limit"` + + // Used in conjunction with limit to return a slice of items. + Offset int `q:"offset"` + + // The ID of the last-seen item. + Marker string `q:"marker"` +} + +// ToVolumeListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToVolumeListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns Volumes optionally limited by the conditions provided in ListOpts. +func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToVolumeListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return VolumePage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the +// Update request. +type UpdateOptsBuilder interface { + ToVolumeUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts contain options for updating an existing Volume. This object is passed +// to the volumes.Update function. For more information about the parameters, see +// the Volume object. +type UpdateOpts struct { + Name *string `json:"name,omitempty"` + Description *string `json:"description,omitempty"` + Metadata map[string]string `json:"metadata,omitempty"` +} + +// ToVolumeUpdateMap assembles a request body based on the contents of an +// UpdateOpts. +func (opts UpdateOpts) ToVolumeUpdateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "volume") +} + +// Update will update the Volume with provided information. To extract the updated +// Volume from the response, call the Extract method on the UpdateResult. +func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToVolumeUpdateMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Put(updateURL(client, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v3/volumes/results.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v3/volumes/results.go new file mode 100644 index 00000000..df97e669 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v3/volumes/results.go @@ -0,0 +1,179 @@ +package volumes + +import ( + "encoding/json" + "time" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// Attachment represents a Volume Attachment record +type Attachment struct { + AttachedAt time.Time `json:"-"` + AttachmentID string `json:"attachment_id"` + Device string `json:"device"` + HostName string `json:"host_name"` + ID string `json:"id"` + ServerID string `json:"server_id"` + VolumeID string `json:"volume_id"` +} + +// UnmarshalJSON is our unmarshalling helper +func (r *Attachment) UnmarshalJSON(b []byte) error { + type tmp Attachment + var s struct { + tmp + AttachedAt gophercloud.JSONRFC3339MilliNoZ `json:"attached_at"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = Attachment(s.tmp) + + r.AttachedAt = time.Time(s.AttachedAt) + + return err +} + +// Volume contains all the information associated with an OpenStack Volume. +type Volume struct { + // Unique identifier for the volume. + ID string `json:"id"` + // Current status of the volume. + Status string `json:"status"` + // Size of the volume in GB. + Size int `json:"size"` + // AvailabilityZone is which availability zone the volume is in. + AvailabilityZone string `json:"availability_zone"` + // The date when this volume was created. + CreatedAt time.Time `json:"-"` + // The date when this volume was last updated + UpdatedAt time.Time `json:"-"` + // Instances onto which the volume is attached. + Attachments []Attachment `json:"attachments"` + // Human-readable display name for the volume. + Name string `json:"name"` + // Human-readable description for the volume. + Description string `json:"description"` + // The type of volume to create, either SATA or SSD. + VolumeType string `json:"volume_type"` + // The ID of the snapshot from which the volume was created + SnapshotID string `json:"snapshot_id"` + // The ID of another block storage volume from which the current volume was created + SourceVolID string `json:"source_volid"` + // The backup ID, from which the volume was restored + // This field is supported since 3.47 microversion + BackupID *string `json:"backup_id"` + // Arbitrary key-value pairs defined by the user. + Metadata map[string]string `json:"metadata"` + // UserID is the id of the user who created the volume. + UserID string `json:"user_id"` + // Indicates whether this is a bootable volume. + Bootable string `json:"bootable"` + // Encrypted denotes if the volume is encrypted. + Encrypted bool `json:"encrypted"` + // ReplicationStatus is the status of replication. + ReplicationStatus string `json:"replication_status"` + // ConsistencyGroupID is the consistency group ID. + ConsistencyGroupID string `json:"consistencygroup_id"` + // Multiattach denotes if the volume is multi-attach capable. + Multiattach bool `json:"multiattach"` + // Image metadata entries, only included for volumes that were created from an image, or from a snapshot of a volume originally created from an image. + VolumeImageMetadata map[string]string `json:"volume_image_metadata"` +} + +// UnmarshalJSON another unmarshalling function +func (r *Volume) UnmarshalJSON(b []byte) error { + type tmp Volume + var s struct { + tmp + CreatedAt gophercloud.JSONRFC3339MilliNoZ `json:"created_at"` + UpdatedAt gophercloud.JSONRFC3339MilliNoZ `json:"updated_at"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = Volume(s.tmp) + + r.CreatedAt = time.Time(s.CreatedAt) + r.UpdatedAt = time.Time(s.UpdatedAt) + + return err +} + +// VolumePage is a pagination.pager that is returned from a call to the List function. +type VolumePage struct { + pagination.LinkedPageBase +} + +// IsEmpty returns true if a ListResult contains no Volumes. +func (r VolumePage) IsEmpty() (bool, error) { + if r.StatusCode == 204 { + return true, nil + } + + volumes, err := ExtractVolumes(r) + return len(volumes) == 0, err +} + +func (page VolumePage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"volumes_links"` + } + err := page.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// ExtractVolumes extracts and returns Volumes. It is used while iterating over a volumes.List call. +func ExtractVolumes(r pagination.Page) ([]Volume, error) { + var s []Volume + err := ExtractVolumesInto(r, &s) + return s, err +} + +type commonResult struct { + gophercloud.Result +} + +// Extract will get the Volume object out of the commonResult object. +func (r commonResult) Extract() (*Volume, error) { + var s Volume + err := r.ExtractInto(&s) + return &s, err +} + +// ExtractInto converts our response data into a volume struct +func (r commonResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "volume") +} + +// ExtractVolumesInto similar to ExtractInto but operates on a `list` of volumes +func ExtractVolumesInto(r pagination.Page, v interface{}) error { + return r.(VolumePage).Result.ExtractIntoSlicePtr(v, "volumes") +} + +// CreateResult contains the response body and error from a Create request. +type CreateResult struct { + commonResult +} + +// GetResult contains the response body and error from a Get request. +type GetResult struct { + commonResult +} + +// UpdateResult contains the response body and error from an Update request. +type UpdateResult struct { + commonResult +} + +// DeleteResult contains the response body and error from a Delete request. +type DeleteResult struct { + gophercloud.ErrResult +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v3/volumes/urls.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v3/volumes/urls.go new file mode 100644 index 00000000..17072490 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v3/volumes/urls.go @@ -0,0 +1,23 @@ +package volumes + +import "github.com/gophercloud/gophercloud" + +func createURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("volumes") +} + +func listURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("volumes", "detail") +} + +func deleteURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("volumes", id) +} + +func getURL(c *gophercloud.ServiceClient, id string) string { + return deleteURL(c, id) +} + +func updateURL(c *gophercloud.ServiceClient, id string) string { + return deleteURL(c, id) +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v3/volumes/util.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v3/volumes/util.go new file mode 100644 index 00000000..e86c1b4b --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v3/volumes/util.go @@ -0,0 +1,22 @@ +package volumes + +import ( + "github.com/gophercloud/gophercloud" +) + +// WaitForStatus will continually poll the resource, checking for a particular +// status. It will do this for the amount of seconds defined. +func WaitForStatus(c *gophercloud.ServiceClient, id, status string, secs int) error { + return gophercloud.WaitFor(secs, func() (bool, error) { + current, err := Get(c, id).Extract() + if err != nil { + return false, err + } + + if current.Status == status { + return true, nil + } + + return false, nil + }) +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/client.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/client.go new file mode 100644 index 00000000..81c907c3 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/client.go @@ -0,0 +1,503 @@ +package openstack + +import ( + "fmt" + "reflect" + "strings" + + "github.com/gophercloud/gophercloud" + tokens2 "github.com/gophercloud/gophercloud/openstack/identity/v2/tokens" + "github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/ec2tokens" + "github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/oauth1" + tokens3 "github.com/gophercloud/gophercloud/openstack/identity/v3/tokens" + "github.com/gophercloud/gophercloud/openstack/utils" +) + +const ( + // v2 represents Keystone v2. + // It should never increase beyond 2.0. + v2 = "v2.0" + + // v3 represents Keystone v3. + // The version can be anything from v3 to v3.x. + v3 = "v3" +) + +/* +NewClient prepares an unauthenticated ProviderClient instance. +Most users will probably prefer using the AuthenticatedClient function +instead. + +This is useful if you wish to explicitly control the version of the identity +service that's used for authentication explicitly, for example. + +A basic example of using this would be: + + ao, err := openstack.AuthOptionsFromEnv() + provider, err := openstack.NewClient(ao.IdentityEndpoint) + client, err := openstack.NewIdentityV3(provider, gophercloud.EndpointOpts{}) +*/ +func NewClient(endpoint string) (*gophercloud.ProviderClient, error) { + base, err := utils.BaseEndpoint(endpoint) + if err != nil { + return nil, err + } + + endpoint = gophercloud.NormalizeURL(endpoint) + base = gophercloud.NormalizeURL(base) + + p := new(gophercloud.ProviderClient) + p.IdentityBase = base + p.IdentityEndpoint = endpoint + p.UseTokenLock() + + return p, nil +} + +/* +AuthenticatedClient logs in to an OpenStack cloud found at the identity endpoint +specified by the options, acquires a token, and returns a Provider Client +instance that's ready to operate. + +If the full path to a versioned identity endpoint was specified (example: +http://example.com:5000/v3), that path will be used as the endpoint to query. + +If a versionless endpoint was specified (example: http://example.com:5000/), +the endpoint will be queried to determine which versions of the identity service +are available, then chooses the most recent or most supported version. + +Example: + + ao, err := openstack.AuthOptionsFromEnv() + provider, err := openstack.AuthenticatedClient(ao) + client, err := openstack.NewNetworkV2(provider, gophercloud.EndpointOpts{ + Region: os.Getenv("OS_REGION_NAME"), + }) +*/ +func AuthenticatedClient(options gophercloud.AuthOptions) (*gophercloud.ProviderClient, error) { + client, err := NewClient(options.IdentityEndpoint) + if err != nil { + return nil, err + } + + err = Authenticate(client, options) + if err != nil { + return nil, err + } + return client, nil +} + +// Authenticate or re-authenticate against the most recent identity service +// supported at the provided endpoint. +func Authenticate(client *gophercloud.ProviderClient, options gophercloud.AuthOptions) error { + versions := []*utils.Version{ + {ID: v2, Priority: 20, Suffix: "/v2.0/"}, + {ID: v3, Priority: 30, Suffix: "/v3/"}, + } + + chosen, endpoint, err := utils.ChooseVersion(client, versions) + if err != nil { + return err + } + + switch chosen.ID { + case v2: + return v2auth(client, endpoint, options, gophercloud.EndpointOpts{}) + case v3: + return v3auth(client, endpoint, &options, gophercloud.EndpointOpts{}) + default: + // The switch statement must be out of date from the versions list. + return fmt.Errorf("Unrecognized identity version: %s", chosen.ID) + } +} + +// AuthenticateV2 explicitly authenticates against the identity v2 endpoint. +func AuthenticateV2(client *gophercloud.ProviderClient, options gophercloud.AuthOptions, eo gophercloud.EndpointOpts) error { + return v2auth(client, "", options, eo) +} + +func v2auth(client *gophercloud.ProviderClient, endpoint string, options gophercloud.AuthOptions, eo gophercloud.EndpointOpts) error { + v2Client, err := NewIdentityV2(client, eo) + if err != nil { + return err + } + + if endpoint != "" { + v2Client.Endpoint = endpoint + } + + v2Opts := tokens2.AuthOptions{ + IdentityEndpoint: options.IdentityEndpoint, + Username: options.Username, + Password: options.Password, + TenantID: options.TenantID, + TenantName: options.TenantName, + AllowReauth: options.AllowReauth, + TokenID: options.TokenID, + } + + result := tokens2.Create(v2Client, v2Opts) + + err = client.SetTokenAndAuthResult(result) + if err != nil { + return err + } + + catalog, err := result.ExtractServiceCatalog() + if err != nil { + return err + } + + if options.AllowReauth { + // here we're creating a throw-away client (tac). it's a copy of the user's provider client, but + // with the token and reauth func zeroed out. combined with setting `AllowReauth` to `false`, + // this should retry authentication only once + tac := *client + tac.SetThrowaway(true) + tac.ReauthFunc = nil + tac.SetTokenAndAuthResult(nil) + tao := options + tao.AllowReauth = false + client.ReauthFunc = func() error { + err := v2auth(&tac, endpoint, tao, eo) + if err != nil { + return err + } + client.CopyTokenFrom(&tac) + return nil + } + } + client.EndpointLocator = func(opts gophercloud.EndpointOpts) (string, error) { + return V2EndpointURL(catalog, opts) + } + + return nil +} + +// AuthenticateV3 explicitly authenticates against the identity v3 service. +func AuthenticateV3(client *gophercloud.ProviderClient, options tokens3.AuthOptionsBuilder, eo gophercloud.EndpointOpts) error { + return v3auth(client, "", options, eo) +} + +func v3auth(client *gophercloud.ProviderClient, endpoint string, opts tokens3.AuthOptionsBuilder, eo gophercloud.EndpointOpts) error { + // Override the generated service endpoint with the one returned by the version endpoint. + v3Client, err := NewIdentityV3(client, eo) + if err != nil { + return err + } + + if endpoint != "" { + v3Client.Endpoint = endpoint + } + + var catalog *tokens3.ServiceCatalog + + var tokenID string + // passthroughToken allows to passthrough the token without a scope + var passthroughToken bool + switch v := opts.(type) { + case *gophercloud.AuthOptions: + tokenID = v.TokenID + passthroughToken = (v.Scope == nil || *v.Scope == gophercloud.AuthScope{}) + case *tokens3.AuthOptions: + tokenID = v.TokenID + passthroughToken = (v.Scope == tokens3.Scope{}) + } + + if tokenID != "" && passthroughToken { + // passing through the token ID without requesting a new scope + if opts.CanReauth() { + return fmt.Errorf("cannot use AllowReauth, when the token ID is defined and auth scope is not set") + } + + v3Client.SetToken(tokenID) + result := tokens3.Get(v3Client, tokenID) + if result.Err != nil { + return result.Err + } + + err = client.SetTokenAndAuthResult(result) + if err != nil { + return err + } + + catalog, err = result.ExtractServiceCatalog() + if err != nil { + return err + } + } else { + var result tokens3.CreateResult + switch opts.(type) { + case *ec2tokens.AuthOptions: + result = ec2tokens.Create(v3Client, opts) + case *oauth1.AuthOptions: + result = oauth1.Create(v3Client, opts) + default: + result = tokens3.Create(v3Client, opts) + } + + err = client.SetTokenAndAuthResult(result) + if err != nil { + return err + } + + catalog, err = result.ExtractServiceCatalog() + if err != nil { + return err + } + } + + if opts.CanReauth() { + // here we're creating a throw-away client (tac). it's a copy of the user's provider client, but + // with the token and reauth func zeroed out. combined with setting `AllowReauth` to `false`, + // this should retry authentication only once + tac := *client + tac.SetThrowaway(true) + tac.ReauthFunc = nil + tac.SetTokenAndAuthResult(nil) + var tao tokens3.AuthOptionsBuilder + switch ot := opts.(type) { + case *gophercloud.AuthOptions: + o := *ot + o.AllowReauth = false + tao = &o + case *tokens3.AuthOptions: + o := *ot + o.AllowReauth = false + tao = &o + case *ec2tokens.AuthOptions: + o := *ot + o.AllowReauth = false + tao = &o + case *oauth1.AuthOptions: + o := *ot + o.AllowReauth = false + tao = &o + default: + tao = opts + } + client.ReauthFunc = func() error { + err := v3auth(&tac, endpoint, tao, eo) + if err != nil { + return err + } + client.CopyTokenFrom(&tac) + return nil + } + } + client.EndpointLocator = func(opts gophercloud.EndpointOpts) (string, error) { + return V3EndpointURL(catalog, opts) + } + + return nil +} + +// NewIdentityV2 creates a ServiceClient that may be used to interact with the +// v2 identity service. +func NewIdentityV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + endpoint := client.IdentityBase + "v2.0/" + clientType := "identity" + var err error + if !reflect.DeepEqual(eo, gophercloud.EndpointOpts{}) { + eo.ApplyDefaults(clientType) + endpoint, err = client.EndpointLocator(eo) + if err != nil { + return nil, err + } + } + + return &gophercloud.ServiceClient{ + ProviderClient: client, + Endpoint: endpoint, + Type: clientType, + }, nil +} + +// NewIdentityV3 creates a ServiceClient that may be used to access the v3 +// identity service. +func NewIdentityV3(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + endpoint := client.IdentityBase + "v3/" + clientType := "identity" + var err error + if !reflect.DeepEqual(eo, gophercloud.EndpointOpts{}) { + eo.ApplyDefaults(clientType) + endpoint, err = client.EndpointLocator(eo) + if err != nil { + return nil, err + } + } + + // Ensure endpoint still has a suffix of v3. + // This is because EndpointLocator might have found a versionless + // endpoint or the published endpoint is still /v2.0. In both + // cases, we need to fix the endpoint to point to /v3. + base, err := utils.BaseEndpoint(endpoint) + if err != nil { + return nil, err + } + + base = gophercloud.NormalizeURL(base) + + endpoint = base + "v3/" + + return &gophercloud.ServiceClient{ + ProviderClient: client, + Endpoint: endpoint, + Type: clientType, + }, nil +} + +func initClientOpts(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts, clientType string) (*gophercloud.ServiceClient, error) { + sc := new(gophercloud.ServiceClient) + eo.ApplyDefaults(clientType) + url, err := client.EndpointLocator(eo) + if err != nil { + return sc, err + } + sc.ProviderClient = client + sc.Endpoint = url + sc.Type = clientType + return sc, nil +} + +// NewBareMetalV1 creates a ServiceClient that may be used with the v1 +// bare metal package. +func NewBareMetalV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + return initClientOpts(client, eo, "baremetal") +} + +// NewBareMetalIntrospectionV1 creates a ServiceClient that may be used with the v1 +// bare metal introspection package. +func NewBareMetalIntrospectionV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + return initClientOpts(client, eo, "baremetal-introspection") +} + +// NewObjectStorageV1 creates a ServiceClient that may be used with the v1 +// object storage package. +func NewObjectStorageV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + return initClientOpts(client, eo, "object-store") +} + +// NewComputeV2 creates a ServiceClient that may be used with the v2 compute +// package. +func NewComputeV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + return initClientOpts(client, eo, "compute") +} + +// NewNetworkV2 creates a ServiceClient that may be used with the v2 network +// package. +func NewNetworkV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "network") + sc.ResourceBase = sc.Endpoint + "v2.0/" + return sc, err +} + +// NewBlockStorageV1 creates a ServiceClient that may be used to access the v1 +// block storage service. +func NewBlockStorageV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + return initClientOpts(client, eo, "volume") +} + +// NewBlockStorageV2 creates a ServiceClient that may be used to access the v2 +// block storage service. +func NewBlockStorageV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + return initClientOpts(client, eo, "volumev2") +} + +// NewBlockStorageV3 creates a ServiceClient that may be used to access the v3 block storage service. +func NewBlockStorageV3(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + return initClientOpts(client, eo, "volumev3") +} + +// NewSharedFileSystemV2 creates a ServiceClient that may be used to access the v2 shared file system service. +func NewSharedFileSystemV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + return initClientOpts(client, eo, "sharev2") +} + +// NewCDNV1 creates a ServiceClient that may be used to access the OpenStack v1 +// CDN service. +func NewCDNV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + return initClientOpts(client, eo, "cdn") +} + +// NewOrchestrationV1 creates a ServiceClient that may be used to access the v1 +// orchestration service. +func NewOrchestrationV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + return initClientOpts(client, eo, "orchestration") +} + +// NewDBV1 creates a ServiceClient that may be used to access the v1 DB service. +func NewDBV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + return initClientOpts(client, eo, "database") +} + +// NewDNSV2 creates a ServiceClient that may be used to access the v2 DNS +// service. +func NewDNSV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "dns") + sc.ResourceBase = sc.Endpoint + "v2/" + return sc, err +} + +// NewImageServiceV2 creates a ServiceClient that may be used to access the v2 +// image service. +func NewImageServiceV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "image") + sc.ResourceBase = sc.Endpoint + "v2/" + return sc, err +} + +// NewLoadBalancerV2 creates a ServiceClient that may be used to access the v2 +// load balancer service. +func NewLoadBalancerV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "load-balancer") + + // Fixes edge case having an OpenStack lb endpoint with trailing version number. + endpoint := strings.Replace(sc.Endpoint, "v2.0/", "", -1) + + sc.ResourceBase = endpoint + "v2.0/" + return sc, err +} + +// NewClusteringV1 creates a ServiceClient that may be used with the v1 clustering +// package. +func NewClusteringV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + return initClientOpts(client, eo, "clustering") +} + +// NewMessagingV2 creates a ServiceClient that may be used with the v2 messaging +// service. +func NewMessagingV2(client *gophercloud.ProviderClient, clientID string, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "messaging") + sc.MoreHeaders = map[string]string{"Client-ID": clientID} + return sc, err +} + +// NewContainerV1 creates a ServiceClient that may be used with v1 container package +func NewContainerV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + return initClientOpts(client, eo, "container") +} + +// NewKeyManagerV1 creates a ServiceClient that may be used with the v1 key +// manager service. +func NewKeyManagerV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "key-manager") + sc.ResourceBase = sc.Endpoint + "v1/" + return sc, err +} + +// NewContainerInfraV1 creates a ServiceClient that may be used with the v1 container infra management +// package. +func NewContainerInfraV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + return initClientOpts(client, eo, "container-infra") +} + +// NewWorkflowV2 creates a ServiceClient that may be used with the v2 workflow management package. +func NewWorkflowV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + return initClientOpts(client, eo, "workflowv2") +} + +// NewPlacementV1 creates a ServiceClient that may be used with the placement package. +func NewPlacementV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + return initClientOpts(client, eo, "placement") +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/availabilityzones/doc.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/availabilityzones/doc.go new file mode 100644 index 00000000..13b6d9d4 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/availabilityzones/doc.go @@ -0,0 +1,61 @@ +/* +Package availabilityzones provides the ability to get lists and detailed +availability zone information and to extend a server result with +availability zone information. + +Example of Extend server result with Availability Zone Information: + + type ServerWithAZ struct { + servers.Server + availabilityzones.ServerAvailabilityZoneExt + } + + var allServers []ServerWithAZ + + allPages, err := servers.List(client, nil).AllPages() + if err != nil { + panic("Unable to retrieve servers: %s", err) + } + + err = servers.ExtractServersInto(allPages, &allServers) + if err != nil { + panic("Unable to extract servers: %s", err) + } + + for _, server := range allServers { + fmt.Println(server.AvailabilityZone) + } + +Example of Get Availability Zone Information + + allPages, err := availabilityzones.List(computeClient).AllPages() + if err != nil { + panic(err) + } + + availabilityZoneInfo, err := availabilityzones.ExtractAvailabilityZones(allPages) + if err != nil { + panic(err) + } + + for _, zoneInfo := range availabilityZoneInfo { + fmt.Printf("%+v\n", zoneInfo) + } + +Example of Get Detailed Availability Zone Information + + allPages, err := availabilityzones.ListDetail(computeClient).AllPages() + if err != nil { + panic(err) + } + + availabilityZoneInfo, err := availabilityzones.ExtractAvailabilityZones(allPages) + if err != nil { + panic(err) + } + + for _, zoneInfo := range availabilityZoneInfo { + fmt.Printf("%+v\n", zoneInfo) + } +*/ +package availabilityzones diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/availabilityzones/requests.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/availabilityzones/requests.go new file mode 100644 index 00000000..f9a2e86e --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/availabilityzones/requests.go @@ -0,0 +1,20 @@ +package availabilityzones + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// List will return the existing availability zones. +func List(client *gophercloud.ServiceClient) pagination.Pager { + return pagination.NewPager(client, listURL(client), func(r pagination.PageResult) pagination.Page { + return AvailabilityZonePage{pagination.SinglePageBase(r)} + }) +} + +// ListDetail will return the existing availability zones with detailed information. +func ListDetail(client *gophercloud.ServiceClient) pagination.Pager { + return pagination.NewPager(client, listDetailURL(client), func(r pagination.PageResult) pagination.Page { + return AvailabilityZonePage{pagination.SinglePageBase(r)} + }) +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/availabilityzones/results.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/availabilityzones/results.go new file mode 100644 index 00000000..d48a0ea8 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/availabilityzones/results.go @@ -0,0 +1,76 @@ +package availabilityzones + +import ( + "encoding/json" + "time" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// ServerAvailabilityZoneExt is an extension to the base Server object. +type ServerAvailabilityZoneExt struct { + // AvailabilityZone is the availabilty zone the server is in. + AvailabilityZone string `json:"OS-EXT-AZ:availability_zone"` +} + +// ServiceState represents the state of a service in an AvailabilityZone. +type ServiceState struct { + Active bool `json:"active"` + Available bool `json:"available"` + UpdatedAt time.Time `json:"-"` +} + +// UnmarshalJSON to override default +func (r *ServiceState) UnmarshalJSON(b []byte) error { + type tmp ServiceState + var s struct { + tmp + UpdatedAt gophercloud.JSONRFC3339MilliNoZ `json:"updated_at"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = ServiceState(s.tmp) + + r.UpdatedAt = time.Time(s.UpdatedAt) + + return nil +} + +// Services is a map of services contained in an AvailabilityZone. +type Services map[string]ServiceState + +// Hosts is map of hosts/nodes contained in an AvailabilityZone. +// Each host can have multiple services. +type Hosts map[string]Services + +// ZoneState represents the current state of the availability zone. +type ZoneState struct { + // Returns true if the availability zone is available + Available bool `json:"available"` +} + +// AvailabilityZone contains all the information associated with an OpenStack +// AvailabilityZone. +type AvailabilityZone struct { + Hosts Hosts `json:"hosts"` + // The availability zone name + ZoneName string `json:"zoneName"` + ZoneState ZoneState `json:"zoneState"` +} + +type AvailabilityZonePage struct { + pagination.SinglePageBase +} + +// ExtractAvailabilityZones returns a slice of AvailabilityZones contained in a +// single page of results. +func ExtractAvailabilityZones(r pagination.Page) ([]AvailabilityZone, error) { + var s struct { + AvailabilityZoneInfo []AvailabilityZone `json:"availabilityZoneInfo"` + } + err := (r.(AvailabilityZonePage)).ExtractInto(&s) + return s.AvailabilityZoneInfo, err +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/availabilityzones/urls.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/availabilityzones/urls.go new file mode 100644 index 00000000..9d99ec74 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/availabilityzones/urls.go @@ -0,0 +1,11 @@ +package availabilityzones + +import "github.com/gophercloud/gophercloud" + +func listURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("os-availability-zone") +} + +func listDetailURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("os-availability-zone", "detail") +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/bootfromvolume/doc.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/bootfromvolume/doc.go new file mode 100644 index 00000000..79a09b33 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/bootfromvolume/doc.go @@ -0,0 +1,152 @@ +/* +Package bootfromvolume extends a server create request with the ability to +specify block device options. This can be used to boot a server from a block +storage volume as well as specify multiple ephemeral disks upon creation. + +It is recommended to refer to the Block Device Mapping documentation to see +all possible ways to configure a server's block devices at creation time: + +https://docs.openstack.org/nova/latest/user/block-device-mapping.html + +Note that this package implements `block_device_mapping_v2`. + +# Example of Creating a Server From an Image + +This example will boot a server from an image and use a standard ephemeral +disk as the server's root disk. This is virtually no different than creating +a server without using block device mappings. + + blockDevices := []bootfromvolume.BlockDevice{ + bootfromvolume.BlockDevice{ + BootIndex: 0, + DeleteOnTermination: true, + DestinationType: bootfromvolume.DestinationLocal, + SourceType: bootfromvolume.SourceImage, + UUID: "image-uuid", + }, + } + + serverCreateOpts := servers.CreateOpts{ + Name: "server_name", + FlavorRef: "flavor-uuid", + ImageRef: "image-uuid", + } + + createOpts := bootfromvolume.CreateOptsExt{ + CreateOptsBuilder: serverCreateOpts, + BlockDevice: blockDevices, + } + + server, err := bootfromvolume.Create(client, createOpts).Extract() + if err != nil { + panic(err) + } + +# Example of Creating a Server From a New Volume + +This example will create a block storage volume based on the given Image. The +server will use this volume as its root disk. + + blockDevices := []bootfromvolume.BlockDevice{ + bootfromvolume.BlockDevice{ + DeleteOnTermination: true, + DestinationType: bootfromvolume.DestinationVolume, + SourceType: bootfromvolume.SourceImage, + UUID: "image-uuid", + VolumeSize: 2, + }, + } + + serverCreateOpts := servers.CreateOpts{ + Name: "server_name", + FlavorRef: "flavor-uuid", + } + + createOpts := bootfromvolume.CreateOptsExt{ + CreateOptsBuilder: serverCreateOpts, + BlockDevice: blockDevices, + } + + server, err := bootfromvolume.Create(client, createOpts).Extract() + if err != nil { + panic(err) + } + +# Example of Creating a Server From an Existing Volume + +This example will create a server with an existing volume as its root disk. + + blockDevices := []bootfromvolume.BlockDevice{ + bootfromvolume.BlockDevice{ + DeleteOnTermination: true, + DestinationType: bootfromvolume.DestinationVolume, + SourceType: bootfromvolume.SourceVolume, + UUID: "volume-uuid", + }, + } + + serverCreateOpts := servers.CreateOpts{ + Name: "server_name", + FlavorRef: "flavor-uuid", + } + + createOpts := bootfromvolume.CreateOptsExt{ + CreateOptsBuilder: serverCreateOpts, + BlockDevice: blockDevices, + } + + server, err := bootfromvolume.Create(client, createOpts).Extract() + if err != nil { + panic(err) + } + +# Example of Creating a Server with Multiple Ephemeral Disks + +This example will create a server with multiple ephemeral disks. The first +block device will be based off of an existing Image. Each additional +ephemeral disks must have an index of -1. + + blockDevices := []bootfromvolume.BlockDevice{ + bootfromvolume.BlockDevice{ + BootIndex: 0, + DestinationType: bootfromvolume.DestinationLocal, + DeleteOnTermination: true, + SourceType: bootfromvolume.SourceImage, + UUID: "image-uuid", + VolumeSize: 5, + }, + bootfromvolume.BlockDevice{ + BootIndex: -1, + DestinationType: bootfromvolume.DestinationLocal, + DeleteOnTermination: true, + GuestFormat: "ext4", + SourceType: bootfromvolume.SourceBlank, + VolumeSize: 1, + }, + bootfromvolume.BlockDevice{ + BootIndex: -1, + DestinationType: bootfromvolume.DestinationLocal, + DeleteOnTermination: true, + GuestFormat: "ext4", + SourceType: bootfromvolume.SourceBlank, + VolumeSize: 1, + }, + } + + serverCreateOpts := servers.CreateOpts{ + Name: "server_name", + FlavorRef: "flavor-uuid", + ImageRef: "image-uuid", + } + + createOpts := bootfromvolume.CreateOptsExt{ + CreateOptsBuilder: serverCreateOpts, + BlockDevice: blockDevices, + } + + server, err := bootfromvolume.Create(client, createOpts).Extract() + if err != nil { + panic(err) + } +*/ +package bootfromvolume diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/bootfromvolume/requests.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/bootfromvolume/requests.go new file mode 100644 index 00000000..05f45aec --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/bootfromvolume/requests.go @@ -0,0 +1,142 @@ +package bootfromvolume + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/compute/v2/servers" +) + +type ( + // DestinationType represents the type of medium being used as the + // destination of the bootable device. + DestinationType string + + // SourceType represents the type of medium being used as the source of the + // bootable device. + SourceType string +) + +const ( + // DestinationLocal DestinationType is for using an ephemeral disk as the + // destination. + DestinationLocal DestinationType = "local" + + // DestinationVolume DestinationType is for using a volume as the destination. + DestinationVolume DestinationType = "volume" + + // SourceBlank SourceType is for a "blank" or empty source. + SourceBlank SourceType = "blank" + + // SourceImage SourceType is for using images as the source of a block device. + SourceImage SourceType = "image" + + // SourceSnapshot SourceType is for using a volume snapshot as the source of + // a block device. + SourceSnapshot SourceType = "snapshot" + + // SourceVolume SourceType is for using a volume as the source of block + // device. + SourceVolume SourceType = "volume" +) + +// BlockDevice is a structure with options for creating block devices in a +// server. The block device may be created from an image, snapshot, new volume, +// or existing volume. The destination may be a new volume, existing volume +// which will be attached to the instance, ephemeral disk, or boot device. +type BlockDevice struct { + // SourceType must be one of: "volume", "snapshot", "image", or "blank". + SourceType SourceType `json:"source_type" required:"true"` + + // UUID is the unique identifier for the existing volume, snapshot, or + // image (see above). + UUID string `json:"uuid,omitempty"` + + // BootIndex is the boot index. It defaults to 0. + BootIndex int `json:"boot_index"` + + // DeleteOnTermination specifies whether or not to delete the attached volume + // when the server is deleted. Defaults to `false`. + DeleteOnTermination bool `json:"delete_on_termination"` + + // DestinationType is the type that gets created. Possible values are "volume" + // and "local". + DestinationType DestinationType `json:"destination_type,omitempty"` + + // GuestFormat specifies the format of the block device. + // Not specifying this will cause the device to be formatted to the default in Nova + // which is currently vfat. + // https://opendev.org/openstack/nova/src/commit/d0b459423dd81644e8d9382b6c87fabaa4f03ad4/nova/privsep/fs.py#L257 + GuestFormat string `json:"guest_format,omitempty"` + + // VolumeSize is the size of the volume to create (in gigabytes). This can be + // omitted for existing volumes. + VolumeSize int `json:"volume_size,omitempty"` + + // DeviceType specifies the device type of the block devices. + // Examples of this are disk, cdrom, floppy, lun, etc. + DeviceType string `json:"device_type,omitempty"` + + // DiskBus is the bus type of the block devices. + // Examples of this are ide, usb, virtio, scsi, etc. + DiskBus string `json:"disk_bus,omitempty"` + + // VolumeType is the volume type of the block device. + // This requires Compute API microversion 2.67 or later. + VolumeType string `json:"volume_type,omitempty"` + + // Tag is an arbitrary string that can be applied to a block device. + // Information about the device tags can be obtained from the metadata API + // and the config drive, allowing devices to be easily identified. + // This requires Compute API microversion 2.42 or later. + Tag string `json:"tag,omitempty"` +} + +// CreateOptsExt is a structure that extends the server `CreateOpts` structure +// by allowing for a block device mapping. +type CreateOptsExt struct { + servers.CreateOptsBuilder + BlockDevice []BlockDevice `json:"block_device_mapping_v2,omitempty"` +} + +// ToServerCreateMap adds the block device mapping option to the base server +// creation options. +func (opts CreateOptsExt) ToServerCreateMap() (map[string]interface{}, error) { + base, err := opts.CreateOptsBuilder.ToServerCreateMap() + if err != nil { + return nil, err + } + + if len(opts.BlockDevice) == 0 { + err := gophercloud.ErrMissingInput{} + err.Argument = "bootfromvolume.CreateOptsExt.BlockDevice" + return nil, err + } + + serverMap := base["server"].(map[string]interface{}) + + blockDevice := make([]map[string]interface{}, len(opts.BlockDevice)) + + for i, bd := range opts.BlockDevice { + b, err := gophercloud.BuildRequestBody(bd, "") + if err != nil { + return nil, err + } + blockDevice[i] = b + } + serverMap["block_device_mapping_v2"] = blockDevice + + return base, nil +} + +// Create requests the creation of a server from the given block device mapping. +func Create(client *gophercloud.ServiceClient, opts servers.CreateOptsBuilder) (r servers.CreateResult) { + b, err := opts.ToServerCreateMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(createURL(client), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 202}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/bootfromvolume/results.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/bootfromvolume/results.go new file mode 100644 index 00000000..ba1eafab --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/bootfromvolume/results.go @@ -0,0 +1,12 @@ +package bootfromvolume + +import ( + os "github.com/gophercloud/gophercloud/openstack/compute/v2/servers" +) + +// CreateResult temporarily contains the response from a Create call. +// It embeds the standard servers.CreateResults type and so can be used the +// same way as a standard server request result. +type CreateResult struct { + os.CreateResult +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/bootfromvolume/urls.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/bootfromvolume/urls.go new file mode 100644 index 00000000..e74422d0 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/bootfromvolume/urls.go @@ -0,0 +1,7 @@ +package bootfromvolume + +import "github.com/gophercloud/gophercloud" + +func createURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("servers") +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs/doc.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs/doc.go new file mode 100644 index 00000000..9fa914ec --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs/doc.go @@ -0,0 +1,119 @@ +/* +Package keypairs provides the ability to manage key pairs as well as create +servers with a specified key pair. + +Example to List Key Pairs + + allPages, err := keypairs.List(computeClient, nil).AllPages() + if err != nil { + panic(err) + } + + allKeyPairs, err := keypairs.ExtractKeyPairs(allPages) + if err != nil { + panic(err) + } + + for _, kp := range allKeyPairs { + fmt.Printf("%+v\n", kp) + } + +Example to List Key Pairs using microversion 2.10 or greater + + client.Microversion = "2.10" + + listOpts := keypairs.ListOpts{ + UserID: "user-id", + } + + allPages, err := keypairs.List(computeClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allKeyPairs, err := keypairs.ExtractKeyPairs(allPages) + if err != nil { + panic(err) + } + + for _, kp := range allKeyPairs { + fmt.Printf("%+v\n", kp) + } + +Example to Create a Key Pair + + createOpts := keypairs.CreateOpts{ + Name: "keypair-name", + } + + keypair, err := keypairs.Create(computeClient, createOpts).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v", keypair) + +Example to Import a Key Pair + + createOpts := keypairs.CreateOpts{ + Name: "keypair-name", + PublicKey: "public-key", + } + + keypair, err := keypairs.Create(computeClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a Key Pair + + err := keypairs.Delete(computeClient, "keypair-name", nil).ExtractErr() + if err != nil { + panic(err) + } + +Example to Delete a Key Pair owned by a certain user using microversion 2.10 or greater + + client.Microversion = "2.10" + + deleteOpts := keypairs.DeleteOpts{ + UserID: "user-id", + } + + err := keypairs.Delete(client, "keypair-name", deleteOpts).ExtractErr() + if err != nil { + panic(err) + } + +Example to Create a Server With a Key Pair + + serverCreateOpts := servers.CreateOpts{ + Name: "server_name", + ImageRef: "image-uuid", + FlavorRef: "flavor-uuid", + } + + createOpts := keypairs.CreateOptsExt{ + CreateOptsBuilder: serverCreateOpts, + KeyName: "keypair-name", + } + + server, err := servers.Create(computeClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Get a Key Pair owned by a certain user using microversion 2.10 or greater + + client.Microversion = "2.10" + + getOpts := keypairs.GetOpts{ + UserID: "user-id", + } + + keypair, err := keypairs.Get(computeClient, "keypair-name", getOpts).Extract() + if err != nil { + panic(err) + } +*/ +package keypairs diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs/requests.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs/requests.go new file mode 100644 index 00000000..aba11fb4 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs/requests.go @@ -0,0 +1,183 @@ +package keypairs + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/compute/v2/servers" + "github.com/gophercloud/gophercloud/pagination" +) + +// CreateOptsExt adds a KeyPair option to the base CreateOpts. +type CreateOptsExt struct { + servers.CreateOptsBuilder + + // KeyName is the name of the key pair. + KeyName string `json:"key_name,omitempty"` +} + +// ToServerCreateMap adds the key_name to the base server creation options. +func (opts CreateOptsExt) ToServerCreateMap() (map[string]interface{}, error) { + base, err := opts.CreateOptsBuilder.ToServerCreateMap() + if err != nil { + return nil, err + } + + if opts.KeyName == "" { + return base, nil + } + + serverMap := base["server"].(map[string]interface{}) + serverMap["key_name"] = opts.KeyName + + return base, nil +} + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToKeyPairListQuery() (string, error) +} + +// ListOpts enables listing KeyPairs based on specific attributes. +type ListOpts struct { + // UserID is the user ID that owns the key pair. + // This requires microversion 2.10 or higher. + UserID string `q:"user_id"` +} + +// ToKeyPairListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToKeyPairListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns a Pager that allows you to iterate over a collection of KeyPairs. +func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToKeyPairListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return KeyPairPage{pagination.SinglePageBase(r)} + }) +} + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToKeyPairCreateMap() (map[string]interface{}, error) +} + +// CreateOpts specifies KeyPair creation or import parameters. +type CreateOpts struct { + // Name is a friendly name to refer to this KeyPair in other services. + Name string `json:"name" required:"true"` + + // UserID [optional] is the user_id for a keypair. + // This allows administrative users to upload keys for other users than themselves. + // This requires microversion 2.10 or higher. + UserID string `json:"user_id,omitempty"` + + // The type of the keypair. Allowed values are ssh or x509 + // This requires microversion 2.2 or higher. + Type string `json:"type,omitempty"` + + // PublicKey [optional] is a pregenerated OpenSSH-formatted public key. + // If provided, this key will be imported and no new key will be created. + PublicKey string `json:"public_key,omitempty"` +} + +// ToKeyPairCreateMap constructs a request body from CreateOpts. +func (opts CreateOpts) ToKeyPairCreateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "keypair") +} + +// Create requests the creation of a new KeyPair on the server, or to import a +// pre-existing keypair. +func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToKeyPairCreateMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(createURL(client), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 201}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// GetOptsBuilder allows extensions to add additional parameters to the +// Get request. +type GetOptsBuilder interface { + ToKeyPairGetQuery() (string, error) +} + +// GetOpts enables retrieving KeyPairs based on specific attributes. +type GetOpts struct { + // UserID is the user ID that owns the key pair. + // This requires microversion 2.10 or higher. + UserID string `q:"user_id"` +} + +// ToKeyPairGetQuery formats a GetOpts into a query string. +func (opts GetOpts) ToKeyPairGetQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// Get returns public data about a previously uploaded KeyPair. +func Get(client *gophercloud.ServiceClient, name string, opts GetOptsBuilder) (r GetResult) { + url := getURL(client, name) + if opts != nil { + query, err := opts.ToKeyPairGetQuery() + if err != nil { + r.Err = err + return + } + url += query + } + + resp, err := client.Get(url, &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// DeleteOptsBuilder allows extensions to add additional parameters to the +// Delete request. +type DeleteOptsBuilder interface { + ToKeyPairDeleteQuery() (string, error) +} + +// DeleteOpts enables deleting KeyPairs based on specific attributes. +type DeleteOpts struct { + // UserID is the user ID of the user that owns the key pair. + // This requires microversion 2.10 or higher. + UserID string `q:"user_id"` +} + +// ToKeyPairDeleteQuery formats a DeleteOpts into a query string. +func (opts DeleteOpts) ToKeyPairDeleteQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// Delete requests the deletion of a previous stored KeyPair from the server. +func Delete(client *gophercloud.ServiceClient, name string, opts DeleteOptsBuilder) (r DeleteResult) { + url := deleteURL(client, name) + if opts != nil { + query, err := opts.ToKeyPairDeleteQuery() + if err != nil { + r.Err = err + return + } + url += query + } + + resp, err := client.Delete(url, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs/results.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs/results.go new file mode 100644 index 00000000..0ac05c36 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs/results.go @@ -0,0 +1,98 @@ +package keypairs + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// KeyPair is an SSH key known to the OpenStack Cloud that is available to be +// injected into servers. +type KeyPair struct { + // Name is used to refer to this keypair from other services within this + // region. + Name string `json:"name"` + + // Fingerprint is a short sequence of bytes that can be used to authenticate + // or validate a longer public key. + Fingerprint string `json:"fingerprint"` + + // PublicKey is the public key from this pair, in OpenSSH format. + // "ssh-rsa AAAAB3Nz..." + PublicKey string `json:"public_key"` + + // PrivateKey is the private key from this pair, in PEM format. + // "-----BEGIN RSA PRIVATE KEY-----\nMIICXA..." + // It is only present if this KeyPair was just returned from a Create call. + PrivateKey string `json:"private_key"` + + // UserID is the user who owns this KeyPair. + UserID string `json:"user_id"` + + // The type of the keypair + Type string `json:"type"` +} + +// KeyPairPage stores a single page of all KeyPair results from a List call. +// Use the ExtractKeyPairs function to convert the results to a slice of +// KeyPairs. +type KeyPairPage struct { + pagination.SinglePageBase +} + +// IsEmpty determines whether or not a KeyPairPage is empty. +func (page KeyPairPage) IsEmpty() (bool, error) { + if page.StatusCode == 204 { + return true, nil + } + + ks, err := ExtractKeyPairs(page) + return len(ks) == 0, err +} + +// ExtractKeyPairs interprets a page of results as a slice of KeyPairs. +func ExtractKeyPairs(r pagination.Page) ([]KeyPair, error) { + type pair struct { + KeyPair KeyPair `json:"keypair"` + } + var s struct { + KeyPairs []pair `json:"keypairs"` + } + err := (r.(KeyPairPage)).ExtractInto(&s) + results := make([]KeyPair, len(s.KeyPairs)) + for i, pair := range s.KeyPairs { + results[i] = pair.KeyPair + } + return results, err +} + +type keyPairResult struct { + gophercloud.Result +} + +// Extract is a method that attempts to interpret any KeyPair resource response +// as a KeyPair struct. +func (r keyPairResult) Extract() (*KeyPair, error) { + var s struct { + KeyPair *KeyPair `json:"keypair"` + } + err := r.ExtractInto(&s) + return s.KeyPair, err +} + +// CreateResult is the response from a Create operation. Call its Extract method +// to interpret it as a KeyPair. +type CreateResult struct { + keyPairResult +} + +// GetResult is the response from a Get operation. Call its Extract method to +// interpret it as a KeyPair. +type GetResult struct { + keyPairResult +} + +// DeleteResult is the response from a Delete operation. Call its ExtractErr +// method to determine if the call succeeded or failed. +type DeleteResult struct { + gophercloud.ErrResult +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs/urls.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs/urls.go new file mode 100644 index 00000000..fec38f36 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs/urls.go @@ -0,0 +1,25 @@ +package keypairs + +import "github.com/gophercloud/gophercloud" + +const resourcePath = "os-keypairs" + +func resourceURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(resourcePath) +} + +func listURL(c *gophercloud.ServiceClient) string { + return resourceURL(c) +} + +func createURL(c *gophercloud.ServiceClient) string { + return resourceURL(c) +} + +func getURL(c *gophercloud.ServiceClient, name string) string { + return c.ServiceURL(resourcePath, name) +} + +func deleteURL(c *gophercloud.ServiceClient, name string) string { + return getURL(c, name) +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/volumeattach/doc.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/volumeattach/doc.go new file mode 100644 index 00000000..857ab19c --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/volumeattach/doc.go @@ -0,0 +1,30 @@ +/* +Package volumeattach provides the ability to attach and detach volumes +from servers. + +Example to Attach a Volume + + serverID := "7ac8686c-de71-4acb-9600-ec18b1a1ed6d" + volumeID := "87463836-f0e2-4029-abf6-20c8892a3103" + + createOpts := volumeattach.CreateOpts{ + Device: "/dev/vdc", + VolumeID: volumeID, + } + + result, err := volumeattach.Create(computeClient, serverID, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Detach a Volume + + serverID := "7ac8686c-de71-4acb-9600-ec18b1a1ed6d" + volumeID := "ed081613-1c9b-4231-aa5e-ebfd4d87f983" + + err := volumeattach.Delete(computeClient, serverID, volumeID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package volumeattach diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/volumeattach/requests.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/volumeattach/requests.go new file mode 100644 index 00000000..fe0b1075 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/volumeattach/requests.go @@ -0,0 +1,71 @@ +package volumeattach + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// List returns a Pager that allows you to iterate over a collection of +// VolumeAttachments. +func List(client *gophercloud.ServiceClient, serverID string) pagination.Pager { + return pagination.NewPager(client, listURL(client, serverID), func(r pagination.PageResult) pagination.Page { + return VolumeAttachmentPage{pagination.SinglePageBase(r)} + }) +} + +// CreateOptsBuilder allows extensions to add parameters to the Create request. +type CreateOptsBuilder interface { + ToVolumeAttachmentCreateMap() (map[string]interface{}, error) +} + +// CreateOpts specifies volume attachment creation or import parameters. +type CreateOpts struct { + // Device is the device that the volume will attach to the instance as. + // Omit for "auto". + Device string `json:"device,omitempty"` + + // VolumeID is the ID of the volume to attach to the instance. + VolumeID string `json:"volumeId" required:"true"` + + // Tag is a device role tag that can be applied to a volume when attaching + // it to the VM. Requires 2.49 microversion + Tag string `json:"tag,omitempty"` + + // DeleteOnTermination specifies whether or not to delete the volume when the server + // is destroyed. Requires 2.79 microversion + DeleteOnTermination bool `json:"delete_on_termination,omitempty"` +} + +// ToVolumeAttachmentCreateMap constructs a request body from CreateOpts. +func (opts CreateOpts) ToVolumeAttachmentCreateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "volumeAttachment") +} + +// Create requests the creation of a new volume attachment on the server. +func Create(client *gophercloud.ServiceClient, serverID string, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToVolumeAttachmentCreateMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(createURL(client, serverID), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Get returns public data about a previously created VolumeAttachment. +func Get(client *gophercloud.ServiceClient, serverID, volumeID string) (r GetResult) { + resp, err := client.Get(getURL(client, serverID, volumeID), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Delete requests the deletion of a previous stored VolumeAttachment from +// the server. +func Delete(client *gophercloud.ServiceClient, serverID, volumeID string) (r DeleteResult) { + resp, err := client.Delete(deleteURL(client, serverID, volumeID), nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/volumeattach/results.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/volumeattach/results.go new file mode 100644 index 00000000..e5f565a3 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/volumeattach/results.go @@ -0,0 +1,89 @@ +package volumeattach + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// VolumeAttachment contains attachment information between a volume +// and server. +type VolumeAttachment struct { + // ID is a unique id of the attachment. + ID string `json:"id"` + + // Device is what device the volume is attached as. + Device string `json:"device"` + + // VolumeID is the ID of the attached volume. + VolumeID string `json:"volumeId"` + + // ServerID is the ID of the instance that has the volume attached. + ServerID string `json:"serverId"` + + // Tag is a device role tag that can be applied to a volume when attaching + // it to the VM. Requires 2.70 microversion + Tag *string `json:"tag"` + + // DeleteOnTermination specifies whether or not to delete the volume when the server + // is destroyed. Requires 2.79 microversion + DeleteOnTermination *bool `json:"delete_on_termination"` +} + +// VolumeAttachmentPage stores a single page all of VolumeAttachment +// results from a List call. +type VolumeAttachmentPage struct { + pagination.SinglePageBase +} + +// IsEmpty determines whether or not a VolumeAttachmentPage is empty. +func (page VolumeAttachmentPage) IsEmpty() (bool, error) { + if page.StatusCode == 204 { + return true, nil + } + + va, err := ExtractVolumeAttachments(page) + return len(va) == 0, err +} + +// ExtractVolumeAttachments interprets a page of results as a slice of +// VolumeAttachment. +func ExtractVolumeAttachments(r pagination.Page) ([]VolumeAttachment, error) { + var s struct { + VolumeAttachments []VolumeAttachment `json:"volumeAttachments"` + } + err := (r.(VolumeAttachmentPage)).ExtractInto(&s) + return s.VolumeAttachments, err +} + +// VolumeAttachmentResult is the result from a volume attachment operation. +type VolumeAttachmentResult struct { + gophercloud.Result +} + +// Extract is a method that attempts to interpret any VolumeAttachment resource +// response as a VolumeAttachment struct. +func (r VolumeAttachmentResult) Extract() (*VolumeAttachment, error) { + var s struct { + VolumeAttachment *VolumeAttachment `json:"volumeAttachment"` + } + err := r.ExtractInto(&s) + return s.VolumeAttachment, err +} + +// CreateResult is the response from a Create operation. Call its Extract method +// to interpret it as a VolumeAttachment. +type CreateResult struct { + VolumeAttachmentResult +} + +// GetResult is the response from a Get operation. Call its Extract method to +// interpret it as a VolumeAttachment. +type GetResult struct { + VolumeAttachmentResult +} + +// DeleteResult is the response from a Delete operation. Call its ExtractErr +// method to determine if the call succeeded or failed. +type DeleteResult struct { + gophercloud.ErrResult +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/volumeattach/urls.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/volumeattach/urls.go new file mode 100644 index 00000000..083f8dc4 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/volumeattach/urls.go @@ -0,0 +1,25 @@ +package volumeattach + +import "github.com/gophercloud/gophercloud" + +const resourcePath = "os-volume_attachments" + +func resourceURL(c *gophercloud.ServiceClient, serverID string) string { + return c.ServiceURL("servers", serverID, resourcePath) +} + +func listURL(c *gophercloud.ServiceClient, serverID string) string { + return resourceURL(c, serverID) +} + +func createURL(c *gophercloud.ServiceClient, serverID string) string { + return resourceURL(c, serverID) +} + +func getURL(c *gophercloud.ServiceClient, serverID, aID string) string { + return c.ServiceURL("servers", serverID, resourcePath, aID) +} + +func deleteURL(c *gophercloud.ServiceClient, serverID, aID string) string { + return getURL(c, serverID, aID) +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/doc.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/doc.go new file mode 100644 index 00000000..747966d8 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/doc.go @@ -0,0 +1,150 @@ +/* +Package flavors provides information and interaction with the flavor API +in the OpenStack Compute service. + +A flavor is an available hardware configuration for a server. Each flavor +has a unique combination of disk space, memory capacity and priority for CPU +time. + +Example to List Flavors + + listOpts := flavors.ListOpts{ + AccessType: flavors.PublicAccess, + } + + allPages, err := flavors.ListDetail(computeClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allFlavors, err := flavors.ExtractFlavors(allPages) + if err != nil { + panic(err) + } + + for _, flavor := range allFlavors { + fmt.Printf("%+v\n", flavor) + } + +Example to Create a Flavor + + createOpts := flavors.CreateOpts{ + ID: "1", + Name: "m1.tiny", + Disk: gophercloud.IntToPointer(1), + RAM: 512, + VCPUs: 1, + RxTxFactor: 1.0, + } + + flavor, err := flavors.Create(computeClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Update a Flavor + + flavorID := "e91758d6-a54a-4778-ad72-0c73a1cb695b" + + updateOpts := flavors.UpdateOpts{ + Description: "This is a good description" + } + + flavor, err := flavors.Update(computeClient, flavorID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to List Flavor Access + + flavorID := "e91758d6-a54a-4778-ad72-0c73a1cb695b" + + allPages, err := flavors.ListAccesses(computeClient, flavorID).AllPages() + if err != nil { + panic(err) + } + + allAccesses, err := flavors.ExtractAccesses(allPages) + if err != nil { + panic(err) + } + + for _, access := range allAccesses { + fmt.Printf("%+v", access) + } + +Example to Grant Access to a Flavor + + flavorID := "e91758d6-a54a-4778-ad72-0c73a1cb695b" + + accessOpts := flavors.AddAccessOpts{ + Tenant: "15153a0979884b59b0592248ef947921", + } + + accessList, err := flavors.AddAccess(computeClient, flavor.ID, accessOpts).Extract() + if err != nil { + panic(err) + } + +Example to Remove/Revoke Access to a Flavor + + flavorID := "e91758d6-a54a-4778-ad72-0c73a1cb695b" + + accessOpts := flavors.RemoveAccessOpts{ + Tenant: "15153a0979884b59b0592248ef947921", + } + + accessList, err := flavors.RemoveAccess(computeClient, flavor.ID, accessOpts).Extract() + if err != nil { + panic(err) + } + +Example to Create Extra Specs for a Flavor + + flavorID := "e91758d6-a54a-4778-ad72-0c73a1cb695b" + + createOpts := flavors.ExtraSpecsOpts{ + "hw:cpu_policy": "CPU-POLICY", + "hw:cpu_thread_policy": "CPU-THREAD-POLICY", + } + createdExtraSpecs, err := flavors.CreateExtraSpecs(computeClient, flavorID, createOpts).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v", createdExtraSpecs) + +Example to Get Extra Specs for a Flavor + + flavorID := "e91758d6-a54a-4778-ad72-0c73a1cb695b" + + extraSpecs, err := flavors.ListExtraSpecs(computeClient, flavorID).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v", extraSpecs) + +Example to Update Extra Specs for a Flavor + + flavorID := "e91758d6-a54a-4778-ad72-0c73a1cb695b" + + updateOpts := flavors.ExtraSpecsOpts{ + "hw:cpu_thread_policy": "CPU-THREAD-POLICY-UPDATED", + } + updatedExtraSpec, err := flavors.UpdateExtraSpec(computeClient, flavorID, updateOpts).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v", updatedExtraSpec) + +Example to Delete an Extra Spec for a Flavor + + flavorID := "e91758d6-a54a-4778-ad72-0c73a1cb695b" + err := flavors.DeleteExtraSpec(computeClient, flavorID, "hw:cpu_thread_policy").ExtractErr() + if err != nil { + panic(err) + } +*/ +package flavors diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/requests.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/requests.go new file mode 100644 index 00000000..3887cdfd --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/requests.go @@ -0,0 +1,364 @@ +package flavors + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToFlavorListQuery() (string, error) +} + +/* +AccessType maps to OpenStack's Flavor.is_public field. Although the is_public +field is boolean, the request options are ternary, which is why AccessType is +a string. The following values are allowed: + +The AccessType arguement is optional, and if it is not supplied, OpenStack +returns the PublicAccess flavors. +*/ +type AccessType string + +const ( + // PublicAccess returns public flavors and private flavors associated with + // that project. + PublicAccess AccessType = "true" + + // PrivateAccess (admin only) returns private flavors, across all projects. + PrivateAccess AccessType = "false" + + // AllAccess (admin only) returns public and private flavors across all + // projects. + AllAccess AccessType = "None" +) + +/* +ListOpts filters the results returned by the List() function. +For example, a flavor with a minDisk field of 10 will not be returned if you +specify MinDisk set to 20. + +Typically, software will use the last ID of the previous call to List to set +the Marker for the current call. +*/ +type ListOpts struct { + // ChangesSince, if provided, instructs List to return only those things which + // have changed since the timestamp provided. + ChangesSince string `q:"changes-since"` + + // MinDisk and MinRAM, if provided, elides flavors which do not meet your + // criteria. + MinDisk int `q:"minDisk"` + MinRAM int `q:"minRam"` + + // SortDir allows to select sort direction. + // It can be "asc" or "desc" (default). + SortDir string `q:"sort_dir"` + + // SortKey allows to sort by one of the flavors attributes. + // Default is flavorid. + SortKey string `q:"sort_key"` + + // Marker and Limit control paging. + // Marker instructs List where to start listing from. + Marker string `q:"marker"` + + // Limit instructs List to refrain from sending excessively large lists of + // flavors. + Limit int `q:"limit"` + + // AccessType, if provided, instructs List which set of flavors to return. + // If IsPublic not provided, flavors for the current project are returned. + AccessType AccessType `q:"is_public"` +} + +// ToFlavorListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToFlavorListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// ListDetail instructs OpenStack to provide a list of flavors. +// You may provide criteria by which List curtails its results for easier +// processing. +func ListDetail(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToFlavorListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return FlavorPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +type CreateOptsBuilder interface { + ToFlavorCreateMap() (map[string]interface{}, error) +} + +// CreateOpts specifies parameters used for creating a flavor. +type CreateOpts struct { + // Name is the name of the flavor. + Name string `json:"name" required:"true"` + + // RAM is the memory of the flavor, measured in MB. + RAM int `json:"ram" required:"true"` + + // VCPUs is the number of vcpus for the flavor. + VCPUs int `json:"vcpus" required:"true"` + + // Disk the amount of root disk space, measured in GB. + Disk *int `json:"disk" required:"true"` + + // ID is a unique ID for the flavor. + ID string `json:"id,omitempty"` + + // Swap is the amount of swap space for the flavor, measured in MB. + Swap *int `json:"swap,omitempty"` + + // RxTxFactor alters the network bandwidth of a flavor. + RxTxFactor float64 `json:"rxtx_factor,omitempty"` + + // IsPublic flags a flavor as being available to all projects or not. + IsPublic *bool `json:"os-flavor-access:is_public,omitempty"` + + // Ephemeral is the amount of ephemeral disk space, measured in GB. + Ephemeral *int `json:"OS-FLV-EXT-DATA:ephemeral,omitempty"` + + // Description is a free form description of the flavor. Limited to + // 65535 characters in length. Only printable characters are allowed. + // New in version 2.55 + Description string `json:"description,omitempty"` +} + +// ToFlavorCreateMap constructs a request body from CreateOpts. +func (opts CreateOpts) ToFlavorCreateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "flavor") +} + +// Create requests the creation of a new flavor. +func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToFlavorCreateMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(createURL(client), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 201}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +type UpdateOptsBuilder interface { + ToFlavorUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts specifies parameters used for updating a flavor. +type UpdateOpts struct { + // Description is a free form description of the flavor. Limited to + // 65535 characters in length. Only printable characters are allowed. + // New in version 2.55 + Description string `json:"description,omitempty"` +} + +// ToFlavorUpdateMap constructs a request body from UpdateOpts. +func (opts UpdateOpts) ToFlavorUpdateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "flavor") +} + +// Update requests the update of a new flavor. +func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToFlavorUpdateMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Put(updateURL(client, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Get retrieves details of a single flavor. Use Extract to convert its +// result into a Flavor. +func Get(client *gophercloud.ServiceClient, id string) (r GetResult) { + resp, err := client.Get(getURL(client, id), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Delete deletes the specified flavor ID. +func Delete(client *gophercloud.ServiceClient, id string) (r DeleteResult) { + resp, err := client.Delete(deleteURL(client, id), nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// ListAccesses retrieves the tenants which have access to a flavor. +func ListAccesses(client *gophercloud.ServiceClient, id string) pagination.Pager { + url := accessURL(client, id) + + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return AccessPage{pagination.SinglePageBase(r)} + }) +} + +// AddAccessOptsBuilder allows extensions to add additional parameters to the +// AddAccess requests. +type AddAccessOptsBuilder interface { + ToFlavorAddAccessMap() (map[string]interface{}, error) +} + +// AddAccessOpts represents options for adding access to a flavor. +type AddAccessOpts struct { + // Tenant is the project/tenant ID to grant access. + Tenant string `json:"tenant"` +} + +// ToFlavorAddAccessMap constructs a request body from AddAccessOpts. +func (opts AddAccessOpts) ToFlavorAddAccessMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "addTenantAccess") +} + +// AddAccess grants a tenant/project access to a flavor. +func AddAccess(client *gophercloud.ServiceClient, id string, opts AddAccessOptsBuilder) (r AddAccessResult) { + b, err := opts.ToFlavorAddAccessMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(accessActionURL(client, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// RemoveAccessOptsBuilder allows extensions to add additional parameters to the +// RemoveAccess requests. +type RemoveAccessOptsBuilder interface { + ToFlavorRemoveAccessMap() (map[string]interface{}, error) +} + +// RemoveAccessOpts represents options for removing access to a flavor. +type RemoveAccessOpts struct { + // Tenant is the project/tenant ID to grant access. + Tenant string `json:"tenant"` +} + +// ToFlavorRemoveAccessMap constructs a request body from RemoveAccessOpts. +func (opts RemoveAccessOpts) ToFlavorRemoveAccessMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "removeTenantAccess") +} + +// RemoveAccess removes/revokes a tenant/project access to a flavor. +func RemoveAccess(client *gophercloud.ServiceClient, id string, opts RemoveAccessOptsBuilder) (r RemoveAccessResult) { + b, err := opts.ToFlavorRemoveAccessMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(accessActionURL(client, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// ExtraSpecs requests all the extra-specs for the given flavor ID. +func ListExtraSpecs(client *gophercloud.ServiceClient, flavorID string) (r ListExtraSpecsResult) { + resp, err := client.Get(extraSpecsListURL(client, flavorID), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +func GetExtraSpec(client *gophercloud.ServiceClient, flavorID string, key string) (r GetExtraSpecResult) { + resp, err := client.Get(extraSpecsGetURL(client, flavorID, key), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// CreateExtraSpecsOptsBuilder allows extensions to add additional parameters to the +// CreateExtraSpecs requests. +type CreateExtraSpecsOptsBuilder interface { + ToFlavorExtraSpecsCreateMap() (map[string]interface{}, error) +} + +// ExtraSpecsOpts is a map that contains key-value pairs. +type ExtraSpecsOpts map[string]string + +// ToFlavorExtraSpecsCreateMap assembles a body for a Create request based on +// the contents of ExtraSpecsOpts. +func (opts ExtraSpecsOpts) ToFlavorExtraSpecsCreateMap() (map[string]interface{}, error) { + return map[string]interface{}{"extra_specs": opts}, nil +} + +// CreateExtraSpecs will create or update the extra-specs key-value pairs for +// the specified Flavor. +func CreateExtraSpecs(client *gophercloud.ServiceClient, flavorID string, opts CreateExtraSpecsOptsBuilder) (r CreateExtraSpecsResult) { + b, err := opts.ToFlavorExtraSpecsCreateMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(extraSpecsCreateURL(client, flavorID), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// UpdateExtraSpecOptsBuilder allows extensions to add additional parameters to +// the Update request. +type UpdateExtraSpecOptsBuilder interface { + ToFlavorExtraSpecUpdateMap() (map[string]string, string, error) +} + +// ToFlavorExtraSpecUpdateMap assembles a body for an Update request based on +// the contents of a ExtraSpecOpts. +func (opts ExtraSpecsOpts) ToFlavorExtraSpecUpdateMap() (map[string]string, string, error) { + if len(opts) != 1 { + err := gophercloud.ErrInvalidInput{} + err.Argument = "flavors.ExtraSpecOpts" + err.Info = "Must have 1 and only one key-value pair" + return nil, "", err + } + + var key string + for k := range opts { + key = k + } + + return opts, key, nil +} + +// UpdateExtraSpec will updates the value of the specified flavor's extra spec +// for the key in opts. +func UpdateExtraSpec(client *gophercloud.ServiceClient, flavorID string, opts UpdateExtraSpecOptsBuilder) (r UpdateExtraSpecResult) { + b, key, err := opts.ToFlavorExtraSpecUpdateMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Put(extraSpecUpdateURL(client, flavorID, key), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// DeleteExtraSpec will delete the key-value pair with the given key for the given +// flavor ID. +func DeleteExtraSpec(client *gophercloud.ServiceClient, flavorID, key string) (r DeleteExtraSpecResult) { + resp, err := client.Delete(extraSpecDeleteURL(client, flavorID, key), &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/results.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/results.go new file mode 100644 index 00000000..4da14118 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/results.go @@ -0,0 +1,271 @@ +package flavors + +import ( + "encoding/json" + "strconv" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +type commonResult struct { + gophercloud.Result +} + +// CreateResult is the response of a Get operations. Call its Extract method to +// interpret it as a Flavor. +type CreateResult struct { + commonResult +} + +// UpdateResult is the response of a Put operation. Call its Extract method to +// interpret it as a Flavor. +type UpdateResult struct { + commonResult +} + +// GetResult is the response of a Get operations. Call its Extract method to +// interpret it as a Flavor. +type GetResult struct { + commonResult +} + +// DeleteResult is the result from a Delete operation. Call its ExtractErr +// method to determine if the call succeeded or failed. +type DeleteResult struct { + gophercloud.ErrResult +} + +// Extract provides access to the individual Flavor returned by the Get and +// Create functions. +func (r commonResult) Extract() (*Flavor, error) { + var s struct { + Flavor *Flavor `json:"flavor"` + } + err := r.ExtractInto(&s) + return s.Flavor, err +} + +// Flavor represent (virtual) hardware configurations for server resources +// in a region. +type Flavor struct { + // ID is the flavor's unique ID. + ID string `json:"id"` + + // Disk is the amount of root disk, measured in GB. + Disk int `json:"disk"` + + // RAM is the amount of memory, measured in MB. + RAM int `json:"ram"` + + // Name is the name of the flavor. + Name string `json:"name"` + + // RxTxFactor describes bandwidth alterations of the flavor. + RxTxFactor float64 `json:"rxtx_factor"` + + // Swap is the amount of swap space, measured in MB. + Swap int `json:"-"` + + // VCPUs indicates how many (virtual) CPUs are available for this flavor. + VCPUs int `json:"vcpus"` + + // IsPublic indicates whether the flavor is public. + IsPublic bool `json:"os-flavor-access:is_public"` + + // Ephemeral is the amount of ephemeral disk space, measured in GB. + Ephemeral int `json:"OS-FLV-EXT-DATA:ephemeral"` + + // Description is a free form description of the flavor. Limited to + // 65535 characters in length. Only printable characters are allowed. + // New in version 2.55 + Description string `json:"description"` +} + +func (r *Flavor) UnmarshalJSON(b []byte) error { + type tmp Flavor + var s struct { + tmp + Swap interface{} `json:"swap"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + + *r = Flavor(s.tmp) + + switch t := s.Swap.(type) { + case float64: + r.Swap = int(t) + case string: + switch t { + case "": + r.Swap = 0 + default: + swap, err := strconv.ParseFloat(t, 64) + if err != nil { + return err + } + r.Swap = int(swap) + } + } + + return nil +} + +// FlavorPage contains a single page of all flavors from a ListDetails call. +type FlavorPage struct { + pagination.LinkedPageBase +} + +// IsEmpty determines if a FlavorPage contains any results. +func (page FlavorPage) IsEmpty() (bool, error) { + if page.StatusCode == 204 { + return true, nil + } + + flavors, err := ExtractFlavors(page) + return len(flavors) == 0, err +} + +// NextPageURL uses the response's embedded link reference to navigate to the +// next page of results. +func (page FlavorPage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"flavors_links"` + } + err := page.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// ExtractFlavors provides access to the list of flavors in a page acquired +// from the ListDetail operation. +func ExtractFlavors(r pagination.Page) ([]Flavor, error) { + var s struct { + Flavors []Flavor `json:"flavors"` + } + err := (r.(FlavorPage)).ExtractInto(&s) + return s.Flavors, err +} + +// AccessPage contains a single page of all FlavorAccess entries for a flavor. +type AccessPage struct { + pagination.SinglePageBase +} + +// IsEmpty indicates whether an AccessPage is empty. +func (page AccessPage) IsEmpty() (bool, error) { + if page.StatusCode == 204 { + return true, nil + } + + v, err := ExtractAccesses(page) + return len(v) == 0, err +} + +// ExtractAccesses interprets a page of results as a slice of FlavorAccess. +func ExtractAccesses(r pagination.Page) ([]FlavorAccess, error) { + var s struct { + FlavorAccesses []FlavorAccess `json:"flavor_access"` + } + err := (r.(AccessPage)).ExtractInto(&s) + return s.FlavorAccesses, err +} + +type accessResult struct { + gophercloud.Result +} + +// AddAccessResult is the response of an AddAccess operation. Call its +// Extract method to interpret it as a slice of FlavorAccess. +type AddAccessResult struct { + accessResult +} + +// RemoveAccessResult is the response of a RemoveAccess operation. Call its +// Extract method to interpret it as a slice of FlavorAccess. +type RemoveAccessResult struct { + accessResult +} + +// Extract provides access to the result of an access create or delete. +// The result will be all accesses that the flavor has. +func (r accessResult) Extract() ([]FlavorAccess, error) { + var s struct { + FlavorAccesses []FlavorAccess `json:"flavor_access"` + } + err := r.ExtractInto(&s) + return s.FlavorAccesses, err +} + +// FlavorAccess represents an ACL of tenant access to a specific Flavor. +type FlavorAccess struct { + // FlavorID is the unique ID of the flavor. + FlavorID string `json:"flavor_id"` + + // TenantID is the unique ID of the tenant. + TenantID string `json:"tenant_id"` +} + +// Extract interprets any extraSpecsResult as ExtraSpecs, if possible. +func (r extraSpecsResult) Extract() (map[string]string, error) { + var s struct { + ExtraSpecs map[string]string `json:"extra_specs"` + } + err := r.ExtractInto(&s) + return s.ExtraSpecs, err +} + +// extraSpecsResult contains the result of a call for (potentially) multiple +// key-value pairs. Call its Extract method to interpret it as a +// map[string]interface. +type extraSpecsResult struct { + gophercloud.Result +} + +// ListExtraSpecsResult contains the result of a Get operation. Call its Extract +// method to interpret it as a map[string]interface. +type ListExtraSpecsResult struct { + extraSpecsResult +} + +// CreateExtraSpecResult contains the result of a Create operation. Call its +// Extract method to interpret it as a map[string]interface. +type CreateExtraSpecsResult struct { + extraSpecsResult +} + +// extraSpecResult contains the result of a call for individual a single +// key-value pair. +type extraSpecResult struct { + gophercloud.Result +} + +// GetExtraSpecResult contains the result of a Get operation. Call its Extract +// method to interpret it as a map[string]interface. +type GetExtraSpecResult struct { + extraSpecResult +} + +// UpdateExtraSpecResult contains the result of an Update operation. Call its +// Extract method to interpret it as a map[string]interface. +type UpdateExtraSpecResult struct { + extraSpecResult +} + +// DeleteExtraSpecResult contains the result of a Delete operation. Call its +// ExtractErr method to determine if the call succeeded or failed. +type DeleteExtraSpecResult struct { + gophercloud.ErrResult +} + +// Extract interprets any extraSpecResult as an ExtraSpec, if possible. +func (r extraSpecResult) Extract() (map[string]string, error) { + var s map[string]string + err := r.ExtractInto(&s) + return s, err +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/urls.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/urls.go new file mode 100644 index 00000000..65bbb654 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/urls.go @@ -0,0 +1,53 @@ +package flavors + +import ( + "github.com/gophercloud/gophercloud" +) + +func getURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("flavors", id) +} + +func listURL(client *gophercloud.ServiceClient) string { + return client.ServiceURL("flavors", "detail") +} + +func createURL(client *gophercloud.ServiceClient) string { + return client.ServiceURL("flavors") +} + +func updateURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("flavors", id) +} + +func deleteURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("flavors", id) +} + +func accessURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("flavors", id, "os-flavor-access") +} + +func accessActionURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("flavors", id, "action") +} + +func extraSpecsListURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("flavors", id, "os-extra_specs") +} + +func extraSpecsGetURL(client *gophercloud.ServiceClient, id, key string) string { + return client.ServiceURL("flavors", id, "os-extra_specs", key) +} + +func extraSpecsCreateURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("flavors", id, "os-extra_specs") +} + +func extraSpecUpdateURL(client *gophercloud.ServiceClient, id, key string) string { + return client.ServiceURL("flavors", id, "os-extra_specs", key) +} + +func extraSpecDeleteURL(client *gophercloud.ServiceClient, id, key string) string { + return client.ServiceURL("flavors", id, "os-extra_specs", key) +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/doc.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/doc.go new file mode 100644 index 00000000..bab72c15 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/doc.go @@ -0,0 +1,135 @@ +/* +Package servers provides information and interaction with the server API +resource in the OpenStack Compute service. + +A server is a virtual machine instance in the compute system. In order for +one to be provisioned, a valid flavor and image are required. + +Example to List Servers + + listOpts := servers.ListOpts{ + AllTenants: true, + } + + allPages, err := servers.ListSimple(computeClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allServers, err := servers.ExtractServers(allPages) + if err != nil { + panic(err) + } + + for _, server := range allServers { + fmt.Printf("%+v\n", server) + } + +Example to List Detail Servers + + listOpts := servers.ListOpts{ + AllTenants: true, + } + + allPages, err := servers.List(computeClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allServers, err := servers.ExtractServers(allPages) + if err != nil { + panic(err) + } + + for _, server := range allServers { + fmt.Printf("%+v\n", server) + } + +Example to Create a Server + + createOpts := servers.CreateOpts{ + Name: "server_name", + ImageRef: "image-uuid", + FlavorRef: "flavor-uuid", + } + + server, err := servers.Create(computeClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a Server + + serverID := "d9072956-1560-487c-97f2-18bdf65ec749" + err := servers.Delete(computeClient, serverID).ExtractErr() + if err != nil { + panic(err) + } + +Example to Force Delete a Server + + serverID := "d9072956-1560-487c-97f2-18bdf65ec749" + err := servers.ForceDelete(computeClient, serverID).ExtractErr() + if err != nil { + panic(err) + } + +Example to Reboot a Server + + rebootOpts := servers.RebootOpts{ + Type: servers.SoftReboot, + } + + serverID := "d9072956-1560-487c-97f2-18bdf65ec749" + + err := servers.Reboot(computeClient, serverID, rebootOpts).ExtractErr() + if err != nil { + panic(err) + } + +Example to Rebuild a Server + + rebuildOpts := servers.RebuildOpts{ + Name: "new_name", + ImageID: "image-uuid", + } + + serverID := "d9072956-1560-487c-97f2-18bdf65ec749" + + server, err := servers.Rebuilt(computeClient, serverID, rebuildOpts).Extract() + if err != nil { + panic(err) + } + +Example to Resize a Server + + resizeOpts := servers.ResizeOpts{ + FlavorRef: "flavor-uuid", + } + + serverID := "d9072956-1560-487c-97f2-18bdf65ec749" + + err := servers.Resize(computeClient, serverID, resizeOpts).ExtractErr() + if err != nil { + panic(err) + } + + err = servers.ConfirmResize(computeClient, serverID).ExtractErr() + if err != nil { + panic(err) + } + +Example to Snapshot a Server + + snapshotOpts := servers.CreateImageOpts{ + Name: "snapshot_name", + } + + serverID := "d9072956-1560-487c-97f2-18bdf65ec749" + + image, err := servers.CreateImage(computeClient, serverID, snapshotOpts).ExtractImageID() + if err != nil { + panic(err) + } +*/ +package servers diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/errors.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/errors.go new file mode 100644 index 00000000..c9f0e3c2 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/errors.go @@ -0,0 +1,71 @@ +package servers + +import ( + "fmt" + + "github.com/gophercloud/gophercloud" +) + +// ErrNeitherImageIDNorImageNameProvided is the error when neither the image +// ID nor the image name is provided for a server operation +type ErrNeitherImageIDNorImageNameProvided struct{ gophercloud.ErrMissingInput } + +func (e ErrNeitherImageIDNorImageNameProvided) Error() string { + return "One and only one of the image ID and the image name must be provided." +} + +// ErrNeitherFlavorIDNorFlavorNameProvided is the error when neither the flavor +// ID nor the flavor name is provided for a server operation +type ErrNeitherFlavorIDNorFlavorNameProvided struct{ gophercloud.ErrMissingInput } + +func (e ErrNeitherFlavorIDNorFlavorNameProvided) Error() string { + return "One and only one of the flavor ID and the flavor name must be provided." +} + +type ErrNoClientProvidedForIDByName struct{ gophercloud.ErrMissingInput } + +func (e ErrNoClientProvidedForIDByName) Error() string { + return "A service client must be provided to find a resource ID by name." +} + +// ErrInvalidHowParameterProvided is the error when an unknown value is given +// for the `how` argument +type ErrInvalidHowParameterProvided struct{ gophercloud.ErrInvalidInput } + +// ErrNoAdminPassProvided is the error when an administrative password isn't +// provided for a server operation +type ErrNoAdminPassProvided struct{ gophercloud.ErrMissingInput } + +// ErrNoImageIDProvided is the error when an image ID isn't provided for a server +// operation +type ErrNoImageIDProvided struct{ gophercloud.ErrMissingInput } + +// ErrNoIDProvided is the error when a server ID isn't provided for a server +// operation +type ErrNoIDProvided struct{ gophercloud.ErrMissingInput } + +// ErrServer is a generic error type for servers HTTP operations. +type ErrServer struct { + gophercloud.ErrUnexpectedResponseCode + ID string +} + +func (se ErrServer) Error() string { + return fmt.Sprintf("Error while executing HTTP request for server [%s]", se.ID) +} + +// Error404 overrides the generic 404 error message. +func (se ErrServer) Error404(e gophercloud.ErrUnexpectedResponseCode) error { + se.ErrUnexpectedResponseCode = e + return &ErrServerNotFound{se} +} + +// ErrServerNotFound is the error when a 404 is received during server HTTP +// operations. +type ErrServerNotFound struct { + ErrServer +} + +func (e ErrServerNotFound) Error() string { + return fmt.Sprintf("I couldn't find server [%s]", e.ID) +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/requests.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/requests.go new file mode 100644 index 00000000..d6a903aa --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/requests.go @@ -0,0 +1,784 @@ +package servers + +import ( + "encoding/base64" + "encoding/json" + "fmt" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToServerListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the server attributes you want to see returned. Marker and Limit are used +// for pagination. +type ListOpts struct { + // ChangesSince is a time/date stamp for when the server last changed status. + ChangesSince string `q:"changes-since"` + + // Image is the name of the image in URL format. + Image string `q:"image"` + + // Flavor is the name of the flavor in URL format. + Flavor string `q:"flavor"` + + // IP is a regular expression to match the IPv4 address of the server. + IP string `q:"ip"` + + // This requires the client to be set to microversion 2.5 or later, unless + // the user is an admin. + // IP is a regular expression to match the IPv6 address of the server. + IP6 string `q:"ip6"` + + // Name of the server as a string; can be queried with regular expressions. + // Realize that ?name=bob returns both bob and bobb. If you need to match bob + // only, you can use a regular expression matching the syntax of the + // underlying database server implemented for Compute. + Name string `q:"name"` + + // Status is the value of the status of the server so that you can filter on + // "ACTIVE" for example. + Status string `q:"status"` + + // Host is the name of the host as a string. + Host string `q:"host"` + + // Marker is a UUID of the server at which you want to set a marker. + Marker string `q:"marker"` + + // Limit is an integer value for the limit of values to return. + Limit int `q:"limit"` + + // AllTenants is a bool to show all tenants. + AllTenants bool `q:"all_tenants"` + + // TenantID lists servers for a particular tenant. + // Setting "AllTenants = true" is required. + TenantID string `q:"tenant_id"` + + // This requires the client to be set to microversion 2.83 or later, unless + // the user is an admin. + // UserID lists servers for a particular user. + UserID string `q:"user_id"` + + // This requires the client to be set to microversion 2.26 or later. + // Tags filters on specific server tags. All tags must be present for the server. + Tags string `q:"tags"` + + // This requires the client to be set to microversion 2.26 or later. + // TagsAny filters on specific server tags. At least one of the tags must be present for the server. + TagsAny string `q:"tags-any"` + + // This requires the client to be set to microversion 2.26 or later. + // NotTags filters on specific server tags. All tags must be absent for the server. + NotTags string `q:"not-tags"` + + // This requires the client to be set to microversion 2.26 or later. + // NotTagsAny filters on specific server tags. At least one of the tags must be absent for the server. + NotTagsAny string `q:"not-tags-any"` + + // Display servers based on their availability zone (Admin only until microversion 2.82). + AvailabilityZone string `q:"availability_zone"` +} + +// ToServerListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToServerListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// ListSimple makes a request against the API to list servers accessible to you. +func ListSimple(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToServerListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return ServerPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// List makes a request against the API to list servers details accessible to you. +func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listDetailURL(client) + if opts != nil { + query, err := opts.ToServerListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return ServerPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToServerCreateMap() (map[string]interface{}, error) +} + +// Network is used within CreateOpts to control a new server's network +// attachments. +type Network struct { + // UUID of a network to attach to the newly provisioned server. + // Required unless Port is provided. + UUID string + + // Port of a neutron network to attach to the newly provisioned server. + // Required unless UUID is provided. + Port string + + // FixedIP specifies a fixed IPv4 address to be used on this network. + FixedIP string + + // Tag may contain an optional device role tag for the server's virtual + // network interface. This can be used to identify network interfaces when + // multiple networks are connected to one server. + // + // Requires microversion 2.32 through 2.36 or 2.42 or later. + Tag string +} + +// Personality is an array of files that are injected into the server at launch. +type Personality []*File + +// File is used within CreateOpts and RebuildOpts to inject a file into the +// server at launch. +// File implements the json.Marshaler interface, so when a Create or Rebuild +// operation is requested, json.Marshal will call File's MarshalJSON method. +type File struct { + // Path of the file. + Path string + + // Contents of the file. Maximum content size is 255 bytes. + Contents []byte +} + +// MarshalJSON marshals the escaped file, base64 encoding the contents. +func (f *File) MarshalJSON() ([]byte, error) { + file := struct { + Path string `json:"path"` + Contents string `json:"contents"` + }{ + Path: f.Path, + Contents: base64.StdEncoding.EncodeToString(f.Contents), + } + return json.Marshal(file) +} + +// CreateOpts specifies server creation parameters. +type CreateOpts struct { + // Name is the name to assign to the newly launched server. + Name string `json:"name" required:"true"` + + // ImageRef is the ID or full URL to the image that contains the + // server's OS and initial state. + // Also optional if using the boot-from-volume extension. + ImageRef string `json:"imageRef"` + + // FlavorRef is the ID or full URL to the flavor that describes the server's specs. + FlavorRef string `json:"flavorRef"` + + // SecurityGroups lists the names of the security groups to which this server + // should belong. + SecurityGroups []string `json:"-"` + + // UserData contains configuration information or scripts to use upon launch. + // Create will base64-encode it for you, if it isn't already. + UserData []byte `json:"-"` + + // AvailabilityZone in which to launch the server. + AvailabilityZone string `json:"availability_zone,omitempty"` + + // Networks dictates how this server will be attached to available networks. + // By default, the server will be attached to all isolated networks for the + // tenant. + // Starting with microversion 2.37 networks can also be an "auto" or "none" + // string. + Networks interface{} `json:"-"` + + // Metadata contains key-value pairs (up to 255 bytes each) to attach to the + // server. + Metadata map[string]string `json:"metadata,omitempty"` + + // Personality includes files to inject into the server at launch. + // Create will base64-encode file contents for you. + Personality Personality `json:"personality,omitempty"` + + // ConfigDrive enables metadata injection through a configuration drive. + ConfigDrive *bool `json:"config_drive,omitempty"` + + // AdminPass sets the root user password. If not set, a randomly-generated + // password will be created and returned in the response. + AdminPass string `json:"adminPass,omitempty"` + + // AccessIPv4 specifies an IPv4 address for the instance. + AccessIPv4 string `json:"accessIPv4,omitempty"` + + // AccessIPv6 specifies an IPv6 address for the instance. + AccessIPv6 string `json:"accessIPv6,omitempty"` + + // Min specifies Minimum number of servers to launch. + Min int `json:"min_count,omitempty"` + + // Max specifies Maximum number of servers to launch. + Max int `json:"max_count,omitempty"` + + // Tags allows a server to be tagged with single-word metadata. + // Requires microversion 2.52 or later. + Tags []string `json:"tags,omitempty"` +} + +// ToServerCreateMap assembles a request body based on the contents of a +// CreateOpts. +func (opts CreateOpts) ToServerCreateMap() (map[string]interface{}, error) { + b, err := gophercloud.BuildRequestBody(opts, "") + if err != nil { + return nil, err + } + + if opts.UserData != nil { + var userData string + if _, err := base64.StdEncoding.DecodeString(string(opts.UserData)); err != nil { + userData = base64.StdEncoding.EncodeToString(opts.UserData) + } else { + userData = string(opts.UserData) + } + b["user_data"] = &userData + } + + if len(opts.SecurityGroups) > 0 { + securityGroups := make([]map[string]interface{}, len(opts.SecurityGroups)) + for i, groupName := range opts.SecurityGroups { + securityGroups[i] = map[string]interface{}{"name": groupName} + } + b["security_groups"] = securityGroups + } + + switch v := opts.Networks.(type) { + case []Network: + if len(v) > 0 { + networks := make([]map[string]interface{}, len(v)) + for i, net := range v { + networks[i] = make(map[string]interface{}) + if net.UUID != "" { + networks[i]["uuid"] = net.UUID + } + if net.Port != "" { + networks[i]["port"] = net.Port + } + if net.FixedIP != "" { + networks[i]["fixed_ip"] = net.FixedIP + } + if net.Tag != "" { + networks[i]["tag"] = net.Tag + } + } + b["networks"] = networks + } + case string: + if v == "auto" || v == "none" { + b["networks"] = v + } else { + return nil, fmt.Errorf(`networks must be a slice of Network struct or a string with "auto" or "none" values, current value is %q`, v) + } + } + + if opts.Min != 0 { + b["min_count"] = opts.Min + } + + if opts.Max != 0 { + b["max_count"] = opts.Max + } + + return map[string]interface{}{"server": b}, nil +} + +// Create requests a server to be provisioned to the user in the current tenant. +func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + reqBody, err := opts.ToServerCreateMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(listURL(client), reqBody, &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Delete requests that a server previously provisioned be removed from your +// account. +func Delete(client *gophercloud.ServiceClient, id string) (r DeleteResult) { + resp, err := client.Delete(deleteURL(client, id), nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// ForceDelete forces the deletion of a server. +func ForceDelete(client *gophercloud.ServiceClient, id string) (r ActionResult) { + resp, err := client.Post(actionURL(client, id), map[string]interface{}{"forceDelete": ""}, nil, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Get requests details on a single server, by ID. +func Get(client *gophercloud.ServiceClient, id string) (r GetResult) { + resp, err := client.Get(getURL(client, id), &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 203}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// UpdateOptsBuilder allows extensions to add additional attributes to the +// Update request. +type UpdateOptsBuilder interface { + ToServerUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts specifies the base attributes that may be updated on an existing +// server. +type UpdateOpts struct { + // Name changes the displayed name of the server. + // The server host name will *not* change. + // Server names are not constrained to be unique, even within the same tenant. + Name string `json:"name,omitempty"` + + // AccessIPv4 provides a new IPv4 address for the instance. + AccessIPv4 string `json:"accessIPv4,omitempty"` + + // AccessIPv6 provides a new IPv6 address for the instance. + AccessIPv6 string `json:"accessIPv6,omitempty"` +} + +// ToServerUpdateMap formats an UpdateOpts structure into a request body. +func (opts UpdateOpts) ToServerUpdateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "server") +} + +// Update requests that various attributes of the indicated server be changed. +func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToServerUpdateMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Put(updateURL(client, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// ChangeAdminPassword alters the administrator or root password for a specified +// server. +func ChangeAdminPassword(client *gophercloud.ServiceClient, id, newPassword string) (r ActionResult) { + b := map[string]interface{}{ + "changePassword": map[string]string{ + "adminPass": newPassword, + }, + } + resp, err := client.Post(actionURL(client, id), b, nil, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// RebootMethod describes the mechanisms by which a server reboot can be requested. +type RebootMethod string + +// These constants determine how a server should be rebooted. +// See the Reboot() function for further details. +const ( + SoftReboot RebootMethod = "SOFT" + HardReboot RebootMethod = "HARD" + OSReboot = SoftReboot + PowerCycle = HardReboot +) + +// RebootOptsBuilder allows extensions to add additional parameters to the +// reboot request. +type RebootOptsBuilder interface { + ToServerRebootMap() (map[string]interface{}, error) +} + +// RebootOpts provides options to the reboot request. +type RebootOpts struct { + // Type is the type of reboot to perform on the server. + Type RebootMethod `json:"type" required:"true"` +} + +// ToServerRebootMap builds a body for the reboot request. +func (opts RebootOpts) ToServerRebootMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "reboot") +} + +/* +Reboot requests that a given server reboot. + +Two methods exist for rebooting a server: + +HardReboot (aka PowerCycle) starts the server instance by physically cutting +power to the machine, or if a VM, terminating it at the hypervisor level. +It's done. Caput. Full stop. +Then, after a brief while, power is restored or the VM instance restarted. + +SoftReboot (aka OSReboot) simply tells the OS to restart under its own +procedure. +E.g., in Linux, asking it to enter runlevel 6, or executing +"sudo shutdown -r now", or by asking Windows to rtart the machine. +*/ +func Reboot(client *gophercloud.ServiceClient, id string, opts RebootOptsBuilder) (r ActionResult) { + b, err := opts.ToServerRebootMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(actionURL(client, id), b, nil, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// RebuildOptsBuilder allows extensions to provide additional parameters to the +// rebuild request. +type RebuildOptsBuilder interface { + ToServerRebuildMap() (map[string]interface{}, error) +} + +// RebuildOpts represents the configuration options used in a server rebuild +// operation. +type RebuildOpts struct { + // AdminPass is the server's admin password + AdminPass string `json:"adminPass,omitempty"` + + // ImageRef is the ID of the image you want your server to be provisioned on. + ImageRef string `json:"imageRef"` + + // Name to set the server to + Name string `json:"name,omitempty"` + + // AccessIPv4 [optional] provides a new IPv4 address for the instance. + AccessIPv4 string `json:"accessIPv4,omitempty"` + + // AccessIPv6 [optional] provides a new IPv6 address for the instance. + AccessIPv6 string `json:"accessIPv6,omitempty"` + + // Metadata [optional] contains key-value pairs (up to 255 bytes each) + // to attach to the server. + Metadata map[string]string `json:"metadata,omitempty"` + + // Personality [optional] includes files to inject into the server at launch. + // Rebuild will base64-encode file contents for you. + Personality Personality `json:"personality,omitempty"` +} + +// ToServerRebuildMap formats a RebuildOpts struct into a map for use in JSON +func (opts RebuildOpts) ToServerRebuildMap() (map[string]interface{}, error) { + b, err := gophercloud.BuildRequestBody(opts, "") + if err != nil { + return nil, err + } + + return map[string]interface{}{"rebuild": b}, nil +} + +// Rebuild will reprovision the server according to the configuration options +// provided in the RebuildOpts struct. +func Rebuild(client *gophercloud.ServiceClient, id string, opts RebuildOptsBuilder) (r RebuildResult) { + b, err := opts.ToServerRebuildMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(actionURL(client, id), b, &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// ResizeOptsBuilder allows extensions to add additional parameters to the +// resize request. +type ResizeOptsBuilder interface { + ToServerResizeMap() (map[string]interface{}, error) +} + +// ResizeOpts represents the configuration options used to control a Resize +// operation. +type ResizeOpts struct { + // FlavorRef is the ID of the flavor you wish your server to become. + FlavorRef string `json:"flavorRef" required:"true"` +} + +// ToServerResizeMap formats a ResizeOpts as a map that can be used as a JSON +// request body for the Resize request. +func (opts ResizeOpts) ToServerResizeMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "resize") +} + +// Resize instructs the provider to change the flavor of the server. +// +// Note that this implies rebuilding it. +// +// Unfortunately, one cannot pass rebuild parameters to the resize function. +// When the resize completes, the server will be in VERIFY_RESIZE state. +// While in this state, you can explore the use of the new server's +// configuration. If you like it, call ConfirmResize() to commit the resize +// permanently. Otherwise, call RevertResize() to restore the old configuration. +func Resize(client *gophercloud.ServiceClient, id string, opts ResizeOptsBuilder) (r ActionResult) { + b, err := opts.ToServerResizeMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(actionURL(client, id), b, nil, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// ConfirmResize confirms a previous resize operation on a server. +// See Resize() for more details. +func ConfirmResize(client *gophercloud.ServiceClient, id string) (r ActionResult) { + resp, err := client.Post(actionURL(client, id), map[string]interface{}{"confirmResize": nil}, nil, &gophercloud.RequestOpts{ + OkCodes: []int{201, 202, 204}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// RevertResize cancels a previous resize operation on a server. +// See Resize() for more details. +func RevertResize(client *gophercloud.ServiceClient, id string) (r ActionResult) { + resp, err := client.Post(actionURL(client, id), map[string]interface{}{"revertResize": nil}, nil, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// ResetMetadataOptsBuilder allows extensions to add additional parameters to +// the Reset request. +type ResetMetadataOptsBuilder interface { + ToMetadataResetMap() (map[string]interface{}, error) +} + +// MetadataOpts is a map that contains key-value pairs. +type MetadataOpts map[string]string + +// ToMetadataResetMap assembles a body for a Reset request based on the contents +// of a MetadataOpts. +func (opts MetadataOpts) ToMetadataResetMap() (map[string]interface{}, error) { + return map[string]interface{}{"metadata": opts}, nil +} + +// ToMetadataUpdateMap assembles a body for an Update request based on the +// contents of a MetadataOpts. +func (opts MetadataOpts) ToMetadataUpdateMap() (map[string]interface{}, error) { + return map[string]interface{}{"metadata": opts}, nil +} + +// ResetMetadata will create multiple new key-value pairs for the given server +// ID. +// Note: Using this operation will erase any already-existing metadata and +// create the new metadata provided. To keep any already-existing metadata, +// use the UpdateMetadatas or UpdateMetadata function. +func ResetMetadata(client *gophercloud.ServiceClient, id string, opts ResetMetadataOptsBuilder) (r ResetMetadataResult) { + b, err := opts.ToMetadataResetMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Put(metadataURL(client, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Metadata requests all the metadata for the given server ID. +func Metadata(client *gophercloud.ServiceClient, id string) (r GetMetadataResult) { + resp, err := client.Get(metadataURL(client, id), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// UpdateMetadataOptsBuilder allows extensions to add additional parameters to +// the Create request. +type UpdateMetadataOptsBuilder interface { + ToMetadataUpdateMap() (map[string]interface{}, error) +} + +// UpdateMetadata updates (or creates) all the metadata specified by opts for +// the given server ID. This operation does not affect already-existing metadata +// that is not specified by opts. +func UpdateMetadata(client *gophercloud.ServiceClient, id string, opts UpdateMetadataOptsBuilder) (r UpdateMetadataResult) { + b, err := opts.ToMetadataUpdateMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(metadataURL(client, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// MetadatumOptsBuilder allows extensions to add additional parameters to the +// Create request. +type MetadatumOptsBuilder interface { + ToMetadatumCreateMap() (map[string]interface{}, string, error) +} + +// MetadatumOpts is a map of length one that contains a key-value pair. +type MetadatumOpts map[string]string + +// ToMetadatumCreateMap assembles a body for a Create request based on the +// contents of a MetadataumOpts. +func (opts MetadatumOpts) ToMetadatumCreateMap() (map[string]interface{}, string, error) { + if len(opts) != 1 { + err := gophercloud.ErrInvalidInput{} + err.Argument = "servers.MetadatumOpts" + err.Info = "Must have 1 and only 1 key-value pair" + return nil, "", err + } + metadatum := map[string]interface{}{"meta": opts} + var key string + for k := range metadatum["meta"].(MetadatumOpts) { + key = k + } + return metadatum, key, nil +} + +// CreateMetadatum will create or update the key-value pair with the given key +// for the given server ID. +func CreateMetadatum(client *gophercloud.ServiceClient, id string, opts MetadatumOptsBuilder) (r CreateMetadatumResult) { + b, key, err := opts.ToMetadatumCreateMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Put(metadatumURL(client, id, key), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Metadatum requests the key-value pair with the given key for the given +// server ID. +func Metadatum(client *gophercloud.ServiceClient, id, key string) (r GetMetadatumResult) { + resp, err := client.Get(metadatumURL(client, id, key), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// DeleteMetadatum will delete the key-value pair with the given key for the +// given server ID. +func DeleteMetadatum(client *gophercloud.ServiceClient, id, key string) (r DeleteMetadatumResult) { + resp, err := client.Delete(metadatumURL(client, id, key), nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// ListAddresses makes a request against the API to list the servers IP +// addresses. +func ListAddresses(client *gophercloud.ServiceClient, id string) pagination.Pager { + return pagination.NewPager(client, listAddressesURL(client, id), func(r pagination.PageResult) pagination.Page { + return AddressPage{pagination.SinglePageBase(r)} + }) +} + +// ListAddressesByNetwork makes a request against the API to list the servers IP +// addresses for the given network. +func ListAddressesByNetwork(client *gophercloud.ServiceClient, id, network string) pagination.Pager { + return pagination.NewPager(client, listAddressesByNetworkURL(client, id, network), func(r pagination.PageResult) pagination.Page { + return NetworkAddressPage{pagination.SinglePageBase(r)} + }) +} + +// CreateImageOptsBuilder allows extensions to add additional parameters to the +// CreateImage request. +type CreateImageOptsBuilder interface { + ToServerCreateImageMap() (map[string]interface{}, error) +} + +// CreateImageOpts provides options to pass to the CreateImage request. +type CreateImageOpts struct { + // Name of the image/snapshot. + Name string `json:"name" required:"true"` + + // Metadata contains key-value pairs (up to 255 bytes each) to attach to + // the created image. + Metadata map[string]string `json:"metadata,omitempty"` +} + +// ToServerCreateImageMap formats a CreateImageOpts structure into a request +// body. +func (opts CreateImageOpts) ToServerCreateImageMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "createImage") +} + +// CreateImage makes a request against the nova API to schedule an image to be +// created of the server +func CreateImage(client *gophercloud.ServiceClient, id string, opts CreateImageOptsBuilder) (r CreateImageResult) { + b, err := opts.ToServerCreateImageMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(actionURL(client, id), b, nil, &gophercloud.RequestOpts{ + OkCodes: []int{202}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// GetPassword makes a request against the nova API to get the encrypted +// administrative password. +func GetPassword(client *gophercloud.ServiceClient, serverId string) (r GetPasswordResult) { + resp, err := client.Get(passwordURL(client, serverId), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// ShowConsoleOutputOptsBuilder is the interface types must satisfy in order to be +// used as ShowConsoleOutput options +type ShowConsoleOutputOptsBuilder interface { + ToServerShowConsoleOutputMap() (map[string]interface{}, error) +} + +// ShowConsoleOutputOpts satisfies the ShowConsoleOutputOptsBuilder +type ShowConsoleOutputOpts struct { + // The number of lines to fetch from the end of console log. + // All lines will be returned if this is not specified. + Length int `json:"length,omitempty"` +} + +// ToServerShowConsoleOutputMap formats a ShowConsoleOutputOpts structure into a request body. +func (opts ShowConsoleOutputOpts) ToServerShowConsoleOutputMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "os-getConsoleOutput") +} + +// ShowConsoleOutput makes a request against the nova API to get console log from the server +func ShowConsoleOutput(client *gophercloud.ServiceClient, id string, opts ShowConsoleOutputOptsBuilder) (r ShowConsoleOutputResult) { + b, err := opts.ToServerShowConsoleOutputMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(actionURL(client, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/results.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/results.go new file mode 100644 index 00000000..2c22a3c4 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/results.go @@ -0,0 +1,444 @@ +package servers + +import ( + "crypto/rsa" + "encoding/base64" + "encoding/json" + "fmt" + "net/url" + "path" + "time" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +type serverResult struct { + gophercloud.Result +} + +// Extract interprets any serverResult as a Server, if possible. +func (r serverResult) Extract() (*Server, error) { + var s Server + err := r.ExtractInto(&s) + return &s, err +} + +func (r serverResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "server") +} + +func ExtractServersInto(r pagination.Page, v interface{}) error { + return r.(ServerPage).Result.ExtractIntoSlicePtr(v, "servers") +} + +// CreateResult is the response from a Create operation. Call its Extract +// method to interpret it as a Server. +type CreateResult struct { + serverResult +} + +// GetResult is the response from a Get operation. Call its Extract +// method to interpret it as a Server. +type GetResult struct { + serverResult +} + +// UpdateResult is the response from an Update operation. Call its Extract +// method to interpret it as a Server. +type UpdateResult struct { + serverResult +} + +// DeleteResult is the response from a Delete operation. Call its ExtractErr +// method to determine if the call succeeded or failed. +type DeleteResult struct { + gophercloud.ErrResult +} + +// RebuildResult is the response from a Rebuild operation. Call its Extract +// method to interpret it as a Server. +type RebuildResult struct { + serverResult +} + +// ActionResult represents the result of server action operations, like reboot. +// Call its ExtractErr method to determine if the action succeeded or failed. +type ActionResult struct { + gophercloud.ErrResult +} + +// CreateImageResult is the response from a CreateImage operation. Call its +// ExtractImageID method to retrieve the ID of the newly created image. +type CreateImageResult struct { + gophercloud.Result +} + +// ShowConsoleOutputResult represents the result of console output from a server +type ShowConsoleOutputResult struct { + gophercloud.Result +} + +// Extract will return the console output from a ShowConsoleOutput request. +func (r ShowConsoleOutputResult) Extract() (string, error) { + var s struct { + Output string `json:"output"` + } + + err := r.ExtractInto(&s) + return s.Output, err +} + +// GetPasswordResult represent the result of a get os-server-password operation. +// Call its ExtractPassword method to retrieve the password. +type GetPasswordResult struct { + gophercloud.Result +} + +// ExtractPassword gets the encrypted password. +// If privateKey != nil the password is decrypted with the private key. +// If privateKey == nil the encrypted password is returned and can be decrypted +// with: +// +// echo '' | base64 -D | openssl rsautl -decrypt -inkey +func (r GetPasswordResult) ExtractPassword(privateKey *rsa.PrivateKey) (string, error) { + var s struct { + Password string `json:"password"` + } + err := r.ExtractInto(&s) + if err == nil && privateKey != nil && s.Password != "" { + return decryptPassword(s.Password, privateKey) + } + return s.Password, err +} + +func decryptPassword(encryptedPassword string, privateKey *rsa.PrivateKey) (string, error) { + b64EncryptedPassword := make([]byte, base64.StdEncoding.DecodedLen(len(encryptedPassword))) + + n, err := base64.StdEncoding.Decode(b64EncryptedPassword, []byte(encryptedPassword)) + if err != nil { + return "", fmt.Errorf("Failed to base64 decode encrypted password: %s", err) + } + password, err := rsa.DecryptPKCS1v15(nil, privateKey, b64EncryptedPassword[0:n]) + if err != nil { + return "", fmt.Errorf("Failed to decrypt password: %s", err) + } + + return string(password), nil +} + +// ExtractImageID gets the ID of the newly created server image from the header. +func (r CreateImageResult) ExtractImageID() (string, error) { + if r.Err != nil { + return "", r.Err + } + // Get the image id from the header + u, err := url.ParseRequestURI(r.Header.Get("Location")) + if err != nil { + return "", err + } + imageID := path.Base(u.Path) + if imageID == "." || imageID == "/" { + return "", fmt.Errorf("Failed to parse the ID of newly created image: %s", u) + } + return imageID, nil +} + +// Server represents a server/instance in the OpenStack cloud. +type Server struct { + // ID uniquely identifies this server amongst all other servers, + // including those not accessible to the current tenant. + ID string `json:"id"` + + // TenantID identifies the tenant owning this server resource. + TenantID string `json:"tenant_id"` + + // UserID uniquely identifies the user account owning the tenant. + UserID string `json:"user_id"` + + // Name contains the human-readable name for the server. + Name string `json:"name"` + + // Updated and Created contain ISO-8601 timestamps of when the state of the + // server last changed, and when it was created. + Updated time.Time `json:"updated"` + Created time.Time `json:"created"` + + // HostID is the host where the server is located in the cloud. + HostID string `json:"hostid"` + + // Status contains the current operational status of the server, + // such as IN_PROGRESS or ACTIVE. + Status string `json:"status"` + + // Progress ranges from 0..100. + // A request made against the server completes only once Progress reaches 100. + Progress int `json:"progress"` + + // AccessIPv4 and AccessIPv6 contain the IP addresses of the server, + // suitable for remote access for administration. + AccessIPv4 string `json:"accessIPv4"` + AccessIPv6 string `json:"accessIPv6"` + + // Image refers to a JSON object, which itself indicates the OS image used to + // deploy the server. + Image map[string]interface{} `json:"-"` + + // Flavor refers to a JSON object, which itself indicates the hardware + // configuration of the deployed server. + Flavor map[string]interface{} `json:"flavor"` + + // Addresses includes a list of all IP addresses assigned to the server, + // keyed by pool. + Addresses map[string]interface{} `json:"addresses"` + + // Metadata includes a list of all user-specified key-value pairs attached + // to the server. + Metadata map[string]string `json:"metadata"` + + // Links includes HTTP references to the itself, useful for passing along to + // other APIs that might want a server reference. + Links []interface{} `json:"links"` + + // KeyName indicates which public key was injected into the server on launch. + KeyName string `json:"key_name"` + + // AdminPass will generally be empty (""). However, it will contain the + // administrative password chosen when provisioning a new server without a + // set AdminPass setting in the first place. + // Note that this is the ONLY time this field will be valid. + AdminPass string `json:"adminPass"` + + // SecurityGroups includes the security groups that this instance has applied + // to it. + SecurityGroups []map[string]interface{} `json:"security_groups"` + + // AttachedVolumes includes the volume attachments of this instance + AttachedVolumes []AttachedVolume `json:"os-extended-volumes:volumes_attached"` + + // Fault contains failure information about a server. + Fault Fault `json:"fault"` + + // Tags is a slice/list of string tags in a server. + // The requires microversion 2.26 or later. + Tags *[]string `json:"tags"` + + // ServerGroups is a slice of strings containing the UUIDs of the + // server groups to which the server belongs. Currently this can + // contain at most one entry. + // New in microversion 2.71 + ServerGroups *[]string `json:"server_groups"` +} + +type AttachedVolume struct { + ID string `json:"id"` +} + +type Fault struct { + Code int `json:"code"` + Created time.Time `json:"created"` + Details string `json:"details"` + Message string `json:"message"` +} + +func (r *Server) UnmarshalJSON(b []byte) error { + type tmp Server + var s struct { + tmp + Image interface{} `json:"image"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + + *r = Server(s.tmp) + + switch t := s.Image.(type) { + case map[string]interface{}: + r.Image = t + case string: + switch t { + case "": + r.Image = nil + } + } + + return err +} + +// ServerPage abstracts the raw results of making a List() request against +// the API. As OpenStack extensions may freely alter the response bodies of +// structures returned to the client, you may only safely access the data +// provided through the ExtractServers call. +type ServerPage struct { + pagination.LinkedPageBase +} + +// IsEmpty returns true if a page contains no Server results. +func (r ServerPage) IsEmpty() (bool, error) { + if r.StatusCode == 204 { + return true, nil + } + + s, err := ExtractServers(r) + return len(s) == 0, err +} + +// NextPageURL uses the response's embedded link reference to navigate to the +// next page of results. +func (r ServerPage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"servers_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// ExtractServers interprets the results of a single page from a List() call, +// producing a slice of Server entities. +func ExtractServers(r pagination.Page) ([]Server, error) { + var s []Server + err := ExtractServersInto(r, &s) + return s, err +} + +// MetadataResult contains the result of a call for (potentially) multiple +// key-value pairs. Call its Extract method to interpret it as a +// map[string]interface. +type MetadataResult struct { + gophercloud.Result +} + +// GetMetadataResult contains the result of a Get operation. Call its Extract +// method to interpret it as a map[string]interface. +type GetMetadataResult struct { + MetadataResult +} + +// ResetMetadataResult contains the result of a Reset operation. Call its +// Extract method to interpret it as a map[string]interface. +type ResetMetadataResult struct { + MetadataResult +} + +// UpdateMetadataResult contains the result of an Update operation. Call its +// Extract method to interpret it as a map[string]interface. +type UpdateMetadataResult struct { + MetadataResult +} + +// MetadatumResult contains the result of a call for individual a single +// key-value pair. +type MetadatumResult struct { + gophercloud.Result +} + +// GetMetadatumResult contains the result of a Get operation. Call its Extract +// method to interpret it as a map[string]interface. +type GetMetadatumResult struct { + MetadatumResult +} + +// CreateMetadatumResult contains the result of a Create operation. Call its +// Extract method to interpret it as a map[string]interface. +type CreateMetadatumResult struct { + MetadatumResult +} + +// DeleteMetadatumResult contains the result of a Delete operation. Call its +// ExtractErr method to determine if the call succeeded or failed. +type DeleteMetadatumResult struct { + gophercloud.ErrResult +} + +// Extract interprets any MetadataResult as a Metadata, if possible. +func (r MetadataResult) Extract() (map[string]string, error) { + var s struct { + Metadata map[string]string `json:"metadata"` + } + err := r.ExtractInto(&s) + return s.Metadata, err +} + +// Extract interprets any MetadatumResult as a Metadatum, if possible. +func (r MetadatumResult) Extract() (map[string]string, error) { + var s struct { + Metadatum map[string]string `json:"meta"` + } + err := r.ExtractInto(&s) + return s.Metadatum, err +} + +// Address represents an IP address. +type Address struct { + Version int `json:"version"` + Address string `json:"addr"` +} + +// AddressPage abstracts the raw results of making a ListAddresses() request +// against the API. As OpenStack extensions may freely alter the response bodies +// of structures returned to the client, you may only safely access the data +// provided through the ExtractAddresses call. +type AddressPage struct { + pagination.SinglePageBase +} + +// IsEmpty returns true if an AddressPage contains no networks. +func (r AddressPage) IsEmpty() (bool, error) { + if r.StatusCode == 204 { + return true, nil + } + + addresses, err := ExtractAddresses(r) + return len(addresses) == 0, err +} + +// ExtractAddresses interprets the results of a single page from a +// ListAddresses() call, producing a map of addresses. +func ExtractAddresses(r pagination.Page) (map[string][]Address, error) { + var s struct { + Addresses map[string][]Address `json:"addresses"` + } + err := (r.(AddressPage)).ExtractInto(&s) + return s.Addresses, err +} + +// NetworkAddressPage abstracts the raw results of making a +// ListAddressesByNetwork() request against the API. +// As OpenStack extensions may freely alter the response bodies of structures +// returned to the client, you may only safely access the data provided through +// the ExtractAddresses call. +type NetworkAddressPage struct { + pagination.SinglePageBase +} + +// IsEmpty returns true if a NetworkAddressPage contains no addresses. +func (r NetworkAddressPage) IsEmpty() (bool, error) { + if r.StatusCode == 204 { + return true, nil + } + + addresses, err := ExtractNetworkAddresses(r) + return len(addresses) == 0, err +} + +// ExtractNetworkAddresses interprets the results of a single page from a +// ListAddressesByNetwork() call, producing a slice of addresses. +func ExtractNetworkAddresses(r pagination.Page) ([]Address, error) { + var s map[string][]Address + err := (r.(NetworkAddressPage)).ExtractInto(&s) + if err != nil { + return nil, err + } + + var key string + for k := range s { + key = k + } + + return s[key], err +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/urls.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/urls.go new file mode 100644 index 00000000..e892e8d9 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/urls.go @@ -0,0 +1,51 @@ +package servers + +import "github.com/gophercloud/gophercloud" + +func createURL(client *gophercloud.ServiceClient) string { + return client.ServiceURL("servers") +} + +func listURL(client *gophercloud.ServiceClient) string { + return createURL(client) +} + +func listDetailURL(client *gophercloud.ServiceClient) string { + return client.ServiceURL("servers", "detail") +} + +func deleteURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("servers", id) +} + +func getURL(client *gophercloud.ServiceClient, id string) string { + return deleteURL(client, id) +} + +func updateURL(client *gophercloud.ServiceClient, id string) string { + return deleteURL(client, id) +} + +func actionURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("servers", id, "action") +} + +func metadatumURL(client *gophercloud.ServiceClient, id, key string) string { + return client.ServiceURL("servers", id, "metadata", key) +} + +func metadataURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("servers", id, "metadata") +} + +func listAddressesURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("servers", id, "ips") +} + +func listAddressesByNetworkURL(client *gophercloud.ServiceClient, id, network string) string { + return client.ServiceURL("servers", id, "ips", network) +} + +func passwordURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("servers", id, "os-server-password") +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/util.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/util.go new file mode 100644 index 00000000..cadef054 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/util.go @@ -0,0 +1,21 @@ +package servers + +import "github.com/gophercloud/gophercloud" + +// WaitForStatus will continually poll a server until it successfully +// transitions to a specified status. It will do this for at most the number +// of seconds specified. +func WaitForStatus(c *gophercloud.ServiceClient, id, status string, secs int) error { + return gophercloud.WaitFor(secs, func() (bool, error) { + current, err := Get(c, id).Extract() + if err != nil { + return false, err + } + + if current.Status == status { + return true, nil + } + + return false, nil + }) +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/doc.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/doc.go new file mode 100644 index 00000000..af4bd512 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/doc.go @@ -0,0 +1,14 @@ +/* +Package openstack contains resources for the individual OpenStack projects +supported in Gophercloud. It also includes functions to authenticate to an +OpenStack cloud and for provisioning various service-level clients. + +Example of Creating a Service Client + + ao, err := openstack.AuthOptionsFromEnv() + provider, err := openstack.AuthenticatedClient(ao) + client, err := openstack.NewNetworkV2(provider, gophercloud.EndpointOpts{ + Region: os.Getenv("OS_REGION_NAME"), + }) +*/ +package openstack diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/endpoint_location.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/endpoint_location.go new file mode 100644 index 00000000..50970079 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/endpoint_location.go @@ -0,0 +1,111 @@ +package openstack + +import ( + "github.com/gophercloud/gophercloud" + tokens2 "github.com/gophercloud/gophercloud/openstack/identity/v2/tokens" + tokens3 "github.com/gophercloud/gophercloud/openstack/identity/v3/tokens" +) + +/* +V2EndpointURL discovers the endpoint URL for a specific service from a +ServiceCatalog acquired during the v2 identity service. + +The specified EndpointOpts are used to identify a unique, unambiguous endpoint +to return. It's an error both when multiple endpoints match the provided +criteria and when none do. The minimum that can be specified is a Type, but you +will also often need to specify a Name and/or a Region depending on what's +available on your OpenStack deployment. +*/ +func V2EndpointURL(catalog *tokens2.ServiceCatalog, opts gophercloud.EndpointOpts) (string, error) { + // Extract Endpoints from the catalog entries that match the requested Type, Name if provided, and Region if provided. + var endpoints = make([]tokens2.Endpoint, 0, 1) + for _, entry := range catalog.Entries { + if (entry.Type == opts.Type) && (opts.Name == "" || entry.Name == opts.Name) { + for _, endpoint := range entry.Endpoints { + if opts.Region == "" || endpoint.Region == opts.Region { + endpoints = append(endpoints, endpoint) + } + } + } + } + + // If multiple endpoints were found, use the first result + // and disregard the other endpoints. + // + // This behavior matches the Python library. See GH-1764. + if len(endpoints) > 1 { + endpoints = endpoints[0:1] + } + + // Extract the appropriate URL from the matching Endpoint. + for _, endpoint := range endpoints { + switch opts.Availability { + case gophercloud.AvailabilityPublic: + return gophercloud.NormalizeURL(endpoint.PublicURL), nil + case gophercloud.AvailabilityInternal: + return gophercloud.NormalizeURL(endpoint.InternalURL), nil + case gophercloud.AvailabilityAdmin: + return gophercloud.NormalizeURL(endpoint.AdminURL), nil + default: + err := &ErrInvalidAvailabilityProvided{} + err.Argument = "Availability" + err.Value = opts.Availability + return "", err + } + } + + // Report an error if there were no matching endpoints. + err := &gophercloud.ErrEndpointNotFound{} + return "", err +} + +/* +V3EndpointURL discovers the endpoint URL for a specific service from a Catalog +acquired during the v3 identity service. + +The specified EndpointOpts are used to identify a unique, unambiguous endpoint +to return. It's an error both when multiple endpoints match the provided +criteria and when none do. The minimum that can be specified is a Type, but you +will also often need to specify a Name and/or a Region depending on what's +available on your OpenStack deployment. +*/ +func V3EndpointURL(catalog *tokens3.ServiceCatalog, opts gophercloud.EndpointOpts) (string, error) { + // Extract Endpoints from the catalog entries that match the requested Type, Interface, + // Name if provided, and Region if provided. + var endpoints = make([]tokens3.Endpoint, 0, 1) + for _, entry := range catalog.Entries { + if (entry.Type == opts.Type) && (opts.Name == "" || entry.Name == opts.Name) { + for _, endpoint := range entry.Endpoints { + if opts.Availability != gophercloud.AvailabilityAdmin && + opts.Availability != gophercloud.AvailabilityPublic && + opts.Availability != gophercloud.AvailabilityInternal { + err := &ErrInvalidAvailabilityProvided{} + err.Argument = "Availability" + err.Value = opts.Availability + return "", err + } + if (opts.Availability == gophercloud.Availability(endpoint.Interface)) && + (opts.Region == "" || endpoint.Region == opts.Region || endpoint.RegionID == opts.Region) { + endpoints = append(endpoints, endpoint) + } + } + } + } + + // If multiple endpoints were found, use the first result + // and disregard the other endpoints. + // + // This behavior matches the Python library. See GH-1764. + if len(endpoints) > 1 { + endpoints = endpoints[0:1] + } + + // Extract the URL from the matching Endpoint. + for _, endpoint := range endpoints { + return gophercloud.NormalizeURL(endpoint.URL), nil + } + + // Report an error if there were no matching endpoints. + err := &gophercloud.ErrEndpointNotFound{} + return "", err +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/errors.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/errors.go new file mode 100644 index 00000000..cba6ae5f --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/errors.go @@ -0,0 +1,47 @@ +package openstack + +import ( + "fmt" + + "github.com/gophercloud/gophercloud" +) + +// ErrEndpointNotFound is the error when no suitable endpoint can be found +// in the user's catalog +type ErrEndpointNotFound struct{ gophercloud.BaseError } + +func (e ErrEndpointNotFound) Error() string { + return "No suitable endpoint could be found in the service catalog." +} + +// ErrInvalidAvailabilityProvided is the error when an invalid endpoint +// availability is provided +type ErrInvalidAvailabilityProvided struct{ gophercloud.ErrInvalidInput } + +func (e ErrInvalidAvailabilityProvided) Error() string { + return fmt.Sprintf("Unexpected availability in endpoint query: %s", e.Value) +} + +// ErrNoAuthURL is the error when the OS_AUTH_URL environment variable is not +// found +type ErrNoAuthURL struct{ gophercloud.ErrInvalidInput } + +func (e ErrNoAuthURL) Error() string { + return "Environment variable OS_AUTH_URL needs to be set." +} + +// ErrNoUsername is the error when the OS_USERNAME environment variable is not +// found +type ErrNoUsername struct{ gophercloud.ErrInvalidInput } + +func (e ErrNoUsername) Error() string { + return "Environment variable OS_USERNAME needs to be set." +} + +// ErrNoPassword is the error when the OS_PASSWORD environment variable is not +// found +type ErrNoPassword struct{ gophercloud.ErrInvalidInput } + +func (e ErrNoPassword) Error() string { + return "Environment variable OS_PASSWORD needs to be set." +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tenants/doc.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tenants/doc.go new file mode 100644 index 00000000..348dd208 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tenants/doc.go @@ -0,0 +1,65 @@ +/* +Package tenants provides information and interaction with the +tenants API resource for the OpenStack Identity service. + +See http://developer.openstack.org/api-ref-identity-v2.html#identity-auth-v2 +and http://developer.openstack.org/api-ref-identity-v2.html#admin-tenants +for more information. + +Example to List Tenants + + listOpts := &tenants.ListOpts{ + Limit: 2, + } + + allPages, err := tenants.List(identityClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allTenants, err := tenants.ExtractTenants(allPages) + if err != nil { + panic(err) + } + + for _, tenant := range allTenants { + fmt.Printf("%+v\n", tenant) + } + +Example to Create a Tenant + + createOpts := tenants.CreateOpts{ + Name: "tenant_name", + Description: "this is a tenant", + Enabled: gophercloud.Enabled, + } + + tenant, err := tenants.Create(identityClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Update a Tenant + + tenantID := "e6db6ed6277c461a853458589063b295" + + updateOpts := tenants.UpdateOpts{ + Description: "this is a new description", + Enabled: gophercloud.Disabled, + } + + tenant, err := tenants.Update(identityClient, tenantID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a Tenant + + tenantID := "e6db6ed6277c461a853458589063b295" + + err := tenants.Delete(identitYClient, tenantID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package tenants diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tenants/requests.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tenants/requests.go new file mode 100644 index 00000000..f16df38e --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tenants/requests.go @@ -0,0 +1,120 @@ +package tenants + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// ListOpts filters the Tenants that are returned by the List call. +type ListOpts struct { + // Marker is the ID of the last Tenant on the previous page. + Marker string `q:"marker"` + + // Limit specifies the page size. + Limit int `q:"limit"` +} + +// List enumerates the Tenants to which the current token has access. +func List(client *gophercloud.ServiceClient, opts *ListOpts) pagination.Pager { + url := listURL(client) + if opts != nil { + q, err := gophercloud.BuildQueryString(opts) + if err != nil { + return pagination.Pager{Err: err} + } + url += q.String() + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return TenantPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// CreateOpts represents the options needed when creating new tenant. +type CreateOpts struct { + // Name is the name of the tenant. + Name string `json:"name" required:"true"` + + // Description is the description of the tenant. + Description string `json:"description,omitempty"` + + // Enabled sets the tenant status to enabled or disabled. + Enabled *bool `json:"enabled,omitempty"` +} + +// CreateOptsBuilder enables extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToTenantCreateMap() (map[string]interface{}, error) +} + +// ToTenantCreateMap assembles a request body based on the contents of +// a CreateOpts. +func (opts CreateOpts) ToTenantCreateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "tenant") +} + +// Create is the operation responsible for creating new tenant. +func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToTenantCreateMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(createURL(client), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 201}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Get requests details on a single tenant by ID. +func Get(client *gophercloud.ServiceClient, id string) (r GetResult) { + resp, err := client.Get(getURL(client, id), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the +// Update request. +type UpdateOptsBuilder interface { + ToTenantUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts specifies the base attributes that may be updated on an existing +// tenant. +type UpdateOpts struct { + // Name is the name of the tenant. + Name string `json:"name,omitempty"` + + // Description is the description of the tenant. + Description *string `json:"description,omitempty"` + + // Enabled sets the tenant status to enabled or disabled. + Enabled *bool `json:"enabled,omitempty"` +} + +// ToTenantUpdateMap formats an UpdateOpts structure into a request body. +func (opts UpdateOpts) ToTenantUpdateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "tenant") +} + +// Update is the operation responsible for updating exist tenants by their TenantID. +func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToTenantUpdateMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Put(updateURL(client, id), &b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Delete is the operation responsible for permanently deleting a tenant. +func Delete(client *gophercloud.ServiceClient, id string) (r DeleteResult) { + resp, err := client.Delete(deleteURL(client, id), nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tenants/results.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tenants/results.go new file mode 100644 index 00000000..2daff984 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tenants/results.go @@ -0,0 +1,95 @@ +package tenants + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// Tenant is a grouping of users in the identity service. +type Tenant struct { + // ID is a unique identifier for this tenant. + ID string `json:"id"` + + // Name is a friendlier user-facing name for this tenant. + Name string `json:"name"` + + // Description is a human-readable explanation of this Tenant's purpose. + Description string `json:"description"` + + // Enabled indicates whether or not a tenant is active. + Enabled bool `json:"enabled"` +} + +// TenantPage is a single page of Tenant results. +type TenantPage struct { + pagination.LinkedPageBase +} + +// IsEmpty determines whether or not a page of Tenants contains any results. +func (r TenantPage) IsEmpty() (bool, error) { + if r.StatusCode == 204 { + return true, nil + } + + tenants, err := ExtractTenants(r) + return len(tenants) == 0, err +} + +// NextPageURL extracts the "next" link from the tenants_links section of the result. +func (r TenantPage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"tenants_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// ExtractTenants returns a slice of Tenants contained in a single page of +// results. +func ExtractTenants(r pagination.Page) ([]Tenant, error) { + var s struct { + Tenants []Tenant `json:"tenants"` + } + err := (r.(TenantPage)).ExtractInto(&s) + return s.Tenants, err +} + +type tenantResult struct { + gophercloud.Result +} + +// Extract interprets any tenantResults as a Tenant. +func (r tenantResult) Extract() (*Tenant, error) { + var s struct { + Tenant *Tenant `json:"tenant"` + } + err := r.ExtractInto(&s) + return s.Tenant, err +} + +// GetResult is the response from a Get request. Call its Extract method to +// interpret it as a Tenant. +type GetResult struct { + tenantResult +} + +// CreateResult is the response from a Create request. Call its Extract method +// to interpret it as a Tenant. +type CreateResult struct { + tenantResult +} + +// DeleteResult is the response from a Get request. Call its ExtractErr method +// to determine if the call succeeded or failed. +type DeleteResult struct { + gophercloud.ErrResult +} + +// UpdateResult is the response from a Update request. Call its Extract method +// to interpret it as a Tenant. +type UpdateResult struct { + tenantResult +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tenants/urls.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tenants/urls.go new file mode 100644 index 00000000..0f026690 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tenants/urls.go @@ -0,0 +1,23 @@ +package tenants + +import "github.com/gophercloud/gophercloud" + +func listURL(client *gophercloud.ServiceClient) string { + return client.ServiceURL("tenants") +} + +func getURL(client *gophercloud.ServiceClient, tenantID string) string { + return client.ServiceURL("tenants", tenantID) +} + +func createURL(client *gophercloud.ServiceClient) string { + return client.ServiceURL("tenants") +} + +func deleteURL(client *gophercloud.ServiceClient, tenantID string) string { + return client.ServiceURL("tenants", tenantID) +} + +func updateURL(client *gophercloud.ServiceClient, tenantID string) string { + return client.ServiceURL("tenants", tenantID) +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tokens/doc.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tokens/doc.go new file mode 100644 index 00000000..5375eea8 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tokens/doc.go @@ -0,0 +1,46 @@ +/* +Package tokens provides information and interaction with the token API +resource for the OpenStack Identity service. + +For more information, see: +http://developer.openstack.org/api-ref-identity-v2.html#identity-auth-v2 + +Example to Create an Unscoped Token from a Password + + authOpts := gophercloud.AuthOptions{ + Username: "user", + Password: "pass" + } + + token, err := tokens.Create(identityClient, authOpts).ExtractToken() + if err != nil { + panic(err) + } + +Example to Create a Token from a Tenant ID and Password + + authOpts := gophercloud.AuthOptions{ + Username: "user", + Password: "password", + TenantID: "fc394f2ab2df4114bde39905f800dc57" + } + + token, err := tokens.Create(identityClient, authOpts).ExtractToken() + if err != nil { + panic(err) + } + +Example to Create a Token from a Tenant Name and Password + + authOpts := gophercloud.AuthOptions{ + Username: "user", + Password: "password", + TenantName: "tenantname" + } + + token, err := tokens.Create(identityClient, authOpts).ExtractToken() + if err != nil { + panic(err) + } +*/ +package tokens diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tokens/requests.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tokens/requests.go new file mode 100644 index 00000000..84f16c3f --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tokens/requests.go @@ -0,0 +1,105 @@ +package tokens + +import "github.com/gophercloud/gophercloud" + +// PasswordCredentialsV2 represents the required options to authenticate +// with a username and password. +type PasswordCredentialsV2 struct { + Username string `json:"username" required:"true"` + Password string `json:"password" required:"true"` +} + +// TokenCredentialsV2 represents the required options to authenticate +// with a token. +type TokenCredentialsV2 struct { + ID string `json:"id,omitempty" required:"true"` +} + +// AuthOptionsV2 wraps a gophercloud AuthOptions in order to adhere to the +// AuthOptionsBuilder interface. +type AuthOptionsV2 struct { + PasswordCredentials *PasswordCredentialsV2 `json:"passwordCredentials,omitempty" xor:"TokenCredentials"` + + // The TenantID and TenantName fields are optional for the Identity V2 API. + // Some providers allow you to specify a TenantName instead of the TenantId. + // Some require both. Your provider's authentication policies will determine + // how these fields influence authentication. + TenantID string `json:"tenantId,omitempty"` + TenantName string `json:"tenantName,omitempty"` + + // TokenCredentials allows users to authenticate (possibly as another user) + // with an authentication token ID. + TokenCredentials *TokenCredentialsV2 `json:"token,omitempty" xor:"PasswordCredentials"` +} + +// AuthOptionsBuilder allows extensions to add additional parameters to the +// token create request. +type AuthOptionsBuilder interface { + // ToTokenCreateMap assembles the Create request body, returning an error + // if parameters are missing or inconsistent. + ToTokenV2CreateMap() (map[string]interface{}, error) +} + +// AuthOptions are the valid options for Openstack Identity v2 authentication. +// For field descriptions, see gophercloud.AuthOptions. +type AuthOptions struct { + IdentityEndpoint string `json:"-"` + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty"` + TenantID string `json:"tenantId,omitempty"` + TenantName string `json:"tenantName,omitempty"` + AllowReauth bool `json:"-"` + TokenID string +} + +// ToTokenV2CreateMap builds a token request body from the given AuthOptions. +func (opts AuthOptions) ToTokenV2CreateMap() (map[string]interface{}, error) { + v2Opts := AuthOptionsV2{ + TenantID: opts.TenantID, + TenantName: opts.TenantName, + } + + if opts.Password != "" { + v2Opts.PasswordCredentials = &PasswordCredentialsV2{ + Username: opts.Username, + Password: opts.Password, + } + } else { + v2Opts.TokenCredentials = &TokenCredentialsV2{ + ID: opts.TokenID, + } + } + + b, err := gophercloud.BuildRequestBody(v2Opts, "auth") + if err != nil { + return nil, err + } + return b, nil +} + +// Create authenticates to the identity service and attempts to acquire a Token. +// Generally, rather than interact with this call directly, end users should +// call openstack.AuthenticatedClient(), which abstracts all of the gory details +// about navigating service catalogs and such. +func Create(client *gophercloud.ServiceClient, auth AuthOptionsBuilder) (r CreateResult) { + b, err := auth.ToTokenV2CreateMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(CreateURL(client), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 203}, + OmitHeaders: []string{"X-Auth-Token"}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Get validates and retrieves information for user's token. +func Get(client *gophercloud.ServiceClient, token string) (r GetResult) { + resp, err := client.Get(GetURL(client, token), &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 203}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tokens/results.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tokens/results.go new file mode 100644 index 00000000..ee5da37f --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tokens/results.go @@ -0,0 +1,174 @@ +package tokens + +import ( + "time" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/identity/v2/tenants" +) + +// Token provides only the most basic information related to an authentication +// token. +type Token struct { + // ID provides the primary means of identifying a user to the OpenStack API. + // OpenStack defines this field as an opaque value, so do not depend on its + // content. It is safe, however, to compare for equality. + ID string + + // ExpiresAt provides a timestamp in ISO 8601 format, indicating when the + // authentication token becomes invalid. After this point in time, future + // API requests made using this authentication token will respond with + // errors. Either the caller will need to reauthenticate manually, or more + // preferably, the caller should exploit automatic re-authentication. + // See the AuthOptions structure for more details. + ExpiresAt time.Time + + // Tenant provides information about the tenant to which this token grants + // access. + Tenant tenants.Tenant +} + +// Role is a role for a user. +type Role struct { + Name string `json:"name"` +} + +// User is an OpenStack user. +type User struct { + ID string `json:"id"` + Name string `json:"name"` + UserName string `json:"username"` + Roles []Role `json:"roles"` +} + +// Endpoint represents a single API endpoint offered by a service. +// It provides the public and internal URLs, if supported, along with a region +// specifier, again if provided. +// +// The significance of the Region field will depend upon your provider. +// +// In addition, the interface offered by the service will have version +// information associated with it through the VersionId, VersionInfo, and +// VersionList fields, if provided or supported. +// +// In all cases, fields which aren't supported by the provider and service +// combined will assume a zero-value (""). +type Endpoint struct { + TenantID string `json:"tenantId"` + PublicURL string `json:"publicURL"` + InternalURL string `json:"internalURL"` + AdminURL string `json:"adminURL"` + Region string `json:"region"` + VersionID string `json:"versionId"` + VersionInfo string `json:"versionInfo"` + VersionList string `json:"versionList"` +} + +// CatalogEntry provides a type-safe interface to an Identity API V2 service +// catalog listing. +// +// Each class of service, such as cloud DNS or block storage services, will have +// a single CatalogEntry representing it. +// +// Note: when looking for the desired service, try, whenever possible, to key +// off the type field. Otherwise, you'll tie the representation of the service +// to a specific provider. +type CatalogEntry struct { + // Name will contain the provider-specified name for the service. + Name string `json:"name"` + + // Type will contain a type string if OpenStack defines a type for the + // service. Otherwise, for provider-specific services, the provider may assign + // their own type strings. + Type string `json:"type"` + + // Endpoints will let the caller iterate over all the different endpoints that + // may exist for the service. + Endpoints []Endpoint `json:"endpoints"` +} + +// ServiceCatalog provides a view into the service catalog from a previous, +// successful authentication. +type ServiceCatalog struct { + Entries []CatalogEntry +} + +// CreateResult is the response from a Create request. Use ExtractToken() to +// interpret it as a Token, or ExtractServiceCatalog() to interpret it as a +// service catalog. +type CreateResult struct { + gophercloud.Result +} + +// GetResult is the deferred response from a Get call, which is the same with a +// Created token. Use ExtractUser() to interpret it as a User. +type GetResult struct { + CreateResult +} + +// ExtractToken returns the just-created Token from a CreateResult. +func (r CreateResult) ExtractToken() (*Token, error) { + var s struct { + Access struct { + Token struct { + Expires string `json:"expires"` + ID string `json:"id"` + Tenant tenants.Tenant `json:"tenant"` + } `json:"token"` + } `json:"access"` + } + + err := r.ExtractInto(&s) + if err != nil { + return nil, err + } + + expiresTs, err := time.Parse(gophercloud.RFC3339Milli, s.Access.Token.Expires) + if err != nil { + return nil, err + } + + return &Token{ + ID: s.Access.Token.ID, + ExpiresAt: expiresTs, + Tenant: s.Access.Token.Tenant, + }, nil +} + +// ExtractTokenID implements the gophercloud.AuthResult interface. The returned +// string is the same as the ID field of the Token struct returned from +// ExtractToken(). +func (r CreateResult) ExtractTokenID() (string, error) { + var s struct { + Access struct { + Token struct { + ID string `json:"id"` + } `json:"token"` + } `json:"access"` + } + err := r.ExtractInto(&s) + return s.Access.Token.ID, err +} + +// ExtractServiceCatalog returns the ServiceCatalog that was generated along +// with the user's Token. +func (r CreateResult) ExtractServiceCatalog() (*ServiceCatalog, error) { + var s struct { + Access struct { + Entries []CatalogEntry `json:"serviceCatalog"` + } `json:"access"` + } + err := r.ExtractInto(&s) + return &ServiceCatalog{Entries: s.Access.Entries}, err +} + +// ExtractUser returns the User from a GetResult. +func (r GetResult) ExtractUser() (*User, error) { + var s struct { + Access struct { + User User `json:"user"` + } `json:"access"` + } + err := r.ExtractInto(&s) + return &s.Access.User, err +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tokens/urls.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tokens/urls.go new file mode 100644 index 00000000..ee0a28f2 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tokens/urls.go @@ -0,0 +1,13 @@ +package tokens + +import "github.com/gophercloud/gophercloud" + +// CreateURL generates the URL used to create new Tokens. +func CreateURL(client *gophercloud.ServiceClient) string { + return client.ServiceURL("tokens") +} + +// GetURL generates the URL used to Validate Tokens. +func GetURL(client *gophercloud.ServiceClient, token string) string { + return client.ServiceURL("tokens", token) +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/ec2tokens/doc.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/ec2tokens/doc.go new file mode 100644 index 00000000..a30d0faf --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/ec2tokens/doc.go @@ -0,0 +1,40 @@ +/* +Package tokens provides information and interaction with the EC2 token API +resource for the OpenStack Identity service. + +For more information, see: +https://docs.openstack.org/api-ref/identity/v2-ext/ + +Example to Create a Token From an EC2 access and secret keys + + var authOptions tokens.AuthOptionsBuilder + authOptions = &ec2tokens.AuthOptions{ + Access: "a7f1e798b7c2417cba4a02de97dc3cdc", + Secret: "18f4f6761ada4e3795fa5273c30349b9", + } + + token, err := ec2tokens.Create(identityClient, authOptions).ExtractToken() + if err != nil { + panic(err) + } + +Example to auth a client using EC2 access and secret keys + + client, err := openstack.NewClient("http://localhost:5000/v3") + if err != nil { + panic(err) + } + + var authOptions tokens.AuthOptionsBuilder + authOptions = &ec2tokens.AuthOptions{ + Access: "a7f1e798b7c2417cba4a02de97dc3cdc", + Secret: "18f4f6761ada4e3795fa5273c30349b9", + AllowReauth: true, + } + + err = openstack.AuthenticateV3(client, authOptions, gophercloud.EndpointOpts{}) + if err != nil { + panic(err) + } +*/ +package ec2tokens diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/ec2tokens/requests.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/ec2tokens/requests.go new file mode 100644 index 00000000..32ba0e62 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/ec2tokens/requests.go @@ -0,0 +1,377 @@ +package ec2tokens + +import ( + "crypto/hmac" + "crypto/sha1" + "crypto/sha256" + "encoding/hex" + "fmt" + "math/rand" + "net/url" + "sort" + "strings" + "time" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/identity/v3/tokens" +) + +const ( + // EC2CredentialsAwsRequestV4 is a constant, used to generate AWS + // Credential V4. + EC2CredentialsAwsRequestV4 = "aws4_request" + // EC2CredentialsHmacSha1V2 is a HMAC SHA1 signature method. Used to + // generate AWS Credential V2. + EC2CredentialsHmacSha1V2 = "HmacSHA1" + // EC2CredentialsHmacSha256V2 is a HMAC SHA256 signature method. Used + // to generate AWS Credential V2. + EC2CredentialsHmacSha256V2 = "HmacSHA256" + // EC2CredentialsAwsHmacV4 is an AWS signature V4 signing method. + // More details: + // https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html + EC2CredentialsAwsHmacV4 = "AWS4-HMAC-SHA256" + // EC2CredentialsTimestampFormatV4 is an AWS signature V4 timestamp + // format. + EC2CredentialsTimestampFormatV4 = "20060102T150405Z" + // EC2CredentialsDateFormatV4 is an AWS signature V4 date format. + EC2CredentialsDateFormatV4 = "20060102" +) + +// AuthOptions represents options for authenticating a user using EC2 credentials. +type AuthOptions struct { + // Access is the EC2 Credential Access ID. + Access string `json:"access" required:"true"` + // Secret is the EC2 Credential Secret, used to calculate signature. + // Not used, when a Signature is is. + Secret string `json:"-"` + // Host is a HTTP request Host header. Used to calculate an AWS + // signature V2. For signature V4 set the Host inside Headers map. + // Optional. + Host string `json:"host"` + // Path is a HTTP request path. Optional. + Path string `json:"path"` + // Verb is a HTTP request method. Optional. + Verb string `json:"verb"` + // Headers is a map of HTTP request headers. Optional. + Headers map[string]string `json:"headers"` + // Region is a region name to calculate an AWS signature V4. Optional. + Region string `json:"-"` + // Service is a service name to calculate an AWS signature V4. Optional. + Service string `json:"-"` + // Params is a map of GET method parameters. Optional. + Params map[string]string `json:"params"` + // AllowReauth allows Gophercloud to re-authenticate automatically + // if/when your token expires. + AllowReauth bool `json:"-"` + // Signature can be either a []byte (encoded to base64 automatically) or + // a string. You can set the singature explicitly, when you already know + // it. In this case default Params won't be automatically set. Optional. + Signature interface{} `json:"signature"` + // BodyHash is a HTTP request body sha256 hash. When nil and Signature + // is not set, a random hash is generated. Optional. + BodyHash *string `json:"body_hash"` + // Timestamp is a timestamp to calculate a V4 signature. Optional. + Timestamp *time.Time `json:"-"` + // Token is a []byte string (encoded to base64 automatically) which was + // signed by an EC2 secret key. Used by S3 tokens for validation only. + // Token must be set with a Signature. If a Signature is not provided, + // a Token will be generated automatically along with a Signature. + Token []byte `json:"token,omitempty"` +} + +// EC2CredentialsBuildCanonicalQueryStringV2 builds a canonical query string +// for an AWS signature V2. +// https://github.com/openstack/python-keystoneclient/blob/stable/train/keystoneclient/contrib/ec2/utils.py#L133 +func EC2CredentialsBuildCanonicalQueryStringV2(params map[string]string) string { + var keys []string + for k := range params { + keys = append(keys, k) + } + sort.Strings(keys) + + var pairs []string + for _, k := range keys { + pairs = append(pairs, fmt.Sprintf("%s=%s", k, url.QueryEscape(params[k]))) + } + + return strings.Join(pairs, "&") +} + +// EC2CredentialsBuildStringToSignV2 builds a string to sign an AWS signature +// V2. +// https://github.com/openstack/python-keystoneclient/blob/stable/train/keystoneclient/contrib/ec2/utils.py#L148 +func EC2CredentialsBuildStringToSignV2(opts AuthOptions) []byte { + stringToSign := strings.Join([]string{ + opts.Verb, + opts.Host, + opts.Path, + }, "\n") + + return []byte(strings.Join([]string{ + stringToSign, + EC2CredentialsBuildCanonicalQueryStringV2(opts.Params), + }, "\n")) +} + +// EC2CredentialsBuildCanonicalQueryStringV2 builds a canonical query string +// for an AWS signature V4. +// https://github.com/openstack/python-keystoneclient/blob/stable/train/keystoneclient/contrib/ec2/utils.py#L244 +func EC2CredentialsBuildCanonicalQueryStringV4(verb string, params map[string]string) string { + if verb == "POST" { + return "" + } + return EC2CredentialsBuildCanonicalQueryStringV2(params) +} + +// EC2CredentialsBuildCanonicalHeadersV4 builds a canonical string based on +// "headers" map and "signedHeaders" string parameters. +// https://github.com/openstack/python-keystoneclient/blob/stable/train/keystoneclient/contrib/ec2/utils.py#L216 +func EC2CredentialsBuildCanonicalHeadersV4(headers map[string]string, signedHeaders string) string { + headersLower := make(map[string]string, len(headers)) + for k, v := range headers { + headersLower[strings.ToLower(k)] = v + } + + var headersList []string + for _, h := range strings.Split(signedHeaders, ";") { + if v, ok := headersLower[h]; ok { + headersList = append(headersList, h+":"+v) + } + } + + return strings.Join(headersList, "\n") + "\n" +} + +// EC2CredentialsBuildSignatureKeyV4 builds a HMAC 256 signature key based on +// input parameters. +// https://github.com/openstack/python-keystoneclient/blob/stable/train/keystoneclient/contrib/ec2/utils.py#L169 +func EC2CredentialsBuildSignatureKeyV4(secret, region, service string, date time.Time) []byte { + kDate := sumHMAC256([]byte("AWS4"+secret), []byte(date.Format(EC2CredentialsDateFormatV4))) + kRegion := sumHMAC256(kDate, []byte(region)) + kService := sumHMAC256(kRegion, []byte(service)) + return sumHMAC256(kService, []byte(EC2CredentialsAwsRequestV4)) +} + +// EC2CredentialsBuildStringToSignV4 builds an AWS v4 signature string to sign +// based on input parameters. +// https://github.com/openstack/python-keystoneclient/blob/stable/train/keystoneclient/contrib/ec2/utils.py#L251 +func EC2CredentialsBuildStringToSignV4(opts AuthOptions, signedHeaders string, bodyHash string, date time.Time) []byte { + scope := strings.Join([]string{ + date.Format(EC2CredentialsDateFormatV4), + opts.Region, + opts.Service, + EC2CredentialsAwsRequestV4, + }, "/") + + canonicalRequest := strings.Join([]string{ + opts.Verb, + opts.Path, + EC2CredentialsBuildCanonicalQueryStringV4(opts.Verb, opts.Params), + EC2CredentialsBuildCanonicalHeadersV4(opts.Headers, signedHeaders), + signedHeaders, + bodyHash, + }, "\n") + hash := sha256.Sum256([]byte(canonicalRequest)) + + return []byte(strings.Join([]string{ + EC2CredentialsAwsHmacV4, + date.Format(EC2CredentialsTimestampFormatV4), + scope, + hex.EncodeToString(hash[:]), + }, "\n")) +} + +// EC2CredentialsBuildSignatureV4 builds an AWS v4 signature based on input +// parameters. +// https://github.com/openstack/python-keystoneclient/blob/stable/train/keystoneclient/contrib/ec2/utils.py#L285..L286 +func EC2CredentialsBuildSignatureV4(key []byte, stringToSign []byte) string { + return hex.EncodeToString(sumHMAC256(key, stringToSign)) +} + +// EC2CredentialsBuildAuthorizationHeaderV4 builds an AWS v4 Authorization +// header based on auth parameters, date and signature +func EC2CredentialsBuildAuthorizationHeaderV4(opts AuthOptions, signedHeaders string, signature string, date time.Time) string { + return fmt.Sprintf("%s Credential=%s/%s/%s/%s/%s, SignedHeaders=%s, Signature=%s", + EC2CredentialsAwsHmacV4, + opts.Access, + date.Format(EC2CredentialsDateFormatV4), + opts.Region, + opts.Service, + EC2CredentialsAwsRequestV4, + signedHeaders, + signature) +} + +// ToTokenV3ScopeMap is a dummy method to satisfy tokens.AuthOptionsBuilder +// interface. +func (opts *AuthOptions) ToTokenV3ScopeMap() (map[string]interface{}, error) { + return nil, nil +} + +// ToTokenV3HeadersMap allows AuthOptions to satisfy the AuthOptionsBuilder +// interface in the v3 tokens package. +func (opts *AuthOptions) ToTokenV3HeadersMap(map[string]interface{}) (map[string]string, error) { + return nil, nil +} + +// CanReauth is a method method to satisfy tokens.AuthOptionsBuilder interface +func (opts *AuthOptions) CanReauth() bool { + return opts.AllowReauth +} + +// ToTokenV3CreateMap formats an AuthOptions into a create request. +func (opts *AuthOptions) ToTokenV3CreateMap(map[string]interface{}) (map[string]interface{}, error) { + b, err := gophercloud.BuildRequestBody(opts, "credentials") + if err != nil { + return nil, err + } + + if opts.Signature != nil { + return b, nil + } + + // calculate signature, when it is not set + c, _ := b["credentials"].(map[string]interface{}) + h := interfaceToMap(c, "headers") + p := interfaceToMap(c, "params") + + // detect and process a signature v2 + if v, ok := p["SignatureVersion"]; ok && v == "2" { + if _, ok := c["body_hash"]; ok { + delete(c, "body_hash") + } + if _, ok := c["headers"]; ok { + delete(c, "headers") + } + if v, ok := p["SignatureMethod"]; ok { + // params is a map of strings + strToSign := EC2CredentialsBuildStringToSignV2(*opts) + switch v { + case EC2CredentialsHmacSha1V2: + // keystone uses this method only when HmacSHA256 is not available on the server side + // https://github.com/openstack/python-keystoneclient/blob/stable/train/keystoneclient/contrib/ec2/utils.py#L151..L156 + c["signature"] = sumHMAC1([]byte(opts.Secret), strToSign) + return b, nil + case EC2CredentialsHmacSha256V2: + c["signature"] = sumHMAC256([]byte(opts.Secret), strToSign) + return b, nil + } + return nil, fmt.Errorf("unsupported signature method: %s", v) + } + return nil, fmt.Errorf("signature method must be provided") + } else if ok { + return nil, fmt.Errorf("unsupported signature version: %s", v) + } + + // it is not a signature v2, but a signature v4 + date := time.Now().UTC() + if opts.Timestamp != nil { + date = *opts.Timestamp + } + if v, _ := c["body_hash"]; v == nil { + // when body_hash is not set, generate a random one + c["body_hash"] = randomBodyHash() + } + + signedHeaders, _ := h["X-Amz-SignedHeaders"] + + stringToSign := EC2CredentialsBuildStringToSignV4(*opts, signedHeaders, c["body_hash"].(string), date) + key := EC2CredentialsBuildSignatureKeyV4(opts.Secret, opts.Region, opts.Service, date) + c["signature"] = EC2CredentialsBuildSignatureV4(key, stringToSign) + h["X-Amz-Date"] = date.Format(EC2CredentialsTimestampFormatV4) + h["Authorization"] = EC2CredentialsBuildAuthorizationHeaderV4(*opts, signedHeaders, c["signature"].(string), date) + + // token is only used for S3 tokens validation and will be removed when using EC2 validation + c["token"] = stringToSign + + return b, nil +} + +// Create authenticates and either generates a new token from EC2 credentials +func Create(c *gophercloud.ServiceClient, opts tokens.AuthOptionsBuilder) (r tokens.CreateResult) { + b, err := opts.ToTokenV3CreateMap(nil) + if err != nil { + r.Err = err + return + } + + // delete "token" element, since it is used in s3tokens + deleteBodyElements(b, "token") + + resp, err := c.Post(ec2tokensURL(c), b, &r.Body, &gophercloud.RequestOpts{ + MoreHeaders: map[string]string{"X-Auth-Token": ""}, + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// ValidateS3Token authenticates an S3 request using EC2 credentials. Doesn't +// generate a new token ID, but returns a tokens.CreateResult. +func ValidateS3Token(c *gophercloud.ServiceClient, opts tokens.AuthOptionsBuilder) (r tokens.CreateResult) { + b, err := opts.ToTokenV3CreateMap(nil) + if err != nil { + r.Err = err + return + } + + // delete unused element, since it is used in ec2tokens only + deleteBodyElements(b, "body_hash", "headers", "host", "params", "path", "verb") + + resp, err := c.Post(s3tokensURL(c), b, &r.Body, &gophercloud.RequestOpts{ + MoreHeaders: map[string]string{"X-Auth-Token": ""}, + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// The following are small helper functions used to help build the signature. + +// sumHMAC1 is a func to implement the HMAC SHA1 signature method. +func sumHMAC1(key []byte, data []byte) []byte { + hash := hmac.New(sha1.New, key) + hash.Write(data) + return hash.Sum(nil) +} + +// sumHMAC256 is a func to implement the HMAC SHA256 signature method. +func sumHMAC256(key []byte, data []byte) []byte { + hash := hmac.New(sha256.New, key) + hash.Write(data) + return hash.Sum(nil) +} + +// randomBodyHash is a func to generate a random sha256 hexdigest. +func randomBodyHash() string { + h := make([]byte, 64) + rand.Read(h) + return hex.EncodeToString(h) +} + +// interfaceToMap is a func used to represent a "credentials" map element as a +// "map[string]string" +func interfaceToMap(c map[string]interface{}, key string) map[string]string { + // convert map[string]interface{} to map[string]string + m := make(map[string]string) + if v, _ := c[key].(map[string]interface{}); v != nil { + for k, v := range v { + m[k] = v.(string) + } + } + + c[key] = m + + return m +} + +// deleteBodyElements deletes map body elements +func deleteBodyElements(b map[string]interface{}, elements ...string) { + if c, ok := b["credentials"].(map[string]interface{}); ok { + for _, k := range elements { + if _, ok := c[k]; ok { + delete(c, k) + } + } + } +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/ec2tokens/urls.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/ec2tokens/urls.go new file mode 100644 index 00000000..84b33b28 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/ec2tokens/urls.go @@ -0,0 +1,11 @@ +package ec2tokens + +import "github.com/gophercloud/gophercloud" + +func ec2tokensURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("ec2tokens") +} + +func s3tokensURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("s3tokens") +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/oauth1/doc.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/oauth1/doc.go new file mode 100644 index 00000000..4294ef6c --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/oauth1/doc.go @@ -0,0 +1,122 @@ +/* +Package oauth1 enables management of OpenStack OAuth1 tokens and Authentication. + +Example to Create an OAuth1 Consumer + + createConsumerOpts := oauth1.CreateConsumerOpts{ + Description: "My consumer", + } + consumer, err := oauth1.CreateConsumer(identityClient, createConsumerOpts).Extract() + if err != nil { + panic(err) + } + + // NOTE: Consumer secret is available only on create response + fmt.Printf("Consumer: %+v\n", consumer) + +Example to Request an unauthorized OAuth1 token + + requestTokenOpts := oauth1.RequestTokenOpts{ + OAuthConsumerKey: consumer.ID, + OAuthConsumerSecret: consumer.Secret, + OAuthSignatureMethod: oauth1.HMACSHA1, + RequestedProjectID: projectID, + } + requestToken, err := oauth1.RequestToken(identityClient, requestTokenOpts).Extract() + if err != nil { + panic(err) + } + + // NOTE: Request token secret is available only on request response + fmt.Printf("Request token: %+v\n", requestToken) + +Example to Authorize an unauthorized OAuth1 token + + authorizeTokenOpts := oauth1.AuthorizeTokenOpts{ + Roles: []oauth1.Role{ + {Name: "member"}, + }, + } + authToken, err := oauth1.AuthorizeToken(identityClient, requestToken.OAuthToken, authorizeTokenOpts).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("Verifier ID of the unauthorized Token: %+v\n", authToken.OAuthVerifier) + +Example to Create an OAuth1 Access Token + + accessTokenOpts := oauth1.CreateAccessTokenOpts{ + OAuthConsumerKey: consumer.ID, + OAuthConsumerSecret: consumer.Secret, + OAuthToken: requestToken.OAuthToken, + OAuthTokenSecret: requestToken.OAuthTokenSecret, + OAuthVerifier: authToken.OAuthVerifier, + OAuthSignatureMethod: oauth1.HMACSHA1, + } + accessToken, err := oauth1.CreateAccessToken(identityClient, accessTokenOpts).Extract() + if err != nil { + panic(err) + } + + // NOTE: Access token secret is available only on create response + fmt.Printf("OAuth1 Access Token: %+v\n", accessToken) + +Example to List User's OAuth1 Access Tokens + + allPages, err := oauth1.ListAccessTokens(identityClient, userID).AllPages() + if err != nil { + panic(err) + } + accessTokens, err := oauth1.ExtractAccessTokens(allPages) + if err != nil { + panic(err) + } + + for _, accessToken := range accessTokens { + fmt.Printf("Access Token: %+v\n", accessToken) + } + +Example to Authenticate a client using OAuth1 method + + client, err := openstack.NewClient("http://localhost:5000/v3") + if err != nil { + panic(err) + } + + authOptions := &oauth1.AuthOptions{ + // consumer token, created earlier + OAuthConsumerKey: consumer.ID, + OAuthConsumerSecret: consumer.Secret, + // access token, created earlier + OAuthToken: accessToken.OAuthToken, + OAuthTokenSecret: accessToken.OAuthTokenSecret, + OAuthSignatureMethod: oauth1.HMACSHA1, + } + err = openstack.AuthenticateV3(client, authOptions, gophercloud.EndpointOpts{}) + if err != nil { + panic(err) + } + +Example to Create a Token using OAuth1 method + + var oauth1Token struct { + tokens.Token + oauth1.TokenExt + } + + createOpts := &oauth1.AuthOptions{ + // consumer token, created earlier + OAuthConsumerKey: consumer.ID, + OAuthConsumerSecret: consumer.Secret, + // access token, created earlier + OAuthToken: accessToken.OAuthToken, + OAuthTokenSecret: accessToken.OAuthTokenSecret, + OAuthSignatureMethod: oauth1.HMACSHA1, + } + err := tokens.Create(identityClient, createOpts).ExtractInto(&oauth1Token) + if err != nil { + panic(err) + } +*/ +package oauth1 diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/oauth1/requests.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/oauth1/requests.go new file mode 100644 index 00000000..028b5a45 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/oauth1/requests.go @@ -0,0 +1,587 @@ +package oauth1 + +import ( + "crypto/hmac" + "crypto/sha1" + "encoding/base64" + "fmt" + "io/ioutil" + "math/rand" + "net/url" + "sort" + "strconv" + "strings" + "time" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/identity/v3/tokens" + "github.com/gophercloud/gophercloud/pagination" +) + +// Type SignatureMethod is a OAuth1 SignatureMethod type. +type SignatureMethod string + +const ( + // HMACSHA1 is a recommended OAuth1 signature method. + HMACSHA1 SignatureMethod = "HMAC-SHA1" + + // PLAINTEXT signature method is not recommended to be used in + // production environment. + PLAINTEXT SignatureMethod = "PLAINTEXT" + + // OAuth1TokenContentType is a supported content type for an OAuth1 + // token. + OAuth1TokenContentType = "application/x-www-form-urlencoded" +) + +// AuthOptions represents options for authenticating a user using OAuth1 tokens. +type AuthOptions struct { + // OAuthConsumerKey is the OAuth1 Consumer Key. + OAuthConsumerKey string `q:"oauth_consumer_key" required:"true"` + + // OAuthConsumerSecret is the OAuth1 Consumer Secret. Used to generate + // an OAuth1 request signature. + OAuthConsumerSecret string `required:"true"` + + // OAuthToken is the OAuth1 Request Token. + OAuthToken string `q:"oauth_token" required:"true"` + + // OAuthTokenSecret is the OAuth1 Request Token Secret. Used to generate + // an OAuth1 request signature. + OAuthTokenSecret string `required:"true"` + + // OAuthSignatureMethod is the OAuth1 signature method the Consumer used + // to sign the request. Supported values are "HMAC-SHA1" or "PLAINTEXT". + // "PLAINTEXT" is not recommended for production usage. + OAuthSignatureMethod SignatureMethod `q:"oauth_signature_method" required:"true"` + + // OAuthTimestamp is an OAuth1 request timestamp. If nil, current Unix + // timestamp will be used. + OAuthTimestamp *time.Time + + // OAuthNonce is an OAuth1 request nonce. Nonce must be a random string, + // uniquely generated for each request. Will be generated automatically + // when it is not set. + OAuthNonce string `q:"oauth_nonce"` + + // AllowReauth allows Gophercloud to re-authenticate automatically + // if/when your token expires. + AllowReauth bool +} + +// ToTokenV3HeadersMap builds the headers required for an OAuth1-based create +// request. +func (opts AuthOptions) ToTokenV3HeadersMap(headerOpts map[string]interface{}) (map[string]string, error) { + q, err := buildOAuth1QueryString(opts, opts.OAuthTimestamp, "") + if err != nil { + return nil, err + } + + signatureKeys := []string{opts.OAuthConsumerSecret, opts.OAuthTokenSecret} + + method := headerOpts["method"].(string) + u := headerOpts["url"].(string) + stringToSign := buildStringToSign(method, u, q.Query()) + signature := url.QueryEscape(signString(opts.OAuthSignatureMethod, stringToSign, signatureKeys)) + + authHeader := buildAuthHeader(q.Query(), signature) + + headers := map[string]string{ + "Authorization": authHeader, + "X-Auth-Token": "", + } + + return headers, nil +} + +// ToTokenV3ScopeMap allows AuthOptions to satisfy the tokens.AuthOptionsBuilder +// interface. +func (opts AuthOptions) ToTokenV3ScopeMap() (map[string]interface{}, error) { + return nil, nil +} + +// CanReauth allows AuthOptions to satisfy the tokens.AuthOptionsBuilder +// interface. +func (opts AuthOptions) CanReauth() bool { + return opts.AllowReauth +} + +// ToTokenV3CreateMap builds a create request body. +func (opts AuthOptions) ToTokenV3CreateMap(map[string]interface{}) (map[string]interface{}, error) { + // identityReq defines the "identity" portion of an OAuth1-based authentication + // create request body. + type identityReq struct { + Methods []string `json:"methods"` + OAuth1 struct{} `json:"oauth1"` + } + + // authReq defines the "auth" portion of an OAuth1-based authentication + // create request body. + type authReq struct { + Identity identityReq `json:"identity"` + } + + // oauth1Request defines how an OAuth1-based authentication create + // request body looks. + type oauth1Request struct { + Auth authReq `json:"auth"` + } + + var req oauth1Request + + req.Auth.Identity.Methods = []string{"oauth1"} + return gophercloud.BuildRequestBody(req, "") +} + +// Create authenticates and either generates a new OpenStack token from an +// OAuth1 token. +func Create(client *gophercloud.ServiceClient, opts tokens.AuthOptionsBuilder) (r tokens.CreateResult) { + b, err := opts.ToTokenV3CreateMap(nil) + if err != nil { + r.Err = err + return + } + + headerOpts := map[string]interface{}{ + "method": "POST", + "url": authURL(client), + } + + h, err := opts.ToTokenV3HeadersMap(headerOpts) + if err != nil { + r.Err = err + return + } + + resp, err := client.Post(authURL(client), b, &r.Body, &gophercloud.RequestOpts{ + MoreHeaders: h, + OkCodes: []int{201}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// CreateConsumerOptsBuilder allows extensions to add additional parameters to +// the CreateConsumer request. +type CreateConsumerOptsBuilder interface { + ToOAuth1CreateConsumerMap() (map[string]interface{}, error) +} + +// CreateConsumerOpts provides options used to create a new Consumer. +type CreateConsumerOpts struct { + // Description is the consumer description. + Description string `json:"description"` +} + +// ToOAuth1CreateConsumerMap formats a CreateConsumerOpts into a create request. +func (opts CreateConsumerOpts) ToOAuth1CreateConsumerMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "consumer") +} + +// Create creates a new Consumer. +func CreateConsumer(client *gophercloud.ServiceClient, opts CreateConsumerOptsBuilder) (r CreateConsumerResult) { + b, err := opts.ToOAuth1CreateConsumerMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(consumersURL(client), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{201}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Delete deletes a Consumer. +func DeleteConsumer(client *gophercloud.ServiceClient, id string) (r DeleteConsumerResult) { + resp, err := client.Delete(consumerURL(client, id), nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// List enumerates Consumers. +func ListConsumers(client *gophercloud.ServiceClient) pagination.Pager { + return pagination.NewPager(client, consumersURL(client), func(r pagination.PageResult) pagination.Page { + return ConsumersPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// GetConsumer retrieves details on a single Consumer by ID. +func GetConsumer(client *gophercloud.ServiceClient, id string) (r GetConsumerResult) { + resp, err := client.Get(consumerURL(client, id), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// UpdateConsumerOpts provides options used to update a consumer. +type UpdateConsumerOpts struct { + // Description is the consumer description. + Description string `json:"description"` +} + +// ToOAuth1UpdateConsumerMap formats an UpdateConsumerOpts into a consumer update +// request. +func (opts UpdateConsumerOpts) ToOAuth1UpdateConsumerMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "consumer") +} + +// UpdateConsumer updates an existing Consumer. +func UpdateConsumer(client *gophercloud.ServiceClient, id string, opts UpdateConsumerOpts) (r UpdateConsumerResult) { + b, err := opts.ToOAuth1UpdateConsumerMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Patch(consumerURL(client, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// RequestTokenOptsBuilder allows extensions to add additional parameters to the +// RequestToken request. +type RequestTokenOptsBuilder interface { + ToOAuth1RequestTokenHeaders(string, string) (map[string]string, error) +} + +// RequestTokenOpts provides options used to get a consumer unauthorized +// request token. +type RequestTokenOpts struct { + // OAuthConsumerKey is the OAuth1 Consumer Key. + OAuthConsumerKey string `q:"oauth_consumer_key" required:"true"` + + // OAuthConsumerSecret is the OAuth1 Consumer Secret. Used to generate + // an OAuth1 request signature. + OAuthConsumerSecret string `required:"true"` + + // OAuthSignatureMethod is the OAuth1 signature method the Consumer used + // to sign the request. Supported values are "HMAC-SHA1" or "PLAINTEXT". + // "PLAINTEXT" is not recommended for production usage. + OAuthSignatureMethod SignatureMethod `q:"oauth_signature_method" required:"true"` + + // OAuthTimestamp is an OAuth1 request timestamp. If nil, current Unix + // timestamp will be used. + OAuthTimestamp *time.Time + + // OAuthNonce is an OAuth1 request nonce. Nonce must be a random string, + // uniquely generated for each request. Will be generated automatically + // when it is not set. + OAuthNonce string `q:"oauth_nonce"` + + // RequestedProjectID is a Project ID a consumer user requested an + // access to. + RequestedProjectID string `h:"Requested-Project-Id"` +} + +// ToOAuth1RequestTokenHeaders formats a RequestTokenOpts into a map of request +// headers. +func (opts RequestTokenOpts) ToOAuth1RequestTokenHeaders(method, u string) (map[string]string, error) { + q, err := buildOAuth1QueryString(opts, opts.OAuthTimestamp, "oob") + if err != nil { + return nil, err + } + + h, err := gophercloud.BuildHeaders(opts) + if err != nil { + return nil, err + } + + signatureKeys := []string{opts.OAuthConsumerSecret} + stringToSign := buildStringToSign(method, u, q.Query()) + signature := url.QueryEscape(signString(opts.OAuthSignatureMethod, stringToSign, signatureKeys)) + authHeader := buildAuthHeader(q.Query(), signature) + + h["Authorization"] = authHeader + + return h, nil +} + +// RequestToken requests an unauthorized OAuth1 Token. +func RequestToken(client *gophercloud.ServiceClient, opts RequestTokenOptsBuilder) (r TokenResult) { + h, err := opts.ToOAuth1RequestTokenHeaders("POST", requestTokenURL(client)) + if err != nil { + r.Err = err + return + } + + resp, err := client.Post(requestTokenURL(client), nil, nil, &gophercloud.RequestOpts{ + MoreHeaders: h, + OkCodes: []int{201}, + KeepResponseBody: true, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + if r.Err != nil { + return + } + defer resp.Body.Close() + if v := r.Header.Get("Content-Type"); v != OAuth1TokenContentType { + r.Err = fmt.Errorf("unsupported Content-Type: %q", v) + return + } + r.Body, r.Err = ioutil.ReadAll(resp.Body) + return +} + +// AuthorizeTokenOptsBuilder allows extensions to add additional parameters to +// the AuthorizeToken request. +type AuthorizeTokenOptsBuilder interface { + ToOAuth1AuthorizeTokenMap() (map[string]interface{}, error) +} + +// AuthorizeTokenOpts provides options used to authorize a request token. +type AuthorizeTokenOpts struct { + Roles []Role `json:"roles"` +} + +// Role is a struct representing a role object in a AuthorizeTokenOpts struct. +type Role struct { + ID string `json:"id,omitempty"` + Name string `json:"name,omitempty"` +} + +// ToOAuth1AuthorizeTokenMap formats an AuthorizeTokenOpts into an authorize token +// request. +func (opts AuthorizeTokenOpts) ToOAuth1AuthorizeTokenMap() (map[string]interface{}, error) { + for _, r := range opts.Roles { + if r == (Role{}) { + return nil, fmt.Errorf("role must not be empty") + } + } + return gophercloud.BuildRequestBody(opts, "") +} + +// AuthorizeToken authorizes an unauthorized consumer token. +func AuthorizeToken(client *gophercloud.ServiceClient, id string, opts AuthorizeTokenOptsBuilder) (r AuthorizeTokenResult) { + b, err := opts.ToOAuth1AuthorizeTokenMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Put(authorizeTokenURL(client, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// CreateAccessTokenOptsBuilder allows extensions to add additional parameters +// to the CreateAccessToken request. +type CreateAccessTokenOptsBuilder interface { + ToOAuth1CreateAccessTokenHeaders(string, string) (map[string]string, error) +} + +// CreateAccessTokenOpts provides options used to create an OAuth1 token. +type CreateAccessTokenOpts struct { + // OAuthConsumerKey is the OAuth1 Consumer Key. + OAuthConsumerKey string `q:"oauth_consumer_key" required:"true"` + + // OAuthConsumerSecret is the OAuth1 Consumer Secret. Used to generate + // an OAuth1 request signature. + OAuthConsumerSecret string `required:"true"` + + // OAuthToken is the OAuth1 Request Token. + OAuthToken string `q:"oauth_token" required:"true"` + + // OAuthTokenSecret is the OAuth1 Request Token Secret. Used to generate + // an OAuth1 request signature. + OAuthTokenSecret string `required:"true"` + + // OAuthVerifier is the OAuth1 verification code. + OAuthVerifier string `q:"oauth_verifier" required:"true"` + + // OAuthSignatureMethod is the OAuth1 signature method the Consumer used + // to sign the request. Supported values are "HMAC-SHA1" or "PLAINTEXT". + // "PLAINTEXT" is not recommended for production usage. + OAuthSignatureMethod SignatureMethod `q:"oauth_signature_method" required:"true"` + + // OAuthTimestamp is an OAuth1 request timestamp. If nil, current Unix + // timestamp will be used. + OAuthTimestamp *time.Time + + // OAuthNonce is an OAuth1 request nonce. Nonce must be a random string, + // uniquely generated for each request. Will be generated automatically + // when it is not set. + OAuthNonce string `q:"oauth_nonce"` +} + +// ToOAuth1CreateAccessTokenHeaders formats a CreateAccessTokenOpts into a map of +// request headers. +func (opts CreateAccessTokenOpts) ToOAuth1CreateAccessTokenHeaders(method, u string) (map[string]string, error) { + q, err := buildOAuth1QueryString(opts, opts.OAuthTimestamp, "") + if err != nil { + return nil, err + } + + signatureKeys := []string{opts.OAuthConsumerSecret, opts.OAuthTokenSecret} + stringToSign := buildStringToSign(method, u, q.Query()) + signature := url.QueryEscape(signString(opts.OAuthSignatureMethod, stringToSign, signatureKeys)) + authHeader := buildAuthHeader(q.Query(), signature) + + headers := map[string]string{ + "Authorization": authHeader, + } + + return headers, nil +} + +// CreateAccessToken creates a new OAuth1 Access Token +func CreateAccessToken(client *gophercloud.ServiceClient, opts CreateAccessTokenOptsBuilder) (r TokenResult) { + h, err := opts.ToOAuth1CreateAccessTokenHeaders("POST", createAccessTokenURL(client)) + if err != nil { + r.Err = err + return + } + + resp, err := client.Post(createAccessTokenURL(client), nil, nil, &gophercloud.RequestOpts{ + MoreHeaders: h, + OkCodes: []int{201}, + KeepResponseBody: true, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + if r.Err != nil { + return + } + defer resp.Body.Close() + if v := r.Header.Get("Content-Type"); v != OAuth1TokenContentType { + r.Err = fmt.Errorf("unsupported Content-Type: %q", v) + return + } + r.Body, r.Err = ioutil.ReadAll(resp.Body) + return +} + +// GetAccessToken retrieves details on a single OAuth1 access token by an ID. +func GetAccessToken(client *gophercloud.ServiceClient, userID string, id string) (r GetAccessTokenResult) { + resp, err := client.Get(userAccessTokenURL(client, userID, id), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// RevokeAccessToken revokes an OAuth1 access token. +func RevokeAccessToken(client *gophercloud.ServiceClient, userID string, id string) (r RevokeAccessTokenResult) { + resp, err := client.Delete(userAccessTokenURL(client, userID, id), nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// ListAccessTokens enumerates authorized access tokens. +func ListAccessTokens(client *gophercloud.ServiceClient, userID string) pagination.Pager { + url := userAccessTokensURL(client, userID) + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return AccessTokensPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// ListAccessTokenRoles enumerates authorized access token roles. +func ListAccessTokenRoles(client *gophercloud.ServiceClient, userID string, id string) pagination.Pager { + url := userAccessTokenRolesURL(client, userID, id) + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return AccessTokenRolesPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// GetAccessTokenRole retrieves details on a single OAuth1 access token role by +// an ID. +func GetAccessTokenRole(client *gophercloud.ServiceClient, userID string, id string, roleID string) (r GetAccessTokenRoleResult) { + resp, err := client.Get(userAccessTokenRoleURL(client, userID, id, roleID), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// The following are small helper functions used to help build the signature. + +// buildOAuth1QueryString builds a URLEncoded parameters string specific for +// OAuth1-based requests. +func buildOAuth1QueryString(opts interface{}, timestamp *time.Time, callback string) (*url.URL, error) { + q, err := gophercloud.BuildQueryString(opts) + if err != nil { + return nil, err + } + + query := q.Query() + + if timestamp != nil { + // use provided timestamp + query.Set("oauth_timestamp", strconv.FormatInt(timestamp.Unix(), 10)) + } else { + // use current timestamp + query.Set("oauth_timestamp", strconv.FormatInt(time.Now().UTC().Unix(), 10)) + } + + if query.Get("oauth_nonce") == "" { + // when nonce is not set, generate a random one + query.Set("oauth_nonce", strconv.FormatInt(rand.Int63(), 10)+query.Get("oauth_timestamp")) + } + + if callback != "" { + query.Set("oauth_callback", callback) + } + query.Set("oauth_version", "1.0") + + return &url.URL{RawQuery: query.Encode()}, nil +} + +// buildStringToSign builds a string to be signed. +func buildStringToSign(method string, u string, query url.Values) []byte { + parsedURL, _ := url.Parse(u) + p := parsedURL.Port() + s := parsedURL.Scheme + + // Default scheme port must be stripped + if s == "http" && p == "80" || s == "https" && p == "443" { + parsedURL.Host = strings.TrimSuffix(parsedURL.Host, ":"+p) + } + + // Ensure that URL doesn't contain queries + parsedURL.RawQuery = "" + + v := strings.Join( + []string{method, url.QueryEscape(parsedURL.String()), url.QueryEscape(query.Encode())}, "&") + + return []byte(v) +} + +// signString signs a string using an OAuth1 signature method. +func signString(signatureMethod SignatureMethod, strToSign []byte, signatureKeys []string) string { + var key []byte + for i, k := range signatureKeys { + key = append(key, []byte(url.QueryEscape(k))...) + if i == 0 { + key = append(key, '&') + } + } + + var signedString string + switch signatureMethod { + case PLAINTEXT: + signedString = string(key) + default: + h := hmac.New(sha1.New, key) + h.Write(strToSign) + signedString = base64.StdEncoding.EncodeToString(h.Sum(nil)) + } + + return signedString +} + +// buildAuthHeader generates an OAuth1 Authorization header with a signature +// calculated using an OAuth1 signature method. +func buildAuthHeader(query url.Values, signature string) string { + var authHeader []string + var keys []string + for k := range query { + keys = append(keys, k) + } + sort.Strings(keys) + + for _, k := range keys { + for _, v := range query[k] { + authHeader = append(authHeader, fmt.Sprintf("%s=%q", k, url.QueryEscape(v))) + } + } + + authHeader = append(authHeader, fmt.Sprintf("oauth_signature=%q", signature)) + + return "OAuth " + strings.Join(authHeader, ", ") +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/oauth1/results.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/oauth1/results.go new file mode 100644 index 00000000..2a370616 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/oauth1/results.go @@ -0,0 +1,317 @@ +package oauth1 + +import ( + "encoding/json" + "net/url" + "time" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// Consumer represents a delegated authorization request between two +// identities. +type Consumer struct { + ID string `json:"id"` + Secret string `json:"secret"` + Description string `json:"description"` +} + +type consumerResult struct { + gophercloud.Result +} + +// CreateConsumerResult is the response from a Create operation. Call its +// Extract method to interpret it as a Consumer. +type CreateConsumerResult struct { + consumerResult +} + +// UpdateConsumerResult is the response from a Create operation. Call its +// Extract method to interpret it as a Consumer. +type UpdateConsumerResult struct { + consumerResult +} + +// DeleteConsumerResult is the response from a Delete operation. Call its +// ExtractErr to determine if the request succeeded or failed. +type DeleteConsumerResult struct { + gophercloud.ErrResult +} + +// ConsumersPage is a single page of Region results. +type ConsumersPage struct { + pagination.LinkedPageBase +} + +// GetConsumerResult is the response from a Get operation. Call its Extract +// method to interpret it as a Consumer. +type GetConsumerResult struct { + consumerResult +} + +// IsEmpty determines whether or not a page of Consumers contains any results. +func (c ConsumersPage) IsEmpty() (bool, error) { + if c.StatusCode == 204 { + return true, nil + } + + consumers, err := ExtractConsumers(c) + return len(consumers) == 0, err +} + +// NextPageURL extracts the "next" link from the links section of the result. +func (c ConsumersPage) NextPageURL() (string, error) { + var s struct { + Links struct { + Next string `json:"next"` + Previous string `json:"previous"` + } `json:"links"` + } + err := c.ExtractInto(&s) + if err != nil { + return "", err + } + return s.Links.Next, err +} + +// ExtractConsumers returns a slice of Consumers contained in a single page of +// results. +func ExtractConsumers(r pagination.Page) ([]Consumer, error) { + var s struct { + Consumers []Consumer `json:"consumers"` + } + err := (r.(ConsumersPage)).ExtractInto(&s) + return s.Consumers, err +} + +// Extract interprets any consumer result as a Consumer. +func (c consumerResult) Extract() (*Consumer, error) { + var s struct { + Consumer *Consumer `json:"consumer"` + } + err := c.ExtractInto(&s) + return s.Consumer, err +} + +// Token contains an OAuth1 token. +type Token struct { + // OAuthToken is the key value for the oauth token that the Identity API returns. + OAuthToken string `q:"oauth_token"` + // OAuthTokenSecret is the secret value associated with the OAuth Token. + OAuthTokenSecret string `q:"oauth_token_secret"` + // OAuthExpiresAt is the date and time when an OAuth token expires. + OAuthExpiresAt *time.Time `q:"-"` +} + +// TokenResult is a struct to handle +// "Content-Type: application/x-www-form-urlencoded" response. +type TokenResult struct { + gophercloud.Result + Body []byte +} + +// Extract interprets any OAuth1 token result as a Token. +func (r TokenResult) Extract() (*Token, error) { + if r.Err != nil { + return nil, r.Err + } + + values, err := url.ParseQuery(string(r.Body)) + if err != nil { + return nil, err + } + + token := &Token{ + OAuthToken: values.Get("oauth_token"), + OAuthTokenSecret: values.Get("oauth_token_secret"), + } + + if v := values.Get("oauth_expires_at"); v != "" { + if t, err := time.Parse(gophercloud.RFC3339Milli, v); err != nil { + return nil, err + } else { + token.OAuthExpiresAt = &t + } + } + + return token, nil +} + +// AuthorizedToken contains an OAuth1 authorized token info. +type AuthorizedToken struct { + // OAuthVerifier is the ID of the token verifier. + OAuthVerifier string `json:"oauth_verifier"` +} + +type AuthorizeTokenResult struct { + gophercloud.Result +} + +// Extract interprets AuthorizeTokenResult result as a AuthorizedToken. +func (r AuthorizeTokenResult) Extract() (*AuthorizedToken, error) { + var s struct { + AuthorizedToken *AuthorizedToken `json:"token"` + } + err := r.ExtractInto(&s) + return s.AuthorizedToken, err +} + +// AccessToken represents an AccessToken response as a struct. +type AccessToken struct { + ID string `json:"id"` + ConsumerID string `json:"consumer_id"` + ProjectID string `json:"project_id"` + AuthorizingUserID string `json:"authorizing_user_id"` + ExpiresAt *time.Time `json:"-"` +} + +func (r *AccessToken) UnmarshalJSON(b []byte) error { + type tmp AccessToken + var s struct { + tmp + ExpiresAt *gophercloud.JSONRFC3339Milli `json:"expires_at"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = AccessToken(s.tmp) + + if s.ExpiresAt != nil { + t := time.Time(*s.ExpiresAt) + r.ExpiresAt = &t + } + + return nil +} + +type GetAccessTokenResult struct { + gophercloud.Result +} + +// Extract interprets any GetAccessTokenResult result as an AccessToken. +func (r GetAccessTokenResult) Extract() (*AccessToken, error) { + var s struct { + AccessToken *AccessToken `json:"access_token"` + } + err := r.ExtractInto(&s) + return s.AccessToken, err +} + +// RevokeAccessTokenResult is the response from a Delete operation. Call its +// ExtractErr to determine if the request succeeded or failed. +type RevokeAccessTokenResult struct { + gophercloud.ErrResult +} + +// AccessTokensPage is a single page of Access Tokens results. +type AccessTokensPage struct { + pagination.LinkedPageBase +} + +// IsEmpty determines whether or not a an AccessTokensPage contains any results. +func (r AccessTokensPage) IsEmpty() (bool, error) { + if r.StatusCode == 204 { + return true, nil + } + + accessTokens, err := ExtractAccessTokens(r) + return len(accessTokens) == 0, err +} + +// NextPageURL extracts the "next" link from the links section of the result. +func (r AccessTokensPage) NextPageURL() (string, error) { + var s struct { + Links struct { + Next string `json:"next"` + Previous string `json:"previous"` + } `json:"links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return s.Links.Next, err +} + +// ExtractAccessTokens returns a slice of AccessTokens contained in a single +// page of results. +func ExtractAccessTokens(r pagination.Page) ([]AccessToken, error) { + var s struct { + AccessTokens []AccessToken `json:"access_tokens"` + } + err := (r.(AccessTokensPage)).ExtractInto(&s) + return s.AccessTokens, err +} + +// AccessTokenRole represents an Access Token Role struct. +type AccessTokenRole struct { + ID string `json:"id"` + Name string `json:"name"` + DomainID string `json:"domain_id"` +} + +// AccessTokenRolesPage is a single page of Access Token roles results. +type AccessTokenRolesPage struct { + pagination.LinkedPageBase +} + +// IsEmpty determines whether or not a an AccessTokensPage contains any results. +func (r AccessTokenRolesPage) IsEmpty() (bool, error) { + if r.StatusCode == 204 { + return true, nil + } + + accessTokenRoles, err := ExtractAccessTokenRoles(r) + return len(accessTokenRoles) == 0, err +} + +// NextPageURL extracts the "next" link from the links section of the result. +func (r AccessTokenRolesPage) NextPageURL() (string, error) { + var s struct { + Links struct { + Next string `json:"next"` + Previous string `json:"previous"` + } `json:"links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return s.Links.Next, err +} + +// ExtractAccessTokenRoles returns a slice of AccessTokenRole contained in a +// single page of results. +func ExtractAccessTokenRoles(r pagination.Page) ([]AccessTokenRole, error) { + var s struct { + AccessTokenRoles []AccessTokenRole `json:"roles"` + } + err := (r.(AccessTokenRolesPage)).ExtractInto(&s) + return s.AccessTokenRoles, err +} + +type GetAccessTokenRoleResult struct { + gophercloud.Result +} + +// Extract interprets any GetAccessTokenRoleResult result as an AccessTokenRole. +func (r GetAccessTokenRoleResult) Extract() (*AccessTokenRole, error) { + var s struct { + AccessTokenRole *AccessTokenRole `json:"role"` + } + err := r.ExtractInto(&s) + return s.AccessTokenRole, err +} + +// OAuth1 is an OAuth1 object, returned in OAuth1 token result. +type OAuth1 struct { + AccessTokenID string `json:"access_token_id"` + ConsumerID string `json:"consumer_id"` +} + +// TokenExt represents an extension of the base token result. +type TokenExt struct { + OAuth1 OAuth1 `json:"OS-OAUTH1"` +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/oauth1/urls.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/oauth1/urls.go new file mode 100644 index 00000000..9b51d53b --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/oauth1/urls.go @@ -0,0 +1,43 @@ +package oauth1 + +import "github.com/gophercloud/gophercloud" + +func consumersURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("OS-OAUTH1", "consumers") +} + +func consumerURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("OS-OAUTH1", "consumers", id) +} + +func requestTokenURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("OS-OAUTH1", "request_token") +} + +func authorizeTokenURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("OS-OAUTH1", "authorize", id) +} + +func createAccessTokenURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("OS-OAUTH1", "access_token") +} + +func userAccessTokensURL(c *gophercloud.ServiceClient, userID string) string { + return c.ServiceURL("users", userID, "OS-OAUTH1", "access_tokens") +} + +func userAccessTokenURL(c *gophercloud.ServiceClient, userID string, id string) string { + return c.ServiceURL("users", userID, "OS-OAUTH1", "access_tokens", id) +} + +func userAccessTokenRolesURL(c *gophercloud.ServiceClient, userID string, id string) string { + return c.ServiceURL("users", userID, "OS-OAUTH1", "access_tokens", id, "roles") +} + +func userAccessTokenRoleURL(c *gophercloud.ServiceClient, userID string, id string, roleID string) string { + return c.ServiceURL("users", userID, "OS-OAUTH1", "access_tokens", id, "roles", roleID) +} + +func authURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("auth", "tokens") +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/tokens/doc.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/tokens/doc.go new file mode 100644 index 00000000..de74c82e --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/tokens/doc.go @@ -0,0 +1,107 @@ +/* +Package tokens provides information and interaction with the token API +resource for the OpenStack Identity service. + +For more information, see: +http://developer.openstack.org/api-ref-identity-v3.html#tokens-v3 + +Example to Create a Token From a Username and Password + + authOptions := tokens.AuthOptions{ + UserID: "username", + Password: "password", + } + + token, err := tokens.Create(identityClient, authOptions).ExtractToken() + if err != nil { + panic(err) + } + +Example to Create a Token From a Username, Password, and Domain + + authOptions := tokens.AuthOptions{ + UserID: "username", + Password: "password", + DomainID: "default", + } + + token, err := tokens.Create(identityClient, authOptions).ExtractToken() + if err != nil { + panic(err) + } + + authOptions = tokens.AuthOptions{ + UserID: "username", + Password: "password", + DomainName: "default", + } + + token, err = tokens.Create(identityClient, authOptions).ExtractToken() + if err != nil { + panic(err) + } + +Example to Create a Token From a Token + + authOptions := tokens.AuthOptions{ + TokenID: "token_id", + } + + token, err := tokens.Create(identityClient, authOptions).ExtractToken() + if err != nil { + panic(err) + } + +Example to Create a Token from a Username and Password with Project ID Scope + + scope := tokens.Scope{ + ProjectID: "0fe36e73809d46aeae6705c39077b1b3", + } + + authOptions := tokens.AuthOptions{ + Scope: &scope, + UserID: "username", + Password: "password", + } + + token, err = tokens.Create(identityClient, authOptions).ExtractToken() + if err != nil { + panic(err) + } + +Example to Create a Token from a Username and Password with Domain ID Scope + + scope := tokens.Scope{ + DomainID: "default", + } + + authOptions := tokens.AuthOptions{ + Scope: &scope, + UserID: "username", + Password: "password", + } + + token, err = tokens.Create(identityClient, authOptions).ExtractToken() + if err != nil { + panic(err) + } + +Example to Create a Token from a Username and Password with Project Name Scope + + scope := tokens.Scope{ + ProjectName: "project_name", + DomainID: "default", + } + + authOptions := tokens.AuthOptions{ + Scope: &scope, + UserID: "username", + Password: "password", + } + + token, err = tokens.Create(identityClient, authOptions).ExtractToken() + if err != nil { + panic(err) + } +*/ +package tokens diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/tokens/requests.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/tokens/requests.go new file mode 100644 index 00000000..1af55d81 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/tokens/requests.go @@ -0,0 +1,174 @@ +package tokens + +import "github.com/gophercloud/gophercloud" + +// Scope allows a created token to be limited to a specific domain or project. +type Scope struct { + ProjectID string + ProjectName string + DomainID string + DomainName string + System bool +} + +// AuthOptionsBuilder provides the ability for extensions to add additional +// parameters to AuthOptions. Extensions must satisfy all required methods. +type AuthOptionsBuilder interface { + // ToTokenV3CreateMap assembles the Create request body, returning an error + // if parameters are missing or inconsistent. + ToTokenV3CreateMap(map[string]interface{}) (map[string]interface{}, error) + ToTokenV3HeadersMap(map[string]interface{}) (map[string]string, error) + ToTokenV3ScopeMap() (map[string]interface{}, error) + CanReauth() bool +} + +// AuthOptions represents options for authenticating a user. +type AuthOptions struct { + // IdentityEndpoint specifies the HTTP endpoint that is required to work with + // the Identity API of the appropriate version. While it's ultimately needed + // by all of the identity services, it will often be populated by a + // provider-level function. + IdentityEndpoint string `json:"-"` + + // Username is required if using Identity V2 API. Consult with your provider's + // control panel to discover your account's username. In Identity V3, either + // UserID or a combination of Username and DomainID or DomainName are needed. + Username string `json:"username,omitempty"` + UserID string `json:"id,omitempty"` + + Password string `json:"password,omitempty"` + + // Passcode is used in TOTP authentication method + Passcode string `json:"passcode,omitempty"` + + // At most one of DomainID and DomainName must be provided if using Username + // with Identity V3. Otherwise, either are optional. + DomainID string `json:"-"` + DomainName string `json:"name,omitempty"` + + // AllowReauth should be set to true if you grant permission for Gophercloud + // to cache your credentials in memory, and to allow Gophercloud to attempt + // to re-authenticate automatically if/when your token expires. If you set + // it to false, it will not cache these settings, but re-authentication will + // not be possible. This setting defaults to false. + AllowReauth bool `json:"-"` + + // TokenID allows users to authenticate (possibly as another user) with an + // authentication token ID. + TokenID string `json:"-"` + + // Authentication through Application Credentials requires supplying name, project and secret + // For project we can use TenantID + ApplicationCredentialID string `json:"-"` + ApplicationCredentialName string `json:"-"` + ApplicationCredentialSecret string `json:"-"` + + Scope Scope `json:"-"` +} + +// ToTokenV3CreateMap builds a request body from AuthOptions. +func (opts *AuthOptions) ToTokenV3CreateMap(scope map[string]interface{}) (map[string]interface{}, error) { + gophercloudAuthOpts := gophercloud.AuthOptions{ + Username: opts.Username, + UserID: opts.UserID, + Password: opts.Password, + Passcode: opts.Passcode, + DomainID: opts.DomainID, + DomainName: opts.DomainName, + AllowReauth: opts.AllowReauth, + TokenID: opts.TokenID, + ApplicationCredentialID: opts.ApplicationCredentialID, + ApplicationCredentialName: opts.ApplicationCredentialName, + ApplicationCredentialSecret: opts.ApplicationCredentialSecret, + } + + return gophercloudAuthOpts.ToTokenV3CreateMap(scope) +} + +// ToTokenV3ScopeMap builds a scope request body from AuthOptions. +func (opts *AuthOptions) ToTokenV3ScopeMap() (map[string]interface{}, error) { + scope := gophercloud.AuthScope(opts.Scope) + + gophercloudAuthOpts := gophercloud.AuthOptions{ + Scope: &scope, + DomainID: opts.DomainID, + DomainName: opts.DomainName, + } + + return gophercloudAuthOpts.ToTokenV3ScopeMap() +} + +func (opts *AuthOptions) CanReauth() bool { + if opts.Passcode != "" { + // cannot reauth using TOTP passcode + return false + } + + return opts.AllowReauth +} + +// ToTokenV3HeadersMap allows AuthOptions to satisfy the AuthOptionsBuilder +// interface in the v3 tokens package. +func (opts *AuthOptions) ToTokenV3HeadersMap(map[string]interface{}) (map[string]string, error) { + return nil, nil +} + +func subjectTokenHeaders(subjectToken string) map[string]string { + return map[string]string{ + "X-Subject-Token": subjectToken, + } +} + +// Create authenticates and either generates a new token, or changes the Scope +// of an existing token. +func Create(c *gophercloud.ServiceClient, opts AuthOptionsBuilder) (r CreateResult) { + scope, err := opts.ToTokenV3ScopeMap() + if err != nil { + r.Err = err + return + } + + b, err := opts.ToTokenV3CreateMap(scope) + if err != nil { + r.Err = err + return + } + + resp, err := c.Post(tokenURL(c), b, &r.Body, &gophercloud.RequestOpts{ + OmitHeaders: []string{"X-Auth-Token"}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Get validates and retrieves information about another token. +func Get(c *gophercloud.ServiceClient, token string) (r GetResult) { + resp, err := c.Get(tokenURL(c), &r.Body, &gophercloud.RequestOpts{ + MoreHeaders: subjectTokenHeaders(token), + OkCodes: []int{200, 203}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Validate determines if a specified token is valid or not. +func Validate(c *gophercloud.ServiceClient, token string) (bool, error) { + resp, err := c.Head(tokenURL(c), &gophercloud.RequestOpts{ + MoreHeaders: subjectTokenHeaders(token), + OkCodes: []int{200, 204, 404}, + }) + if err != nil { + return false, err + } + + return resp.StatusCode == 200 || resp.StatusCode == 204, nil +} + +// Revoke immediately makes specified token invalid. +func Revoke(c *gophercloud.ServiceClient, token string) (r RevokeResult) { + resp, err := c.Delete(tokenURL(c), &gophercloud.RequestOpts{ + MoreHeaders: subjectTokenHeaders(token), + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/tokens/results.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/tokens/results.go new file mode 100644 index 00000000..f1e17e9f --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/tokens/results.go @@ -0,0 +1,194 @@ +package tokens + +import ( + "time" + + "github.com/gophercloud/gophercloud" +) + +// Endpoint represents a single API endpoint offered by a service. +// It matches either a public, internal or admin URL. +// If supported, it contains a region specifier, again if provided. +// The significance of the Region field will depend upon your provider. +type Endpoint struct { + ID string `json:"id"` + Region string `json:"region"` + RegionID string `json:"region_id"` + Interface string `json:"interface"` + URL string `json:"url"` +} + +// CatalogEntry provides a type-safe interface to an Identity API V3 service +// catalog listing. Each class of service, such as cloud DNS or block storage +// services, could have multiple CatalogEntry representing it (one by interface +// type, e.g public, admin or internal). +// +// Note: when looking for the desired service, try, whenever possible, to key +// off the type field. Otherwise, you'll tie the representation of the service +// to a specific provider. +type CatalogEntry struct { + // Service ID + ID string `json:"id"` + + // Name will contain the provider-specified name for the service. + Name string `json:"name"` + + // Type will contain a type string if OpenStack defines a type for the + // service. Otherwise, for provider-specific services, the provider may + // assign their own type strings. + Type string `json:"type"` + + // Endpoints will let the caller iterate over all the different endpoints that + // may exist for the service. + Endpoints []Endpoint `json:"endpoints"` +} + +// ServiceCatalog provides a view into the service catalog from a previous, +// successful authentication. +type ServiceCatalog struct { + Entries []CatalogEntry `json:"catalog"` +} + +// Domain provides information about the domain to which this token grants +// access. +type Domain struct { + ID string `json:"id"` + Name string `json:"name"` +} + +// User represents a user resource that exists in the Identity Service. +type User struct { + Domain Domain `json:"domain"` + ID string `json:"id"` + Name string `json:"name"` +} + +// Role provides information about roles to which User is authorized. +type Role struct { + ID string `json:"id"` + Name string `json:"name"` +} + +// Project provides information about project to which User is authorized. +type Project struct { + Domain Domain `json:"domain"` + ID string `json:"id"` + Name string `json:"name"` +} + +// commonResult is the response from a request. A commonResult has various +// methods which can be used to extract different details about the result. +type commonResult struct { + gophercloud.Result +} + +// Extract is a shortcut for ExtractToken. +// This function is deprecated and still present for backward compatibility. +func (r commonResult) Extract() (*Token, error) { + return r.ExtractToken() +} + +// ExtractToken interprets a commonResult as a Token. +func (r commonResult) ExtractToken() (*Token, error) { + var s Token + err := r.ExtractInto(&s) + if err != nil { + return nil, err + } + + // Parse the token itself from the stored headers. + s.ID = r.Header.Get("X-Subject-Token") + + return &s, err +} + +// ExtractTokenID implements the gophercloud.AuthResult interface. The returned +// string is the same as the ID field of the Token struct returned from +// ExtractToken(). +func (r CreateResult) ExtractTokenID() (string, error) { + return r.Header.Get("X-Subject-Token"), r.Err +} + +// ExtractTokenID implements the gophercloud.AuthResult interface. The returned +// string is the same as the ID field of the Token struct returned from +// ExtractToken(). +func (r GetResult) ExtractTokenID() (string, error) { + return r.Header.Get("X-Subject-Token"), r.Err +} + +// ExtractServiceCatalog returns the ServiceCatalog that was generated along +// with the user's Token. +func (r commonResult) ExtractServiceCatalog() (*ServiceCatalog, error) { + var s ServiceCatalog + err := r.ExtractInto(&s) + return &s, err +} + +// ExtractUser returns the User that is the owner of the Token. +func (r commonResult) ExtractUser() (*User, error) { + var s struct { + User *User `json:"user"` + } + err := r.ExtractInto(&s) + return s.User, err +} + +// ExtractRoles returns Roles to which User is authorized. +func (r commonResult) ExtractRoles() ([]Role, error) { + var s struct { + Roles []Role `json:"roles"` + } + err := r.ExtractInto(&s) + return s.Roles, err +} + +// ExtractProject returns Project to which User is authorized. +func (r commonResult) ExtractProject() (*Project, error) { + var s struct { + Project *Project `json:"project"` + } + err := r.ExtractInto(&s) + return s.Project, err +} + +// ExtractDomain returns Domain to which User is authorized. +func (r commonResult) ExtractDomain() (*Domain, error) { + var s struct { + Domain *Domain `json:"domain"` + } + err := r.ExtractInto(&s) + return s.Domain, err +} + +// CreateResult is the response from a Create request. Use ExtractToken() +// to interpret it as a Token, or ExtractServiceCatalog() to interpret it +// as a service catalog. +type CreateResult struct { + commonResult +} + +// GetResult is the response from a Get request. Use ExtractToken() +// to interpret it as a Token, or ExtractServiceCatalog() to interpret it +// as a service catalog. +type GetResult struct { + commonResult +} + +// RevokeResult is response from a Revoke request. +type RevokeResult struct { + commonResult +} + +// Token is a string that grants a user access to a controlled set of services +// in an OpenStack provider. Each Token is valid for a set length of time. +type Token struct { + // ID is the issued token. + ID string `json:"id"` + + // ExpiresAt is the timestamp at which this token will no longer be accepted. + ExpiresAt time.Time `json:"expires_at"` +} + +func (r commonResult) ExtractInto(v interface{}) error { + return r.ExtractIntoStructPtr(v, "token") +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/tokens/urls.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/tokens/urls.go new file mode 100644 index 00000000..2f864a31 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/tokens/urls.go @@ -0,0 +1,7 @@ +package tokens + +import "github.com/gophercloud/gophercloud" + +func tokenURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("auth", "tokens") +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/doc.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/doc.go new file mode 100644 index 00000000..14da9ac9 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/doc.go @@ -0,0 +1,60 @@ +/* +Package images enables management and retrieval of images from the OpenStack +Image Service. + +Example to List Images + + images.ListOpts{ + Owner: "a7509e1ae65945fda83f3e52c6296017", + } + + allPages, err := images.List(imagesClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allImages, err := images.ExtractImages(allPages) + if err != nil { + panic(err) + } + + for _, image := range allImages { + fmt.Printf("%+v\n", image) + } + +Example to Create an Image + + createOpts := images.CreateOpts{ + Name: "image_name", + Visibility: images.ImageVisibilityPrivate, + } + + image, err := images.Create(imageClient, createOpts) + if err != nil { + panic(err) + } + +Example to Update an Image + + imageID := "1bea47ed-f6a9-463b-b423-14b9cca9ad27" + + updateOpts := images.UpdateOpts{ + images.ReplaceImageName{ + NewName: "new_name", + }, + } + + image, err := images.Update(imageClient, imageID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete an Image + + imageID := "1bea47ed-f6a9-463b-b423-14b9cca9ad27" + err := images.Delete(imageClient, imageID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package images diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/requests.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/requests.go new file mode 100644 index 00000000..2ab609cb --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/requests.go @@ -0,0 +1,418 @@ +package images + +import ( + "fmt" + "net/url" + "time" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToImageListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the server attributes you want to see returned. Marker and Limit are used +// for pagination. +// +// http://developer.openstack.org/api-ref-image-v2.html +type ListOpts struct { + // ID is the ID of the image. + // Multiple IDs can be specified by constructing a string + // such as "in:uuid1,uuid2,uuid3". + ID string `q:"id"` + + // Integer value for the limit of values to return. + Limit int `q:"limit"` + + // UUID of the server at which you want to set a marker. + Marker string `q:"marker"` + + // Name filters on the name of the image. + // Multiple names can be specified by constructing a string + // such as "in:name1,name2,name3". + Name string `q:"name"` + + // Visibility filters on the visibility of the image. + Visibility ImageVisibility `q:"visibility"` + + // Hidden filters on the hidden status of the image. + Hidden bool `q:"os_hidden"` + + // MemberStatus filters on the member status of the image. + MemberStatus ImageMemberStatus `q:"member_status"` + + // Owner filters on the project ID of the image. + Owner string `q:"owner"` + + // Status filters on the status of the image. + // Multiple statuses can be specified by constructing a string + // such as "in:saving,queued". + Status ImageStatus `q:"status"` + + // SizeMin filters on the size_min image property. + SizeMin int64 `q:"size_min"` + + // SizeMax filters on the size_max image property. + SizeMax int64 `q:"size_max"` + + // Sort sorts the results using the new style of sorting. See the OpenStack + // Image API reference for the exact syntax. + // + // Sort cannot be used with the classic sort options (sort_key and sort_dir). + Sort string `q:"sort"` + + // SortKey will sort the results based on a specified image property. + SortKey string `q:"sort_key"` + + // SortDir will sort the list results either ascending or decending. + SortDir string `q:"sort_dir"` + + // Tags filters on specific image tags. + Tags []string `q:"tag"` + + // CreatedAtQuery filters images based on their creation date. + CreatedAtQuery *ImageDateQuery + + // UpdatedAtQuery filters images based on their updated date. + UpdatedAtQuery *ImageDateQuery + + // ContainerFormat filters images based on the container_format. + // Multiple container formats can be specified by constructing a + // string such as "in:bare,ami". + ContainerFormat string `q:"container_format"` + + // DiskFormat filters images based on the disk_format. + // Multiple disk formats can be specified by constructing a string + // such as "in:qcow2,iso". + DiskFormat string `q:"disk_format"` +} + +// ToImageListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToImageListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + params := q.Query() + + if opts.CreatedAtQuery != nil { + createdAt := opts.CreatedAtQuery.Date.Format(time.RFC3339) + if v := opts.CreatedAtQuery.Filter; v != "" { + createdAt = fmt.Sprintf("%s:%s", v, createdAt) + } + + params.Add("created_at", createdAt) + } + + if opts.UpdatedAtQuery != nil { + updatedAt := opts.UpdatedAtQuery.Date.Format(time.RFC3339) + if v := opts.UpdatedAtQuery.Filter; v != "" { + updatedAt = fmt.Sprintf("%s:%s", v, updatedAt) + } + + params.Add("updated_at", updatedAt) + } + + q = &url.URL{RawQuery: params.Encode()} + + return q.String(), err +} + +// List implements image list request. +func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(c) + if opts != nil { + query, err := opts.ToImageListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + imagePage := ImagePage{ + serviceURL: c.ServiceURL(), + LinkedPageBase: pagination.LinkedPageBase{PageResult: r}, + } + + return imagePage + }) +} + +// CreateOptsBuilder allows extensions to add parameters to the Create request. +type CreateOptsBuilder interface { + // Returns value that can be passed to json.Marshal + ToImageCreateMap() (map[string]interface{}, error) +} + +// CreateOpts represents options used to create an image. +type CreateOpts struct { + // Name is the name of the new image. + Name string `json:"name" required:"true"` + + // Id is the the image ID. + ID string `json:"id,omitempty"` + + // Visibility defines who can see/use the image. + Visibility *ImageVisibility `json:"visibility,omitempty"` + + // Hidden is whether the image is listed in default image list or not. + Hidden *bool `json:"os_hidden,omitempty"` + + // Tags is a set of image tags. + Tags []string `json:"tags,omitempty"` + + // ContainerFormat is the format of the + // container. Valid values are ami, ari, aki, bare, and ovf. + ContainerFormat string `json:"container_format,omitempty"` + + // DiskFormat is the format of the disk. If set, + // valid values are ami, ari, aki, vhd, vmdk, raw, qcow2, vdi, + // and iso. + DiskFormat string `json:"disk_format,omitempty"` + + // MinDisk is the amount of disk space in + // GB that is required to boot the image. + MinDisk int `json:"min_disk,omitempty"` + + // MinRAM is the amount of RAM in MB that + // is required to boot the image. + MinRAM int `json:"min_ram,omitempty"` + + // protected is whether the image is not deletable. + Protected *bool `json:"protected,omitempty"` + + // properties is a set of properties, if any, that + // are associated with the image. + Properties map[string]string `json:"-"` +} + +// ToImageCreateMap assembles a request body based on the contents of +// a CreateOpts. +func (opts CreateOpts) ToImageCreateMap() (map[string]interface{}, error) { + b, err := gophercloud.BuildRequestBody(opts, "") + if err != nil { + return nil, err + } + + if opts.Properties != nil { + for k, v := range opts.Properties { + b[k] = v + } + } + return b, nil +} + +// Create implements create image request. +func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToImageCreateMap() + if err != nil { + r.Err = err + return r + } + resp, err := client.Post(createURL(client), b, &r.Body, &gophercloud.RequestOpts{OkCodes: []int{201}}) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Delete implements image delete request. +func Delete(client *gophercloud.ServiceClient, id string) (r DeleteResult) { + resp, err := client.Delete(deleteURL(client, id), nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Get implements image get request. +func Get(client *gophercloud.ServiceClient, id string) (r GetResult) { + resp, err := client.Get(getURL(client, id), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Update implements image updated request. +func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToImageUpdateMap() + if err != nil { + r.Err = err + return r + } + resp, err := client.Patch(updateURL(client, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + MoreHeaders: map[string]string{"Content-Type": "application/openstack-images-v2.1-json-patch"}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the +// Update request. +type UpdateOptsBuilder interface { + // returns value implementing json.Marshaler which when marshaled matches + // the patch schema: + // http://specs.openstack.org/openstack/glance-specs/specs/api/v2/http-patch-image-api-v2.html + ToImageUpdateMap() ([]interface{}, error) +} + +// UpdateOpts implements UpdateOpts +type UpdateOpts []Patch + +// ToImageUpdateMap assembles a request body based on the contents of +// UpdateOpts. +func (opts UpdateOpts) ToImageUpdateMap() ([]interface{}, error) { + m := make([]interface{}, len(opts)) + for i, patch := range opts { + patchJSON := patch.ToImagePatchMap() + m[i] = patchJSON + } + return m, nil +} + +// Patch represents a single update to an existing image. Multiple updates +// to an image can be submitted at the same time. +type Patch interface { + ToImagePatchMap() map[string]interface{} +} + +// UpdateVisibility represents an updated visibility property request. +type UpdateVisibility struct { + Visibility ImageVisibility +} + +// ToImagePatchMap assembles a request body based on UpdateVisibility. +func (r UpdateVisibility) ToImagePatchMap() map[string]interface{} { + return map[string]interface{}{ + "op": "replace", + "path": "/visibility", + "value": r.Visibility, + } +} + +// ReplaceImageHidden represents an updated os_hidden property request. +type ReplaceImageHidden struct { + NewHidden bool +} + +// ToImagePatchMap assembles a request body based on ReplaceImageHidden. +func (r ReplaceImageHidden) ToImagePatchMap() map[string]interface{} { + return map[string]interface{}{ + "op": "replace", + "path": "/os_hidden", + "value": r.NewHidden, + } +} + +// ReplaceImageName represents an updated image_name property request. +type ReplaceImageName struct { + NewName string +} + +// ToImagePatchMap assembles a request body based on ReplaceImageName. +func (r ReplaceImageName) ToImagePatchMap() map[string]interface{} { + return map[string]interface{}{ + "op": "replace", + "path": "/name", + "value": r.NewName, + } +} + +// ReplaceImageChecksum represents an updated checksum property request. +type ReplaceImageChecksum struct { + Checksum string +} + +// ReplaceImageChecksum assembles a request body based on ReplaceImageChecksum. +func (r ReplaceImageChecksum) ToImagePatchMap() map[string]interface{} { + return map[string]interface{}{ + "op": "replace", + "path": "/checksum", + "value": r.Checksum, + } +} + +// ReplaceImageTags represents an updated tags property request. +type ReplaceImageTags struct { + NewTags []string +} + +// ToImagePatchMap assembles a request body based on ReplaceImageTags. +func (r ReplaceImageTags) ToImagePatchMap() map[string]interface{} { + return map[string]interface{}{ + "op": "replace", + "path": "/tags", + "value": r.NewTags, + } +} + +// ReplaceImageMinDisk represents an updated min_disk property request. +type ReplaceImageMinDisk struct { + NewMinDisk int +} + +// ToImagePatchMap assembles a request body based on ReplaceImageTags. +func (r ReplaceImageMinDisk) ToImagePatchMap() map[string]interface{} { + return map[string]interface{}{ + "op": "replace", + "path": "/min_disk", + "value": r.NewMinDisk, + } +} + +// ReplaceImageMinRam represents an updated min_ram property request. +type ReplaceImageMinRam struct { + NewMinRam int +} + +// ToImagePatchMap assembles a request body based on ReplaceImageTags. +func (r ReplaceImageMinRam) ToImagePatchMap() map[string]interface{} { + return map[string]interface{}{ + "op": "replace", + "path": "/min_ram", + "value": r.NewMinRam, + } +} + +// ReplaceImageProtected represents an updated protected property request. +type ReplaceImageProtected struct { + NewProtected bool +} + +// ToImagePatchMap assembles a request body based on ReplaceImageProtected +func (r ReplaceImageProtected) ToImagePatchMap() map[string]interface{} { + return map[string]interface{}{ + "op": "replace", + "path": "/protected", + "value": r.NewProtected, + } +} + +// UpdateOp represents a valid update operation. +type UpdateOp string + +const ( + AddOp UpdateOp = "add" + ReplaceOp UpdateOp = "replace" + RemoveOp UpdateOp = "remove" +) + +// UpdateImageProperty represents an update property request. +type UpdateImageProperty struct { + Op UpdateOp + Name string + Value string +} + +// ToImagePatchMap assembles a request body based on UpdateImageProperty. +func (r UpdateImageProperty) ToImagePatchMap() map[string]interface{} { + updateMap := map[string]interface{}{ + "op": r.Op, + "path": fmt.Sprintf("/%s", r.Name), + } + + if r.Op != RemoveOp { + updateMap["value"] = r.Value + } + + return updateMap +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/results.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/results.go new file mode 100644 index 00000000..96fd91a2 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/results.go @@ -0,0 +1,246 @@ +package images + +import ( + "encoding/json" + "fmt" + "reflect" + "strings" + "time" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// Image represents an image found in the OpenStack Image service. +type Image struct { + // ID is the image UUID. + ID string `json:"id"` + + // Name is the human-readable display name for the image. + Name string `json:"name"` + + // Status is the image status. It can be "queued" or "active" + // See imageservice/v2/images/type.go + Status ImageStatus `json:"status"` + + // Tags is a list of image tags. Tags are arbitrarily defined strings + // attached to an image. + Tags []string `json:"tags"` + + // ContainerFormat is the format of the container. + // Valid values are ami, ari, aki, bare, and ovf. + ContainerFormat string `json:"container_format"` + + // DiskFormat is the format of the disk. + // If set, valid values are ami, ari, aki, vhd, vmdk, raw, qcow2, vdi, + // and iso. + DiskFormat string `json:"disk_format"` + + // MinDiskGigabytes is the amount of disk space in GB that is required to + // boot the image. + MinDiskGigabytes int `json:"min_disk"` + + // MinRAMMegabytes [optional] is the amount of RAM in MB that is required to + // boot the image. + MinRAMMegabytes int `json:"min_ram"` + + // Owner is the tenant ID the image belongs to. + Owner string `json:"owner"` + + // Protected is whether the image is deletable or not. + Protected bool `json:"protected"` + + // Visibility defines who can see/use the image. + Visibility ImageVisibility `json:"visibility"` + + // Hidden is whether the image is listed in default image list or not. + Hidden bool `json:"os_hidden"` + + // Checksum is the checksum of the data that's associated with the image. + Checksum string `json:"checksum"` + + // SizeBytes is the size of the data that's associated with the image. + SizeBytes int64 `json:"-"` + + // Metadata is a set of metadata associated with the image. + // Image metadata allow for meaningfully define the image properties + // and tags. + // See http://docs.openstack.org/developer/glance/metadefs-concepts.html. + Metadata map[string]string `json:"metadata"` + + // Properties is a set of key-value pairs, if any, that are associated with + // the image. + Properties map[string]interface{} + + // CreatedAt is the date when the image has been created. + CreatedAt time.Time `json:"created_at"` + + // UpdatedAt is the date when the last change has been made to the image or + // its properties. + UpdatedAt time.Time `json:"updated_at"` + + // File is the trailing path after the glance endpoint that represent the + // location of the image or the path to retrieve it. + File string `json:"file"` + + // Schema is the path to the JSON-schema that represent the image or image + // entity. + Schema string `json:"schema"` + + // VirtualSize is the virtual size of the image + VirtualSize int64 `json:"virtual_size"` + + // OpenStackImageImportMethods is a slice listing the types of import + // methods available in the cloud. + OpenStackImageImportMethods []string `json:"-"` + // OpenStackImageStoreIDs is a slice listing the store IDs available in + // the cloud. + OpenStackImageStoreIDs []string `json:"-"` +} + +func (r *Image) UnmarshalJSON(b []byte) error { + type tmp Image + var s struct { + tmp + SizeBytes interface{} `json:"size"` + OpenStackImageImportMethods string `json:"openstack-image-import-methods"` + OpenStackImageStoreIDs string `json:"openstack-image-store-ids"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = Image(s.tmp) + + switch t := s.SizeBytes.(type) { + case nil: + r.SizeBytes = 0 + case float32: + r.SizeBytes = int64(t) + case float64: + r.SizeBytes = int64(t) + default: + return fmt.Errorf("Unknown type for SizeBytes: %v (value: %v)", reflect.TypeOf(t), t) + } + + // Bundle all other fields into Properties + var result interface{} + err = json.Unmarshal(b, &result) + if err != nil { + return err + } + if resultMap, ok := result.(map[string]interface{}); ok { + delete(resultMap, "self") + delete(resultMap, "size") + delete(resultMap, "openstack-image-import-methods") + delete(resultMap, "openstack-image-store-ids") + r.Properties = gophercloud.RemainingKeys(Image{}, resultMap) + } + + if v := strings.FieldsFunc(strings.TrimSpace(s.OpenStackImageImportMethods), splitFunc); len(v) > 0 { + r.OpenStackImageImportMethods = v + } + if v := strings.FieldsFunc(strings.TrimSpace(s.OpenStackImageStoreIDs), splitFunc); len(v) > 0 { + r.OpenStackImageStoreIDs = v + } + + return err +} + +type commonResult struct { + gophercloud.Result +} + +// Extract interprets any commonResult as an Image. +func (r commonResult) Extract() (*Image, error) { + var s *Image + if v, ok := r.Body.(map[string]interface{}); ok { + for k, h := range r.Header { + if strings.ToLower(k) == "openstack-image-import-methods" { + for _, s := range h { + v["openstack-image-import-methods"] = s + } + } + if strings.ToLower(k) == "openstack-image-store-ids" { + for _, s := range h { + v["openstack-image-store-ids"] = s + } + } + } + } + err := r.ExtractInto(&s) + return s, err +} + +// CreateResult represents the result of a Create operation. Call its Extract +// method to interpret it as an Image. +type CreateResult struct { + commonResult +} + +// UpdateResult represents the result of an Update operation. Call its Extract +// method to interpret it as an Image. +type UpdateResult struct { + commonResult +} + +// GetResult represents the result of a Get operation. Call its Extract +// method to interpret it as an Image. +type GetResult struct { + commonResult +} + +// DeleteResult represents the result of a Delete operation. Call its +// ExtractErr method to interpret it as an Image. +type DeleteResult struct { + gophercloud.ErrResult +} + +// ImagePage represents the results of a List request. +type ImagePage struct { + serviceURL string + pagination.LinkedPageBase +} + +// IsEmpty returns true if an ImagePage contains no Images results. +func (r ImagePage) IsEmpty() (bool, error) { + if r.StatusCode == 204 { + return true, nil + } + + images, err := ExtractImages(r) + return len(images) == 0, err +} + +// NextPageURL uses the response's embedded link reference to navigate to +// the next page of results. +func (r ImagePage) NextPageURL() (string, error) { + var s struct { + Next string `json:"next"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + + if s.Next == "" { + return "", nil + } + + return nextPageURL(r.serviceURL, s.Next) +} + +// ExtractImages interprets the results of a single page from a List() call, +// producing a slice of Image entities. +func ExtractImages(r pagination.Page) ([]Image, error) { + var s struct { + Images []Image `json:"images"` + } + err := (r.(ImagePage)).ExtractInto(&s) + return s.Images, err +} + +// splitFunc is a helper function used to avoid a slice of empty strings. +func splitFunc(c rune) bool { + return c == ',' +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/types.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/types.go new file mode 100644 index 00000000..147be199 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/types.go @@ -0,0 +1,108 @@ +package images + +import ( + "time" +) + +// ImageStatus image statuses +// http://docs.openstack.org/developer/glance/statuses.html +type ImageStatus string + +const ( + // ImageStatusQueued is a status for an image which identifier has + // been reserved for an image in the image registry. + ImageStatusQueued ImageStatus = "queued" + + // ImageStatusSaving denotes that an image’s raw data is currently being + // uploaded to Glance + ImageStatusSaving ImageStatus = "saving" + + // ImageStatusActive denotes an image that is fully available in Glance. + ImageStatusActive ImageStatus = "active" + + // ImageStatusKilled denotes that an error occurred during the uploading + // of an image’s data, and that the image is not readable. + ImageStatusKilled ImageStatus = "killed" + + // ImageStatusDeleted is used for an image that is no longer available to use. + // The image information is retained in the image registry. + ImageStatusDeleted ImageStatus = "deleted" + + // ImageStatusPendingDelete is similar to Delete, but the image is not yet + // deleted. + ImageStatusPendingDelete ImageStatus = "pending_delete" + + // ImageStatusDeactivated denotes that access to image data is not allowed to + // any non-admin user. + ImageStatusDeactivated ImageStatus = "deactivated" + + // ImageStatusImporting denotes that an import call has been made but that + // the image is not yet ready for use. + ImageStatusImporting ImageStatus = "importing" +) + +// ImageVisibility denotes an image that is fully available in Glance. +// This occurs when the image data is uploaded, or the image size is explicitly +// set to zero on creation. +// According to design +// https://wiki.openstack.org/wiki/Glance-v2-community-image-visibility-design +type ImageVisibility string + +const ( + // ImageVisibilityPublic all users + ImageVisibilityPublic ImageVisibility = "public" + + // ImageVisibilityPrivate users with tenantId == tenantId(owner) + ImageVisibilityPrivate ImageVisibility = "private" + + // ImageVisibilityShared images are visible to: + // - users with tenantId == tenantId(owner) + // - users with tenantId in the member-list of the image + // - users with tenantId in the member-list with member_status == 'accepted' + ImageVisibilityShared ImageVisibility = "shared" + + // ImageVisibilityCommunity images: + // - all users can see and boot it + // - users with tenantId in the member-list of the image with + // member_status == 'accepted' have this image in their default image-list. + ImageVisibilityCommunity ImageVisibility = "community" +) + +// MemberStatus is a status for adding a new member (tenant) to an image +// member list. +type ImageMemberStatus string + +const ( + // ImageMemberStatusAccepted is the status for an accepted image member. + ImageMemberStatusAccepted ImageMemberStatus = "accepted" + + // ImageMemberStatusPending shows that the member addition is pending + ImageMemberStatusPending ImageMemberStatus = "pending" + + // ImageMemberStatusAccepted is the status for a rejected image member + ImageMemberStatusRejected ImageMemberStatus = "rejected" + + // ImageMemberStatusAll + ImageMemberStatusAll ImageMemberStatus = "all" +) + +// ImageDateFilter represents a valid filter to use for filtering +// images by their date during a List. +type ImageDateFilter string + +const ( + FilterGT ImageDateFilter = "gt" + FilterGTE ImageDateFilter = "gte" + FilterLT ImageDateFilter = "lt" + FilterLTE ImageDateFilter = "lte" + FilterNEQ ImageDateFilter = "neq" + FilterEQ ImageDateFilter = "eq" +) + +// ImageDateQuery represents a date field to be used for listing images. +// If no filter is specified, the query will act as though FilterEQ was +// set. +type ImageDateQuery struct { + Date time.Time + Filter ImageDateFilter +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/urls.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/urls.go new file mode 100644 index 00000000..1780c3c6 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/urls.go @@ -0,0 +1,65 @@ +package images + +import ( + "net/url" + "strings" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/utils" +) + +// `listURL` is a pure function. `listURL(c)` is a URL for which a GET +// request will respond with a list of images in the service `c`. +func listURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("images") +} + +func createURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("images") +} + +// `imageURL(c,i)` is the URL for the image identified by ID `i` in +// the service `c`. +func imageURL(c *gophercloud.ServiceClient, imageID string) string { + return c.ServiceURL("images", imageID) +} + +// `getURL(c,i)` is a URL for which a GET request will respond with +// information about the image identified by ID `i` in the service +// `c`. +func getURL(c *gophercloud.ServiceClient, imageID string) string { + return imageURL(c, imageID) +} + +func updateURL(c *gophercloud.ServiceClient, imageID string) string { + return imageURL(c, imageID) +} + +func deleteURL(c *gophercloud.ServiceClient, imageID string) string { + return imageURL(c, imageID) +} + +// builds next page full url based on current url +func nextPageURL(serviceURL, requestedNext string) (string, error) { + base, err := utils.BaseEndpoint(serviceURL) + if err != nil { + return "", err + } + + requestedNextURL, err := url.Parse(requestedNext) + if err != nil { + return "", err + } + + base = gophercloud.NormalizeURL(base) + nextPath := base + strings.TrimPrefix(requestedNextURL.Path, "/") + + nextURL, err := url.Parse(nextPath) + if err != nil { + return "", err + } + + nextURL.RawQuery = requestedNextURL.RawQuery + + return nextURL.String(), nil +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/l7policies/doc.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/l7policies/doc.go new file mode 100644 index 00000000..81357990 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/l7policies/doc.go @@ -0,0 +1,123 @@ +/* +Package l7policies provides information and interaction with L7Policies and +Rules of the LBaaS v2 extension for the OpenStack Networking service. + +Example to Create a L7Policy + + createOpts := l7policies.CreateOpts{ + Name: "redirect-example.com", + ListenerID: "023f2e34-7806-443b-bfae-16c324569a3d", + Action: l7policies.ActionRedirectToURL, + RedirectURL: "http://www.example.com", + } + l7policy, err := l7policies.Create(lbClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to List L7Policies + + listOpts := l7policies.ListOpts{ + ListenerID: "c79a4468-d788-410c-bf79-9a8ef6354852", + } + allPages, err := l7policies.List(lbClient, listOpts).AllPages() + if err != nil { + panic(err) + } + allL7Policies, err := l7policies.ExtractL7Policies(allPages) + if err != nil { + panic(err) + } + for _, l7policy := range allL7Policies { + fmt.Printf("%+v\n", l7policy) + } + +Example to Get a L7Policy + + l7policy, err := l7policies.Get(lbClient, "023f2e34-7806-443b-bfae-16c324569a3d").Extract() + if err != nil { + panic(err) + } + +Example to Delete a L7Policy + + l7policyID := "d67d56a6-4a86-4688-a282-f46444705c64" + err := l7policies.Delete(lbClient, l7policyID).ExtractErr() + if err != nil { + panic(err) + } + +Example to Update a L7Policy + + l7policyID := "d67d56a6-4a86-4688-a282-f46444705c64" + name := "new-name" + updateOpts := l7policies.UpdateOpts{ + Name: &name, + } + l7policy, err := l7policies.Update(lbClient, l7policyID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Create a Rule + + l7policyID := "d67d56a6-4a86-4688-a282-f46444705c64" + createOpts := l7policies.CreateRuleOpts{ + RuleType: l7policies.TypePath, + CompareType: l7policies.CompareTypeRegex, + Value: "/images*", + } + rule, err := l7policies.CreateRule(lbClient, l7policyID, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to List L7 Rules + + l7policyID := "d67d56a6-4a86-4688-a282-f46444705c64" + listOpts := l7policies.ListRulesOpts{ + RuleType: l7policies.TypePath, + } + allPages, err := l7policies.ListRules(lbClient, l7policyID, listOpts).AllPages() + if err != nil { + panic(err) + } + allRules, err := l7policies.ExtractRules(allPages) + if err != nil { + panic(err) + } + for _, rule := allRules { + fmt.Printf("%+v\n", rule) + } + +Example to Get a l7 rule + + l7rule, err := l7policies.GetRule(lbClient, "023f2e34-7806-443b-bfae-16c324569a3d", "53ad8ab8-40fa-11e8-a508-00224d6b7bc1").Extract() + if err != nil { + panic(err) + } + +Example to Delete a l7 rule + + l7policyID := "d67d56a6-4a86-4688-a282-f46444705c64" + ruleID := "64dba99f-8af8-4200-8882-e32a0660f23e" + err := l7policies.DeleteRule(lbClient, l7policyID, ruleID).ExtractErr() + if err != nil { + panic(err) + } + +Example to Update a Rule + + l7policyID := "d67d56a6-4a86-4688-a282-f46444705c64" + ruleID := "64dba99f-8af8-4200-8882-e32a0660f23e" + updateOpts := l7policies.UpdateRuleOpts{ + RuleType: l7policies.TypePath, + CompareType: l7policies.CompareTypeRegex, + Value: "/images/special*", + } + rule, err := l7policies.UpdateRule(lbClient, l7policyID, ruleID, updateOpts).Extract() + if err != nil { + panic(err) + } +*/ +package l7policies diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/l7policies/requests.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/l7policies/requests.go new file mode 100644 index 00000000..2b84bcad --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/l7policies/requests.go @@ -0,0 +1,418 @@ +package l7policies + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToL7PolicyCreateMap() (map[string]interface{}, error) +} + +type Action string +type RuleType string +type CompareType string + +const ( + ActionRedirectPrefix Action = "REDIRECT_PREFIX" + ActionRedirectToPool Action = "REDIRECT_TO_POOL" + ActionRedirectToURL Action = "REDIRECT_TO_URL" + ActionReject Action = "REJECT" + + TypeCookie RuleType = "COOKIE" + TypeFileType RuleType = "FILE_TYPE" + TypeHeader RuleType = "HEADER" + TypeHostName RuleType = "HOST_NAME" + TypePath RuleType = "PATH" + + CompareTypeContains CompareType = "CONTAINS" + CompareTypeEndWith CompareType = "ENDS_WITH" + CompareTypeEqual CompareType = "EQUAL_TO" + CompareTypeRegex CompareType = "REGEX" + CompareTypeStartWith CompareType = "STARTS_WITH" +) + +// CreateOpts is the common options struct used in this package's Create +// operation. +type CreateOpts struct { + // Name of the L7 policy. + Name string `json:"name,omitempty"` + + // The ID of the listener. + ListenerID string `json:"listener_id,omitempty"` + + // The L7 policy action. One of REDIRECT_PREFIX, REDIRECT_TO_POOL, REDIRECT_TO_URL, or REJECT. + Action Action `json:"action" required:"true"` + + // The position of this policy on the listener. + Position int32 `json:"position,omitempty"` + + // A human-readable description for the resource. + Description string `json:"description,omitempty"` + + // ProjectID is the UUID of the project who owns the L7 policy in octavia. + // Only administrative users can specify a project UUID other than their own. + ProjectID string `json:"project_id,omitempty"` + + // Requests matching this policy will be redirected to this Prefix URL. + // Only valid if action is REDIRECT_PREFIX. + RedirectPrefix string `json:"redirect_prefix,omitempty"` + + // Requests matching this policy will be redirected to the pool with this ID. + // Only valid if action is REDIRECT_TO_POOL. + RedirectPoolID string `json:"redirect_pool_id,omitempty"` + + // Requests matching this policy will be redirected to this URL. + // Only valid if action is REDIRECT_TO_URL. + RedirectURL string `json:"redirect_url,omitempty"` + + // Requests matching this policy will be redirected to the specified URL or Prefix URL + // with the HTTP response code. Valid if action is REDIRECT_TO_URL or REDIRECT_PREFIX. + // Valid options are: 301, 302, 303, 307, or 308. Default is 302. Requires version 2.9 + RedirectHttpCode int32 `json:"redirect_http_code,omitempty"` + + // The administrative state of the Loadbalancer. A valid value is true (UP) + // or false (DOWN). + AdminStateUp *bool `json:"admin_state_up,omitempty"` + + // Rules is a slice of CreateRuleOpts which allows a set of rules + // to be created at the same time the policy is created. + // + // This is only possible to use when creating a fully populated + // Loadbalancer. + Rules []CreateRuleOpts `json:"rules,omitempty"` +} + +// ToL7PolicyCreateMap builds a request body from CreateOpts. +func (opts CreateOpts) ToL7PolicyCreateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "l7policy") +} + +// Create accepts a CreateOpts struct and uses the values to create a new l7policy. +func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToL7PolicyCreateMap() + if err != nil { + r.Err = err + return + } + resp, err := c.Post(rootURL(c), b, &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToL7PolicyListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. +type ListOpts struct { + Name string `q:"name"` + Description string `q:"description"` + ListenerID string `q:"listener_id"` + Action string `q:"action"` + ProjectID string `q:"project_id"` + RedirectPoolID string `q:"redirect_pool_id"` + RedirectURL string `q:"redirect_url"` + Position int32 `q:"position"` + AdminStateUp bool `q:"admin_state_up"` + ID string `q:"id"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` +} + +// ToL7PolicyListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToL7PolicyListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns a Pager which allows you to iterate over a collection of +// l7policies. It accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +// +// Default policy settings return only those l7policies that are owned by the +// project who submits the request, unless an admin user submits the request. +func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := rootURL(c) + if opts != nil { + query, err := opts.ToL7PolicyListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return L7PolicyPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get retrieves a particular l7policy based on its unique ID. +func Get(c *gophercloud.ServiceClient, id string) (r GetResult) { + resp, err := c.Get(resourceURL(c, id), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Delete will permanently delete a particular l7policy based on its unique ID. +func Delete(c *gophercloud.ServiceClient, id string) (r DeleteResult) { + resp, err := c.Delete(resourceURL(c, id), nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the +// Update request. +type UpdateOptsBuilder interface { + ToL7PolicyUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts is the common options struct used in this package's Update +// operation. +type UpdateOpts struct { + // Name of the L7 policy, empty string is allowed. + Name *string `json:"name,omitempty"` + + // The L7 policy action. One of REDIRECT_PREFIX, REDIRECT_TO_POOL, REDIRECT_TO_URL, or REJECT. + Action Action `json:"action,omitempty"` + + // The position of this policy on the listener. + Position int32 `json:"position,omitempty"` + + // A human-readable description for the resource, empty string is allowed. + Description *string `json:"description,omitempty"` + + // Requests matching this policy will be redirected to this Prefix URL. + // Only valid if action is REDIRECT_PREFIX. + RedirectPrefix *string `json:"redirect_prefix,omitempty"` + + // Requests matching this policy will be redirected to the pool with this ID. + // Only valid if action is REDIRECT_TO_POOL. + RedirectPoolID *string `json:"redirect_pool_id,omitempty"` + + // Requests matching this policy will be redirected to this URL. + // Only valid if action is REDIRECT_TO_URL. + RedirectURL *string `json:"redirect_url,omitempty"` + + // Requests matching this policy will be redirected to the specified URL or Prefix URL + // with the HTTP response code. Valid if action is REDIRECT_TO_URL or REDIRECT_PREFIX. + // Valid options are: 301, 302, 303, 307, or 308. Default is 302. Requires version 2.9 + RedirectHttpCode int32 `json:"redirect_http_code,omitempty"` + + // The administrative state of the Loadbalancer. A valid value is true (UP) + // or false (DOWN). + AdminStateUp *bool `json:"admin_state_up,omitempty"` +} + +// ToL7PolicyUpdateMap builds a request body from UpdateOpts. +func (opts UpdateOpts) ToL7PolicyUpdateMap() (map[string]interface{}, error) { + b, err := gophercloud.BuildRequestBody(opts, "l7policy") + if err != nil { + return nil, err + } + + m := b["l7policy"].(map[string]interface{}) + + if m["redirect_pool_id"] == "" { + m["redirect_pool_id"] = nil + } + + if m["redirect_url"] == "" { + m["redirect_url"] = nil + } + + if m["redirect_prefix"] == "" { + m["redirect_prefix"] = nil + } + + if m["redirect_http_code"] == 0 { + m["redirect_http_code"] = nil + } + + return b, nil +} + +// Update allows l7policy to be updated. +func Update(c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToL7PolicyUpdateMap() + if err != nil { + r.Err = err + return + } + resp, err := c.Put(resourceURL(c, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// CreateRuleOpts is the common options struct used in this package's CreateRule +// operation. +type CreateRuleOpts struct { + // The L7 rule type. One of COOKIE, FILE_TYPE, HEADER, HOST_NAME, or PATH. + RuleType RuleType `json:"type" required:"true"` + + // The comparison type for the L7 rule. One of CONTAINS, ENDS_WITH, EQUAL_TO, REGEX, or STARTS_WITH. + CompareType CompareType `json:"compare_type" required:"true"` + + // The value to use for the comparison. For example, the file type to compare. + Value string `json:"value" required:"true"` + + // ProjectID is the UUID of the project who owns the rule in octavia. + // Only administrative users can specify a project UUID other than their own. + ProjectID string `json:"project_id,omitempty"` + + // The key to use for the comparison. For example, the name of the cookie to evaluate. + Key string `json:"key,omitempty"` + + // When true the logic of the rule is inverted. For example, with invert true, + // equal to would become not equal to. Default is false. + Invert bool `json:"invert,omitempty"` + + // The administrative state of the Loadbalancer. A valid value is true (UP) + // or false (DOWN). + AdminStateUp *bool `json:"admin_state_up,omitempty"` +} + +// ToRuleCreateMap builds a request body from CreateRuleOpts. +func (opts CreateRuleOpts) ToRuleCreateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "rule") +} + +// CreateRule will create and associate a Rule with a particular L7Policy. +func CreateRule(c *gophercloud.ServiceClient, policyID string, opts CreateRuleOpts) (r CreateRuleResult) { + b, err := opts.ToRuleCreateMap() + if err != nil { + r.Err = err + return + } + resp, err := c.Post(ruleRootURL(c, policyID), b, &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// ListRulesOptsBuilder allows extensions to add additional parameters to the +// ListRules request. +type ListRulesOptsBuilder interface { + ToRulesListQuery() (string, error) +} + +// ListRulesOpts allows the filtering and sorting of paginated collections +// through the API. +type ListRulesOpts struct { + RuleType RuleType `q:"type"` + ProjectID string `q:"project_id"` + CompareType CompareType `q:"compare_type"` + Value string `q:"value"` + Key string `q:"key"` + Invert bool `q:"invert"` + AdminStateUp bool `q:"admin_state_up"` + ID string `q:"id"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` +} + +// ToRulesListQuery formats a ListOpts into a query string. +func (opts ListRulesOpts) ToRulesListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// ListRules returns a Pager which allows you to iterate over a collection of +// rules. It accepts a ListRulesOptsBuilder, which allows you to filter and +// sort the returned collection for greater efficiency. +// +// Default policy settings return only those rules that are owned by the +// project who submits the request, unless an admin user submits the request. +func ListRules(c *gophercloud.ServiceClient, policyID string, opts ListRulesOptsBuilder) pagination.Pager { + url := ruleRootURL(c, policyID) + if opts != nil { + query, err := opts.ToRulesListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return RulePage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// GetRule retrieves a particular L7Policy Rule based on its unique ID. +func GetRule(c *gophercloud.ServiceClient, policyID string, ruleID string) (r GetRuleResult) { + resp, err := c.Get(ruleResourceURL(c, policyID, ruleID), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// DeleteRule will remove a Rule from a particular L7Policy. +func DeleteRule(c *gophercloud.ServiceClient, policyID string, ruleID string) (r DeleteRuleResult) { + resp, err := c.Delete(ruleResourceURL(c, policyID, ruleID), nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// UpdateRuleOptsBuilder allows to add additional parameters to the PUT request. +type UpdateRuleOptsBuilder interface { + ToRuleUpdateMap() (map[string]interface{}, error) +} + +// UpdateRuleOpts is the common options struct used in this package's Update +// operation. +type UpdateRuleOpts struct { + // The L7 rule type. One of COOKIE, FILE_TYPE, HEADER, HOST_NAME, or PATH. + RuleType RuleType `json:"type,omitempty"` + + // The comparison type for the L7 rule. One of CONTAINS, ENDS_WITH, EQUAL_TO, REGEX, or STARTS_WITH. + CompareType CompareType `json:"compare_type,omitempty"` + + // The value to use for the comparison. For example, the file type to compare. + Value string `json:"value,omitempty"` + + // The key to use for the comparison. For example, the name of the cookie to evaluate. + Key *string `json:"key,omitempty"` + + // When true the logic of the rule is inverted. For example, with invert true, + // equal to would become not equal to. Default is false. + Invert *bool `json:"invert,omitempty"` + + // The administrative state of the Loadbalancer. A valid value is true (UP) + // or false (DOWN). + AdminStateUp *bool `json:"admin_state_up,omitempty"` +} + +// ToRuleUpdateMap builds a request body from UpdateRuleOpts. +func (opts UpdateRuleOpts) ToRuleUpdateMap() (map[string]interface{}, error) { + b, err := gophercloud.BuildRequestBody(opts, "rule") + if err != nil { + return nil, err + } + + if m := b["rule"].(map[string]interface{}); m["key"] == "" { + m["key"] = nil + } + + return b, nil +} + +// UpdateRule allows Rule to be updated. +func UpdateRule(c *gophercloud.ServiceClient, policyID string, ruleID string, opts UpdateRuleOptsBuilder) (r UpdateRuleResult) { + b, err := opts.ToRuleUpdateMap() + if err != nil { + r.Err = err + return + } + resp, err := c.Put(ruleResourceURL(c, policyID, ruleID), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 201, 202}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/l7policies/results.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/l7policies/results.go new file mode 100644 index 00000000..bc5b3213 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/l7policies/results.go @@ -0,0 +1,253 @@ +package l7policies + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// L7Policy is a collection of L7 rules associated with a Listener, and which +// may also have an association to a back-end pool. +type L7Policy struct { + // The unique ID for the L7 policy. + ID string `json:"id"` + + // Name of the L7 policy. + Name string `json:"name"` + + // The ID of the listener. + ListenerID string `json:"listener_id"` + + // The L7 policy action. One of REDIRECT_PREFIX, REDIRECT_TO_POOL, REDIRECT_TO_URL, or REJECT. + Action string `json:"action"` + + // The position of this policy on the listener. + Position int32 `json:"position"` + + // A human-readable description for the resource. + Description string `json:"description"` + + // ProjectID is the UUID of the project who owns the L7 policy in octavia. + // Only administrative users can specify a project UUID other than their own. + ProjectID string `json:"project_id"` + + // Requests matching this policy will be redirected to the pool with this ID. + // Only valid if action is REDIRECT_TO_POOL. + RedirectPoolID string `json:"redirect_pool_id"` + + // Requests matching this policy will be redirected to this Prefix URL. + // Only valid if action is REDIRECT_PREFIX. + RedirectPrefix string `json:"redirect_prefix"` + + // Requests matching this policy will be redirected to this URL. + // Only valid if action is REDIRECT_TO_URL. + RedirectURL string `json:"redirect_url"` + + // Requests matching this policy will be redirected to the specified URL or Prefix URL + // with the HTTP response code. Valid if action is REDIRECT_TO_URL or REDIRECT_PREFIX. + RedirectHttpCode int32 `json:"redirect_http_code"` + + // The administrative state of the L7 policy, which is up (true) or down (false). + AdminStateUp bool `json:"admin_state_up"` + + // The provisioning status of the L7 policy. + // This value is ACTIVE, PENDING_* or ERROR. + ProvisioningStatus string `json:"provisioning_status"` + + // The operating status of the L7 policy. + OperatingStatus string `json:"operating_status"` + + // Rules are List of associated L7 rule IDs. + Rules []Rule `json:"rules"` +} + +// Rule represents layer 7 load balancing rule. +type Rule struct { + // The unique ID for the L7 rule. + ID string `json:"id"` + + // The L7 rule type. One of COOKIE, FILE_TYPE, HEADER, HOST_NAME, or PATH. + RuleType string `json:"type"` + + // The comparison type for the L7 rule. One of CONTAINS, ENDS_WITH, EQUAL_TO, REGEX, or STARTS_WITH. + CompareType string `json:"compare_type"` + + // The value to use for the comparison. For example, the file type to compare. + Value string `json:"value"` + + // ProjectID is the UUID of the project who owns the rule in octavia. + // Only administrative users can specify a project UUID other than their own. + ProjectID string `json:"project_id"` + + // The key to use for the comparison. For example, the name of the cookie to evaluate. + Key string `json:"key"` + + // When true the logic of the rule is inverted. For example, with invert true, + // equal to would become not equal to. Default is false. + Invert bool `json:"invert"` + + // The administrative state of the L7 rule, which is up (true) or down (false). + AdminStateUp bool `json:"admin_state_up"` + + // The provisioning status of the L7 rule. + // This value is ACTIVE, PENDING_* or ERROR. + ProvisioningStatus string `json:"provisioning_status"` + + // The operating status of the L7 policy. + OperatingStatus string `json:"operating_status"` +} + +type commonResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a l7policy. +func (r commonResult) Extract() (*L7Policy, error) { + var s struct { + L7Policy *L7Policy `json:"l7policy"` + } + err := r.ExtractInto(&s) + return s.L7Policy, err +} + +// CreateResult represents the result of a Create operation. Call its Extract +// method to interpret the result as a L7Policy. +type CreateResult struct { + commonResult +} + +// L7PolicyPage is the page returned by a pager when traversing over a +// collection of l7policies. +type L7PolicyPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of l7policies has reached +// the end of a page and the pager seeks to traverse over a new one. In order +// to do this, it needs to construct the next page's URL. +func (r L7PolicyPage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"l7policies_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a L7PolicyPage struct is empty. +func (r L7PolicyPage) IsEmpty() (bool, error) { + if r.StatusCode == 204 { + return true, nil + } + + is, err := ExtractL7Policies(r) + return len(is) == 0, err +} + +// ExtractL7Policies accepts a Page struct, specifically a L7PolicyPage struct, +// and extracts the elements into a slice of L7Policy structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractL7Policies(r pagination.Page) ([]L7Policy, error) { + var s struct { + L7Policies []L7Policy `json:"l7policies"` + } + err := (r.(L7PolicyPage)).ExtractInto(&s) + return s.L7Policies, err +} + +// GetResult represents the result of a Get operation. Call its Extract +// method to interpret the result as a L7Policy. +type GetResult struct { + commonResult +} + +// DeleteResult represents the result of a Delete operation. Call its +// ExtractErr method to determine if the request succeeded or failed. +type DeleteResult struct { + gophercloud.ErrResult +} + +// UpdateResult represents the result of an Update operation. Call its Extract +// method to interpret the result as a L7Policy. +type UpdateResult struct { + commonResult +} + +type commonRuleResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a rule. +func (r commonRuleResult) Extract() (*Rule, error) { + var s struct { + Rule *Rule `json:"rule"` + } + err := r.ExtractInto(&s) + return s.Rule, err +} + +// CreateRuleResult represents the result of a CreateRule operation. +// Call its Extract method to interpret it as a Rule. +type CreateRuleResult struct { + commonRuleResult +} + +// RulePage is the page returned by a pager when traversing over a +// collection of Rules in a L7Policy. +type RulePage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of rules has reached +// the end of a page and the pager seeks to traverse over a new one. In order +// to do this, it needs to construct the next page's URL. +func (r RulePage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"rules_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a RulePage struct is empty. +func (r RulePage) IsEmpty() (bool, error) { + if r.StatusCode == 204 { + return true, nil + } + + is, err := ExtractRules(r) + return len(is) == 0, err +} + +// ExtractRules accepts a Page struct, specifically a RulePage struct, +// and extracts the elements into a slice of Rules structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractRules(r pagination.Page) ([]Rule, error) { + var s struct { + Rules []Rule `json:"rules"` + } + err := (r.(RulePage)).ExtractInto(&s) + return s.Rules, err +} + +// GetRuleResult represents the result of a GetRule operation. +// Call its Extract method to interpret it as a Rule. +type GetRuleResult struct { + commonRuleResult +} + +// DeleteRuleResult represents the result of a DeleteRule operation. +// Call its ExtractErr method to determine if the request succeeded or failed. +type DeleteRuleResult struct { + gophercloud.ErrResult +} + +// UpdateRuleResult represents the result of an UpdateRule operation. +// Call its Extract method to interpret it as a Rule. +type UpdateRuleResult struct { + commonRuleResult +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/l7policies/urls.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/l7policies/urls.go new file mode 100644 index 00000000..ecb607a8 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/l7policies/urls.go @@ -0,0 +1,25 @@ +package l7policies + +import "github.com/gophercloud/gophercloud" + +const ( + rootPath = "lbaas" + resourcePath = "l7policies" + rulePath = "rules" +) + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(rootPath, resourcePath) +} + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(rootPath, resourcePath, id) +} + +func ruleRootURL(c *gophercloud.ServiceClient, policyID string) string { + return c.ServiceURL(rootPath, resourcePath, policyID, rulePath) +} + +func ruleResourceURL(c *gophercloud.ServiceClient, policyID string, ruleID string) string { + return c.ServiceURL(rootPath, resourcePath, policyID, rulePath, ruleID) +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/listeners/doc.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/listeners/doc.go new file mode 100644 index 00000000..c99bbc70 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/listeners/doc.go @@ -0,0 +1,77 @@ +/* +Package listeners provides information and interaction with Listeners of the +LBaaS v2 extension for the OpenStack Networking service. + +Example to List Listeners + + listOpts := listeners.ListOpts{ + LoadbalancerID : "ca430f80-1737-4712-8dc6-3f640d55594b", + } + + allPages, err := listeners.List(networkClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allListeners, err := listeners.ExtractListeners(allPages) + if err != nil { + panic(err) + } + + for _, listener := range allListeners { + fmt.Printf("%+v\n", listener) + } + +Example to Create a Listener + + createOpts := listeners.CreateOpts{ + Protocol: "TCP", + Name: "db", + LoadbalancerID: "79e05663-7f03-45d2-a092-8b94062f22ab", + AdminStateUp: gophercloud.Enabled, + DefaultPoolID: "41efe233-7591-43c5-9cf7-923964759f9e", + ProtocolPort: 3306, + Tags: []string{"test", "stage"}, + } + + listener, err := listeners.Create(networkClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Update a Listener + + listenerID := "d67d56a6-4a86-4688-a282-f46444705c64" + + i1001 := 1001 + i181000 := 181000 + newTags := []string{"prod"} + updateOpts := listeners.UpdateOpts{ + ConnLimit: &i1001, + TimeoutClientData: &i181000, + TimeoutMemberData: &i181000, + Tags: &newTags, + } + + listener, err := listeners.Update(networkClient, listenerID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a Listener + + listenerID := "d67d56a6-4a86-4688-a282-f46444705c64" + err := listeners.Delete(networkClient, listenerID).ExtractErr() + if err != nil { + panic(err) + } + +Example to Get the Statistics of a Listener + + listenerID := "d67d56a6-4a86-4688-a282-f46444705c64" + stats, err := listeners.GetStats(networkClient, listenerID).Extract() + if err != nil { + panic(err) + } +*/ +package listeners diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/listeners/requests.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/listeners/requests.go new file mode 100644 index 00000000..a0a06f64 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/listeners/requests.go @@ -0,0 +1,303 @@ +package listeners + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/l7policies" + "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/pools" + "github.com/gophercloud/gophercloud/pagination" +) + +// Type Protocol represents a listener protocol. +type Protocol string + +// Supported attributes for create/update operations. +const ( + ProtocolTCP Protocol = "TCP" + ProtocolUDP Protocol = "UDP" + ProtocolPROXY Protocol = "PROXY" + ProtocolHTTP Protocol = "HTTP" + ProtocolHTTPS Protocol = "HTTPS" + // Protocol SCTP requires octavia microversion 2.23 + ProtocolSCTP Protocol = "SCTP" + // Protocol Prometheus requires octavia microversion 2.25 + ProtocolPrometheus Protocol = "PROMETHEUS" + ProtocolTerminatedHTTPS Protocol = "TERMINATED_HTTPS" +) + +// Type TLSVersion represents a tls version +type TLSVersion string + +const ( + TLSVersionSSLv3 TLSVersion = "SSLv3" + TLSVersionTLSv1 TLSVersion = "TLSv1" + TLSVersionTLSv1_1 TLSVersion = "TLSv1.1" + TLSVersionTLSv1_2 TLSVersion = "TLSv1.2" + TLSVersionTLSv1_3 TLSVersion = "TLSv1.3" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToListenerListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the floating IP attributes you want to see returned. SortKey allows you to +// sort by a particular listener attribute. SortDir sets the direction, and is +// either `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + ID string `q:"id"` + Name string `q:"name"` + AdminStateUp *bool `q:"admin_state_up"` + ProjectID string `q:"project_id"` + LoadbalancerID string `q:"loadbalancer_id"` + DefaultPoolID string `q:"default_pool_id"` + Protocol string `q:"protocol"` + ProtocolPort int `q:"protocol_port"` + ConnectionLimit int `q:"connection_limit"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` + TimeoutClientData *int `q:"timeout_client_data"` + TimeoutMemberData *int `q:"timeout_member_data"` + TimeoutMemberConnect *int `q:"timeout_member_connect"` + TimeoutTCPInspect *int `q:"timeout_tcp_inspect"` +} + +// ToListenerListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToListenerListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns a Pager which allows you to iterate over a collection of +// listeners. It accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +// +// Default policy settings return only those listeners that are owned by the +// project who submits the request, unless an admin user submits the request. +func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := rootURL(c) + if opts != nil { + query, err := opts.ToListenerListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return ListenerPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToListenerCreateMap() (map[string]interface{}, error) +} + +// CreateOpts represents options for creating a listener. +type CreateOpts struct { + // The load balancer on which to provision this listener. + LoadbalancerID string `json:"loadbalancer_id,omitempty"` + + // The protocol - can either be TCP, SCTP, HTTP, HTTPS or TERMINATED_HTTPS. + Protocol Protocol `json:"protocol" required:"true"` + + // The port on which to listen for client traffic. + ProtocolPort int `json:"protocol_port" required:"true"` + + // ProjectID is only required if the caller has an admin role and wants + // to create a pool for another project. + ProjectID string `json:"project_id,omitempty"` + + // Human-readable name for the Listener. Does not have to be unique. + Name string `json:"name,omitempty"` + + // The ID of the default pool with which the Listener is associated. + DefaultPoolID string `json:"default_pool_id,omitempty"` + + // DefaultPool an instance of pools.CreateOpts which allows a + // (default) pool to be created at the same time the listener is created. + // + // This is only possible to use when creating a fully populated + // load balancer. + DefaultPool *pools.CreateOpts `json:"default_pool,omitempty"` + + // Human-readable description for the Listener. + Description string `json:"description,omitempty"` + + // The maximum number of connections allowed for the Listener. + ConnLimit *int `json:"connection_limit,omitempty"` + + // A reference to a Barbican container of TLS secrets. + DefaultTlsContainerRef string `json:"default_tls_container_ref,omitempty"` + + // A list of references to TLS secrets. + SniContainerRefs []string `json:"sni_container_refs,omitempty"` + + // The administrative state of the Listener. A valid value is true (UP) + // or false (DOWN). + AdminStateUp *bool `json:"admin_state_up,omitempty"` + + // L7Policies is a slice of l7policies.CreateOpts which allows a set + // of policies to be created at the same time the listener is created. + // + // This is only possible to use when creating a fully populated + // Loadbalancer. + L7Policies []l7policies.CreateOpts `json:"l7policies,omitempty"` + + // Frontend client inactivity timeout in milliseconds + TimeoutClientData *int `json:"timeout_client_data,omitempty"` + + // Backend member inactivity timeout in milliseconds + TimeoutMemberData *int `json:"timeout_member_data,omitempty"` + + // Backend member connection timeout in milliseconds + TimeoutMemberConnect *int `json:"timeout_member_connect,omitempty"` + + // Time, in milliseconds, to wait for additional TCP packets for content inspection + TimeoutTCPInspect *int `json:"timeout_tcp_inspect,omitempty"` + + // A dictionary of optional headers to insert into the request before it is sent to the backend member. + InsertHeaders map[string]string `json:"insert_headers,omitempty"` + + // A list of IPv4, IPv6 or mix of both CIDRs + AllowedCIDRs []string `json:"allowed_cidrs,omitempty"` + + // A list of TLS protocol versions. Available from microversion 2.17 + TLSVersions []TLSVersion `json:"tls_versions,omitempty"` + + // Tags is a set of resource tags. New in version 2.5 + Tags []string `json:"tags,omitempty"` +} + +// ToListenerCreateMap builds a request body from CreateOpts. +func (opts CreateOpts) ToListenerCreateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "listener") +} + +// Create is an operation which provisions a new Listeners based on the +// configuration defined in the CreateOpts struct. Once the request is +// validated and progress has started on the provisioning process, a +// CreateResult will be returned. +// +// Users with an admin role can create Listeners on behalf of other projects by +// specifying a ProjectID attribute different than their own. +func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToListenerCreateMap() + if err != nil { + r.Err = err + return + } + resp, err := c.Post(rootURL(c), b, &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Get retrieves a particular Listeners based on its unique ID. +func Get(c *gophercloud.ServiceClient, id string) (r GetResult) { + resp, err := c.Get(resourceURL(c, id), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the +// Update request. +type UpdateOptsBuilder interface { + ToListenerUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts represents options for updating a Listener. +type UpdateOpts struct { + // Human-readable name for the Listener. Does not have to be unique. + Name *string `json:"name,omitempty"` + + // The ID of the default pool with which the Listener is associated. + DefaultPoolID *string `json:"default_pool_id,omitempty"` + + // Human-readable description for the Listener. + Description *string `json:"description,omitempty"` + + // The maximum number of connections allowed for the Listener. + ConnLimit *int `json:"connection_limit,omitempty"` + + // A reference to a Barbican container of TLS secrets. + DefaultTlsContainerRef *string `json:"default_tls_container_ref,omitempty"` + + // A list of references to TLS secrets. + SniContainerRefs *[]string `json:"sni_container_refs,omitempty"` + + // The administrative state of the Listener. A valid value is true (UP) + // or false (DOWN). + AdminStateUp *bool `json:"admin_state_up,omitempty"` + + // Frontend client inactivity timeout in milliseconds + TimeoutClientData *int `json:"timeout_client_data,omitempty"` + + // Backend member inactivity timeout in milliseconds + TimeoutMemberData *int `json:"timeout_member_data,omitempty"` + + // Backend member connection timeout in milliseconds + TimeoutMemberConnect *int `json:"timeout_member_connect,omitempty"` + + // Time, in milliseconds, to wait for additional TCP packets for content inspection + TimeoutTCPInspect *int `json:"timeout_tcp_inspect,omitempty"` + + // A dictionary of optional headers to insert into the request before it is sent to the backend member. + InsertHeaders *map[string]string `json:"insert_headers,omitempty"` + + // A list of IPv4, IPv6 or mix of both CIDRs + AllowedCIDRs *[]string `json:"allowed_cidrs,omitempty"` + + // A list of TLS protocol versions. Available from microversion 2.17 + TLSVersions *[]TLSVersion `json:"tls_versions,omitempty"` + + // Tags is a set of resource tags. New in version 2.5 + Tags *[]string `json:"tags,omitempty"` +} + +// ToListenerUpdateMap builds a request body from UpdateOpts. +func (opts UpdateOpts) ToListenerUpdateMap() (map[string]interface{}, error) { + b, err := gophercloud.BuildRequestBody(opts, "listener") + if err != nil { + return nil, err + } + + if m := b["listener"].(map[string]interface{}); m["default_pool_id"] == "" { + m["default_pool_id"] = nil + } + + return b, nil +} + +// Update is an operation which modifies the attributes of the specified +// Listener. +func Update(c *gophercloud.ServiceClient, id string, opts UpdateOpts) (r UpdateResult) { + b, err := opts.ToListenerUpdateMap() + if err != nil { + r.Err = err + return + } + resp, err := c.Put(resourceURL(c, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 202}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Delete will permanently delete a particular Listeners based on its unique ID. +func Delete(c *gophercloud.ServiceClient, id string) (r DeleteResult) { + resp, err := c.Delete(resourceURL(c, id), nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// GetStats will return the shows the current statistics of a particular Listeners. +func GetStats(c *gophercloud.ServiceClient, id string) (r StatsResult) { + resp, err := c.Get(statisticsRootURL(c, id), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/listeners/results.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/listeners/results.go new file mode 100644 index 00000000..234b6cb0 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/listeners/results.go @@ -0,0 +1,230 @@ +package listeners + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/l7policies" + "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/pools" + "github.com/gophercloud/gophercloud/pagination" +) + +type LoadBalancerID struct { + ID string `json:"id"` +} + +// Listener is the primary load balancing configuration object that specifies +// the loadbalancer and port on which client traffic is received, as well +// as other details such as the load balancing method to be use, protocol, etc. +type Listener struct { + // The unique ID for the Listener. + ID string `json:"id"` + + // Owner of the Listener. + ProjectID string `json:"project_id"` + + // Human-readable name for the Listener. Does not have to be unique. + Name string `json:"name"` + + // Human-readable description for the Listener. + Description string `json:"description"` + + // The protocol to loadbalance. A valid value is TCP, SCTP, HTTP, HTTPS or TERMINATED_HTTPS. + Protocol string `json:"protocol"` + + // The port on which to listen to client traffic that is associated with the + // Loadbalancer. A valid value is from 0 to 65535. + ProtocolPort int `json:"protocol_port"` + + // The UUID of default pool. Must have compatible protocol with listener. + DefaultPoolID string `json:"default_pool_id"` + + // The default pool with which the Listener is associated. + DefaultPool *pools.Pool `json:"default_pool"` + + // A list of load balancer IDs. + Loadbalancers []LoadBalancerID `json:"loadbalancers"` + + // The maximum number of connections allowed for the Loadbalancer. + // Default is -1, meaning no limit. + ConnLimit int `json:"connection_limit"` + + // The list of references to TLS secrets. + SniContainerRefs []string `json:"sni_container_refs"` + + // A reference to a Barbican container of TLS secrets. + DefaultTlsContainerRef string `json:"default_tls_container_ref"` + + // The administrative state of the Listener. A valid value is true (UP) or false (DOWN). + AdminStateUp bool `json:"admin_state_up"` + + // Pools are the pools which are part of this listener. + Pools []pools.Pool `json:"pools"` + + // L7policies are the L7 policies which are part of this listener. + L7Policies []l7policies.L7Policy `json:"l7policies"` + + // The provisioning status of the Listener. + // This value is ACTIVE, PENDING_* or ERROR. + ProvisioningStatus string `json:"provisioning_status"` + + // Frontend client inactivity timeout in milliseconds + TimeoutClientData int `json:"timeout_client_data"` + + // Backend member inactivity timeout in milliseconds + TimeoutMemberData int `json:"timeout_member_data"` + + // Backend member connection timeout in milliseconds + TimeoutMemberConnect int `json:"timeout_member_connect"` + + // Time, in milliseconds, to wait for additional TCP packets for content inspection + TimeoutTCPInspect int `json:"timeout_tcp_inspect"` + + // A dictionary of optional headers to insert into the request before it is sent to the backend member. + InsertHeaders map[string]string `json:"insert_headers"` + + // A list of IPv4, IPv6 or mix of both CIDRs + AllowedCIDRs []string `json:"allowed_cidrs"` + + // List of ciphers in OpenSSL format (colon-separated). See + // https://www.openssl.org/docs/man1.1.1/man1/ciphers.html + // New in version 2.15 + TLSCiphers string `json:"tls_ciphers"` + + // A list of TLS protocol versions. Available from microversion 2.17 + TLSVersions []string `json:"tls_versions"` + + // Tags is a list of resource tags. Tags are arbitrarily defined strings + // attached to the resource. New in version 2.5 + Tags []string `json:"tags"` + + // A list of ALPN protocols. Available protocols: http/1.0, http/1.1, h2 + // New in version 2.20 + ALPNProtocols []string `json:"alpn_protocols"` + + // The TLS client authentication mode. One of the options NONE, OPTIONAL or MANDATORY. + // New in version 2.8 + ClientAuthentication string `json:"client_authentication"` + + // The ref of the key manager service secret containing a PEM format + // client CA certificate bundle for TERMINATED_HTTPS listeners. + // New in version 2.8 + ClientCATLSContainerRef string `json:"client_ca_tls_container_ref"` + + // The URI of the key manager service secret containing a PEM format CA + // revocation list file for TERMINATED_HTTPS listeners. + // New in version 2.8 + ClientCRLContainerRef string `json:"client_crl_container_ref"` + + // The operating status of the resource + OperatingStatus string `json:"operating_status"` +} + +type Stats struct { + // The currently active connections. + ActiveConnections int `json:"active_connections"` + + // The total bytes received. + BytesIn int `json:"bytes_in"` + + // The total bytes sent. + BytesOut int `json:"bytes_out"` + + // The total requests that were unable to be fulfilled. + RequestErrors int `json:"request_errors"` + + // The total connections handled. + TotalConnections int `json:"total_connections"` +} + +// ListenerPage is the page returned by a pager when traversing over a +// collection of listeners. +type ListenerPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of listeners has reached +// the end of a page and the pager seeks to traverse over a new one. In order +// to do this, it needs to construct the next page's URL. +func (r ListenerPage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"listeners_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a ListenerPage struct is empty. +func (r ListenerPage) IsEmpty() (bool, error) { + if r.StatusCode == 204 { + return true, nil + } + + is, err := ExtractListeners(r) + return len(is) == 0, err +} + +// ExtractListeners accepts a Page struct, specifically a ListenerPage struct, +// and extracts the elements into a slice of Listener structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractListeners(r pagination.Page) ([]Listener, error) { + var s struct { + Listeners []Listener `json:"listeners"` + } + err := (r.(ListenerPage)).ExtractInto(&s) + return s.Listeners, err +} + +type commonResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a listener. +func (r commonResult) Extract() (*Listener, error) { + var s struct { + Listener *Listener `json:"listener"` + } + err := r.ExtractInto(&s) + return s.Listener, err +} + +// CreateResult represents the result of a create operation. Call its Extract +// method to interpret it as a Listener. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. Call its Extract +// method to interpret it as a Listener. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. Call its Extract +// method to interpret it as a Listener. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. Call its +// ExtractErr method to determine if the request succeeded or failed. +type DeleteResult struct { + gophercloud.ErrResult +} + +// StatsResult represents the result of a GetStats operation. +// Call its Extract method to interpret it as a Stats. +type StatsResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts the status of +// a Listener. +func (r StatsResult) Extract() (*Stats, error) { + var s struct { + Stats *Stats `json:"stats"` + } + err := r.ExtractInto(&s) + return s.Stats, err +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/listeners/urls.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/listeners/urls.go new file mode 100644 index 00000000..e9e3bccd --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/listeners/urls.go @@ -0,0 +1,21 @@ +package listeners + +import "github.com/gophercloud/gophercloud" + +const ( + rootPath = "lbaas" + resourcePath = "listeners" + statisticsPath = "stats" +) + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(rootPath, resourcePath) +} + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(rootPath, resourcePath, id) +} + +func statisticsRootURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(rootPath, resourcePath, id, statisticsPath) +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/loadbalancers/doc.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/loadbalancers/doc.go new file mode 100644 index 00000000..8587a0d9 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/loadbalancers/doc.go @@ -0,0 +1,140 @@ +/* +Package loadbalancers provides information and interaction with Load Balancers +of the LBaaS v2 extension for the OpenStack Networking service. + +Example to List Load Balancers + + listOpts := loadbalancers.ListOpts{ + Provider: "haproxy", + } + + allPages, err := loadbalancers.List(networkClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allLoadbalancers, err := loadbalancers.ExtractLoadBalancers(allPages) + if err != nil { + panic(err) + } + + for _, lb := range allLoadbalancers { + fmt.Printf("%+v\n", lb) + } + +Example to Create a Load Balancer + + createOpts := loadbalancers.CreateOpts{ + Name: "db_lb", + AdminStateUp: gophercloud.Enabled, + VipSubnetID: "9cedb85d-0759-4898-8a4b-fa5a5ea10086", + VipAddress: "10.30.176.48", + FlavorID: "60df399a-ee85-11e9-81b4-2a2ae2dbcce4", + Provider: "haproxy", + Tags: []string{"test", "stage"}, + } + + lb, err := loadbalancers.Create(networkClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Create a fully populated Load Balancer + + createOpts := loadbalancers.CreateOpts{ + Name: "db_lb", + AdminStateUp: gophercloud.Enabled, + VipSubnetID: "9cedb85d-0759-4898-8a4b-fa5a5ea10086", + VipAddress: "10.30.176.48", + FlavorID: "60df399a-ee85-11e9-81b4-2a2ae2dbcce4", + Provider: "haproxy", + Tags: []string{"test", "stage"}, + Listeners: []listeners.CreateOpts{{ + Protocol: "HTTP", + ProtocolPort: 8080, + Name: "redirect_listener", + L7Policies: []l7policies.CreateOpts{{ + Name: "redirect-example.com", + Action: l7policies.ActionRedirectToURL, + RedirectURL: "http://www.example.com", + Rules: []l7policies.CreateRuleOpts{{ + RuleType: l7policies.TypePath, + CompareType: l7policies.CompareTypeRegex, + Value: "/images*", + }}, + }}, + DefaultPool: &pools.CreateOpts{ + LBMethod: pools.LBMethodRoundRobin, + Protocol: "HTTP", + Name: "example pool", + Members: []pools.BatchUpdateMemberOpts{{ + Address: "192.0.2.51", + ProtocolPort: 80, + },}, + Monitor: &monitors.CreateOpts{ + Name: "db", + Type: "HTTP", + Delay: 3, + MaxRetries: 2, + Timeout: 1, + }, + }, + }}, + } + + lb, err := loadbalancers.Create(networkClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Update a Load Balancer + + lbID := "d67d56a6-4a86-4688-a282-f46444705c64" + name := "new-name" + updateOpts := loadbalancers.UpdateOpts{ + Name: &name, + } + lb, err := loadbalancers.Update(networkClient, lbID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a Load Balancers + + deleteOpts := loadbalancers.DeleteOpts{ + Cascade: true, + } + + lbID := "d67d56a6-4a86-4688-a282-f46444705c64" + + err := loadbalancers.Delete(networkClient, lbID, deleteOpts).ExtractErr() + if err != nil { + panic(err) + } + +Example to Get the Status of a Load Balancer + + lbID := "d67d56a6-4a86-4688-a282-f46444705c64" + status, err := loadbalancers.GetStatuses(networkClient, LBID).Extract() + if err != nil { + panic(err) + } + +Example to Get the Statistics of a Load Balancer + + lbID := "d67d56a6-4a86-4688-a282-f46444705c64" + stats, err := loadbalancers.GetStats(networkClient, LBID).Extract() + if err != nil { + panic(err) + } + +Example to Failover a Load Balancers + + lbID := "d67d56a6-4a86-4688-a282-f46444705c64" + + err := loadbalancers.Failover(networkClient, lbID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package loadbalancers diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/loadbalancers/requests.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/loadbalancers/requests.go new file mode 100644 index 00000000..099113c4 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/loadbalancers/requests.go @@ -0,0 +1,275 @@ +package loadbalancers + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/listeners" + "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/pools" + "github.com/gophercloud/gophercloud/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToLoadBalancerListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the Loadbalancer attributes you want to see returned. SortKey allows you to +// sort by a particular attribute. SortDir sets the direction, and is +// either `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + Description string `q:"description"` + AdminStateUp *bool `q:"admin_state_up"` + ProjectID string `q:"project_id"` + ProvisioningStatus string `q:"provisioning_status"` + VipAddress string `q:"vip_address"` + VipPortID string `q:"vip_port_id"` + VipSubnetID string `q:"vip_subnet_id"` + VipNetworkID string `q:"vip_network_id"` + ID string `q:"id"` + OperatingStatus string `q:"operating_status"` + Name string `q:"name"` + FlavorID string `q:"flavor_id"` + AvailabilityZone string `q:"availability_zone"` + Provider string `q:"provider"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` + Tags []string `q:"tags"` + TagsAny []string `q:"tags-any"` + TagsNot []string `q:"not-tags"` + TagsNotAny []string `q:"not-tags-any"` +} + +// ToLoadBalancerListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToLoadBalancerListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns a Pager which allows you to iterate over a collection of +// load balancers. It accepts a ListOpts struct, which allows you to filter +// and sort the returned collection for greater efficiency. +// +// Default policy settings return only those load balancers that are owned by +// the project who submits the request, unless an admin user submits the request. +func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := rootURL(c) + if opts != nil { + query, err := opts.ToLoadBalancerListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return LoadBalancerPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToLoadBalancerCreateMap() (map[string]interface{}, error) +} + +// CreateOpts is the common options struct used in this package's Create +// operation. +type CreateOpts struct { + // Human-readable name for the Loadbalancer. Does not have to be unique. + Name string `json:"name,omitempty"` + + // Human-readable description for the Loadbalancer. + Description string `json:"description,omitempty"` + + // Providing a neutron port ID for the vip_port_id tells Octavia to use this + // port for the VIP. If the port has more than one subnet you must specify + // either the vip_subnet_id or vip_address to clarify which address should + // be used for the VIP. + VipPortID string `json:"vip_port_id,omitempty"` + + // The subnet on which to allocate the Loadbalancer's address. A project can + // only create Loadbalancers on networks authorized by policy (e.g. networks + // that belong to them or networks that are shared). + VipSubnetID string `json:"vip_subnet_id,omitempty"` + + // The network on which to allocate the Loadbalancer's address. A tenant can + // only create Loadbalancers on networks authorized by policy (e.g. networks + // that belong to them or networks that are shared). + VipNetworkID string `json:"vip_network_id,omitempty"` + + // ProjectID is the UUID of the project who owns the Loadbalancer. + // Only administrative users can specify a project UUID other than their own. + ProjectID string `json:"project_id,omitempty"` + + // The IP address of the Loadbalancer. + VipAddress string `json:"vip_address,omitempty"` + + // The ID of the QoS Policy which will apply to the Virtual IP + VipQosPolicyID string `json:"vip_qos_policy_id,omitempty"` + + // The administrative state of the Loadbalancer. A valid value is true (UP) + // or false (DOWN). + AdminStateUp *bool `json:"admin_state_up,omitempty"` + + // The UUID of a flavor. + FlavorID string `json:"flavor_id,omitempty"` + + // The name of an Octavia availability zone. + // Requires Octavia API version 2.14 or later. + AvailabilityZone string `json:"availability_zone,omitempty"` + + // The name of the provider. + Provider string `json:"provider,omitempty"` + + // Listeners is a slice of listeners.CreateOpts which allows a set + // of listeners to be created at the same time the Loadbalancer is created. + // + // This is only possible to use when creating a fully populated + // load balancer. + Listeners []listeners.CreateOpts `json:"listeners,omitempty"` + + // Pools is a slice of pools.CreateOpts which allows a set of pools + // to be created at the same time the Loadbalancer is created. + // + // This is only possible to use when creating a fully populated + // load balancer. + Pools []pools.CreateOpts `json:"pools,omitempty"` + + // Tags is a set of resource tags. + Tags []string `json:"tags,omitempty"` +} + +// ToLoadBalancerCreateMap builds a request body from CreateOpts. +func (opts CreateOpts) ToLoadBalancerCreateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "loadbalancer") +} + +// Create is an operation which provisions a new loadbalancer based on the +// configuration defined in the CreateOpts struct. Once the request is +// validated and progress has started on the provisioning process, a +// CreateResult will be returned. +func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToLoadBalancerCreateMap() + if err != nil { + r.Err = err + return + } + resp, err := c.Post(rootURL(c), b, &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Get retrieves a particular Loadbalancer based on its unique ID. +func Get(c *gophercloud.ServiceClient, id string) (r GetResult) { + resp, err := c.Get(resourceURL(c, id), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the +// Update request. +type UpdateOptsBuilder interface { + ToLoadBalancerUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts is the common options struct used in this package's Update +// operation. +type UpdateOpts struct { + // Human-readable name for the Loadbalancer. Does not have to be unique. + Name *string `json:"name,omitempty"` + + // Human-readable description for the Loadbalancer. + Description *string `json:"description,omitempty"` + + // The administrative state of the Loadbalancer. A valid value is true (UP) + // or false (DOWN). + AdminStateUp *bool `json:"admin_state_up,omitempty"` + + // The ID of the QoS Policy which will apply to the Virtual IP + VipQosPolicyID *string `json:"vip_qos_policy_id,omitempty"` + + // Tags is a set of resource tags. + Tags *[]string `json:"tags,omitempty"` +} + +// ToLoadBalancerUpdateMap builds a request body from UpdateOpts. +func (opts UpdateOpts) ToLoadBalancerUpdateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "loadbalancer") +} + +// Update is an operation which modifies the attributes of the specified +// LoadBalancer. +func Update(c *gophercloud.ServiceClient, id string, opts UpdateOpts) (r UpdateResult) { + b, err := opts.ToLoadBalancerUpdateMap() + if err != nil { + r.Err = err + return + } + resp, err := c.Put(resourceURL(c, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 202}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// DeleteOptsBuilder allows extensions to add additional parameters to the +// Delete request. +type DeleteOptsBuilder interface { + ToLoadBalancerDeleteQuery() (string, error) +} + +// DeleteOpts is the common options struct used in this package's Delete +// operation. +type DeleteOpts struct { + // Cascade will delete all children of the load balancer (listners, monitors, etc). + Cascade bool `q:"cascade"` +} + +// ToLoadBalancerDeleteQuery formats a DeleteOpts into a query string. +func (opts DeleteOpts) ToLoadBalancerDeleteQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// Delete will permanently delete a particular LoadBalancer based on its +// unique ID. +func Delete(c *gophercloud.ServiceClient, id string, opts DeleteOptsBuilder) (r DeleteResult) { + url := resourceURL(c, id) + if opts != nil { + query, err := opts.ToLoadBalancerDeleteQuery() + if err != nil { + r.Err = err + return + } + url += query + } + resp, err := c.Delete(url, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// GetStatuses will return the status of a particular LoadBalancer. +func GetStatuses(c *gophercloud.ServiceClient, id string) (r GetStatusesResult) { + resp, err := c.Get(statusRootURL(c, id), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// GetStats will return the shows the current statistics of a particular LoadBalancer. +func GetStats(c *gophercloud.ServiceClient, id string) (r StatsResult) { + resp, err := c.Get(statisticsRootURL(c, id), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Failover performs a failover of a load balancer. +func Failover(c *gophercloud.ServiceClient, id string) (r FailoverResult) { + resp, err := c.Put(failoverRootURL(c, id), nil, nil, &gophercloud.RequestOpts{ + OkCodes: []int{202}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/loadbalancers/results.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/loadbalancers/results.go new file mode 100644 index 00000000..71f750dd --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/loadbalancers/results.go @@ -0,0 +1,256 @@ +package loadbalancers + +import ( + "encoding/json" + "time" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/listeners" + "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/pools" + "github.com/gophercloud/gophercloud/pagination" +) + +// LoadBalancer is the primary load balancing configuration object that +// specifies the virtual IP address on which client traffic is received, as well +// as other details such as the load balancing method to be use, protocol, etc. +type LoadBalancer struct { + // Human-readable description for the Loadbalancer. + Description string `json:"description"` + + // The administrative state of the Loadbalancer. + // A valid value is true (UP) or false (DOWN). + AdminStateUp bool `json:"admin_state_up"` + + // Owner of the LoadBalancer. + ProjectID string `json:"project_id"` + + // UpdatedAt and CreatedAt contain ISO-8601 timestamps of when the state of the + // loadbalancer last changed, and when it was created. + UpdatedAt time.Time `json:"-"` + CreatedAt time.Time `json:"-"` + + // The provisioning status of the LoadBalancer. + // This value is ACTIVE, PENDING_CREATE or ERROR. + ProvisioningStatus string `json:"provisioning_status"` + + // The IP address of the Loadbalancer. + VipAddress string `json:"vip_address"` + + // The UUID of the port associated with the IP address. + VipPortID string `json:"vip_port_id"` + + // The UUID of the subnet on which to allocate the virtual IP for the + // Loadbalancer address. + VipSubnetID string `json:"vip_subnet_id"` + + // The UUID of the network on which to allocate the virtual IP for the + // Loadbalancer address. + VipNetworkID string `json:"vip_network_id"` + + // The ID of the QoS Policy which will apply to the Virtual IP + VipQosPolicyID string `json:"vip_qos_policy_id"` + + // The unique ID for the LoadBalancer. + ID string `json:"id"` + + // The operating status of the LoadBalancer. This value is ONLINE or OFFLINE. + OperatingStatus string `json:"operating_status"` + + // Human-readable name for the LoadBalancer. Does not have to be unique. + Name string `json:"name"` + + // The UUID of a flavor if set. + FlavorID string `json:"flavor_id"` + + // The name of an Octavia availability zone if set. + AvailabilityZone string `json:"availability_zone"` + + // The name of the provider. + Provider string `json:"provider"` + + // Listeners are the listeners related to this Loadbalancer. + Listeners []listeners.Listener `json:"listeners"` + + // Pools are the pools related to this Loadbalancer. + Pools []pools.Pool `json:"pools"` + + // Tags is a list of resource tags. Tags are arbitrarily defined strings + // attached to the resource. + Tags []string `json:"tags"` +} + +func (r *LoadBalancer) UnmarshalJSON(b []byte) error { + type tmp LoadBalancer + + // Support for older neutron time format + var s1 struct { + tmp + CreatedAt gophercloud.JSONRFC3339NoZ `json:"created_at"` + UpdatedAt gophercloud.JSONRFC3339NoZ `json:"updated_at"` + } + + err := json.Unmarshal(b, &s1) + if err == nil { + *r = LoadBalancer(s1.tmp) + r.CreatedAt = time.Time(s1.CreatedAt) + r.UpdatedAt = time.Time(s1.UpdatedAt) + + return nil + } + + // Support for newer neutron time format + var s2 struct { + tmp + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + } + + err = json.Unmarshal(b, &s2) + if err != nil { + return err + } + + *r = LoadBalancer(s2.tmp) + r.CreatedAt = time.Time(s2.CreatedAt) + r.UpdatedAt = time.Time(s2.UpdatedAt) + + return nil +} + +// StatusTree represents the status of a loadbalancer. +type StatusTree struct { + Loadbalancer *LoadBalancer `json:"loadbalancer"` +} + +type Stats struct { + // The currently active connections. + ActiveConnections int `json:"active_connections"` + + // The total bytes received. + BytesIn int `json:"bytes_in"` + + // The total bytes sent. + BytesOut int `json:"bytes_out"` + + // The total requests that were unable to be fulfilled. + RequestErrors int `json:"request_errors"` + + // The total connections handled. + TotalConnections int `json:"total_connections"` +} + +// LoadBalancerPage is the page returned by a pager when traversing over a +// collection of load balancers. +type LoadBalancerPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of load balancers has +// reached the end of a page and the pager seeks to traverse over a new one. +// In order to do this, it needs to construct the next page's URL. +func (r LoadBalancerPage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"loadbalancers_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a LoadBalancerPage struct is empty. +func (r LoadBalancerPage) IsEmpty() (bool, error) { + if r.StatusCode == 204 { + return true, nil + } + + is, err := ExtractLoadBalancers(r) + return len(is) == 0, err +} + +// ExtractLoadBalancers accepts a Page struct, specifically a LoadbalancerPage +// struct, and extracts the elements into a slice of LoadBalancer structs. In +// other words, a generic collection is mapped into a relevant slice. +func ExtractLoadBalancers(r pagination.Page) ([]LoadBalancer, error) { + var s struct { + LoadBalancers []LoadBalancer `json:"loadbalancers"` + } + err := (r.(LoadBalancerPage)).ExtractInto(&s) + return s.LoadBalancers, err +} + +type commonResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a loadbalancer. +func (r commonResult) Extract() (*LoadBalancer, error) { + var s struct { + LoadBalancer *LoadBalancer `json:"loadbalancer"` + } + err := r.ExtractInto(&s) + return s.LoadBalancer, err +} + +// GetStatusesResult represents the result of a GetStatuses operation. +// Call its Extract method to interpret it as a StatusTree. +type GetStatusesResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts the status of +// a Loadbalancer. +func (r GetStatusesResult) Extract() (*StatusTree, error) { + var s struct { + Statuses *StatusTree `json:"statuses"` + } + err := r.ExtractInto(&s) + return s.Statuses, err +} + +// StatsResult represents the result of a GetStats operation. +// Call its Extract method to interpret it as a Stats. +type StatsResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts the status of +// a Loadbalancer. +func (r StatsResult) Extract() (*Stats, error) { + var s struct { + Stats *Stats `json:"stats"` + } + err := r.ExtractInto(&s) + return s.Stats, err +} + +// CreateResult represents the result of a create operation. Call its Extract +// method to interpret it as a LoadBalancer. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. Call its Extract +// method to interpret it as a LoadBalancer. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. Call its Extract +// method to interpret it as a LoadBalancer. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. Call its +// ExtractErr method to determine if the request succeeded or failed. +type DeleteResult struct { + gophercloud.ErrResult +} + +// FailoverResult represents the result of a failover operation. Call its +// ExtractErr method to determine if the request succeeded or failed. +type FailoverResult struct { + gophercloud.ErrResult +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/loadbalancers/urls.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/loadbalancers/urls.go new file mode 100644 index 00000000..7b184e35 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/loadbalancers/urls.go @@ -0,0 +1,31 @@ +package loadbalancers + +import "github.com/gophercloud/gophercloud" + +const ( + rootPath = "lbaas" + resourcePath = "loadbalancers" + statusPath = "status" + statisticsPath = "stats" + failoverPath = "failover" +) + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(rootPath, resourcePath) +} + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(rootPath, resourcePath, id) +} + +func statusRootURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(rootPath, resourcePath, id, statusPath) +} + +func statisticsRootURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(rootPath, resourcePath, id, statisticsPath) +} + +func failoverRootURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(rootPath, resourcePath, id, failoverPath) +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/monitors/doc.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/monitors/doc.go new file mode 100644 index 00000000..b191b45e --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/monitors/doc.go @@ -0,0 +1,71 @@ +/* +Package monitors provides information and interaction with Monitors +of the LBaaS v2 extension for the OpenStack Networking service. + +Example to List Monitors + + listOpts := monitors.ListOpts{ + PoolID: "c79a4468-d788-410c-bf79-9a8ef6354852", + } + + allPages, err := monitors.List(networkClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allMonitors, err := monitors.ExtractMonitors(allPages) + if err != nil { + panic(err) + } + + for _, monitor := range allMonitors { + fmt.Printf("%+v\n", monitor) + } + +Example to Create a Monitor + + createOpts := monitors.CreateOpts{ + Type: "HTTP", + Name: "db", + PoolID: "84f1b61f-58c4-45bf-a8a9-2dafb9e5214d", + Delay: 20, + Timeout: 10, + MaxRetries: 5, + MaxRetriesDown: 4, + URLPath: "/check", + ExpectedCodes: "200-299", + } + + monitor, err := monitors.Create(networkClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Update a Monitor + + monitorID := "d67d56a6-4a86-4688-a282-f46444705c64" + + updateOpts := monitors.UpdateOpts{ + Name: "NewHealthmonitorName", + Delay: 3, + Timeout: 20, + MaxRetries: 10, + MaxRetriesDown: 8, + URLPath: "/another_check", + ExpectedCodes: "301", + } + + monitor, err := monitors.Update(networkClient, monitorID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a Monitor + + monitorID := "d67d56a6-4a86-4688-a282-f46444705c64" + err := monitors.Delete(networkClient, monitorID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package monitors diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/monitors/requests.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/monitors/requests.go new file mode 100644 index 00000000..7d7466a0 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/monitors/requests.go @@ -0,0 +1,255 @@ +package monitors + +import ( + "fmt" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToMonitorListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the Monitor attributes you want to see returned. SortKey allows you to +// sort by a particular Monitor attribute. SortDir sets the direction, and is +// either `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + ID string `q:"id"` + Name string `q:"name"` + TenantID string `q:"tenant_id"` + ProjectID string `q:"project_id"` + PoolID string `q:"pool_id"` + Type string `q:"type"` + Delay int `q:"delay"` + Timeout int `q:"timeout"` + MaxRetries int `q:"max_retries"` + MaxRetriesDown int `q:"max_retries_down"` + HTTPMethod string `q:"http_method"` + URLPath string `q:"url_path"` + ExpectedCodes string `q:"expected_codes"` + AdminStateUp *bool `q:"admin_state_up"` + Status string `q:"status"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` +} + +// ToMonitorListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToMonitorListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + if err != nil { + return "", err + } + return q.String(), nil +} + +// List returns a Pager which allows you to iterate over a collection of +// health monitors. It accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +// +// Default policy settings return only those health monitors that are owned by the +// tenant who submits the request, unless an admin user submits the request. +func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := rootURL(c) + if opts != nil { + query, err := opts.ToMonitorListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return MonitorPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Constants that represent approved monitoring types. +const ( + TypePING = "PING" + TypeTCP = "TCP" + TypeHTTP = "HTTP" + TypeHTTPS = "HTTPS" + TypeTLSHELLO = "TLS-HELLO" + TypeUDPConnect = "UDP-CONNECT" + TypeSCTP = "SCTP" +) + +var ( + errDelayMustGETimeout = fmt.Errorf("Delay must be greater than or equal to timeout") +) + +// CreateOptsBuilder allows extensions to add additional parameters to the +// List request. +type CreateOptsBuilder interface { + ToMonitorCreateMap() (map[string]interface{}, error) +} + +// CreateOpts is the common options struct used in this package's Create +// operation. +type CreateOpts struct { + // The Pool to Monitor. + PoolID string `json:"pool_id,omitempty"` + + // The type of probe, which is PING, TCP, HTTP, or HTTPS, that is + // sent by the load balancer to verify the member state. + Type string `json:"type" required:"true"` + + // The time, in seconds, between sending probes to members. + Delay int `json:"delay" required:"true"` + + // Maximum number of seconds for a Monitor to wait for a ping reply + // before it times out. The value must be less than the delay value. + Timeout int `json:"timeout" required:"true"` + + // Number of permissible ping failures before changing the member's + // status to INACTIVE. Must be a number between 1 and 10. + MaxRetries int `json:"max_retries" required:"true"` + + // Number of permissible ping failures befor changing the member's + // status to ERROR. Must be a number between 1 and 10. + MaxRetriesDown int `json:"max_retries_down,omitempty"` + + // URI path that will be accessed if Monitor type is HTTP or HTTPS. + URLPath string `json:"url_path,omitempty"` + + // The HTTP method used for requests by the Monitor. If this attribute + // is not specified, it defaults to "GET". Required for HTTP(S) types. + HTTPMethod string `json:"http_method,omitempty"` + + // Expected HTTP codes for a passing HTTP(S) Monitor. You can either specify + // a single status like "200", a range like "200-202", or a combination like + // "200-202, 401". + ExpectedCodes string `json:"expected_codes,omitempty"` + + // TenantID is the UUID of the project who owns the Monitor. + // Only administrative users can specify a project UUID other than their own. + TenantID string `json:"tenant_id,omitempty"` + + // ProjectID is the UUID of the project who owns the Monitor. + // Only administrative users can specify a project UUID other than their own. + ProjectID string `json:"project_id,omitempty"` + + // The Name of the Monitor. + Name string `json:"name,omitempty"` + + // The administrative state of the Monitor. A valid value is true (UP) + // or false (DOWN). + AdminStateUp *bool `json:"admin_state_up,omitempty"` +} + +// ToMonitorCreateMap builds a request body from CreateOpts. +func (opts CreateOpts) ToMonitorCreateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "healthmonitor") +} + +/* +Create is an operation which provisions a new Health Monitor. There are +different types of Monitor you can provision: PING, TCP or HTTP(S). Below +are examples of how to create each one. + +Here is an example config struct to use when creating a PING or TCP Monitor: + +CreateOpts{Type: TypePING, Delay: 20, Timeout: 10, MaxRetries: 3} +CreateOpts{Type: TypeTCP, Delay: 20, Timeout: 10, MaxRetries: 3} + +Here is an example config struct to use when creating a HTTP(S) Monitor: + +CreateOpts{Type: TypeHTTP, Delay: 20, Timeout: 10, MaxRetries: 3, +HttpMethod: "HEAD", ExpectedCodes: "200", PoolID: "2c946bfc-1804-43ab-a2ff-58f6a762b505"} +*/ +func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToMonitorCreateMap() + if err != nil { + r.Err = err + return + } + resp, err := c.Post(rootURL(c), b, &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Get retrieves a particular Health Monitor based on its unique ID. +func Get(c *gophercloud.ServiceClient, id string) (r GetResult) { + resp, err := c.Get(resourceURL(c, id), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the +// Update request. +type UpdateOptsBuilder interface { + ToMonitorUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts is the common options struct used in this package's Update +// operation. +type UpdateOpts struct { + // The time, in seconds, between sending probes to members. + Delay int `json:"delay,omitempty"` + + // Maximum number of seconds for a Monitor to wait for a ping reply + // before it times out. The value must be less than the delay value. + Timeout int `json:"timeout,omitempty"` + + // Number of permissible ping failures before changing the member's + // status to INACTIVE. Must be a number between 1 and 10. + MaxRetries int `json:"max_retries,omitempty"` + + // Number of permissible ping failures befor changing the member's + // status to ERROR. Must be a number between 1 and 10. + MaxRetriesDown int `json:"max_retries_down,omitempty"` + + // URI path that will be accessed if Monitor type is HTTP or HTTPS. + // Required for HTTP(S) types. + URLPath string `json:"url_path,omitempty"` + + // The HTTP method used for requests by the Monitor. If this attribute + // is not specified, it defaults to "GET". Required for HTTP(S) types. + HTTPMethod string `json:"http_method,omitempty"` + + // Expected HTTP codes for a passing HTTP(S) Monitor. You can either specify + // a single status like "200", or a range like "200-202". Required for HTTP(S) + // types. + ExpectedCodes string `json:"expected_codes,omitempty"` + + // The Name of the Monitor. + Name *string `json:"name,omitempty"` + + // The administrative state of the Monitor. A valid value is true (UP) + // or false (DOWN). + AdminStateUp *bool `json:"admin_state_up,omitempty"` +} + +// ToMonitorUpdateMap builds a request body from UpdateOpts. +func (opts UpdateOpts) ToMonitorUpdateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "healthmonitor") +} + +// Update is an operation which modifies the attributes of the specified +// Monitor. +func Update(c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToMonitorUpdateMap() + if err != nil { + r.Err = err + return + } + + resp, err := c.Put(resourceURL(c, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 202}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Delete will permanently delete a particular Monitor based on its unique ID. +func Delete(c *gophercloud.ServiceClient, id string) (r DeleteResult) { + resp, err := c.Delete(resourceURL(c, id), nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/monitors/results.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/monitors/results.go new file mode 100644 index 00000000..502581fe --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/monitors/results.go @@ -0,0 +1,167 @@ +package monitors + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +type PoolID struct { + ID string `json:"id"` +} + +// Monitor represents a load balancer health monitor. A health monitor is used +// to determine whether or not back-end members of the VIP's pool are usable +// for processing a request. A pool can have several health monitors associated +// with it. There are different types of health monitors supported: +// +// PING: used to ping the members using ICMP. +// TCP: used to connect to the members using TCP. +// HTTP: used to send an HTTP request to the member. +// HTTPS: used to send a secure HTTP request to the member. +// TLS-HELLO: used to send TLS-HELLO request to the member. +// UDP-CONNECT: used to send UDP-CONNECT request to the member. +// SCTP: used to send SCTP request to the member. +// +// When a pool has several monitors associated with it, each member of the pool +// is monitored by all these monitors. If any monitor declares the member as +// unhealthy, then the member status is changed to INACTIVE and the member +// won't participate in its pool's load balancing. In other words, ALL monitors +// must declare the member to be healthy for it to stay ACTIVE. +type Monitor struct { + // The unique ID for the Monitor. + ID string `json:"id"` + + // The Name of the Monitor. + Name string `json:"name"` + + // The owner of the Monitor. + ProjectID string `json:"project_id"` + + // The type of probe sent by the load balancer to verify the member state, + // which is PING, TCP, HTTP, HTTPS, TLS-HELLO, UDP-CONNECT or SCTP. + Type string `json:"type"` + + // The time, in seconds, between sending probes to members. + Delay int `json:"delay"` + + // The maximum number of seconds for a monitor to wait for a connection to be + // established before it times out. This value must be less than the delay + // value. + Timeout int `json:"timeout"` + + // Number of allowed connection failures before changing the status of the + // member to INACTIVE. A valid value is from 1 to 10. + MaxRetries int `json:"max_retries"` + + // Number of allowed connection failures before changing the status of the + // member to Error. A valid value is from 1 to 10. + MaxRetriesDown int `json:"max_retries_down"` + + // The HTTP method that the monitor uses for requests. + HTTPMethod string `json:"http_method"` + + // The HTTP path of the request sent by the monitor to test the health of a + // member. Must be a string beginning with a forward slash (/). + URLPath string `json:"url_path" ` + + // Expected HTTP codes for a passing HTTP(S) monitor. + ExpectedCodes string `json:"expected_codes"` + + // The administrative state of the health monitor, which is up (true) or + // down (false). + AdminStateUp bool `json:"admin_state_up"` + + // The status of the health monitor. Indicates whether the health monitor is + // operational. + Status string `json:"status"` + + // List of pools that are associated with the health monitor. + Pools []PoolID `json:"pools"` + + // The provisioning status of the Monitor. + // This value is ACTIVE, PENDING_* or ERROR. + ProvisioningStatus string `json:"provisioning_status"` + + // The operating status of the monitor. + OperatingStatus string `json:"operating_status"` +} + +// MonitorPage is the page returned by a pager when traversing over a +// collection of health monitors. +type MonitorPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of monitors has reached +// the end of a page and the pager seeks to traverse over a new one. In order +// to do this, it needs to construct the next page's URL. +func (r MonitorPage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"healthmonitors_links"` + } + + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + + return gophercloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a MonitorPage struct is empty. +func (r MonitorPage) IsEmpty() (bool, error) { + if r.StatusCode == 204 { + return true, nil + } + + is, err := ExtractMonitors(r) + return len(is) == 0, err +} + +// ExtractMonitors accepts a Page struct, specifically a MonitorPage struct, +// and extracts the elements into a slice of Monitor structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractMonitors(r pagination.Page) ([]Monitor, error) { + var s struct { + Monitors []Monitor `json:"healthmonitors"` + } + err := (r.(MonitorPage)).ExtractInto(&s) + return s.Monitors, err +} + +type commonResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a monitor. +func (r commonResult) Extract() (*Monitor, error) { + var s struct { + Monitor *Monitor `json:"healthmonitor"` + } + err := r.ExtractInto(&s) + return s.Monitor, err +} + +// CreateResult represents the result of a create operation. Call its Extract +// method to interpret it as a Monitor. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. Call its Extract +// method to interpret it as a Monitor. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. Call its Extract +// method to interpret it as a Monitor. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. Call its +// ExtractErr method to determine if the result succeeded or failed. +type DeleteResult struct { + gophercloud.ErrResult +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/monitors/urls.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/monitors/urls.go new file mode 100644 index 00000000..a222e52a --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/monitors/urls.go @@ -0,0 +1,16 @@ +package monitors + +import "github.com/gophercloud/gophercloud" + +const ( + rootPath = "lbaas" + resourcePath = "healthmonitors" +) + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(rootPath, resourcePath) +} + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(rootPath, resourcePath, id) +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/pools/doc.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/pools/doc.go new file mode 100644 index 00000000..5657cd87 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/pools/doc.go @@ -0,0 +1,157 @@ +/* +Package pools provides information and interaction with Pools and +Members of the LBaaS v2 extension for the OpenStack Networking service. + +Example to List Pools + + listOpts := pools.ListOpts{ + LoadbalancerID: "c79a4468-d788-410c-bf79-9a8ef6354852", + } + + allPages, err := pools.List(networkClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allPools, err := pools.ExtractMonitors(allPages) + if err != nil { + panic(err) + } + + for _, pools := range allPools { + fmt.Printf("%+v\n", pool) + } + +Example to Create a Pool + + createOpts := pools.CreateOpts{ + LBMethod: pools.LBMethodRoundRobin, + Protocol: "HTTP", + Name: "Example pool", + Tags: []string{"test"}, + LoadbalancerID: "79e05663-7f03-45d2-a092-8b94062f22ab", + } + + pool, err := pools.Create(networkClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Update a Pool + + poolID := "d67d56a6-4a86-4688-a282-f46444705c64" + + newTags := []string{"prod"} + updateOpts := pools.UpdateOpts{ + Name: "new-name", + Tags: &newTags, + } + + pool, err := pools.Update(networkClient, poolID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a Pool + + poolID := "d67d56a6-4a86-4688-a282-f46444705c64" + err := pools.Delete(networkClient, poolID).ExtractErr() + if err != nil { + panic(err) + } + +Example to List Pool Members + + poolID := "d67d56a6-4a86-4688-a282-f46444705c64" + + listOpts := pools.ListMemberOpts{ + ProtocolPort: 80, + } + + allPages, err := pools.ListMembers(networkClient, poolID, listOpts).AllPages() + if err != nil { + panic(err) + } + + allMembers, err := pools.ExtractMembers(allPages) + if err != nil { + panic(err) + } + + for _, member := allMembers { + fmt.Printf("%+v\n", member) + } + +Example to Create a Member + + poolID := "d67d56a6-4a86-4688-a282-f46444705c64" + + weight := 10 + createOpts := pools.CreateMemberOpts{ + Name: "db", + SubnetID: "1981f108-3c48-48d2-b908-30f7d28532c9", + Address: "10.0.2.11", + ProtocolPort: 80, + Weight: &weight, + } + + member, err := pools.CreateMember(networkClient, poolID, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Update a Member + + poolID := "d67d56a6-4a86-4688-a282-f46444705c64" + memberID := "64dba99f-8af8-4200-8882-e32a0660f23e" + + weight := 4 + updateOpts := pools.UpdateMemberOpts{ + Name: "new-name", + Weight: &weight, + } + + member, err := pools.UpdateMember(networkClient, poolID, memberID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a Member + + poolID := "d67d56a6-4a86-4688-a282-f46444705c64" + memberID := "64dba99f-8af8-4200-8882-e32a0660f23e" + + err := pools.DeleteMember(networkClient, poolID, memberID).ExtractErr() + if err != nil { + panic(err) + } + +Example to Update Members: + + poolID := "d67d56a6-4a86-4688-a282-f46444705c64" + + weight_1 := 20 + member1 := pools.BatchUpdateMemberOpts{ + Address: "192.0.2.16", + ProtocolPort: 80, + Name: "web-server-1", + SubnetID: "bbb35f84-35cc-4b2f-84c2-a6a29bba68aa", + Weight: &weight_1, + } + + weight_2 := 10 + member2 := pools.BatchUpdateMemberOpts{ + Address: "192.0.2.17", + ProtocolPort: 80, + Name: "web-server-2", + Weight: &weight_2, + SubnetID: "bbb35f84-35cc-4b2f-84c2-a6a29bba68aa", + } + members := []pools.BatchUpdateMemberOpts{member1, member2} + + err := pools.BatchUpdateMembers(networkClient, poolID, members).ExtractErr() + if err != nil { + panic(err) + } +*/ +package pools diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/pools/requests.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/pools/requests.go new file mode 100644 index 00000000..69e6a2a7 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/pools/requests.go @@ -0,0 +1,499 @@ +package pools + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/monitors" + "github.com/gophercloud/gophercloud/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToPoolListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the Pool attributes you want to see returned. SortKey allows you to +// sort by a particular Pool attribute. SortDir sets the direction, and is +// either `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + LBMethod string `q:"lb_algorithm"` + Protocol string `q:"protocol"` + ProjectID string `q:"project_id"` + AdminStateUp *bool `q:"admin_state_up"` + Name string `q:"name"` + ID string `q:"id"` + LoadbalancerID string `q:"loadbalancer_id"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` +} + +// ToPoolListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToPoolListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns a Pager which allows you to iterate over a collection of +// pools. It accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +// +// Default policy settings return only those pools that are owned by the +// project who submits the request, unless an admin user submits the request. +func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := rootURL(c) + if opts != nil { + query, err := opts.ToPoolListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return PoolPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +type LBMethod string +type Protocol string + +// Supported attributes for create/update operations. +const ( + LBMethodRoundRobin LBMethod = "ROUND_ROBIN" + LBMethodLeastConnections LBMethod = "LEAST_CONNECTIONS" + LBMethodSourceIp LBMethod = "SOURCE_IP" + LBMethodSourceIpPort LBMethod = "SOURCE_IP_PORT" + + ProtocolTCP Protocol = "TCP" + ProtocolUDP Protocol = "UDP" + ProtocolPROXY Protocol = "PROXY" + ProtocolHTTP Protocol = "HTTP" + ProtocolHTTPS Protocol = "HTTPS" + // Protocol PROXYV2 requires octavia microversion 2.22 + ProtocolPROXYV2 Protocol = "PROXYV2" + // Protocol SCTP requires octavia microversion 2.23 + ProtocolSCTP Protocol = "SCTP" +) + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToPoolCreateMap() (map[string]interface{}, error) +} + +// CreateOpts is the common options struct used in this package's Create +// operation. +type CreateOpts struct { + // The algorithm used to distribute load between the members of the pool. The + // current specification supports LBMethodRoundRobin, LBMethodLeastConnections, + // LBMethodSourceIp and LBMethodSourceIpPort as valid values for this attribute. + LBMethod LBMethod `json:"lb_algorithm" required:"true"` + + // The protocol used by the pool members, you can use either + // ProtocolTCP, ProtocolUDP, ProtocolPROXY, ProtocolHTTP, ProtocolHTTPS, + // ProtocolSCTP or ProtocolPROXYV2. + Protocol Protocol `json:"protocol" required:"true"` + + // The Loadbalancer on which the members of the pool will be associated with. + // Note: one of LoadbalancerID or ListenerID must be provided. + LoadbalancerID string `json:"loadbalancer_id,omitempty"` + + // The Listener on which the members of the pool will be associated with. + // Note: one of LoadbalancerID or ListenerID must be provided. + ListenerID string `json:"listener_id,omitempty"` + + // ProjectID is the UUID of the project who owns the Pool. + // Only administrative users can specify a project UUID other than their own. + ProjectID string `json:"project_id,omitempty"` + + // Name of the pool. + Name string `json:"name,omitempty"` + + // Human-readable description for the pool. + Description string `json:"description,omitempty"` + + // Persistence is the session persistence of the pool. + // Omit this field to prevent session persistence. + Persistence *SessionPersistence `json:"session_persistence,omitempty"` + + // The administrative state of the Pool. A valid value is true (UP) + // or false (DOWN). + AdminStateUp *bool `json:"admin_state_up,omitempty"` + + // Members is a slice of BatchUpdateMemberOpts which allows a set of + // members to be created at the same time the pool is created. + // + // This is only possible to use when creating a fully populated + // Loadbalancer. + Members []BatchUpdateMemberOpts `json:"members,omitempty"` + + // Monitor is an instance of monitors.CreateOpts which allows a monitor + // to be created at the same time the pool is created. + // + // This is only possible to use when creating a fully populated + // Loadbalancer. + Monitor *monitors.CreateOpts `json:"healthmonitor,omitempty"` + + // Tags is a set of resource tags. New in version 2.5 + Tags []string `json:"tags,omitempty"` +} + +// ToPoolCreateMap builds a request body from CreateOpts. +func (opts CreateOpts) ToPoolCreateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "pool") +} + +// Create accepts a CreateOpts struct and uses the values to create a new +// load balancer pool. +func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToPoolCreateMap() + if err != nil { + r.Err = err + return + } + resp, err := c.Post(rootURL(c), b, &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Get retrieves a particular pool based on its unique ID. +func Get(c *gophercloud.ServiceClient, id string) (r GetResult) { + resp, err := c.Get(resourceURL(c, id), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the +// Update request. +type UpdateOptsBuilder interface { + ToPoolUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts is the common options struct used in this package's Update +// operation. +type UpdateOpts struct { + // Name of the pool. + Name *string `json:"name,omitempty"` + + // Human-readable description for the pool. + Description *string `json:"description,omitempty"` + + // The algorithm used to distribute load between the members of the pool. The + // current specification supports LBMethodRoundRobin, LBMethodLeastConnections, + // LBMethodSourceIp and LBMethodSourceIpPort as valid values for this attribute. + LBMethod LBMethod `json:"lb_algorithm,omitempty"` + + // The administrative state of the Pool. A valid value is true (UP) + // or false (DOWN). + AdminStateUp *bool `json:"admin_state_up,omitempty"` + + // Persistence is the session persistence of the pool. + Persistence *SessionPersistence `json:"session_persistence,omitempty"` + + // Tags is a set of resource tags. New in version 2.5 + Tags *[]string `json:"tags,omitempty"` +} + +// ToPoolUpdateMap builds a request body from UpdateOpts. +func (opts UpdateOpts) ToPoolUpdateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "pool") +} + +// Update allows pools to be updated. +func Update(c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToPoolUpdateMap() + if err != nil { + r.Err = err + return + } + resp, err := c.Put(resourceURL(c, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Delete will permanently delete a particular pool based on its unique ID. +func Delete(c *gophercloud.ServiceClient, id string) (r DeleteResult) { + resp, err := c.Delete(resourceURL(c, id), nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// ListMemberOptsBuilder allows extensions to add additional parameters to the +// ListMembers request. +type ListMembersOptsBuilder interface { + ToMembersListQuery() (string, error) +} + +// ListMembersOpts allows the filtering and sorting of paginated collections +// through the API. Filtering is achieved by passing in struct field values +// that map to the Member attributes you want to see returned. SortKey allows +// you to sort by a particular Member attribute. SortDir sets the direction, +// and is either `asc' or `desc'. Marker and Limit are used for pagination. +type ListMembersOpts struct { + Name string `q:"name"` + Weight int `q:"weight"` + AdminStateUp *bool `q:"admin_state_up"` + ProjectID string `q:"project_id"` + Address string `q:"address"` + ProtocolPort int `q:"protocol_port"` + ID string `q:"id"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` +} + +// ToMemberListQuery formats a ListOpts into a query string. +func (opts ListMembersOpts) ToMembersListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// ListMembers returns a Pager which allows you to iterate over a collection of +// members. It accepts a ListMembersOptsBuilder, which allows you to filter and +// sort the returned collection for greater efficiency. +// +// Default policy settings return only those members that are owned by the +// project who submits the request, unless an admin user submits the request. +func ListMembers(c *gophercloud.ServiceClient, poolID string, opts ListMembersOptsBuilder) pagination.Pager { + url := memberRootURL(c, poolID) + if opts != nil { + query, err := opts.ToMembersListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return MemberPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// CreateMemberOptsBuilder allows extensions to add additional parameters to the +// CreateMember request. +type CreateMemberOptsBuilder interface { + ToMemberCreateMap() (map[string]interface{}, error) +} + +// CreateMemberOpts is the common options struct used in this package's CreateMember +// operation. +type CreateMemberOpts struct { + // The IP address of the member to receive traffic from the load balancer. + Address string `json:"address" required:"true"` + + // The port on which to listen for client traffic. + ProtocolPort int `json:"protocol_port" required:"true"` + + // Name of the Member. + Name string `json:"name,omitempty"` + + // ProjectID is the UUID of the project who owns the Member. + // Only administrative users can specify a project UUID other than their own. + ProjectID string `json:"project_id,omitempty"` + + // A positive integer value that indicates the relative portion of traffic + // that this member should receive from the pool. For example, a member with + // a weight of 10 receives five times as much traffic as a member with a + // weight of 2. + Weight *int `json:"weight,omitempty"` + + // If you omit this parameter, LBaaS uses the vip_subnet_id parameter value + // for the subnet UUID. + SubnetID string `json:"subnet_id,omitempty"` + + // The administrative state of the Pool. A valid value is true (UP) + // or false (DOWN). + AdminStateUp *bool `json:"admin_state_up,omitempty"` + + // Is the member a backup? Backup members only receive traffic when all + // non-backup members are down. + // Requires microversion 2.1 or later. + Backup *bool `json:"backup,omitempty"` + + // An alternate IP address used for health monitoring a backend member. + MonitorAddress string `json:"monitor_address,omitempty"` + + // An alternate protocol port used for health monitoring a backend member. + MonitorPort *int `json:"monitor_port,omitempty"` + + // A list of simple strings assigned to the resource. + // Requires microversion 2.5 or later. + Tags []string `json:"tags,omitempty"` +} + +// ToMemberCreateMap builds a request body from CreateMemberOpts. +func (opts CreateMemberOpts) ToMemberCreateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "member") +} + +// CreateMember will create and associate a Member with a particular Pool. +func CreateMember(c *gophercloud.ServiceClient, poolID string, opts CreateMemberOptsBuilder) (r CreateMemberResult) { + b, err := opts.ToMemberCreateMap() + if err != nil { + r.Err = err + return + } + resp, err := c.Post(memberRootURL(c, poolID), b, &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// GetMember retrieves a particular Pool Member based on its unique ID. +func GetMember(c *gophercloud.ServiceClient, poolID string, memberID string) (r GetMemberResult) { + resp, err := c.Get(memberResourceURL(c, poolID, memberID), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// UpdateMemberOptsBuilder allows extensions to add additional parameters to the +// List request. +type UpdateMemberOptsBuilder interface { + ToMemberUpdateMap() (map[string]interface{}, error) +} + +// UpdateMemberOpts is the common options struct used in this package's Update +// operation. +type UpdateMemberOpts struct { + // Name of the Member. + Name *string `json:"name,omitempty"` + + // A positive integer value that indicates the relative portion of traffic + // that this member should receive from the pool. For example, a member with + // a weight of 10 receives five times as much traffic as a member with a + // weight of 2. + Weight *int `json:"weight,omitempty"` + + // The administrative state of the Pool. A valid value is true (UP) + // or false (DOWN). + AdminStateUp *bool `json:"admin_state_up,omitempty"` + + // Is the member a backup? Backup members only receive traffic when all + // non-backup members are down. + // Requires microversion 2.1 or later. + Backup *bool `json:"backup,omitempty"` + + // An alternate IP address used for health monitoring a backend member. + MonitorAddress *string `json:"monitor_address,omitempty"` + + // An alternate protocol port used for health monitoring a backend member. + MonitorPort *int `json:"monitor_port,omitempty"` + + // A list of simple strings assigned to the resource. + // Requires microversion 2.5 or later. + Tags []string `json:"tags,omitempty"` +} + +// ToMemberUpdateMap builds a request body from UpdateMemberOpts. +func (opts UpdateMemberOpts) ToMemberUpdateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "member") +} + +// Update allows Member to be updated. +func UpdateMember(c *gophercloud.ServiceClient, poolID string, memberID string, opts UpdateMemberOptsBuilder) (r UpdateMemberResult) { + b, err := opts.ToMemberUpdateMap() + if err != nil { + r.Err = err + return + } + resp, err := c.Put(memberResourceURL(c, poolID, memberID), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 201, 202}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// BatchUpdateMemberOptsBuilder allows extensions to add additional parameters to the BatchUpdateMembers request. +type BatchUpdateMemberOptsBuilder interface { + ToBatchMemberUpdateMap() (map[string]interface{}, error) +} + +// BatchUpdateMemberOpts is the common options struct used in this package's BatchUpdateMembers +// operation. +type BatchUpdateMemberOpts struct { + // The IP address of the member to receive traffic from the load balancer. + Address string `json:"address" required:"true"` + + // The port on which to listen for client traffic. + ProtocolPort int `json:"protocol_port" required:"true"` + + // Name of the Member. + Name *string `json:"name,omitempty"` + + // ProjectID is the UUID of the project who owns the Member. + // Only administrative users can specify a project UUID other than their own. + ProjectID string `json:"project_id,omitempty"` + + // A positive integer value that indicates the relative portion of traffic + // that this member should receive from the pool. For example, a member with + // a weight of 10 receives five times as much traffic as a member with a + // weight of 2. + Weight *int `json:"weight,omitempty"` + + // If you omit this parameter, LBaaS uses the vip_subnet_id parameter value + // for the subnet UUID. + SubnetID *string `json:"subnet_id,omitempty"` + + // The administrative state of the Pool. A valid value is true (UP) + // or false (DOWN). + AdminStateUp *bool `json:"admin_state_up,omitempty"` + + // Is the member a backup? Backup members only receive traffic when all + // non-backup members are down. + // Requires microversion 2.1 or later. + Backup *bool `json:"backup,omitempty"` + + // An alternate IP address used for health monitoring a backend member. + MonitorAddress *string `json:"monitor_address,omitempty"` + + // An alternate protocol port used for health monitoring a backend member. + MonitorPort *int `json:"monitor_port,omitempty"` + + // A list of simple strings assigned to the resource. + // Requires microversion 2.5 or later. + Tags []string `json:"tags,omitempty"` +} + +// ToBatchMemberUpdateMap builds a request body from BatchUpdateMemberOpts. +func (opts BatchUpdateMemberOpts) ToBatchMemberUpdateMap() (map[string]interface{}, error) { + b, err := gophercloud.BuildRequestBody(opts, "") + if err != nil { + return nil, err + } + + if b["subnet_id"] == "" { + b["subnet_id"] = nil + } + + return b, nil +} + +// BatchUpdateMembers updates the pool members in batch +func BatchUpdateMembers(c *gophercloud.ServiceClient, poolID string, opts []BatchUpdateMemberOpts) (r UpdateMembersResult) { + members := []map[string]interface{}{} + for _, opt := range opts { + b, err := opt.ToBatchMemberUpdateMap() + if err != nil { + r.Err = err + return + } + members = append(members, b) + } + + b := map[string]interface{}{"members": members} + + resp, err := c.Put(memberRootURL(c, poolID), b, nil, &gophercloud.RequestOpts{OkCodes: []int{202}}) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// DeleteMember will remove and disassociate a Member from a particular Pool. +func DeleteMember(c *gophercloud.ServiceClient, poolID string, memberID string) (r DeleteMemberResult) { + resp, err := c.Delete(memberResourceURL(c, poolID, memberID), nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/pools/results.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/pools/results.go new file mode 100644 index 00000000..44379b6f --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/pools/results.go @@ -0,0 +1,349 @@ +package pools + +import ( + "encoding/json" + "time" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/monitors" + "github.com/gophercloud/gophercloud/pagination" +) + +// SessionPersistence represents the session persistence feature of the load +// balancing service. It attempts to force connections or requests in the same +// session to be processed by the same member as long as it is ative. Three +// types of persistence are supported: +// +// SOURCE_IP: With this mode, all connections originating from the same source +// +// IP address, will be handled by the same Member of the Pool. +// +// HTTP_COOKIE: With this persistence mode, the load balancing function will +// +// create a cookie on the first request from a client. Subsequent +// requests containing the same cookie value will be handled by +// the same Member of the Pool. +// +// APP_COOKIE: With this persistence mode, the load balancing function will +// +// rely on a cookie established by the backend application. All +// requests carrying the same cookie value will be handled by the +// same Member of the Pool. +type SessionPersistence struct { + // The type of persistence mode. + Type string `json:"type"` + + // Name of cookie if persistence mode is set appropriately. + CookieName string `json:"cookie_name,omitempty"` +} + +// LoadBalancerID represents a load balancer. +type LoadBalancerID struct { + ID string `json:"id"` +} + +// ListenerID represents a listener. +type ListenerID struct { + ID string `json:"id"` +} + +// Pool represents a logical set of devices, such as web servers, that you +// group together to receive and process traffic. The load balancing function +// chooses a Member of the Pool according to the configured load balancing +// method to handle the new requests or connections received on the VIP address. +type Pool struct { + // The load-balancer algorithm, which is round-robin, least-connections, and + // so on. This value, which must be supported, is dependent on the provider. + // Round-robin must be supported. + LBMethod string `json:"lb_algorithm"` + + // The protocol of the Pool, which is TCP, HTTP, or HTTPS. + Protocol string `json:"protocol"` + + // Description for the Pool. + Description string `json:"description"` + + // A list of listeners objects IDs. + Listeners []ListenerID `json:"listeners"` //[]map[string]interface{} + + // A list of member objects IDs. + Members []Member `json:"members"` + + // The ID of associated health monitor. + MonitorID string `json:"healthmonitor_id"` + + // The network on which the members of the Pool will be located. Only members + // that are on this network can be added to the Pool. + SubnetID string `json:"subnet_id"` + + // Owner of the Pool. + ProjectID string `json:"project_id"` + + // The administrative state of the Pool, which is up (true) or down (false). + AdminStateUp bool `json:"admin_state_up"` + + // Pool name. Does not have to be unique. + Name string `json:"name"` + + // The unique ID for the Pool. + ID string `json:"id"` + + // A list of load balancer objects IDs. + Loadbalancers []LoadBalancerID `json:"loadbalancers"` + + // Indicates whether connections in the same session will be processed by the + // same Pool member or not. + Persistence SessionPersistence `json:"session_persistence"` + + // The load balancer provider. + Provider string `json:"provider"` + + // The Monitor associated with this Pool. + Monitor monitors.Monitor `json:"healthmonitor"` + + // The provisioning status of the pool. + // This value is ACTIVE, PENDING_* or ERROR. + ProvisioningStatus string `json:"provisioning_status"` + + // The operating status of the pool. + OperatingStatus string `json:"operating_status"` + + // Tags is a list of resource tags. Tags are arbitrarily defined strings + // attached to the resource. New in version 2.5 + Tags []string `json:"tags"` +} + +// PoolPage is the page returned by a pager when traversing over a +// collection of pools. +type PoolPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of pools has reached +// the end of a page and the pager seeks to traverse over a new one. In order +// to do this, it needs to construct the next page's URL. +func (r PoolPage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"pools_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a PoolPage struct is empty. +func (r PoolPage) IsEmpty() (bool, error) { + if r.StatusCode == 204 { + return true, nil + } + + is, err := ExtractPools(r) + return len(is) == 0, err +} + +// ExtractPools accepts a Page struct, specifically a PoolPage struct, +// and extracts the elements into a slice of Pool structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractPools(r pagination.Page) ([]Pool, error) { + var s struct { + Pools []Pool `json:"pools"` + } + err := (r.(PoolPage)).ExtractInto(&s) + return s.Pools, err +} + +type commonResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a pool. +func (r commonResult) Extract() (*Pool, error) { + var s struct { + Pool *Pool `json:"pool"` + } + err := r.ExtractInto(&s) + return s.Pool, err +} + +// CreateResult represents the result of a Create operation. Call its Extract +// method to interpret the result as a Pool. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a Get operation. Call its Extract +// method to interpret the result as a Pool. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an Update operation. Call its Extract +// method to interpret the result as a Pool. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a Delete operation. Call its +// ExtractErr method to determine if the request succeeded or failed. +type DeleteResult struct { + gophercloud.ErrResult +} + +// Member represents the application running on a backend server. +type Member struct { + // Name of the Member. + Name string `json:"name"` + + // Weight of Member. + Weight int `json:"weight"` + + // The administrative state of the member, which is up (true) or down (false). + AdminStateUp bool `json:"admin_state_up"` + + // Owner of the Member. + ProjectID string `json:"project_id"` + + // Parameter value for the subnet UUID. + SubnetID string `json:"subnet_id"` + + // The Pool to which the Member belongs. + PoolID string `json:"pool_id"` + + // The IP address of the Member. + Address string `json:"address"` + + // The port on which the application is hosted. + ProtocolPort int `json:"protocol_port"` + + // The unique ID for the Member. + ID string `json:"id"` + + // The provisioning status of the pool. + // This value is ACTIVE, PENDING_* or ERROR. + ProvisioningStatus string `json:"provisioning_status"` + + // DateTime when the member was created + CreatedAt time.Time `json:"-"` + + // DateTime when the member was updated + UpdatedAt time.Time `json:"-"` + + // The operating status of the member + OperatingStatus string `json:"operating_status"` + + // Is the member a backup? Backup members only receive traffic when all non-backup members are down. + Backup bool `json:"backup"` + + // An alternate IP address used for health monitoring a backend member. + MonitorAddress string `json:"monitor_address"` + + // An alternate protocol port used for health monitoring a backend member. + MonitorPort int `json:"monitor_port"` + + // A list of simple strings assigned to the resource. + // Requires microversion 2.5 or later. + Tags []string `json:"tags"` +} + +// MemberPage is the page returned by a pager when traversing over a +// collection of Members in a Pool. +type MemberPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of members has reached +// the end of a page and the pager seeks to traverse over a new one. In order +// to do this, it needs to construct the next page's URL. +func (r MemberPage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"members_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a MemberPage struct is empty. +func (r MemberPage) IsEmpty() (bool, error) { + if r.StatusCode == 204 { + return true, nil + } + + is, err := ExtractMembers(r) + return len(is) == 0, err +} + +// ExtractMembers accepts a Page struct, specifically a MemberPage struct, +// and extracts the elements into a slice of Members structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractMembers(r pagination.Page) ([]Member, error) { + var s struct { + Members []Member `json:"members"` + } + err := (r.(MemberPage)).ExtractInto(&s) + return s.Members, err +} + +type commonMemberResult struct { + gophercloud.Result +} + +func (r *Member) UnmarshalJSON(b []byte) error { + type tmp Member + var s struct { + tmp + CreatedAt gophercloud.JSONRFC3339NoZ `json:"created_at"` + UpdatedAt gophercloud.JSONRFC3339NoZ `json:"updated_at"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = Member(s.tmp) + r.CreatedAt = time.Time(s.CreatedAt) + r.UpdatedAt = time.Time(s.UpdatedAt) + return nil +} + +// ExtractMember is a function that accepts a result and extracts a member. +func (r commonMemberResult) Extract() (*Member, error) { + var s struct { + Member *Member `json:"member"` + } + err := r.ExtractInto(&s) + return s.Member, err +} + +// CreateMemberResult represents the result of a CreateMember operation. +// Call its Extract method to interpret it as a Member. +type CreateMemberResult struct { + commonMemberResult +} + +// GetMemberResult represents the result of a GetMember operation. +// Call its Extract method to interpret it as a Member. +type GetMemberResult struct { + commonMemberResult +} + +// UpdateMemberResult represents the result of an UpdateMember operation. +// Call its Extract method to interpret it as a Member. +type UpdateMemberResult struct { + commonMemberResult +} + +// UpdateMembersResult represents the result of an UpdateMembers operation. +// Call its ExtractErr method to determine if the request succeeded or failed. +type UpdateMembersResult struct { + gophercloud.ErrResult +} + +// DeleteMemberResult represents the result of a DeleteMember operation. +// Call its ExtractErr method to determine if the request succeeded or failed. +type DeleteMemberResult struct { + gophercloud.ErrResult +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/pools/urls.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/pools/urls.go new file mode 100644 index 00000000..e7443c4f --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/pools/urls.go @@ -0,0 +1,25 @@ +package pools + +import "github.com/gophercloud/gophercloud" + +const ( + rootPath = "lbaas" + resourcePath = "pools" + memberPath = "members" +) + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(rootPath, resourcePath) +} + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(rootPath, resourcePath, id) +} + +func memberRootURL(c *gophercloud.ServiceClient, poolId string) string { + return c.ServiceURL(rootPath, resourcePath, poolId, memberPath) +} + +func memberResourceURL(c *gophercloud.ServiceClient, poolID string, memberID string) string { + return c.ServiceURL(rootPath, resourcePath, poolID, memberPath, memberID) +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/doc.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/doc.go new file mode 100644 index 00000000..a71a3ec8 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/doc.go @@ -0,0 +1,71 @@ +/* +package floatingips enables management and retrieval of Floating IPs from the +OpenStack Networking service. + +Example to List Floating IPs + + listOpts := floatingips.ListOpts{ + FloatingNetworkID: "a6917946-38ab-4ffd-a55a-26c0980ce5ee", + } + + allPages, err := floatingips.List(networkClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allFIPs, err := floatingips.ExtractFloatingIPs(allPages) + if err != nil { + panic(err) + } + + for _, fip := range allFIPs { + fmt.Printf("%+v\n", fip) + } + +Example to Create a Floating IP + + createOpts := floatingips.CreateOpts{ + FloatingNetworkID: "a6917946-38ab-4ffd-a55a-26c0980ce5ee", + } + + fip, err := floatingips.Create(networkingClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Update a Floating IP + + fipID := "2f245a7b-796b-4f26-9cf9-9e82d248fda7" + portID := "76d0a61b-b8e5-490c-9892-4cf674f2bec8" + + updateOpts := floatingips.UpdateOpts{ + PortID: &portID, + } + + fip, err := floatingips.Update(networkingClient, fipID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Disassociate a Floating IP with a Port + + fipID := "2f245a7b-796b-4f26-9cf9-9e82d248fda7" + + updateOpts := floatingips.UpdateOpts{ + PortID: new(string), + } + + fip, err := floatingips.Update(networkingClient, fipID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a Floating IP + + fipID := "2f245a7b-796b-4f26-9cf9-9e82d248fda7" + err := floatingips.Delete(networkClient, fipID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package floatingips diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/requests.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/requests.go new file mode 100644 index 00000000..2a4ff1ac --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/requests.go @@ -0,0 +1,186 @@ +package floatingips + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToFloatingIPListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the floating IP attributes you want to see returned. SortKey allows you to +// sort by a particular network attribute. SortDir sets the direction, and is +// either `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + ID string `q:"id"` + Description string `q:"description"` + FloatingNetworkID string `q:"floating_network_id"` + PortID string `q:"port_id"` + FixedIP string `q:"fixed_ip_address"` + FloatingIP string `q:"floating_ip_address"` + TenantID string `q:"tenant_id"` + ProjectID string `q:"project_id"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` + RouterID string `q:"router_id"` + Status string `q:"status"` + Tags string `q:"tags"` + TagsAny string `q:"tags-any"` + NotTags string `q:"not-tags"` + NotTagsAny string `q:"not-tags-any"` +} + +// ToNetworkListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToFloatingIPListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns a Pager which allows you to iterate over a collection of +// floating IP resources. It accepts a ListOpts struct, which allows you to +// filter and sort the returned collection for greater efficiency. +func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := rootURL(c) + if opts != nil { + query, err := opts.ToFloatingIPListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return FloatingIPPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToFloatingIPCreateMap() (map[string]interface{}, error) +} + +// CreateOpts contains all the values needed to create a new floating IP +// resource. The only required fields are FloatingNetworkID and PortID which +// refer to the external network and internal port respectively. +type CreateOpts struct { + Description string `json:"description,omitempty"` + FloatingNetworkID string `json:"floating_network_id" required:"true"` + FloatingIP string `json:"floating_ip_address,omitempty"` + PortID string `json:"port_id,omitempty"` + FixedIP string `json:"fixed_ip_address,omitempty"` + SubnetID string `json:"subnet_id,omitempty"` + TenantID string `json:"tenant_id,omitempty"` + ProjectID string `json:"project_id,omitempty"` +} + +// ToFloatingIPCreateMap allows CreateOpts to satisfy the CreateOptsBuilder +// interface +func (opts CreateOpts) ToFloatingIPCreateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "floatingip") +} + +// Create accepts a CreateOpts struct and uses the values provided to create a +// new floating IP resource. You can create floating IPs on external networks +// only. If you provide a FloatingNetworkID which refers to a network that is +// not external (i.e. its `router:external' attribute is False), the operation +// will fail and return a 400 error. +// +// If you do not specify a FloatingIP address value, the operation will +// automatically allocate an available address for the new resource. If you do +// choose to specify one, it must fall within the subnet range for the external +// network - otherwise the operation returns a 400 error. If the FloatingIP +// address is already in use, the operation returns a 409 error code. +// +// You can associate the new resource with an internal port by using the PortID +// field. If you specify a PortID that is not valid, the operation will fail and +// return 404 error code. +// +// You must also configure an IP address for the port associated with the PortID +// you have provided - this is what the FixedIP refers to: an IP fixed to a +// port. Because a port might be associated with multiple IP addresses, you can +// use the FixedIP field to associate a particular IP address rather than have +// the API assume for you. If you specify an IP address that is not valid, the +// operation will fail and return a 400 error code. If the PortID and FixedIP +// are already associated with another resource, the operation will fail and +// returns a 409 error code. +func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToFloatingIPCreateMap() + if err != nil { + r.Err = err + return + } + resp, err := c.Post(rootURL(c), b, &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Get retrieves a particular floating IP resource based on its unique ID. +func Get(c *gophercloud.ServiceClient, id string) (r GetResult) { + resp, err := c.Get(resourceURL(c, id), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the +// Update request. +type UpdateOptsBuilder interface { + ToFloatingIPUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts contains the values used when updating a floating IP resource. The +// only value that can be updated is which internal port the floating IP is +// linked to. To associate the floating IP with a new internal port, provide its +// ID. To disassociate the floating IP from all ports, provide an empty string. +type UpdateOpts struct { + Description *string `json:"description,omitempty"` + PortID *string `json:"port_id,omitempty"` + FixedIP string `json:"fixed_ip_address,omitempty"` +} + +// ToFloatingIPUpdateMap allows UpdateOpts to satisfy the UpdateOptsBuilder +// interface +func (opts UpdateOpts) ToFloatingIPUpdateMap() (map[string]interface{}, error) { + b, err := gophercloud.BuildRequestBody(opts, "floatingip") + if err != nil { + return nil, err + } + + if m := b["floatingip"].(map[string]interface{}); m["port_id"] == "" { + m["port_id"] = nil + } + + return b, nil +} + +// Update allows floating IP resources to be updated. Currently, the only way to +// "update" a floating IP is to associate it with a new internal port, or +// disassociated it from all ports. See UpdateOpts for instructions of how to +// do this. +func Update(c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToFloatingIPUpdateMap() + if err != nil { + r.Err = err + return + } + resp, err := c.Put(resourceURL(c, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Delete will permanently delete a particular floating IP resource. Please +// ensure this is what you want - you can also disassociate the IP from existing +// internal ports. +func Delete(c *gophercloud.ServiceClient, id string) (r DeleteResult) { + resp, err := c.Delete(resourceURL(c, id), nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/results.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/results.go new file mode 100644 index 00000000..c4c019bc --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/results.go @@ -0,0 +1,181 @@ +package floatingips + +import ( + "encoding/json" + "time" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// FloatingIP represents a floating IP resource. A floating IP is an external +// IP address that is mapped to an internal port and, optionally, a specific +// IP address on a private network. In other words, it enables access to an +// instance on a private network from an external network. For this reason, +// floating IPs can only be defined on networks where the `router:external' +// attribute (provided by the external network extension) is set to True. +type FloatingIP struct { + // ID is the unique identifier for the floating IP instance. + ID string `json:"id"` + + // Description for the floating IP instance. + Description string `json:"description"` + + // FloatingNetworkID is the UUID of the external network where the floating + // IP is to be created. + FloatingNetworkID string `json:"floating_network_id"` + + // FloatingIP is the address of the floating IP on the external network. + FloatingIP string `json:"floating_ip_address"` + + // PortID is the UUID of the port on an internal network that is associated + // with the floating IP. + PortID string `json:"port_id"` + + // FixedIP is the specific IP address of the internal port which should be + // associated with the floating IP. + FixedIP string `json:"fixed_ip_address"` + + // TenantID is the project owner of the floating IP. Only admin users can + // specify a project identifier other than its own. + TenantID string `json:"tenant_id"` + + // UpdatedAt and CreatedAt contain ISO-8601 timestamps of when the state of + // the floating ip last changed, and when it was created. + UpdatedAt time.Time `json:"-"` + CreatedAt time.Time `json:"-"` + + // ProjectID is the project owner of the floating IP. + ProjectID string `json:"project_id"` + + // Status is the condition of the API resource. + Status string `json:"status"` + + // RouterID is the ID of the router used for this floating IP. + RouterID string `json:"router_id"` + + // Tags optionally set via extensions/attributestags + Tags []string `json:"tags"` +} + +func (r *FloatingIP) UnmarshalJSON(b []byte) error { + type tmp FloatingIP + + // Support for older neutron time format + var s1 struct { + tmp + CreatedAt gophercloud.JSONRFC3339NoZ `json:"created_at"` + UpdatedAt gophercloud.JSONRFC3339NoZ `json:"updated_at"` + } + + err := json.Unmarshal(b, &s1) + if err == nil { + *r = FloatingIP(s1.tmp) + r.CreatedAt = time.Time(s1.CreatedAt) + r.UpdatedAt = time.Time(s1.UpdatedAt) + + return nil + } + + // Support for newer neutron time format + var s2 struct { + tmp + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + } + + err = json.Unmarshal(b, &s2) + if err != nil { + return err + } + + *r = FloatingIP(s2.tmp) + r.CreatedAt = time.Time(s2.CreatedAt) + r.UpdatedAt = time.Time(s2.UpdatedAt) + + return nil +} + +type commonResult struct { + gophercloud.Result +} + +// Extract will extract a FloatingIP resource from a result. +func (r commonResult) Extract() (*FloatingIP, error) { + var s FloatingIP + err := r.ExtractInto(&s) + return &s, err +} + +func (r commonResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "floatingip") +} + +// CreateResult represents the result of a create operation. Call its Extract +// method to interpret it as a FloatingIP. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. Call its Extract +// method to interpret it as a FloatingIP. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. Call its Extract +// method to interpret it as a FloatingIP. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of an update operation. Call its +// ExtractErr method to determine if the request succeeded or failed. +type DeleteResult struct { + gophercloud.ErrResult +} + +// FloatingIPPage is the page returned by a pager when traversing over a +// collection of floating IPs. +type FloatingIPPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of floating IPs has +// reached the end of a page and the pager seeks to traverse over a new one. +// In order to do this, it needs to construct the next page's URL. +func (r FloatingIPPage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"floatingips_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a FloatingIPPage struct is empty. +func (r FloatingIPPage) IsEmpty() (bool, error) { + if r.StatusCode == 204 { + return true, nil + } + + is, err := ExtractFloatingIPs(r) + return len(is) == 0, err +} + +// ExtractFloatingIPs accepts a Page struct, specifically a FloatingIPPage +// struct, and extracts the elements into a slice of FloatingIP structs. In +// other words, a generic collection is mapped into a relevant slice. +func ExtractFloatingIPs(r pagination.Page) ([]FloatingIP, error) { + var s struct { + FloatingIPs []FloatingIP `json:"floatingips"` + } + err := (r.(FloatingIPPage)).ExtractInto(&s) + return s.FloatingIPs, err +} + +func ExtractFloatingIPsInto(r pagination.Page, v interface{}) error { + return r.(FloatingIPPage).Result.ExtractIntoSlicePtr(v, "floatingips") +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/urls.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/urls.go new file mode 100644 index 00000000..1318a184 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips/urls.go @@ -0,0 +1,13 @@ +package floatingips + +import "github.com/gophercloud/gophercloud" + +const resourcePath = "floatingips" + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(resourcePath) +} + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(resourcePath, id) +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/groups/doc.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/groups/doc.go new file mode 100644 index 00000000..7d8bbcaa --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/groups/doc.go @@ -0,0 +1,58 @@ +/* +Package groups provides information and interaction with Security Groups +for the OpenStack Networking service. + +Example to List Security Groups + + listOpts := groups.ListOpts{ + TenantID: "966b3c7d36a24facaf20b7e458bf2192", + } + + allPages, err := groups.List(networkClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allGroups, err := groups.ExtractGroups(allPages) + if err != nil { + panic(err) + } + + for _, group := range allGroups { + fmt.Printf("%+v\n", group) + } + +Example to Create a Security Group + + createOpts := groups.CreateOpts{ + Name: "group_name", + Description: "A Security Group", + } + + group, err := groups.Create(networkClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Update a Security Group + + groupID := "37d94f8a-d136-465c-ae46-144f0d8ef141" + + updateOpts := groups.UpdateOpts{ + Name: "new_name", + } + + group, err := groups.Update(networkClient, groupID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a Security Group + + groupID := "37d94f8a-d136-465c-ae46-144f0d8ef141" + err := groups.Delete(networkClient, groupID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package groups diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/groups/requests.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/groups/requests.go new file mode 100644 index 00000000..566a730e --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/groups/requests.go @@ -0,0 +1,133 @@ +package groups + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the group attributes you want to see returned. SortKey allows you to +// sort by a particular network attribute. SortDir sets the direction, and is +// either `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + ID string `q:"id"` + Name string `q:"name"` + Description string `q:"description"` + TenantID string `q:"tenant_id"` + ProjectID string `q:"project_id"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` + Tags string `q:"tags"` + TagsAny string `q:"tags-any"` + NotTags string `q:"not-tags"` + NotTagsAny string `q:"not-tags-any"` +} + +// List returns a Pager which allows you to iterate over a collection of +// security groups. It accepts a ListOpts struct, which allows you to filter +// and sort the returned collection for greater efficiency. +func List(c *gophercloud.ServiceClient, opts ListOpts) pagination.Pager { + q, err := gophercloud.BuildQueryString(&opts) + if err != nil { + return pagination.Pager{Err: err} + } + u := rootURL(c) + q.String() + return pagination.NewPager(c, u, func(r pagination.PageResult) pagination.Page { + return SecGroupPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToSecGroupCreateMap() (map[string]interface{}, error) +} + +// CreateOpts contains all the values needed to create a new security group. +type CreateOpts struct { + // Human-readable name for the Security Group. Does not have to be unique. + Name string `json:"name" required:"true"` + + // TenantID is the UUID of the project who owns the Group. + // Only administrative users can specify a tenant UUID other than their own. + TenantID string `json:"tenant_id,omitempty"` + + // ProjectID is the UUID of the project who owns the Group. + // Only administrative users can specify a tenant UUID other than their own. + ProjectID string `json:"project_id,omitempty"` + + // Describes the security group. + Description string `json:"description,omitempty"` +} + +// ToSecGroupCreateMap builds a request body from CreateOpts. +func (opts CreateOpts) ToSecGroupCreateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "security_group") +} + +// Create is an operation which provisions a new security group with default +// security group rules for the IPv4 and IPv6 ether types. +func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToSecGroupCreateMap() + if err != nil { + r.Err = err + return + } + resp, err := c.Post(rootURL(c), b, &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the +// Update request. +type UpdateOptsBuilder interface { + ToSecGroupUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts contains all the values needed to update an existing security +// group. +type UpdateOpts struct { + // Human-readable name for the Security Group. Does not have to be unique. + Name string `json:"name,omitempty"` + + // Describes the security group. + Description *string `json:"description,omitempty"` +} + +// ToSecGroupUpdateMap builds a request body from UpdateOpts. +func (opts UpdateOpts) ToSecGroupUpdateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "security_group") +} + +// Update is an operation which updates an existing security group. +func Update(c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToSecGroupUpdateMap() + if err != nil { + r.Err = err + return + } + + resp, err := c.Put(resourceURL(c, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Get retrieves a particular security group based on its unique ID. +func Get(c *gophercloud.ServiceClient, id string) (r GetResult) { + resp, err := c.Get(resourceURL(c, id), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Delete will permanently delete a particular security group based on its +// unique ID. +func Delete(c *gophercloud.ServiceClient, id string) (r DeleteResult) { + resp, err := c.Delete(resourceURL(c, id), nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/groups/results.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/groups/results.go new file mode 100644 index 00000000..2027037d --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/groups/results.go @@ -0,0 +1,158 @@ +package groups + +import ( + "encoding/json" + "time" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/rules" + "github.com/gophercloud/gophercloud/pagination" +) + +// SecGroup represents a container for security group rules. +type SecGroup struct { + // The UUID for the security group. + ID string + + // Human-readable name for the security group. Might not be unique. + // Cannot be named "default" as that is automatically created for a tenant. + Name string + + // The security group description. + Description string + + // A slice of security group rules that dictate the permitted behaviour for + // traffic entering and leaving the group. + Rules []rules.SecGroupRule `json:"security_group_rules"` + + // TenantID is the project owner of the security group. + TenantID string `json:"tenant_id"` + + // UpdatedAt and CreatedAt contain ISO-8601 timestamps of when the state of the + // security group last changed, and when it was created. + UpdatedAt time.Time `json:"-"` + CreatedAt time.Time `json:"-"` + + // ProjectID is the project owner of the security group. + ProjectID string `json:"project_id"` + + // Tags optionally set via extensions/attributestags + Tags []string `json:"tags"` +} + +func (r *SecGroup) UnmarshalJSON(b []byte) error { + type tmp SecGroup + + // Support for older neutron time format + var s1 struct { + tmp + CreatedAt gophercloud.JSONRFC3339NoZ `json:"created_at"` + UpdatedAt gophercloud.JSONRFC3339NoZ `json:"updated_at"` + } + + err := json.Unmarshal(b, &s1) + if err == nil { + *r = SecGroup(s1.tmp) + r.CreatedAt = time.Time(s1.CreatedAt) + r.UpdatedAt = time.Time(s1.UpdatedAt) + + return nil + } + + // Support for newer neutron time format + var s2 struct { + tmp + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + } + + err = json.Unmarshal(b, &s2) + if err != nil { + return err + } + + *r = SecGroup(s2.tmp) + r.CreatedAt = time.Time(s2.CreatedAt) + r.UpdatedAt = time.Time(s2.UpdatedAt) + + return nil +} + +// SecGroupPage is the page returned by a pager when traversing over a +// collection of security groups. +type SecGroupPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of security groups has +// reached the end of a page and the pager seeks to traverse over a new one. In +// order to do this, it needs to construct the next page's URL. +func (r SecGroupPage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"security_groups_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + + return gophercloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a SecGroupPage struct is empty. +func (r SecGroupPage) IsEmpty() (bool, error) { + if r.StatusCode == 204 { + return true, nil + } + + is, err := ExtractGroups(r) + return len(is) == 0, err +} + +// ExtractGroups accepts a Page struct, specifically a SecGroupPage struct, +// and extracts the elements into a slice of SecGroup structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractGroups(r pagination.Page) ([]SecGroup, error) { + var s struct { + SecGroups []SecGroup `json:"security_groups"` + } + err := (r.(SecGroupPage)).ExtractInto(&s) + return s.SecGroups, err +} + +type commonResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a security group. +func (r commonResult) Extract() (*SecGroup, error) { + var s struct { + SecGroup *SecGroup `json:"security_group"` + } + err := r.ExtractInto(&s) + return s.SecGroup, err +} + +// CreateResult represents the result of a create operation. Call its Extract +// method to interpret it as a SecGroup. +type CreateResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. Call its Extract +// method to interpret it as a SecGroup. +type UpdateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. Call its Extract +// method to interpret it as a SecGroup. +type GetResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. Call its +// ExtractErr method to determine if the request succeeded or failed. +type DeleteResult struct { + gophercloud.ErrResult +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/groups/urls.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/groups/urls.go new file mode 100644 index 00000000..104cbcc5 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/groups/urls.go @@ -0,0 +1,13 @@ +package groups + +import "github.com/gophercloud/gophercloud" + +const rootPath = "security-groups" + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(rootPath) +} + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(rootPath, id) +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/rules/doc.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/rules/doc.go new file mode 100644 index 00000000..bf66dc8b --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/rules/doc.go @@ -0,0 +1,50 @@ +/* +Package rules provides information and interaction with Security Group Rules +for the OpenStack Networking service. + +Example to List Security Groups Rules + + listOpts := rules.ListOpts{ + Protocol: "tcp", + } + + allPages, err := rules.List(networkClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allRules, err := rules.ExtractRules(allPages) + if err != nil { + panic(err) + } + + for _, rule := range allRules { + fmt.Printf("%+v\n", rule) + } + +Example to Create a Security Group Rule + + createOpts := rules.CreateOpts{ + Direction: "ingress", + PortRangeMin: 80, + EtherType: rules.EtherType4, + PortRangeMax: 80, + Protocol: "tcp", + RemoteGroupID: "85cc3048-abc3-43cc-89b3-377341426ac5", + SecGroupID: "a7734e61-b545-452d-a3cd-0189cbd9747a", + } + + rule, err := rules.Create(networkClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a Security Group Rule + + ruleID := "37d94f8a-d136-465c-ae46-144f0d8ef141" + err := rules.Delete(networkClient, ruleID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package rules diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/rules/requests.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/rules/requests.go new file mode 100644 index 00000000..364f7f5c --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/rules/requests.go @@ -0,0 +1,164 @@ +package rules + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the security group rule attributes you want to see returned. SortKey allows +// you to sort by a particular network attribute. SortDir sets the direction, +// and is either `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + Direction string `q:"direction"` + EtherType string `q:"ethertype"` + ID string `q:"id"` + Description string `q:"description"` + PortRangeMax int `q:"port_range_max"` + PortRangeMin int `q:"port_range_min"` + Protocol string `q:"protocol"` + RemoteGroupID string `q:"remote_group_id"` + RemoteIPPrefix string `q:"remote_ip_prefix"` + SecGroupID string `q:"security_group_id"` + TenantID string `q:"tenant_id"` + ProjectID string `q:"project_id"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` +} + +// List returns a Pager which allows you to iterate over a collection of +// security group rules. It accepts a ListOpts struct, which allows you to filter +// and sort the returned collection for greater efficiency. +func List(c *gophercloud.ServiceClient, opts ListOpts) pagination.Pager { + q, err := gophercloud.BuildQueryString(&opts) + if err != nil { + return pagination.Pager{Err: err} + } + u := rootURL(c) + q.String() + return pagination.NewPager(c, u, func(r pagination.PageResult) pagination.Page { + return SecGroupRulePage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +type RuleDirection string +type RuleProtocol string +type RuleEtherType string + +// Constants useful for CreateOpts +const ( + DirIngress RuleDirection = "ingress" + DirEgress RuleDirection = "egress" + EtherType4 RuleEtherType = "IPv4" + EtherType6 RuleEtherType = "IPv6" + ProtocolAH RuleProtocol = "ah" + ProtocolDCCP RuleProtocol = "dccp" + ProtocolEGP RuleProtocol = "egp" + ProtocolESP RuleProtocol = "esp" + ProtocolGRE RuleProtocol = "gre" + ProtocolICMP RuleProtocol = "icmp" + ProtocolIGMP RuleProtocol = "igmp" + ProtocolIPIP RuleProtocol = "ipip" + ProtocolIPv6Encap RuleProtocol = "ipv6-encap" + ProtocolIPv6Frag RuleProtocol = "ipv6-frag" + ProtocolIPv6ICMP RuleProtocol = "ipv6-icmp" + ProtocolIPv6NoNxt RuleProtocol = "ipv6-nonxt" + ProtocolIPv6Opts RuleProtocol = "ipv6-opts" + ProtocolIPv6Route RuleProtocol = "ipv6-route" + ProtocolOSPF RuleProtocol = "ospf" + ProtocolPGM RuleProtocol = "pgm" + ProtocolRSVP RuleProtocol = "rsvp" + ProtocolSCTP RuleProtocol = "sctp" + ProtocolTCP RuleProtocol = "tcp" + ProtocolUDP RuleProtocol = "udp" + ProtocolUDPLite RuleProtocol = "udplite" + ProtocolVRRP RuleProtocol = "vrrp" + ProtocolAny RuleProtocol = "any" +) + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToSecGroupRuleCreateMap() (map[string]interface{}, error) +} + +// CreateOpts contains all the values needed to create a new security group +// rule. +type CreateOpts struct { + // Must be either "ingress" or "egress": the direction in which the security + // group rule is applied. + Direction RuleDirection `json:"direction" required:"true"` + + // String description of each rule, optional + Description string `json:"description,omitempty"` + + // Must be "IPv4" or "IPv6", and addresses represented in CIDR must match the + // ingress or egress rules. + EtherType RuleEtherType `json:"ethertype" required:"true"` + + // The security group ID to associate with this security group rule. + SecGroupID string `json:"security_group_id" required:"true"` + + // The maximum port number in the range that is matched by the security group + // rule. The PortRangeMin attribute constrains the PortRangeMax attribute. If + // the protocol is ICMP, this value must be an ICMP type. + PortRangeMax int `json:"port_range_max,omitempty"` + + // The minimum port number in the range that is matched by the security group + // rule. If the protocol is TCP or UDP, this value must be less than or equal + // to the value of the PortRangeMax attribute. If the protocol is ICMP, this + // value must be an ICMP type. + PortRangeMin int `json:"port_range_min,omitempty"` + + // The protocol that is matched by the security group rule. Valid values are + // "tcp", "udp", "icmp" or an empty string. + Protocol RuleProtocol `json:"protocol,omitempty"` + + // The remote group ID to be associated with this security group rule. You can + // specify either RemoteGroupID or RemoteIPPrefix. + RemoteGroupID string `json:"remote_group_id,omitempty"` + + // The remote IP prefix to be associated with this security group rule. You can + // specify either RemoteGroupID or RemoteIPPrefix. This attribute matches the + // specified IP prefix as the source IP address of the IP packet. + RemoteIPPrefix string `json:"remote_ip_prefix,omitempty"` + + // TenantID is the UUID of the project who owns the Rule. + // Only administrative users can specify a project UUID other than their own. + ProjectID string `json:"project_id,omitempty"` +} + +// ToSecGroupRuleCreateMap builds a request body from CreateOpts. +func (opts CreateOpts) ToSecGroupRuleCreateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "security_group_rule") +} + +// Create is an operation which adds a new security group rule and associates it +// with an existing security group (whose ID is specified in CreateOpts). +func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToSecGroupRuleCreateMap() + if err != nil { + r.Err = err + return + } + resp, err := c.Post(rootURL(c), b, &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Get retrieves a particular security group rule based on its unique ID. +func Get(c *gophercloud.ServiceClient, id string) (r GetResult) { + resp, err := c.Get(resourceURL(c, id), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Delete will permanently delete a particular security group rule based on its +// unique ID. +func Delete(c *gophercloud.ServiceClient, id string) (r DeleteResult) { + resp, err := c.Delete(resourceURL(c, id), nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/rules/results.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/rules/results.go new file mode 100644 index 00000000..1e65f062 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/rules/results.go @@ -0,0 +1,131 @@ +package rules + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// SecGroupRule represents a rule to dictate the behaviour of incoming or +// outgoing traffic for a particular security group. +type SecGroupRule struct { + // The UUID for this security group rule. + ID string + + // The direction in which the security group rule is applied. The only values + // allowed are "ingress" or "egress". For a compute instance, an ingress + // security group rule is applied to incoming (ingress) traffic for that + // instance. An egress rule is applied to traffic leaving the instance. + Direction string + + // Description of the rule + Description string `json:"description"` + + // Must be IPv4 or IPv6, and addresses represented in CIDR must match the + // ingress or egress rules. + EtherType string `json:"ethertype"` + + // The security group ID to associate with this security group rule. + SecGroupID string `json:"security_group_id"` + + // The minimum port number in the range that is matched by the security group + // rule. If the protocol is TCP or UDP, this value must be less than or equal + // to the value of the PortRangeMax attribute. If the protocol is ICMP, this + // value must be an ICMP type. + PortRangeMin int `json:"port_range_min"` + + // The maximum port number in the range that is matched by the security group + // rule. The PortRangeMin attribute constrains the PortRangeMax attribute. If + // the protocol is ICMP, this value must be an ICMP type. + PortRangeMax int `json:"port_range_max"` + + // The protocol that is matched by the security group rule. Valid values are + // "tcp", "udp", "icmp" or an empty string. + Protocol string + + // The remote group ID to be associated with this security group rule. You + // can specify either RemoteGroupID or RemoteIPPrefix. + RemoteGroupID string `json:"remote_group_id"` + + // The remote IP prefix to be associated with this security group rule. You + // can specify either RemoteGroupID or RemoteIPPrefix . This attribute + // matches the specified IP prefix as the source IP address of the IP packet. + RemoteIPPrefix string `json:"remote_ip_prefix"` + + // TenantID is the project owner of this security group rule. + TenantID string `json:"tenant_id"` + + // ProjectID is the project owner of this security group rule. + ProjectID string `json:"project_id"` +} + +// SecGroupRulePage is the page returned by a pager when traversing over a +// collection of security group rules. +type SecGroupRulePage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of security group rules has +// reached the end of a page and the pager seeks to traverse over a new one. In +// order to do this, it needs to construct the next page's URL. +func (r SecGroupRulePage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"security_group_rules_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a SecGroupRulePage struct is empty. +func (r SecGroupRulePage) IsEmpty() (bool, error) { + if r.StatusCode == 204 { + return true, nil + } + + is, err := ExtractRules(r) + return len(is) == 0, err +} + +// ExtractRules accepts a Page struct, specifically a SecGroupRulePage struct, +// and extracts the elements into a slice of SecGroupRule structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractRules(r pagination.Page) ([]SecGroupRule, error) { + var s struct { + SecGroupRules []SecGroupRule `json:"security_group_rules"` + } + err := (r.(SecGroupRulePage)).ExtractInto(&s) + return s.SecGroupRules, err +} + +type commonResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a security rule. +func (r commonResult) Extract() (*SecGroupRule, error) { + var s struct { + SecGroupRule *SecGroupRule `json:"security_group_rule"` + } + err := r.ExtractInto(&s) + return s.SecGroupRule, err +} + +// CreateResult represents the result of a create operation. Call its Extract +// method to interpret it as a SecGroupRule. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. Call its Extract +// method to interpret it as a SecGroupRule. +type GetResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. Call its +// ExtractErr method to determine if the request succeeded or failed. +type DeleteResult struct { + gophercloud.ErrResult +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/rules/urls.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/rules/urls.go new file mode 100644 index 00000000..a5ede0e8 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/rules/urls.go @@ -0,0 +1,13 @@ +package rules + +import "github.com/gophercloud/gophercloud" + +const rootPath = "security-group-rules" + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(rootPath) +} + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(rootPath, id) +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/ports/doc.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/ports/doc.go new file mode 100644 index 00000000..cfb1774f --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/ports/doc.go @@ -0,0 +1,73 @@ +/* +Package ports contains functionality for working with Neutron port resources. + +A port represents a virtual switch port on a logical network switch. Virtual +instances attach their interfaces into ports. The logical port also defines +the MAC address and the IP address(es) to be assigned to the interfaces +plugged into them. When IP addresses are associated to a port, this also +implies the port is associated with a subnet, as the IP address was taken +from the allocation pool for a specific subnet. + +Example to List Ports + + listOpts := ports.ListOpts{ + DeviceID: "b0b89efe-82f8-461d-958b-adbf80f50c7d", + } + + allPages, err := ports.List(networkClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allPorts, err := ports.ExtractPorts(allPages) + if err != nil { + panic(err) + } + + for _, port := range allPorts { + fmt.Printf("%+v\n", port) + } + +Example to Create a Port + + createOtps := ports.CreateOpts{ + Name: "private-port", + AdminStateUp: &asu, + NetworkID: "a87cc70a-3e15-4acf-8205-9b711a3531b7", + FixedIPs: []ports.IP{ + {SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.2"}, + }, + SecurityGroups: &[]string{"foo"}, + AllowedAddressPairs: []ports.AddressPair{ + {IPAddress: "10.0.0.4", MACAddress: "fa:16:3e:c9:cb:f0"}, + }, + } + + port, err := ports.Create(networkClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Update a Port + + portID := "c34bae2b-7641-49b6-bf6d-d8e473620ed8" + + updateOpts := ports.UpdateOpts{ + Name: "new_name", + SecurityGroups: &[]string{}, + } + + port, err := ports.Update(networkClient, portID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a Port + + portID := "c34bae2b-7641-49b6-bf6d-d8e473620ed8" + err := ports.Delete(networkClient, portID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package ports diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/ports/requests.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/ports/requests.go new file mode 100644 index 00000000..805f0e5b --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/ports/requests.go @@ -0,0 +1,209 @@ +package ports + +import ( + "fmt" + "net/url" + "strings" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToPortListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the port attributes you want to see returned. SortKey allows you to sort +// by a particular port attribute. SortDir sets the direction, and is either +// `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + Status string `q:"status"` + Name string `q:"name"` + Description string `q:"description"` + AdminStateUp *bool `q:"admin_state_up"` + NetworkID string `q:"network_id"` + TenantID string `q:"tenant_id"` + ProjectID string `q:"project_id"` + DeviceOwner string `q:"device_owner"` + MACAddress string `q:"mac_address"` + ID string `q:"id"` + DeviceID string `q:"device_id"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` + Tags string `q:"tags"` + TagsAny string `q:"tags-any"` + NotTags string `q:"not-tags"` + NotTagsAny string `q:"not-tags-any"` + SecurityGroups []string `q:"security_groups"` + FixedIPs []FixedIPOpts +} + +type FixedIPOpts struct { + IPAddress string + IPAddressSubstr string + SubnetID string +} + +func (f FixedIPOpts) String() string { + var res []string + if f.IPAddress != "" { + res = append(res, fmt.Sprintf("ip_address=%s", f.IPAddress)) + } + if f.IPAddressSubstr != "" { + res = append(res, fmt.Sprintf("ip_address_substr=%s", f.IPAddressSubstr)) + } + if f.SubnetID != "" { + res = append(res, fmt.Sprintf("subnet_id=%s", f.SubnetID)) + } + return strings.Join(res, ",") +} + +// ToPortListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToPortListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + params := q.Query() + for _, fixedIP := range opts.FixedIPs { + params.Add("fixed_ips", fixedIP.String()) + } + q = &url.URL{RawQuery: params.Encode()} + return q.String(), err +} + +// List returns a Pager which allows you to iterate over a collection of +// ports. It accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +// +// Default policy settings return only those ports that are owned by the tenant +// who submits the request, unless the request is submitted by a user with +// administrative rights. +func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(c) + if opts != nil { + query, err := opts.ToPortListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return PortPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get retrieves a specific port based on its unique ID. +func Get(c *gophercloud.ServiceClient, id string) (r GetResult) { + resp, err := c.Get(getURL(c, id), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToPortCreateMap() (map[string]interface{}, error) +} + +// CreateOpts represents the attributes used when creating a new port. +type CreateOpts struct { + NetworkID string `json:"network_id" required:"true"` + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + AdminStateUp *bool `json:"admin_state_up,omitempty"` + MACAddress string `json:"mac_address,omitempty"` + FixedIPs interface{} `json:"fixed_ips,omitempty"` + DeviceID string `json:"device_id,omitempty"` + DeviceOwner string `json:"device_owner,omitempty"` + TenantID string `json:"tenant_id,omitempty"` + ProjectID string `json:"project_id,omitempty"` + SecurityGroups *[]string `json:"security_groups,omitempty"` + AllowedAddressPairs []AddressPair `json:"allowed_address_pairs,omitempty"` + PropagateUplinkStatus *bool `json:"propagate_uplink_status,omitempty"` + ValueSpecs *map[string]string `json:"value_specs,omitempty"` +} + +// ToPortCreateMap builds a request body from CreateOpts. +func (opts CreateOpts) ToPortCreateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "port") +} + +// Create accepts a CreateOpts struct and creates a new network using the values +// provided. You must remember to provide a NetworkID value. +func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToPortCreateMap() + if err != nil { + r.Err = err + return + } + resp, err := c.Post(createURL(c), b, &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the +// Update request. +type UpdateOptsBuilder interface { + ToPortUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts represents the attributes used when updating an existing port. +type UpdateOpts struct { + Name *string `json:"name,omitempty"` + Description *string `json:"description,omitempty"` + AdminStateUp *bool `json:"admin_state_up,omitempty"` + FixedIPs interface{} `json:"fixed_ips,omitempty"` + DeviceID *string `json:"device_id,omitempty"` + DeviceOwner *string `json:"device_owner,omitempty"` + SecurityGroups *[]string `json:"security_groups,omitempty"` + AllowedAddressPairs *[]AddressPair `json:"allowed_address_pairs,omitempty"` + PropagateUplinkStatus *bool `json:"propagate_uplink_status,omitempty"` + ValueSpecs *map[string]string `json:"value_specs,omitempty"` + + // RevisionNumber implements extension:standard-attr-revisions. If != "" it + // will set revision_number=%s. If the revision number does not match, the + // update will fail. + RevisionNumber *int `json:"-" h:"If-Match"` +} + +// ToPortUpdateMap builds a request body from UpdateOpts. +func (opts UpdateOpts) ToPortUpdateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "port") +} + +// Update accepts a UpdateOpts struct and updates an existing port using the +// values provided. +func Update(c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToPortUpdateMap() + if err != nil { + r.Err = err + return + } + h, err := gophercloud.BuildHeaders(opts) + if err != nil { + r.Err = err + return + } + for k := range h { + if k == "If-Match" { + h[k] = fmt.Sprintf("revision_number=%s", h[k]) + } + } + resp, err := c.Put(updateURL(c, id), b, &r.Body, &gophercloud.RequestOpts{ + MoreHeaders: h, + OkCodes: []int{200, 201}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Delete accepts a unique ID and deletes the port associated with it. +func Delete(c *gophercloud.ServiceClient, id string) (r DeleteResult) { + resp, err := c.Delete(deleteURL(c, id), nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/ports/results.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/ports/results.go new file mode 100644 index 00000000..a39133fc --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/ports/results.go @@ -0,0 +1,209 @@ +package ports + +import ( + "encoding/json" + "time" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +type commonResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a port resource. +func (r commonResult) Extract() (*Port, error) { + var s Port + err := r.ExtractInto(&s) + return &s, err +} + +func (r commonResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "port") +} + +// CreateResult represents the result of a create operation. Call its Extract +// method to interpret it as a Port. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. Call its Extract +// method to interpret it as a Port. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. Call its Extract +// method to interpret it as a Port. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. Call its +// ExtractErr method to determine if the request succeeded or failed. +type DeleteResult struct { + gophercloud.ErrResult +} + +// IP is a sub-struct that represents an individual IP. +type IP struct { + SubnetID string `json:"subnet_id"` + IPAddress string `json:"ip_address,omitempty"` +} + +// AddressPair contains the IP Address and the MAC address. +type AddressPair struct { + IPAddress string `json:"ip_address,omitempty"` + MACAddress string `json:"mac_address,omitempty"` +} + +// Port represents a Neutron port. See package documentation for a top-level +// description of what this is. +type Port struct { + // UUID for the port. + ID string `json:"id"` + + // Network that this port is associated with. + NetworkID string `json:"network_id"` + + // Human-readable name for the port. Might not be unique. + Name string `json:"name"` + + // Describes the port. + Description string `json:"description"` + + // Administrative state of port. If false (down), port does not forward + // packets. + AdminStateUp bool `json:"admin_state_up"` + + // Indicates whether network is currently operational. Possible values include + // `ACTIVE', `DOWN', `BUILD', or `ERROR'. Plug-ins might define additional + // values. + Status string `json:"status"` + + // Mac address to use on this port. + MACAddress string `json:"mac_address"` + + // Specifies IP addresses for the port thus associating the port itself with + // the subnets where the IP addresses are picked from + FixedIPs []IP `json:"fixed_ips"` + + // TenantID is the project owner of the port. + TenantID string `json:"tenant_id"` + + // ProjectID is the project owner of the port. + ProjectID string `json:"project_id"` + + // Identifies the entity (e.g.: dhcp agent) using this port. + DeviceOwner string `json:"device_owner"` + + // Specifies the IDs of any security groups associated with a port. + SecurityGroups []string `json:"security_groups"` + + // Identifies the device (e.g., virtual server) using this port. + DeviceID string `json:"device_id"` + + // Identifies the list of IP addresses the port will recognize/accept + AllowedAddressPairs []AddressPair `json:"allowed_address_pairs"` + + // Tags optionally set via extensions/attributestags + Tags []string `json:"tags"` + + // PropagateUplinkStatus enables/disables propagate uplink status on the port. + PropagateUplinkStatus bool `json:"propagate_uplink_status"` + + // Extra parameters to include in the request. + ValueSpecs map[string]string `json:"value_specs"` + + // RevisionNumber optionally set via extensions/standard-attr-revisions + RevisionNumber int `json:"revision_number"` + + // Timestamp when the port was created + CreatedAt time.Time `json:"created_at"` + + // Timestamp when the port was last updated + UpdatedAt time.Time `json:"updated_at"` +} + +func (r *Port) UnmarshalJSON(b []byte) error { + type tmp Port + + // Support for older neutron time format + var s1 struct { + tmp + CreatedAt gophercloud.JSONRFC3339NoZ `json:"created_at"` + UpdatedAt gophercloud.JSONRFC3339NoZ `json:"updated_at"` + } + + err := json.Unmarshal(b, &s1) + if err == nil { + *r = Port(s1.tmp) + r.CreatedAt = time.Time(s1.CreatedAt) + r.UpdatedAt = time.Time(s1.UpdatedAt) + + return nil + } + + // Support for newer neutron time format + var s2 struct { + tmp + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + } + + err = json.Unmarshal(b, &s2) + if err != nil { + return err + } + + *r = Port(s2.tmp) + r.CreatedAt = time.Time(s2.CreatedAt) + r.UpdatedAt = time.Time(s2.UpdatedAt) + + return nil +} + +// PortPage is the page returned by a pager when traversing over a collection +// of network ports. +type PortPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of ports has reached +// the end of a page and the pager seeks to traverse over a new one. In order +// to do this, it needs to construct the next page's URL. +func (r PortPage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"ports_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a PortPage struct is empty. +func (r PortPage) IsEmpty() (bool, error) { + if r.StatusCode == 204 { + return true, nil + } + + is, err := ExtractPorts(r) + return len(is) == 0, err +} + +// ExtractPorts accepts a Page struct, specifically a PortPage struct, +// and extracts the elements into a slice of Port structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractPorts(r pagination.Page) ([]Port, error) { + var s []Port + err := ExtractPortsInto(r, &s) + return s, err +} + +func ExtractPortsInto(r pagination.Page, v interface{}) error { + return r.(PortPage).Result.ExtractIntoSlicePtr(v, "ports") +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/ports/urls.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/ports/urls.go new file mode 100644 index 00000000..600d6f2f --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/ports/urls.go @@ -0,0 +1,31 @@ +package ports + +import "github.com/gophercloud/gophercloud" + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("ports", id) +} + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("ports") +} + +func listURL(c *gophercloud.ServiceClient) string { + return rootURL(c) +} + +func getURL(c *gophercloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func createURL(c *gophercloud.ServiceClient) string { + return rootURL(c) +} + +func updateURL(c *gophercloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func deleteURL(c *gophercloud.ServiceClient, id string) string { + return resourceURL(c, id) +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/subnets/doc.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/subnets/doc.go new file mode 100644 index 00000000..8bb4468c --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/subnets/doc.go @@ -0,0 +1,138 @@ +/* +Package subnets contains functionality for working with Neutron subnet +resources. A subnet represents an IP address block that can be used to +assign IP addresses to virtual instances. Each subnet must have a CIDR and +must be associated with a network. IPs can either be selected from the whole +subnet CIDR or from allocation pools specified by the user. + +A subnet can also have a gateway, a list of DNS name servers, and host routes. +This information is pushed to instances whose interfaces are associated with +the subnet. + +Example to List Subnets + + listOpts := subnets.ListOpts{ + IPVersion: 4, + } + + allPages, err := subnets.List(networkClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allSubnets, err := subnets.ExtractSubnets(allPages) + if err != nil { + panic(err) + } + + for _, subnet := range allSubnets { + fmt.Printf("%+v\n", subnet) + } + +Example to Create a Subnet With Specified Gateway + + var gatewayIP = "192.168.199.1" + createOpts := subnets.CreateOpts{ + NetworkID: "d32019d3-bc6e-4319-9c1d-6722fc136a22", + IPVersion: 4, + CIDR: "192.168.199.0/24", + GatewayIP: &gatewayIP, + AllocationPools: []subnets.AllocationPool{ + { + Start: "192.168.199.2", + End: "192.168.199.254", + }, + }, + DNSNameservers: []string{"foo"}, + ServiceTypes: []string{"network:floatingip"}, + } + + subnet, err := subnets.Create(networkClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Create a Subnet With No Gateway + + var noGateway = "" + + createOpts := subnets.CreateOpts{ + NetworkID: "d32019d3-bc6e-4319-9c1d-6722fc136a23", + IPVersion: 4, + CIDR: "192.168.1.0/24", + GatewayIP: &noGateway, + AllocationPools: []subnets.AllocationPool{ + { + Start: "192.168.1.2", + End: "192.168.1.254", + }, + }, + DNSNameservers: []string{}, + } + + subnet, err := subnets.Create(networkClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Create a Subnet With a Default Gateway + + createOpts := subnets.CreateOpts{ + NetworkID: "d32019d3-bc6e-4319-9c1d-6722fc136a23", + IPVersion: 4, + CIDR: "192.168.1.0/24", + AllocationPools: []subnets.AllocationPool{ + { + Start: "192.168.1.2", + End: "192.168.1.254", + }, + }, + DNSNameservers: []string{}, + } + + subnet, err := subnets.Create(networkClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Update a Subnet + + subnetID := "db77d064-e34f-4d06-b060-f21e28a61c23" + dnsNameservers := []string{"8.8.8.8"} + serviceTypes := []string{"network:floatingip", "network:routed"} + name := "new_name" + + updateOpts := subnets.UpdateOpts{ + Name: &name, + DNSNameservers: &dnsNameservers, + ServiceTypes: &serviceTypes, + } + + subnet, err := subnets.Update(networkClient, subnetID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Remove a Gateway From a Subnet + + var noGateway = "" + subnetID := "db77d064-e34f-4d06-b060-f21e28a61c23" + + updateOpts := subnets.UpdateOpts{ + GatewayIP: &noGateway, + } + + subnet, err := subnets.Update(networkClient, subnetID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a Subnet + + subnetID := "db77d064-e34f-4d06-b060-f21e28a61c23" + err := subnets.Delete(networkClient, subnetID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package subnets diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/subnets/requests.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/subnets/requests.go new file mode 100644 index 00000000..2e879075 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/subnets/requests.go @@ -0,0 +1,261 @@ +package subnets + +import ( + "fmt" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToSubnetListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the subnet attributes you want to see returned. SortKey allows you to sort +// by a particular subnet attribute. SortDir sets the direction, and is either +// `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + Name string `q:"name"` + Description string `q:"description"` + EnableDHCP *bool `q:"enable_dhcp"` + NetworkID string `q:"network_id"` + TenantID string `q:"tenant_id"` + ProjectID string `q:"project_id"` + IPVersion int `q:"ip_version"` + GatewayIP string `q:"gateway_ip"` + CIDR string `q:"cidr"` + IPv6AddressMode string `q:"ipv6_address_mode"` + IPv6RAMode string `q:"ipv6_ra_mode"` + ID string `q:"id"` + SubnetPoolID string `q:"subnetpool_id"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` + Tags string `q:"tags"` + TagsAny string `q:"tags-any"` + NotTags string `q:"not-tags"` + NotTagsAny string `q:"not-tags-any"` +} + +// ToSubnetListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToSubnetListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns a Pager which allows you to iterate over a collection of +// subnets. It accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +// +// Default policy settings return only those subnets that are owned by the tenant +// who submits the request, unless the request is submitted by a user with +// administrative rights. +func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(c) + if opts != nil { + query, err := opts.ToSubnetListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return SubnetPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get retrieves a specific subnet based on its unique ID. +func Get(c *gophercloud.ServiceClient, id string) (r GetResult) { + resp, err := c.Get(getURL(c, id), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// CreateOptsBuilder allows extensions to add additional parameters to the +// List request. +type CreateOptsBuilder interface { + ToSubnetCreateMap() (map[string]interface{}, error) +} + +// CreateOpts represents the attributes used when creating a new subnet. +type CreateOpts struct { + // NetworkID is the UUID of the network the subnet will be associated with. + NetworkID string `json:"network_id" required:"true"` + + // CIDR is the address CIDR of the subnet. + CIDR string `json:"cidr,omitempty"` + + // Name is a human-readable name of the subnet. + Name string `json:"name,omitempty"` + + // Description of the subnet. + Description string `json:"description,omitempty"` + + // The UUID of the project who owns the Subnet. Only administrative users + // can specify a project UUID other than their own. + TenantID string `json:"tenant_id,omitempty"` + + // The UUID of the project who owns the Subnet. Only administrative users + // can specify a project UUID other than their own. + ProjectID string `json:"project_id,omitempty"` + + // AllocationPools are IP Address pools that will be available for DHCP. + AllocationPools []AllocationPool `json:"allocation_pools,omitempty"` + + // GatewayIP sets gateway information for the subnet. Setting to nil will + // cause a default gateway to automatically be created. Setting to an empty + // string will cause the subnet to be created with no gateway. Setting to + // an explicit address will set that address as the gateway. + GatewayIP *string `json:"gateway_ip,omitempty"` + + // IPVersion is the IP version for the subnet. + IPVersion gophercloud.IPVersion `json:"ip_version,omitempty"` + + // EnableDHCP will either enable to disable the DHCP service. + EnableDHCP *bool `json:"enable_dhcp,omitempty"` + + // DNSNameservers are the nameservers to be set via DHCP. + DNSNameservers []string `json:"dns_nameservers,omitempty"` + + // ServiceTypes are the service types associated with the subnet. + ServiceTypes []string `json:"service_types,omitempty"` + + // HostRoutes are any static host routes to be set via DHCP. + HostRoutes []HostRoute `json:"host_routes,omitempty"` + + // The IPv6 address modes specifies mechanisms for assigning IPv6 IP addresses. + IPv6AddressMode string `json:"ipv6_address_mode,omitempty"` + + // The IPv6 router advertisement specifies whether the networking service + // should transmit ICMPv6 packets. + IPv6RAMode string `json:"ipv6_ra_mode,omitempty"` + + // SubnetPoolID is the id of the subnet pool that subnet should be associated to. + SubnetPoolID string `json:"subnetpool_id,omitempty"` + + // Prefixlen is used when user creates a subnet from the subnetpool. It will + // overwrite the "default_prefixlen" value of the referenced subnetpool. + Prefixlen int `json:"prefixlen,omitempty"` +} + +// ToSubnetCreateMap builds a request body from CreateOpts. +func (opts CreateOpts) ToSubnetCreateMap() (map[string]interface{}, error) { + b, err := gophercloud.BuildRequestBody(opts, "subnet") + if err != nil { + return nil, err + } + + if m := b["subnet"].(map[string]interface{}); m["gateway_ip"] == "" { + m["gateway_ip"] = nil + } + + return b, nil +} + +// Create accepts a CreateOpts struct and creates a new subnet using the values +// provided. You must remember to provide a valid NetworkID, CIDR and IP +// version. +func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToSubnetCreateMap() + if err != nil { + r.Err = err + return + } + resp, err := c.Post(createURL(c), b, &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the +// Update request. +type UpdateOptsBuilder interface { + ToSubnetUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts represents the attributes used when updating an existing subnet. +type UpdateOpts struct { + // Name is a human-readable name of the subnet. + Name *string `json:"name,omitempty"` + + // Description of the subnet. + Description *string `json:"description,omitempty"` + + // AllocationPools are IP Address pools that will be available for DHCP. + AllocationPools []AllocationPool `json:"allocation_pools,omitempty"` + + // GatewayIP sets gateway information for the subnet. Setting to nil will + // cause a default gateway to automatically be created. Setting to an empty + // string will cause the subnet to be created with no gateway. Setting to + // an explicit address will set that address as the gateway. + GatewayIP *string `json:"gateway_ip,omitempty"` + + // DNSNameservers are the nameservers to be set via DHCP. + DNSNameservers *[]string `json:"dns_nameservers,omitempty"` + + // ServiceTypes are the service types associated with the subnet. + ServiceTypes *[]string `json:"service_types,omitempty"` + + // HostRoutes are any static host routes to be set via DHCP. + HostRoutes *[]HostRoute `json:"host_routes,omitempty"` + + // EnableDHCP will either enable to disable the DHCP service. + EnableDHCP *bool `json:"enable_dhcp,omitempty"` + + // RevisionNumber implements extension:standard-attr-revisions. If != "" it + // will set revision_number=%s. If the revision number does not match, the + // update will fail. + RevisionNumber *int `json:"-" h:"If-Match"` +} + +// ToSubnetUpdateMap builds a request body from UpdateOpts. +func (opts UpdateOpts) ToSubnetUpdateMap() (map[string]interface{}, error) { + b, err := gophercloud.BuildRequestBody(opts, "subnet") + if err != nil { + return nil, err + } + + if m := b["subnet"].(map[string]interface{}); m["gateway_ip"] == "" { + m["gateway_ip"] = nil + } + + return b, nil +} + +// Update accepts a UpdateOpts struct and updates an existing subnet using the +// values provided. +func Update(c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToSubnetUpdateMap() + if err != nil { + r.Err = err + return + } + h, err := gophercloud.BuildHeaders(opts) + if err != nil { + r.Err = err + return + } + for k := range h { + if k == "If-Match" { + h[k] = fmt.Sprintf("revision_number=%s", h[k]) + } + } + + resp, err := c.Put(updateURL(c, id), b, &r.Body, &gophercloud.RequestOpts{ + MoreHeaders: h, + OkCodes: []int{200, 201}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Delete accepts a unique ID and deletes the subnet associated with it. +func Delete(c *gophercloud.ServiceClient, id string) (r DeleteResult) { + resp, err := c.Delete(deleteURL(c, id), nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/subnets/results.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/subnets/results.go new file mode 100644 index 00000000..cd09b6f6 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/subnets/results.go @@ -0,0 +1,162 @@ +package subnets + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +type commonResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a subnet resource. +func (r commonResult) Extract() (*Subnet, error) { + var s struct { + Subnet *Subnet `json:"subnet"` + } + err := r.ExtractInto(&s) + return s.Subnet, err +} + +// CreateResult represents the result of a create operation. Call its Extract +// method to interpret it as a Subnet. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. Call its Extract +// method to interpret it as a Subnet. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. Call its Extract +// method to interpret it as a Subnet. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. Call its +// ExtractErr method to determine if the request succeeded or failed. +type DeleteResult struct { + gophercloud.ErrResult +} + +// AllocationPool represents a sub-range of cidr available for dynamic +// allocation to ports, e.g. {Start: "10.0.0.2", End: "10.0.0.254"} +type AllocationPool struct { + Start string `json:"start"` + End string `json:"end"` +} + +// HostRoute represents a route that should be used by devices with IPs from +// a subnet (not including local subnet route). +type HostRoute struct { + DestinationCIDR string `json:"destination"` + NextHop string `json:"nexthop"` +} + +// Subnet represents a subnet. See package documentation for a top-level +// description of what this is. +type Subnet struct { + // UUID representing the subnet. + ID string `json:"id"` + + // UUID of the parent network. + NetworkID string `json:"network_id"` + + // Human-readable name for the subnet. Might not be unique. + Name string `json:"name"` + + // Description for the subnet. + Description string `json:"description"` + + // IP version, either `4' or `6'. + IPVersion int `json:"ip_version"` + + // CIDR representing IP range for this subnet, based on IP version. + CIDR string `json:"cidr"` + + // Default gateway used by devices in this subnet. + GatewayIP string `json:"gateway_ip"` + + // DNS name servers used by hosts in this subnet. + DNSNameservers []string `json:"dns_nameservers"` + + // Service types associated with the subnet. + ServiceTypes []string `json:"service_types"` + + // Sub-ranges of CIDR available for dynamic allocation to ports. + // See AllocationPool. + AllocationPools []AllocationPool `json:"allocation_pools"` + + // Routes that should be used by devices with IPs from this subnet + // (not including local subnet route). + HostRoutes []HostRoute `json:"host_routes"` + + // Specifies whether DHCP is enabled for this subnet or not. + EnableDHCP bool `json:"enable_dhcp"` + + // TenantID is the project owner of the subnet. + TenantID string `json:"tenant_id"` + + // ProjectID is the project owner of the subnet. + ProjectID string `json:"project_id"` + + // The IPv6 address modes specifies mechanisms for assigning IPv6 IP addresses. + IPv6AddressMode string `json:"ipv6_address_mode"` + + // The IPv6 router advertisement specifies whether the networking service + // should transmit ICMPv6 packets. + IPv6RAMode string `json:"ipv6_ra_mode"` + + // SubnetPoolID is the id of the subnet pool associated with the subnet. + SubnetPoolID string `json:"subnetpool_id"` + + // Tags optionally set via extensions/attributestags + Tags []string `json:"tags"` + + // RevisionNumber optionally set via extensions/standard-attr-revisions + RevisionNumber int `json:"revision_number"` +} + +// SubnetPage is the page returned by a pager when traversing over a collection +// of subnets. +type SubnetPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of subnets has reached +// the end of a page and the pager seeks to traverse over a new one. In order +// to do this, it needs to construct the next page's URL. +func (r SubnetPage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"subnets_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a SubnetPage struct is empty. +func (r SubnetPage) IsEmpty() (bool, error) { + if r.StatusCode == 204 { + return true, nil + } + + is, err := ExtractSubnets(r) + return len(is) == 0, err +} + +// ExtractSubnets accepts a Page struct, specifically a SubnetPage struct, +// and extracts the elements into a slice of Subnet structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractSubnets(r pagination.Page) ([]Subnet, error) { + var s struct { + Subnets []Subnet `json:"subnets"` + } + err := (r.(SubnetPage)).ExtractInto(&s) + return s.Subnets, err +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/subnets/urls.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/subnets/urls.go new file mode 100644 index 00000000..7a4f2f7d --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/subnets/urls.go @@ -0,0 +1,31 @@ +package subnets + +import "github.com/gophercloud/gophercloud" + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("subnets", id) +} + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("subnets") +} + +func listURL(c *gophercloud.ServiceClient) string { + return rootURL(c) +} + +func getURL(c *gophercloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func createURL(c *gophercloud.ServiceClient) string { + return rootURL(c) +} + +func updateURL(c *gophercloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func deleteURL(c *gophercloud.ServiceClient, id string) string { + return resourceURL(c, id) +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/utils/base_endpoint.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/utils/base_endpoint.go new file mode 100644 index 00000000..40080f7a --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/utils/base_endpoint.go @@ -0,0 +1,28 @@ +package utils + +import ( + "net/url" + "regexp" + "strings" +) + +// BaseEndpoint will return a URL without the /vX.Y +// portion of the URL. +func BaseEndpoint(endpoint string) (string, error) { + u, err := url.Parse(endpoint) + if err != nil { + return "", err + } + + u.RawQuery, u.Fragment = "", "" + + path := u.Path + versionRe := regexp.MustCompile("v[0-9.]+/?") + + if version := versionRe.FindString(path); version != "" { + versionIndex := strings.Index(path, version) + u.Path = path[:versionIndex] + } + + return u.String(), nil +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/utils/choose_version.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/utils/choose_version.go new file mode 100644 index 00000000..27da19f9 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/openstack/utils/choose_version.go @@ -0,0 +1,111 @@ +package utils + +import ( + "fmt" + "strings" + + "github.com/gophercloud/gophercloud" +) + +// Version is a supported API version, corresponding to a vN package within the appropriate service. +type Version struct { + ID string + Suffix string + Priority int +} + +var goodStatus = map[string]bool{ + "current": true, + "supported": true, + "stable": true, +} + +// ChooseVersion queries the base endpoint of an API to choose the most recent non-experimental alternative from a service's +// published versions. +// It returns the highest-Priority Version among the alternatives that are provided, as well as its corresponding endpoint. +func ChooseVersion(client *gophercloud.ProviderClient, recognized []*Version) (*Version, string, error) { + type linkResp struct { + Href string `json:"href"` + Rel string `json:"rel"` + } + + type valueResp struct { + ID string `json:"id"` + Status string `json:"status"` + Links []linkResp `json:"links"` + } + + type versionsResp struct { + Values []valueResp `json:"values"` + } + + type response struct { + Versions versionsResp `json:"versions"` + } + + normalize := func(endpoint string) string { + if !strings.HasSuffix(endpoint, "/") { + return endpoint + "/" + } + return endpoint + } + identityEndpoint := normalize(client.IdentityEndpoint) + + // If a full endpoint is specified, check version suffixes for a match first. + for _, v := range recognized { + if strings.HasSuffix(identityEndpoint, v.Suffix) { + return v, identityEndpoint, nil + } + } + + var resp response + _, err := client.Request("GET", client.IdentityBase, &gophercloud.RequestOpts{ + JSONResponse: &resp, + OkCodes: []int{200, 300}, + }) + + if err != nil { + return nil, "", err + } + + var highest *Version + var endpoint string + + for _, value := range resp.Versions.Values { + href := "" + for _, link := range value.Links { + if link.Rel == "self" { + href = normalize(link.Href) + } + } + + for _, version := range recognized { + if strings.Contains(value.ID, version.ID) { + // Prefer a version that exactly matches the provided endpoint. + if href == identityEndpoint { + if href == "" { + return nil, "", fmt.Errorf("Endpoint missing in version %s response from %s", value.ID, client.IdentityBase) + } + return version, href, nil + } + + // Otherwise, find the highest-priority version with a whitelisted status. + if goodStatus[strings.ToLower(value.Status)] { + if highest == nil || version.Priority > highest.Priority { + highest = version + endpoint = href + } + } + } + } + } + + if highest == nil { + return nil, "", fmt.Errorf("No supported version available from endpoint %s", client.IdentityBase) + } + if endpoint == "" { + return nil, "", fmt.Errorf("Endpoint missing in version %s response from %s", highest.ID, client.IdentityBase) + } + + return highest, endpoint, nil +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/pagination/http.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/pagination/http.go new file mode 100644 index 00000000..7845cda1 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/pagination/http.go @@ -0,0 +1,62 @@ +package pagination + +import ( + "encoding/json" + "io/ioutil" + "net/http" + "net/url" + "strings" + + "github.com/gophercloud/gophercloud" +) + +// PageResult stores the HTTP response that returned the current page of results. +type PageResult struct { + gophercloud.Result + url.URL +} + +// PageResultFrom parses an HTTP response as JSON and returns a PageResult containing the +// results, interpreting it as JSON if the content type indicates. +func PageResultFrom(resp *http.Response) (PageResult, error) { + var parsedBody interface{} + + defer resp.Body.Close() + rawBody, err := ioutil.ReadAll(resp.Body) + if err != nil { + return PageResult{}, err + } + + if strings.HasPrefix(resp.Header.Get("Content-Type"), "application/json") { + err = json.Unmarshal(rawBody, &parsedBody) + if err != nil { + return PageResult{}, err + } + } else { + parsedBody = rawBody + } + + return PageResultFromParsed(resp, parsedBody), err +} + +// PageResultFromParsed constructs a PageResult from an HTTP response that has already had its +// body parsed as JSON (and closed). +func PageResultFromParsed(resp *http.Response, body interface{}) PageResult { + return PageResult{ + Result: gophercloud.Result{ + Body: body, + StatusCode: resp.StatusCode, + Header: resp.Header, + }, + URL: *resp.Request.URL, + } +} + +// Request performs an HTTP request and extracts the http.Response from the result. +func Request(client *gophercloud.ServiceClient, headers map[string]string, url string) (*http.Response, error) { + return client.Get(url, nil, &gophercloud.RequestOpts{ + MoreHeaders: headers, + OkCodes: []int{200, 204, 300}, + KeepResponseBody: true, + }) +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/pagination/linked.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/pagination/linked.go new file mode 100644 index 00000000..a664e056 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/pagination/linked.go @@ -0,0 +1,92 @@ +package pagination + +import ( + "fmt" + "reflect" + + "github.com/gophercloud/gophercloud" +) + +// LinkedPageBase may be embedded to implement a page that provides navigational "Next" and "Previous" links within its result. +type LinkedPageBase struct { + PageResult + + // LinkPath lists the keys that should be traversed within a response to arrive at the "next" pointer. + // If any link along the path is missing, an empty URL will be returned. + // If any link results in an unexpected value type, an error will be returned. + // When left as "nil", []string{"links", "next"} will be used as a default. + LinkPath []string +} + +// NextPageURL extracts the pagination structure from a JSON response and returns the "next" link, if one is present. +// It assumes that the links are available in a "links" element of the top-level response object. +// If this is not the case, override NextPageURL on your result type. +func (current LinkedPageBase) NextPageURL() (string, error) { + var path []string + var key string + + if current.LinkPath == nil { + path = []string{"links", "next"} + } else { + path = current.LinkPath + } + + submap, ok := current.Body.(map[string]interface{}) + if !ok { + err := gophercloud.ErrUnexpectedType{} + err.Expected = "map[string]interface{}" + err.Actual = fmt.Sprintf("%v", reflect.TypeOf(current.Body)) + return "", err + } + + for { + key, path = path[0], path[1:] + + value, ok := submap[key] + if !ok { + return "", nil + } + + if len(path) > 0 { + submap, ok = value.(map[string]interface{}) + if !ok { + err := gophercloud.ErrUnexpectedType{} + err.Expected = "map[string]interface{}" + err.Actual = fmt.Sprintf("%v", reflect.TypeOf(value)) + return "", err + } + } else { + if value == nil { + // Actual null element. + return "", nil + } + + url, ok := value.(string) + if !ok { + err := gophercloud.ErrUnexpectedType{} + err.Expected = "string" + err.Actual = fmt.Sprintf("%v", reflect.TypeOf(value)) + return "", err + } + + return url, nil + } + } +} + +// IsEmpty satisifies the IsEmpty method of the Page interface +func (current LinkedPageBase) IsEmpty() (bool, error) { + if b, ok := current.Body.([]interface{}); ok { + return len(b) == 0, nil + } + err := gophercloud.ErrUnexpectedType{} + err.Expected = "[]interface{}" + err.Actual = fmt.Sprintf("%v", reflect.TypeOf(current.Body)) + return true, err +} + +// GetBody returns the linked page's body. This method is needed to satisfy the +// Page interface. +func (current LinkedPageBase) GetBody() interface{} { + return current.Body +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/pagination/marker.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/pagination/marker.go new file mode 100644 index 00000000..52e53bae --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/pagination/marker.go @@ -0,0 +1,58 @@ +package pagination + +import ( + "fmt" + "reflect" + + "github.com/gophercloud/gophercloud" +) + +// MarkerPage is a stricter Page interface that describes additional functionality required for use with NewMarkerPager. +// For convenience, embed the MarkedPageBase struct. +type MarkerPage interface { + Page + + // LastMarker returns the last "marker" value on this page. + LastMarker() (string, error) +} + +// MarkerPageBase is a page in a collection that's paginated by "limit" and "marker" query parameters. +type MarkerPageBase struct { + PageResult + + // Owner is a reference to the embedding struct. + Owner MarkerPage +} + +// NextPageURL generates the URL for the page of results after this one. +func (current MarkerPageBase) NextPageURL() (string, error) { + currentURL := current.URL + + mark, err := current.Owner.LastMarker() + if err != nil { + return "", err + } + + q := currentURL.Query() + q.Set("marker", mark) + currentURL.RawQuery = q.Encode() + + return currentURL.String(), nil +} + +// IsEmpty satisifies the IsEmpty method of the Page interface +func (current MarkerPageBase) IsEmpty() (bool, error) { + if b, ok := current.Body.([]interface{}); ok { + return len(b) == 0, nil + } + err := gophercloud.ErrUnexpectedType{} + err.Expected = "[]interface{}" + err.Actual = fmt.Sprintf("%v", reflect.TypeOf(current.Body)) + return true, err +} + +// GetBody returns the linked page's body. This method is needed to satisfy the +// Page interface. +func (current MarkerPageBase) GetBody() interface{} { + return current.Body +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/pagination/pager.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/pagination/pager.go new file mode 100644 index 00000000..1dec2703 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/pagination/pager.go @@ -0,0 +1,254 @@ +package pagination + +import ( + "errors" + "fmt" + "net/http" + "reflect" + "strings" + + "github.com/gophercloud/gophercloud" +) + +var ( + // ErrPageNotAvailable is returned from a Pager when a next or previous page is requested, but does not exist. + ErrPageNotAvailable = errors.New("The requested page does not exist.") +) + +// Page must be satisfied by the result type of any resource collection. +// It allows clients to interact with the resource uniformly, regardless of whether or not or how it's paginated. +// Generally, rather than implementing this interface directly, implementors should embed one of the concrete PageBase structs, +// instead. +// Depending on the pagination strategy of a particular resource, there may be an additional subinterface that the result type +// will need to implement. +type Page interface { + // NextPageURL generates the URL for the page of data that follows this collection. + // Return "" if no such page exists. + NextPageURL() (string, error) + + // IsEmpty returns true if this Page has no items in it. + IsEmpty() (bool, error) + + // GetBody returns the Page Body. This is used in the `AllPages` method. + GetBody() interface{} +} + +// Pager knows how to advance through a specific resource collection, one page at a time. +type Pager struct { + client *gophercloud.ServiceClient + + initialURL string + + createPage func(r PageResult) Page + + firstPage Page + + Err error + + // Headers supplies additional HTTP headers to populate on each paged request. + Headers map[string]string +} + +// NewPager constructs a manually-configured pager. +// Supply the URL for the first page, a function that requests a specific page given a URL, and a function that counts a page. +func NewPager(client *gophercloud.ServiceClient, initialURL string, createPage func(r PageResult) Page) Pager { + return Pager{ + client: client, + initialURL: initialURL, + createPage: createPage, + } +} + +// WithPageCreator returns a new Pager that substitutes a different page creation function. This is +// useful for overriding List functions in delegation. +func (p Pager) WithPageCreator(createPage func(r PageResult) Page) Pager { + return Pager{ + client: p.client, + initialURL: p.initialURL, + createPage: createPage, + } +} + +func (p Pager) fetchNextPage(url string) (Page, error) { + resp, err := Request(p.client, p.Headers, url) + if err != nil { + return nil, err + } + + remembered, err := PageResultFrom(resp) + if err != nil { + return nil, err + } + + return p.createPage(remembered), nil +} + +// EachPage iterates over each page returned by a Pager, yielding one at a time to a handler function. +// Return "false" from the handler to prematurely stop iterating. +func (p Pager) EachPage(handler func(Page) (bool, error)) error { + if p.Err != nil { + return p.Err + } + currentURL := p.initialURL + for { + var currentPage Page + + // if first page has already been fetched, no need to fetch it again + if p.firstPage != nil { + currentPage = p.firstPage + p.firstPage = nil + } else { + var err error + currentPage, err = p.fetchNextPage(currentURL) + if err != nil { + return err + } + } + + empty, err := currentPage.IsEmpty() + if err != nil { + return err + } + if empty { + return nil + } + + ok, err := handler(currentPage) + if err != nil { + return err + } + if !ok { + return nil + } + + currentURL, err = currentPage.NextPageURL() + if err != nil { + return err + } + if currentURL == "" { + return nil + } + } +} + +// AllPages returns all the pages from a `List` operation in a single page, +// allowing the user to retrieve all the pages at once. +func (p Pager) AllPages() (Page, error) { + if p.Err != nil { + return nil, p.Err + } + // pagesSlice holds all the pages until they get converted into as Page Body. + var pagesSlice []interface{} + // body will contain the final concatenated Page body. + var body reflect.Value + + // Grab a first page to ascertain the page body type. + firstPage, err := p.fetchNextPage(p.initialURL) + if err != nil { + return nil, err + } + // Store the page type so we can use reflection to create a new mega-page of + // that type. + pageType := reflect.TypeOf(firstPage) + + // if it's a single page, just return the firstPage (first page) + if _, found := pageType.FieldByName("SinglePageBase"); found { + return firstPage, nil + } + + // store the first page to avoid getting it twice + p.firstPage = firstPage + + // Switch on the page body type. Recognized types are `map[string]interface{}`, + // `[]byte`, and `[]interface{}`. + switch pb := firstPage.GetBody().(type) { + case map[string]interface{}: + // key is the map key for the page body if the body type is `map[string]interface{}`. + var key string + // Iterate over the pages to concatenate the bodies. + err = p.EachPage(func(page Page) (bool, error) { + b := page.GetBody().(map[string]interface{}) + for k, v := range b { + // If it's a linked page, we don't want the `links`, we want the other one. + if !strings.HasSuffix(k, "links") { + // check the field's type. we only want []interface{} (which is really []map[string]interface{}) + switch vt := v.(type) { + case []interface{}: + key = k + pagesSlice = append(pagesSlice, vt...) + } + } + } + return true, nil + }) + if err != nil { + return nil, err + } + // Set body to value of type `map[string]interface{}` + body = reflect.MakeMap(reflect.MapOf(reflect.TypeOf(key), reflect.TypeOf(pagesSlice))) + body.SetMapIndex(reflect.ValueOf(key), reflect.ValueOf(pagesSlice)) + case []byte: + // Iterate over the pages to concatenate the bodies. + err = p.EachPage(func(page Page) (bool, error) { + b := page.GetBody().([]byte) + pagesSlice = append(pagesSlice, b) + // seperate pages with a comma + pagesSlice = append(pagesSlice, []byte{10}) + return true, nil + }) + if err != nil { + return nil, err + } + if len(pagesSlice) > 0 { + // Remove the trailing comma. + pagesSlice = pagesSlice[:len(pagesSlice)-1] + } + var b []byte + // Combine the slice of slices in to a single slice. + for _, slice := range pagesSlice { + b = append(b, slice.([]byte)...) + } + // Set body to value of type `bytes`. + body = reflect.New(reflect.TypeOf(b)).Elem() + body.SetBytes(b) + case []interface{}: + // Iterate over the pages to concatenate the bodies. + err = p.EachPage(func(page Page) (bool, error) { + b := page.GetBody().([]interface{}) + pagesSlice = append(pagesSlice, b...) + return true, nil + }) + if err != nil { + return nil, err + } + // Set body to value of type `[]interface{}` + body = reflect.MakeSlice(reflect.TypeOf(pagesSlice), len(pagesSlice), len(pagesSlice)) + for i, s := range pagesSlice { + body.Index(i).Set(reflect.ValueOf(s)) + } + default: + err := gophercloud.ErrUnexpectedType{} + err.Expected = "map[string]interface{}/[]byte/[]interface{}" + err.Actual = fmt.Sprintf("%T", pb) + return nil, err + } + + // Each `Extract*` function is expecting a specific type of page coming back, + // otherwise the type assertion in those functions will fail. pageType is needed + // to create a type in this method that has the same type that the `Extract*` + // function is expecting and set the Body of that object to the concatenated + // pages. + page := reflect.New(pageType) + // Set the page body to be the concatenated pages. + page.Elem().FieldByName("Body").Set(body) + // Set any additional headers that were pass along. The `objectstorage` pacakge, + // for example, passes a Content-Type header. + h := make(http.Header) + for k, v := range p.Headers { + h.Add(k, v) + } + page.Elem().FieldByName("Header").Set(reflect.ValueOf(h)) + // Type assert the page to a Page interface so that the type assertion in the + // `Extract*` methods will work. + return page.Elem().Interface().(Page), err +} diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/pagination/pkg.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/pagination/pkg.go new file mode 100644 index 00000000..912daea3 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/pagination/pkg.go @@ -0,0 +1,4 @@ +/* +Package pagination contains utilities and convenience structs that implement common pagination idioms within OpenStack APIs. +*/ +package pagination diff --git a/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/pagination/single.go b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/pagination/single.go new file mode 100644 index 00000000..4251d649 --- /dev/null +++ b/src/openstack_cpi_golang/vendor/github.com/gophercloud/gophercloud/pagination/single.go @@ -0,0 +1,33 @@ +package pagination + +import ( + "fmt" + "reflect" + + "github.com/gophercloud/gophercloud" +) + +// SinglePageBase may be embedded in a Page that contains all of the results from an operation at once. +type SinglePageBase PageResult + +// NextPageURL always returns "" to indicate that there are no more pages to return. +func (current SinglePageBase) NextPageURL() (string, error) { + return "", nil +} + +// IsEmpty satisifies the IsEmpty method of the Page interface +func (current SinglePageBase) IsEmpty() (bool, error) { + if b, ok := current.Body.([]interface{}); ok { + return len(b) == 0, nil + } + err := gophercloud.ErrUnexpectedType{} + err.Expected = "[]interface{}" + err.Actual = fmt.Sprintf("%v", reflect.TypeOf(current.Body)) + return true, err +} + +// GetBody returns the single page's body. This method is needed to satisfy the +// Page interface. +func (current SinglePageBase) GetBody() interface{} { + return current.Body +} diff --git a/src/openstack_cpi_golang/vendor/modules.txt b/src/openstack_cpi_golang/vendor/modules.txt index 5ddaccf2..5cac42a2 100644 --- a/src/openstack_cpi_golang/vendor/modules.txt +++ b/src/openstack_cpi_golang/vendor/modules.txt @@ -24,9 +24,40 @@ github.com/google/go-cmp/cmp/internal/value # github.com/google/pprof v0.0.0-20240207164012-fb44976bdcd5 ## explicit; go 1.19 github.com/google/pprof/profile +# github.com/google/uuid v1.6.0 +## explicit +github.com/google/uuid # github.com/gophercloud/gophercloud v1.12.0 ## explicit; go 1.14 github.com/gophercloud/gophercloud +github.com/gophercloud/gophercloud/openstack +github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/volumeactions +github.com/gophercloud/gophercloud/openstack/blockstorage/v3/snapshots +github.com/gophercloud/gophercloud/openstack/blockstorage/v3/volumes +github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/availabilityzones +github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/bootfromvolume +github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs +github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/volumeattach +github.com/gophercloud/gophercloud/openstack/compute/v2/flavors +github.com/gophercloud/gophercloud/openstack/compute/v2/servers +github.com/gophercloud/gophercloud/openstack/identity/v2/tenants +github.com/gophercloud/gophercloud/openstack/identity/v2/tokens +github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/ec2tokens +github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/oauth1 +github.com/gophercloud/gophercloud/openstack/identity/v3/tokens +github.com/gophercloud/gophercloud/openstack/imageservice/v2/images +github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/l7policies +github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/listeners +github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/loadbalancers +github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/monitors +github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/pools +github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips +github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/groups +github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/rules +github.com/gophercloud/gophercloud/openstack/networking/v2/ports +github.com/gophercloud/gophercloud/openstack/networking/v2/subnets +github.com/gophercloud/gophercloud/openstack/utils +github.com/gophercloud/gophercloud/pagination # github.com/maxbrunsfeld/counterfeiter/v6 v6.8.1 ## explicit; go 1.20 github.com/maxbrunsfeld/counterfeiter/v6