forked from ashcrow/filetranspiler
-
Notifications
You must be signed in to change notification settings - Fork 0
/
filetranspile
executable file
·346 lines (298 loc) · 11.1 KB
/
filetranspile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
#!/usr/bin/env python3
"""
Takes a fake root and appends them into a provided ignition configuration.
"""
import abc
import argparse
import json
import os
import pathlib
import stat
import yaml
from urllib.parse import quote
__version__ = '1.1.1'
class FileTranspilerError(Exception):
"""
Base exception for FileTranspiler errors.
"""
pass
class IgnitionSpec(abc.ABC):
"""
Base class for IgnitionSpec classes.
"""
def __init__(self, ignition_cfg, cli_args):
"""
Initialize a spec merger.
:param ignition_cfg: loaded ignition config json
:type ignition_cfg: dict
:param cli_args: Command line arguments
:type cli_args: argparse.Namespace
"""
self.ignition_cfg = ignition_cfg
self.fake_root = cli_args.fake_root
self.dereference_symlinks = cli_args.dereference_symlinks
@abc.abstractmethod
def file_to_ignition(self, file_path, file_contents, mode):
"""
Turns a file into an ignition snippet.
:param file_path: Path to where the file should be placed.
:type file_path: str
:param file_contents: The raw contents of the file
:type file_contents: str
:param mode: Octal mode to use (will translate to decimal)
:type mode: int
:returns: Ignition config snippet
:rtype: dict
"""
raise NotImplementedError('Must be implemented in a subclass')
@abc.abstractmethod
def link_to_ignition(self, file_path, target_path):
"""
Turns a symbolic link into an ignition snippet.
:param file_path: Path to where the file should be placed.
:type file_path: str
:param target_path: The target path of the symbolic link
:type target_path: str
:returns: Ignition config snippet
:rtype: dict
"""
raise NotImplementedError('Must be implemented in a subclass')
def merge_with_ignition(self, ignition_cfg, files, links):
"""
Merge file snippets into the ignition config.
:param ignition_cfg: Ignition structure to append to
:type ignition_cfg: dict
:param files: List of Ignition file snippets
:type files: list
:returns: Merged ignition dict
:rtype: dict
"""
# Check that the storage exists
storage_check = ignition_cfg.get('storage')
if storage_check is None:
ignition_cfg['storage'] = {}
if files:
# Check that files entry exists
files_check = ignition_cfg['storage'].get('files')
if files_check is None:
ignition_cfg['storage']['files'] = []
for a_file in files:
ignition_cfg['storage']['files'].append(a_file)
if links:
# Check that links entry exists
links_check = ignition_cfg['storage'].get('links')
if links_check is None:
ignition_cfg['storage']['links'] = []
for a_link in links:
ignition_cfg['storage']['links'].append(a_link)
return ignition_cfg
def merge(self):
"""
Merges the fakeroot into the ignition config.
:returns: The merged ignition config
:rtype: dict
"""
# Walk through the files and append them for merging
all_files = []
all_links = []
for root, _, files in os.walk(self.fake_root):
for file in files:
path = os.path.sep.join([root, file])
host_path = path.replace(self.fake_root, '')
if not host_path.startswith(os.path.sep):
host_path = os.path.sep + host_path
if os.path.islink(path):
# If we are dereferencing symlinks then treat it as
# a file
if self.dereference_symlinks:
source_path = str(pathlib.Path(path).resolve())
# Ensure the path is within the fakeroot
if not source_path.startswith(
os.path.realpath(self.fake_root)):
raise FileTranspilerError(
'link: {} is not in the fake root: {}'.format(
source_path, self.fake_root))
mode = oct(stat.S_IMODE(os.stat(source_path).st_mode))
with open(source_path, 'r') as file_obj:
snippet = self.file_to_ignition(
host_path, file_obj.read(), mode)
all_files.append(snippet)
else:
target_path = os.readlink(path)
snippet = self.link_to_ignition(
host_path, target_path)
all_links.append(snippet)
else:
mode = oct(stat.S_IMODE(os.stat(path).st_mode))
with open(path, 'r') as file_obj:
snippet = self.file_to_ignition(
host_path, file_obj.read(), mode)
all_files.append(snippet)
# Merge the and output the results
merged_ignition = self.merge_with_ignition(
self.ignition_cfg, all_files, all_links)
return merged_ignition
class SpecV2(IgnitionSpec):
"""
Spec v2 implementation for merging files.
"""
def file_to_ignition(self, file_path, file_contents, mode):
"""
Turns a file into an ignition snippet.
:param file_path: Path to where the file should be placed.
:type file_path: str
:param file_contents: The raw contents of the file
:type file_contents: str
:param mode: Octal mode to use (will translate to decimal)
:type mode: int
:returns: Ignition config snippet
:rtype: dict
"""
return {
'path': file_path,
"filesystem": 'root',
'mode': int(mode, 8),
'contents': {
'source': 'data:,{}'.format(quote(file_contents))
}
}
def link_to_ignition(self, file_path, target_path):
"""
Turns a symbolic link into an ignition snippet.
:param file_path: Path to where the file should be placed.
:type file_path: str
:param target_path: The target path of the symbolic link
:type target_path: str
:returns: Ignition config snippet
:rtype: dict
"""
return {
'path': file_path,
"filesystem": 'root',
'target' : target_path,
'hard': False
}
class SpecV3(IgnitionSpec):
"""
Spec v3 implementation for merging files.
"""
def file_to_ignition(self, file_path, file_contents, mode):
"""
Turns a file into an ignition snippet.
:param file_path: Path to where the file should be placed.
:type file_path: str
:param file_contents: The raw contents of the file
:type file_contents: str
:param mode: Octal mode to use (will translate to decimal)
:type mode: int
:returns: Ignition config snippet
:rtype: dict
"""
return {
'path': file_path,
'mode': int(mode, 8),
'overwrite': True,
'contents': {
'source': 'data:,{}'.format(quote(file_contents))
}
}
def link_to_ignition(self, file_path, target_path):
"""
Turns a symbolic link into an ignition snippet.
:param file_path: Path to where the file should be placed.
:type file_path: str
:param target_path: The target path of the symbolic link
:type target_path: str
:returns: Ignition config snippet
:rtype: dict
"""
return {
'path': file_path,
'overwrite': True,
'target' : target_path,
'hard': False
}
def loader(ignition_file):
"""
Loads the ignition json into a structure, senses the ignition
spec version, and returns the structure and it's spec class.
:param ignition_file: Path to the ignition file to parse
:type ignition_file: str
:returns: The ignition structure and spec class
:rtype: tuple
:raises: FileTranspilerError
"""
try:
with open(ignition_file, 'r') as f:
ignition_cfg = json.load(f)
ignition_version = ignition_cfg['ignition']['version']
version_tpl = ignition_version.split('.')
if version_tpl[0] == '2':
return ignition_cfg, SpecV2
elif version_tpl[0] == '3':
return ignition_cfg, SpecV3
raise FileTranspilerError(
'Unkown ignition spec: {}'.format(ignition_version))
except (KeyError, IndexError) as err:
raise FileTranspilerError(
'Unable to find version in spec: {}'.format(err))
except json.JSONDecodeError as err:
raise FileTranspilerError(
'Unable to read JSON: {}'.format(err))
def main():
"""
Main entry point
"""
parser = argparse.ArgumentParser()
parser.add_argument(
'-i', '--ignition', help='Path to ignition file to use as the base')
parser.add_argument(
'-f', '--fake-root', help='Path to the fake root',
required=True)
parser.add_argument(
'-o', '--output',
help='Where to output the file. If empty, will print to stdout')
parser.add_argument(
'-p', '--pretty', default=False, action='store_true',
help='Make the output pretty')
parser.add_argument(
'--dereference-symlinks', default=False, action='store_true',
help=('Write out file contents instead of making symlinks '
'NOTE: Target files must exist in the fakeroot'))
parser.add_argument(
'--format', default='json', choices=['json', 'yaml'],
help='What format of file to write out. `yaml` or `json` (default)')
parser.add_argument(
'--version', action='version',
version='%(prog)s {}'.format(__version__))
args = parser.parse_args()
# Open the base ignition file and load it
if args.ignition is not None:
# Get the ignition config
try:
ignition_cfg, spec_cls = loader(args.ignition)
ignition_spec = spec_cls(ignition_cfg, args)
except FileTranspilerError as err:
parser.error(err)
else:
# Default to empty spec 2.3.0
ignition_cfg = { "ignition": { "version": "2.3.0" } }
ignition_spec = SpecV2(ignition_cfg, args)
# Merge the and output the results
merged_ignition = ignition_spec.merge()
if args.format == 'json':
if args.pretty:
ignition_out = json.dumps(
merged_ignition, sort_keys=True,
indent=4, separators=(',', ': '))
else:
ignition_out = json.dumps(merged_ignition)
else:
ignition_out = yaml.safe_dump(merged_ignition)
if args.output:
with open(args.output, 'w') as out_f:
out_f.write(ignition_out)
else:
print(ignition_out)
if __name__ == '__main__':
main()