3
from __future__ import print_function
5
from argparse import ArgumentParser
15
from time import sleep
18
from utility import until_timeout
22
USER_AGENT = "juju-cloud-tool/{} ({}) Python/{}".format(
23
VERSION, sys.platform, sys.version.split(None, 1)[0])
24
ISO_8601_FORMAT = '%Y-%m-%dT%H:%M:%S.%fZ'
29
openssl dgst -sha256 -sign {1} |
37
class DeleteRequest(urllib2.Request):
43
class HeadRequest(urllib2.Request):
49
class PostRequest(urllib2.Request):
55
class PutRequest(urllib2.Request):
61
def parse_iso_date(string):
62
return datetime.strptime(string, ISO_8601_FORMAT)
66
"""A class that mirrors MantaClient without the modern Crypto.
68
See https://github.com/joyent/python-manta
71
def __init__(self, sdc_url, account, key_id, key_path, manta_url,
72
user_agent=USER_AGENT, pause=3, dry_run=False, verbose=False):
73
if sdc_url.endswith('/'):
75
self.sdc_url = sdc_url
76
if manta_url.endswith('/'):
77
manta_url = manta_url[1:]
78
self.manta_url = manta_url
79
self.account = account
81
self.key_path = key_path
82
self.user_agent = user_agent
84
self.dry_run = dry_run
85
self.verbose = verbose
87
def make_request_headers(self, headers=None):
88
"""Return a dict of required headers.
90
The Authorization header is always a signing of the "Date" header,
91
where "date" must be lowercase.
93
timestamp = datetime.utcnow().strftime("%a, %d %b %Y %H:%M:%S GMT")
94
script = SSL_SIGN.format(timestamp, self.key_path)
95
signature = subprocess.check_output(['bash', '-c', script])
96
key = "/{}/keys/{}".format(self.account, self.key_id)
98
'Signature keyId="{}",algorithm="rsa-sha256",'.format(key) +
99
'signature="{}"'.format(signature))
102
headers['Date'] = timestamp
103
headers['Authorization'] = auth
104
headers["User-Agent"] = USER_AGENT
107
def _request(self, path, method="GET", body=None, headers=None,
109
headers = self.make_request_headers(headers)
110
if path.startswith('/'):
113
base_url = self.manta_url
115
base_url = self.sdc_url
116
uri = "{}/{}/{}".format(base_url, self.account, path)
117
if method == 'DELETE':
118
request = DeleteRequest(uri, headers=headers)
119
elif method == 'HEAD':
120
request = HeadRequest(uri, headers=headers)
121
elif method == 'POST':
122
request = PostRequest(uri, data=body, headers=headers)
123
elif method == 'PUT':
124
request = PutRequest(uri, data=body, headers=headers)
126
request = urllib2.Request(uri, headers=headers)
128
response = urllib2.urlopen(request)
129
except Exception as err:
130
print(request.header_items())
133
content = response.read()
134
headers = dict(response.headers.items())
135
headers['status'] = str(response.getcode())
136
headers['reason'] = response.msg
137
return headers, content
139
def _list_objects(self, path, deep=False):
140
headers, content = self._request(path, is_manta=True)
142
for line in content.splitlines():
143
obj = json.loads(line)
144
obj['path'] = '%s/%s' % (path, obj['name'])
146
if obj['type'] == 'directory' and deep:
147
objects.extend(self._list_objects(obj['path'], deep=True))
150
def list_objects(self, path, deep=False):
151
objects = self._list_objects(path, deep=deep)
153
print('{type:9} {mtime} {path}'.format(**obj))
155
def delete_old_objects(self, path, old_age):
156
now = datetime.utcnow()
157
ago = timedelta(hours=old_age)
158
objects = self._list_objects(path, deep=True)
159
# The list is dir, the sub objects. Manta requires the sub objects
160
# to be deleted first.
163
if '.joyent' in obj['path']:
164
# The .joyent dir cannot be deleted.
165
print('ignoring %s' % obj['path'])
167
mtime = parse_iso_date(obj['mtime'])
170
print('ignoring young %s' % obj['path'])
173
print('Deleting %s' % obj['path'])
175
headers, content = self._request(
176
obj['path'], method='DELETE', is_manta=True)
178
def _list_machines(self, machine_id=None):
179
"""Return a list of machine dicts."""
181
path = '/machines/{}'.format(machine_id)
184
headers, content = self._request(path)
185
machines = json.loads(content)
190
def list_machines(self, machine_id=None):
191
machines = self._list_machines(machine_id)
192
pprint.pprint(machines, indent=2)
194
def _list_machine_tags(self, machine_id):
195
path = '/machines/{}/tags'.format(machine_id)
196
headers, content = self._request(path)
197
tags = json.loads(content)
202
def list_machine_tags(self, machine_id):
203
tags = self._list_machine_tags(machine_id)
204
pprint.pprint(tags, indent=2)
206
def stop_machine(self, machine_id):
207
path = '/machines/{}?action=stop'.format(machine_id)
208
print("Stopping machine {}".format(machine_id))
210
headers, content = self._request(path, method='POST')
212
def delete_machine(self, machine_id):
213
path = '/machines/{}'.format(machine_id)
214
print("Deleting machine {}".format(machine_id))
216
headers, content = self._request(path, method='DELETE')
218
def attempt_deletion(self, current_stuck):
220
for machine_id in current_stuck:
222
print("Attempting to delete {} stuck in provisioning.".format(
226
# Officially the we cannot delete non-stopped machines,
227
# but using the UI, we can delete machines stuck in
228
# provisioning or stopping, so we try.
229
self.delete_machine(machine_id)
231
print("Deleted {}".format(machine_id))
233
print('Delete stuck machine {} using the UI.'.format(
238
def _delete_running_machine(self, machine_id):
239
self.stop_machine(machine_id)
240
for ignored in until_timeout(120):
245
stopping_machine = self._list_machines(machine_id)
246
if stopping_machine['state'] == 'stopped':
250
self.delete_machine(machine_id)
252
def delete_old_machines(self, old_age):
253
machines = self._list_machines()
254
now = datetime.utcnow()
256
for machine in machines:
257
created = parse_iso_date(machine['created'])
259
if age > timedelta(hours=old_age):
260
machine_id = machine['id']
261
tags = self._list_machine_tags(machine_id)
262
if tags.get('permanent', 'false') == 'true':
264
if machine['state'] == 'provisioning':
265
current_stuck.append(machine)
268
print("Machine {} is {} old".format(machine_id, age))
270
self._delete_running_machine(machine_id)
271
if not self.dry_run and current_stuck:
272
self.attempt_deletion(current_stuck)
275
def parse_args(argv=None):
276
"""Return the argument parser for this program."""
277
parser = ArgumentParser('Query and manage joyent.')
279
'-d', '--dry-run', action='store_true', default=False,
280
help='Do not make changes.')
282
'-v', '--verbose', action="store_true", help='Increse verbosity.')
284
"-u", "--url", dest="sdc_url",
285
help="SDC URL. Environment: SDC_URL=URL",
286
default=os.environ.get("SDC_URL"))
288
"-m", "--manta-url", dest="manta_url",
289
help="Manta URL. Environment: MANTA_URL=URL",
290
default=os.environ.get("MANTA_URL"))
293
help="Manta account. Environment: MANTA_USER=ACCOUNT",
294
default=os.environ.get("MANTA_USER"))
296
"-k", "--key-id", dest="key_id",
297
help="SSH key fingerprint. Environment: MANTA_KEY_ID=FINGERPRINT",
298
default=os.environ.get("MANTA_KEY_ID"))
300
"-p", "--key-path", dest="key_path",
301
help="Path to the SSH key",
302
default=os.path.join(os.environ.get('JUJU_HOME', '~/.juju'), 'id_rsa'))
303
subparsers = parser.add_subparsers(help='sub-command help', dest="command")
304
subparsers.add_parser('list-machines', help='List running machines')
305
parser_delete_old_machine = subparsers.add_parser(
306
'delete-old-machines',
307
help='Delete machines older than %d hours' % OLD_MACHINE_AGE)
308
parser_delete_old_machine.add_argument(
309
'-o', '--old-age', default=OLD_MACHINE_AGE, type=int,
310
help='Set old machine age to n hours.')
311
parser_list_tags = subparsers.add_parser(
312
'list-tags', help='List tags of running machines')
313
parser_list_tags.add_argument('machine_id', help='The machine id.')
314
parser_list_objects = subparsers.add_parser(
315
'list-objects', help='List directories and files in manta')
316
parser_list_objects.add_argument(
317
'-r', '--recursive', action='store_true', default=False,
318
help='Include content in subdirectories.')
319
parser_list_objects.add_argument('path', help='The path')
320
parser_delete_old_objects = subparsers.add_parser(
321
'delete-old-objects',
322
help='Delete objects older than %d hours' % OLD_MACHINE_AGE)
323
parser_delete_old_objects.add_argument(
324
'-o', '--old-age', default=OLD_MACHINE_AGE, type=int,
325
help='Set old object age to n hours.')
326
parser_delete_old_objects.add_argument('path', help='The path')
328
args = parser.parse_args(argv)
330
print('SDC_URL must be sourced into the environment.')
336
args = parse_args(argv)
338
args.sdc_url, args.account, args.key_id, args.key_path, args.manta_url,
339
dry_run=args.dry_run, verbose=args.verbose)
340
if args.command == 'list-machines':
341
client.list_machines()
342
elif args.command == 'list-tags':
343
client.list_machine_tags(args.machine_id)
344
elif args.command == 'list-objects':
345
client.list_objects(args.path, deep=args.recursive)
346
elif args.command == 'delete-old-machines':
347
client.delete_old_machines(args.old_age)
348
elif args.command == 'delete-old-objects':
349
client.delete_old_objects(args.path, args.old_age)
351
print("action not understood.")
354
if __name__ == '__main__':
355
sys.exit(main(sys.argv[1:]))