cangallo 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: a5714dca973ec1755fc348bbef4494c7e1da8c41
4
+ data.tar.gz: 4abacf5c9616cb358befa15117d1682b26236a1b
5
+ SHA512:
6
+ metadata.gz: f817d57b34e4758aa33886d34ca0e9d744385a47c497ca33764ffcf988658f2172085ca2f25c3f778a09e8ee74b74844571b86d65e0b4bc4f7e5c1e17fa3eaf4
7
+ data.tar.gz: d2877b68c3ed720db1163dcb0c352a0d97fefcff866501d5f54375f223b1b57f02f08b5acafea0f59a907f16f447ac67d6bb623f6aaf427c2d7db62c05031b6a
data/bin/canga ADDED
@@ -0,0 +1,307 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # vim:ts=2:sw=2
4
+
5
+ # Copyright 2016, Javier Fontán Muiños <jfontan@gmail.com>
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License"); you may
8
+ # not use this file except in compliance with the License. You may obtain
9
+ # a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+
19
+ $: << File.dirname(__FILE__) + "/../lib"
20
+
21
+ require "rubygems"
22
+ require "thor"
23
+ require "fileutils"
24
+ require "digest"
25
+ require "cangallo"
26
+ require "tempfile"
27
+
28
+ require "pp"
29
+
30
+ $cangallo = Cangallo.new
31
+
32
+ class Canga < Thor
33
+ map ['--version', '-V'] => :__print_version
34
+
35
+ desc "--version, -V", "show version"
36
+ def __print_version
37
+ puts Cangallo::VERSION
38
+ end
39
+
40
+ desc "create FILE [SIZE]", "create a new qcow2 image"
41
+ option :parent, :desc => "id of the parent image"
42
+ def create(file, size = nil)
43
+ puts [file, size]
44
+ Cangallo::Qcow2.create(file, options[:parent], size)
45
+ end
46
+
47
+ desc "add FILE [REPO]", "add a new file to the repository"
48
+ option :parent, :desc => "id of the parent image"
49
+ option :tag, :desc => "tag name of the new image"
50
+ option :copy, :desc => "do not process the image, make an exact copy",
51
+ :type => :boolean
52
+ def add(file, repo_name = nil)
53
+ repo = $cangallo.repo(repo_name)
54
+ sha256 = repo.add_image(file, "only_copy" => options[:copy])
55
+
56
+ repo.add_tag(options[:tag], sha256) if options[:tag]
57
+ end
58
+
59
+ desc "del IMAGE", "delete an image from the repository"
60
+ def del(name)
61
+ image = $cangallo.get(name)
62
+
63
+ if !image
64
+ STDERR.puts %Q{Image "#{name}" not found}
65
+ exit(-1)
66
+ end
67
+
68
+ repo = $cangallo.repo(image["repo"])
69
+ repo.del_image(image["sha256"])
70
+ end
71
+
72
+ desc "tag TAGNAME IMAGE", "add a tag name to an existing image"
73
+ def tag(tag, name)
74
+ image = $cangallo.get(name)
75
+
76
+ if !image
77
+ STDERR.puts %Q{Image "#{name}" not found}
78
+ exit(-1)
79
+ end
80
+
81
+ repo = $cangallo.repo(image['repo'])
82
+ repo.add_tag(tag, image['sha256'])
83
+ end
84
+
85
+ desc "deltag TAGNAME", "delete a tag"
86
+ def deltag(tag)
87
+ repo, name = $cangallo.parse_name(tag)
88
+ image = $cangallo.get(tag)
89
+
90
+ if !image
91
+ STDERR.puts %Q{Image "#{name}" not found}
92
+ exit(-1)
93
+ end
94
+
95
+ repo = $cangallo.repo(repo)
96
+ repo.del_tag(name)
97
+ end
98
+
99
+ desc "list [REPO]", "list images"
100
+ def list(repo_name = nil)
101
+ images = $cangallo.get_images(repo_name)
102
+
103
+ format = "%-50.50s %11.11s %8.8s"
104
+
105
+ print "\e[1;4m"
106
+ print format % %w{NAME SIZE DAYS_AGO}
107
+ puts "\e[0m"
108
+
109
+ images.each do |image|
110
+ parent = image["parent"] ? "^" : " "
111
+ available = image["available"] ? " " : "*"
112
+ name = "#{parent}#{available}#{image["name"]}"
113
+
114
+ size = image["size"].to_f / (1024 * 1024) # Mb
115
+ size = size.round(1)
116
+
117
+ if image["creation-time"]
118
+ days = ( Time.new - image["creation-time"] ) / (60*60*24)
119
+ days = days.round(1)
120
+ else
121
+ days = "N/A"
122
+ end
123
+
124
+ print "\e[1m"
125
+ print format % [name, "#{size} Mb", days]
126
+ puts "\e[0m"
127
+
128
+ puts " %-70.70s" % [image["description"]] if image["description"]
129
+ end
130
+ end
131
+
132
+ desc "show IMAGE", "show information about an image"
133
+ def show(name)
134
+ image = $cangallo.get(name)
135
+
136
+ if image
137
+ puts image.to_yaml
138
+ else
139
+ STDERR.puts "No image found with name '#{name}'"
140
+ exit(-1)
141
+ end
142
+ end
143
+
144
+ desc "overlay IMAGE FILE", "create a new image based on another one"
145
+ def overlay(name, file)
146
+ image = $cangallo.get(name)
147
+
148
+ if !image
149
+ STDERR.puts "Image not found"
150
+ exit(-1)
151
+ end
152
+
153
+ repo = $cangallo.repo(image['repo'])
154
+ path = File.expand_path(repo.image_path(image['sha256']))
155
+
156
+ Cangallo::Qcow2.create_from_base(path, file)
157
+ end
158
+
159
+ desc "build CANGAFILE", "create a new image using a Cangafile"
160
+ option :tag, :desc => "tag name of the new image"
161
+ def build(file)
162
+ cangafile = Cangallo::Cangafile.new(file)
163
+
164
+ commands, params = cangafile.render
165
+ puts commands
166
+
167
+ begin
168
+ image = $cangallo.get(cangafile.parent)
169
+ rescue => e
170
+ STDERR.puts e.message
171
+ exit(-1)
172
+ end
173
+
174
+ if !image
175
+ STDERR.puts "Image not found"
176
+ exit(-1)
177
+ end
178
+
179
+ sha256 = image['sha256']
180
+ repo = $cangallo.repo(image['repo'])
181
+
182
+ parent_path = File.expand_path(repo.image_path(sha256))
183
+
184
+ # temp image path
185
+ temp_image = Tempfile.new([File.basename(file), '.qcow2'], repo.path)
186
+ temp_image.close
187
+
188
+ Cangallo::Qcow2.create_from_base(parent_path, temp_image.path)
189
+
190
+ rc = Cangallo::LibGuestfs.virt_customize(temp_image.path, commands, params)
191
+ exit(-1) if !rc
192
+
193
+ rc = Cangallo::LibGuestfs.virt_sparsify(temp_image.path)
194
+ exit(-1) if !rc
195
+
196
+ data = {}
197
+ data["parent"] = sha256
198
+ data["description"] = cangafile.data["description"]
199
+ data["os"] = cangafile.data["os"]
200
+ cangafile.data.delete("description")
201
+ cangafile.data.delete("os")
202
+
203
+ data["cangafile"] = cangafile.data
204
+
205
+ new_image = repo.add_image(temp_image.path, data)
206
+
207
+ puts "Deleting temporary image"
208
+ temp_image.delete
209
+
210
+ tag = options[:tag] || cangafile.data['tag']
211
+
212
+ repo.add_tag(tag, new_image) if tag
213
+ end
214
+
215
+ desc "fetch [REPO]", "download the index of the repository"
216
+ def fetch(repo_name = nil)
217
+ repo = $cangallo.repo(repo_name)
218
+
219
+ repo.fetch
220
+ end
221
+
222
+ desc "sign [REPO]", "sign the index file with keybase"
223
+ def sign(repo_name = nil)
224
+ repo = $cangallo.repo(repo_name)
225
+ repo.sign
226
+ end
227
+
228
+ desc "verify [REPO]", "verify index signature with keybase"
229
+ def verify(repo_name = nil)
230
+ repo = $cangallo.repo(repo_name)
231
+ repo.verify
232
+ end
233
+
234
+ desc "pull NAME", "download an image from a remote repository"
235
+ def pull(name)
236
+ image = $cangallo.get(name)
237
+
238
+ if !image
239
+ STDERR.puts "Image not found"
240
+ exit(-1)
241
+ end
242
+
243
+ repo = $cangallo.repo(image["repo"])
244
+ images = repo.ancestors(image["sha256"])
245
+
246
+ images.reverse.each do |img|
247
+ name = $cangallo.short_name(img, image["repo"])
248
+ if File.exist?(repo.image_path(img))
249
+ STDERR.puts "Image #{name} already downloaded"
250
+ else
251
+ STDERR.puts "Downloading #{name}"
252
+ repo.pull(img)
253
+ end
254
+ end
255
+ end
256
+
257
+ desc "export IMAGE OUTPUT", "export an image to a file"
258
+ option :format, :desc => "format for output image", :default => :qcow2,
259
+ :aliases => :f
260
+ option :compress, :desc => "compress output qcow2 image", :default => false,
261
+ :aliases => :c
262
+ def export(image, output)
263
+ image = $cangallo.get(image)
264
+
265
+ if !image
266
+ STDERR.puts "Image not found"
267
+ exit(-1)
268
+ end
269
+
270
+ repo = $cangallo.repo(image["repo"])
271
+ path = File.expand_path(repo.image_path(image["sha256"]))
272
+
273
+ Cangallo::Qcow2.new(path).convert(output,options)
274
+ end
275
+
276
+ desc "import IMAGE [REPO]", "import an image from a remote repository"
277
+ option :tag, :desc => "tag name of the new image"
278
+ def import(image_name, repo = nil)
279
+ image = $cangallo.get(image_name)
280
+
281
+ if !image
282
+ STDERR.puts "Image not found"
283
+ exit(-1)
284
+ end
285
+
286
+ sha256 = image["sha256"]
287
+
288
+ remote_repo = $cangallo.repo(image["repo"])
289
+ remote_path = remote_repo.image_path(sha256)
290
+
291
+ repository = $cangallo.repo(repo)
292
+ image_path = repository.image_path(sha256)
293
+
294
+ FileUtils.ln(remote_path, image_path, force: true)
295
+
296
+ image.delete("repo")
297
+ repository.add(sha256, image)
298
+
299
+ repository.add_tag(options[:tag], sha256) if options[:tag]
300
+
301
+ repository.write_index
302
+ end
303
+ end
304
+
305
+ Canga.start(ARGV)
306
+
307
+
data/lib/cangallo.rb ADDED
@@ -0,0 +1,120 @@
1
+
2
+ # vim:ts=2:sw=2
3
+
4
+ # Copyright 2016, Javier Fontán Muiños <jfontan@gmail.com>
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License"); you may
7
+ # not use this file except in compliance with the License. You may obtain
8
+ # a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+
18
+ require 'cangallo/qcow2'
19
+ require 'cangallo/config'
20
+ require 'cangallo/repo'
21
+ require 'cangallo/cangafile'
22
+ require 'cangallo/libguestfs'
23
+ require 'cangallo/keybase'
24
+ require 'cangallo/version'
25
+
26
+ class Cangallo
27
+ def initialize
28
+ @config = Cangallo::Config.new
29
+ end
30
+
31
+ def repo(name = nil)
32
+ @config.repo(name)
33
+ end
34
+
35
+ def get_images(repo_name = nil)
36
+ info = []
37
+ repos = []
38
+
39
+ if repo_name
40
+ repos = [repo_name]
41
+ else
42
+ repos = @config.repos
43
+ end
44
+
45
+ repos.each do |r|
46
+ repo = self.repo(r)
47
+
48
+ repo.images.each do |sha256, image|
49
+ name = repo.short_name(sha256)
50
+
51
+ info << {
52
+ "repo" => r,
53
+ "sha256" => sha256,
54
+ "name" => "#{r}:#{name}",
55
+ "size" => image["actual-size"],
56
+ "parent" => short_name(image["parent"], r),
57
+ "description" => image["description"],
58
+ "available" => File.exist?(repo.image_path(sha256)),
59
+ "creation-time" => image["creation-time"]
60
+ }
61
+ end
62
+ end
63
+
64
+ info
65
+ end
66
+
67
+ def parse_name(name)
68
+ slices = name.split(':')
69
+
70
+ repo = nil
71
+ name = name
72
+
73
+ if slices.length > 1
74
+ repo = slices[0]
75
+ name = slices[1]
76
+ end
77
+
78
+ return repo, name
79
+ end
80
+
81
+ def short_name(string, repo = nil)
82
+ return nil if !string
83
+
84
+ img_repo, img_name = parse_name(string)
85
+ img_repo ||= repo
86
+
87
+ image = self.repo(img_repo).find(img_name)
88
+ name = self.repo(img_repo).short_name(image)
89
+
90
+ "#{img_repo}:#{name}"
91
+ end
92
+
93
+ def find(string)
94
+ repo, name = parse_name(string)
95
+ return "#{repo}:#{self.repo(repo).find(name)}" if repo
96
+
97
+ image = self.repo.find(name)
98
+ return "#{self.repo.name}:#{image}" if image
99
+
100
+ @config.repos.each do |r|
101
+ image = self.repo(r).find(name)
102
+ return "#{r}:#{image}" if image
103
+ end
104
+
105
+ nil
106
+ end
107
+
108
+ def get(string)
109
+ image = find(string)
110
+ return nil if !image
111
+
112
+ repo, name = parse_name(image)
113
+
114
+ img = self.repo(repo).get(name)
115
+
116
+ img["repo"] = repo if img
117
+ img
118
+ end
119
+ end
120
+
@@ -0,0 +1,187 @@
1
+
2
+ # vim:tabstop=2:sw=2:et:
3
+
4
+ # Copyright 2016, Javier Fontán Muiños <jfontan@gmail.com>
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License"); you may
7
+ # not use this file except in compliance with the License. You may obtain
8
+ # a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+
18
+ require 'json'
19
+ require 'systemu'
20
+ require 'tempfile'
21
+ require 'fileutils'
22
+ require 'yaml'
23
+
24
+ class Cangallo
25
+
26
+ class Cangafile
27
+ attr_accessor :data
28
+
29
+ ACTIONS = {
30
+ "copy" => {
31
+ "action" => lambda do |input|
32
+ add_head "copy-in #{input.gsub(" ", ":")}"
33
+ end
34
+ },
35
+
36
+ "run" => {
37
+ "type" => [Array, String],
38
+ "action" => lambda do |input|
39
+ if input.class == String
40
+ commands = input.split("\n").reject {|l| l.empty? }
41
+ else
42
+ commands = input
43
+ end
44
+
45
+ commands.each do |cmd|
46
+ add_head "run-command #{cmd}"
47
+ end
48
+ end
49
+ },
50
+
51
+ "change" => {
52
+ "type" => Hash,
53
+ "action" => lambda do |input|
54
+ regexp = input["regexp"].gsub("/", "\\/")
55
+ text = input["text"]
56
+ file = input["file"]
57
+ add_head "run-command sed -i 's/#{regexp}/#{text}/g' #{file}"
58
+ end
59
+ },
60
+
61
+ "delete" => {
62
+ "action" => lambda do |input|
63
+ add_head "run-command rm -rf #{input}"
64
+ end
65
+ },
66
+
67
+ "password" => {
68
+ "type" => Hash,
69
+ "action" => lambda do |input|
70
+ if input["disabled"]
71
+ password = "disabled"
72
+ else
73
+ password = "password:#{input["password"]}"
74
+ end
75
+
76
+ add_parameter "--password '#{input["user"]}:#{password}'"
77
+ end
78
+ }
79
+ }
80
+
81
+ def initialize(file)
82
+ text = File.read(file)
83
+ @data = YAML.load(text)
84
+
85
+ @params = []
86
+ @head = []
87
+ @tail = []
88
+
89
+ if !@data["tasks"] || @data["tasks"].class != Array
90
+ raise "No tasks defined or it's not an array"
91
+ end
92
+
93
+ @tasks = @data["tasks"]
94
+ end
95
+
96
+ def render
97
+ @tasks.each do |task|
98
+ raise %Q{Task "#{task.inspect}" malformed} if task.class != Hash
99
+
100
+ action_name = task.keys.first
101
+ action_data = task[action_name]
102
+
103
+ if !ACTIONS.keys.include?(action_name)
104
+ raise %Q{Invalid action "#{action_name}"}
105
+ end
106
+
107
+ action = ACTIONS[action_name]
108
+
109
+ if action["type"]
110
+ type = [action["type"]].flatten
111
+ else
112
+ type = [String]
113
+ end
114
+
115
+ # Check action value type
116
+ task_type = action_data.class
117
+ if !type.include?(task_type)
118
+ raise %Q{Action parameters for "#{action_name}" must be "#{type.inspect}"}
119
+ end
120
+
121
+ if action["action"]
122
+ instance_exec(action_data, &action["action"])
123
+ end
124
+ end
125
+
126
+ return @head + @tail, @params
127
+ end
128
+
129
+ def add_head(str)
130
+ @head << str
131
+ end
132
+
133
+ def add_tail(str)
134
+ @tail.unshift(str)
135
+ end
136
+
137
+ def add_parameter(str)
138
+ @params << str
139
+ end
140
+
141
+ def file_commands
142
+ text = ""
143
+
144
+ if @data["files"]
145
+ @data["files"].each do |line|
146
+ l = line.gsub(" ", ":")
147
+ text << "copy-in #{l}\n"
148
+ end
149
+ end
150
+
151
+ return text
152
+ end
153
+
154
+ def run_commands
155
+ text = ""
156
+
157
+ if @data["run"]
158
+ @data["run"].each do |line|
159
+ text << "run-command #{line}\n"
160
+ end
161
+ end
162
+
163
+ return text
164
+ end
165
+
166
+ def libguestfs_commands
167
+ @tasks.each do |task|
168
+ if task.class != Hash
169
+ raise "This task is not a hash: #{task}"
170
+ end
171
+
172
+
173
+ end
174
+ end
175
+
176
+ def libguestfs_commands_old
177
+ text = file_commands
178
+ text << run_commands
179
+ text
180
+ end
181
+
182
+ def parent
183
+ @data["parent"]
184
+ end
185
+ end
186
+
187
+ end
@@ -0,0 +1,95 @@
1
+
2
+ # vim:ts=2:sw=2
3
+
4
+ # Copyright 2016, Javier Fontán Muiños <jfontan@gmail.com>
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License"); you may
7
+ # not use this file except in compliance with the License. You may obtain
8
+ # a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+
18
+ require 'fileutils'
19
+ require 'yaml'
20
+
21
+ class Cangallo
22
+ class Config
23
+ CONFIG_DIR = '.cangallo'
24
+ CONFIG_FILE = 'config.yaml'
25
+
26
+ DEFAULT_CONFIG = <<EOT
27
+ default_repo: default
28
+ repos:
29
+ default:
30
+ type: local
31
+ path: ~/#{CONFIG_DIR}/default
32
+ EOT
33
+
34
+ def initialize
35
+ create_config_dir
36
+ create_default_config
37
+ load_conf
38
+ end
39
+
40
+ def repo(name = nil)
41
+ repo_name = name || @conf['default_repo'] || 'default'
42
+ raise(%q{Configuration malformed, no 'repos'}) if !@conf['repos']
43
+
44
+ repo_conf = @conf['repos'][repo_name]
45
+ raise(%Q<No repo with name '#{repo_name}'>) if !repo_conf
46
+ raise(%Q<Repo path no defined for '#{repo_name}'>) if !repo_conf['path']
47
+
48
+ path = File.expand_path(repo_conf["path"])
49
+ repo_conf["path"] = path
50
+ repo_conf["name"] = repo_name
51
+ create_repo_dir(path)
52
+ Cangallo::Repo.new(repo_conf)
53
+ end
54
+
55
+ def load_conf
56
+ @conf = YAML.load_file(config_file)
57
+ end
58
+
59
+ def create_config_dir
60
+ if !File.exist?(config_dir)
61
+ FileUtils.mkdir_p(config_dir)
62
+ end
63
+ end
64
+
65
+ def create_default_config
66
+ if !File.exist?(config_file)
67
+ open(config_file, 'w') do |f|
68
+ f.write(DEFAULT_CONFIG)
69
+ end
70
+
71
+ load_conf
72
+ path = File.expand_path(@conf["repos"]["default"]["path"])
73
+ create_repo_dir(path)
74
+ end
75
+ end
76
+
77
+ def create_repo_dir(path)
78
+ if !File.exist?(path)
79
+ FileUtils.mkdir_p(path)
80
+ end
81
+ end
82
+
83
+ def config_dir
84
+ File.join(ENV['HOME'], CONFIG_DIR)
85
+ end
86
+
87
+ def config_file
88
+ File.join(config_dir, CONFIG_FILE)
89
+ end
90
+
91
+ def repos
92
+ @conf["repos"].keys
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,39 @@
1
+
2
+ # vim:tabstop=2:sw=2:et:
3
+
4
+ # Copyright 2016, Javier Fontán Muiños <jfontan@gmail.com>
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License"); you may
7
+ # not use this file except in compliance with the License. You may obtain
8
+ # a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+
18
+ class Cangallo
19
+
20
+ module Keybase
21
+ def self.sign(file)
22
+ sig_file = "#{file}.sig"
23
+ cmd = "keybase pgp sign --detached --infile '#{file}' " \
24
+ "--outfile '#{sig_file}'"
25
+ rc = system(cmd)
26
+ raise "Error executing keybase sign command" if !rc
27
+ end
28
+
29
+ def self.verify(file)
30
+ sig_file = "#{file}.sig"
31
+ cmd = "keybase pgp verify --detached '#{sig_file}' " \
32
+ "--infile '#{file}'"
33
+ rc = system(cmd)
34
+ raise "Error executing keybase verify command" if !rc
35
+ end
36
+ end
37
+ end
38
+
39
+
@@ -0,0 +1,46 @@
1
+
2
+ # vim:tabstop=2:sw=2:et:
3
+
4
+ # Copyright 2016, Javier Fontán Muiños <jfontan@gmail.com>
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License"); you may
7
+ # not use this file except in compliance with the License. You may obtain
8
+ # a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+
18
+ require 'json'
19
+ require 'systemu'
20
+ require 'tempfile'
21
+ require 'fileutils'
22
+
23
+ class Cangallo
24
+
25
+ class LibGuestfs
26
+ def self.virt_customize(image, commands, params = "")
27
+ cmd_file = Tempfile.new("canga")
28
+
29
+ cmd_file.puts(commands)
30
+ cmd_file.close
31
+
32
+ #rc = system("virt-customize -v -x -a #{image} --commands-from-file #{cmd_file.path}")
33
+ rc = system("virt-customize -a #{image} #{params.join(" ")} " <<
34
+ "--commands-from-file #{cmd_file.path}")
35
+ cmd_file.unlink
36
+
37
+ return rc
38
+ end
39
+
40
+ def self.virt_sparsify(image)
41
+ rc = system("virt-sparsify --in-place #{image}")
42
+
43
+ return rc
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,151 @@
1
+
2
+ # vim:tabstop=2:sw=2:et:
3
+
4
+ # Copyright 2016, Javier Fontán Muiños <jfontan@gmail.com>
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License"); you may
7
+ # not use this file except in compliance with the License. You may obtain
8
+ # a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+
18
+ require 'json'
19
+ require 'systemu'
20
+ require 'tempfile'
21
+ require 'fileutils'
22
+
23
+ class Cangallo
24
+
25
+ class Qcow2
26
+ attr_reader :path
27
+
28
+ def initialize(path=nil)
29
+ @path=path
30
+ end
31
+
32
+ def info
33
+ res = execute :info, '--output=json', @path
34
+
35
+ JSON.parse res
36
+ end
37
+
38
+ def compress(destination = nil, parent = nil)
39
+ copy(destination, :parent => parent, :compress => true)
40
+ end
41
+
42
+ def convert(destination, options = {})
43
+ command = [:convert]
44
+ command << '-c' if options[:compress]
45
+ command << "-O #{options[:format]}" if options[:format]
46
+ command += [@path, destination]
47
+
48
+ execute(*command)
49
+ end
50
+
51
+ def copy(destination = nil, options = {})
52
+ ops = {
53
+ :parent => nil,
54
+ :compress => true,
55
+ :only_copy => false
56
+ }.merge(options)
57
+
58
+ parent = ops[:parent]
59
+
60
+ new_path = destination || @path + '.compressed'
61
+
62
+ command = [:convert, "-p", "-O qcow2"]
63
+ #command = ["convert", "-p", "-O qcow2"]
64
+ command << '-c' if ops[:compress]
65
+ command << "-o backing_file=#{parent}" if parent
66
+ command += [@path, new_path]
67
+
68
+ if ops[:only_copy]
69
+ FileUtils.cp(@path, new_path)
70
+ else
71
+ execute *command
72
+ end
73
+
74
+ # pp command
75
+ # system(*command)
76
+
77
+ if !destination
78
+ begin
79
+ File.rm @path
80
+ File.mv new_path, @path
81
+ ensure
82
+ File.rm new_path if File.exist? new_path
83
+ end
84
+ else
85
+ @path = new_path
86
+ end
87
+ end
88
+
89
+ def sparsify(destination)
90
+ parent = info['backing_file']
91
+ parent_options = ''
92
+
93
+ parent_options = "-o backing_file=#{parent}" if parent
94
+
95
+ command = "TMPDIR=#{File.dirname(destination)} virt-sparsify #{parent_options} #{@path} #{destination}"
96
+ status, stdout, stderr = systemu command
97
+ end
98
+
99
+ def sha(ver = 256)
100
+ command = "guestfish --progress-bars --ro -a #{@path} " <<
101
+ "run : checksum-device sha#{ver} /dev/sda"
102
+ %x{#{command}}.strip
103
+ end
104
+
105
+ def sha1
106
+ sha(1)
107
+ end
108
+
109
+ def sha256
110
+ sha(256)
111
+ end
112
+
113
+ def rebase(new_base)
114
+ execute :rebase, '-u', "-b #{new_base}", @path
115
+ end
116
+
117
+ def execute(command, *params)
118
+ self.class.execute(command, params)
119
+ end
120
+
121
+ def self.execute(command, *params)
122
+ command = "qemu-img #{command} #{params.join(' ')}"
123
+ STDERR.puts command
124
+
125
+ status, stdout, stderr = systemu command
126
+
127
+ if status.success?
128
+ stdout
129
+ else
130
+ raise stderr
131
+ end
132
+ end
133
+
134
+ def self.create_from_base(origin, destination, size=nil)
135
+ cmd = [:create, '-f qcow2', "-o backing_file=#{origin}", destination]
136
+ cmd << size if size
137
+
138
+ execute(*cmd)
139
+ end
140
+
141
+ def self.create(image, parent=nil, size=nil)
142
+ cmd = [:create, '-f qcow2']
143
+ cmd << "-o backing_file=#{parent}" if parent
144
+ cmd << image
145
+ cmd << size if size
146
+
147
+ execute(*cmd)
148
+ end
149
+ end
150
+
151
+ end
@@ -0,0 +1,276 @@
1
+
2
+ # vim:ts=2:sw=2
3
+
4
+ # Copyright 2016, Javier Fontán Muiños <jfontan@gmail.com>
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License"); you may
7
+ # not use this file except in compliance with the License. You may obtain
8
+ # a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+
18
+ require "yaml"
19
+ require "open-uri"
20
+ require "uri"
21
+
22
+ class Cangallo
23
+
24
+ class Repo
25
+ attr_reader :images, :tags, :path, :name
26
+
27
+ VERSION = 0
28
+
29
+ def initialize(conf)
30
+ @conf = conf
31
+ @path = File.expand_path(@conf["path"])
32
+ @name = @conf["name"]
33
+ @type = @conf["type"]
34
+ @url = @conf["url"]
35
+
36
+ if !@type && @url
37
+ @type = "remote"
38
+ else
39
+ @type = "local"
40
+ end
41
+
42
+ read_index
43
+ end
44
+
45
+ def index_data(images = {}, tags = {}, version = VERSION)
46
+ {
47
+ "version" => version,
48
+ "images" => images,
49
+ "tags" => tags
50
+ }
51
+ end
52
+
53
+ def index_path
54
+ metadata_path("index")
55
+ end
56
+
57
+ def read_index(index = nil)
58
+ if !index
59
+ if File.exist?(index_path)
60
+ data = YAML.load(File.read(index_path))
61
+ else
62
+ data = index_data()
63
+ end
64
+ else
65
+ data = YAML.load(index)
66
+ end
67
+
68
+ @images = data["images"]
69
+ @tags = data["tags"]
70
+ @reverse_tags = @tags.invert
71
+ end
72
+
73
+ def write_index
74
+ data = index_data(@images, @tags)
75
+
76
+ open(metadata_path("index"), "w") do |f|
77
+ f.write(data.to_yaml)
78
+ end
79
+ end
80
+
81
+ def metadata_path(name)
82
+ File.join(@path, "#{name}.yaml")
83
+ end
84
+
85
+ def image_path(name)
86
+ File.join(@path, "#{name}.qcow2")
87
+ end
88
+
89
+ def remote_url(name)
90
+ URI.join(@url, name)
91
+ end
92
+
93
+ def remote_image_url(name)
94
+ remote_url("#{name}.qcow2")
95
+ end
96
+
97
+ def add(name, data)
98
+ data["creation-time"] = Time.now
99
+ data["sha256"] = name
100
+ @images[name] = data
101
+ end
102
+
103
+ def add_image(file, data = {})
104
+ parent_sha256 = nil
105
+ parent = nil
106
+ parent_path = nil
107
+
108
+ only_copy = data.delete("only_copy")
109
+
110
+ if data["parent"]
111
+ parent_sha256 = data["parent"]
112
+ parent = self.images[parent_sha256]
113
+
114
+ if !parent
115
+ raise "Parent not found"
116
+ end
117
+
118
+ parent_path = File.expand_path(self.image_path(parent_sha256))
119
+ end
120
+
121
+ puts "Calculating image sha256 with libguestfs (it will take some time)"
122
+ qcow2 = Cangallo::Qcow2.new(file)
123
+ sha256 = qcow2.sha256
124
+ sha256.strip! if sha256
125
+
126
+ puts "Image SHA256: #{sha256}"
127
+
128
+ puts "Copying file to repository"
129
+ image_path = self.image_path(sha256)
130
+ qcow2.copy(image_path, parent: parent_path, only_copy: only_copy)
131
+
132
+ qcow2 = Cangallo::Qcow2.new(image_path)
133
+ info = qcow2.info
134
+
135
+ info_data = info.select do |k,v|
136
+ %w{virtual-size format actual-size format-specific}.include?(k)
137
+ end
138
+
139
+ data.merge!(info_data)
140
+
141
+ data["file-sha256"] = Digest::SHA256.file(file).hexdigest
142
+
143
+ if parent
144
+ qcow2.rebase("#{parent_sha256}.qcow2")
145
+ data["parent"] = parent_sha256
146
+ end
147
+
148
+ self.add(sha256, data)
149
+ self.write_index
150
+
151
+ sha256
152
+ end
153
+
154
+ def del_image(image)
155
+ sha256 = find(image)
156
+
157
+ raise %Q{Image "#{image}" does not exist} if !sha256
158
+
159
+ path = image_path(sha256)
160
+ File.delete(path)
161
+
162
+ @images.delete(sha256)
163
+ write_index
164
+ end
165
+
166
+ def add_tag(tag, image)
167
+ img = find(image)
168
+ @tags[tag] = img
169
+ write_index
170
+ end
171
+
172
+ def del_tag(tag)
173
+ @tags.delete(tag)
174
+ write_index
175
+ end
176
+
177
+ def find(name, search_tags = true)
178
+ length = name.length
179
+ found = @images.select do |sha256, data|
180
+ sha256[0, length] == name
181
+ end
182
+
183
+ if found && found.length > 0
184
+ return found.first.first
185
+ end
186
+
187
+ if search_tags
188
+ found = @tags.select do |tag, sha256|
189
+ tag == name
190
+ end
191
+ end
192
+
193
+ if found && found.length > 0
194
+ return found.first[1]
195
+ end
196
+
197
+ nil
198
+ end
199
+
200
+ def get(name)
201
+ image = find(name)
202
+
203
+ return nil if !image
204
+
205
+ @images[image]
206
+ end
207
+
208
+ def ancestors(name)
209
+ ancestors = []
210
+
211
+ image = get(name)
212
+ ancestors << image["sha256"]
213
+
214
+ while image["parent"]
215
+ image = get(image["parent"])
216
+ ancestors << image["sha256"]
217
+ end
218
+
219
+ ancestors
220
+ end
221
+
222
+ def url
223
+ @conf["url"]
224
+ end
225
+
226
+ def fetch
227
+ return nil if @conf["type"] != "remote"
228
+
229
+ uri = remote_url("index.yaml")
230
+
231
+ open(uri, "r") do |f|
232
+ data = f.read
233
+ read_index(data)
234
+ end
235
+
236
+ write_index
237
+ end
238
+
239
+ def sign
240
+ Keybase.sign(index_path)
241
+ end
242
+
243
+ def verify
244
+ Keybase.verify(index_path)
245
+ end
246
+
247
+ def pull(name)
248
+ image = get(name)
249
+
250
+ raise "Image not found" if !image
251
+
252
+ sha256 = image["sha256"]
253
+ image_url = remote_image_url(sha256)
254
+ image_path = image_path(sha256)
255
+ cmd = "curl -o '#{image_path}' '#{image_url}'"
256
+
257
+ STDERR.puts(cmd)
258
+
259
+ system(cmd)
260
+ end
261
+
262
+ def short_name(sha256)
263
+ tag = @reverse_tags[sha256]
264
+
265
+ if tag
266
+ name = "#{tag}"
267
+ else
268
+ name = "#{sha256[0..15]}"
269
+ end
270
+
271
+ name
272
+ end
273
+ end
274
+
275
+ end
276
+
@@ -0,0 +1,20 @@
1
+
2
+ # vim:ts=2:sw=2
3
+
4
+ # Copyright 2016, Javier Fontán Muiños <jfontan@gmail.com>
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License"); you may
7
+ # not use this file except in compliance with the License. You may obtain
8
+ # a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+
18
+ class Cangallo
19
+ VERSION = '0.0.1'
20
+ end
metadata ADDED
@@ -0,0 +1,82 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cangallo
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Javier Fontan
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-06-28 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: thor
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: systemu
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ description: VM Image manager
42
+ email: jfontan@gmail.com
43
+ executables:
44
+ - canga
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - bin/canga
49
+ - lib/cangallo.rb
50
+ - lib/cangallo/cangafile.rb
51
+ - lib/cangallo/config.rb
52
+ - lib/cangallo/keybase.rb
53
+ - lib/cangallo/libguestfs.rb
54
+ - lib/cangallo/qcow2.rb
55
+ - lib/cangallo/repo.rb
56
+ - lib/cangallo/version.rb
57
+ homepage: http://canga.io
58
+ licenses:
59
+ - Apache-2.0
60
+ metadata: {}
61
+ post_install_message:
62
+ rdoc_options: []
63
+ require_paths:
64
+ - lib
65
+ required_ruby_version: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ required_rubygems_version: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ requirements: []
76
+ rubyforge_project:
77
+ rubygems_version: 2.5.1
78
+ signing_key:
79
+ specification_version: 4
80
+ summary: Cangallo!!
81
+ test_files: []
82
+ has_rdoc: