|
15 | 15 | # under the License.
|
16 | 16 |
|
17 | 17 | """Fixtures for Nova tests."""
|
| 18 | + |
18 | 19 | import collections
|
19 | 20 | from contextlib import contextmanager
|
20 | 21 | import copy
|
| 22 | +import datetime |
21 | 23 | import logging as std_logging
|
22 | 24 | import os
|
23 | 25 | import random
|
|
56 | 58 | from nova.network import model as network_model
|
57 | 59 | from nova import objects
|
58 | 60 | from nova.objects import base as obj_base
|
| 61 | +from nova.objects import fields as obj_fields |
59 | 62 | from nova.objects import service as service_obj
|
60 | 63 | import nova.privsep
|
61 | 64 | from nova import quota as nova_quota
|
@@ -2046,6 +2049,318 @@ def setUp(self):
|
2046 | 2049 | lambda *args, **kwargs: mock.MagicMock()))
|
2047 | 2050 |
|
2048 | 2051 |
|
| 2052 | +class GlanceFixture(fixtures.Fixture): |
| 2053 | + """A fixture for simulating Glance.""" |
| 2054 | + |
| 2055 | + # NOTE(justinsb): The OpenStack API can't upload an image? |
| 2056 | + # So, make sure we've got one.. |
| 2057 | + timestamp = datetime.datetime(2011, 1, 1, 1, 2, 3) |
| 2058 | + |
| 2059 | + image1 = { |
| 2060 | + 'id': '155d900f-4e14-4e4c-a73d-069cbf4541e6', |
| 2061 | + 'name': 'fakeimage123456', |
| 2062 | + 'created_at': timestamp, |
| 2063 | + 'updated_at': timestamp, |
| 2064 | + 'deleted_at': None, |
| 2065 | + 'deleted': False, |
| 2066 | + 'status': 'active', |
| 2067 | + 'is_public': False, |
| 2068 | + 'container_format': 'raw', |
| 2069 | + 'disk_format': 'raw', |
| 2070 | + 'size': '25165824', |
| 2071 | + 'min_ram': 0, |
| 2072 | + 'min_disk': 0, |
| 2073 | + 'protected': False, |
| 2074 | + 'visibility': 'public', |
| 2075 | + 'tags': ['tag1', 'tag2'], |
| 2076 | + 'properties': { |
| 2077 | + 'kernel_id': 'nokernel', |
| 2078 | + 'ramdisk_id': 'nokernel', |
| 2079 | + 'architecture': obj_fields.Architecture.X86_64, |
| 2080 | + }, |
| 2081 | + } |
| 2082 | + |
| 2083 | + image2 = { |
| 2084 | + 'id': 'a2459075-d96c-40d5-893e-577ff92e721c', |
| 2085 | + 'name': 'fakeimage123456', |
| 2086 | + 'created_at': timestamp, |
| 2087 | + 'updated_at': timestamp, |
| 2088 | + 'deleted_at': None, |
| 2089 | + 'deleted': False, |
| 2090 | + 'status': 'active', |
| 2091 | + 'is_public': True, |
| 2092 | + 'container_format': 'ami', |
| 2093 | + 'disk_format': 'ami', |
| 2094 | + 'size': '58145823', |
| 2095 | + 'min_ram': 0, |
| 2096 | + 'min_disk': 0, |
| 2097 | + 'protected': False, |
| 2098 | + 'visibility': 'public', |
| 2099 | + 'tags': [], |
| 2100 | + 'properties': { |
| 2101 | + 'kernel_id': 'nokernel', |
| 2102 | + 'ramdisk_id': 'nokernel', |
| 2103 | + }, |
| 2104 | + } |
| 2105 | + |
| 2106 | + image3 = { |
| 2107 | + 'id': '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6', |
| 2108 | + 'name': 'fakeimage123456', |
| 2109 | + 'created_at': timestamp, |
| 2110 | + 'updated_at': timestamp, |
| 2111 | + 'deleted_at': None, |
| 2112 | + 'deleted': False, |
| 2113 | + 'status': 'active', |
| 2114 | + 'is_public': True, |
| 2115 | + 'container_format': 'bare', |
| 2116 | + 'disk_format': 'raw', |
| 2117 | + 'size': '83594576', |
| 2118 | + 'min_ram': 0, |
| 2119 | + 'min_disk': 0, |
| 2120 | + 'protected': False, |
| 2121 | + 'visibility': 'public', |
| 2122 | + 'tags': ['tag3', 'tag4'], |
| 2123 | + 'properties': { |
| 2124 | + 'kernel_id': 'nokernel', |
| 2125 | + 'ramdisk_id': 'nokernel', |
| 2126 | + 'architecture': obj_fields.Architecture.X86_64, |
| 2127 | + }, |
| 2128 | + } |
| 2129 | + |
| 2130 | + image4 = { |
| 2131 | + 'id': 'cedef40a-ed67-4d10-800e-17455edce175', |
| 2132 | + 'name': 'fakeimage123456', |
| 2133 | + 'created_at': timestamp, |
| 2134 | + 'updated_at': timestamp, |
| 2135 | + 'deleted_at': None, |
| 2136 | + 'deleted': False, |
| 2137 | + 'status': 'active', |
| 2138 | + 'is_public': True, |
| 2139 | + 'container_format': 'ami', |
| 2140 | + 'disk_format': 'ami', |
| 2141 | + 'size': '84035174', |
| 2142 | + 'min_ram': 0, |
| 2143 | + 'min_disk': 0, |
| 2144 | + 'protected': False, |
| 2145 | + 'visibility': 'public', |
| 2146 | + 'tags': [], |
| 2147 | + 'properties': { |
| 2148 | + 'kernel_id': 'nokernel', |
| 2149 | + 'ramdisk_id': 'nokernel', |
| 2150 | + }, |
| 2151 | + } |
| 2152 | + |
| 2153 | + image5 = { |
| 2154 | + 'id': 'c905cedb-7281-47e4-8a62-f26bc5fc4c77', |
| 2155 | + 'name': 'fakeimage123456', |
| 2156 | + 'created_at': timestamp, |
| 2157 | + 'updated_at': timestamp, |
| 2158 | + 'deleted_at': None, |
| 2159 | + 'deleted': False, |
| 2160 | + 'status': 'active', |
| 2161 | + 'is_public': True, |
| 2162 | + 'container_format': 'ami', |
| 2163 | + 'disk_format': 'ami', |
| 2164 | + 'size': '26360814', |
| 2165 | + 'min_ram': 0, |
| 2166 | + 'min_disk': 0, |
| 2167 | + 'protected': False, |
| 2168 | + 'visibility': 'public', |
| 2169 | + 'tags': [], |
| 2170 | + 'properties': { |
| 2171 | + 'kernel_id': '155d900f-4e14-4e4c-a73d-069cbf4541e6', |
| 2172 | + 'ramdisk_id': None, |
| 2173 | + }, |
| 2174 | + } |
| 2175 | + |
| 2176 | + auto_disk_config_disabled_image = { |
| 2177 | + 'id': 'a440c04b-79fa-479c-bed1-0b816eaec379', |
| 2178 | + 'name': 'fakeimage6', |
| 2179 | + 'created_at': timestamp, |
| 2180 | + 'updated_at': timestamp, |
| 2181 | + 'deleted_at': None, |
| 2182 | + 'deleted': False, |
| 2183 | + 'status': 'active', |
| 2184 | + 'is_public': False, |
| 2185 | + 'container_format': 'ova', |
| 2186 | + 'disk_format': 'vhd', |
| 2187 | + 'size': '49163826', |
| 2188 | + 'min_ram': 0, |
| 2189 | + 'min_disk': 0, |
| 2190 | + 'protected': False, |
| 2191 | + 'visibility': 'public', |
| 2192 | + 'tags': [], |
| 2193 | + 'properties': { |
| 2194 | + 'kernel_id': 'nokernel', |
| 2195 | + 'ramdisk_id': 'nokernel', |
| 2196 | + 'architecture': obj_fields.Architecture.X86_64, |
| 2197 | + 'auto_disk_config': 'False', |
| 2198 | + }, |
| 2199 | + } |
| 2200 | + |
| 2201 | + auto_disk_config_enabled_image = { |
| 2202 | + 'id': '70a599e0-31e7-49b7-b260-868f441e862b', |
| 2203 | + 'name': 'fakeimage7', |
| 2204 | + 'created_at': timestamp, |
| 2205 | + 'updated_at': timestamp, |
| 2206 | + 'deleted_at': None, |
| 2207 | + 'deleted': False, |
| 2208 | + 'status': 'active', |
| 2209 | + 'is_public': False, |
| 2210 | + 'container_format': 'ova', |
| 2211 | + 'disk_format': 'vhd', |
| 2212 | + 'size': '74185822', |
| 2213 | + 'min_ram': 0, |
| 2214 | + 'min_disk': 0, |
| 2215 | + 'protected': False, |
| 2216 | + 'visibility': 'public', |
| 2217 | + 'tags': [], |
| 2218 | + 'properties': { |
| 2219 | + 'kernel_id': 'nokernel', |
| 2220 | + 'ramdisk_id': 'nokernel', |
| 2221 | + 'architecture': obj_fields.Architecture.X86_64, |
| 2222 | + 'auto_disk_config': 'True', |
| 2223 | + }, |
| 2224 | + } |
| 2225 | + |
| 2226 | + def __init__(self, test): |
| 2227 | + super().__init__() |
| 2228 | + self.test = test |
| 2229 | + self.images = {} |
| 2230 | + |
| 2231 | + def setUp(self): |
| 2232 | + super().setUp() |
| 2233 | + |
| 2234 | + self.test.useFixture( |
| 2235 | + ConfPatcher(group='glance', api_servers=['http://localhost:9292']) |
| 2236 | + ) |
| 2237 | + self.test.stub_out( |
| 2238 | + 'nova.image.glance.API.get_remote_image_service', |
| 2239 | + lambda context, image_href: (self, image_href)) |
| 2240 | + self.test.stub_out( |
| 2241 | + 'nova.image.glance.get_default_image_service', |
| 2242 | + lambda: self) |
| 2243 | + |
| 2244 | + self.create(None, self.image1) |
| 2245 | + self.create(None, self.image2) |
| 2246 | + self.create(None, self.image3) |
| 2247 | + self.create(None, self.image4) |
| 2248 | + self.create(None, self.image5) |
| 2249 | + self.create(None, self.auto_disk_config_disabled_image) |
| 2250 | + self.create(None, self.auto_disk_config_enabled_image) |
| 2251 | + |
| 2252 | + self._imagedata = {} |
| 2253 | + |
| 2254 | + # TODO(bcwaldon): implement optional kwargs such as limit, sort_dir |
| 2255 | + def detail(self, context, **kwargs): |
| 2256 | + """Return list of detailed image information.""" |
| 2257 | + return copy.deepcopy(list(self.images.values())) |
| 2258 | + |
| 2259 | + def download( |
| 2260 | + self, context, image_id, data=None, dst_path=None, trusted_certs=None, |
| 2261 | + ): |
| 2262 | + self.show(context, image_id) |
| 2263 | + if data: |
| 2264 | + data.write(self._imagedata.get(image_id, b'')) |
| 2265 | + elif dst_path: |
| 2266 | + with open(dst_path, 'wb') as data: |
| 2267 | + data.write(self._imagedata.get(image_id, b'')) |
| 2268 | + |
| 2269 | + def show( |
| 2270 | + self, context, image_id, include_locations=False, show_deleted=True, |
| 2271 | + ): |
| 2272 | + """Get data about specified image. |
| 2273 | +
|
| 2274 | + Returns a dict containing image data for the given opaque image id. |
| 2275 | + """ |
| 2276 | + image = self.images.get(str(image_id)) |
| 2277 | + if image: |
| 2278 | + return copy.deepcopy(image) |
| 2279 | + |
| 2280 | + LOG.warning( |
| 2281 | + 'Unable to find image id %s. Have images: %s', |
| 2282 | + image_id, self.images) |
| 2283 | + raise exception.ImageNotFound(image_id=image_id) |
| 2284 | + |
| 2285 | + def create(self, context, metadata, data=None): |
| 2286 | + """Store the image data and return the new image id. |
| 2287 | +
|
| 2288 | + :raises: Duplicate if the image already exist. |
| 2289 | +
|
| 2290 | + """ |
| 2291 | + image_id = str(metadata.get('id', uuidutils.generate_uuid())) |
| 2292 | + metadata['id'] = image_id |
| 2293 | + if image_id in self.images: |
| 2294 | + raise exception.CouldNotUploadImage(image_id=image_id) |
| 2295 | + |
| 2296 | + image_meta = copy.deepcopy(metadata) |
| 2297 | + |
| 2298 | + # Glance sets the size value when an image is created, so we |
| 2299 | + # need to do that here to fake things out if it's not provided |
| 2300 | + # by the caller. This is needed to avoid a KeyError in the |
| 2301 | + # image-size API. |
| 2302 | + if 'size' not in image_meta: |
| 2303 | + image_meta['size'] = None |
| 2304 | + |
| 2305 | + # Similarly, Glance provides the status on the image once it's created |
| 2306 | + # and this is checked in the compute API when booting a server from |
| 2307 | + # this image, so we just fake it out to be 'active' even though this |
| 2308 | + # is mostly a lie on a newly created image. |
| 2309 | + if 'status' not in metadata: |
| 2310 | + image_meta['status'] = 'active' |
| 2311 | + |
| 2312 | + # The owner of the image is by default the request context project_id. |
| 2313 | + if context and 'owner' not in image_meta.get('properties', {}): |
| 2314 | + # Note that normally "owner" is a top-level field in an image |
| 2315 | + # resource in glance but we have to fake this out for the images |
| 2316 | + # proxy API by throwing it into the generic "properties" dict. |
| 2317 | + image_meta.get('properties', {})['owner'] = context.project_id |
| 2318 | + |
| 2319 | + self.images[image_id] = image_meta |
| 2320 | + |
| 2321 | + if data: |
| 2322 | + self._imagedata[image_id] = data.read() |
| 2323 | + |
| 2324 | + return self.images[image_id] |
| 2325 | + |
| 2326 | + def update(self, context, image_id, metadata, data=None, |
| 2327 | + purge_props=False): |
| 2328 | + """Replace the contents of the given image with the new data. |
| 2329 | +
|
| 2330 | + :raises: ImageNotFound if the image does not exist. |
| 2331 | + """ |
| 2332 | + if not self.images.get(image_id): |
| 2333 | + raise exception.ImageNotFound(image_id=image_id) |
| 2334 | + |
| 2335 | + if purge_props: |
| 2336 | + self.images[image_id] = copy.deepcopy(metadata) |
| 2337 | + else: |
| 2338 | + image = self.images[image_id] |
| 2339 | + |
| 2340 | + try: |
| 2341 | + image['properties'].update(metadata.pop('properties')) |
| 2342 | + except KeyError: |
| 2343 | + pass |
| 2344 | + |
| 2345 | + image.update(metadata) |
| 2346 | + |
| 2347 | + return self.images[image_id] |
| 2348 | + |
| 2349 | + def delete(self, context, image_id): |
| 2350 | + """Delete the given image. |
| 2351 | +
|
| 2352 | + :raises: ImageNotFound if the image does not exist. |
| 2353 | + """ |
| 2354 | + removed = self.images.pop(image_id, None) |
| 2355 | + if not removed: |
| 2356 | + raise exception.ImageNotFound(image_id=image_id) |
| 2357 | + |
| 2358 | + def get_location(self, context, image_id): |
| 2359 | + if image_id in self.images: |
| 2360 | + return 'fake_location' |
| 2361 | + return None |
| 2362 | + |
| 2363 | + |
2049 | 2364 | class CinderFixture(fixtures.Fixture):
|
2050 | 2365 | """A fixture to volume operations with the new Cinder attach/detach API"""
|
2051 | 2366 |
|
|
0 commit comments