sunzi 0.8.0 → 0.9.0
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.
- data/README.md +3 -2
- data/lib/sunzi.rb +4 -3
- data/lib/sunzi/cli.rb +15 -15
- data/lib/sunzi/cloud/base.rb +2 -0
- data/lib/sunzi/cloud/digital_ocean.rb +147 -0
- data/lib/sunzi/cloud/linode.rb +4 -8
- data/lib/sunzi/dependency.rb +2 -1
- data/lib/sunzi/version.rb +1 -1
- data/lib/templates/setup/digital_ocean/digital_ocean.yml +26 -0
- data/lib/templates/setup/linode/linode.yml +4 -4
- metadata +29 -21
data/README.md
CHANGED
@@ -18,6 +18,7 @@ Its design goals are:
|
|
18
18
|
|
19
19
|
### What's new:
|
20
20
|
|
21
|
+
* v0.9: Support for [DigitalOcean](https://www.digitalocean.com) setup / teardown.
|
21
22
|
* v0.8: Added `--sudo` option to `sunzi deploy`.
|
22
23
|
* v0.7: Added `erase_remote_folder` and `cache_remote_recipes` preferences for customized behavior.
|
23
24
|
* v0.6: System function sunzi::silencer() added for succinct log messages.
|
@@ -67,8 +68,8 @@ $ sunzi compile # Compile Sunzi project
|
|
67
68
|
$ sunzi create # Create a new Sunzi project
|
68
69
|
$ sunzi deploy [user@host:port] [role] [--sudo] # Deploy Sunzi project
|
69
70
|
|
70
|
-
$ sunzi setup [linode|
|
71
|
-
$ sunzi teardown [linode|
|
71
|
+
$ sunzi setup [linode|digital_ocean] # Setup a new VM on the Cloud services
|
72
|
+
$ sunzi teardown [linode|digital_ocean] [name] # Teardown an existing VM on the Cloud services
|
72
73
|
```
|
73
74
|
|
74
75
|
Directory structure
|
data/lib/sunzi.rb
CHANGED
@@ -10,8 +10,9 @@ module Sunzi
|
|
10
10
|
autoload :Version, 'sunzi/version'
|
11
11
|
|
12
12
|
module Cloud
|
13
|
-
autoload :Base,
|
14
|
-
autoload :Linode,
|
15
|
-
autoload :EC2,
|
13
|
+
autoload :Base, 'sunzi/cloud/base'
|
14
|
+
autoload :Linode, 'sunzi/cloud/linode'
|
15
|
+
autoload :EC2, 'sunzi/cloud/ec2'
|
16
|
+
autoload :DigitalOcean, 'sunzi/cloud/digital_ocean'
|
16
17
|
end
|
17
18
|
end
|
data/lib/sunzi/cli.rb
CHANGED
@@ -4,28 +4,28 @@ module Sunzi
|
|
4
4
|
class Cli < Thor
|
5
5
|
include Thor::Actions
|
6
6
|
|
7
|
-
desc
|
7
|
+
desc 'create', 'Create sunzi project'
|
8
8
|
def create(project = 'sunzi')
|
9
9
|
do_create(project)
|
10
10
|
end
|
11
11
|
|
12
|
-
desc
|
12
|
+
desc 'deploy [user@host:port] [role] [--sudo]', 'Deploy sunzi project'
|
13
13
|
method_options :sudo => false
|
14
14
|
def deploy(target, role = nil)
|
15
15
|
do_deploy(target, role, options.sudo?)
|
16
16
|
end
|
17
17
|
|
18
|
-
desc
|
18
|
+
desc 'compile', 'Compile sunzi project'
|
19
19
|
def compile(role = nil)
|
20
20
|
do_compile(role)
|
21
21
|
end
|
22
22
|
|
23
|
-
desc
|
23
|
+
desc 'setup [linode|digital_ocean]', 'Setup a new VM'
|
24
24
|
def setup(target)
|
25
25
|
Cloud::Base.choose(self, target).setup
|
26
26
|
end
|
27
27
|
|
28
|
-
desc
|
28
|
+
desc 'teardown [linode|digital_ocean] [name]', 'Teardown an existing VM'
|
29
29
|
def teardown(target, name)
|
30
30
|
Cloud::Base.choose(self, target).teardown(name)
|
31
31
|
end
|
@@ -38,14 +38,14 @@ module Sunzi
|
|
38
38
|
end
|
39
39
|
|
40
40
|
def do_create(project)
|
41
|
-
template
|
42
|
-
template
|
43
|
-
template
|
44
|
-
template
|
45
|
-
template
|
46
|
-
template
|
47
|
-
template
|
48
|
-
template
|
41
|
+
template 'templates/create/.gitignore', "#{project}/.gitignore"
|
42
|
+
template 'templates/create/sunzi.yml', "#{project}/sunzi.yml"
|
43
|
+
template 'templates/create/install.sh', "#{project}/install.sh"
|
44
|
+
template 'templates/create/recipes/sunzi.sh', "#{project}/recipes/sunzi.sh"
|
45
|
+
template 'templates/create/recipes/ssh_key.sh', "#{project}/recipes/ssh_key.sh"
|
46
|
+
template 'templates/create/roles/app.sh', "#{project}/roles/app.sh"
|
47
|
+
template 'templates/create/roles/db.sh', "#{project}/roles/db.sh"
|
48
|
+
template 'templates/create/roles/web.sh', "#{project}/roles/web.sh"
|
49
49
|
end
|
50
50
|
|
51
51
|
def do_deploy(target, role, force_sudo)
|
@@ -91,7 +91,7 @@ module Sunzi
|
|
91
91
|
|
92
92
|
def do_compile(role)
|
93
93
|
# Check if you're in the sunzi directory
|
94
|
-
abort_with
|
94
|
+
abort_with 'You must be in the sunzi folder' unless File.exists?('sunzi.yml')
|
95
95
|
# Check if role exists
|
96
96
|
abort_with "#{role} doesn't exist!" if role and !File.exists?("roles/#{role}.sh")
|
97
97
|
|
@@ -115,7 +115,7 @@ module Sunzi
|
|
115
115
|
|
116
116
|
# Build install.sh
|
117
117
|
if role
|
118
|
-
create_file 'compiled/install.sh', File.binread(
|
118
|
+
create_file 'compiled/install.sh', File.binread('install.sh') << "\n" << File.binread("roles/#{role}.sh")
|
119
119
|
else
|
120
120
|
copy_file File.expand_path('install.sh'), 'compiled/install.sh'
|
121
121
|
end
|
data/lib/sunzi/cloud/base.rb
CHANGED
@@ -0,0 +1,147 @@
|
|
1
|
+
Sunzi::Dependency.load('digital_ocean')
|
2
|
+
|
3
|
+
module Sunzi
|
4
|
+
module Cloud
|
5
|
+
class DigitalOcean < Base
|
6
|
+
def setup
|
7
|
+
unless File.exist? 'digital_ocean/digital_ocean.yml'
|
8
|
+
@cli.empty_directory 'digital_ocean/instances'
|
9
|
+
@cli.template 'templates/setup/digital_ocean/digital_ocean.yml', 'digital_ocean/digital_ocean.yml'
|
10
|
+
exit_with 'Now go ahead and edit digital_ocean.yml, then run this command again!'
|
11
|
+
end
|
12
|
+
|
13
|
+
@config = YAML.load(File.read('digital_ocean/digital_ocean.yml'))
|
14
|
+
|
15
|
+
if @config['fqdn']['zone'] == 'example.com'
|
16
|
+
abort_with 'You must have your own settings in digital_ocean.yml'
|
17
|
+
end
|
18
|
+
|
19
|
+
# When route53 is specified for DNS, check if it's properly configured and if not, fail earlier.
|
20
|
+
setup_route53 if @config['dns'] == 'route53'
|
21
|
+
|
22
|
+
@api = ::DigitalOcean::API.new :api_key => @config['api_key'], :client_id => @config['client_id']
|
23
|
+
|
24
|
+
# Ask environment and hostname
|
25
|
+
@env = ask("environment? (#{@config['environments'].join(' / ')}): ", String) {|q| q.in = @config['environments'] }.to_s
|
26
|
+
@host = ask('hostname? (only the first part of subdomain): ', String).to_s
|
27
|
+
|
28
|
+
@fqdn = @config['fqdn'][@env].gsub(/%{host}/, @host)
|
29
|
+
@name = @config['name'][@env].gsub(/%{host}/, @host)
|
30
|
+
|
31
|
+
# Choose a size
|
32
|
+
result = @api.sizes.list.sizes
|
33
|
+
result.each{|i| say "#{i.id}: #{i.name}" }
|
34
|
+
@size_id = ask('which size?: ', Integer) {|q| q.in = result.map(&:id); q.default = result.first.id }
|
35
|
+
@size_name = result.find{|i| i.id == @size_id }.name
|
36
|
+
|
37
|
+
# Choose a region
|
38
|
+
result = @api.regions.list.regions
|
39
|
+
result.each{|i| say "#{i.id}: #{i.name}" }
|
40
|
+
@region_id = ask('which region?: ', Integer) {|q| q.in = result.map(&:id); q.default = result.first.id }
|
41
|
+
@region_name = result.find{|i| i.id == @region_id }.name
|
42
|
+
|
43
|
+
# Choose a image
|
44
|
+
result = @api.images.list({'filter' => 'global'}).images
|
45
|
+
if @config['distributions_filter']
|
46
|
+
result = result.select{|i| i.distribution.match Regexp.new(@config['distributions_filter'], Regexp::IGNORECASE) }
|
47
|
+
end
|
48
|
+
result.each{|i| say "#{i.id}: #{i.name}" }
|
49
|
+
@image_id = ask('which image?: ', Integer) {|q| q.in = result.map(&:id); q.default = result.first.id }
|
50
|
+
@image_name = result.find{|i| i.id == @image_id }.name
|
51
|
+
|
52
|
+
# Go ahead?
|
53
|
+
moveon = ask("Are you ready to go ahead and create #{@fqdn}? (y/n) ", String) {|q| q.in = ['y','n']}
|
54
|
+
exit unless moveon == 'y'
|
55
|
+
|
56
|
+
@ssh_key_ids = @api.ssh_keys.list.ssh_keys.map(&:id).join(',')
|
57
|
+
|
58
|
+
# Create
|
59
|
+
say "creating a new droplets..."
|
60
|
+
result = @api.droplets.create(:name => @name,
|
61
|
+
:size_id => @size_id,
|
62
|
+
:image_id => @image_id,
|
63
|
+
:region_id => @region_id,
|
64
|
+
:ssh_key_ids => @ssh_key_ids)
|
65
|
+
|
66
|
+
@droplet_id = result.droplet.id
|
67
|
+
say "Created a new droplet (id: #{@droplet_id}). Booting..."
|
68
|
+
|
69
|
+
# Boot
|
70
|
+
while @api.droplets.show(@droplet_id).droplet.status.downcase != 'active'
|
71
|
+
sleep 5
|
72
|
+
end
|
73
|
+
|
74
|
+
@public_ip = @api.droplets.show(@droplet_id).droplet.ip_address
|
75
|
+
say "Done. ip address = #{@public_ip}"
|
76
|
+
|
77
|
+
# Register IP to DNS
|
78
|
+
case @config['dns']
|
79
|
+
when 'route53'
|
80
|
+
# Set the public IP to AWS Route 53
|
81
|
+
say "Setting the public IP to AWS Route 53..."
|
82
|
+
Route53::DNSRecord.new(@fqdn, "A", "300", [@public_ip], @route53_zone).create
|
83
|
+
end
|
84
|
+
|
85
|
+
# Save the instance info
|
86
|
+
hash = {
|
87
|
+
:droplet_id => @droplet_id,
|
88
|
+
:env => @env,
|
89
|
+
:host => @host,
|
90
|
+
:fqdn => @fqdn,
|
91
|
+
:name => @name,
|
92
|
+
:ip_address => @public_ip,
|
93
|
+
:size_id => @size_id,
|
94
|
+
:size_name => @size_name,
|
95
|
+
:region_id => @region_id,
|
96
|
+
:region_name => @region_name,
|
97
|
+
:image_id => @image_id,
|
98
|
+
:image_name => @image_name,
|
99
|
+
}
|
100
|
+
@cli.create_file "digital_ocean/instances/#{@name}.yml", YAML.dump(hash)
|
101
|
+
|
102
|
+
end
|
103
|
+
|
104
|
+
def teardown(name)
|
105
|
+
unless File.exist?("digital_ocean/instances/#{name}.yml")
|
106
|
+
abort_with "#{name}.yml was not found in the instances directory."
|
107
|
+
end
|
108
|
+
|
109
|
+
@config = YAML.load(File.read('digital_ocean/digital_ocean.yml'))
|
110
|
+
setup_route53 if @config['dns'] == 'route53'
|
111
|
+
|
112
|
+
@instance = YAML.load(File.read("digital_ocean/instances/#{name}.yml"))
|
113
|
+
@droplet_id = @instance[:droplet_id]
|
114
|
+
|
115
|
+
@api = ::DigitalOcean::API.new :api_key => @config['api_key'], :client_id => @config['client_id']
|
116
|
+
|
117
|
+
# Are you sure?
|
118
|
+
moveon = ask("Are you sure about deleting #{@instance[:fqdn]} permanently? (y/n) ", String) {|q| q.in = ['y','n']}
|
119
|
+
exit unless moveon == 'y'
|
120
|
+
|
121
|
+
# Delete the droplet
|
122
|
+
say 'deleting droplet...'
|
123
|
+
@api.droplets.delete(@droplet_id)
|
124
|
+
|
125
|
+
# Delete DNS record
|
126
|
+
case @config['dns']
|
127
|
+
when 'route53'
|
128
|
+
say 'deleting the public IP from AWS Route 53...'
|
129
|
+
@record = @route53_zone.get_records.find{|i| i.values.first == @instance[:ip_address] }
|
130
|
+
@record.delete if @record
|
131
|
+
end
|
132
|
+
|
133
|
+
# Remove the instance config file
|
134
|
+
@cli.remove_file "digital_ocean/instances/#{name}.yml"
|
135
|
+
|
136
|
+
say 'Done.'
|
137
|
+
end
|
138
|
+
|
139
|
+
def setup_route53
|
140
|
+
Sunzi::Dependency.load('route53')
|
141
|
+
route53 = Route53::Connection.new(@config['route53']['key'], @config['route53']['secret'])
|
142
|
+
@route53_zone = route53.get_zones.find{|i| i.name.sub(/\.$/,'') == @config['fqdn']['zone'] }
|
143
|
+
abort_with "zone for #{@config['fqdn']['zone']} was not found on route53!" unless @route53_zone
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
data/lib/sunzi/cloud/linode.rb
CHANGED
@@ -1,8 +1,5 @@
|
|
1
1
|
Sunzi::Dependency.load('linode')
|
2
2
|
|
3
|
-
# Workaround the bug: https://github.com/rick/linode/issues/12
|
4
|
-
YAML::ENGINE.yamler = 'syck'
|
5
|
-
|
6
3
|
module Sunzi
|
7
4
|
module Cloud
|
8
5
|
class Linode < Base
|
@@ -30,7 +27,7 @@ module Sunzi
|
|
30
27
|
|
31
28
|
# Ask environment and hostname
|
32
29
|
@env = ask("environment? (#{@config['environments'].join(' / ')}): ", String) {|q| q.in = @config['environments'] }.to_s
|
33
|
-
@host = ask('hostname? (only the
|
30
|
+
@host = ask('hostname? (only the first part of subdomain): ', String).to_s
|
34
31
|
|
35
32
|
@fqdn = @config['fqdn'][@env].gsub(/%{host}/, @host)
|
36
33
|
@label = @config['label'][@env].gsub(/%{host}/, @host)
|
@@ -200,14 +197,12 @@ module Sunzi
|
|
200
197
|
# Delete DNS record
|
201
198
|
case @config['dns']
|
202
199
|
when 'linode'
|
203
|
-
|
204
|
-
say "deleting the public IP to Linode DNS Manager..."
|
200
|
+
say 'deleting the public IP from Linode DNS Manager...'
|
205
201
|
@domainid = @api.domain.list.find{|i| i.domain == @config['fqdn']['zone'] }.domainid
|
206
202
|
@resource = @api.domain.resource.list(:DomainID => @domainid).find{|i| i.target == @instance[:public_ip] }
|
207
203
|
@api.domain.resource.delete(:DomainID => @domainid, :ResourceID => @resource.resourceid)
|
208
204
|
when 'route53'
|
209
|
-
|
210
|
-
say "deleting the public IP to AWS Route 53..."
|
205
|
+
say 'deleting the public IP from AWS Route 53...'
|
211
206
|
@record = @route53_zone.get_records.find{|i| i.values.first == @instance[:public_ip] }
|
212
207
|
@record.delete if @record
|
213
208
|
end
|
@@ -222,6 +217,7 @@ module Sunzi
|
|
222
217
|
Sunzi::Dependency.load('route53')
|
223
218
|
route53 = Route53::Connection.new(@config['route53']['key'], @config['route53']['secret'])
|
224
219
|
@route53_zone = route53.get_zones.find{|i| i.name.sub(/\.$/,'') == @config['fqdn']['zone'] }
|
220
|
+
abort_with "zone for #{@config['fqdn']['zone']} was not found on route53!" unless @route53_zone
|
225
221
|
end
|
226
222
|
|
227
223
|
def wait_for(action)
|
data/lib/sunzi/dependency.rb
CHANGED
@@ -2,9 +2,10 @@ module Sunzi
|
|
2
2
|
class Dependency
|
3
3
|
def self.all
|
4
4
|
{
|
5
|
-
'linode' => { :require => 'linode', :version => '>= 0.7.
|
5
|
+
'linode' => { :require => 'linode', :version => '>= 0.7.9' },
|
6
6
|
'highline' => { :require => 'highline', :version => '>= 1.6.11'},
|
7
7
|
'route53' => { :require => 'route53', :version => '>= 0.2.1' },
|
8
|
+
'digital_ocean' => { :require => 'digital_ocean', :version => '>= 0.0.1' },
|
8
9
|
}
|
9
10
|
end
|
10
11
|
|
data/lib/sunzi/version.rb
CHANGED
@@ -0,0 +1,26 @@
|
|
1
|
+
---
|
2
|
+
api_key: your_api_key
|
3
|
+
client_id: your_client_id
|
4
|
+
|
5
|
+
# add / remove environments
|
6
|
+
environments:
|
7
|
+
- production
|
8
|
+
- staging
|
9
|
+
fqdn:
|
10
|
+
zone: example.com
|
11
|
+
production: '%{host}.example.com'
|
12
|
+
staging: '%{host}.staging.example.com'
|
13
|
+
name:
|
14
|
+
production: 'example-%{host}'
|
15
|
+
staging: 'example-staging-%{host}'
|
16
|
+
|
17
|
+
# filter out large lists by keyword
|
18
|
+
distributions_filter: debian
|
19
|
+
|
20
|
+
# dns takes "route53"
|
21
|
+
# dns: route53
|
22
|
+
|
23
|
+
# only used when route53 is chosen for DNS
|
24
|
+
route53:
|
25
|
+
key: your_aws_key
|
26
|
+
secret: your_aws_secret
|
@@ -12,11 +12,11 @@ environments:
|
|
12
12
|
- staging
|
13
13
|
fqdn:
|
14
14
|
zone: example.com
|
15
|
-
production: %{host}.example.com
|
16
|
-
staging: %{host}.staging.example.com
|
15
|
+
production: '%{host}.example.com'
|
16
|
+
staging: '%{host}.staging.example.com'
|
17
17
|
label:
|
18
|
-
production: example-%{host}
|
19
|
-
staging: example-staging-%{host}
|
18
|
+
production: 'example-%{host}'
|
19
|
+
staging: 'example-staging-%{host}'
|
20
20
|
group:
|
21
21
|
production: example
|
22
22
|
staging: example-staging
|
metadata
CHANGED
@@ -1,64 +1,64 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sunzi
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.8.0
|
5
4
|
prerelease:
|
5
|
+
version: 0.9.0
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
8
|
- Kenn Ejima
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2013-02-20 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
|
+
version_requirements: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ! '>='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
none: false
|
21
|
+
prerelease: false
|
15
22
|
name: thor
|
16
23
|
requirement: !ruby/object:Gem::Requirement
|
17
|
-
none: false
|
18
24
|
requirements:
|
19
25
|
- - ! '>='
|
20
26
|
- !ruby/object:Gem::Version
|
21
27
|
version: '0'
|
28
|
+
none: false
|
22
29
|
type: :runtime
|
23
|
-
|
30
|
+
- !ruby/object:Gem::Dependency
|
24
31
|
version_requirements: !ruby/object:Gem::Requirement
|
25
|
-
none: false
|
26
32
|
requirements:
|
27
33
|
- - ! '>='
|
28
34
|
- !ruby/object:Gem::Version
|
29
35
|
version: '0'
|
30
|
-
|
36
|
+
none: false
|
37
|
+
prerelease: false
|
31
38
|
name: rainbow
|
32
39
|
requirement: !ruby/object:Gem::Requirement
|
33
|
-
none: false
|
34
40
|
requirements:
|
35
41
|
- - ! '>='
|
36
42
|
- !ruby/object:Gem::Version
|
37
43
|
version: '0'
|
44
|
+
none: false
|
38
45
|
type: :runtime
|
39
|
-
|
46
|
+
- !ruby/object:Gem::Dependency
|
40
47
|
version_requirements: !ruby/object:Gem::Requirement
|
41
|
-
none: false
|
42
48
|
requirements:
|
43
49
|
- - ! '>='
|
44
50
|
- !ruby/object:Gem::Version
|
45
51
|
version: '0'
|
46
|
-
|
52
|
+
none: false
|
53
|
+
prerelease: false
|
47
54
|
name: rake
|
48
55
|
requirement: !ruby/object:Gem::Requirement
|
49
|
-
none: false
|
50
56
|
requirements:
|
51
57
|
- - ! '>='
|
52
58
|
- !ruby/object:Gem::Version
|
53
59
|
version: '0'
|
54
|
-
type: :development
|
55
|
-
prerelease: false
|
56
|
-
version_requirements: !ruby/object:Gem::Requirement
|
57
60
|
none: false
|
58
|
-
|
59
|
-
- - ! '>='
|
60
|
-
- !ruby/object:Gem::Version
|
61
|
-
version: '0'
|
61
|
+
type: :development
|
62
62
|
description: Server provisioning utility for minimalists
|
63
63
|
email:
|
64
64
|
- kenn.ejima@gmail.com
|
@@ -75,6 +75,7 @@ files:
|
|
75
75
|
- lib/sunzi.rb
|
76
76
|
- lib/sunzi/cli.rb
|
77
77
|
- lib/sunzi/cloud/base.rb
|
78
|
+
- lib/sunzi/cloud/digital_ocean.rb
|
78
79
|
- lib/sunzi/cloud/ec2.rb
|
79
80
|
- lib/sunzi/cloud/linode.rb
|
80
81
|
- lib/sunzi/dependency.rb
|
@@ -89,6 +90,7 @@ files:
|
|
89
90
|
- lib/templates/create/roles/db.sh
|
90
91
|
- lib/templates/create/roles/web.sh
|
91
92
|
- lib/templates/create/sunzi.yml
|
93
|
+
- lib/templates/setup/digital_ocean/digital_ocean.yml
|
92
94
|
- lib/templates/setup/linode/linode.yml
|
93
95
|
- sunzi.gemspec
|
94
96
|
- test/test_cli.rb
|
@@ -99,20 +101,26 @@ rdoc_options: []
|
|
99
101
|
require_paths:
|
100
102
|
- lib
|
101
103
|
required_ruby_version: !ruby/object:Gem::Requirement
|
102
|
-
none: false
|
103
104
|
requirements:
|
104
105
|
- - ! '>='
|
105
106
|
- !ruby/object:Gem::Version
|
106
107
|
version: '0'
|
107
|
-
|
108
|
+
segments:
|
109
|
+
- 0
|
110
|
+
hash: 3774298767527921378
|
108
111
|
none: false
|
112
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
109
113
|
requirements:
|
110
114
|
- - ! '>='
|
111
115
|
- !ruby/object:Gem::Version
|
112
116
|
version: '0'
|
117
|
+
segments:
|
118
|
+
- 0
|
119
|
+
hash: 3774298767527921378
|
120
|
+
none: false
|
113
121
|
requirements: []
|
114
122
|
rubyforge_project: sunzi
|
115
|
-
rubygems_version: 1.8.
|
123
|
+
rubygems_version: 1.8.24
|
116
124
|
signing_key:
|
117
125
|
specification_version: 3
|
118
126
|
summary: Server provisioning utility for minimalists
|