1767.1.1
by Curtis Hovey
Added aws.py based on gce.py. |
1 |
#!/usr/bin/python
|
2 |
||
3 |
from __future__ import print_function |
|
4 |
||
5 |
from argparse import ArgumentParser |
|
6 |
from datetime import ( |
|
7 |
datetime, |
|
8 |
timedelta, |
|
9 |
)
|
|
10 |
import fnmatch |
|
11 |
import logging |
|
12 |
import os |
|
13 |
import sys |
|
14 |
||
15 |
from dateutil import tz |
|
16 |
||
17 |
||
18 |
__metaclass__ = type |
|
19 |
||
20 |
||
21 |
PERMANENT = 'permanent' |
|
22 |
OLD_MACHINE_AGE = 14 |
|
23 |
||
24 |
||
25 |
# This logger strictly reports the activity of this script.
|
|
26 |
log = logging.getLogger("aws") |
|
27 |
handler = logging.StreamHandler(sys.stderr) |
|
28 |
handler.setFormatter(logging.Formatter( |
|
29 |
fmt='%(asctime)s %(levelname)s %(message)s', |
|
30 |
datefmt='%Y-%m-%d %H:%M:%S')) |
|
31 |
log.addHandler(handler) |
|
32 |
||
33 |
||
34 |
def is_permanent(node): |
|
35 |
"""Return True of the node is permanent."""
|
|
36 |
# the tags keys only exists if there are tags.
|
|
37 |
tags = node.extra.get('tags', {}) |
|
38 |
permanent = tags.get(PERMANENT, 'false') |
|
39 |
return permanent.lower() == 'true' |
|
40 |
||
41 |
||
42 |
def is_young(node, old_age): |
|
43 |
"""Return True if the node is young."""
|
|
44 |
now = datetime.now(tz.gettz('UTC')) |
|
45 |
young = True |
|
46 |
# The value is not guaranteed, but is always present in running instances.
|
|
47 |
created = node.created_at |
|
48 |
if created: |
|
49 |
creation_time = node.created_at |
|
50 |
age = now - creation_time |
|
51 |
hours = age.total_seconds() // 3600 |
|
52 |
log.debug('{} is {} old'.format(node.name, hours)) |
|
53 |
ago = timedelta(hours=old_age) |
|
54 |
if age > ago: |
|
55 |
young = False |
|
56 |
return young |
|
57 |
||
58 |
||
59 |
def get_client(aws_access_key, aws_secret, region): |
|
60 |
"""Delay imports and activation of AWS client as needed."""
|
|
61 |
import libcloud |
|
62 |
aws = libcloud.compute.providers.get_driver( |
|
63 |
libcloud.compute.types.Provider.EC2) |
|
64 |
return aws(aws_access_key, aws_secret, region=region) |
|
65 |
||
66 |
||
67 |
def list_instances(client, glob='*', print_out=False, states=['running']): |
|
68 |
"""Return a list of cloud Nodes.
|
|
69 |
||
70 |
Use print_out=True to print a listing of nodes.
|
|
71 |
||
72 |
:param client: The AWS client.
|
|
73 |
:param glob: The glob to find matching resource groups to delete.
|
|
74 |
:param print_out: Print the found resources to STDOUT?
|
|
75 |
:return: A list of Nodes
|
|
76 |
"""
|
|
77 |
nodes = [] |
|
78 |
for node in client.list_nodes(): |
|
1767.1.2
by Curtis Hovey
Added tests for aws. |
79 |
if not (fnmatch.fnmatch(node.name, glob) and node.state in states): |
1767.1.1
by Curtis Hovey
Added aws.py based on gce.py. |
80 |
log.debug('Skipping {}'.format(node.name)) |
81 |
continue
|
|
82 |
nodes.append(node) |
|
83 |
if print_out: |
|
84 |
for node in nodes: |
|
1767.1.2
by Curtis Hovey
Added tests for aws. |
85 |
if node.created_at: |
86 |
created = node.created_at.isoformat() |
|
87 |
else: |
|
88 |
created = 'UNKNOWN' |
|
1767.1.1
by Curtis Hovey
Added aws.py based on gce.py. |
89 |
region_name = client.region_name |
90 |
print('{}\t{}\t{}\t{}'.format( |
|
91 |
node.name, region_name, created, node.state)) |
|
92 |
return nodes |
|
93 |
||
94 |
||
95 |
def delete_instances(client, name_id, old_age=OLD_MACHINE_AGE, dry_run=False): |
|
96 |
"""Delete a node instance.
|
|
97 |
||
1767.1.3
by Curtis Hovey
Fixed doc. |
98 |
:param client: The AWS client.
|
1767.1.1
by Curtis Hovey
Added aws.py based on gce.py. |
99 |
:param name_id: A glob to match the aws name or Juju instance-id.
|
100 |
:param old_age: The minimum age to delete.
|
|
101 |
:param dry_run: Do not make changes when True.
|
|
102 |
"""
|
|
103 |
nodes = list_instances(client, glob=name_id) |
|
104 |
deleted_count = 0 |
|
105 |
deletable = [] |
|
106 |
for node in nodes: |
|
107 |
if is_permanent(node): |
|
108 |
log.debug('Skipping {} because it is permanent'.format(node.name)) |
|
109 |
continue
|
|
110 |
if is_young(node, old_age): |
|
111 |
log.debug('Skipping {} because it is young:'.format(node.name)) |
|
112 |
continue
|
|
113 |
deletable.append(node) |
|
114 |
if not deletable: |
|
115 |
log.warning( |
|
116 |
'The no machines match {} that are older than {}'.format( |
|
117 |
name_id, old_age)) |
|
118 |
for node in deletable: |
|
119 |
node_name = node.name |
|
120 |
log.debug('Deleting {}'.format(node_name)) |
|
121 |
if not dry_run: |
|
122 |
# Do not pass destroy_boot_disk=True unless the node has a special
|
|
123 |
# boot disk that is not set to autodestroy.
|
|
124 |
success = client.destroy_node(node) |
|
125 |
if success: |
|
126 |
log.debug('Deleted {}'.format(node_name)) |
|
127 |
deleted_count += 1 |
|
128 |
else: |
|
129 |
log.error('Cannot delete {}'.format(node_name)) |
|
130 |
return deleted_count |
|
131 |
||
132 |
||
133 |
def parse_args(argv): |
|
134 |
"""Return the argument parser for this program."""
|
|
135 |
parser = ArgumentParser(description='Query and manage AWS.') |
|
136 |
parser.add_argument( |
|
137 |
'-d', '--dry-run', action='store_true', default=False, |
|
138 |
help='Do not make changes.') |
|
139 |
parser.add_argument( |
|
140 |
'-v', '--verbose', action='store_const', |
|
141 |
default=logging.INFO, const=logging.DEBUG, |
|
142 |
help='Verbose test harness output.') |
|
143 |
parser.add_argument( |
|
144 |
'--aws-access-key', |
|
145 |
help=("The AWS EC2 access key." |
|
146 |
"Environment: $AWS_ACCESS_KEY."), |
|
147 |
default=os.environ.get('AWS_ACCESS_KEY')) |
|
148 |
parser.add_argument( |
|
149 |
'--aws-secret', |
|
150 |
help=("The AWS EC2 secret." |
|
151 |
"Environment: $AWS_SECRET_KEY."), |
|
152 |
default=os.environ.get('AWS_SECRET_KEY')) |
|
1767.1.4
by Curtis Hovey
make region a positional arg. |
153 |
parser.add_argument('region', help="The EC2 region.") |
1767.1.1
by Curtis Hovey
Added aws.py based on gce.py. |
154 |
subparsers = parser.add_subparsers(help='sub-command help', dest="command") |
155 |
ls_parser = subparsers.add_parser( |
|
156 |
'list-instances', help='List vm instances.') |
|
157 |
ls_parser.add_argument( |
|
158 |
'filter', default='*', nargs='?', |
|
159 |
help='A glob pattern to match services to.') |
|
160 |
di_parser = subparsers.add_parser( |
|
161 |
'delete-instances', |
|
162 |
help='delete old resource groups and their vm, networks, etc.') |
|
163 |
di_parser.add_argument( |
|
164 |
'-o', '--old-age', default=OLD_MACHINE_AGE, type=int, |
|
165 |
help='Set old machine age to n hours.') |
|
166 |
di_parser.add_argument( |
|
167 |
'filter', |
|
168 |
help='A glob pattern to select AWS name or juju instance-id') |
|
169 |
args = parser.parse_args(argv[1:]) |
|
170 |
if not all([args.aws_access_key, args.aws_secret]): |
|
171 |
log.error("$AWS_ACCESS_KEY, $AWS_SECRET_KEY was not provided.") |
|
172 |
return args |
|
173 |
||
174 |
||
175 |
def main(argv): |
|
176 |
args = parse_args(argv) |
|
177 |
log.setLevel(args.verbose) |
|
178 |
client = get_client(args.aws_access_key, args.aws_secret, args.region) |
|
179 |
try: |
|
180 |
if args.command == 'list-instances': |
|
181 |
list_instances(client, glob=args.filter, print_out=True) |
|
182 |
elif args.command == 'delete-instances': |
|
183 |
delete_instances( |
|
184 |
client, args.filter, |
|
185 |
old_age=args.old_age, dry_run=args.dry_run) |
|
186 |
except Exception as e: |
|
187 |
print(e) |
|
188 |
return 1 |
|
189 |
return 0 |
|
190 |
||
191 |
||
192 |
if __name__ == '__main__': |
|
193 |
sys.exit(main(sys.argv)) |