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