18
class FriendlyParser(argparse.ArgumentParser):
19
def error(self, message):
20
sys.stderr.write('\nerror: %s\n' % message)
25
def find_on_path(command):
26
"""Is command on the executable search path?"""
28
if 'PATH' not in os.environ:
31
path = os.environ['PATH']
32
for element in path.split(os.pathsep):
35
filename = os.path.join(element, command)
36
if os.path.isfile(filename) and os.access(filename, os.X_OK):
42
class UnixHTTPConnection(http.client.HTTPConnection):
43
def __init__(self, path):
44
http.client.HTTPConnection.__init__(self, 'localhost')
48
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
49
sock.connect(self.path)
56
def __init__(self, path):
57
self.lxd = UnixHTTPConnection(path)
60
self.workdir = tempfile.mkdtemp()
61
atexit.register(self.cleanup)
65
shutil.rmtree(self.workdir)
67
def rest_call(self, path, data=None, method="GET", headers={}):
68
if method == "GET" and data:
71
"%s?%s" % "&".join(["%s=%s" % (key, value)
72
for key, value in data.items()]), headers)
74
self.lxd.request(method, path, data, headers)
76
r = self.lxd.getresponse()
77
d = json.loads(r.read().decode("utf-8"))
80
def aliases_create(self, name, target):
81
data = json.dumps({"target": target,
84
status, data = self.rest_call("/1.0/images/aliases", data, "POST")
87
raise Exception("Failed to create alias: %s" % name)
89
def aliases_remove(self, name):
90
status, data = self.rest_call("/1.0/images/aliases/%s" % name,
94
raise Exception("Failed to remove alias: %s" % name)
96
def aliases_list(self):
97
status, data = self.rest_call("/1.0/images/aliases")
99
return [alias.split("/1.0/images/aliases/")[-1]
100
for alias in data['metadata']]
102
def images_list(self, recursive=False):
104
status, data = self.rest_call("/1.0/images?recursion=1")
105
return data['metadata']
107
status, data = self.rest_call("/1.0/images")
108
return [image.split("/1.0/images/")[-1]
109
for image in data['metadata']]
111
def images_upload(self, path, filename, public):
114
headers['X-LXD-public'] = "1"
116
if isinstance(path, str):
117
headers['Content-Type'] = "application/octet-stream"
119
status, data = self.rest_call("/1.0/images", open(path, "rb"),
122
meta_path, rootfs_path = path
123
boundary = str(uuid.uuid1())
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")
136
with open(path, "rb") as fd:
137
shutil.copyfileobj(fd, body)
140
body.write(bytes("--%s--\r\n" % boundary, "utf-8"))
144
headers['Content-Type'] = "multipart/form-data; boundary=%s" \
147
status, data = self.rest_call("/1.0/images",
148
open(upload_path, "rb"),
152
raise Exception("Failed to upload the image: %s" % status)
154
status, data = self.rest_call(data['operation'] + "/wait",
157
raise Exception("Failed to query the operation: %s" % status)
159
if data['status_code'] != 200:
160
raise Exception("Failed to import the image: %s" %
163
return data['metadata']['metadata']
166
class Busybox(object):
171
self.workdir = tempfile.mkdtemp()
172
atexit.register(self.cleanup)
176
shutil.rmtree(self.workdir)
178
def create_tarball(self, split=False):
179
xz = "pxz" if find_on_path("pxz") else "xz"
181
destination_tar = os.path.join(self.workdir, "busybox.tar")
182
target_tarball = tarfile.open(destination_tar, "w:")
185
destination_tar_rootfs = os.path.join(self.workdir,
186
"busybox.rootfs.tar")
187
target_tarball_rootfs = tarfile.open(destination_tar_rootfs, "w:")
189
metadata = {'architecture': os.uname()[4],
190
'creation_date': int(os.stat("/bin/busybox").st_ctime),
193
'architecture': os.uname()[4],
194
'description': "Busybox %s" % os.uname()[4],
195
'name': "busybox-%s" % os.uname()[4]
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
205
busybox_file.name = "bin/busybox"
206
target_tarball_rootfs.addfile(busybox_file, fd)
208
busybox_file.name = "rootfs/bin/busybox"
209
target_tarball.addfile(busybox_file, fd)
212
busybox = subprocess.Popen(["/bin/busybox", "--list-full"],
213
stdout=subprocess.PIPE,
214
universal_newlines=True)
217
for path in busybox.stdout.read().split("\n"):
221
symlink_file = tarfile.TarInfo()
222
symlink_file.type = tarfile.SYMTYPE
223
symlink_file.linkname = "/bin/busybox"
225
symlink_file.name = "%s" % path.strip()
226
target_tarball_rootfs.addfile(symlink_file)
228
symlink_file.name = "rootfs/%s" % path.strip()
229
target_tarball.addfile(symlink_file)
232
for path in ("dev", "mnt", "proc", "root", "sys", "tmp"):
233
directory_file = tarfile.TarInfo()
234
directory_file.type = tarfile.DIRTYPE
236
directory_file.name = "%s" % path
237
target_tarball_rootfs.addfile(directory_file)
239
directory_file.name = "rootfs/%s" % path
240
target_tarball.addfile(directory_file)
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"
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))
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()
260
inittab.name = "/rootfs/etc/inittab"
261
target_tarball.addfile(inittab, io.BytesIO(b"\n"))
263
target_tarball.close()
265
target_tarball_rootfs.close()
267
# Compress the tarball
268
r = subprocess.call([xz, "-9", destination_tar])
270
raise Exception("Failed to compress: %s" % destination_tar)
273
r = subprocess.call([xz, "-9", destination_tar_rootfs])
275
raise Exception("Failed to compress: %s" %
276
destination_tar_rootfs)
277
return destination_tar + ".xz", destination_tar_rootfs + ".xz"
279
return destination_tar + ".xz"
282
if __name__ == "__main__":
283
if "LXD_DIR" in os.environ:
284
lxd_socket = os.path.join(os.environ['LXD_DIR'], "unix.socket")
286
lxd_socket = "/var/lib/lxd/unix.socket"
288
if not os.path.exists(lxd_socket):
289
print("LXD isn't running.")
292
lxd = LXD(lxd_socket)
294
def setup_alias(aliases, fingerprint):
295
existing = lxd.aliases_list()
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)
303
def import_busybox(parser, args):
307
meta_path, rootfs_path = busybox.create_tarball(split=True)
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()
314
if fingerprint in lxd.images_list():
315
parser.exit(1, "This image is already in the store.\n")
317
r = lxd.images_upload((meta_path, rootfs_path),
318
meta_path.split("/")[-1], args.public)
319
print("Image imported as: %s" % r['fingerprint'])
321
path = busybox.create_tarball()
323
with open(path, "rb") as fd:
324
fingerprint = hashlib.sha256(fd.read()).hexdigest()
326
if fingerprint in lxd.images_list():
327
parser.exit(1, "This image is already in the store.\n")
329
r = lxd.images_upload(path, path.split("/")[-1], args.public)
330
print("Image imported as: %s" % r['fingerprint'])
332
setup_alias(args.alias, fingerprint)
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)
344
args = parser.parse_args()
347
args.func(parser, args)
348
except Exception as e: