kytoon 1.0.2 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +4 -0
- data/README.rdoc +14 -6
- data/VERSION +1 -1
- data/config/server_group_libvirt.json +12 -0
- data/lib/kytoon/providers/libvirt.rb +1 -0
- data/lib/kytoon/providers/libvirt/server_group.rb +340 -0
- data/lib/kytoon/server_group.rb +3 -0
- metadata +6 -3
data/CHANGELOG
CHANGED
@@ -1,3 +1,7 @@
|
|
1
|
+
* Wed Aug 22 2012 Dan Prince <dprince@redhat.com> - 1.1.0
|
2
|
+
- Add local libvirt provider based on virt-clone.
|
3
|
+
- Libvirt: Support creating qcow2 disks during group creation.
|
4
|
+
|
1
5
|
* Fri Jul 27 2012 Dan Prince <dprince@redhat.com> - 1.0.2
|
2
6
|
- XenServer: Use force=true when shutting down VMs.
|
3
7
|
|
data/README.rdoc
CHANGED
@@ -1,17 +1,18 @@
|
|
1
1
|
= Kytoon
|
2
2
|
|
3
|
-
Create
|
3
|
+
Create small virtual server groups
|
4
4
|
|
5
5
|
== Description
|
6
6
|
|
7
|
-
A set of Rake tasks that provide a framework to help automate the creation and configuration of
|
7
|
+
A set of Rake tasks that provide a framework to help automate the creation and configuration of virtual server groups. Kytoon provides the ability to create projects that can be used by team members and continuous integration systems to create similar (if not identical) groups of servers for development and/or testing. Configuration information is stored in JSON and YAML formats which can be easily parsed, edited, and version controlled.
|
8
8
|
|
9
9
|
Inspired by and based on the Chef VPC Toolkit.
|
10
10
|
|
11
11
|
== Supports
|
12
12
|
|
13
|
-
-
|
14
|
-
- XenServer
|
13
|
+
- Libvirt: manage instances on local machine w/ libvirt, virt-clone, and libguestfs
|
14
|
+
- XenServer: manage instances on a remote XenServer box (via ssh)
|
15
|
+
- Cloud Servers VPC: API driven. Supports Rackspace and OpenStack
|
15
16
|
|
16
17
|
== Installation
|
17
18
|
|
@@ -27,8 +28,9 @@ Inspired by and based on the Chef VPC Toolkit.
|
|
27
28
|
3) Create a .kytoon.conf file in your HOME directory that contains the following:
|
28
29
|
|
29
30
|
# Set one of the following group_types
|
30
|
-
group_type:
|
31
|
+
group_type: libvirt
|
31
32
|
#group_type: xenserver
|
33
|
+
#group_type: cloud_server_vpc
|
32
34
|
|
33
35
|
# Cloud Servers VPC credentials
|
34
36
|
cloud_servers_vpc_url: https://your.vpc.url/
|
@@ -65,11 +67,17 @@ Example commands:
|
|
65
67
|
The following is an example bash script to spin up a group and run commands via SSH.
|
66
68
|
|
67
69
|
#!/bin/bash
|
70
|
+
# override the group type specified in .kytoon.conf
|
71
|
+
export GROUP_TYPE=libvirt
|
72
|
+
|
68
73
|
trap "rake group:delete" INT TERM EXIT # cleanup the group on exit
|
69
74
|
|
70
|
-
# create a server group
|
75
|
+
# create a server group (uses config/server_group.json)
|
71
76
|
rake group:create
|
72
77
|
|
78
|
+
# create a server group with alternate json file
|
79
|
+
rake group:create SERVER_GROUP_JSON=config/my_group.json
|
80
|
+
|
73
81
|
# Run some scripts on the login server
|
74
82
|
rake ssh bash <<-EOF_BASH
|
75
83
|
echo 'It works!'
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
1.0
|
1
|
+
1.1.0
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'kytoon/providers/libvirt/server_group'
|
@@ -0,0 +1,340 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'kytoon/util'
|
3
|
+
require 'rexml/document'
|
4
|
+
require 'rexml/xpath'
|
5
|
+
|
6
|
+
module Kytoon
|
7
|
+
|
8
|
+
module Providers
|
9
|
+
|
10
|
+
module Libvirt
|
11
|
+
# All in one Libvirt server group provider.
|
12
|
+
#
|
13
|
+
# Required setup:
|
14
|
+
# 1) Libvirt domain XML file or running domain to clone.
|
15
|
+
#
|
16
|
+
# 2) Generate an ssh keypair to be injected into the image.
|
17
|
+
#
|
18
|
+
class ServerGroup
|
19
|
+
|
20
|
+
KIB_PER_GIG = 1048576
|
21
|
+
|
22
|
+
@@data_dir=File.join(KYTOON_PROJECT, "tmp", "libvirt")
|
23
|
+
|
24
|
+
def self.data_dir
|
25
|
+
@@data_dir
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.data_dir=(dir)
|
29
|
+
@@data_dir=dir
|
30
|
+
end
|
31
|
+
|
32
|
+
CONFIG_FILE = KYTOON_PROJECT + File::SEPARATOR + "config" + File::SEPARATOR + "server_group.json"
|
33
|
+
|
34
|
+
attr_accessor :id
|
35
|
+
attr_accessor :name
|
36
|
+
|
37
|
+
def initialize(options={})
|
38
|
+
@id = options[:id] || Time.now.to_i
|
39
|
+
@name = options[:name]
|
40
|
+
@servers=[]
|
41
|
+
end
|
42
|
+
|
43
|
+
def server(name)
|
44
|
+
@servers.select {|s| s['hostname'] == name}[0] if @servers.size > 0
|
45
|
+
end
|
46
|
+
|
47
|
+
def servers
|
48
|
+
@servers
|
49
|
+
end
|
50
|
+
|
51
|
+
def gateway_ip
|
52
|
+
@servers.select {|s| s['gateway'] == 'true' }[0]['ip_address'] if @servers.size > 0
|
53
|
+
end
|
54
|
+
|
55
|
+
# generate a Server Group XML from server_group.json
|
56
|
+
def self.from_json(json)
|
57
|
+
|
58
|
+
json_hash=JSON.parse(json)
|
59
|
+
|
60
|
+
sg=ServerGroup.new(
|
61
|
+
:id => json_hash["id"],
|
62
|
+
:name => json_hash["name"]
|
63
|
+
)
|
64
|
+
json_hash["servers"].each do |server_hash|
|
65
|
+
|
66
|
+
sg.servers << {
|
67
|
+
'hostname' => server_hash['hostname'],
|
68
|
+
'memory' => server_hash['memory'],
|
69
|
+
'original' => server_hash['original'],
|
70
|
+
'original_xml' => server_hash['original_xml'],
|
71
|
+
'create_cow' => server_hash['create_cow'],
|
72
|
+
'disk_path' => server_hash['disk_path'],
|
73
|
+
'ip_address' => server_hash['ip_address'],
|
74
|
+
'gateway' => server_hash['gateway'] || "false"
|
75
|
+
}
|
76
|
+
end
|
77
|
+
return sg
|
78
|
+
end
|
79
|
+
|
80
|
+
def pretty_print
|
81
|
+
|
82
|
+
puts "Group ID: #{@id}"
|
83
|
+
puts "name: #{@name}"
|
84
|
+
puts "gateway IP: #{self.gateway_ip}"
|
85
|
+
puts "Servers:"
|
86
|
+
servers.each do |server|
|
87
|
+
puts "\tname: #{server['hostname']}"
|
88
|
+
puts "\t--"
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
92
|
+
|
93
|
+
def server_names
|
94
|
+
|
95
|
+
names=[]
|
96
|
+
|
97
|
+
servers.each do |server|
|
98
|
+
if block_given? then
|
99
|
+
yield server['hostname']
|
100
|
+
else
|
101
|
+
names << server['hostname']
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
names
|
106
|
+
|
107
|
+
end
|
108
|
+
|
109
|
+
def cache_to_disk
|
110
|
+
|
111
|
+
sg_hash = {
|
112
|
+
'id' => @id,
|
113
|
+
'name' => @name,
|
114
|
+
'servers' => []
|
115
|
+
}
|
116
|
+
@servers.each do |server|
|
117
|
+
sg_hash['servers'] << {'hostname' => server['hostname'], 'memory' => server['memory'], 'gateway' => server['gateway'], 'original' => server['original'], 'original_xml' => server['original_xml'], 'create_cow' => server['create_cow'], 'disk_path' => server['disk_path'], 'ip_address' => server['ip_address']}
|
118
|
+
end
|
119
|
+
|
120
|
+
FileUtils.mkdir_p(@@data_dir)
|
121
|
+
File.open(File.join(@@data_dir, "#{@id}.json"), 'w') do |f|
|
122
|
+
f.chmod(0600)
|
123
|
+
f.write(sg_hash.to_json)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def delete
|
128
|
+
servers.each do |server|
|
129
|
+
ServerGroup.cleanup_instances(@id, server['hostname'], server['disk_path'])
|
130
|
+
end
|
131
|
+
out_file=File.join(@@data_dir, "#{@id}.json")
|
132
|
+
File.delete(out_file) if File.exists?(out_file)
|
133
|
+
end
|
134
|
+
|
135
|
+
def self.create(sg)
|
136
|
+
ssh_public_key = Kytoon::Util.load_public_key
|
137
|
+
|
138
|
+
hosts_file_data = "127.0.0.1\tlocalhost localhost.localdomain\n"
|
139
|
+
sg.servers.each do |server|
|
140
|
+
|
141
|
+
image_dir=server['image_dir'] || '/var/lib/libvirt/images'
|
142
|
+
disk_path=File.join(image_dir, "#{sg.id}_#{server['hostname']}.img")
|
143
|
+
server['disk_path'] = disk_path
|
144
|
+
|
145
|
+
instance_ip = create_instance(sg.id, server['hostname'], server['memory'], server['original'], server['original_xml'], disk_path, server['create_cow'], ssh_public_key)
|
146
|
+
server['ip_address'] = instance_ip
|
147
|
+
hosts_file_data += "#{instance_ip}\t#{server['hostname']}\n"
|
148
|
+
sg.cache_to_disk
|
149
|
+
end
|
150
|
+
|
151
|
+
puts "Copying hosts files..."
|
152
|
+
#now that we have IP info copy hosts files into the servers
|
153
|
+
sg.servers.each do |server|
|
154
|
+
Kytoon::Util.remote_exec(%{
|
155
|
+
cat > /etc/hosts <<-EOF_CAT
|
156
|
+
#{hosts_file_data}
|
157
|
+
EOF_CAT
|
158
|
+
hostname "#{server['hostname']}"
|
159
|
+
if [ -f /etc/sysconfig/network ]; then
|
160
|
+
sed -e "s|^HOSTNAME.*|HOSTNAME=#{server['hostname']}|" -i /etc/sysconfig/network
|
161
|
+
fi
|
162
|
+
}, server['ip_address']) do |ok, out|
|
163
|
+
if not ok
|
164
|
+
puts out
|
165
|
+
raise "Failed to copy host file to instance #{server['hostname']}."
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
sg
|
171
|
+
end
|
172
|
+
|
173
|
+
def self.get(options={})
|
174
|
+
id = options[:id]
|
175
|
+
if id.nil? then
|
176
|
+
group=ServerGroup.most_recent
|
177
|
+
raise "No server group files exist." if group.nil?
|
178
|
+
id=group.id
|
179
|
+
end
|
180
|
+
|
181
|
+
out_file=File.join(@@data_dir, "#{id}.json")
|
182
|
+
raise "No server group files exist." if not File.exists?(out_file)
|
183
|
+
ServerGroup.from_json(IO.read(out_file))
|
184
|
+
end
|
185
|
+
|
186
|
+
def self.index(options={})
|
187
|
+
|
188
|
+
server_groups=[]
|
189
|
+
Dir[File.join(ServerGroup.data_dir, '*.json')].each do |file|
|
190
|
+
server_groups << ServerGroup.from_json(IO.read(file))
|
191
|
+
end
|
192
|
+
server_groups
|
193
|
+
|
194
|
+
end
|
195
|
+
|
196
|
+
def self.most_recent
|
197
|
+
server_groups=[]
|
198
|
+
Dir[File.join(@@data_dir, "*.json")].each do |file|
|
199
|
+
server_groups << ServerGroup.from_json(IO.read(file))
|
200
|
+
end
|
201
|
+
if server_groups.size > 0 then
|
202
|
+
server_groups.sort { |a,b| b.id <=> a.id }[0]
|
203
|
+
else
|
204
|
+
nil
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
# Determine the path of the source disk to be used
|
209
|
+
def self.source_disk_filename(original, original_xml)
|
210
|
+
if original and not original.empty? then
|
211
|
+
dom = REXML::Document.new(%x{virsh dumpxml nova1})
|
212
|
+
else
|
213
|
+
dom = REXML::Document.new(IO.read(original_xml))
|
214
|
+
end
|
215
|
+
REXML::XPath.each(dom, "//disk[1]/source") do |source_xml|
|
216
|
+
return source_xml.attributes['file']
|
217
|
+
end
|
218
|
+
raise "Unable to find disk path for instance."
|
219
|
+
end
|
220
|
+
|
221
|
+
def self.create_instance(group_id, inst_name, memory_gigs, original, original_xml, disk_path, create_cow, ssh_public_key)
|
222
|
+
|
223
|
+
puts "Creating instance: #{inst_name}"
|
224
|
+
instance_memory = (KIB_PER_GIG * memory_gigs.to_f).to_i
|
225
|
+
original_disk_path = source_disk_filename(original, original_xml) #cow only
|
226
|
+
domain_name="#{group_id}_#{inst_name}"
|
227
|
+
|
228
|
+
out = %x{
|
229
|
+
if [ -n "$DEBUG" ]; then
|
230
|
+
set -x
|
231
|
+
fi
|
232
|
+
export VIRSH_DEFAULT_CONNECT_URI="qemu:///system"
|
233
|
+
if [ -n "#{original_xml}" ]; then
|
234
|
+
ORIGIN="--original-xml #{original_xml}"
|
235
|
+
elif [ -n "#{original}" ]; then
|
236
|
+
ORIGIN="--original #{original}"
|
237
|
+
else
|
238
|
+
{ echo "Please specify 'original' or 'original_xml'."; exit 1; }
|
239
|
+
fi
|
240
|
+
|
241
|
+
if [ -n "#{create_cow}" ]; then
|
242
|
+
|
243
|
+
virt-clone --connect="$VIRSH_DEFAULT_CONNECT_URI" \
|
244
|
+
--name '#{domain_name}' \
|
245
|
+
--file '#{disk_path}' \
|
246
|
+
--force \
|
247
|
+
$ORIGIN \
|
248
|
+
--preserve-data \
|
249
|
+
|| { echo "failed to virt-clone"; exit 1; }
|
250
|
+
|
251
|
+
qemu-img create -f qcow2 -o backing_file=#{original_disk_path} "#{disk_path}"
|
252
|
+
|
253
|
+
else
|
254
|
+
|
255
|
+
virt-clone --connect="$VIRSH_DEFAULT_CONNECT_URI" \
|
256
|
+
--name '#{domain_name}' \
|
257
|
+
--file '#{disk_path}' \
|
258
|
+
--force \
|
259
|
+
$ORIGIN \
|
260
|
+
|| { echo "failed to virt-clone"; exit 1; }
|
261
|
+
|
262
|
+
fi
|
263
|
+
|
264
|
+
LV_ROOT=$(virt-filesystems -a #{disk_path} --logical-volumes | grep root)
|
265
|
+
# If using LVM we inject the ssh key this way
|
266
|
+
if [ -n "$LV_ROOT" ]; then
|
267
|
+
guestfish --selinux add #{disk_path} : \
|
268
|
+
run : \
|
269
|
+
mount $LV_ROOT / : \
|
270
|
+
sh "/bin/mkdir -p /root/.ssh" : \
|
271
|
+
write-append /root/.ssh/authorized_keys "#{ssh_public_key}" : \
|
272
|
+
sh "/bin/chmod -R 700 /root/.ssh"
|
273
|
+
fi
|
274
|
+
|
275
|
+
virsh setmaxmem #{domain_name} #{instance_memory}
|
276
|
+
virsh start #{domain_name}
|
277
|
+
virsh setmem #{domain_name} #{instance_memory}
|
278
|
+
|
279
|
+
}
|
280
|
+
retval=$?
|
281
|
+
if not retval.success?
|
282
|
+
puts out
|
283
|
+
raise "Failed to create instance #{inst_name}."
|
284
|
+
end
|
285
|
+
|
286
|
+
# lookup server IP here...
|
287
|
+
mac_addr = nil
|
288
|
+
dom_xml = %x{virsh --connect=qemu:///system dumpxml #{domain_name}}
|
289
|
+
dom = REXML::Document.new(dom_xml)
|
290
|
+
REXML::XPath.each(dom, "//interface/mac") do |interface_xml|
|
291
|
+
mac_addr = interface_xml.attributes['address']
|
292
|
+
end
|
293
|
+
raise "Failed to lookup mac address for #{inst_name}" if mac_addr.nil?
|
294
|
+
|
295
|
+
instance_ip = %x{grep -i #{mac_addr} /var/lib/libvirt/dnsmasq/default.leases | cut -d " " -f 3}.chomp
|
296
|
+
count = 0
|
297
|
+
until not instance_ip.empty? do
|
298
|
+
instance_ip = %x{grep -i #{mac_addr} /var/lib/libvirt/dnsmasq/default.leases | cut -d " " -f 3}.chomp
|
299
|
+
sleep 1
|
300
|
+
count += 1
|
301
|
+
if count >= 60 then
|
302
|
+
raise "Failed to lookup ip address for #{inst_name}"
|
303
|
+
end
|
304
|
+
end
|
305
|
+
return instance_ip
|
306
|
+
|
307
|
+
end
|
308
|
+
|
309
|
+
def self.cleanup_instances(group_id, inst_name, disk_path)
|
310
|
+
domain_name="#{group_id}_#{inst_name}"
|
311
|
+
out = %x{
|
312
|
+
if [ -n "$DEBUG" ]; then
|
313
|
+
set -x
|
314
|
+
fi
|
315
|
+
export VIRSH_DEFAULT_CONNECT_URI="qemu:///system"
|
316
|
+
if virsh dumpxml #{domain_name} &> /dev/null; then
|
317
|
+
virsh destroy "#{domain_name}" &> /dev/null
|
318
|
+
virsh undefine "#{domain_name}"
|
319
|
+
fi
|
320
|
+
# If we used --preserve-data there will be no volume... ignore it
|
321
|
+
virsh vol-delete --pool default "#{group_id}_#{inst_name}.img" &> /dev/null
|
322
|
+
if [ -f "#{disk_path}" ]; then
|
323
|
+
rm -f "#{disk_path}"
|
324
|
+
fi
|
325
|
+
}
|
326
|
+
puts out
|
327
|
+
retval=$?
|
328
|
+
if not retval.success?
|
329
|
+
puts out
|
330
|
+
raise "Failed to cleanup instances."
|
331
|
+
end
|
332
|
+
end
|
333
|
+
|
334
|
+
end
|
335
|
+
|
336
|
+
end
|
337
|
+
|
338
|
+
end
|
339
|
+
|
340
|
+
end
|
data/lib/kytoon/server_group.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'kytoon/providers/cloud_servers_vpc'
|
2
|
+
require 'kytoon/providers/libvirt'
|
2
3
|
require 'kytoon/providers/xenserver'
|
3
4
|
|
4
5
|
class ServerGroup
|
@@ -14,6 +15,8 @@ class ServerGroup
|
|
14
15
|
@@group_class = Kytoon::Providers::CloudServersVPC::ServerGroup
|
15
16
|
elsif group_type == "xenserver" then
|
16
17
|
@@group_class = Kytoon::Providers::Xenserver::ServerGroup
|
18
|
+
elsif group_type == "libvirt" then
|
19
|
+
@@group_class = Kytoon::Providers::Libvirt::ServerGroup
|
17
20
|
else
|
18
21
|
raise "Invalid 'group_type' specified in config file."
|
19
22
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: kytoon
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0
|
4
|
+
version: 1.1.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-08-
|
12
|
+
date: 2012-08-22 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rdoc
|
@@ -188,6 +188,7 @@ files:
|
|
188
188
|
- README.rdoc
|
189
189
|
- Rakefile
|
190
190
|
- VERSION
|
191
|
+
- config/server_group_libvirt.json
|
191
192
|
- config/server_group_vpc.json
|
192
193
|
- config/server_group_xen.json
|
193
194
|
- lib/kytoon.rb
|
@@ -198,6 +199,8 @@ files:
|
|
198
199
|
- lib/kytoon/providers/cloud_servers_vpc/server_group.rb
|
199
200
|
- lib/kytoon/providers/cloud_servers_vpc/ssh_public_key.rb
|
200
201
|
- lib/kytoon/providers/cloud_servers_vpc/vpn_network_interface.rb
|
202
|
+
- lib/kytoon/providers/libvirt.rb
|
203
|
+
- lib/kytoon/providers/libvirt/server_group.rb
|
201
204
|
- lib/kytoon/providers/xenserver.rb
|
202
205
|
- lib/kytoon/providers/xenserver/server_group.rb
|
203
206
|
- lib/kytoon/server_group.rb
|
@@ -232,7 +235,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
232
235
|
version: '0'
|
233
236
|
segments:
|
234
237
|
- 0
|
235
|
-
hash: -
|
238
|
+
hash: -990140921897305519
|
236
239
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
237
240
|
none: false
|
238
241
|
requirements:
|