skilleter-thingy 0.0.24__py3-none-any.whl → 0.0.25__py3-none-any.whl

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.

Potentially problematic release.


This version of skilleter-thingy might be problematic. Click here for more details.

skilleter_thingy/aws.py DELETED
@@ -1,548 +0,0 @@
1
- #! /usr/bin/env python3
2
-
3
- ################################################################################
4
- """ Simple AWS module - provides an abstraction layer on top of the boto3 client
5
- for simple operations such as reading/writing S3 buckets, sending emails
6
- etc.
7
-
8
- Currently this is a random collection of functions extracted from the S3
9
- bucket ACL checking code but it is hoped it will evolve over time into
10
- something more comprehensive.
11
-
12
- Author: John Skilleter
13
- """
14
- ################################################################################
15
-
16
- # System modules
17
-
18
- import sys
19
- import logging
20
- import json
21
-
22
- # AWS modules
23
-
24
- import boto3
25
- import botocore
26
-
27
- ################################################################################
28
-
29
- class GenericAWS:
30
- """ Class providing generic S3 functionality from which the
31
- other S3 classes are derived """
32
-
33
- ################################################################################
34
-
35
- def __init__(self, service, session=None, profile=None, createresource=True, createclient=True, region=None):
36
- """ Initialisation - just create a client handle, optionally using
37
- the specified profile """
38
-
39
- if session:
40
- self.session = session
41
- elif profile:
42
- self.session = boto3.session.Session(profile_name=profile)
43
- else:
44
- self.session = boto3
45
-
46
- args = {}
47
- if region:
48
- args['region_name'] = region
49
-
50
- if createresource:
51
- self.resource = self.session.resource(service, **args)
52
-
53
- if createclient:
54
- self.client = self.session.client(service, **args)
55
-
56
- self.log = logging.getLogger('%s:%s' % (__name__, service))
57
-
58
- ################################################################################
59
-
60
- def set_log_level(self, level):
61
- """ Set logging for the module """
62
-
63
- self.log.setLevel(level)
64
-
65
- ################################################################################
66
-
67
- class SES(GenericAWS):
68
- """ Class for AWS Simple Email Service """
69
-
70
- ################################################################################
71
-
72
- def __init__(self, session=None, profile=None):
73
- """ Initialisation """
74
-
75
- super().__init__('ses', session, profile, createresource=False)
76
-
77
- ################################################################################
78
-
79
- def send(self, sender, recipient, subject, body):
80
- """ Send an email """
81
-
82
- destination = \
83
- {
84
- 'ToAddresses': [recipient]
85
- }
86
-
87
- message = \
88
- {
89
- 'Subject':
90
- {
91
- 'Data': subject
92
- },
93
- 'Body':
94
- {
95
- 'Text':
96
- {
97
- 'Data': body
98
- }
99
- }
100
- }
101
-
102
- # Attempt to send the email - just report the error and quit on failure
103
-
104
- try:
105
- self.log.info('Sending email from %s to %s', sender, recipient)
106
-
107
- self.client.send_email(Source=sender,
108
- Destination=destination,
109
- Message=message)
110
-
111
- except (botocore.exceptions.EndpointConnectionError,
112
- self.client.exceptions.MessageRejected) as err:
113
- print('')
114
- print('Error sending email: %sn' % err)
115
- print(' Sender: %s' % sender)
116
- print(' Recipient: %s' % recipient)
117
- print(' Subject: %s' % subject)
118
- print(' Body:')
119
-
120
- for txt in body.split('\n'):
121
- print(' %s' % txt)
122
-
123
- print('')
124
-
125
- sys.exit(1)
126
-
127
- ################################################################################
128
-
129
- class S3Bucket(GenericAWS):
130
- """ Class providing access to S3 buckets """
131
-
132
- ################################################################################
133
-
134
- def __init__(self, session=None, profile=None):
135
- """ Initialisation - just create a client handle, optionally using
136
- the specified profile """
137
-
138
- super().__init__('s3', session, profile)
139
-
140
- ################################################################################
141
-
142
- def read(self, bucket, key):
143
- """ Read the specified data from the specified bucket.
144
- Returns the data, or raises a boto3 exception on error. """
145
-
146
- self.log.info('Get object from key %s in bucket %s', key, bucket)
147
-
148
- response = self.client.get_object(Bucket=bucket, Key=key)
149
-
150
- self.log.debug('Get object: %s', response)
151
-
152
- return response['Body'].read()
153
-
154
- ################################################################################
155
-
156
- def write(self, bucket, key, data):
157
- """ Write data into the specified key of the specified bucket
158
- Raises a boto3 exception on error. """
159
-
160
- self.log.info('Writing %d bytes of data to key %s in bucket %s', len(data), key, bucket)
161
-
162
- self.client.put_object(Bucket=bucket, Key=key, Body=data)
163
-
164
- ################################################################################
165
-
166
- def get_tags(self, bucket):
167
- """ Read the tags from a bucket. Returns a dictionary of tags, which will be
168
- empty if the bucket has no tags. """
169
-
170
- self.log.info('Reading tags from bucket %s', bucket)
171
-
172
- try:
173
- tags = self.client.get_bucket_tagging(Bucket=bucket)['TagSet']
174
-
175
- except botocore.exceptions.ClientError as err:
176
- # Any exception except NoSuchTagSet gets raised.
177
- # NoSuchTagSet simply means that the bucket has no tags
178
- # and isn't really an error, as such, so we just return
179
- # an empty dictionary.
180
-
181
- if err.response['Error']['Code'] != 'NoSuchTagSet':
182
- self.log.error('Error reading tags for bucket %s: %s', bucket, err.response['Error']['Code'])
183
- raise
184
-
185
- tags = {}
186
-
187
- self.log.info('Tags: %s', tags)
188
-
189
- return tags
190
-
191
- ################################################################################
192
-
193
- def get_location(self, bucket):
194
- """ Return the location of a bucket - note that the Boto3 returns the location
195
- set to none, rather than us-east-1 if you query a bucket located in that
196
- region. """
197
-
198
- try:
199
- location = self.client.get_bucket_location(Bucket=bucket)['LocationConstraint']
200
- except botocore.exceptions.ClientError as err:
201
- self.log.info('Error getting location of bucket %s: %s', bucket, err.response['Error']['Code'])
202
- raise
203
-
204
- if not location:
205
- location = 'us-east-1'
206
-
207
- return location
208
-
209
- ################################################################################
210
-
211
- def get_acl(self, bucket):
212
- """ Return the bucket ACLs """
213
-
214
- try:
215
- return self.client.get_bucket_acl(Bucket=bucket)['Grants']
216
- except botocore.exceptions.ClientError as err:
217
- self.log.error('Error getting ACL for bucket %s: %s', bucket, err.response['Error']['Code'])
218
- raise
219
-
220
- ################################################################################
221
-
222
- def get_policy(self, bucket):
223
- """ Return bucket policy information as a list of policy dictionaries
224
- Returns an empty list if the bucket has no policies (similar to
225
- get_tags() above). """
226
-
227
- try:
228
- policy_data = self.client.get_bucket_policy(Bucket=bucket)
229
-
230
- except botocore.exceptions.ClientError as err:
231
- # Any exception which *isn't* a no-policy exception gets raised
232
- # to the caller, the no-policy one causes the function to return
233
- # an empty policy list.
234
-
235
- if err.response['Error']['Code'] != 'NoSuchBucketPolicy':
236
- self.log.error('Error reading policy for bucket %s: %s', bucket, err.response['Error']['Code'])
237
- raise
238
-
239
- return []
240
-
241
- return json.loads(policy_data['Policy'])
242
-
243
- ################################################################################
244
-
245
- def get_buckets(self):
246
- """ Return a list of all the available buckets """
247
-
248
- return [bucket.name for bucket in self.resource.buckets.all()]
249
-
250
- ################################################################################
251
-
252
- def get_website(self, bucket):
253
- """ Return the web site configuration for the bucket, or None if the bucket
254
- is not configured for hosting """
255
-
256
- try:
257
- web = self.client.get_bucket_website(Bucket=bucket)
258
-
259
- except botocore.exceptions.ClientError as err:
260
- if err.response['Error']['Code'] != 'NoSuchWebsiteConfiguration':
261
- print('>>>%s' % err.response)
262
- raise
263
-
264
- web = None
265
-
266
- return web
267
-
268
- ################################################################################
269
-
270
- def get_objects(self, bucket, max_objects=None):
271
- """ Yield a list of the details of the objects in a bucket, stopping after
272
- returning max_objects (if specified). """
273
-
274
- paginator = self.client.get_paginator('list_objects_v2')
275
-
276
- pagesize = min(25, max_objects) if max_objects else 25
277
-
278
- objects = paginator.paginate(Bucket=bucket, PaginationConfig={'PageSize': pagesize})
279
-
280
- count = 0
281
-
282
- for data in objects:
283
- if 'Contents' in data:
284
- for obj in data['Contents']:
285
-
286
- if max_objects:
287
- count += 1
288
- if count > max_objects:
289
- break
290
-
291
- yield obj
292
-
293
- ################################################################################
294
-
295
- def get_object_acl(self, bucket, obj):
296
- """ Return the ACL data for an object in a bucket """
297
-
298
- try:
299
- return self.client.get_object_acl(Bucket=bucket, Key=obj)['Grants']
300
- except botocore.exceptions.ClientError as err:
301
- self.log.error('Error getting ACL for object %s in bucket %s: %s', obj, bucket, err.response['Error']['Code'])
302
- raise
303
-
304
- ################################################################################
305
-
306
- def get_lifecycle(self, bucket):
307
- """ Return the bucket lifecycle data """
308
-
309
- try:
310
- lifecycle = self.client.get_bucket_lifecycle_configuration(Bucket=bucket)
311
- except botocore.exceptions.ClientError as err:
312
- if err.response['Error']['Code'] == 'NoSuchLifecycleConfiguration':
313
- lifecycle = None
314
- else:
315
- raise
316
-
317
- return lifecycle
318
-
319
- ################################################################################
320
-
321
- class STS(GenericAWS):
322
- """ Class providing access to STS functionality """
323
-
324
- def __init__(self, session=None, profile=None):
325
- """ Initialise the STS client (there is no STS resource in boto3) """
326
-
327
- super().__init__('sts', session, profile, createresource=False)
328
-
329
- ################################################################################
330
-
331
- def account(self):
332
- """ Return the name of the current AWS account """
333
-
334
- return self.client.get_caller_identity()['Account']
335
-
336
- ################################################################################
337
-
338
- class IAM(GenericAWS):
339
- """ Class providing access to IAM """
340
-
341
- def __init__(self, session=None, profile=None):
342
- """ Initialise the IAM client/resource """
343
-
344
- super().__init__('iam', session, profile, createresource=False)
345
-
346
- def role_exists(self, role):
347
- """ Return True if the role exists """
348
-
349
- try:
350
- self.client.get_role(RoleName=role)
351
- except botocore.exceptions.ClientError as err:
352
- if err.response['Error']['Code'] != 'NoSuchEntity':
353
- raise
354
-
355
- return False
356
- else:
357
- return True
358
-
359
- def create_role(self, role, description, policy):
360
- """ Create a new role """
361
-
362
- response = self.client.create_role(RoleName=role, Description=description, AssumeRolePolicyDocument=policy)
363
-
364
- return response
365
-
366
- def put_role_policy(self, role, policy_name, policy):
367
- """ Update the policy in an existing role """
368
-
369
- response = self.client.put_role_policy(RoleName=role, PolicyName=policy_name, PolicyDocument=policy)
370
-
371
- return response
372
-
373
- def get_role_policy(self, role, policy_name):
374
- """ Read the policy in an existing role """
375
-
376
- response = self.client.get_role_policy(RoleName=role, PolicyName=policy_name)
377
-
378
- return response
379
-
380
- ################################################################################
381
-
382
- class Events(GenericAWS):
383
- """ Class providing access to CloudWatch Events """
384
-
385
- def __init__(self, session=None, profile=None):
386
- """ Initialise the CloudWatch Events client/resource """
387
-
388
- super().__init__('events', session, profile, createresource=False)
389
-
390
- def put_rule(self, name, schedule):
391
- """ Create or update the specified rule """
392
-
393
- self.client.put_rule(Name=name, ScheduleExpression=schedule)
394
-
395
- def put_target(self, name, targets):
396
- """ Add the specified target(s) to the specified rule, or update
397
- existing targets """
398
-
399
- self.client.put_targets(Rule=name, Targets=targets)
400
-
401
- ################################################################################
402
-
403
- DEFAULT_LAMBDA_TIMEOUT = 3
404
- DEFAULT_LAMBDA_MEMORY = 128
405
- DEFAULT_LAMBDA_HANDLER = 'main.main'
406
- DEFAULT_LAMBDA_RUNTIME = 'python3.6'
407
-
408
- class Lambda(GenericAWS):
409
- """ Class providing access to Lambda functions """
410
-
411
- def __init__(self, session=None, profile=None, region=None):
412
- """ Initialise the Lambda client/resource """
413
-
414
- super().__init__('lambda', session, profile, createresource=False, region=region)
415
-
416
- def exists(self, name):
417
- """ Return True if the specified Lambda function exists """
418
-
419
- try:
420
- self.client.get_function(FunctionName=name)
421
- except botocore.exceptions.ClientError as err:
422
- if err.response['Error']['Code'] != 'ResourceNotFoundException':
423
- raise
424
- return False
425
- else:
426
- return True
427
-
428
- def update_function(self, name, zipfile):
429
- """ Update the specified Lambda function given a zip file """
430
-
431
- with open(zipfile, 'rb') as zipper:
432
- zipdata = zipper.read()
433
-
434
- response = self.client.update_function_code(FunctionName=name, ZipFile=zipdata)
435
-
436
- return response
437
-
438
- def update_function_configuration(self, name,
439
- handler=None,
440
- environment=None):
441
- """ Update the handler associated with a Lambda fucntion """
442
-
443
- update_args = {}
444
- update_args['FunctionName'] = name
445
-
446
- if handler:
447
- update_args['Handler'] = handler
448
-
449
- if environment:
450
- update_args['Environment'] = {'Variables': environment}
451
-
452
- self.client.update_function_configuration(**update_args)
453
-
454
- def create_function(self, name, role, description, zipfile,
455
- runtime=DEFAULT_LAMBDA_RUNTIME,
456
- handler=DEFAULT_LAMBDA_HANDLER,
457
- timeout=DEFAULT_LAMBDA_TIMEOUT,
458
- memory=DEFAULT_LAMBDA_MEMORY):
459
- """ Create the specified Lambda function given a zip file"""
460
-
461
- with open(zipfile, 'rb') as zipper:
462
- zipdata = zipper.read()
463
-
464
- response = self.client.create_function(FunctionName=name,
465
- Runtime=runtime,
466
- Role=role,
467
- Handler=handler,
468
- Code={'ZipFile': zipdata},
469
- Description=description,
470
- Timeout=timeout,
471
- MemorySize=memory)
472
-
473
- return response
474
-
475
- ################################################################################
476
-
477
- def get_session(profile_name=None):
478
- """ Wrapper for boto3.session.Session """
479
-
480
- return boto3.session.Session(profile_name=profile_name)
481
-
482
- ################################################################################
483
-
484
- def set_stream_logger(name='botocore', level=10, format_string=None):
485
- """ Wrapper for boto3.set_stream_logger """
486
-
487
- return boto3.set_stream_logger(name=name, level=level, format_string=format_string)
488
-
489
- ################################################################################
490
-
491
- def set_default_region(region):
492
- """ Set the default region for the module """
493
-
494
- boto3.setup_default_session(region_name=region)
495
-
496
- ################################################################################
497
-
498
- def get_regions(servicename):
499
- """ Generate a list of regions where a service is supported """
500
-
501
- return boto3.session.Session().get_available_regions(servicename)
502
-
503
- ################################################################################
504
-
505
- def get_resources():
506
- """ Devious method of getting the list of clients supported by boto3
507
- Note that I'm somewhat embarrassed by this, but can't find a better
508
- way of doing it! """
509
-
510
- try:
511
- boto3.resource('XXX-NOT-A-RESOURCE-XXX')
512
- except boto3.exceptions.ResourceNotExistsError as err:
513
- return str(err).replace(' - ', '').split('\n')[2:-1]
514
-
515
- ################################################################################
516
-
517
- def get_clients():
518
- """ Similarly devious way of getting the list of supported clients.
519
- This is equally as yukky as the get_resources() function. """
520
-
521
- try:
522
- boto3.client('XXX-NOT-A-CLIENT-XXX')
523
- except botocore.exceptions.UnknownServiceError as err:
524
- return repr(err)[:-3].split(': ')[2].split(', ')
525
-
526
- ################################################################################
527
-
528
- def test_function():
529
- """ Module test function """
530
-
531
- # Test STS
532
-
533
- sts_client = STS()
534
-
535
- print('Current account: %s' % sts_client.account())
536
-
537
- # TODO: Test SES
538
- # TODO: Test S3
539
- # TODO: Test IAM
540
- # TODO: Test Cloudwatch Events
541
- # TODO: Test Lambda
542
- # TODO: Test set_default_region(), get_session(), get_resources(), get_regions(), get_clients()
543
-
544
- ################################################################################
545
- # Test code
546
-
547
- if __name__ == '__main__':
548
- test_function()