This repository was archived by the owner on Oct 24, 2025. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 20
Expand file tree
/
Copy pathpacker.py
More file actions
272 lines (228 loc) · 9.03 KB
/
packer.py
File metadata and controls
272 lines (228 loc) · 9.03 KB
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
import copy
import json
import os
import subprocess
import zipfile
from collections import namedtuple
DEFAULT_PACKER_PATH = 'packer'
class Packer(object):
"""A packer client
"""
def __init__(self,
packerfile,
exc=None,
only=None,
vars=None,
var_file=None,
exec_path=DEFAULT_PACKER_PATH,
out_iter=None,
err_iter=None):
"""
:param string packerfile: Path to Packer template file
:param list exc: List of builders to exclude
:param list only: List of builders to include
:param dict vars: key=value pairs of template variables
:param string var_file: Path to variables file
:param string exec_path: Path to Packer executable
"""
self.packerfile = self._validate_argtype(packerfile, str)
self.var_file = var_file
if not os.path.isfile(self.packerfile):
raise OSError(
'packerfile not found at path: {0}'.format(self.packerfile))
self.exc = self._validate_argtype(exc or [], list)
self.only = self._validate_argtype(only or [], list)
self.vars = self._validate_argtype(vars or {}, dict)
kwargs = dict()
if out_iter is not None:
kwargs["_out"] = out_iter
kwargs["_out_bufsize"] = 1
if err_iter is not None:
kwargs["_err"] = err_iter
kwargs["_out_bufsize"] = 1
command = []
command.append(exec_path)
command.extend(self.dict_to_command(kwargs))
self.packer = command
def dict_to_command(self, kwargs):
"""Convert dict to '--key=value' command parameters"""
param = []
for parameter, value in kwargs.items():
param.append('--{}={}'.format(parameter, value))
return param
def build(self,
parallel=True,
debug=False,
force=False,
machine_readable=False):
"""Executes a `packer build`
:param bool parallel: Run builders in parallel
:param bool debug: Run in debug mode
:param bool force: Force artifact output even if exists
:param bool machine_readable: Make output machine-readable
"""
cmd = copy.copy(self.packer)
cmd.append('build')
self.packer_cmd = cmd
self._add_opt('-parallel=true' if parallel else None)
self._add_opt('-debug' if debug else None)
self._add_opt('-force' if force else None)
self._add_opt('-machine-readable' if machine_readable else None)
self._append_base_arguments()
self._add_opt(self.packerfile)
return self._run_command(self.packer_cmd)
def fix(self, to_file=None):
"""Implements the `packer fix` function
:param string to_file: File to output fixed template to
"""
cmd = copy.copy(self.packer)
cmd.append('fix')
self.packer_cmd = cmd
self._add_opt(self.packerfile)
result = self._run_command(self.packer_cmd)
if to_file:
with open(to_file, 'w') as f:
f.write(result.stdout)
result = json.loads(result.stdout)
return result
def inspect(self, mrf=True):
"""Inspects a Packer Templates file (`packer inspect -machine-readable`)
To return the output in a readable form, the `-machine-readable` flag
is appended automatically, afterwhich the output is parsed and returned
as a dict of the following format:
"variables": [
{
"name": "aws_access_key",
"value": "{{env `AWS_ACCESS_KEY_ID`}}"
},
{
"name": "aws_secret_key",
"value": "{{env `AWS_ACCESS_KEY`}}"
}
],
"provisioners": [
{
"type": "shell"
}
],
"builders": [
{
"type": "amazon-ebs",
"name": "amazon"
}
]
:param bool mrf: output in machine-readable form.
"""
cmd = copy.copy(self.packer)
cmd.append('inspect')
self.packer_cmd = cmd
self._add_opt('-machine-readable' if mrf else None)
self._add_opt(self.packerfile)
output = self._run_command(self.packer_cmd)
result = output.stdout
if mrf:
result = self._parse_inspection_output(output.stdout)
return result
def push(self, create=True, token=False):
"""Implmenets the `packer push` function
UNTESTED! Must be used alongside an Atlas account
"""
cmd = copy.copy(self.packer)
cmd.append('push')
self.packer_cmd = cmd
# self._add_opt('-create=true' if create else None)
self._add_opt('-token={0}'.format(token) if token else None)
self._add_opt(self.packerfile)
result = self._run_command(self.packer_cmd)
return result
def validate(self, syntax_only=False):
"""Validates a Packer Template file (`packer validate`)
If the validation failed, an `sh` exception will be raised.
:param bool syntax_only: Whether to validate the syntax only
without validating the configuration itself.
"""
cmd = copy.copy(self.packer)
cmd.append('validate')
self.packer_cmd = cmd
self._add_opt('-syntax-only' if syntax_only else None)
self._append_base_arguments()
self._add_opt(self.packerfile)
result = self._run_command(self.packer_cmd)
if result.returncode:
raise PackerException(result.stdout)
return result
def version(self):
"""Returns Packer's version number (`packer version`)
As of v0.7.5, the format shows when running `packer version`
is: Packer vX.Y.Z. This method will only returns the number, without
the `packer v` prefix so that you don't have to parse the version
yourself.
"""
cmd = copy.copy(self.packer)
cmd.append('version')
output = self._run_command(cmd)
version = output.stdout.split('\n')[0].split('v')[1]
return version
def _add_opt(self, option):
if option:
self.packer_cmd.append(option)
def _validate_argtype(self, arg, argtype):
if not isinstance(arg, argtype):
raise PackerException(
'{0} argument must be of type {1}'.format(arg, argtype))
return arg
def _append_base_arguments(self):
"""Appends base arguments to packer commands.
-except, -only, -var and -var-file are appeneded to almost
all subcommands in packer. As such this can be called to add
these flags to the subcommand.
"""
if self.exc and self.only:
raise PackerException('Cannot provide both "except" and "only"')
elif self.exc:
self._add_opt('-except={0}'.format(self._join_comma(self.exc)))
elif self.only:
self._add_opt('-only={0}'.format(self._join_comma(self.only)))
for var, value in self.vars.items():
self._add_opt("-var")
self._add_opt("{0}={1}".format(var, value))
if self.var_file:
self._add_opt('-var-file={0}'.format(self.var_file))
def _join_comma(self, lst):
"""Returns a comma delimited string from a list"""
return str(','.join(lst))
def _parse_inspection_output(self, output):
"""Parses the machine-readable output `packer inspect` provides.
See the inspect method for more info.
This has been tested vs. Packer v0.7.5
"""
parts = {'variables': [], 'builders': [], 'provisioners': []}
for line in output.splitlines():
line = line.split(',')
packer_type = line[2]
if packer_type.startswith('template'):
del line[0:2] # Remove date
component = line[0]
name = line[1]
if component == 'template-variable':
variable = {"name": name, "value": line[2]}
parts['variables'].append(variable)
elif component == 'template-builder':
builder = {"name": name, "type": line[2]}
parts['builders'].append(builder)
elif component == 'template-provisioner':
provisioner = {"type": name}
parts['provisioners'].append(provisioner)
return parts
def _run_command(self, command):
"""Wrapper to execute command"""
PackerOutput = namedtuple('PackerOutput',
['stdout', 'stderr', 'returncode'])
executed = subprocess.run(
command, stderr=subprocess.PIPE, stdout=subprocess.PIPE)
packer_output = PackerOutput(executed.stdout.decode(),
executed.stderr.decode(),
executed.returncode)
return packer_output
class PackerException(Exception):
pass