cloudbridge 4.2.1__tar.gz → 4.3.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {cloudbridge-4.2.1 → cloudbridge-4.3.1}/PKG-INFO +1 -1
- {cloudbridge-4.2.1 → cloudbridge-4.3.1}/cloudbridge/__init__.py +1 -1
- {cloudbridge-4.2.1 → cloudbridge-4.3.1}/cloudbridge/base/resources.py +110 -8
- {cloudbridge-4.2.1 → cloudbridge-4.3.1}/cloudbridge/interfaces/resources.py +50 -16
- {cloudbridge-4.2.1 → cloudbridge-4.3.1}/cloudbridge/interfaces/services.py +27 -0
- {cloudbridge-4.2.1 → cloudbridge-4.3.1}/cloudbridge/providers/aws/resources.py +18 -6
- {cloudbridge-4.2.1 → cloudbridge-4.3.1}/cloudbridge/providers/aws/services.py +59 -10
- {cloudbridge-4.2.1 → cloudbridge-4.3.1}/cloudbridge/providers/azure/azure_client.py +6 -0
- {cloudbridge-4.2.1 → cloudbridge-4.3.1}/cloudbridge/providers/azure/resources.py +12 -2
- {cloudbridge-4.2.1 → cloudbridge-4.3.1}/cloudbridge/providers/azure/services.py +8 -0
- {cloudbridge-4.2.1 → cloudbridge-4.3.1}/cloudbridge/providers/gcp/resources.py +2 -2
- {cloudbridge-4.2.1 → cloudbridge-4.3.1}/cloudbridge/providers/gcp/services.py +11 -0
- {cloudbridge-4.2.1 → cloudbridge-4.3.1}/cloudbridge/providers/openstack/resources.py +2 -2
- {cloudbridge-4.2.1 → cloudbridge-4.3.1}/cloudbridge/providers/openstack/services.py +10 -0
- {cloudbridge-4.2.1 → cloudbridge-4.3.1}/cloudbridge.egg-info/PKG-INFO +1 -1
- {cloudbridge-4.2.1 → cloudbridge-4.3.1}/cloudbridge.egg-info/SOURCES.txt +3 -0
- cloudbridge-4.3.1/tests/test_aws_dns_waiters.py +141 -0
- cloudbridge-4.3.1/tests/test_aws_vm_types.py +166 -0
- cloudbridge-4.3.1/tests/test_download_driver.py +306 -0
- {cloudbridge-4.2.1 → cloudbridge-4.3.1}/tests/test_multipart_driver.py +2 -2
- {cloudbridge-4.2.1 → cloudbridge-4.3.1}/tests/test_object_store_service.py +97 -8
- {cloudbridge-4.2.1 → cloudbridge-4.3.1}/LICENSE +0 -0
- {cloudbridge-4.2.1 → cloudbridge-4.3.1}/README.rst +0 -0
- {cloudbridge-4.2.1 → cloudbridge-4.3.1}/cloudbridge/base/__init__.py +0 -0
- {cloudbridge-4.2.1 → cloudbridge-4.3.1}/cloudbridge/base/helpers.py +0 -0
- {cloudbridge-4.2.1 → cloudbridge-4.3.1}/cloudbridge/base/middleware.py +0 -0
- {cloudbridge-4.2.1 → cloudbridge-4.3.1}/cloudbridge/base/provider.py +0 -0
- {cloudbridge-4.2.1 → cloudbridge-4.3.1}/cloudbridge/base/services.py +0 -0
- {cloudbridge-4.2.1 → cloudbridge-4.3.1}/cloudbridge/base/subservices.py +0 -0
- {cloudbridge-4.2.1 → cloudbridge-4.3.1}/cloudbridge/factory.py +0 -0
- {cloudbridge-4.2.1 → cloudbridge-4.3.1}/cloudbridge/interfaces/__init__.py +0 -0
- {cloudbridge-4.2.1 → cloudbridge-4.3.1}/cloudbridge/interfaces/exceptions.py +0 -0
- {cloudbridge-4.2.1 → cloudbridge-4.3.1}/cloudbridge/interfaces/provider.py +0 -0
- {cloudbridge-4.2.1 → cloudbridge-4.3.1}/cloudbridge/interfaces/subservices.py +0 -0
- {cloudbridge-4.2.1 → cloudbridge-4.3.1}/cloudbridge/providers/__init__.py +0 -0
- {cloudbridge-4.2.1 → cloudbridge-4.3.1}/cloudbridge/providers/aws/__init__.py +0 -0
- {cloudbridge-4.2.1 → cloudbridge-4.3.1}/cloudbridge/providers/aws/helpers.py +0 -0
- {cloudbridge-4.2.1 → cloudbridge-4.3.1}/cloudbridge/providers/aws/provider.py +0 -0
- {cloudbridge-4.2.1 → cloudbridge-4.3.1}/cloudbridge/providers/aws/subservices.py +0 -0
- {cloudbridge-4.2.1 → cloudbridge-4.3.1}/cloudbridge/providers/azure/__init__.py +0 -0
- {cloudbridge-4.2.1 → cloudbridge-4.3.1}/cloudbridge/providers/azure/helpers.py +0 -0
- {cloudbridge-4.2.1 → cloudbridge-4.3.1}/cloudbridge/providers/azure/provider.py +0 -0
- {cloudbridge-4.2.1 → cloudbridge-4.3.1}/cloudbridge/providers/azure/subservices.py +0 -0
- {cloudbridge-4.2.1 → cloudbridge-4.3.1}/cloudbridge/providers/gcp/__init__.py +0 -0
- {cloudbridge-4.2.1 → cloudbridge-4.3.1}/cloudbridge/providers/gcp/helpers.py +0 -0
- {cloudbridge-4.2.1 → cloudbridge-4.3.1}/cloudbridge/providers/gcp/provider.py +0 -0
- {cloudbridge-4.2.1 → cloudbridge-4.3.1}/cloudbridge/providers/gcp/subservices.py +0 -0
- {cloudbridge-4.2.1 → cloudbridge-4.3.1}/cloudbridge/providers/mock/__init__.py +0 -0
- {cloudbridge-4.2.1 → cloudbridge-4.3.1}/cloudbridge/providers/mock/provider.py +0 -0
- {cloudbridge-4.2.1 → cloudbridge-4.3.1}/cloudbridge/providers/openstack/__init__.py +0 -0
- {cloudbridge-4.2.1 → cloudbridge-4.3.1}/cloudbridge/providers/openstack/helpers.py +0 -0
- {cloudbridge-4.2.1 → cloudbridge-4.3.1}/cloudbridge/providers/openstack/provider.py +0 -0
- {cloudbridge-4.2.1 → cloudbridge-4.3.1}/cloudbridge/providers/openstack/subservices.py +0 -0
- {cloudbridge-4.2.1 → cloudbridge-4.3.1}/cloudbridge/py.typed +0 -0
- {cloudbridge-4.2.1 → cloudbridge-4.3.1}/cloudbridge.egg-info/dependency_links.txt +0 -0
- {cloudbridge-4.2.1 → cloudbridge-4.3.1}/cloudbridge.egg-info/requires.txt +0 -0
- {cloudbridge-4.2.1 → cloudbridge-4.3.1}/cloudbridge.egg-info/top_level.txt +0 -0
- {cloudbridge-4.2.1 → cloudbridge-4.3.1}/pyproject.toml +0 -0
- {cloudbridge-4.2.1 → cloudbridge-4.3.1}/setup.cfg +0 -0
- {cloudbridge-4.2.1 → cloudbridge-4.3.1}/tests/test_base_helpers.py +0 -0
- {cloudbridge-4.2.1 → cloudbridge-4.3.1}/tests/test_block_store_service.py +0 -0
- {cloudbridge-4.2.1 → cloudbridge-4.3.1}/tests/test_cloud_factory.py +0 -0
- {cloudbridge-4.2.1 → cloudbridge-4.3.1}/tests/test_cloud_helpers.py +0 -0
- {cloudbridge-4.2.1 → cloudbridge-4.3.1}/tests/test_compute_service.py +0 -0
- {cloudbridge-4.2.1 → cloudbridge-4.3.1}/tests/test_dns_service.py +0 -0
- {cloudbridge-4.2.1 → cloudbridge-4.3.1}/tests/test_image_service.py +0 -0
- {cloudbridge-4.2.1 → cloudbridge-4.3.1}/tests/test_interface.py +0 -0
- {cloudbridge-4.2.1 → cloudbridge-4.3.1}/tests/test_middleware_system.py +0 -0
- {cloudbridge-4.2.1 → cloudbridge-4.3.1}/tests/test_network_service.py +0 -0
- {cloudbridge-4.2.1 → cloudbridge-4.3.1}/tests/test_object_life_cycle.py +0 -0
- {cloudbridge-4.2.1 → cloudbridge-4.3.1}/tests/test_region_service.py +0 -0
- {cloudbridge-4.2.1 → cloudbridge-4.3.1}/tests/test_security_service.py +0 -0
- {cloudbridge-4.2.1 → cloudbridge-4.3.1}/tests/test_vm_types_service.py +0 -0
|
@@ -9,6 +9,7 @@ import os
|
|
|
9
9
|
import queue
|
|
10
10
|
import re
|
|
11
11
|
import shutil
|
|
12
|
+
import threading
|
|
12
13
|
import time
|
|
13
14
|
import uuid
|
|
14
15
|
from concurrent.futures import FIRST_COMPLETED
|
|
@@ -59,7 +60,7 @@ from cloudbridge.interfaces.resources import Snapshot
|
|
|
59
60
|
from cloudbridge.interfaces.resources import SnapshotState
|
|
60
61
|
from cloudbridge.interfaces.resources import Subnet
|
|
61
62
|
from cloudbridge.interfaces.resources import SubnetState
|
|
62
|
-
from cloudbridge.interfaces.resources import
|
|
63
|
+
from cloudbridge.interfaces.resources import TransferConfig
|
|
63
64
|
from cloudbridge.interfaces.resources import UploadPart
|
|
64
65
|
from cloudbridge.interfaces.resources import VMFirewall
|
|
65
66
|
from cloudbridge.interfaces.resources import VMFirewallRule
|
|
@@ -884,23 +885,124 @@ class BaseBucketObject(BaseCloudResource, BucketObject):
|
|
|
884
885
|
shutil.copyfileobj(
|
|
885
886
|
cast("SupportsRead[bytes]", self.iter_content()), target_stream)
|
|
886
887
|
|
|
888
|
+
def download_to_file(self, path: str,
|
|
889
|
+
config: TransferConfig | None = None) -> None:
|
|
890
|
+
# Assemble the object in a private file alongside the destination and
|
|
891
|
+
# rename it into place once complete, so ``path`` only ever holds a
|
|
892
|
+
# whole object. Callers commonly download every copy of an object to
|
|
893
|
+
# one well-known path (a cache entry, say), so writing in place would
|
|
894
|
+
# let concurrent downloads truncate each other's file - or rename it
|
|
895
|
+
# away mid-transfer - and would destroy a previously downloaded copy
|
|
896
|
+
# when a transfer fails.
|
|
897
|
+
part_path = f"{path}.{uuid.uuid4().hex}.cbpart"
|
|
898
|
+
try:
|
|
899
|
+
self._download_to_path(part_path, config)
|
|
900
|
+
os.replace(part_path, path)
|
|
901
|
+
except BaseException:
|
|
902
|
+
try:
|
|
903
|
+
os.remove(part_path)
|
|
904
|
+
except OSError:
|
|
905
|
+
pass
|
|
906
|
+
raise
|
|
907
|
+
|
|
908
|
+
def _download_to_path(self, path: str,
|
|
909
|
+
config: TransferConfig | None = None) -> None:
|
|
910
|
+
"""
|
|
911
|
+
Write this object's content to ``path``, which the caller owns.
|
|
912
|
+
|
|
913
|
+
Providers with an efficient, thread-safe native downloader (e.g. AWS
|
|
914
|
+
via boto3's ``download_file``, Azure via ``download_blob``) override
|
|
915
|
+
this to use it; the default implementation streams small objects and
|
|
916
|
+
fetches larger ones as parallel ranged reads.
|
|
917
|
+
"""
|
|
918
|
+
size = self.size
|
|
919
|
+
if size <= self._multipart_threshold(config):
|
|
920
|
+
with open(path, 'wb') as f:
|
|
921
|
+
self.save_content(f)
|
|
922
|
+
return
|
|
923
|
+
with open(path, 'w+b') as f:
|
|
924
|
+
self._download_ranged(f, size, config)
|
|
925
|
+
|
|
926
|
+
def _download_ranged(self, target: IO[bytes], size: int,
|
|
927
|
+
config: TransferConfig | None = None) -> None:
|
|
928
|
+
"""
|
|
929
|
+
Fetch the object as ranged reads across a bounded thread pool,
|
|
930
|
+
writing each range at its offset into a preallocated file.
|
|
931
|
+
|
|
932
|
+
To stay safe even on providers whose SDK client/connection is not
|
|
933
|
+
thread-safe, each worker reads through its own cloned provider (see
|
|
934
|
+
:meth:`.CloudProvider.clone`), so no provider state is shared between
|
|
935
|
+
threads. Memory is bounded to ~concurrency * part_size.
|
|
936
|
+
"""
|
|
937
|
+
part_size = self._multipart_part_size(config)
|
|
938
|
+
if part_size < 1:
|
|
939
|
+
raise InvalidValueException('part_size', part_size)
|
|
940
|
+
concurrency = max(1, self._multipart_max_concurrency(config))
|
|
941
|
+
target.truncate(size)
|
|
942
|
+
ranges = [(offset, min(part_size, size - offset))
|
|
943
|
+
for offset in range(0, size, part_size)]
|
|
944
|
+
if concurrency == 1:
|
|
945
|
+
bucket_objects = self._bucket_objects
|
|
946
|
+
for offset, length in ranges:
|
|
947
|
+
target.seek(offset)
|
|
948
|
+
target.write(bucket_objects.download_range(
|
|
949
|
+
self.bucket, self.name, offset, length))
|
|
950
|
+
else:
|
|
951
|
+
self._download_ranges_concurrently(target, ranges, concurrency)
|
|
952
|
+
|
|
953
|
+
def _download_ranges_concurrently(
|
|
954
|
+
self, target: IO[bytes], ranges: list[tuple[int, int]],
|
|
955
|
+
concurrency: int) -> None:
|
|
956
|
+
# A pool of cloned bucket-object services, one per worker, so each
|
|
957
|
+
# thread touches an isolated provider/connection.
|
|
958
|
+
clones: "queue.Queue[BucketObjectService]" = queue.Queue()
|
|
959
|
+
for _ in range(concurrency):
|
|
960
|
+
storage = cast("BaseStorageService",
|
|
961
|
+
self._provider.clone().storage)
|
|
962
|
+
clones.put(storage._bucket_objects)
|
|
963
|
+
|
|
964
|
+
bucket = self.bucket
|
|
965
|
+
name = self.name
|
|
966
|
+
write_lock = threading.Lock()
|
|
967
|
+
|
|
968
|
+
def fetch_one(offset: int, length: int) -> None:
|
|
969
|
+
service = clones.get()
|
|
970
|
+
try:
|
|
971
|
+
data = service.download_range(bucket, name, offset, length)
|
|
972
|
+
finally:
|
|
973
|
+
clones.put(service)
|
|
974
|
+
# Ranges are fetched in parallel but written through the one
|
|
975
|
+
# handle the caller opened, so a range can never be written to a
|
|
976
|
+
# file that has since been replaced. Serializing the writes costs
|
|
977
|
+
# little next to the fetches, and the data is released as soon as
|
|
978
|
+
# it is written, bounding memory to ~concurrency * part_size.
|
|
979
|
+
with write_lock:
|
|
980
|
+
target.seek(offset)
|
|
981
|
+
target.write(data)
|
|
982
|
+
|
|
983
|
+
with ThreadPoolExecutor(max_workers=concurrency) as executor:
|
|
984
|
+
futures = [executor.submit(fetch_one, offset, length)
|
|
985
|
+
for offset, length in ranges]
|
|
986
|
+
for future in futures:
|
|
987
|
+
future.result()
|
|
988
|
+
|
|
887
989
|
# The three resolvers below pick, in order of precedence: an explicit
|
|
888
|
-
# per-call
|
|
990
|
+
# per-call TransferConfig field, the provider/global config, then the class
|
|
889
991
|
# default constant.
|
|
890
|
-
def _multipart_threshold(self, config:
|
|
992
|
+
def _multipart_threshold(self, config: TransferConfig | None = None) -> int:
|
|
891
993
|
if config is not None and config.threshold is not None:
|
|
892
994
|
return int(config.threshold)
|
|
893
995
|
return int(self._provider._get_config_value(
|
|
894
996
|
'multipart_threshold', self.CB_MULTIPART_THRESHOLD))
|
|
895
997
|
|
|
896
|
-
def _multipart_part_size(self, config:
|
|
998
|
+
def _multipart_part_size(self, config: TransferConfig | None = None) -> int:
|
|
897
999
|
if config is not None and config.part_size is not None:
|
|
898
1000
|
return int(config.part_size)
|
|
899
1001
|
return int(self._provider._get_config_value(
|
|
900
1002
|
'multipart_part_size', self.CB_MULTIPART_PART_SIZE))
|
|
901
1003
|
|
|
902
1004
|
def _multipart_max_concurrency(
|
|
903
|
-
self, config:
|
|
1005
|
+
self, config: TransferConfig | None = None) -> int:
|
|
904
1006
|
if config is not None and config.max_concurrency is not None:
|
|
905
1007
|
return int(config.max_concurrency)
|
|
906
1008
|
return int(self._provider._get_config_value(
|
|
@@ -936,7 +1038,7 @@ class BaseBucketObject(BaseCloudResource, BucketObject):
|
|
|
936
1038
|
return data
|
|
937
1039
|
|
|
938
1040
|
def upload(self, data: str | bytes | IO[bytes],
|
|
939
|
-
config:
|
|
1041
|
+
config: TransferConfig | None = None) -> BucketObject:
|
|
940
1042
|
size = self._data_size(data)
|
|
941
1043
|
if size is not None and size > self._multipart_threshold(config):
|
|
942
1044
|
return self._upload_multipart(self._as_stream(data), config)
|
|
@@ -944,14 +1046,14 @@ class BaseBucketObject(BaseCloudResource, BucketObject):
|
|
|
944
1046
|
|
|
945
1047
|
def upload_from_file(
|
|
946
1048
|
self, path: str,
|
|
947
|
-
config:
|
|
1049
|
+
config: TransferConfig | None = None) -> BucketObject:
|
|
948
1050
|
if os.path.getsize(path) > self._multipart_threshold(config):
|
|
949
1051
|
with open(path, 'rb') as f:
|
|
950
1052
|
return self._upload_multipart(f, config)
|
|
951
1053
|
return self._upload_from_file_single_shot(path)
|
|
952
1054
|
|
|
953
1055
|
def _upload_multipart(self, stream: IO[bytes],
|
|
954
|
-
config:
|
|
1056
|
+
config: TransferConfig | None = None) -> BucketObject:
|
|
955
1057
|
"""
|
|
956
1058
|
Drive the explicit multipart lifecycle over a stream, reading it one
|
|
957
1059
|
part at a time so the whole payload is never held in memory.
|
|
@@ -2161,14 +2161,18 @@ class VMFirewallRule(CloudResource):
|
|
|
2161
2161
|
pass
|
|
2162
2162
|
|
|
2163
2163
|
|
|
2164
|
-
class
|
|
2164
|
+
class TransferConfig(object):
|
|
2165
2165
|
"""
|
|
2166
|
-
Provider-agnostic, per-call tuning for an object
|
|
2166
|
+
Provider-agnostic, per-call tuning for an object transfer in either
|
|
2167
|
+
direction.
|
|
2167
2168
|
|
|
2168
|
-
Passed optionally to :meth:`.BucketObject.upload
|
|
2169
|
-
:meth:`.BucketObject.upload_from_file
|
|
2170
|
-
|
|
2171
|
-
|
|
2169
|
+
Passed optionally to :meth:`.BucketObject.upload`,
|
|
2170
|
+
:meth:`.BucketObject.upload_from_file` and
|
|
2171
|
+
:meth:`.BucketObject.download_to_file`. Any field left as ``None`` falls
|
|
2172
|
+
back to the provider/global configuration (the ``CB_MULTIPART_*``
|
|
2173
|
+
settings). Each provider maps these fields onto its native transfer
|
|
2174
|
+
mechanism. To tune uploads and downloads differently, pass a different
|
|
2175
|
+
instance to each call.
|
|
2172
2176
|
"""
|
|
2173
2177
|
|
|
2174
2178
|
def __init__(self, threshold: int | None = None,
|
|
@@ -2176,22 +2180,24 @@ class UploadConfig(object):
|
|
|
2176
2180
|
max_concurrency: int | None = None) -> None:
|
|
2177
2181
|
"""
|
|
2178
2182
|
:type threshold: ``int``
|
|
2179
|
-
:param threshold: Size in bytes above which the
|
|
2180
|
-
multiple parts.
|
|
2183
|
+
:param threshold: Size in bytes above which the transfer is split
|
|
2184
|
+
into multiple parts.
|
|
2181
2185
|
|
|
2182
2186
|
:type part_size: ``int``
|
|
2183
|
-
:param part_size: Size in bytes of each part.
|
|
2184
|
-
provider minimum (5 MiB on S3) for all but the
|
|
2187
|
+
:param part_size: Size in bytes of each part. For uploads this must
|
|
2188
|
+
be at least the provider minimum (5 MiB on S3) for all but the
|
|
2189
|
+
final part; downloads have no minimum.
|
|
2185
2190
|
|
|
2186
2191
|
:type max_concurrency: ``int``
|
|
2187
|
-
:param max_concurrency: Maximum number of parts to
|
|
2192
|
+
:param max_concurrency: Maximum number of parts to transfer in
|
|
2193
|
+
parallel.
|
|
2188
2194
|
"""
|
|
2189
2195
|
self.threshold = threshold
|
|
2190
2196
|
self.part_size = part_size
|
|
2191
2197
|
self.max_concurrency = max_concurrency
|
|
2192
2198
|
|
|
2193
2199
|
def __repr__(self) -> str:
|
|
2194
|
-
return ("<CB-
|
|
2200
|
+
return ("<CB-TransferConfig: threshold={0}, part_size={1}, "
|
|
2195
2201
|
"max_concurrency={2}>".format(
|
|
2196
2202
|
self.threshold, self.part_size, self.max_concurrency))
|
|
2197
2203
|
|
|
@@ -2368,13 +2374,41 @@ class BucketObject(CloudResource):
|
|
|
2368
2374
|
"""
|
|
2369
2375
|
pass
|
|
2370
2376
|
|
|
2377
|
+
@abstractmethod
|
|
2378
|
+
def download_to_file(self, path: str,
|
|
2379
|
+
config: TransferConfig | None = None) -> None:
|
|
2380
|
+
"""
|
|
2381
|
+
Download this object's content to a local file.
|
|
2382
|
+
|
|
2383
|
+
Objects larger than the configured transfer threshold are fetched as
|
|
2384
|
+
ranged reads of ``part_size`` bytes, up to ``max_concurrency`` parts
|
|
2385
|
+
in parallel, so large downloads are not bound to a single connection
|
|
2386
|
+
(and the whole object is never held in memory). Smaller objects are
|
|
2387
|
+
streamed in a single request. ``iter_content``/``save_content``
|
|
2388
|
+
remain single-stream alternatives for arbitrary target streams.
|
|
2389
|
+
|
|
2390
|
+
:type path: ``str``
|
|
2391
|
+
:param path: Local path to write the object's content to. The object
|
|
2392
|
+
is assembled out of the way and moved into place once complete,
|
|
2393
|
+
so ``path`` never holds a partial object: an existing file is
|
|
2394
|
+
replaced atomically, and a failed transfer leaves it untouched.
|
|
2395
|
+
Concurrent downloads to one path are therefore safe, with the
|
|
2396
|
+
last to complete winning.
|
|
2397
|
+
|
|
2398
|
+
:type config: :class:`.TransferConfig`
|
|
2399
|
+
:param config: Optional per-call transfer tuning (threshold, part
|
|
2400
|
+
size, concurrency). Any field left unset falls back to the
|
|
2401
|
+
provider/global configuration.
|
|
2402
|
+
"""
|
|
2403
|
+
pass
|
|
2404
|
+
|
|
2371
2405
|
@abstractmethod
|
|
2372
2406
|
def upload(self, source_stream: IO[bytes],
|
|
2373
|
-
config:
|
|
2407
|
+
config: TransferConfig | None = None) -> BucketObject | None:
|
|
2374
2408
|
"""
|
|
2375
2409
|
Set the contents of the object to the data read from the source stream.
|
|
2376
2410
|
|
|
2377
|
-
:type config: :class:`.
|
|
2411
|
+
:type config: :class:`.TransferConfig`
|
|
2378
2412
|
:param config: Optional per-call upload tuning (multipart threshold,
|
|
2379
2413
|
part size, concurrency). Any field left unset falls back to the
|
|
2380
2414
|
provider/global configuration.
|
|
@@ -2386,7 +2420,7 @@ class BucketObject(CloudResource):
|
|
|
2386
2420
|
|
|
2387
2421
|
@abstractmethod
|
|
2388
2422
|
def upload_from_file(self, path: str,
|
|
2389
|
-
config:
|
|
2423
|
+
config: TransferConfig | None = None) -> BucketObject | None:
|
|
2390
2424
|
"""
|
|
2391
2425
|
Store the contents of the file pointed by the "path" variable.
|
|
2392
2426
|
|
|
@@ -2396,7 +2430,7 @@ class BucketObject(CloudResource):
|
|
|
2396
2430
|
:type path: ``str``
|
|
2397
2431
|
:param path: Absolute path to the file to be uploaded to S3.
|
|
2398
2432
|
|
|
2399
|
-
:type config: :class:`.
|
|
2433
|
+
:type config: :class:`.TransferConfig`
|
|
2400
2434
|
:param config: Optional per-call upload tuning (multipart threshold,
|
|
2401
2435
|
part size, concurrency). Any field left unset falls back to the
|
|
2402
2436
|
provider/global configuration.
|
|
@@ -1383,6 +1383,33 @@ class BucketObjectService(CloudService):
|
|
|
1383
1383
|
"""
|
|
1384
1384
|
pass
|
|
1385
1385
|
|
|
1386
|
+
@abstractmethod
|
|
1387
|
+
def download_range(self, bucket: Bucket | str, object_name: str,
|
|
1388
|
+
offset: int, length: int) -> bytes:
|
|
1389
|
+
"""
|
|
1390
|
+
Read a byte range of an object.
|
|
1391
|
+
|
|
1392
|
+
This is the primitive behind
|
|
1393
|
+
:meth:`.BucketObject.download_to_file`'s parallel ranged downloads;
|
|
1394
|
+
it is also usable directly for partial reads.
|
|
1395
|
+
|
|
1396
|
+
:type bucket: :class:`.Bucket`
|
|
1397
|
+
:param bucket: The bucket containing the object.
|
|
1398
|
+
|
|
1399
|
+
:type object_name: ``str``
|
|
1400
|
+
:param object_name: The key of the object to read.
|
|
1401
|
+
|
|
1402
|
+
:type offset: ``int``
|
|
1403
|
+
:param offset: Zero-based byte offset to start reading at.
|
|
1404
|
+
|
|
1405
|
+
:type length: ``int``
|
|
1406
|
+
:param length: Number of bytes to read.
|
|
1407
|
+
|
|
1408
|
+
:rtype: ``bytes``
|
|
1409
|
+
:return: The requested byte range of the object's content.
|
|
1410
|
+
"""
|
|
1411
|
+
pass
|
|
1412
|
+
|
|
1386
1413
|
|
|
1387
1414
|
class SecurityService(CloudService):
|
|
1388
1415
|
|
|
@@ -13,7 +13,7 @@ from typing import Iterator
|
|
|
13
13
|
from typing import TYPE_CHECKING
|
|
14
14
|
from typing import cast
|
|
15
15
|
|
|
16
|
-
from boto3.s3.transfer import TransferConfig
|
|
16
|
+
from boto3.s3.transfer import TransferConfig as S3TransferConfig
|
|
17
17
|
|
|
18
18
|
from botocore.exceptions import ClientError
|
|
19
19
|
|
|
@@ -58,7 +58,7 @@ from cloudbridge.interfaces.resources import SnapshotState
|
|
|
58
58
|
from cloudbridge.interfaces.resources import Subnet
|
|
59
59
|
from cloudbridge.interfaces.resources import SubnetState
|
|
60
60
|
from cloudbridge.interfaces.resources import TrafficDirection
|
|
61
|
-
from cloudbridge.interfaces.resources import
|
|
61
|
+
from cloudbridge.interfaces.resources import TransferConfig
|
|
62
62
|
from cloudbridge.interfaces.resources import VMFirewall
|
|
63
63
|
from cloudbridge.interfaces.resources import VMType
|
|
64
64
|
from cloudbridge.interfaces.resources import Volume
|
|
@@ -920,11 +920,11 @@ class AWSBucketObject(BaseBucketObject):
|
|
|
920
920
|
|
|
921
921
|
def _upload_multipart(
|
|
922
922
|
self, stream: IO[bytes],
|
|
923
|
-
config:
|
|
923
|
+
config: TransferConfig | None = None) -> BucketObject:
|
|
924
924
|
# boto3's TransferManager uploads parts concurrently with a thread-safe
|
|
925
925
|
# client, so the transparent multipart path delegates to it rather than
|
|
926
926
|
# CloudBridge's generic clone-pool driver.
|
|
927
|
-
transfer_config =
|
|
927
|
+
transfer_config = S3TransferConfig(
|
|
928
928
|
multipart_threshold=self._multipart_part_size(config),
|
|
929
929
|
multipart_chunksize=self._multipart_part_size(config),
|
|
930
930
|
max_concurrency=self._multipart_max_concurrency(config))
|
|
@@ -932,18 +932,30 @@ class AWSBucketObject(BaseBucketObject):
|
|
|
932
932
|
return self
|
|
933
933
|
|
|
934
934
|
def upload_from_file(self, path: str,
|
|
935
|
-
config:
|
|
935
|
+
config: TransferConfig | None = None) -> BucketObject:
|
|
936
936
|
# boto3's upload_file streams large files in parts via its
|
|
937
937
|
# TransferManager. Drive it with CloudBridge's multipart knobs so that
|
|
938
938
|
# upload_from_file and upload() honour the same configuration rather
|
|
939
939
|
# than boto3's own defaults.
|
|
940
|
-
transfer_config =
|
|
940
|
+
transfer_config = S3TransferConfig(
|
|
941
941
|
multipart_threshold=self._multipart_threshold(config),
|
|
942
942
|
multipart_chunksize=self._multipart_part_size(config),
|
|
943
943
|
max_concurrency=self._multipart_max_concurrency(config))
|
|
944
944
|
self._obj.upload_file(path, Config=transfer_config)
|
|
945
945
|
return self
|
|
946
946
|
|
|
947
|
+
def _download_to_path(self, path: str,
|
|
948
|
+
config: TransferConfig | None = None) -> None:
|
|
949
|
+
# boto3's TransferManager downloads large objects as parallel ranged
|
|
950
|
+
# GETs with a thread-safe client, so the transparent ranged path
|
|
951
|
+
# delegates to it rather than CloudBridge's generic clone-pool driver.
|
|
952
|
+
transfer_config = S3TransferConfig(
|
|
953
|
+
multipart_threshold=self._multipart_threshold(config),
|
|
954
|
+
multipart_chunksize=self._multipart_part_size(config),
|
|
955
|
+
max_concurrency=self._multipart_max_concurrency(config))
|
|
956
|
+
self._obj.meta.client.download_file(
|
|
957
|
+
self._obj.bucket_name, self.id, path, Config=transfer_config)
|
|
958
|
+
|
|
947
959
|
def delete(self) -> None:
|
|
948
960
|
self._obj.delete()
|
|
949
961
|
|
|
@@ -733,6 +733,16 @@ class AWSBucketObjectService(BaseBucketObjectService):
|
|
|
733
733
|
Bucket=cast(Any, bucket).name, Key=upload.object_name,
|
|
734
734
|
UploadId=upload.id)
|
|
735
735
|
|
|
736
|
+
@dispatch(event="provider.storage._bucket_objects.download_range",
|
|
737
|
+
priority=BaseBucketObjectService.STANDARD_EVENT_PRIORITY)
|
|
738
|
+
def download_range(self, bucket: Bucket | str, object_name: str,
|
|
739
|
+
offset: int, length: int) -> bytes:
|
|
740
|
+
client = cast("AWSCloudProvider", self.provider).s3_conn.meta.client
|
|
741
|
+
response = client.get_object(
|
|
742
|
+
Bucket=cast(Any, bucket).name, Key=object_name,
|
|
743
|
+
Range='bytes=%d-%d' % (offset, offset + length - 1))
|
|
744
|
+
return cast(bytes, response['Body'].read())
|
|
745
|
+
|
|
736
746
|
|
|
737
747
|
class AWSComputeService(BaseComputeService):
|
|
738
748
|
|
|
@@ -1013,6 +1023,9 @@ class AWSVMTypeService(BaseVMTypeService):
|
|
|
1013
1023
|
|
|
1014
1024
|
def __init__(self, provider: CloudProvider) -> None:
|
|
1015
1025
|
super(AWSVMTypeService, self).__init__(provider)
|
|
1026
|
+
# Raw instance type dicts, keyed by availability zone. See
|
|
1027
|
+
# _get_catalogue for why this is memoised.
|
|
1028
|
+
self._catalogue: dict[str | None, list[dict[str, Any]]] = {}
|
|
1016
1029
|
|
|
1017
1030
|
@dispatch(event="provider.compute.vm_types.get",
|
|
1018
1031
|
priority=BaseVMTypeService.STANDARD_EVENT_PRIORITY)
|
|
@@ -1029,15 +1042,12 @@ class AWSVMTypeService(BaseVMTypeService):
|
|
|
1029
1042
|
else:
|
|
1030
1043
|
raise e
|
|
1031
1044
|
|
|
1032
|
-
|
|
1033
|
-
priority=BaseVMTypeService.STANDARD_EVENT_PRIORITY)
|
|
1034
|
-
def list(self, limit: int | None = None,
|
|
1035
|
-
marker: str | None = None) -> ResultList[VMType]:
|
|
1045
|
+
def _fetch_catalogue(self, zone: str | None) -> list[dict[str, Any]]:
|
|
1036
1046
|
client = cast("AWSCloudProvider", self.provider).ec2_conn.meta.client
|
|
1037
1047
|
vmt_list_resp = client.describe_instance_type_offerings(
|
|
1038
1048
|
LocationType='availability-zone',
|
|
1039
1049
|
Filters=[{'Name': 'location',
|
|
1040
|
-
'Values': [
|
|
1050
|
+
'Values': [zone]}],
|
|
1041
1051
|
# MaxResults is set to max value (1000)
|
|
1042
1052
|
# and client-side pagination is used
|
|
1043
1053
|
**trim_empty_params({'MaxResults': 1000, 'NextToken': None}))
|
|
@@ -1046,7 +1056,7 @@ class AWSVMTypeService(BaseVMTypeService):
|
|
|
1046
1056
|
vmt_list_resp = client.describe_instance_type_offerings(
|
|
1047
1057
|
LocationType='availability-zone',
|
|
1048
1058
|
Filters=[{'Name': 'location',
|
|
1049
|
-
'Values': [
|
|
1059
|
+
'Values': [zone]}],
|
|
1050
1060
|
**trim_empty_params(
|
|
1051
1061
|
{'MaxResults': 1000,
|
|
1052
1062
|
'NextToken': vmt_list_resp.get("NextToken")}))
|
|
@@ -1057,12 +1067,41 @@ class AWSVMTypeService(BaseVMTypeService):
|
|
|
1057
1067
|
# describe_instance_types call can get at most 100 types at once
|
|
1058
1068
|
chunks = [vmt_list_names[x:x + 100]
|
|
1059
1069
|
for x in range(0, len(vmt_list_names), 100)]
|
|
1060
|
-
raw_types = []
|
|
1070
|
+
raw_types: list[dict[str, Any]] = []
|
|
1061
1071
|
for chunk in chunks:
|
|
1062
1072
|
raw_chunk = client.describe_instance_types(
|
|
1063
1073
|
InstanceTypes=chunk).get('InstanceTypes')
|
|
1064
1074
|
raw_types.extend(raw_chunk)
|
|
1065
|
-
|
|
1075
|
+
return raw_types
|
|
1076
|
+
|
|
1077
|
+
def _get_catalogue(self) -> list[dict[str, Any]]:
|
|
1078
|
+
"""
|
|
1079
|
+
Return the raw instance type catalogue for the provider's zone,
|
|
1080
|
+
fetching it at most once per zone.
|
|
1081
|
+
|
|
1082
|
+
EC2 has no server-side paging for instance types, so ``list()`` must
|
|
1083
|
+
materialise the whole catalogue and page it client-side. Fetching it
|
|
1084
|
+
costs one ``DescribeInstanceTypeOfferings`` walk plus one
|
|
1085
|
+
``DescribeInstanceTypes`` call per 100 types — around 14 calls for a
|
|
1086
|
+
real region. Without memoisation every page of a ``list(marker=...)``
|
|
1087
|
+
walk repeats all of that, making a full walk quadratic in API calls:
|
|
1088
|
+
1343 types at a result limit of 5 costs ~4300 calls instead of ~14.
|
|
1089
|
+
|
|
1090
|
+
The catalogue is static for the lifetime of a provider, so it is held
|
|
1091
|
+
per zone (a provider may be cloned to another zone, which genuinely
|
|
1092
|
+
offers a different set of types).
|
|
1093
|
+
"""
|
|
1094
|
+
zone = self.provider.zone_name
|
|
1095
|
+
if zone not in self._catalogue:
|
|
1096
|
+
self._catalogue[zone] = self._fetch_catalogue(zone)
|
|
1097
|
+
return self._catalogue[zone]
|
|
1098
|
+
|
|
1099
|
+
@dispatch(event="provider.compute.vm_types.list",
|
|
1100
|
+
priority=BaseVMTypeService.STANDARD_EVENT_PRIORITY)
|
|
1101
|
+
def list(self, limit: int | None = None,
|
|
1102
|
+
marker: str | None = None) -> ResultList[VMType]:
|
|
1103
|
+
cb_types = [AWSVMType(cast("AWSCloudProvider", self.provider), t)
|
|
1104
|
+
for t in self._get_catalogue()]
|
|
1066
1105
|
return ClientPagedResultList(self.provider, cb_types,
|
|
1067
1106
|
limit=limit, marker=marker)
|
|
1068
1107
|
|
|
@@ -1664,6 +1703,14 @@ class AWSDnsZoneService(BaseDnsZoneService):
|
|
|
1664
1703
|
client.delete_hosted_zone(Id=dns_zone.aws_id)
|
|
1665
1704
|
|
|
1666
1705
|
|
|
1706
|
+
# Route53 reports record changes INSYNC within seconds, but boto3's
|
|
1707
|
+
# resource_record_sets_changed waiter polls every 30s by default, so every
|
|
1708
|
+
# record change costs a full 30s of sleep no matter how fast it propagated.
|
|
1709
|
+
# Poll often enough that the granularity stops dominating, while keeping the
|
|
1710
|
+
# same ~30 minute ceiling for changes that genuinely are slow.
|
|
1711
|
+
DNS_CHANGE_WAITER_CONFIG = {'Delay': 5, 'MaxAttempts': 360}
|
|
1712
|
+
|
|
1713
|
+
|
|
1667
1714
|
class AWSDnsRecordService(BaseDnsRecordService):
|
|
1668
1715
|
|
|
1669
1716
|
def __init__(self, provider: CloudProvider) -> None:
|
|
@@ -1755,7 +1802,8 @@ class AWSDnsRecordService(BaseDnsRecordService):
|
|
|
1755
1802
|
# waiting, this is skipped for mock tests.
|
|
1756
1803
|
if not cast("AWSCloudProvider", self.provider).PROVIDER_ID == 'mock':
|
|
1757
1804
|
waiter = client.get_waiter('resource_record_sets_changed')
|
|
1758
|
-
waiter.wait(Id=response.get('ChangeInfo').get('Id')
|
|
1805
|
+
waiter.wait(Id=response.get('ChangeInfo').get('Id'),
|
|
1806
|
+
WaiterConfig=DNS_CHANGE_WAITER_CONFIG)
|
|
1759
1807
|
return cast(DnsRecord, self.get(dns_zone, name + ":" + type))
|
|
1760
1808
|
|
|
1761
1809
|
def delete(self, dns_zone: DnsZone | str,
|
|
@@ -1783,4 +1831,5 @@ class AWSDnsRecordService(BaseDnsRecordService):
|
|
|
1783
1831
|
# waiting, this is skipped for mock tests.
|
|
1784
1832
|
if not cast("AWSCloudProvider", self.provider).PROVIDER_ID == 'mock':
|
|
1785
1833
|
waiter = client.get_waiter('resource_record_sets_changed')
|
|
1786
|
-
waiter.wait(Id=response.get('ChangeInfo').get('Id')
|
|
1834
|
+
waiter.wait(Id=response.get('ChangeInfo').get('Id'),
|
|
1835
|
+
WaiterConfig=DNS_CHANGE_WAITER_CONFIG)
|
|
@@ -507,6 +507,12 @@ class AzureClient(object):
|
|
|
507
507
|
blob_client = self.blob_client(container_name, blob_name)
|
|
508
508
|
blob_client.delete_blob(delete_snapshots)
|
|
509
509
|
|
|
510
|
+
def get_blob_range(self, container_name: str, blob_name: str,
|
|
511
|
+
offset: int, length: int) -> bytes:
|
|
512
|
+
blob_client = self.blob_client(container_name, blob_name)
|
|
513
|
+
downloader = blob_client.download_blob(offset=offset, length=length)
|
|
514
|
+
return downloader.readall()
|
|
515
|
+
|
|
510
516
|
def get_blob_url(self, container_name: Any, blob_name: str,
|
|
511
517
|
expiry_time: int, writable: bool,
|
|
512
518
|
content_disposition: str | None = None,
|
|
@@ -62,7 +62,7 @@ from cloudbridge.interfaces.resources import SnapshotState
|
|
|
62
62
|
from cloudbridge.interfaces.resources import Subnet
|
|
63
63
|
from cloudbridge.interfaces.resources import SubnetState
|
|
64
64
|
from cloudbridge.interfaces.resources import TrafficDirection
|
|
65
|
-
from cloudbridge.interfaces.resources import
|
|
65
|
+
from cloudbridge.interfaces.resources import TransferConfig
|
|
66
66
|
from cloudbridge.interfaces.resources import VMFirewall
|
|
67
67
|
from cloudbridge.interfaces.resources import VMType
|
|
68
68
|
from cloudbridge.interfaces.resources import Volume
|
|
@@ -293,7 +293,7 @@ class AzureBucketObject(BaseBucketObject):
|
|
|
293
293
|
return self
|
|
294
294
|
|
|
295
295
|
def _upload_multipart(self, stream: IO[bytes],
|
|
296
|
-
config:
|
|
296
|
+
config: TransferConfig | None = None) -> BucketObject:
|
|
297
297
|
# The Azure SDK's upload_blob stages blocks concurrently (max_concurrency
|
|
298
298
|
# workers) over a thread-safe client, so the transparent multipart path
|
|
299
299
|
# delegates to it rather than CloudBridge's generic clone-pool driver.
|
|
@@ -302,6 +302,16 @@ class AzureBucketObject(BaseBucketObject):
|
|
|
302
302
|
max_concurrency=self._multipart_max_concurrency(config))
|
|
303
303
|
return self
|
|
304
304
|
|
|
305
|
+
def _download_to_path(self, path: str,
|
|
306
|
+
config: TransferConfig | None = None) -> None:
|
|
307
|
+
# azure-storage-blob's downloader fetches block ranges concurrently
|
|
308
|
+
# with a thread-safe client, so delegate to it rather than
|
|
309
|
+
# CloudBridge's generic clone-pool driver.
|
|
310
|
+
with open(path, 'wb') as f:
|
|
311
|
+
self._blob_client.download_blob(
|
|
312
|
+
max_concurrency=self._multipart_max_concurrency(config)
|
|
313
|
+
).readinto(f)
|
|
314
|
+
|
|
305
315
|
def delete(self) -> None:
|
|
306
316
|
"""
|
|
307
317
|
Delete this object.
|
|
@@ -766,6 +766,14 @@ class AzureBucketObjectService(BaseBucketObjectService):
|
|
|
766
766
|
"%s/%s will expire automatically.",
|
|
767
767
|
cast("Bucket", bucket).name, upload.object_name)
|
|
768
768
|
|
|
769
|
+
@dispatch(event="provider.storage._bucket_objects.download_range",
|
|
770
|
+
priority=BaseBucketObjectService.STANDARD_EVENT_PRIORITY)
|
|
771
|
+
def download_range(self, bucket: Bucket | str, object_name: str,
|
|
772
|
+
offset: int, length: int) -> bytes:
|
|
773
|
+
azure_client = cast("AzureCloudProvider", self.provider).azure_client
|
|
774
|
+
return cast(bytes, azure_client.get_blob_range(
|
|
775
|
+
cast("Bucket", bucket).name, object_name, offset, length))
|
|
776
|
+
|
|
769
777
|
|
|
770
778
|
class AzureComputeService(BaseComputeService):
|
|
771
779
|
def __init__(self, provider: CloudProvider) -> None:
|
|
@@ -81,7 +81,7 @@ if TYPE_CHECKING:
|
|
|
81
81
|
from cloudbridge.interfaces.resources import Snapshot
|
|
82
82
|
from cloudbridge.interfaces.resources import Subnet
|
|
83
83
|
from cloudbridge.interfaces.resources import SubnetSubService
|
|
84
|
-
from cloudbridge.interfaces.resources import
|
|
84
|
+
from cloudbridge.interfaces.resources import TransferConfig
|
|
85
85
|
from cloudbridge.interfaces.resources import VMFirewall
|
|
86
86
|
from cloudbridge.interfaces.resources import VMFirewallRuleSubService
|
|
87
87
|
from cloudbridge.interfaces.resources import VMType
|
|
@@ -2169,7 +2169,7 @@ class GCPBucketObject(BaseBucketObject):
|
|
|
2169
2169
|
return self
|
|
2170
2170
|
|
|
2171
2171
|
def upload_from_file(self, path: str,
|
|
2172
|
-
config:
|
|
2172
|
+
config: TransferConfig | None = None) -> BucketObject:
|
|
2173
2173
|
"""
|
|
2174
2174
|
Upload a binary file.
|
|
2175
2175
|
|
|
@@ -1769,6 +1769,17 @@ class GCPBucketObjectService(BaseBucketObjectService):
|
|
|
1769
1769
|
self._delete_objects(bucket, self._list_temp_objects(bucket, upload),
|
|
1770
1770
|
ignore_missing=True)
|
|
1771
1771
|
|
|
1772
|
+
@dispatch(event="provider.storage._bucket_objects.download_range",
|
|
1773
|
+
priority=BaseBucketObjectService.STANDARD_EVENT_PRIORITY)
|
|
1774
|
+
def download_range(self, bucket: Bucket | str, object_name: str,
|
|
1775
|
+
offset: int, length: int) -> bytes:
|
|
1776
|
+
provider = cast("GCPCloudProvider", self.provider)
|
|
1777
|
+
request = provider.gcp_storage.objects().get_media(
|
|
1778
|
+
bucket=cast(Any, bucket).name, object=object_name)
|
|
1779
|
+
request.headers['range'] = 'bytes=%d-%d' % (
|
|
1780
|
+
offset, offset + length - 1)
|
|
1781
|
+
return cast(bytes, request.execute())
|
|
1782
|
+
|
|
1772
1783
|
def _compose(self, bucket: Bucket | str, upload: MultipartUpload,
|
|
1773
1784
|
destination: str, sources: builtins.list[str]) -> builtins.list[str]:
|
|
1774
1785
|
"""
|
|
@@ -64,7 +64,7 @@ from cloudbridge.interfaces.resources import SnapshotState
|
|
|
64
64
|
from cloudbridge.interfaces.resources import Subnet
|
|
65
65
|
from cloudbridge.interfaces.resources import SubnetState
|
|
66
66
|
from cloudbridge.interfaces.resources import TrafficDirection
|
|
67
|
-
from cloudbridge.interfaces.resources import
|
|
67
|
+
from cloudbridge.interfaces.resources import TransferConfig
|
|
68
68
|
from cloudbridge.interfaces.resources import VMFirewall
|
|
69
69
|
from cloudbridge.interfaces.resources import VMType
|
|
70
70
|
from cloudbridge.interfaces.resources import Volume
|
|
@@ -1413,7 +1413,7 @@ class OpenStackBucketObject(BaseBucketObject):
|
|
|
1413
1413
|
return self
|
|
1414
1414
|
|
|
1415
1415
|
def upload_from_file(self, path: str,
|
|
1416
|
-
config:
|
|
1416
|
+
config: TransferConfig | None = None) -> BucketObject:
|
|
1417
1417
|
"""
|
|
1418
1418
|
Stores the contents of the file pointed by the ``path`` variable.
|
|
1419
1419
|
If the file is bigger than 5 Gig, it will be broken into segments.
|