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 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 & configure ephemeral virtual private clouds
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 “identical” VPC server groups. Kytoon provides the ability to create projects that can be used by team members and continuous integration systems to spin up 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.
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
- - Cloud Servers VPC (Rackspace and OpenStack)
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: cloud_server_vpc
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.2
1
+ 1.1.0
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "Fedora",
3
+ "servers": [
4
+ {
5
+ "hostname": "nova1",
6
+ "memory": "1",
7
+ "gateway": "true",
8
+ "original_xml": "/home/dprince/f17.xml",
9
+ "create_cow": "true"
10
+ }
11
+ ]
12
+ }
@@ -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
@@ -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.2
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-13 00:00:00.000000000 Z
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: -1167225568058979135
238
+ hash: -990140921897305519
236
239
  required_rubygems_version: !ruby/object:Gem::Requirement
237
240
  none: false
238
241
  requirements: