sunzi 0.8.0 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
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|ec2] # Setup a new VM on the Cloud services
71
- $ sunzi teardown [linode|ec2] [name] # Teardown an existing VM on the Cloud services
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, 'sunzi/cloud/base'
14
- autoload :Linode, 'sunzi/cloud/linode'
15
- autoload :EC2, 'sunzi/cloud/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 "create", "Create sunzi project"
7
+ desc 'create', 'Create sunzi project'
8
8
  def create(project = 'sunzi')
9
9
  do_create(project)
10
10
  end
11
11
 
12
- desc "deploy [user@host:port] [role] [--sudo]", "Deploy sunzi project"
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 "compile", "Compile sunzi project"
18
+ desc 'compile', 'Compile sunzi project'
19
19
  def compile(role = nil)
20
20
  do_compile(role)
21
21
  end
22
22
 
23
- desc "setup [linode|ec2]", "Setup a new VM"
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 "teardown [linode|ec2] [name]", "Teardown an existing VM"
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 "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"
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 "You must be in the sunzi folder" unless File.exists?('sunzi.yml')
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("install.sh") << "\n" << File.binread("roles/#{role}.sh")
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
@@ -11,6 +11,8 @@ module Sunzi
11
11
  Cloud::Linode.new(cli)
12
12
  when 'ec2'
13
13
  Cloud::EC2.new(cli)
14
+ when 'digital_ocean'
15
+ Cloud::DigitalOcean.new(cli)
14
16
  else
15
17
  abort_with "#{target} is not valid!"
16
18
  end
@@ -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
@@ -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 last part of subdomain): ', String).to_s
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
- # Set the public IP to Linode DNS Manager
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
- # Set the public IP to AWS Route 53
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)
@@ -2,9 +2,10 @@ module Sunzi
2
2
  class Dependency
3
3
  def self.all
4
4
  {
5
- 'linode' => { :require => 'linode', :version => '>= 0.7.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
@@ -1,3 +1,3 @@
1
1
  module Sunzi
2
- VERSION = "0.8.0"
2
+ VERSION = "0.9.0"
3
3
  end
@@ -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: 2012-03-27 00:00:00.000000000 Z
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
- prerelease: false
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
- - !ruby/object:Gem::Dependency
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
- prerelease: false
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
- - !ruby/object:Gem::Dependency
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
- requirements:
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
- required_rubygems_version: !ruby/object:Gem::Requirement
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.19
123
+ rubygems_version: 1.8.24
116
124
  signing_key:
117
125
  specification_version: 3
118
126
  summary: Server provisioning utility for minimalists