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.
@@ -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)