~veebers/juju-ci-tools/migration-add-migrate-back-to-original

846.1.1 by Curtis Hovey
Copied azure and joyent from juju-release-tools.
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 json
11
import os
12
import pprint
13
import subprocess
14
import sys
15
from time import sleep
16
import urllib2
17
846.1.2 by Curtis Hovey
Fix imports.
18
from utility import until_timeout
846.1.1 by Curtis Hovey
Copied azure and joyent from juju-release-tools.
19
20
21
VERSION = '0.1.0'
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'
25
26
27
SSL_SIGN = """
28
echo -n "date:" {0} |
29
    openssl dgst -sha256 -sign {1} |
30
    openssl enc -e -a |
31
    tr -d '\n'
32
"""
33
34
OLD_MACHINE_AGE = 12
35
36
37
class DeleteRequest(urllib2.Request):
38
39
    def get_method(self):
40
        return "DELETE"
41
42
43
class HeadRequest(urllib2.Request):
44
45
    def get_method(self):
46
        return "HEAD"
47
48
49
class PostRequest(urllib2.Request):
50
51
    def get_method(self):
52
        return "POST"
53
54
55
class PutRequest(urllib2.Request):
56
57
    def get_method(self):
58
        return "PUT"
59
60
61
def parse_iso_date(string):
62
    return datetime.strptime(string, ISO_8601_FORMAT)
63
64
65
class Client:
66
    """A class that mirrors MantaClient without the modern Crypto.
67
68
    See https://github.com/joyent/python-manta
69
    """
70
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('/'):
74
            sdc_url = sdc_url[1:]
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
80
        self.key_id = key_id
81
        self.key_path = key_path
82
        self.user_agent = user_agent
83
        self.pause = pause
84
        self.dry_run = dry_run
85
        self.verbose = verbose
86
87
    def make_request_headers(self, headers=None):
88
        """Return a dict of required headers.
89
90
        The Authorization header is always a signing of the "Date" header,
91
        where "date" must be lowercase.
92
        """
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)
97
        auth = (
98
            'Signature keyId="{}",algorithm="rsa-sha256",'.format(key) +
99
            'signature="{}"'.format(signature))
100
        if headers is None:
101
            headers = {}
102
        headers['Date'] = timestamp
103
        headers['Authorization'] = auth
104
        headers["User-Agent"] = USER_AGENT
105
        return headers
106
107
    def _request(self, path, method="GET", body=None, headers=None,
108
                 is_manta=False):
109
        headers = self.make_request_headers(headers)
110
        if path.startswith('/'):
111
            path = path[1:]
112
        if is_manta:
113
            base_url = self.manta_url
114
        else:
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)
125
        else:
126
            request = urllib2.Request(uri, headers=headers)
127
        try:
128
            response = urllib2.urlopen(request)
129
        except Exception as err:
130
            print(request.header_items())
131
            print(err.read())
132
            raise
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
138
139
    def _list_objects(self, path, deep=False):
140
        headers, content = self._request(path, is_manta=True)
141
        objects = []
142
        for line in content.splitlines():
143
            obj = json.loads(line)
144
            obj['path'] = '%s/%s' % (path, obj['name'])
145
            objects.append(obj)
146
            if obj['type'] == 'directory' and deep:
147
                objects.extend(self._list_objects(obj['path'], deep=True))
148
        return objects
149
150
    def list_objects(self, path, deep=False):
151
        objects = self._list_objects(path, deep=deep)
152
        for obj in objects:
153
            print('{type:9} {mtime} {path}'.format(**obj))
154
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.
161
        objects.reverse()
162
        for obj in objects:
163
            if '.joyent' in obj['path']:
164
                # The .joyent dir cannot be deleted.
165
                print('ignoring %s' % obj['path'])
166
                continue
167
            mtime = parse_iso_date(obj['mtime'])
168
            age = now - mtime
169
            if age < ago:
170
                print('ignoring young %s' % obj['path'])
171
                continue
172
            if self.verbose:
173
                print('Deleting %s' % obj['path'])
174
            if not self.dry_run:
175
                headers, content = self._request(
176
                    obj['path'], method='DELETE', is_manta=True)
177
178
    def _list_machines(self, machine_id=None):
179
        """Return a list of machine dicts."""
180
        if machine_id:
181
            path = '/machines/{}'.format(machine_id)
182
        else:
183
            path = '/machines'
184
        headers, content = self._request(path)
185
        machines = json.loads(content)
186
        if self.verbose:
187
            print(machines)
188
        return machines
189
190
    def list_machines(self, machine_id=None):
191
        machines = self._list_machines(machine_id)
192
        pprint.pprint(machines, indent=2)
193
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)
198
        if self.verbose:
199
            print(tags)
200
        return tags
201
202
    def list_machine_tags(self, machine_id):
203
        tags = self._list_machine_tags(machine_id)
204
        pprint.pprint(tags, indent=2)
205
206
    def stop_machine(self, machine_id):
207
        path = '/machines/{}?action=stop'.format(machine_id)
208
        print("Stopping machine {}".format(machine_id))
209
        if not self.dry_run:
210
            headers, content = self._request(path, method='POST')
211
212
    def delete_machine(self, machine_id):
213
        path = '/machines/{}'.format(machine_id)
214
        print("Deleting machine {}".format(machine_id))
215
        if not self.dry_run:
216
            headers, content = self._request(path, method='DELETE')
217
1167.1.2 by Curtis Hovey
Attempt to delete machines stuck in provisioning. Explain when it must be done in the Joyent UI.
218
    def attempt_deletion(self, current_stuck):
219
        all_success = True
220
        for machine_id in current_stuck:
221
            if self.verbose:
222
                print("Attempting to delete {} stuck in provisioning.".format(
223
                      machine_id))
224
            if not self.dry_run:
225
                try:
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)
230
                    if self.verbose:
231
                        print("Deleted {}".format(machine_id))
232
                except:
233
                    print('Delete stuck machine {} using the UI.'.format(
234
                          machine_id))
235
                    all_success = False
236
        return all_success
846.1.1 by Curtis Hovey
Copied azure and joyent from juju-release-tools.
237
238
    def _delete_running_machine(self, machine_id):
239
        self.stop_machine(machine_id)
240
        for ignored in until_timeout(120):
241
            if self.verbose:
242
                print(".", end="")
243
                sys.stdout.flush()
244
            sleep(self.pause)
245
            stopping_machine = self._list_machines(machine_id)
246
            if stopping_machine['state'] == 'stopped':
247
                break
248
        if self.verbose:
249
            print("stopped")
250
        self.delete_machine(machine_id)
251
1167.1.2 by Curtis Hovey
Attempt to delete machines stuck in provisioning. Explain when it must be done in the Joyent UI.
252
    def delete_old_machines(self, old_age):
846.1.1 by Curtis Hovey
Copied azure and joyent from juju-release-tools.
253
        machines = self._list_machines()
254
        now = datetime.utcnow()
255
        current_stuck = []
256
        for machine in machines:
257
            created = parse_iso_date(machine['created'])
258
            age = now - 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':
263
                    continue
264
                if machine['state'] == 'provisioning':
265
                    current_stuck.append(machine)
266
                    continue
267
                if self.verbose:
268
                    print("Machine {} is {} old".format(machine_id, age))
269
                if not self.dry_run:
270
                    self._delete_running_machine(machine_id)
271
        if not self.dry_run and current_stuck:
1167.1.2 by Curtis Hovey
Attempt to delete machines stuck in provisioning. Explain when it must be done in the Joyent UI.
272
            self.attempt_deletion(current_stuck)
846.1.1 by Curtis Hovey
Copied azure and joyent from juju-release-tools.
273
274
275
def parse_args(argv=None):
276
    """Return the argument parser for this program."""
277
    parser = ArgumentParser('Query and manage joyent.')
278
    parser.add_argument(
279
        '-d', '--dry-run', action='store_true', default=False,
280
        help='Do not make changes.')
281
    parser.add_argument(
282
        '-v', '--verbose', action="store_true", help='Increse verbosity.')
283
    parser.add_argument(
284
        "-u", "--url", dest="sdc_url",
285
        help="SDC URL. Environment: SDC_URL=URL",
286
        default=os.environ.get("SDC_URL"))
287
    parser.add_argument(
288
        "-m", "--manta-url", dest="manta_url",
289
        help="Manta URL. Environment: MANTA_URL=URL",
290
        default=os.environ.get("MANTA_URL"))
291
    parser.add_argument(
292
        "-a", "--account",
293
        help="Manta account. Environment: MANTA_USER=ACCOUNT",
294
        default=os.environ.get("MANTA_USER"))
295
    parser.add_argument(
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"))
299
    parser.add_argument(
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')
327
328
    args = parser.parse_args(argv)
329
    if not args.sdc_url:
330
        print('SDC_URL must be sourced into the environment.')
331
        sys.exit(1)
332
    return args
333
334
335
def main(argv):
336
    args = parse_args(argv)
337
    client = Client(
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':
1167.1.2 by Curtis Hovey
Attempt to delete machines stuck in provisioning. Explain when it must be done in the Joyent UI.
347
        client.delete_old_machines(args.old_age)
846.1.1 by Curtis Hovey
Copied azure and joyent from juju-release-tools.
348
    elif args.command == 'delete-old-objects':
349
        client.delete_old_objects(args.path, args.old_age)
350
    else:
351
        print("action not understood.")
352
353
354
if __name__ == '__main__':
355
    sys.exit(main(sys.argv[1:]))