cangallo 0.0.1

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.
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: