diff --git a/libcloud/compute/drivers/openstack.py b/libcloud/compute/drivers/openstack.py index 023d93c6bd..f18e5ab3ea 100644 --- a/libcloud/compute/drivers/openstack.py +++ b/libcloud/compute/drivers/openstack.py @@ -114,6 +114,12 @@ class OpenStackVolumeV3Connection(OpenStackBaseConnection): service_region = "RegionOne" +class OpenStackReservationConnection(OpenStackBaseConnection): + service_type = "reservation" + service_name = "blazar" + service_region = "RegionOne" + + class OpenStackNodeDriver(NodeDriver, OpenStackDriverMixin): """ Base OpenStack node driver. Should not be used directly. @@ -2813,6 +2819,15 @@ def encode_data(self, data): return json.dumps(data) +class OpenStack_2_ReservationConnection(OpenStackReservationConnection): + responseCls = OpenStack_1_1_Response + accept_format = "application/json" + default_content_type = "application/json; charset=UTF-8" + + def encode_data(self, data): + return json.dumps(data) + + class OpenStack_2_PortInterfaceState(Type): """ Standard states of OpenStack_2_PortInterfaceState @@ -2873,6 +2888,10 @@ class OpenStack_2_NodeDriver(OpenStack_1_1_NodeDriver): volumev3_connection = None volume_connection = None + # Connection to the Blazar reservation API + reservation_connectionCls = OpenStack_2_ReservationConnection + reservation_connection = None + type = Provider.OPENSTACK features = {"create_node": ["generates_password"]} @@ -2929,6 +2948,16 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.network_connection = self.connection + # We run the init once to get the Blazar API connection + # and put that on the object under self.reservation_connection. + if original_ex_force_base_url or kwargs.get("ex_force_reservation_url"): + kwargs["ex_force_base_url"] = str( + kwargs.pop("ex_force_reservation_url", original_ex_force_base_url) + ) + self.connectionCls = self.reservation_connectionCls + super().__init__(*args, **kwargs) + self.reservation_connection = self.connection + # We run the init once again to get the compute API connection # and that's put under self.connection as normal. self._ex_force_base_url = original_ex_force_base_url @@ -4368,6 +4397,100 @@ def ex_detach_floating_ip_from_node(self, node, ip): ) return resp.status == httplib.OK + def ex_list_leases(self): + """ + List leases + + :rtype: ``list`` of :class:`OpenStack_2_Lease` + """ + return self._to_leases(self.reservation_connection.request("/leases").object) + + def _to_leases(self, obj): + lease_elements = obj["leases"] + return [self._to_lease(lease) for lease in lease_elements] + + def _to_lease(self, obj): + return OpenStack_2_Lease( + id=obj["id"], + name=obj["name"], + start=obj["start_date"], + end=obj["end_date"], + status=obj["status"], + reservations=obj["reservations"], + driver=self.reservation_connection.driver, + ) + + def ex_list_hosts(self): + """ + List leases + + :rtype: ``list`` of :class:`OpenStack_2_Host` + """ + return self._to_hosts(self.reservation_connection.request("/os-hosts").object) + + def _to_hosts(self, obj): + host_elements = obj["hosts"] + return [self._to_host(host) for host in host_elements] + + def _to_host(self, obj): + return OpenStack_2_Host( + id=obj["id"], + hypervisor_hostname=obj["hypervisor_hostname"], + vcpus=obj["vcpus"], + memory_mb=obj["memory_mb"], + local_gb=obj["local_gb"], + service_name=obj["service_name"], + ) + + +class OpenStack_2_Host: + """ + Host info. + """ + + def __init__(self, id, hypervisor_hostname, vcpus, memory_mb, local_gb, service_name): + self.id = id + self.hypervisor_hostname = hypervisor_hostname + self.vcpus = vcpus + self.memory_mb = memory_mb + self.local_gb = local_gb + self.service_name = service_name + + def __repr__(self): + return "".format( + self.id, + self.hypervisor_hostname, + ) + + +class OpenStack_2_Lease: + """ + Lease info. + """ + + PENDING = "PENDING" + ACTIVE = "ACTIVE" + TERMINATED = "TERMINATED" + DELETED = "DELETED" + CREATING = "CREATING" + UPDATING = "UPDATING" + DELETING = "DELETING" + ERROR = "ERROR" + + def __init__(self, id, name, start, end, status, reservations, driver): + self.id = id + self.name = name + self.start = start + self.end = end + self.status = status + self.reservations = reservations + self.driver = driver + + def __repr__(self): + return "".format( + self.id, self.name, self.status + ) + class OpenStack_1_1_FloatingIpPool: """ diff --git a/libcloud/test/common/test_openstack_identity.py b/libcloud/test/common/test_openstack_identity.py index 2e17cf5737..480112d0ea 100644 --- a/libcloud/test/common/test_openstack_identity.py +++ b/libcloud/test/common/test_openstack_identity.py @@ -833,7 +833,7 @@ def test_parsing_auth_v2(self): catalog = OpenStackServiceCatalog(service_catalog=service_catalog, auth_version="2.0") entries = catalog.get_entries() - self.assertEqual(len(entries), 10) + self.assertEqual(len(entries), 11) entry = [e for e in entries if e.service_name == "cloudServers"][0] self.assertEqual(entry.service_type, "compute") @@ -904,6 +904,7 @@ def test_get_service_types(self): "network", "object-store", "rax:object-cdn", + "reservation", "volumev2", "volumev3", ], @@ -923,6 +924,7 @@ def test_get_service_names(self): self.assertEqual( service_names, [ + "blazar", "cinderv2", "cinderv3", "cloudFiles", diff --git a/libcloud/test/compute/fixtures/openstack/_v2_0__auth.json b/libcloud/test/compute/fixtures/openstack/_v2_0__auth.json index 70c8ba8b73..3c9f5a7056 100644 --- a/libcloud/test/compute/fixtures/openstack/_v2_0__auth.json +++ b/libcloud/test/compute/fixtures/openstack/_v2_0__auth.json @@ -164,6 +164,20 @@ "name": "cinderv3", "type": "volumev3" }, + { + "endpoints": [ + { + "region": "RegionOne", + "tenantId": "1337", + "publicURL": "https://test_endpoint.com/v1", + "versionInfo": "https://test_endpoint.com/v1/", + "versionList": "https://test_endpoint.com/", + "versionId": "1" + } + ], + "name": "blazar", + "type": "reservation" + }, { "endpoints": [ { diff --git a/libcloud/test/compute/fixtures/openstack_v1_1/_leases.json b/libcloud/test/compute/fixtures/openstack_v1_1/_leases.json new file mode 100644 index 0000000000..2acb5fd9c4 --- /dev/null +++ b/libcloud/test/compute/fixtures/openstack_v1_1/_leases.json @@ -0,0 +1,84 @@ +{ + "leases": [ + { + "id": "6ee55c78-ac52-41a6-99af-2d2d73bcc466", + "name": "lease_foo", + "start_date": "2017-12-26T12:00:00.000000", + "end_date": "2017-12-27T12:00:00.000000", + "status":"PENDING", + "degraded": false, + "user_id": "5434f637520d4c17bbf254af034b0320", + "project_id": "aa45f56901ef45ee95e3d211097c0ea3", + "trust_id": "b442a580b9504ababf305bf2b4c49512", + "created_at": "2017-12-27 10:00:00", + "updated_at": null, + "reservations": [ + { + "id": "087bc740-6d2d-410b-9d47-c7b2b55a9d36", + "lease_id": "6ee55c78-ac52-41a6-99af-2d2d73bcc466", + "status": "pending", + "missing_resources": false, + "resources_changed": false, + "resource_id": "5e6c0e6e-f1e6-490b-baaf-50deacbbe371", + "resource_type": "physical:host", + "min": 4, + "max": 6, + "hypervisor_properties": "[\">=\", \"$vcpus\", \"4\"]", + "resource_properties": "", + "before_end": "default", + "created_at": "2017-12-27 10:00:00", + "updated_at": null + }, + { + "id": "ddc45423-f863-4e4e-8e7a-51d27cfec962", + "lease_id": "6ee55c78-ac52-41a6-99af-2d2d73bcc466", + "status": "pending", + "missing_resources": false, + "resources_changed": false, + "resource_id": "0b901727-cca2-43ed-bcc8-c21b0982dcb1", + "resource_type": "virtual:instance", + "amount": 4, + "vcpus": 2, + "memory_mb": 4096, + "disk_gb": 100, + "affinity": false, + "resource_properties": "", + "flavor_id": "ddc45423-f863-4e4e-8e7a-51d27cfec962", + "server_group_id": "33cdfc42-5a04-4fcc-b190-1abebaa056bb", + "aggregate_id": 11, + "created_at": "2017-12-27 10:00:00", + "updated_at": null + } + ], + "events": [ + { + "id": "188a8584-f832-4df9-9a4a-51e6364420ff", + "lease_id": "6ee55c78-ac52-41a6-99af-2d2d73bcc466", + "status": "UNDONE", + "event_type": "start_lease", + "time": "2017-12-26T12:00:00.000000", + "created_at": "2017-12-27 10:00:00", + "updated_at": null + }, + { + "id": "277d6436-dfcb-4eae-ae5e-ac7fa9c2fd56", + "lease_id": "6ee55c78-ac52-41a6-99af-2d2d73bcc466", + "status": "UNDONE", + "event_type": "end_lease", + "time": "2017-12-27T12:00:00.000000", + "created_at": "2017-12-27 10:00:00", + "updated_at": null + }, + { + "id": "f583af71-ca21-4b66-87de-52211d118029", + "lease_id": "6ee55c78-ac52-41a6-99af-2d2d73bcc466", + "status": "UNDONE", + "time": "2017-12-27T11:00:00.000000", + "event_type": "before_end_lease", + "created_at": "2017-12-27 10:00:00", + "updated_at": null + } + ] + } + ] +} \ No newline at end of file diff --git a/libcloud/test/compute/fixtures/openstack_v1_1/_os_hosts.json b/libcloud/test/compute/fixtures/openstack_v1_1/_os_hosts.json new file mode 100644 index 0000000000..ff773eb143 --- /dev/null +++ b/libcloud/test/compute/fixtures/openstack_v1_1/_os_hosts.json @@ -0,0 +1,21 @@ +{ + "hosts": [ + { + "id": "1", + "hypervisor_hostname": "compute-1", + "hypervisor_type": "QEMU", + "hypervisor_version": 2010001, + "vcpus": 4, + "cpu_info": "{'arch': 'x86_64', 'model': 'cpu64-rhel6', 'vendor': 'Intel'}", + "memory_mb": 8192, + "local_gb": 100, + "service_name": "compute-1", + "reservable": true, + "status": null, + "trust_id": "5f67f11215cf4c52906453a181bfcfea", + "created_at": "2017-12-27 10:00:00", + "updated_at": null, + "extra_capability_sample": "foo" + } + ] +} \ No newline at end of file diff --git a/libcloud/test/compute/test_openstack.py b/libcloud/test/compute/test_openstack.py index 90e4061f4f..0b33b6c5df 100644 --- a/libcloud/test/compute/test_openstack.py +++ b/libcloud/test/compute/test_openstack.py @@ -2005,6 +2005,11 @@ def setUp(self): # normally authentication happens lazily, but we force it here self.driver.volumev3_connection._populate_hosts_and_request_paths() + self.driver_klass.reservation_connectionCls.conn_class = OpenStack_2_0_MockHttp + self.driver_klass.reservation_connectionCls.auth_url = "https://auth.api.example.com" + # normally authentication happens lazily, but we force it here + self.driver.reservation_connection._populate_hosts_and_request_paths() + def test__paginated_request_single_page(self): snapshots = self.driver._paginated_request( "/snapshots/detail", "snapshots", self.driver._get_volume_connection() @@ -2547,6 +2552,22 @@ def test_ex_delete_floating_ip(self): ip = OpenStack_1_1_FloatingIpAddress("foo-bar-id", "42.42.42.42", None) self.assertTrue(self.driver.ex_delete_floating_ip(ip)) + def test_ex_list_leases(self): + leases = self.driver.ex_list_leases() + self.assertEqual(len(leases), 1) + self.assertEqual(leases[0].id, "6ee55c78-ac52-41a6-99af-2d2d73bcc466") + self.assertEqual(leases[0].name, "lease_foo") + self.assertEqual(leases[0].start, "2017-12-26T12:00:00.000000") + self.assertEqual(leases[0].end, "2017-12-27T12:00:00.000000") + self.assertEqual(leases[0].status, "PENDING") + + def test_ex_list_hosts(self): + hosts = self.driver.ex_list_hosts() + self.assertEqual(len(hosts), 1) + self.assertEqual(hosts[0].id, "1") + self.assertEqual(hosts[0].hypervisor_hostname, "compute-1") + self.assertEqual(hosts[0].vcpus, 4) + def test_ex_attach_floating_ip_to_node(self): image = NodeImage(id=11, name="Ubuntu 8.10 (intrepid)", driver=self.driver) size = NodeSize(1, "256 slice", None, None, None, None, driver=self.driver) @@ -4004,6 +4025,28 @@ def _v2_1337_servers_4242_os_interface(self, method, url, body, headers): httplib.responses[httplib.OK], ) + def _v1_leases(self, method, url, body, headers): + if method == "GET": + body = self.fixtures.load("_leases.json") + + return ( + httplib.OK, + body, + self.json_content_headers, + httplib.responses[httplib.OK], + ) + + def _v1_os_hosts(self, method, url, body, headers): + if method == "GET": + body = self.fixtures.load("_os_hosts.json") + + return ( + httplib.OK, + body, + self.json_content_headers, + httplib.responses[httplib.OK], + ) + # This exists because the nova compute url in devstack has v2 in there but the v1.1 fixtures # work fine.