~nskaggs/+junk/xenial-test

« back to all changes in this revision

Viewing changes to src/github.com/lxc/lxd/test/deps/import-busybox

  • Committer: Nicholas Skaggs
  • Date: 2016-10-24 20:56:05 UTC
  • Revision ID: nicholas.skaggs@canonical.com-20161024205605-z8lta0uvuhtxwzwl
Initi with beta15

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#!/usr/bin/env python3
 
2
import argparse
 
3
import atexit
 
4
import hashlib
 
5
import http.client
 
6
import io
 
7
import json
 
8
import os
 
9
import shutil
 
10
import socket
 
11
import subprocess
 
12
import sys
 
13
import tarfile
 
14
import tempfile
 
15
import uuid
 
16
 
 
17
 
 
18
class FriendlyParser(argparse.ArgumentParser):
 
19
    def error(self, message):
 
20
        sys.stderr.write('\nerror: %s\n' % message)
 
21
        self.print_help()
 
22
        sys.exit(2)
 
23
 
 
24
 
 
25
def find_on_path(command):
 
26
    """Is command on the executable search path?"""
 
27
 
 
28
    if 'PATH' not in os.environ:
 
29
        return False
 
30
 
 
31
    path = os.environ['PATH']
 
32
    for element in path.split(os.pathsep):
 
33
        if not element:
 
34
            continue
 
35
        filename = os.path.join(element, command)
 
36
        if os.path.isfile(filename) and os.access(filename, os.X_OK):
 
37
            return True
 
38
 
 
39
    return False
 
40
 
 
41
 
 
42
class UnixHTTPConnection(http.client.HTTPConnection):
 
43
    def __init__(self, path):
 
44
        http.client.HTTPConnection.__init__(self, 'localhost')
 
45
        self.path = path
 
46
 
 
47
    def connect(self):
 
48
        sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
 
49
        sock.connect(self.path)
 
50
        self.sock = sock
 
51
 
 
52
 
 
53
class LXD(object):
 
54
    workdir = None
 
55
 
 
56
    def __init__(self, path):
 
57
        self.lxd = UnixHTTPConnection(path)
 
58
 
 
59
        # Create our workdir
 
60
        self.workdir = tempfile.mkdtemp()
 
61
        atexit.register(self.cleanup)
 
62
 
 
63
    def cleanup(self):
 
64
        if self.workdir:
 
65
            shutil.rmtree(self.workdir)
 
66
 
 
67
    def rest_call(self, path, data=None, method="GET", headers={}):
 
68
        if method == "GET" and data:
 
69
            self.lxd.request(
 
70
                method,
 
71
                "%s?%s" % "&".join(["%s=%s" % (key, value)
 
72
                                    for key, value in data.items()]), headers)
 
73
        else:
 
74
            self.lxd.request(method, path, data, headers)
 
75
 
 
76
        r = self.lxd.getresponse()
 
77
        d = json.loads(r.read().decode("utf-8"))
 
78
        return r.status, d
 
79
 
 
80
    def aliases_create(self, name, target):
 
81
        data = json.dumps({"target": target,
 
82
                           "name": name})
 
83
 
 
84
        status, data = self.rest_call("/1.0/images/aliases", data, "POST")
 
85
 
 
86
        if status != 200:
 
87
            raise Exception("Failed to create alias: %s" % name)
 
88
 
 
89
    def aliases_remove(self, name):
 
90
        status, data = self.rest_call("/1.0/images/aliases/%s" % name,
 
91
                                      method="DELETE")
 
92
 
 
93
        if status != 200:
 
94
            raise Exception("Failed to remove alias: %s" % name)
 
95
 
 
96
    def aliases_list(self):
 
97
        status, data = self.rest_call("/1.0/images/aliases")
 
98
 
 
99
        return [alias.split("/1.0/images/aliases/")[-1]
 
100
                for alias in data['metadata']]
 
101
 
 
102
    def images_list(self, recursive=False):
 
103
        if recursive:
 
104
            status, data = self.rest_call("/1.0/images?recursion=1")
 
105
            return data['metadata']
 
106
        else:
 
107
            status, data = self.rest_call("/1.0/images")
 
108
            return [image.split("/1.0/images/")[-1]
 
109
                    for image in data['metadata']]
 
110
 
 
111
    def images_upload(self, path, filename, public):
 
112
        headers = {}
 
113
        if public:
 
114
            headers['X-LXD-public'] = "1"
 
115
 
 
116
        if isinstance(path, str):
 
117
            headers['Content-Type'] = "application/octet-stream"
 
118
 
 
119
            status, data = self.rest_call("/1.0/images", open(path, "rb"),
 
120
                                          "POST", headers)
 
121
        else:
 
122
            meta_path, rootfs_path = path
 
123
            boundary = str(uuid.uuid1())
 
124
 
 
125
            upload_path = os.path.join(self.workdir, "upload")
 
126
            body = open(upload_path, "wb+")
 
127
            for name, path in [("metadata", meta_path),
 
128
                               ("rootfs", rootfs_path)]:
 
129
                filename = os.path.basename(path)
 
130
                body.write(bytes("--%s\r\n" % boundary, "utf-8"))
 
131
                body.write(bytes("Content-Disposition: form-data; "
 
132
                                 "name=%s; filename=%s\r\n" %
 
133
                                 (name, filename), "utf-8"))
 
134
                body.write(b"Content-Type: application/octet-stream\r\n")
 
135
                body.write(b"\r\n")
 
136
                with open(path, "rb") as fd:
 
137
                    shutil.copyfileobj(fd, body)
 
138
                body.write(b"\r\n")
 
139
 
 
140
            body.write(bytes("--%s--\r\n" % boundary, "utf-8"))
 
141
            body.write(b"\r\n")
 
142
            body.close()
 
143
 
 
144
            headers['Content-Type'] = "multipart/form-data; boundary=%s" \
 
145
                % boundary
 
146
 
 
147
            status, data = self.rest_call("/1.0/images",
 
148
                                          open(upload_path, "rb"),
 
149
                                          "POST", headers)
 
150
 
 
151
        if status != 202:
 
152
            raise Exception("Failed to upload the image: %s" % status)
 
153
 
 
154
        status, data = self.rest_call(data['operation'] + "/wait",
 
155
                                      "", "GET", {})
 
156
        if status != 200:
 
157
            raise Exception("Failed to query the operation: %s" % status)
 
158
 
 
159
        if data['status_code'] != 200:
 
160
            raise Exception("Failed to import the image: %s" %
 
161
                            data['metadata'])
 
162
 
 
163
        return data['metadata']['metadata']
 
164
 
 
165
 
 
166
class Busybox(object):
 
167
    workdir = None
 
168
 
 
169
    def __init__(self):
 
170
        # Create our workdir
 
171
        self.workdir = tempfile.mkdtemp()
 
172
        atexit.register(self.cleanup)
 
173
 
 
174
    def cleanup(self):
 
175
        if self.workdir:
 
176
            shutil.rmtree(self.workdir)
 
177
 
 
178
    def create_tarball(self, split=False):
 
179
        xz = "pxz" if find_on_path("pxz") else "xz"
 
180
 
 
181
        destination_tar = os.path.join(self.workdir, "busybox.tar")
 
182
        target_tarball = tarfile.open(destination_tar, "w:")
 
183
 
 
184
        if split:
 
185
            destination_tar_rootfs = os.path.join(self.workdir,
 
186
                                                  "busybox.rootfs.tar")
 
187
            target_tarball_rootfs = tarfile.open(destination_tar_rootfs, "w:")
 
188
 
 
189
        metadata = {'architecture': os.uname()[4],
 
190
                    'creation_date': int(os.stat("/bin/busybox").st_ctime),
 
191
                    'properties': {
 
192
                        'os': "Busybox",
 
193
                        'architecture': os.uname()[4],
 
194
                        'description': "Busybox %s" % os.uname()[4],
 
195
                        'name': "busybox-%s" % os.uname()[4]
 
196
                        },
 
197
                    }
 
198
 
 
199
        # Add busybox
 
200
        with open("/bin/busybox", "rb") as fd:
 
201
            busybox_file = tarfile.TarInfo()
 
202
            busybox_file.size = os.stat("/bin/busybox").st_size
 
203
            busybox_file.mode = 0o755
 
204
            if split:
 
205
                busybox_file.name = "bin/busybox"
 
206
                target_tarball_rootfs.addfile(busybox_file, fd)
 
207
            else:
 
208
                busybox_file.name = "rootfs/bin/busybox"
 
209
                target_tarball.addfile(busybox_file, fd)
 
210
 
 
211
        # Add symlinks
 
212
        busybox = subprocess.Popen(["/bin/busybox", "--list-full"],
 
213
                                   stdout=subprocess.PIPE,
 
214
                                   universal_newlines=True)
 
215
        busybox.wait()
 
216
 
 
217
        for path in busybox.stdout.read().split("\n"):
 
218
            if not path.strip():
 
219
                continue
 
220
 
 
221
            symlink_file = tarfile.TarInfo()
 
222
            symlink_file.type = tarfile.SYMTYPE
 
223
            symlink_file.linkname = "/bin/busybox"
 
224
            if split:
 
225
                symlink_file.name = "%s" % path.strip()
 
226
                target_tarball_rootfs.addfile(symlink_file)
 
227
            else:
 
228
                symlink_file.name = "rootfs/%s" % path.strip()
 
229
                target_tarball.addfile(symlink_file)
 
230
 
 
231
        # Add directories
 
232
        for path in ("dev", "mnt", "proc", "root", "sys", "tmp"):
 
233
            directory_file = tarfile.TarInfo()
 
234
            directory_file.type = tarfile.DIRTYPE
 
235
            if split:
 
236
                directory_file.name = "%s" % path
 
237
                target_tarball_rootfs.addfile(directory_file)
 
238
            else:
 
239
                directory_file.name = "rootfs/%s" % path
 
240
                target_tarball.addfile(directory_file)
 
241
 
 
242
        # Add the metadata file
 
243
        metadata_yaml = json.dumps(metadata, sort_keys=True,
 
244
                                   indent=4, separators=(',', ': '),
 
245
                                   ensure_ascii=False).encode('utf-8') + b"\n"
 
246
 
 
247
        metadata_file = tarfile.TarInfo()
 
248
        metadata_file.size = len(metadata_yaml)
 
249
        metadata_file.name = "metadata.yaml"
 
250
        target_tarball.addfile(metadata_file,
 
251
                               io.BytesIO(metadata_yaml))
 
252
 
 
253
        # Add an /etc/inittab; this is to work around:
 
254
        # http://lists.busybox.net/pipermail/busybox/2015-November/083618.html
 
255
        # Basically, since there are some hardcoded defaults that misbehave, we
 
256
        # just pass an empty inittab so those aren't applied, and then busybox
 
257
        # doesn't spin forever.
 
258
        inittab = tarfile.TarInfo()
 
259
        inittab.size = 1
 
260
        inittab.name = "/rootfs/etc/inittab"
 
261
        target_tarball.addfile(inittab, io.BytesIO(b"\n"))
 
262
 
 
263
        target_tarball.close()
 
264
        if split:
 
265
            target_tarball_rootfs.close()
 
266
 
 
267
        # Compress the tarball
 
268
        r = subprocess.call([xz, "-9", destination_tar])
 
269
        if r:
 
270
            raise Exception("Failed to compress: %s" % destination_tar)
 
271
 
 
272
        if split:
 
273
            r = subprocess.call([xz, "-9", destination_tar_rootfs])
 
274
            if r:
 
275
                raise Exception("Failed to compress: %s" %
 
276
                                destination_tar_rootfs)
 
277
            return destination_tar + ".xz", destination_tar_rootfs + ".xz"
 
278
        else:
 
279
            return destination_tar + ".xz"
 
280
 
 
281
 
 
282
if __name__ == "__main__":
 
283
    if "LXD_DIR" in os.environ:
 
284
        lxd_socket = os.path.join(os.environ['LXD_DIR'], "unix.socket")
 
285
    else:
 
286
        lxd_socket = "/var/lib/lxd/unix.socket"
 
287
 
 
288
    if not os.path.exists(lxd_socket):
 
289
        print("LXD isn't running.")
 
290
        sys.exit(1)
 
291
 
 
292
    lxd = LXD(lxd_socket)
 
293
 
 
294
    def setup_alias(aliases, fingerprint):
 
295
        existing = lxd.aliases_list()
 
296
 
 
297
        for alias in aliases:
 
298
            if alias in existing:
 
299
                lxd.aliases_remove(alias)
 
300
            lxd.aliases_create(alias, fingerprint)
 
301
            print("Setup alias: %s" % alias)
 
302
 
 
303
    def import_busybox(parser, args):
 
304
        busybox = Busybox()
 
305
 
 
306
        if args.split:
 
307
            meta_path, rootfs_path = busybox.create_tarball(split=True)
 
308
 
 
309
            with open(meta_path, "rb") as meta_fd:
 
310
                with open(rootfs_path, "rb") as rootfs_fd:
 
311
                    fingerprint = hashlib.sha256(meta_fd.read() +
 
312
                                                 rootfs_fd.read()).hexdigest()
 
313
 
 
314
            if fingerprint in lxd.images_list():
 
315
                parser.exit(1, "This image is already in the store.\n")
 
316
 
 
317
            r = lxd.images_upload((meta_path, rootfs_path),
 
318
                                  meta_path.split("/")[-1], args.public)
 
319
            print("Image imported as: %s" % r['fingerprint'])
 
320
        else:
 
321
            path = busybox.create_tarball()
 
322
 
 
323
            with open(path, "rb") as fd:
 
324
                fingerprint = hashlib.sha256(fd.read()).hexdigest()
 
325
 
 
326
            if fingerprint in lxd.images_list():
 
327
                parser.exit(1, "This image is already in the store.\n")
 
328
 
 
329
            r = lxd.images_upload(path, path.split("/")[-1], args.public)
 
330
            print("Image imported as: %s" % r['fingerprint'])
 
331
 
 
332
        setup_alias(args.alias, fingerprint)
 
333
 
 
334
    parser = FriendlyParser(description="Import a busybox image")
 
335
    parser.add_argument("--alias", action="append",
 
336
                        default=[], help="Aliases for the image")
 
337
    parser.add_argument("--public", action="store_true",
 
338
                        default=False, help="Make the image public")
 
339
    parser.add_argument("--split", action="store_true",
 
340
                        default=False, help="Whether to create a split image")
 
341
    parser.set_defaults(func=import_busybox)
 
342
 
 
343
    # Call the function
 
344
    args = parser.parse_args()
 
345
 
 
346
    try:
 
347
        args.func(parser, args)
 
348
    except Exception as e:
 
349
        parser.error(e)