occystrap 0.1.1__py3-none-any.whl → 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- occystrap/common.py +40 -0
- occystrap/constants.py +186 -0
- occystrap/docker_registry.py +118 -85
- occystrap/main.py +114 -9
- occystrap/output_directory.py +318 -0
- occystrap/output_mounts.py +155 -0
- occystrap/output_ocibundle.py +53 -0
- occystrap/output_tarfile.py +55 -0
- occystrap/util.py +9 -1
- occystrap-0.3.0.dist-info/METADATA +131 -0
- occystrap-0.3.0.dist-info/RECORD +20 -0
- {occystrap-0.1.1.dist-info → occystrap-0.3.0.dist-info}/WHEEL +1 -1
- occystrap-0.3.0.dist-info/pbr.json +1 -0
- occystrap-0.1.1.dist-info/METADATA +0 -33
- occystrap-0.1.1.dist-info/RECORD +0 -14
- occystrap-0.1.1.dist-info/pbr.json +0 -1
- {occystrap-0.1.1.dist-info → occystrap-0.3.0.dist-info}/AUTHORS +0 -0
- {occystrap-0.1.1.dist-info → occystrap-0.3.0.dist-info}/LICENSE +0 -0
- {occystrap-0.1.1.dist-info → occystrap-0.3.0.dist-info}/entry_points.txt +0 -0
- {occystrap-0.1.1.dist-info → occystrap-0.3.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
import tarfile
|
|
5
|
+
|
|
6
|
+
from occystrap import constants
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
LOG = logging.getLogger(__name__)
|
|
10
|
+
LOG.setLevel(logging.INFO)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# Python supports the following tarfile object types: REGTYPE, AREGTYPE,
|
|
14
|
+
# LNKTYPE, SYMTYPE, DIRTYPE, FIFOTYPE, CONTTYPE, CHRTYPE, BLKTYPE,
|
|
15
|
+
# GNUTYPE_SPARSE. It also supports GNUTYPE_LONGNAME, GNUTYPE_LONGLINK but
|
|
16
|
+
# they are not mentioned in the documentation just to keep things fun.
|
|
17
|
+
class BundleObject(object):
|
|
18
|
+
def __init__(self, name, tarpath, ti):
|
|
19
|
+
self.name = name
|
|
20
|
+
self.tarpath = tarpath
|
|
21
|
+
self.size = 0
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class BundleFile(BundleObject):
|
|
25
|
+
def __init__(self, name, tarpath, ti):
|
|
26
|
+
super(BundleFile, self).__init__(name, tarpath, ti)
|
|
27
|
+
self.size = ti.size
|
|
28
|
+
self.mtime = ti.mtime
|
|
29
|
+
self.mode = ti.mode
|
|
30
|
+
self.uid = ti.uid
|
|
31
|
+
self.gid = ti.gid
|
|
32
|
+
self.uname = ti.uname
|
|
33
|
+
self.gname = ti.gname
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class BundleDeletedFile(BundleObject):
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class BundleLink(BundleObject):
|
|
41
|
+
def __init__(self, name, tarpath, ti):
|
|
42
|
+
super(BundleLink, self).__init__(name, tarpath, ti)
|
|
43
|
+
self.linkname = ti.linkname
|
|
44
|
+
self.mtime = ti.mtime
|
|
45
|
+
self.mode = ti.mode
|
|
46
|
+
self.uid = ti.uid
|
|
47
|
+
self.gid = ti.gid
|
|
48
|
+
self.uname = ti.uname
|
|
49
|
+
self.gname = ti.gname
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class BundleHardLink(BundleLink):
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class BundleSymLink(BundleLink):
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class BundleDirectory(BundleObject):
|
|
61
|
+
def __init__(self, name, tarpath, ti):
|
|
62
|
+
super(BundleDirectory, self).__init__(name, tarpath, ti)
|
|
63
|
+
self.mtime = ti.mtime
|
|
64
|
+
self.mode = ti.mode
|
|
65
|
+
self.uid = ti.uid
|
|
66
|
+
self.gid = ti.gid
|
|
67
|
+
self.uname = ti.uname
|
|
68
|
+
self.gname = ti.gname
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class BundleFIFO(BundleObject):
|
|
72
|
+
def __init__(self, name, tarpath, ti):
|
|
73
|
+
super(BundleFIFO, self).__init__(name, tarpath, ti)
|
|
74
|
+
self.mtime = ti.mtime
|
|
75
|
+
self.mode = ti.mode
|
|
76
|
+
self.uid = ti.uid
|
|
77
|
+
self.gid = ti.gid
|
|
78
|
+
self.uname = ti.uname
|
|
79
|
+
self.gname = ti.gname
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
TARFILE_TYPE_MAP = {
|
|
83
|
+
tarfile.REGTYPE: BundleFile,
|
|
84
|
+
tarfile.AREGTYPE: BundleFile,
|
|
85
|
+
'deleted': BundleDeletedFile,
|
|
86
|
+
tarfile.LNKTYPE: BundleHardLink,
|
|
87
|
+
tarfile.SYMTYPE: BundleSymLink,
|
|
88
|
+
tarfile.DIRTYPE: BundleDirectory,
|
|
89
|
+
tarfile.FIFOTYPE: BundleFIFO,
|
|
90
|
+
tarfile.CONTTYPE: BundleFile,
|
|
91
|
+
tarfile.CHRTYPE: BundleFile,
|
|
92
|
+
tarfile.BLKTYPE: BundleFile,
|
|
93
|
+
tarfile.GNUTYPE_SPARSE: BundleFile,
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class DirWriter(object):
|
|
98
|
+
def __init__(self, image, tag, image_path, unique_names=False, expand=False):
|
|
99
|
+
self.image = image
|
|
100
|
+
self.tag = tag
|
|
101
|
+
self.image_path = image_path
|
|
102
|
+
self.unique_names = unique_names
|
|
103
|
+
self.expand = expand
|
|
104
|
+
|
|
105
|
+
self.tar_manifest = [{
|
|
106
|
+
'Layers': [],
|
|
107
|
+
'RepoTags': ['%s:%s' % (self.image.split('/')[-1], self.tag)]
|
|
108
|
+
}]
|
|
109
|
+
if self.unique_names:
|
|
110
|
+
self.tar_manifest[0]['ImageName'] = self.image
|
|
111
|
+
|
|
112
|
+
self.bundle = {}
|
|
113
|
+
|
|
114
|
+
if not os.path.exists(self.image_path):
|
|
115
|
+
os.makedirs(self.image_path)
|
|
116
|
+
|
|
117
|
+
def _manifest_filename(self):
|
|
118
|
+
if not self.unique_names:
|
|
119
|
+
return 'manifest'
|
|
120
|
+
else:
|
|
121
|
+
return ('manifest-%s-%s' % (self.image.replace('/', '_'),
|
|
122
|
+
self.tag.replace('/', '_')))
|
|
123
|
+
|
|
124
|
+
def _create_bundle_path(self, path):
|
|
125
|
+
d = self.bundle
|
|
126
|
+
for elem in path.split('/'):
|
|
127
|
+
if elem not in d:
|
|
128
|
+
d[elem] = {}
|
|
129
|
+
d = d[elem]
|
|
130
|
+
return d
|
|
131
|
+
|
|
132
|
+
def fetch_callback(self, digest):
|
|
133
|
+
layer_file_in_dir = os.path.join(self.image_path, digest, 'layer.tar')
|
|
134
|
+
LOG.info('Layer file is %s' % layer_file_in_dir)
|
|
135
|
+
return not os.path.exists(layer_file_in_dir)
|
|
136
|
+
|
|
137
|
+
def process_image_element(self, element_type, name, data):
|
|
138
|
+
if element_type == constants.CONFIG_FILE:
|
|
139
|
+
with open(os.path.join(self.image_path, name), 'wb') as f:
|
|
140
|
+
d = json.loads(data.read())
|
|
141
|
+
f.write(json.dumps(d, indent=4, sort_keys=True).encode('ascii'))
|
|
142
|
+
self.tar_manifest[0]['Config'] = name
|
|
143
|
+
|
|
144
|
+
elif element_type == constants.IMAGE_LAYER:
|
|
145
|
+
layer_dir = os.path.join(self.image_path, name)
|
|
146
|
+
if not os.path.exists(layer_dir):
|
|
147
|
+
os.makedirs(layer_dir)
|
|
148
|
+
|
|
149
|
+
layer_file = os.path.join(name, 'layer.tar')
|
|
150
|
+
self.tar_manifest[0]['Layers'].append(layer_file)
|
|
151
|
+
|
|
152
|
+
layer_file_in_dir = os.path.join(self.image_path, layer_file)
|
|
153
|
+
if os.path.exists(layer_file_in_dir):
|
|
154
|
+
LOG.info('Skipping layer already in output directory')
|
|
155
|
+
else:
|
|
156
|
+
with open(layer_file_in_dir, 'wb') as f:
|
|
157
|
+
d = data.read(102400)
|
|
158
|
+
while d:
|
|
159
|
+
f.write(d)
|
|
160
|
+
d = data.read(102400)
|
|
161
|
+
|
|
162
|
+
if self.expand:
|
|
163
|
+
# Build a in-memory map of the layout of the final image bundle
|
|
164
|
+
with tarfile.open(layer_file_in_dir) as layer:
|
|
165
|
+
for mem in layer.getmembers():
|
|
166
|
+
path = mem.name
|
|
167
|
+
dirname, filename = os.path.split(mem.name)
|
|
168
|
+
|
|
169
|
+
# Some light reading on how this works...
|
|
170
|
+
# https://github.com/opencontainers/image-spec/blob/main/layer.md#opaque-whiteout
|
|
171
|
+
if filename == '.wh..wh..opq':
|
|
172
|
+
# A deleted directory, but only for layers below
|
|
173
|
+
# this one.
|
|
174
|
+
for ent in self.bundle:
|
|
175
|
+
if (ent.startswith(dirname) and
|
|
176
|
+
self.bundle[ent][-1].tarpath != layer_file):
|
|
177
|
+
self.bundle[ent].append(
|
|
178
|
+
BundleDeletedFile(ent, layer_file, mem))
|
|
179
|
+
continue
|
|
180
|
+
|
|
181
|
+
elif filename.startswith('.wh.'):
|
|
182
|
+
# A single deleted element, which might not be a
|
|
183
|
+
# file.
|
|
184
|
+
path = os.path.join(dirname, filename[4:])
|
|
185
|
+
if type(self.bundle[path][-1]) is BundleDirectory:
|
|
186
|
+
for ent in self.bundle:
|
|
187
|
+
if ent.startswith(path):
|
|
188
|
+
self.bundle[ent].append(
|
|
189
|
+
BundleDeletedFile(ent, layer_file, mem))
|
|
190
|
+
|
|
191
|
+
serialized = BundleDeletedFile(
|
|
192
|
+
path, layer_file, mem)
|
|
193
|
+
else:
|
|
194
|
+
serialized = TARFILE_TYPE_MAP[mem.type](
|
|
195
|
+
mem.name, layer_file, mem)
|
|
196
|
+
|
|
197
|
+
self.bundle.setdefault(path, [])
|
|
198
|
+
self.bundle[path].append(serialized)
|
|
199
|
+
|
|
200
|
+
def _log_bundle(self):
|
|
201
|
+
savings = 0
|
|
202
|
+
|
|
203
|
+
for path in self.bundle:
|
|
204
|
+
versions = len(self.bundle[path])
|
|
205
|
+
if versions > 1:
|
|
206
|
+
path_savings = 0
|
|
207
|
+
LOG.info('Bundle path "%s" has %d versions'
|
|
208
|
+
% (path, versions))
|
|
209
|
+
for ver in self.bundle[path][:-1]:
|
|
210
|
+
path_savings += ver.size
|
|
211
|
+
if type(self.bundle[path][-1]) is BundleDeletedFile:
|
|
212
|
+
LOG.info('Bundle path "%s" final version is a deleted file, '
|
|
213
|
+
'which wasted %d bytes.' % (path, path_savings))
|
|
214
|
+
savings += path_savings
|
|
215
|
+
|
|
216
|
+
LOG.info('Flattening image would save %d bytes' % savings)
|
|
217
|
+
|
|
218
|
+
def finalize(self):
|
|
219
|
+
if self.expand:
|
|
220
|
+
self._log_bundle()
|
|
221
|
+
|
|
222
|
+
manifest_filename = self._manifest_filename() + '.json'
|
|
223
|
+
manifest_path = os.path.join(self.image_path, manifest_filename)
|
|
224
|
+
with open(manifest_path, 'wb') as f:
|
|
225
|
+
f.write(json.dumps(self.tar_manifest, indent=4,
|
|
226
|
+
sort_keys=True).encode('ascii'))
|
|
227
|
+
|
|
228
|
+
c = {}
|
|
229
|
+
catalog_path = os.path.join(self.image_path, 'catalog.json')
|
|
230
|
+
if os.path.exists(catalog_path):
|
|
231
|
+
with open(catalog_path, 'r') as f:
|
|
232
|
+
c = json.loads(f.read())
|
|
233
|
+
|
|
234
|
+
c.setdefault(self.image, {})
|
|
235
|
+
c[self.image][self.tag] = manifest_filename
|
|
236
|
+
with open(catalog_path, 'w') as f:
|
|
237
|
+
f.write(json.dumps(c, indent=4, sort_keys=True))
|
|
238
|
+
|
|
239
|
+
def _extract_rootfs(self, rootfs_path):
|
|
240
|
+
# Reading tarfiles is expensive, as tarfile needs to scan the
|
|
241
|
+
# entire file to find the right entry. It builds a cache while
|
|
242
|
+
# doing this however, so performance improves if you access a
|
|
243
|
+
# bunch of files from the same archive. We therefore group
|
|
244
|
+
# entities by layer to improve performance.
|
|
245
|
+
entities_by_layer = {}
|
|
246
|
+
|
|
247
|
+
# We defer changing the permissions of directories until later
|
|
248
|
+
# so that permissions don't affect the writing of files.
|
|
249
|
+
deferred_by_layer = {}
|
|
250
|
+
|
|
251
|
+
# Find all the entities
|
|
252
|
+
for path in self.bundle:
|
|
253
|
+
ent = self.bundle[path][-1]
|
|
254
|
+
|
|
255
|
+
if type(ent) is BundleDirectory:
|
|
256
|
+
deferred_by_layer.setdefault(ent.tarpath, [])
|
|
257
|
+
deferred_by_layer[ent.tarpath].append(ent)
|
|
258
|
+
continue
|
|
259
|
+
|
|
260
|
+
if type(ent) is BundleDeletedFile:
|
|
261
|
+
continue
|
|
262
|
+
|
|
263
|
+
entities_by_layer.setdefault(ent.tarpath, [])
|
|
264
|
+
entities_by_layer[ent.tarpath].append(ent)
|
|
265
|
+
|
|
266
|
+
for tarpath in entities_by_layer:
|
|
267
|
+
with tarfile.open(os.path.join(self.image_path, tarpath)) as layer:
|
|
268
|
+
for ent in entities_by_layer[tarpath]:
|
|
269
|
+
layer.extract(ent.name, path=rootfs_path)
|
|
270
|
+
|
|
271
|
+
for tarpath in deferred_by_layer:
|
|
272
|
+
with tarfile.open(os.path.join(self.image_path, tarpath)) as layer:
|
|
273
|
+
for ent in deferred_by_layer[tarpath]:
|
|
274
|
+
layer.extract(ent.name, path=rootfs_path)
|
|
275
|
+
|
|
276
|
+
def write_bundle(self):
|
|
277
|
+
manifest_filename = self._manifest_filename()
|
|
278
|
+
manifest_path = os.path.join(self.image_path, manifest_filename)
|
|
279
|
+
if not os.path.exists(manifest_path):
|
|
280
|
+
os.makedirs(manifest_path)
|
|
281
|
+
LOG.info('Writing image bundle to %s' % manifest_path)
|
|
282
|
+
self._extract_rootfs(manifest_path)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
class NoSuchImageException(Exception):
|
|
286
|
+
pass
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
class DirReader(object):
|
|
290
|
+
def __init__(self, path, image, tag):
|
|
291
|
+
self.path = path
|
|
292
|
+
self.image = image
|
|
293
|
+
self.tag = tag
|
|
294
|
+
|
|
295
|
+
c = {}
|
|
296
|
+
catalog_path = os.path.join(self.path, 'catalog.json')
|
|
297
|
+
if os.path.exists(catalog_path):
|
|
298
|
+
with open(catalog_path, 'r') as f:
|
|
299
|
+
c = json.loads(f.read())
|
|
300
|
+
|
|
301
|
+
if self.image not in c:
|
|
302
|
+
raise NoSuchImageException(self.image)
|
|
303
|
+
if self.tag not in c[self.image]:
|
|
304
|
+
raise NoSuchImageException(self.image)
|
|
305
|
+
|
|
306
|
+
self.manifest_filename = c[self.image][self.tag]
|
|
307
|
+
|
|
308
|
+
def fetch(self):
|
|
309
|
+
with open(os.path.join(self.path, self.manifest_filename)) as f:
|
|
310
|
+
manifest = json.loads(f.read())
|
|
311
|
+
|
|
312
|
+
config_filename = manifest[0]['Config']
|
|
313
|
+
with open(os.path.join(self.path, config_filename), 'rb') as f:
|
|
314
|
+
yield (constants.CONFIG_FILE, config_filename, f)
|
|
315
|
+
|
|
316
|
+
for layer in manifest[0]['Layers']:
|
|
317
|
+
with open(os.path.join(self.path, layer), 'rb') as f:
|
|
318
|
+
yield (constants.IMAGE_LAYER, layer, f)
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
import stat
|
|
5
|
+
import tarfile
|
|
6
|
+
|
|
7
|
+
from occystrap import common
|
|
8
|
+
from occystrap import constants
|
|
9
|
+
from occystrap import util
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
LOG = logging.getLogger(__name__)
|
|
13
|
+
LOG.setLevel(logging.INFO)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class MountWriter(object):
|
|
17
|
+
def __init__(self, image, tag, image_path):
|
|
18
|
+
self.image = image
|
|
19
|
+
self.tag = tag
|
|
20
|
+
self.image_path = image_path
|
|
21
|
+
|
|
22
|
+
self.tar_manifest = [{
|
|
23
|
+
'Layers': [],
|
|
24
|
+
'RepoTags': ['%s:%s' % (self.image.split('/')[-1], self.tag)]
|
|
25
|
+
}]
|
|
26
|
+
|
|
27
|
+
self.bundle = {}
|
|
28
|
+
|
|
29
|
+
if not os.path.exists(self.image_path):
|
|
30
|
+
os.makedirs(self.image_path)
|
|
31
|
+
|
|
32
|
+
def _manifest_filename(self):
|
|
33
|
+
return 'manifest'
|
|
34
|
+
|
|
35
|
+
def fetch_callback(self, digest):
|
|
36
|
+
layer_file_in_dir = os.path.join(self.image_path, digest, 'layer.tar')
|
|
37
|
+
LOG.info('Layer file is %s' % layer_file_in_dir)
|
|
38
|
+
return not os.path.exists(layer_file_in_dir)
|
|
39
|
+
|
|
40
|
+
def process_image_element(self, element_type, name, data):
|
|
41
|
+
if element_type == constants.CONFIG_FILE:
|
|
42
|
+
with open(os.path.join(self.image_path, name), 'wb') as f:
|
|
43
|
+
d = json.loads(data.read())
|
|
44
|
+
f.write(json.dumps(d, indent=4, sort_keys=True).encode('ascii'))
|
|
45
|
+
self.tar_manifest[0]['Config'] = name
|
|
46
|
+
|
|
47
|
+
elif element_type == constants.IMAGE_LAYER:
|
|
48
|
+
layer_dir = os.path.join(self.image_path, name)
|
|
49
|
+
if not os.path.exists(layer_dir):
|
|
50
|
+
os.makedirs(layer_dir)
|
|
51
|
+
|
|
52
|
+
layer_file = os.path.join(name, 'layer.tar')
|
|
53
|
+
self.tar_manifest[0]['Layers'].append(layer_file)
|
|
54
|
+
|
|
55
|
+
layer_file_in_dir = os.path.join(self.image_path, layer_file)
|
|
56
|
+
if os.path.exists(layer_file_in_dir):
|
|
57
|
+
LOG.info('Skipping layer already in output directory')
|
|
58
|
+
else:
|
|
59
|
+
with open(layer_file_in_dir, 'wb') as f:
|
|
60
|
+
d = data.read(102400)
|
|
61
|
+
while d:
|
|
62
|
+
f.write(d)
|
|
63
|
+
d = data.read(102400)
|
|
64
|
+
|
|
65
|
+
layer_dir_in_dir = os.path.join(self.image_path, name, 'layer')
|
|
66
|
+
os.makedirs(layer_dir_in_dir)
|
|
67
|
+
with tarfile.open(layer_file_in_dir) as layer:
|
|
68
|
+
for mem in layer.getmembers():
|
|
69
|
+
dirname, filename = os.path.split(mem.name)
|
|
70
|
+
|
|
71
|
+
# Some light reading on how this works...
|
|
72
|
+
# https://www.madebymikal.com/interpreting-whiteout-files-in-docker-image-layers/
|
|
73
|
+
# https://github.com/opencontainers/image-spec/blob/main/layer.md#opaque-whiteout
|
|
74
|
+
if filename == '.wh..wh..opq':
|
|
75
|
+
# A deleted directory, but only for layers below
|
|
76
|
+
# this one.
|
|
77
|
+
os.setxattr(os.path.join(layer_dir_in_dir, dirname),
|
|
78
|
+
'trusted.overlay.opaque', b'y')
|
|
79
|
+
|
|
80
|
+
elif filename.startswith('.wh.'):
|
|
81
|
+
# A single deleted element, which might not be a
|
|
82
|
+
# file.
|
|
83
|
+
os.mknod(os.path.join(layer_dir_in_dir,
|
|
84
|
+
mem.name[4:]),
|
|
85
|
+
mode=stat.S_IFCHR, device=0)
|
|
86
|
+
|
|
87
|
+
else:
|
|
88
|
+
path = mem.name
|
|
89
|
+
layer.extract(path, path=layer_dir_in_dir)
|
|
90
|
+
|
|
91
|
+
def finalize(self):
|
|
92
|
+
manifest_filename = self._manifest_filename() + '.json'
|
|
93
|
+
manifest_path = os.path.join(self.image_path, manifest_filename)
|
|
94
|
+
with open(manifest_path, 'wb') as f:
|
|
95
|
+
f.write(json.dumps(self.tar_manifest, indent=4,
|
|
96
|
+
sort_keys=True).encode('ascii'))
|
|
97
|
+
|
|
98
|
+
c = {}
|
|
99
|
+
catalog_path = os.path.join(self.image_path, 'catalog.json')
|
|
100
|
+
if os.path.exists(catalog_path):
|
|
101
|
+
with open(catalog_path, 'r') as f:
|
|
102
|
+
c = json.loads(f.read())
|
|
103
|
+
|
|
104
|
+
c.setdefault(self.image, {})
|
|
105
|
+
c[self.image][self.tag] = manifest_filename
|
|
106
|
+
with open(catalog_path, 'w') as f:
|
|
107
|
+
f.write(json.dumps(c, indent=4, sort_keys=True))
|
|
108
|
+
|
|
109
|
+
def write_bundle(self, container_template=constants.RUNC_SPEC_TEMPLATE,
|
|
110
|
+
container_values=None):
|
|
111
|
+
if not container_values:
|
|
112
|
+
container_values = {}
|
|
113
|
+
|
|
114
|
+
rootfs_path = os.path.join(self.image_path, 'rootfs')
|
|
115
|
+
if not os.path.exists(rootfs_path):
|
|
116
|
+
os.makedirs(rootfs_path)
|
|
117
|
+
LOG.info('Writing image bundle to %s' % rootfs_path)
|
|
118
|
+
|
|
119
|
+
working_path = os.path.join(self.image_path, 'working')
|
|
120
|
+
if not os.path.exists(working_path):
|
|
121
|
+
os.makedirs(working_path)
|
|
122
|
+
|
|
123
|
+
delta_path = os.path.join(self.image_path, 'delta')
|
|
124
|
+
if not os.path.exists(delta_path):
|
|
125
|
+
os.makedirs(delta_path)
|
|
126
|
+
|
|
127
|
+
# The newest layer is listed first in the mount command
|
|
128
|
+
layer_dirs = []
|
|
129
|
+
self.tar_manifest[0]['Layers'].reverse()
|
|
130
|
+
for layer in self.tar_manifest[0]['Layers']:
|
|
131
|
+
layer_dirs.append(os.path.join(
|
|
132
|
+
self.image_path, layer.replace('.tar', '')))
|
|
133
|
+
|
|
134
|
+
# Extract the rootfs as overlay mounts
|
|
135
|
+
util.execute('mount -t overlay overlay -o lowerdir=%(layers)s,'
|
|
136
|
+
'upperdir=%(upper)s,workdir=%(working)s %(rootfs)s'
|
|
137
|
+
% {
|
|
138
|
+
'layers': ':'.join(layer_dirs),
|
|
139
|
+
'upper': delta_path,
|
|
140
|
+
'working': working_path,
|
|
141
|
+
'rootfs': rootfs_path
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
# Rename the container configuration to a well known location. This is
|
|
145
|
+
# not part of the OCI specification, but is convenient for now.
|
|
146
|
+
container_config_filename = os.path.join(self.image_path,
|
|
147
|
+
'container-config.json')
|
|
148
|
+
runtime_config_filename = os.path.join(self.image_path, 'config.json')
|
|
149
|
+
os.rename(os.path.join(self.image_path, self.tar_manifest[0]['Config']),
|
|
150
|
+
container_config_filename)
|
|
151
|
+
|
|
152
|
+
common.write_container_config(container_config_filename,
|
|
153
|
+
runtime_config_filename,
|
|
154
|
+
container_template=container_template,
|
|
155
|
+
container_values=container_values)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# OCI bundles are a special case of our directory output -- they don't contain
|
|
2
|
+
# all of the data that a directory output does, and they place the data they
|
|
3
|
+
# do contain into different locations within the directory structure.
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
import shutil
|
|
8
|
+
|
|
9
|
+
from occystrap.constants import RUNC_SPEC_TEMPLATE
|
|
10
|
+
from occystrap import common
|
|
11
|
+
from occystrap.output_directory import DirWriter
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
LOG = logging.getLogger(__name__)
|
|
15
|
+
LOG.setLevel(logging.INFO)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class OCIBundleWriter(DirWriter):
|
|
19
|
+
def __init__(self, image, tag, image_path):
|
|
20
|
+
super(OCIBundleWriter, self).__init__(
|
|
21
|
+
image, tag, image_path, expand=True)
|
|
22
|
+
|
|
23
|
+
def finalize(self):
|
|
24
|
+
self._log_bundle()
|
|
25
|
+
|
|
26
|
+
def write_bundle(self, container_template=RUNC_SPEC_TEMPLATE,
|
|
27
|
+
container_values=None):
|
|
28
|
+
if not container_values:
|
|
29
|
+
container_values = {}
|
|
30
|
+
|
|
31
|
+
rootfs_path = os.path.join(self.image_path, 'rootfs')
|
|
32
|
+
if not os.path.exists(rootfs_path):
|
|
33
|
+
os.makedirs(rootfs_path)
|
|
34
|
+
LOG.info('Writing image bundle to %s' % rootfs_path)
|
|
35
|
+
self._extract_rootfs(rootfs_path)
|
|
36
|
+
|
|
37
|
+
# Remove parts of the output directory which are not present in OCI
|
|
38
|
+
for layer_file in self.tar_manifest[0]['Layers']:
|
|
39
|
+
shutil.rmtree(os.path.join(self.image_path,
|
|
40
|
+
os.path.split(layer_file)[0]))
|
|
41
|
+
|
|
42
|
+
# Rename the container configuration to a well known location. This is
|
|
43
|
+
# not part of the OCI specification, but is convenient for now.
|
|
44
|
+
container_config_filename = os.path.join(self.image_path,
|
|
45
|
+
'container-config.json')
|
|
46
|
+
runtime_config_filename = os.path.join(self.image_path, 'config.json')
|
|
47
|
+
os.rename(os.path.join(self.image_path, self.tar_manifest[0]['Config']),
|
|
48
|
+
container_config_filename)
|
|
49
|
+
|
|
50
|
+
common.write_container_config(container_config_filename,
|
|
51
|
+
runtime_config_filename,
|
|
52
|
+
container_template=container_template,
|
|
53
|
+
container_values=container_values)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import tarfile
|
|
6
|
+
|
|
7
|
+
from occystrap import constants
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
LOG = logging.getLogger(__name__)
|
|
11
|
+
LOG.setLevel(logging.INFO)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TarWriter(object):
|
|
15
|
+
def __init__(self, image, tag, image_path):
|
|
16
|
+
self.image = image
|
|
17
|
+
self.tag = tag
|
|
18
|
+
self.image_path = image_path
|
|
19
|
+
self.image_tar = tarfile.open(image_path, 'w')
|
|
20
|
+
|
|
21
|
+
self.tar_manifest = [{
|
|
22
|
+
'Layers': [],
|
|
23
|
+
'RepoTags': ['%s:%s' % (self.image.split('/')[-1], self.tag)]
|
|
24
|
+
}]
|
|
25
|
+
|
|
26
|
+
def fetch_callback(self, digest):
|
|
27
|
+
return True
|
|
28
|
+
|
|
29
|
+
def process_image_element(self, element_type, name, data):
|
|
30
|
+
if element_type == constants.CONFIG_FILE:
|
|
31
|
+
LOG.info('Writing config file to tarball')
|
|
32
|
+
|
|
33
|
+
ti = tarfile.TarInfo(name)
|
|
34
|
+
ti.size = len(data.read())
|
|
35
|
+
data.seek(0)
|
|
36
|
+
self.image_tar.addfile(ti, data)
|
|
37
|
+
self.tar_manifest[0]['Config'] = name
|
|
38
|
+
|
|
39
|
+
elif element_type == constants.IMAGE_LAYER:
|
|
40
|
+
LOG.info('Writing layer to tarball')
|
|
41
|
+
|
|
42
|
+
name += '/layer.tar'
|
|
43
|
+
ti = tarfile.TarInfo(name)
|
|
44
|
+
data.seek(0, os.SEEK_END)
|
|
45
|
+
ti.size = data.tell()
|
|
46
|
+
data.seek(0)
|
|
47
|
+
self.image_tar.addfile(ti, data)
|
|
48
|
+
self.tar_manifest[0]['Layers'].append(name)
|
|
49
|
+
|
|
50
|
+
def finalize(self):
|
|
51
|
+
LOG.info('Writing manifest file to tarball')
|
|
52
|
+
encoded_manifest = json.dumps(self.tar_manifest).encode('utf-8')
|
|
53
|
+
ti = tarfile.TarInfo('manifest.json')
|
|
54
|
+
ti.size = len(encoded_manifest)
|
|
55
|
+
self.image_tar.addfile(ti, io.BytesIO(encoded_manifest))
|
occystrap/util.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import logging
|
|
3
|
+
from oslo_concurrency import processutils
|
|
3
4
|
from pbr.version import VersionInfo
|
|
4
5
|
import requests
|
|
5
6
|
|
|
@@ -23,7 +24,7 @@ STATUS_CODES_TO_ERRORS = {
|
|
|
23
24
|
def get_user_agent():
|
|
24
25
|
try:
|
|
25
26
|
version = VersionInfo('occystrap').version_string()
|
|
26
|
-
except:
|
|
27
|
+
except Exception:
|
|
27
28
|
version = '0.0.0'
|
|
28
29
|
return 'Mozilla/5.0 (Ubuntu; Linux x86_64) Occy Strap/%s' % version
|
|
29
30
|
|
|
@@ -74,3 +75,10 @@ def request_url(method, url, headers=None, data=None, stream=False):
|
|
|
74
75
|
raise APIException(
|
|
75
76
|
'API request failed', method, url, r.status_code, r.text, r.headers)
|
|
76
77
|
return r
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def execute(command, check_exit_code=[0], env_variables=None,
|
|
81
|
+
cwd=None):
|
|
82
|
+
return processutils.execute(
|
|
83
|
+
command, check_exit_code=check_exit_code,
|
|
84
|
+
env_variables=env_variables, shell=True, cwd=cwd)
|