Skip to content

Commit e7c4098

Browse files
authored
Bring OBJ-related functionality to API parity (#248)
## 📝 Description Add a list of obj-related functionality to API parity. The list of endpoints can be found in https://jira.linode.com/browse/TPT-1892. ## ✔️ How to Test `tox`
1 parent 9d1c5ff commit e7c4098

16 files changed

Lines changed: 1257 additions & 8 deletions

linode_api4/groups/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from .longview import *
1111
from .networking import *
1212
from .nodebalancer import *
13-
from .obj import *
13+
from .object_storage import *
1414
from .profile import *
1515
from .region import *
1616
from .support import *
Lines changed: 333 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,333 @@
1+
from linode_api4.errors import UnexpectedResponseError
2+
from linode_api4.groups import Group
3+
from linode_api4.objects import (
4+
Base,
5+
MappedObject,
6+
ObjectStorageACL,
7+
ObjectStorageBucket,
8+
ObjectStorageCluster,
9+
ObjectStorageKeys,
10+
)
11+
from linode_api4.util import drop_null_keys
12+
13+
14+
class ObjectStorageGroup(Group):
15+
"""
16+
This group encapsulates all endpoints under /object-storage, including viewing
17+
available clusters, buckets, and managing keys and TLS/SSL certs, etc.
18+
"""
19+
20+
def clusters(self, *filters):
21+
"""
22+
Returns a list of available Object Storage Clusters. You may filter
23+
this query to return only Clusters that are available in a specific region::
24+
25+
us_east_clusters = client.object_storage.clusters(ObjectStorageCluster.region == "us-east")
26+
27+
API Documentation: https://www.linode.com/docs/api/object-storage/#clusters-list
28+
29+
:param filters: Any number of filters to apply to this query.
30+
31+
:returns: A list of Object Storage Clusters that matched the query.
32+
:rtype: PaginatedList of ObjectStorageCluster
33+
"""
34+
return self.client._get_and_filter(ObjectStorageCluster, *filters)
35+
36+
def keys(self, *filters):
37+
"""
38+
Returns a list of Object Storage Keys active on this account. These keys
39+
allow third-party applications to interact directly with Linode Object Storage.
40+
41+
API Documentation: https://www.linode.com/docs/api/object-storage/#object-storage-keys-list
42+
43+
:param filters: Any number of filters to apply to this query.
44+
45+
:returns: A list of Object Storage Keys that matched the query.
46+
:rtype: PaginatedList of ObjectStorageKeys
47+
"""
48+
return self.client._get_and_filter(ObjectStorageKeys, *filters)
49+
50+
def keys_create(self, label, bucket_access=None):
51+
"""
52+
Creates a new Object Storage keypair that may be used to interact directly
53+
with Linode Object Storage in third-party applications. This response is
54+
the only time that "secret_key" will be populated - be sure to capture its
55+
value or it will be lost forever.
56+
57+
If given, `bucket_access` will cause the new keys to be restricted to only
58+
the specified level of access for the specified buckets. For example, to
59+
create a keypair that can only access the "example" bucket in all clusters
60+
(and assuming you own that bucket in every cluster), you might do this::
61+
62+
client = LinodeClient(TOKEN)
63+
64+
# look up clusters
65+
all_clusters = client.object_storage.clusters()
66+
67+
new_keys = client.object_storage.keys_create(
68+
"restricted-keys",
69+
bucket_access=[
70+
client.object_storage.bucket_access(cluster, "example", "read_write")
71+
for cluster in all_clusters
72+
],
73+
)
74+
75+
To create a keypair that can only read from the bucket "example2" in the
76+
"us-east-1" cluster (an assuming you own that bucket in that cluster),
77+
you might do this::
78+
79+
client = LinodeClient(TOKEN)
80+
new_keys_2 = client.object_storage.keys_create(
81+
"restricted-keys-2",
82+
bucket_access=client.object_storage.bucket_access("us-east-1", "example2", "read_only"),
83+
)
84+
85+
API Documentation: https://www.linode.com/docs/api/object-storage/#object-storage-key-create
86+
87+
:param label: The label for this keypair, for identification only.
88+
:type label: str
89+
:param bucket_access: One or a list of dicts with keys "cluster,"
90+
"permissions", and "bucket_name". If given, the
91+
resulting Object Storage keys will only have the
92+
requested level of access to the requested buckets,
93+
if they exist and are owned by you. See the provided
94+
:any:`bucket_access` function for a convenient way
95+
to create these dicts.
96+
:type bucket_access: dict or list of dict
97+
98+
:returns: The new keypair, with the secret key populated.
99+
:rtype: ObjectStorageKeys
100+
"""
101+
params = {"label": label}
102+
103+
if bucket_access is not None:
104+
if not isinstance(bucket_access, list):
105+
bucket_access = [bucket_access]
106+
107+
ba = [
108+
{
109+
"permissions": c.get("permissions"),
110+
"bucket_name": c.get("bucket_name"),
111+
"cluster": c.id
112+
if "cluster" in c and issubclass(type(c["cluster"]), Base)
113+
else c.get("cluster"),
114+
}
115+
for c in bucket_access
116+
]
117+
118+
params["bucket_access"] = ba
119+
120+
result = self.client.post("/object-storage/keys", data=params)
121+
122+
if not "id" in result:
123+
raise UnexpectedResponseError(
124+
"Unexpected response when creating Object Storage Keys!",
125+
json=result,
126+
)
127+
128+
ret = ObjectStorageKeys(self.client, result["id"], result)
129+
return ret
130+
131+
def bucket_access(self, cluster, bucket_name, permissions):
132+
return ObjectStorageBucket.access(
133+
self, cluster, bucket_name, permissions
134+
)
135+
136+
def cancel(self):
137+
"""
138+
Cancels Object Storage service. This may be a destructive operation. Once
139+
cancelled, you will no longer receive the transfer for or be billed for
140+
Object Storage, and all keys will be invalidated.
141+
142+
API Documentation: https://www.linode.com/docs/api/object-storage/#object-storage-cancel
143+
"""
144+
self.client.post("/object-storage/cancel", data={})
145+
return True
146+
147+
def transfer(self):
148+
"""
149+
The amount of outbound data transfer used by your account’s Object Storage buckets,
150+
in bytes, for the current month’s billing cycle. Object Storage adds 1 terabyte
151+
of outbound data transfer to your data transfer pool.
152+
153+
API Documentation: https://www.linode.com/docs/api/object-storage/#object-storage-transfer-view
154+
155+
:returns: The amount of outbound data transfer used by your account’s Object
156+
Storage buckets, in bytes, for the current month’s billing cycle.
157+
:rtype: MappedObject
158+
"""
159+
result = self.client.get("/object-storage/transfer")
160+
161+
if not "used" in result:
162+
raise UnexpectedResponseError(
163+
"Unexpected response when getting Transfer Pool!",
164+
json=result,
165+
)
166+
167+
return MappedObject(**result)
168+
169+
def buckets(self, *filters):
170+
"""
171+
Returns a paginated list of all Object Storage Buckets that you own.
172+
This endpoint is available for convenience.
173+
It is recommended that instead you use the more fully-featured S3 API directly.
174+
175+
API Documentation: https://www.linode.com/docs/api/object-storage/#object-storage-buckets-list
176+
177+
:returns: A list of Object Storage Buckets that matched the query.
178+
:rtype: PaginatedList of ObjectStorageBucket
179+
"""
180+
return self.client._get_and_filter(ObjectStorageBucket, *filters)
181+
182+
def bucket_create(
183+
self,
184+
cluster,
185+
label,
186+
acl: ObjectStorageACL = ObjectStorageACL.PRIVATE,
187+
cors_enabled=False,
188+
):
189+
"""
190+
Creates an Object Storage Bucket in the specified cluster. Accounts with
191+
negative balances cannot access this command. If the bucket already exists
192+
and is owned by you, this endpoint returns a 200 response with that bucket
193+
as if it had just been created.
194+
195+
This endpoint is available for convenience.
196+
It is recommended that instead you use the more fully-featured S3 API directly.
197+
198+
API Documentation: https://www.linode.com/docs/api/object-storage/#object-storage-bucket-create
199+
200+
:param acl: The Access Control Level of the bucket using a canned ACL string.
201+
For more fine-grained control of ACLs, use the S3 API directly.
202+
:type acl: str
203+
Enum: private,public-read,authenticated-read,public-read-write
204+
205+
:param cluster: The ID of the Object Storage Cluster where this bucket
206+
should be created.
207+
:type cluster: str
208+
209+
:param cors_enabled: If true, the bucket will be created with CORS enabled for
210+
all origins. For more fine-grained controls of CORS, use
211+
the S3 API directly.
212+
:type cors_enabled: bool
213+
214+
:param label: The name for this bucket. Must be unique in the cluster you are
215+
creating the bucket in, or an error will be returned. Labels will
216+
be reserved only for the cluster that active buckets are created
217+
and stored in. If you want to reserve this bucket’s label in
218+
another cluster, you must create a new bucket with the same label
219+
in the new cluster.
220+
:type label: str
221+
222+
:returns: A Object Storage Buckets that created by user.
223+
:rtype: ObjectStorageBucket
224+
"""
225+
cluster_id = (
226+
cluster.id if isinstance(cluster, ObjectStorageCluster) else cluster
227+
)
228+
229+
params = {
230+
"cluster": cluster_id,
231+
"label": label,
232+
"acl": acl,
233+
"cors_enabled": cors_enabled,
234+
}
235+
236+
result = self.client.post("/object-storage/buckets", data=params)
237+
238+
if not "label" in result or not "cluster" in result:
239+
raise UnexpectedResponseError(
240+
"Unexpected response when creating Object Storage Bucket!",
241+
json=result,
242+
)
243+
244+
return ObjectStorageBucket(
245+
self.client, result["label"], result["cluster"], result
246+
)
247+
248+
def object_acl_config(self, cluster_id, bucket, name=None):
249+
return ObjectStorageBucket(
250+
self.client, bucket, cluster_id
251+
).object_acl_config(name)
252+
253+
def object_acl_config_update(
254+
self, cluster_id, bucket, acl: ObjectStorageACL, name
255+
):
256+
return ObjectStorageBucket(
257+
self.client, bucket, cluster_id
258+
).object_acl_config_update(acl, name)
259+
260+
def object_url_create(
261+
self,
262+
cluster_id,
263+
bucket,
264+
method,
265+
name,
266+
content_type=None,
267+
expires_in=3600,
268+
):
269+
"""
270+
Creates a pre-signed URL to access a single Object in a bucket.
271+
This can be used to share objects, and also to create/delete objects by using
272+
the appropriate HTTP method in your request body’s method parameter.
273+
274+
This endpoint is available for convenience.
275+
It is recommended that instead you use the more fully-featured S3 API directly.
276+
277+
API Documentation: https://www.linode.com/docs/api/object-storage/#object-storage-object-url-create
278+
279+
:param cluster_id: The ID of the cluster this bucket exists in.
280+
:type cluster_id: str
281+
282+
:param bucket: The bucket name.
283+
:type bucket: str
284+
285+
:param content_type: The expected Content-type header of the request this
286+
signed URL will be valid for. If provided, the
287+
Content-type header must be sent with the request when
288+
this URL is used, and must be the same as it was when
289+
the signed URL was created.
290+
Required for all methods except “GET” or “DELETE”.
291+
:type content_type: str
292+
293+
:param expires_in: How long this signed URL will be valid for, in seconds.
294+
If omitted, the URL will be valid for 3600 seconds (1 hour). Defaults to 3600.
295+
:type expires_in: int 360..86400
296+
297+
:param method: The HTTP method allowed to be used with the pre-signed URL.
298+
:type method: str
299+
300+
:param name: The name of the object that will be accessed with the pre-signed
301+
URL. This object need not exist, and no error will be returned
302+
if it doesn’t. This behavior is useful for generating pre-signed
303+
URLs to upload new objects to by setting the method to “PUT”.
304+
:type name: str
305+
306+
:returns: The signed URL to perform the request at.
307+
:rtype: MappedObject
308+
"""
309+
if method not in ("GET", "DELETE") and content_type is None:
310+
raise ValueError(
311+
"Content-type header is missing for the current method! It's required for all methods except GET or DELETE."
312+
)
313+
params = {
314+
"method": method,
315+
"name": name,
316+
"expires_in": expires_in,
317+
"content_type": content_type,
318+
}
319+
320+
result = self.client.post(
321+
"/object-storage/buckets/{}/{}/object-url".format(
322+
cluster_id, bucket
323+
),
324+
data=drop_null_keys(params),
325+
)
326+
327+
if not "url" in result:
328+
raise UnexpectedResponseError(
329+
"Unexpected response when creating the access url of an object!",
330+
json=result,
331+
)
332+
333+
return MappedObject(**result)

linode_api4/linode_client.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -364,7 +364,7 @@ def volume_create(self, label, region=None, linode=None, size=20, **kwargs):
364364
)
365365

366366
# helper functions
367-
def _get_and_filter(self, obj_type, *filters):
367+
def _get_and_filter(self, obj_type, *filters, endpoint=None):
368368
parsed_filters = None
369369
if filters:
370370
if len(filters) > 1:
@@ -374,6 +374,10 @@ def _get_and_filter(self, obj_type, *filters):
374374
else:
375375
parsed_filters = filters[0].dct
376376

377-
return self._get_objects(
378-
obj_type.api_list(), obj_type, filters=parsed_filters
379-
)
377+
# Use sepcified endpoint
378+
if endpoint:
379+
return self._get_objects(endpoint, obj_type, filters=parsed_filters)
380+
else:
381+
return self._get_objects(
382+
obj_type.api_list(), obj_type, filters=parsed_filters
383+
)

linode_api4/objects/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,6 @@
1414
from .profile import *
1515
from .longview import *
1616
from .tag import Tag
17-
from .object_storage import ObjectStorageCluster, ObjectStorageKeys
17+
from .object_storage import *
1818
from .lke import *
1919
from .database import *

0 commit comments

Comments
 (0)