Odyssey
s3.py
1 """
2 Utils/mixins related to accessing the stack S3 bucket.
3 """
4 
5 import boto3
6 import botocore
7 import getpass
8 import hashlib
9 import json
10 import os
11 from . import base
12 
13 
14 class StackTracker(object):
15  """ State and config for a stack. This wraps the single object kept in
16  the stacks S3 bucket. """
17 
18  default_region = 'us-west-2'
19 
20  def __init__(self, s3_obj):
21  self._s3_obj = s3_obj
22  self._meta_keys = set()
23  self._body_keys = set()
24  try:
25  data = s3_obj.get()
26  except botocore.exceptions.ClientError as e:
27  if e.response['Error']['Code'] != "NoSuchKey":
28  raise
29  else:
30  for key, val in data['Metadata'].items():
31  self._meta_keys.add(key)
32  setattr(self, key, val)
33  for key, val in json.loads(data['Body'].read().decode()).items():
34  self._body_keys.add(key)
35  setattr(self, key, val)
36  assert not (self._meta_keys & self._body_keys), 'key collision'
37 
38  @property
39  def region(self):
40  try:
41  return self._region
42  except AttributeError:
43  return self.default_region
44 
45  @region.setter
46  def region(self, value):
47  self._region = value
48 
49  def set(self, key, value, meta=False):
50  """ Use for new object creation to track a key. """
51  keys = self._meta_keys if meta else self._body_keys
52  keys.add(key)
53  setattr(self, key, value)
54 
55  def unset(self, key, meta=False):
56  '''Use to remove keys from meta and regular keys'''
57  if meta:
58  self._meta_keys.discard(key)
59  else:
60  self._body_keys.discard(key)
61  setattr(self, key, None)
62 
63  def gen_passphrase_hash(self, passphrase):
64  return hashlib.sha256(passphrase.encode()).hexdigest()
65 
66  def save(self, changelog=None, passphrase=None):
67  self.check_passphrase(passphrase)
68  return self._save(changelog)
69 
70  def _save(self, changelog=None):
71  if changelog is not None:
72  self.changelog.append({
73  "ts": base.localnow().isoformat(),
74  "msg": changelog,
75  "operator": os.environ["OPERATOR_IDENT"]
76  })
77  meta = dict((x, getattr(self, x)) for x in self._meta_keys)
78  body = dict((x, getattr(self, x)) for x in self._body_keys)
79  self._s3_obj.put(Body=json.dumps(body), Metadata=meta,
80  ContentType='application/json')
81 
82  def get_changelog(self):
83  """ Parse old tuple format and newer dict format. """
84  to_dict = lambda x: {
85  "ts": x[0],
86  "msg": x[1],
87  "operator": '-'
88  }
89  return [entry if isinstance(entry, dict) else to_dict(entry)
90  for entry in self.changelog]
91 
92  def check_passphrase(self, passphrase=None):
93  if self._passphrase_hash:
94  if passphrase is None:
95  passphrase = getpass.getpass("Passphrase: ")
96  passhash = self.gen_passphrase_hash(passphrase)
97  if self._passphrase_hash != passhash:
98  raise SystemExit("Invalid Passphrase")
99  return passphrase
100 
101  def delete(self, passphrase=None):
102  self.check_passphrase(passphrase)
103  self._s3_obj.delete()
104 
105  def add_resource(self, kind, ident, **options):
106  """ A resource is an AWS asset that we need to track. """
107  if self.has_resource(kind, ident):
108  raise ValueError('Duplicate resource: %s %s' % (kind, ident))
109  self.resources.append({
110  "kind": kind,
111  "ident": ident,
112  "options": options
113  })
114  self._save()
115 
116  def has_resource(self, kind, ident):
117  for x in self.resources:
118  if x['kind'] == kind and x['ident'] == ident:
119  return True
120  return False
121 
122  def update_state(self, state):
123  self.state = state
124  self._save()
125 
126 
127 class S3BucketMixin(object):
128 
129  stacks_bucket = 'homecu.odyssey.stacks'
130 
131  def prerun(self, *args, **kwargs):
132  s3_res = boto3.resource('s3')
133  self.stacks = s3_res.Bucket(self.stacks_bucket)
134  super().prerun(*args, **kwargs)
135 
136  def get_stack(self, name):
137  """ Load a tracker object from S3. If the file does not exist just
138  return None. """
139  stack_obj = self.stacks.Object(name)
140  try:
141  stack_obj.load()
142  except botocore.exceptions.ClientError as e:
143  if e.response['Error']['Code'] != "404":
144  raise
145  else:
146  return StackTracker(stack_obj)
def gen_passphrase_hash(self, passphrase)
Definition: s3.py:63
def _save(self, changelog=None)
Definition: s3.py:70
def check_passphrase(self, passphrase=None)
Definition: s3.py:92
string default_region
Definition: s3.py:18
def has_resource(self, kind, ident)
Definition: s3.py:116
def add_resource(self, kind, ident, **options)
Definition: s3.py:105
def set(self, key, value, meta=False)
Definition: s3.py:49
def unset(self, key, meta=False)
Definition: s3.py:55
def get_stack(self, name)
Definition: s3.py:136
def get_changelog(self)
Definition: s3.py:82
string stacks_bucket
Definition: s3.py:129