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