imagemaster3000 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +12 -0
- data/.rspec +2 -0
- data/.rubocop.yml +38 -0
- data/.travis.yml +22 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +13 -0
- data/README.md +60 -0
- data/Rakefile +17 -0
- data/bin/imagemaster3000 +4 -0
- data/config/definitions/centos-7.json +29 -0
- data/config/definitions/debian-8.json +33 -0
- data/config/definitions/files/.gitkeep +0 -0
- data/config/definitions/files/centos-cloud.cfg +95 -0
- data/config/definitions/files/debian-cloud.cfg +101 -0
- data/config/definitions/files/serial-getty@ttyS0.service +35 -0
- data/config/definitions/files/ttyS0.conf +22 -0
- data/config/definitions/files/ubuntu-cloud.cfg +111 -0
- data/config/definitions/ubuntu-14.04.json +30 -0
- data/config/definitions/ubuntu-16.04.json +30 -0
- data/config/imagemaster3000.yml +15 -0
- data/imagemaster3000.gemspec +42 -0
- data/lib/imagemaster3000/actions/copy.rb +45 -0
- data/lib/imagemaster3000/actions/remove.rb +24 -0
- data/lib/imagemaster3000/actions.rb +6 -0
- data/lib/imagemaster3000/cli.rb +115 -0
- data/lib/imagemaster3000/definitions/parser.rb +45 -0
- data/lib/imagemaster3000/definitions/schemas/imagemaster3000-definition-schema.json +238 -0
- data/lib/imagemaster3000/definitions.rb +5 -0
- data/lib/imagemaster3000/entities/downloadable.rb +47 -0
- data/lib/imagemaster3000/entities/image.rb +92 -0
- data/lib/imagemaster3000/entities.rb +6 -0
- data/lib/imagemaster3000/errors/action_error.rb +5 -0
- data/lib/imagemaster3000/errors/argument_error.rb +5 -0
- data/lib/imagemaster3000/errors/command_execution_error.rb +5 -0
- data/lib/imagemaster3000/errors/download_error.rb +5 -0
- data/lib/imagemaster3000/errors/parsing_error.rb +5 -0
- data/lib/imagemaster3000/errors/standard_error.rb +5 -0
- data/lib/imagemaster3000/errors/verification_error.rb +5 -0
- data/lib/imagemaster3000/errors.rb +11 -0
- data/lib/imagemaster3000/image_list/generator.rb +25 -0
- data/lib/imagemaster3000/image_list/signer.rb +18 -0
- data/lib/imagemaster3000/image_list/templates/image_list.erb +41 -0
- data/lib/imagemaster3000/image_list.rb +6 -0
- data/lib/imagemaster3000/main_process.rb +22 -0
- data/lib/imagemaster3000/settings.rb +19 -0
- data/lib/imagemaster3000/utils/command_executioner.rb +22 -0
- data/lib/imagemaster3000/utils/crypto.rb +28 -0
- data/lib/imagemaster3000/utils/tmp.rb +24 -0
- data/lib/imagemaster3000/utils.rb +7 -0
- data/lib/imagemaster3000/verification/hash.rb +45 -0
- data/lib/imagemaster3000/verification/signatures/clearsign.rb +16 -0
- data/lib/imagemaster3000/verification/signatures/detached.rb +18 -0
- data/lib/imagemaster3000/verification/signatures.rb +8 -0
- data/lib/imagemaster3000/verification/verifiable.rb +10 -0
- data/lib/imagemaster3000/verification.rb +7 -0
- data/lib/imagemaster3000/version.rb +3 -0
- data/lib/imagemaster3000.rb +18 -0
- metadata +347 -0
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'json-schema'
|
3
|
+
|
4
|
+
module Imagemaster3000
|
5
|
+
module Definitions
|
6
|
+
class Parser
|
7
|
+
SCHEMA = File.join File.dirname(__FILE__), 'schemas', 'imagemaster3000-definition-schema.json'
|
8
|
+
|
9
|
+
class << self
|
10
|
+
def parse_image_definitions
|
11
|
+
definition_files = Dir.glob(File.join(Imagemaster3000::Settings[:'definitions-dir'], '*.json')).sort
|
12
|
+
logger.debug "Found definition files: #{definition_files.inspect}"
|
13
|
+
definition_files.map { |file| parse file }.compact
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def parse(file)
|
19
|
+
logger.debug "Parsing file #{file.inspect}"
|
20
|
+
json = File.read file
|
21
|
+
validate json
|
22
|
+
|
23
|
+
hash = JSON.parse json, symbolize_names: true
|
24
|
+
Imagemaster3000::Entities::Image.new hash
|
25
|
+
rescue Imagemaster3000::Errors::ArgumentError, Imagemaster3000::Errors::ParsingError => ex
|
26
|
+
logger.error "Parsing error occured while reading definition #{file.inspect}: #{ex.message}. Skipping."
|
27
|
+
nil
|
28
|
+
end
|
29
|
+
|
30
|
+
def validate(json)
|
31
|
+
logger.debug "Validating json\n#{json}"
|
32
|
+
JSON::Validator.schema_reader = JSON::Schema::Reader.new(accept_uri: false, accept_file: true)
|
33
|
+
JSON::Validator.validate!(SCHEMA, json, json: true)
|
34
|
+
rescue JSON::Schema::JsonParseError => ex
|
35
|
+
raise Imagemaster3000::Errors::ParsingError, ex
|
36
|
+
rescue JSON::Schema::ValidationError => ex
|
37
|
+
raise Imagemaster3000::Errors::ParsingError, "JSON is not valid according to JSON schema: #{ex.message}"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
private_class_method :parse
|
42
|
+
private_class_method :validate
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,238 @@
|
|
1
|
+
{
|
2
|
+
"$schema":"http://json-schema.org/draft-04/schema#",
|
3
|
+
"additionalProperties":false,
|
4
|
+
"definitions":{
|
5
|
+
"source":{
|
6
|
+
"description":"Source file",
|
7
|
+
"id":"#source",
|
8
|
+
"title":"Source",
|
9
|
+
"type":"string"
|
10
|
+
},
|
11
|
+
"target":{
|
12
|
+
"description":"Target directory",
|
13
|
+
"id":"#target",
|
14
|
+
"title":"Target",
|
15
|
+
"type":"string"
|
16
|
+
},
|
17
|
+
"name":{
|
18
|
+
"description":"New name of the copied file",
|
19
|
+
"id":"#name",
|
20
|
+
"title":"Name",
|
21
|
+
"type":"string"
|
22
|
+
},
|
23
|
+
"copy":{
|
24
|
+
"additionalItems":false,
|
25
|
+
"id":"#copy",
|
26
|
+
"items":{
|
27
|
+
"additionalProperties":false,
|
28
|
+
"properties":{
|
29
|
+
"source":{
|
30
|
+
"$ref":"#/definitions/source"
|
31
|
+
},
|
32
|
+
"target":{
|
33
|
+
"$ref":"#/definitions/target"
|
34
|
+
},
|
35
|
+
"name":{
|
36
|
+
"$ref":"#/definitions/name"
|
37
|
+
}
|
38
|
+
},
|
39
|
+
"required":[
|
40
|
+
"source",
|
41
|
+
"target"
|
42
|
+
],
|
43
|
+
"type":"object"
|
44
|
+
},
|
45
|
+
"type":"array"
|
46
|
+
},
|
47
|
+
"remove":{
|
48
|
+
"additionalItems":false,
|
49
|
+
"id":"#remove",
|
50
|
+
"items":{
|
51
|
+
"description":"File to remove",
|
52
|
+
"title":"File",
|
53
|
+
"type":"string"
|
54
|
+
},
|
55
|
+
"type":"array"
|
56
|
+
},
|
57
|
+
"actions":{
|
58
|
+
"additionalProperties":false,
|
59
|
+
"id":"#actions",
|
60
|
+
"properties":{
|
61
|
+
"copy":{
|
62
|
+
"$ref":"#/definitions/copy"
|
63
|
+
},
|
64
|
+
"remove":{
|
65
|
+
"$ref":"#/definitions/remove"
|
66
|
+
}
|
67
|
+
},
|
68
|
+
"type":"object"
|
69
|
+
},
|
70
|
+
"cpu":{
|
71
|
+
"description":"Minimum required CPU",
|
72
|
+
"id":"#cpu",
|
73
|
+
"title":"CPU",
|
74
|
+
"type":"integer"
|
75
|
+
},
|
76
|
+
"distribution":{
|
77
|
+
"description":"Distribution name",
|
78
|
+
"id":"#distribution",
|
79
|
+
"title":"Distribution",
|
80
|
+
"type":"string"
|
81
|
+
},
|
82
|
+
"name":{
|
83
|
+
"description":"Image name",
|
84
|
+
"id":"#name",
|
85
|
+
"title":"Name",
|
86
|
+
"type":"string"
|
87
|
+
},
|
88
|
+
"ram":{
|
89
|
+
"description":"Minimum required RAM",
|
90
|
+
"id":"#ram",
|
91
|
+
"title":"RAM",
|
92
|
+
"type":"integer"
|
93
|
+
},
|
94
|
+
"url":{
|
95
|
+
"description":"Image URL",
|
96
|
+
"id":"#url",
|
97
|
+
"title":"URL",
|
98
|
+
"type":"string"
|
99
|
+
},
|
100
|
+
"function":{
|
101
|
+
"description":"Hash function",
|
102
|
+
"id":"#function",
|
103
|
+
"title":"Function",
|
104
|
+
"type":"string"
|
105
|
+
},
|
106
|
+
"hash":{
|
107
|
+
"additionalProperties":false,
|
108
|
+
"id":"#hash",
|
109
|
+
"properties":{
|
110
|
+
"function":{
|
111
|
+
"$ref":"#/definitions/function"
|
112
|
+
}
|
113
|
+
},
|
114
|
+
"required":[
|
115
|
+
"function"
|
116
|
+
],
|
117
|
+
"type":"object"
|
118
|
+
},
|
119
|
+
"file":{
|
120
|
+
"description":"URL to file with both signature and data",
|
121
|
+
"id":"#file",
|
122
|
+
"title":"File",
|
123
|
+
"type":"string"
|
124
|
+
},
|
125
|
+
"clearsign":{
|
126
|
+
"additionalProperties":false,
|
127
|
+
"id":"#clearsign",
|
128
|
+
"properties":{
|
129
|
+
"file":{
|
130
|
+
"$ref":"#/definitions/file"
|
131
|
+
}
|
132
|
+
},
|
133
|
+
"required":[
|
134
|
+
"file"
|
135
|
+
],
|
136
|
+
"type":"object"
|
137
|
+
},
|
138
|
+
"data":{
|
139
|
+
"description":"URL to file with signed data",
|
140
|
+
"id":"#data",
|
141
|
+
"title":"Data",
|
142
|
+
"type":"string"
|
143
|
+
},
|
144
|
+
"signature_file":{
|
145
|
+
"description":"URL to file with signature",
|
146
|
+
"id":"#signature_file",
|
147
|
+
"title":"Signature",
|
148
|
+
"type":"string"
|
149
|
+
},
|
150
|
+
"detached":{
|
151
|
+
"additionalProperties":false,
|
152
|
+
"id":"#detached",
|
153
|
+
"properties":{
|
154
|
+
"data":{
|
155
|
+
"$ref":"#/definitions/data"
|
156
|
+
},
|
157
|
+
"signature":{
|
158
|
+
"$ref":"#/definitions/signature_file"
|
159
|
+
}
|
160
|
+
},
|
161
|
+
"required":[
|
162
|
+
"data",
|
163
|
+
"signature"
|
164
|
+
],
|
165
|
+
"type":"object"
|
166
|
+
},
|
167
|
+
"signature":{
|
168
|
+
"additionalProperties":false,
|
169
|
+
"id":"#signature",
|
170
|
+
"properties":{
|
171
|
+
"clearsign":{
|
172
|
+
"$ref":"#/definitions/clearsign"
|
173
|
+
},
|
174
|
+
"detached":{
|
175
|
+
"$ref":"#/definitions/detached"
|
176
|
+
}
|
177
|
+
},
|
178
|
+
"type":"object"
|
179
|
+
},
|
180
|
+
"verification":{
|
181
|
+
"additionalProperties":false,
|
182
|
+
"id":"#verification",
|
183
|
+
"properties":{
|
184
|
+
"hash":{
|
185
|
+
"$ref":"#/definitions/hash"
|
186
|
+
},
|
187
|
+
"signature":{
|
188
|
+
"$ref":"#/definitions/signature"
|
189
|
+
}
|
190
|
+
},
|
191
|
+
"required":[
|
192
|
+
"hash",
|
193
|
+
"signature"
|
194
|
+
],
|
195
|
+
"type":"object"
|
196
|
+
},
|
197
|
+
"version":{
|
198
|
+
"description":"Image version",
|
199
|
+
"id":"#version",
|
200
|
+
"title":"Version",
|
201
|
+
"type":"string"
|
202
|
+
}
|
203
|
+
},
|
204
|
+
"id":"imagemaster3000-definition-schema.json",
|
205
|
+
"properties":{
|
206
|
+
"actions":{
|
207
|
+
"$ref":"#/definitions/actions"
|
208
|
+
},
|
209
|
+
"cpu":{
|
210
|
+
"$ref":"#/definitions/cpu"
|
211
|
+
},
|
212
|
+
"distribution":{
|
213
|
+
"$ref":"#/definitions/distribution"
|
214
|
+
},
|
215
|
+
"name":{
|
216
|
+
"$ref":"#/definitions/name"
|
217
|
+
},
|
218
|
+
"ram":{
|
219
|
+
"$ref":"#/definitions/ram"
|
220
|
+
},
|
221
|
+
"url":{
|
222
|
+
"$ref":"#/definitions/url"
|
223
|
+
},
|
224
|
+
"verification":{
|
225
|
+
"$ref":"#/definitions/verification"
|
226
|
+
},
|
227
|
+
"version":{
|
228
|
+
"$ref":"#/definitions/version"
|
229
|
+
}
|
230
|
+
},
|
231
|
+
"required":[
|
232
|
+
"name",
|
233
|
+
"url",
|
234
|
+
"version",
|
235
|
+
"distribution"
|
236
|
+
],
|
237
|
+
"type":"object"
|
238
|
+
}
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'net/https'
|
2
|
+
|
3
|
+
module Imagemaster3000
|
4
|
+
module Entities
|
5
|
+
module Downloadable
|
6
|
+
def download
|
7
|
+
logger.debug "Downloading image from #{url.inspect}"
|
8
|
+
|
9
|
+
uri = URI.parse url
|
10
|
+
filename = generate_filename(uri)
|
11
|
+
retrieve_image(uri, filename)
|
12
|
+
|
13
|
+
logger.debug "Image from #{url.inspect} was saved as #{filename.inspect}"
|
14
|
+
@file = filename
|
15
|
+
@size = File.size filename
|
16
|
+
rescue ::URI::InvalidURIError, ::IOError => ex
|
17
|
+
raise Imagemaster3000::Errors::DownloadError, ex
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def retrieve_image(uri, filename)
|
23
|
+
use_ssl = uri.scheme == 'https'
|
24
|
+
Net::HTTP.start(uri.host, uri.port, use_ssl: use_ssl) do |http|
|
25
|
+
request = Net::HTTP::Get.new(uri)
|
26
|
+
|
27
|
+
http.request(request) do |response|
|
28
|
+
if response.is_a? Net::HTTPRedirection
|
29
|
+
retrieve_image URI.join(uri, response.header['location']), filename
|
30
|
+
break
|
31
|
+
end
|
32
|
+
|
33
|
+
response.value
|
34
|
+
open(filename, 'w') { |file| response.read_body { |chunk| file.write(chunk) } }
|
35
|
+
end
|
36
|
+
end
|
37
|
+
rescue Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, Errno::ECONNREFUSED, Net::HTTPBadResponse,
|
38
|
+
Net::HTTPHeaderSyntaxError, EOFError, Net::HTTPServerException, Net::HTTPRetriableError => ex
|
39
|
+
raise Imagemaster3000::Errors::DownloadError, ex
|
40
|
+
end
|
41
|
+
|
42
|
+
def generate_filename(uri)
|
43
|
+
File.join(Imagemaster3000::Settings[:'image-dir'], File.basename(uri.path))
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
module Imagemaster3000
|
2
|
+
module Entities
|
3
|
+
class Image
|
4
|
+
include Downloadable
|
5
|
+
|
6
|
+
attr_accessor :name, :url, :distribution, :version, :ram, :cpu, :actions, :verification, :file, :size
|
7
|
+
|
8
|
+
def initialize(name: nil, url: nil, distribution: nil, version: nil, ram: nil, cpu: nil, actions: nil, verification: nil)
|
9
|
+
raise Imagemaster3000::Errors::ArgumentError, 'name, url, distribution or version cannot be nil' \
|
10
|
+
if name.blank? || url.blank? || distribution.blank? || version.blank?
|
11
|
+
|
12
|
+
@name = name
|
13
|
+
@url = url
|
14
|
+
@distribution = distribution
|
15
|
+
@version = version
|
16
|
+
@ram = ram
|
17
|
+
@cpu = cpu
|
18
|
+
@actions = prepare_actions actions
|
19
|
+
@verification = prepare_verification verification
|
20
|
+
|
21
|
+
logger.debug "Created image #{inspect}"
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def prepare_actions(actions_hash)
|
27
|
+
return nil if actions_hash.blank?
|
28
|
+
logger.debug 'Preparing actions'
|
29
|
+
|
30
|
+
namespace = Imagemaster3000::Actions
|
31
|
+
prepared_actions = []
|
32
|
+
|
33
|
+
actions_hash.each_pair do |action_name, array|
|
34
|
+
action_const_name = action_name.capitalize.to_sym
|
35
|
+
raise Imagemaster3000::Errors::ActionError, "No such action #{action_name.inspect}" \
|
36
|
+
unless namespace.constants.include? action_const_name
|
37
|
+
|
38
|
+
logger.debug "Recognized action #{action_name.inspect}"
|
39
|
+
action_const = namespace.const_get action_const_name
|
40
|
+
prepared_actions += array.map { |object| action_const.new(object) }
|
41
|
+
end
|
42
|
+
|
43
|
+
prepared_actions
|
44
|
+
end
|
45
|
+
|
46
|
+
def prepare_verification(verification_hash)
|
47
|
+
return nil if verification_hash.blank?
|
48
|
+
logger.debug 'Preparing verification'
|
49
|
+
|
50
|
+
extend Imagemaster3000::Verification::Verifiable
|
51
|
+
prepared_verification = {}
|
52
|
+
|
53
|
+
prepared_verification[:signature] = prepare_signature_verification verification_hash[:signature]
|
54
|
+
prepared_verification[:hash] = prepare_hash_verification verification_hash[:hash]
|
55
|
+
|
56
|
+
prepared_verification
|
57
|
+
end
|
58
|
+
|
59
|
+
def prepare_signature_verification(signature_hash)
|
60
|
+
logger.debug 'Preparing signature verification'
|
61
|
+
namespace = Imagemaster3000::Verification::Signatures
|
62
|
+
signature_name = signature_hash.keys.first
|
63
|
+
signature_const_name = signature_name.capitalize.to_sym
|
64
|
+
|
65
|
+
raise Imagemaster3000::Errors::VerificationError, "No such signature type #{signature_name}" \
|
66
|
+
unless namespace.constants.include? signature_const_name
|
67
|
+
|
68
|
+
logger.debug "Recognized signature #{signature_name}"
|
69
|
+
extend namespace.const_get(signature_const_name)
|
70
|
+
|
71
|
+
signature_hash[signature_name]
|
72
|
+
end
|
73
|
+
|
74
|
+
def prepare_hash_verification(hash_hash)
|
75
|
+
logger.debug 'Preparing hash verification'
|
76
|
+
extend Imagemaster3000::Verification::Hash
|
77
|
+
|
78
|
+
hash_function_name = hash_hash[:function]
|
79
|
+
hash_function_const_name = hash_function_name.upcase.to_sym
|
80
|
+
|
81
|
+
begin
|
82
|
+
Digest(hash_function_const_name)
|
83
|
+
rescue LoadError
|
84
|
+
raise Imagemaster3000::Errors::VerificationError, "No such hash function #{hash_function_name}"
|
85
|
+
end
|
86
|
+
|
87
|
+
logger.debug "Recognized hash function #{hash_function_name}"
|
88
|
+
{ function: Digest.const_get(hash_function_const_name) }
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|