Skip to content

Commit 6452e79

Browse files
committed
cosalib: create libs for s3 and ec2 operations
These libraries contain code to work on s3 buckets (head, list, delete) and ec2 (deregister images and snapshots)
1 parent a1b81c9 commit 6452e79

File tree

5 files changed

+101
-38
lines changed

5 files changed

+101
-38
lines changed

src/cmd-buildprep

+19-34
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,14 @@ import os
99
import subprocess
1010
import sys
1111
import requests
12-
import boto3
1312
import shutil
14-
from botocore.exceptions import ClientError
1513
from tenacity import retry, retry_if_exception_type
1614

1715
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
1816

1917
from cosalib.builds import Builds, BUILDFILES
20-
from cosalib.cmdlib import load_json, rm_allow_noent, retry_stop, retry_s3_exception, retry_callback # noqa: E402
18+
from cosalib.cmdlib import load_json, rm_allow_noent, retry_stop, retry_callback
19+
from cosalib.s3 import download_file, head_bucket, head_object
2120

2221
retry_requests_exception = (retry_if_exception_type(requests.Timeout) |
2322
retry_if_exception_type(requests.ReadTimeout) |
@@ -141,6 +140,23 @@ class Fetcher(object):
141140
return self.exists_impl(url)
142141

143142

143+
class S3Fetcher(Fetcher):
144+
145+
def fetch_impl(self, url, dest):
146+
assert url.startswith("s3://")
147+
bucket, key = url[len("s3://"):].split('/', 1)
148+
# this function does not need to be retried with the decorator as download_file would
149+
# retry automatically based on s3config settings
150+
download_file(bucket, key, dest)
151+
152+
def exists_impl(self, url):
153+
assert url.startswith("s3://")
154+
bucket, key = url[len("s3://"):].split('/', 1)
155+
# sanity check that the bucket exists and we have access to it
156+
head_bucket(bucket=bucket)
157+
return head_object(bucket=bucket, key=key)
158+
159+
144160
class HTTPFetcher(Fetcher):
145161

146162
def __init__(self, url_base):
@@ -165,37 +181,6 @@ class HTTPFetcher(Fetcher):
165181
raise Exception(f"Received rc {r.status_code} for {url}")
166182

167183

168-
class S3Fetcher(Fetcher):
169-
170-
def __init__(self, url_base):
171-
super().__init__(url_base)
172-
self.s3 = boto3.client('s3')
173-
self.s3config = boto3.s3.transfer.TransferConfig(
174-
num_download_attempts=5
175-
)
176-
177-
def fetch_impl(self, url, dest):
178-
assert url.startswith("s3://")
179-
bucket, key = url[len("s3://"):].split('/', 1)
180-
# this function does not need to be retried with the decorator as download_file would
181-
# retry automatically based on s3config settings
182-
self.s3.download_file(bucket, key, dest, Config=self.s3config)
183-
184-
@retry(stop=retry_stop, retry=retry_s3_exception, before_sleep=retry_callback)
185-
def exists_impl(self, url):
186-
assert url.startswith("s3://")
187-
bucket, key = url[len("s3://"):].split('/', 1)
188-
# sanity check that the bucket exists and we have access to it
189-
self.s3.head_bucket(Bucket=bucket)
190-
try:
191-
self.s3.head_object(Bucket=bucket, Key=key)
192-
except ClientError as e:
193-
if e.response['Error']['Code'] == '404':
194-
return False
195-
raise e
196-
return True
197-
198-
199184
class LocalFetcher(Fetcher):
200185

201186
def __init__(self, url_base):

src/cmd-buildupload

+3-3
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ CACHE_MAX_AGE_ARTIFACT = 60 * 60 * 24 * 365
2222
# set metadata caching to 5m
2323
CACHE_MAX_AGE_METADATA = 60 * 5
2424
from cosalib.builds import Builds, BUILDFILES
25-
from cosalib.cmdlib import load_json, retry_stop, retry_s3_exception, retry_callback # noqa: E402
25+
from cosalib.cmdlib import load_json, retry_stop, retry_boto_exception, retry_callback # noqa: E402
2626

2727

2828
def main():
@@ -149,7 +149,7 @@ def s3_upload_build(args, builddir, bucket, prefix):
149149
dry_run=args.dry_run)
150150

151151

152-
@retry(stop=retry_stop, retry=retry_s3_exception, before_sleep=retry_callback)
152+
@retry(stop=retry_stop, retry=retry_boto_exception, before_sleep=retry_callback)
153153
def s3_check_exists(bucket, key):
154154
print(f"Checking if bucket '{bucket}' has key '{key}'")
155155
s3 = boto3.client('s3')
@@ -162,7 +162,7 @@ def s3_check_exists(bucket, key):
162162
return True
163163

164164

165-
@retry(stop=retry_stop, retry=retry_s3_exception, retry_error_callback=retry_callback)
165+
@retry(stop=retry_stop, retry=retry_boto_exception, retry_error_callback=retry_callback)
166166
def s3_copy(src, bucket, key, max_age, acl, extra_args={}, dry_run=False):
167167
if key.endswith('.json') and 'ContentType' not in extra_args:
168168
extra_args['ContentType'] = 'application/json'

src/cosalib/aws.py

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import boto3
2+
from cosalib.cmdlib import (
3+
retry_stop,
4+
retry_boto_exception,
5+
retry_callback
6+
)
7+
from tenacity import retry
8+
9+
10+
@retry(stop=retry_stop, retry=retry_boto_exception, before_sleep=retry_callback)
11+
def deregister_ami(ami_id, region):
12+
ec2 = boto3.client('ec2', region_name=region)
13+
ec2.deregister_image(ImageId=ami_id)
14+
15+
16+
@retry(stop=retry_stop, retry=retry_boto_exception, before_sleep=retry_callback)
17+
def delete_snapshot(snap_id, region):
18+
ec2 = boto3.client('ec2', region_name=region)
19+
ec2.delete_snapshot(SnapshotId=snap_id)

src/cosalib/cmdlib.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
from datetime import datetime
2727

2828
retry_stop = (stop_after_delay(10) | stop_after_attempt(5))
29-
retry_s3_exception = (retry_if_exception_type(ConnectionClosedError) |
29+
retry_boto_exception = (retry_if_exception_type(ConnectionClosedError) |
3030
retry_if_exception_type(ConnectTimeoutError) |
3131
retry_if_exception_type(IncompleteReadError) |
3232
retry_if_exception_type(ReadTimeoutError))

src/cosalib/s3.py

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import boto3
2+
3+
from botocore.exceptions import ClientError
4+
from cosalib.cmdlib import (
5+
retry_stop,
6+
retry_boto_exception,
7+
retry_callback
8+
)
9+
from tenacity import retry
10+
11+
12+
s3 = boto3.client('s3')
13+
s3config = boto3.s3.transfer.TransferConfig(
14+
num_download_attempts=5
15+
)
16+
17+
18+
def download_file(bucket, key, dest):
19+
s3.download_file(bucket, key, dest, Config=s3config)
20+
21+
22+
@retry(stop=retry_stop, retry=retry_boto_exception, before_sleep=retry_callback)
23+
def head_bucket(bucket):
24+
s3.head_bucket(Bucket=bucket)
25+
26+
27+
@retry(stop=retry_stop, retry=retry_boto_exception, before_sleep=retry_callback)
28+
def head_object(bucket, key):
29+
try:
30+
s3.head_object(Bucket=bucket, Key=key)
31+
except ClientError as e:
32+
if e.response['Error']['Code'] == '404':
33+
return False
34+
raise e
35+
return True
36+
37+
38+
@retry(stop=retry_stop, retry=retry_boto_exception, before_sleep=retry_callback)
39+
def list_objects(bucket, prefix, delimiter="/", result_key='Contents'):
40+
kwargs = {
41+
'Bucket': bucket,
42+
'Delimiter': delimiter,
43+
'Prefix': prefix,
44+
}
45+
isTruncated = True
46+
while isTruncated:
47+
batch = s3.list_objects_v2(**kwargs)
48+
yield from batch.get(result_key) or []
49+
kwargs['ContinuationToken'] = batch.get('NextContinuationToken')
50+
isTruncated = batch['IsTruncated']
51+
52+
53+
@retry(stop=retry_stop, retry=retry_boto_exception, before_sleep=retry_callback)
54+
def delete_object(bucket, key):
55+
sub_objects = list(list_objects(bucket, key))
56+
if sub_objects != []:
57+
print("s3: deleting {sub_objects}")
58+
s3.delete_objects(Bucket=bucket, Delete=sub_objects)
59+
s3.delete_object(Bucket=bucket, Key=key)

0 commit comments

Comments
 (0)