sunzi 0.4.0 → 0.4.1

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
@@ -5,13 +5,13 @@ Sunzi
5
5
  "The supreme art of war is to subdue the enemy without fighting." - Sunzi
6
6
  ```
7
7
 
8
- Sunzi is the easiest server provisioning utility designed for mere mortals. If Chef or Puppet is driving you nuts, try Sunzi!
8
+ Sunzi is the easiest [server provisioning](http://en.wikipedia.org/wiki/Provisioning#Server_provisioning) utility designed for mere mortals. If Chef or Puppet is driving you nuts, try Sunzi!
9
9
 
10
10
  Sunzi assumes that modern Linux distributions have (mostly) sane defaults and great package managers.
11
11
 
12
12
  Its design goals are:
13
13
 
14
- * **It's just shell script.** No clunky Ruby DSL involved. Sunzi recipes are written in a plain shell script. Why? Because, most of the information about server configuration on the web is written in shell commands. Just copy-paste them, why should you translate it into a proprietary, inconvenient DSL? Also, shell script is the greatest common denominator on minimum Linux installs.
14
+ * **It's just shell script.** No clunky Ruby DSL involved. Sunzi recipes are written in a plain shell script. Most of the information about server configuration on the web is written in shell commands. Just copy-paste them, rather than translate it into an arbitrary DSL. Also, Bash is the greatest common denominator on minimum Linux installs.
15
15
  * **Focus on diff from default.** No big-bang overwriting. Append or replace the smallest possible piece of data in a config file. Loads of custom configurations make it difficult to understand what you are really doing.
16
16
  * **Always use the root user.** Think twice before blindly assuming you need a regular user - it doesn't add any security benefit for server provisioning, it just adds extra verbosity for nothing. However, it doesn't mean that you shouldn't create regular users with Sunzi - feel free to write your own recipes.
17
17
  * **Minimum dependencies.** No configuration server required. You don't even need a Ruby runtime on the remote server.
@@ -21,26 +21,46 @@ Quickstart
21
21
 
22
22
  Install:
23
23
 
24
- $ gem install sunzi
24
+ ```bash
25
+ gem install sunzi
26
+ ```
25
27
 
26
28
  Go to your project directory, then:
27
29
 
28
- $ sunzi create
30
+ ```bash
31
+ sunzi create
32
+ ```
29
33
 
30
34
  It generates a `sunzi` folder along with subdirectories and templates. Inside `sunzi`, there's `sunzi.yml`, which defines dynamic attributes to be used from recipes. Also there's the `remote` folder, which will be transferred to the remote server, that contains recipes and dynamic variables compiled from `sunzi.yml`.
31
35
 
32
- Go into the `sunzi` directory, then run the `sunzi deploy`:
36
+ Go into the `sunzi` directory, then run `sunzi deploy`:
33
37
 
34
- $ cd sunzi
35
- $ sunzi deploy example.com
38
+ ```bash
39
+ cd sunzi
40
+ sunzi deploy example.com
41
+ ```
36
42
 
37
43
  Now, what it actually does is:
38
44
 
45
+ 1. Compile sunzi.yml to generate attributes and retrieve remote recipes
39
46
  1. SSH to `example.com` and login as `root`
40
47
  1. Transfer the content of the `remote` directory to the remote server and extract in `$HOME/sunzi`
41
48
  1. Run `install.sh` on the remote server
42
49
 
43
- As you can see, what you need to do is edit `install.sh` and add some shell commands. That's it.
50
+ As you can see, all you need to do is edit `install.sh` and add some shell commands. That's it.
51
+
52
+ A Sunzi project with no recipes is totally fine, so that you can start small, go big later.
53
+
54
+ Commands
55
+ --------
56
+
57
+ ```bash
58
+ sunzi # Show command help
59
+ sunzi create # Create a new Sunzi project
60
+ sunzi deploy # Deploy Sunzi project
61
+ sunzi setup # Setup a new VM on the Cloud services
62
+ sunzi teardown # Teardown an existing VM on the Cloud services
63
+ ```
44
64
 
45
65
  Directory structure
46
66
  -------------------
@@ -75,11 +95,12 @@ For instance, given a recipe `greeting.sh`:
75
95
  echo "Goodbye $1, Hello $2!"
76
96
  ```
77
97
 
78
- With `attributes.yml`:
98
+ With `sunzi.yml`:
79
99
 
80
100
  ```yaml
81
- goodbye: Chef
82
- hello: Sunzi
101
+ attributes:
102
+ goodbye: Chef
103
+ hello: Sunzi
83
104
  ```
84
105
 
85
106
  Then, include the recipe in `install.sh`:
@@ -111,12 +132,16 @@ recipes:
111
132
  Cloud Support
112
133
  -------------
113
134
 
114
- You can setup a new VM / teardown an existing VM interactively. Use `sunzi setup` and `sunzi teardown` for that.
135
+ You can setup a new VM, or teardown an existing VM interactively. Use `sunzi setup` and `sunzi teardown` for that.
115
136
 
116
137
  The following screenshot says it all.
117
138
 
118
139
  ![Sunzi for Linode](http://farm8.staticflickr.com/7210/6783789868_ab89010d5c.jpg)
119
140
 
141
+ Right now, only [Linode](http://www.linode.com/) is supported, but EC2 and Rackspace are coming.
142
+
143
+ For DNS, Linode and [Amazon Route 53](http://aws.amazon.com/route53/) are supported.
144
+
120
145
  Vagrant
121
146
  -------
122
147
 
@@ -144,4 +169,6 @@ and now run `vagrant up`, it will change the root password to `vagrant`.
144
169
 
145
170
  Also keep in mind that you need to specify the port number 2222.
146
171
 
147
- $ sunzi deploy localhost:2222
172
+ ```bash
173
+ sunzi deploy localhost:2222
174
+ ```
data/bin/sunzi CHANGED
@@ -1,4 +1,8 @@
1
1
  #!/usr/bin/env ruby
2
+
3
+ # Abort beautifully with ctrl+c.
2
4
  Signal.trap(:INT) { abort "\nAborting." }
5
+
6
+ # Load the main lib and invoke CLI.
3
7
  require File.expand_path('../../lib/sunzi',__FILE__)
4
8
  Sunzi::Cli.start
data/lib/sunzi.rb CHANGED
@@ -1,11 +1,14 @@
1
1
  LIB_PATH = File.join(File.dirname(__FILE__), 'sunzi')
2
2
 
3
3
  require 'thor'
4
+ require 'rainbow'
4
5
  require 'yaml'
5
6
 
6
7
  module Sunzi
7
8
  autoload :Cli, File.join(LIB_PATH, 'cli')
8
9
  autoload :Dependency, File.join(LIB_PATH, 'dependency')
10
+ autoload :Logger, File.join(LIB_PATH, 'logger')
11
+ autoload :Utility, File.join(LIB_PATH, 'utility')
9
12
  autoload :Version, File.join(LIB_PATH, 'version')
10
13
 
11
14
  module Cloud
data/lib/sunzi/cli.rb CHANGED
@@ -1,100 +1,106 @@
1
1
  require 'open3'
2
2
 
3
3
  module Sunzi
4
- CONFIG_DIR = File.join(ENV['HOME'],'.config','sunzi')
5
-
6
4
  class Cli < Thor
7
5
  include Thor::Actions
8
6
 
9
- class << self
10
- def source_root
11
- File.expand_path('../../',__FILE__)
12
- end
13
- end
14
-
15
- # map "c" => :create
16
- # map "d" => :deploy
17
-
18
7
  desc "create", "Create sunzi project"
19
8
  def create(project = 'sunzi')
20
- empty_directory project
21
- empty_directory "#{project}/remote"
22
- empty_directory "#{project}/remote/recipes"
23
- template "templates/create/sunzi.yml", "#{project}/sunzi.yml"
24
- template "templates/create/remote/install.sh", "#{project}/remote/install.sh"
25
- template "templates/create/remote/recipes/ssh_key.sh", "#{project}/remote/recipes/ssh_key.sh"
9
+ do_create(project)
26
10
  end
27
11
 
28
12
  desc "deploy example.com (or user@example.com:2222)", "Deploy sunzi project"
29
13
  def deploy(target)
30
- user, host, port = parse_target(target)
31
- endpoint = "#{user}@#{host}"
32
-
33
- # compile attributes and recipes
34
- compile
35
-
36
- # The host key might change when we instantiate a new VM, so
37
- # we remove (-R) the old host key from known_hosts.
38
- `ssh-keygen -R #{host} 2> /dev/null`
39
-
40
- commands = <<-EOS
41
- cd remote
42
- tar cz . | ssh -o 'StrictHostKeyChecking no' #{endpoint} -p #{port} '
43
- rm -rf ~/sunzi &&
44
- mkdir ~/sunzi &&
45
- cd ~/sunzi &&
46
- tar xz &&
47
- bash install.sh'
48
- EOS
49
-
50
- Open3.popen3(commands) do |stdin, stdout, stderr|
51
- stdin.close
52
- t = Thread.new(stderr) do |terr|
53
- while (line = terr.gets)
54
- print shell.set_color(line, :red, true)
55
- end
56
- end
57
- while (line = stdout.gets)
58
- print print shell.set_color(line, :green, true)
59
- end
60
- t.join
61
- end
14
+ do_deploy(target)
62
15
  end
63
16
 
64
17
  desc "compile", "Compile sunzi project"
65
18
  def compile
66
- # Check if you're in the sunzi directory
67
- unless File.exists?('sunzi.yml')
68
- say shell.set_color("You must be in the sunzi folder", :red, true)
69
- abort
70
- end
71
-
72
- # Load sunzi.yml
73
- hash = YAML.load(File.read('sunzi.yml'))
74
- empty_directory 'remote/attributes'
75
- empty_directory 'remote/recipes'
76
-
77
- # Compile attributes.yml
78
- hash['attributes'].each do |key, value|
79
- File.open("remote/attributes/#{key}", 'w'){|file| file.write(value) }
80
- end
81
- # Compile recipes.yml
82
- hash['recipes'].each do |key, value|
83
- get value, "remote/recipes/#{key}.sh"
84
- end
19
+ do_compile
85
20
  end
86
21
 
87
22
  desc "setup [linode|ec2]", "Setup a new VM"
88
23
  def setup(target)
89
- Cloud::Base.choose(target).setup
24
+ Cloud::Base.choose(self, target).setup
90
25
  end
91
26
 
92
27
  desc "teardown [linode|ec2] [name]", "Teardown an existing VM"
93
28
  def teardown(target, name)
94
- Cloud::Base.choose(target).teardown(name)
29
+ Cloud::Base.choose(self, target).teardown(name)
95
30
  end
96
31
 
97
32
  no_tasks do
33
+ include Sunzi::Utility
34
+
35
+ def self.source_root
36
+ File.expand_path('../../',__FILE__)
37
+ end
38
+
39
+ def do_create(project)
40
+ empty_directory project
41
+ empty_directory "#{project}/remote"
42
+ empty_directory "#{project}/remote/recipes"
43
+ template "templates/create/sunzi.yml", "#{project}/sunzi.yml"
44
+ template "templates/create/remote/install.sh", "#{project}/remote/install.sh"
45
+ template "templates/create/remote/recipes/ssh_key.sh", "#{project}/remote/recipes/ssh_key.sh"
46
+ end
47
+
48
+ def do_deploy(target)
49
+ user, host, port = parse_target(target)
50
+ endpoint = "#{user}@#{host}"
51
+
52
+ # compile attributes and recipes
53
+ compile
54
+
55
+ # The host key might change when we instantiate a new VM, so
56
+ # we remove (-R) the old host key from known_hosts.
57
+ `ssh-keygen -R #{host} 2> /dev/null`
58
+
59
+ commands = <<-EOS
60
+ cd remote
61
+ tar cz . | ssh -o 'StrictHostKeyChecking no' #{endpoint} -p #{port} '
62
+ rm -rf ~/sunzi &&
63
+ mkdir ~/sunzi &&
64
+ cd ~/sunzi &&
65
+ tar xz &&
66
+ bash install.sh'
67
+ EOS
68
+
69
+ Open3.popen3(commands) do |stdin, stdout, stderr|
70
+ stdin.close
71
+ t = Thread.new(stderr) do |terr|
72
+ while (line = terr.gets)
73
+ print shell.set_color(line, :red, true)
74
+ end
75
+ end
76
+ while (line = stdout.gets)
77
+ print print shell.set_color(line, :green, true)
78
+ end
79
+ t.join
80
+ end
81
+ end
82
+
83
+ def do_compile
84
+ # Check if you're in the sunzi directory
85
+ unless File.exists?('sunzi.yml')
86
+ abort_with "You must be in the sunzi folder"
87
+ end
88
+
89
+ # Load sunzi.yml
90
+ hash = YAML.load(File.read('sunzi.yml'))
91
+ empty_directory 'remote/attributes'
92
+ empty_directory 'remote/recipes'
93
+
94
+ # Compile attributes.yml
95
+ hash['attributes'].each do |key, value|
96
+ File.open("remote/attributes/#{key}", 'w'){|file| file.write(value) }
97
+ end
98
+ # Compile recipes.yml
99
+ hash['recipes'].each do |key, value|
100
+ get value, "remote/recipes/#{key}.sh"
101
+ end
102
+ end
103
+
98
104
  def parse_target(target)
99
105
  target.match(/(.*@)?(.*?)(:.*)?$/)
100
106
  [ ($1 && $1.delete('@') || 'root'), $2, ($3 && $3.delete(':') || '22') ]
@@ -2,36 +2,27 @@ Sunzi::Dependency.load('highline')
2
2
 
3
3
  module Sunzi
4
4
  module Cloud
5
- class Base < Thor
6
- include Thor::Actions
5
+ class Base
6
+ include Sunzi::Utility
7
7
 
8
- class << self
9
- def source_root
10
- File.expand_path('../../../',__FILE__)
11
- end
12
-
13
- def choose(target)
14
- case target
15
- when 'linode'
16
- Cloud::Linode.new
17
- when 'ec2'
18
- Cloud::EC2.new
19
- else
20
- say shell.set_color("#{target} is not valid!", :red, true)
21
- abort
22
- end
8
+ def self.choose(cli, target)
9
+ case target
10
+ when 'linode'
11
+ Cloud::Linode.new(cli)
12
+ when 'ec2'
13
+ Cloud::EC2.new(cli)
14
+ else
15
+ abort_with "#{target} is not valid!"
23
16
  end
24
17
  end
25
18
 
26
- def initialize(*args)
19
+ def initialize(cli)
20
+ @cli = cli
27
21
  @ui = HighLine.new
28
- super
29
22
  end
30
23
 
31
- no_tasks do
32
- def ask(question, answer_type, &details)
33
- @ui.ask(@ui.color(question, :green, :bold), answer_type, &details)
34
- end
24
+ def ask(question, answer_type, &details)
25
+ @ui.ask(@ui.color(question, :green, :bold), answer_type, &details)
35
26
  end
36
27
  end
37
28
  end
@@ -1,14 +1,12 @@
1
1
  module Sunzi
2
2
  module Cloud
3
3
  class EC2 < Base
4
- no_tasks do
5
- def setup
6
- say shell.set_color('EC2 is not implemented yet!', :red, true)
7
- end
4
+ def setup
5
+ Logger.error 'EC2 is not implemented yet!'
6
+ end
8
7
 
9
- def teardown(target)
10
- say shell.set_color('EC2 is not implemented yet!', :red, true)
11
- end
8
+ def teardown(target)
9
+ Logger.error 'EC2 is not implemented yet!'
12
10
  end
13
11
  end
14
12
  end
@@ -6,243 +6,236 @@ YAML::ENGINE.yamler = 'syck'
6
6
  module Sunzi
7
7
  module Cloud
8
8
  class Linode < Base
9
- no_tasks do
10
-
11
- def setup
12
- # Only run for the first time
13
- unless File.exist? 'linode/linode.yml'
14
- empty_directory 'linode'
15
- empty_directory 'linode/instances'
16
- template 'templates/setup/linode/linode.yml', 'linode/linode.yml'
17
- say shell.set_color('Now go ahead and edit linode.yml, then run this command again!', :green, true)
18
- abort
19
- end
20
-
21
- @config = YAML.load(File.read('linode/linode.yml'))
22
-
23
- if @config['fqdn']['zone'] == 'example.com'
24
- say shell.set_color('You must have your own settings in linode.yml', :red, true)
25
- abort
26
- end
27
-
28
- # When route53 is specified for DNS, check if it's properly configured and if not, fail earlier.
29
- setup_route53 if @config['dns'] == 'route53'
30
-
31
- @ui = HighLine.new
32
-
33
- @sshkey = File.read(File.expand_path(@config['root_sshkey_path'])).chomp
34
- if @sshkey.match(/\n/)
35
- say shell.set_color("RootSSHKey #{@sshkey.inspect} must not be multi-line! Check inside \"#{@config['root_sshkey_path']}\"", :red, true)
36
- abort
37
- end
38
-
39
- # Ask environment and hostname
40
- @env = ask("environment? (#{@config['environments'].join(' / ')}): ", String) {|q| q.in = @config['environments'] }.to_s
41
- @host = ask('hostname? (only the last part of subdomain): ', String).to_s
42
-
43
- @fqdn = @config['fqdn'][@env].gsub(/%{host}/, @host)
44
- @label = @config['label'][@env].gsub(/%{host}/, @host)
45
- @group = @config['group'][@env]
46
- @api = ::Linode.new(:api_key => @config['api_key'])
47
-
48
- # Choose a plan
49
- result = @api.avail.linodeplans
50
- result.each{|i| say "#{i.planid}: #{i.ram}MB, $#{i.price}" }
51
- @planid = ask('which plan?: ', Integer) {|q| q.in = result.map(&:planid); q.default = result.first.planid }
52
- @plan_label = result.find{|i| i.planid == @planid }.label
53
-
54
- # Choose a datacenter
55
- result = @api.avail.datacenters
56
- result.each{|i| say "#{i.datacenterid}: #{i.location}" }
57
- @datacenterid = ask('which datacenter?: ', Integer) {|q| q.in = result.map(&:datacenterid); q.default = result.first.datacenterid }
58
- @datacenter_location = result.find{|i| i.datacenterid == @datacenterid }.location
59
-
60
- # Choose a distribution
61
- result = @api.avail.distributions
62
- if @config['distributions_filter']
63
- result = result.select{|i| i.label.match Regexp.new(@config['distributions_filter'], Regexp::IGNORECASE) }
64
- end
65
- result.each{|i| say "#{i.distributionid}: #{i.label}" }
66
- @distributionid = ask('which distribution?: ', Integer) {|q| q.in = result.map(&:distributionid); q.default = result.first.distributionid }
67
- @distribution_label = result.find{|i| i.distributionid == @distributionid }.label
68
-
69
- # Choose a kernel
70
- result = @api.avail.kernels
71
- if @config['kernels_filter']
72
- result = result.select{|i| i.label.match Regexp.new(@config['kernels_filter'], Regexp::IGNORECASE) }
73
- end
74
- result.each{|i| say "#{i.kernelid}: #{i.label}" }
75
- @kernelid = ask('which kernel?: ', Integer) {|q| q.in = result.map(&:kernelid); q.default = result.first.kernelid }
76
- @kernel_label = result.find{|i| i.kernelid == @kernelid }.label
77
-
78
- # Choose swap size
79
- @swap_size = ask('swap size in MB? (default: 256MB): ', Integer) { |q| q.default = 256 }
80
-
81
- # Go ahead?
82
- moveon = ask("Are you sure to go ahead and create #{@fqdn}? (y/n) ", String) {|q| q.in = ['y','n']}
83
- exit unless moveon == 'y'
84
-
85
- # Create
86
- say "creating a new linode..."
87
- result = @api.linode.create(:DatacenterID => @datacenterid, :PlanID => @planid, :PaymentTerm => @config['payment_term'])
88
- @linodeid = result.linodeid
89
- say "created a new instance: linodeid = #{@linodeid}"
90
-
91
- result = @api.linode.list.select{|i| i.linodeid == @linodeid }.first
92
- @totalhd = result.totalhd
93
-
94
- # Update settings
95
- say "Updating settings..."
96
- result = @api.linode.update(
97
- :LinodeID => @linodeid,
98
- :Label => @label,
99
- :lpm_displayGroup => @group,
100
- # :Alert_cpu_threshold => 90,
101
- # :Alert_diskio_threshold => 1000,
102
- # :Alert_bwin_threshold => 5,
103
- # :Alert_bwout_threshold => 5,
104
- # :Alert_bwquota_threshold => 80,
105
- )
106
-
107
- # Create a root disk
108
- say "Creating a root disk..."
109
- result = @api.linode.disk.createfromdistribution(
110
- :LinodeID => @linodeid,
111
- :DistributionID => @distributionid,
112
- :Label => "#{@distribution_label} Image",
113
- :Size => @totalhd - @swap_size,
114
- :rootPass => @config['root_pass'],
115
- :rootSSHKey => @sshkey
116
- )
117
- @root_diskid = result.diskid
118
-
119
- # Create a swap disk
120
- say "Creating a swap disk..."
121
- result = @api.linode.disk.create(
122
- :LinodeID => @linodeid,
123
- :Label => "#{@swap_size}MB Swap Image",
124
- :Type => 'swap',
125
- :Size => @swap_size
126
- )
127
- @swap_diskid = result.diskid
128
-
129
- # Create a config profiile
130
- say "Creating a config profile..."
131
- result = @api.linode.config.create(
132
- :LinodeID => @linodeid,
133
- :KernelID => @kernelid,
134
- :Label => "#{@distribution_label} Profile",
135
- :DiskList => [ @root_diskid, @swap_diskid ].join(',')
136
- )
137
- @config_id = result.configid
138
-
139
- # Add a private IP
140
- say "Adding a private IP..."
141
- result = @api.linode.ip.list(:LinodeID => @linodeid)
142
- @public_ip = result.first.ipaddress
143
- result = @api.linode.ip.addprivate(:LinodeID => @linodeid)
144
- result = @api.linode.ip.list(:LinodeID => @linodeid).find{|i| i.ispublic == 0 }
145
- @private_ip = result.ipaddress
146
-
147
- # Register IP to DNS
148
- case @config['dns']
149
- when 'linode'
150
- # Set the public IP to Linode DNS Manager
151
- say "Setting the public IP to Linode DNS Manager..."
152
- @domainid = @api.domain.list.find{|i| i.domain == @config['fqdn']['zone'] }.domainid
153
- @api.domain.resource.create(:DomainID => @domainid, :Type => 'A', :Name => @fqdn, :Target => @public_ip)
154
- when 'route53'
155
- # Set the public IP to AWS Route 53
156
- say "Setting the public IP to AWS Route 53..."
157
- Route53::DNSRecord.new(@fqdn, "A", "300", [@public_ip], @route53_zone).create
158
- end
159
-
160
- # Boot
161
- say shell.set_color("Done. Booting...", :green, true)
162
- @api.linode.boot(:LinodeID => @linodeid)
163
-
164
- hash = {
165
- :linode_id => @linodeid,
166
- :env => @env,
167
- :host => @host,
168
- :fqdn => @fqdn,
169
- :label => @label,
170
- :group => @group,
171
- :plan_id => @planid,
172
- :datacenter_id => @datacenterid,
173
- :datacenter_location => @datacenter_location,
174
- :distribution_id => @distributionid,
175
- :distribution_label => @distribution_label,
176
- :kernel_id => @kernelid,
177
- :kernel_label => @kernel_label,
178
- :swap_size => @swap_size,
179
- :totalhd => @totalhd,
180
- :root_diskid => @root_diskid,
181
- :swap_diskid => @swap_diskid,
182
- :config_id => @config_id,
183
- :public_ip => @public_ip,
184
- :private_ip => @private_ip,
185
- }
186
-
187
- File.open("linode/instances/#{@label}.yml",'w') do |file|
188
- file.write YAML.dump(hash)
189
- end
9
+ def setup
10
+ # Only run for the first time
11
+ unless File.exist? 'linode/linode.yml'
12
+ @cli.empty_directory 'linode'
13
+ @cli.empty_directory 'linode/instances'
14
+ @cli.template 'templates/setup/linode/linode.yml', 'linode/linode.yml'
15
+ exit_with 'Now go ahead and edit linode.yml, then run this command again!'
190
16
  end
191
17
 
192
- def teardown(name)
193
- unless File.exist?("linode/instances/#{name}.yml")
194
- say shell.set_color("#{name}.yml was not found in the instances directory.", :red, true)
195
- abort
196
- end
197
- @config = YAML.load(File.read('linode/linode.yml'))
198
- setup_route53 if @config['dns'] == 'route53'
199
-
200
- @instance = YAML.load(File.read("linode/instances/#{name}.yml"))
201
- @api = ::Linode.new(:api_key => @config['api_key'])
202
-
203
- # Shutdown first or disk deletion will fail
204
- say shell.set_color("shutting down...", :green, true)
205
- @api.linode.shutdown(:LinodeID => @instance[:linode_id])
206
- sleep 10
207
-
208
- # Delete the disks. It is required - http://www.linode.com/api/linode/linode%2Edelete
209
- say shell.set_color("deleting root disk...", :green, true)
210
- @api.linode.disk.delete(:LinodeID => @instance[:linode_id], :DiskID => @instance[:root_diskid]) rescue nil
211
- say shell.set_color("deleting swap disk...", :green, true)
212
- @api.linode.disk.delete(:LinodeID => @instance[:linode_id], :DiskID => @instance[:swap_diskid]) rescue nil
213
- sleep 5
214
-
215
- # Delete the instance
216
- say shell.set_color("deleting linode...", :green, true)
217
- @api.linode.delete(:LinodeID => @instance[:linode_id])
218
-
219
- # Delete DNS record
220
- case @config['dns']
221
- when 'linode'
222
- # Set the public IP to Linode DNS Manager
223
- say "deleting the public IP to Linode DNS Manager..."
224
- @domainid = @api.domain.list.find{|i| i.domain == @config['fqdn']['zone'] }.domainid
225
- @resource = @api.domain.resource.list(:DomainID => @domainid).find{|i| i.target == @instance[:public_ip] }
226
- @api.domain.resource.delete(:DomainID => @domainid, :ResourceID => @resource.resourceid)
227
- when 'route53'
228
- # Set the public IP to AWS Route 53
229
- say "deleting the public IP to AWS Route 53..."
230
- @record = @route53_zone.get_records.find{|i| i.values.first == @instance[:public_ip] }
231
- @record.delete
232
- end
233
-
234
- # Remove the instance config file
235
- remove_file "linode/instances/#{name}.yml"
236
-
237
- say shell.set_color("Done.", :green, true)
18
+ @config = YAML.load(File.read('linode/linode.yml'))
19
+
20
+ if @config['fqdn']['zone'] == 'example.com'
21
+ abort_with 'You must have your own settings in linode.yml'
22
+ end
23
+
24
+ # When route53 is specified for DNS, check if it's properly configured and if not, fail earlier.
25
+ setup_route53 if @config['dns'] == 'route53'
26
+
27
+ @ui = HighLine.new
28
+
29
+ @sshkey = File.read(File.expand_path(@config['root_sshkey_path'])).chomp
30
+ if @sshkey.match(/\n/)
31
+ abort_with "RootSSHKey #{@sshkey.inspect} must not be multi-line! Check inside \"#{@config['root_sshkey_path']}\""
32
+ end
33
+
34
+ # Ask environment and hostname
35
+ @env = ask("environment? (#{@config['environments'].join(' / ')}): ", String) {|q| q.in = @config['environments'] }.to_s
36
+ @host = ask('hostname? (only the last part of subdomain): ', String).to_s
37
+
38
+ @fqdn = @config['fqdn'][@env].gsub(/%{host}/, @host)
39
+ @label = @config['label'][@env].gsub(/%{host}/, @host)
40
+ @group = @config['group'][@env]
41
+ @api = ::Linode.new(:api_key => @config['api_key'])
42
+
43
+ # Choose a plan
44
+ result = @api.avail.linodeplans
45
+ result.each{|i| say "#{i.planid}: #{i.ram}MB, $#{i.price}" }
46
+ @planid = ask('which plan?: ', Integer) {|q| q.in = result.map(&:planid); q.default = result.first.planid }
47
+ @plan_label = result.find{|i| i.planid == @planid }.label
48
+
49
+ # Choose a datacenter
50
+ result = @api.avail.datacenters
51
+ result.each{|i| say "#{i.datacenterid}: #{i.location}" }
52
+ @datacenterid = ask('which datacenter?: ', Integer) {|q| q.in = result.map(&:datacenterid); q.default = result.first.datacenterid }
53
+ @datacenter_location = result.find{|i| i.datacenterid == @datacenterid }.location
54
+
55
+ # Choose a distribution
56
+ result = @api.avail.distributions
57
+ if @config['distributions_filter']
58
+ result = result.select{|i| i.label.match Regexp.new(@config['distributions_filter'], Regexp::IGNORECASE) }
59
+ end
60
+ result.each{|i| say "#{i.distributionid}: #{i.label}" }
61
+ @distributionid = ask('which distribution?: ', Integer) {|q| q.in = result.map(&:distributionid); q.default = result.first.distributionid }
62
+ @distribution_label = result.find{|i| i.distributionid == @distributionid }.label
63
+
64
+ # Choose a kernel
65
+ result = @api.avail.kernels
66
+ if @config['kernels_filter']
67
+ result = result.select{|i| i.label.match Regexp.new(@config['kernels_filter'], Regexp::IGNORECASE) }
68
+ end
69
+ result.each{|i| say "#{i.kernelid}: #{i.label}" }
70
+ @kernelid = ask('which kernel?: ', Integer) {|q| q.in = result.map(&:kernelid); q.default = result.first.kernelid }
71
+ @kernel_label = result.find{|i| i.kernelid == @kernelid }.label
72
+
73
+ # Choose swap size
74
+ @swap_size = ask('swap size in MB? (default: 256MB): ', Integer) { |q| q.default = 256 }
75
+
76
+ # Go ahead?
77
+ moveon = ask("Are you sure to go ahead and create #{@fqdn}? (y/n) ", String) {|q| q.in = ['y','n']}
78
+ exit unless moveon == 'y'
79
+
80
+ # Create
81
+ say "creating a new linode..."
82
+ result = @api.linode.create(:DatacenterID => @datacenterid, :PlanID => @planid, :PaymentTerm => @config['payment_term'])
83
+ @linodeid = result.linodeid
84
+ say "created a new instance: linodeid = #{@linodeid}"
85
+
86
+ result = @api.linode.list.select{|i| i.linodeid == @linodeid }.first
87
+ @totalhd = result.totalhd
88
+
89
+ # Update settings
90
+ say "Updating settings..."
91
+ result = @api.linode.update(
92
+ :LinodeID => @linodeid,
93
+ :Label => @label,
94
+ :lpm_displayGroup => @group,
95
+ # :Alert_cpu_threshold => 90,
96
+ # :Alert_diskio_threshold => 1000,
97
+ # :Alert_bwin_threshold => 5,
98
+ # :Alert_bwout_threshold => 5,
99
+ # :Alert_bwquota_threshold => 80,
100
+ )
101
+
102
+ # Create a root disk
103
+ say "Creating a root disk..."
104
+ result = @api.linode.disk.createfromdistribution(
105
+ :LinodeID => @linodeid,
106
+ :DistributionID => @distributionid,
107
+ :Label => "#{@distribution_label} Image",
108
+ :Size => @totalhd - @swap_size,
109
+ :rootPass => @config['root_pass'],
110
+ :rootSSHKey => @sshkey
111
+ )
112
+ @root_diskid = result.diskid
113
+
114
+ # Create a swap disk
115
+ say "Creating a swap disk..."
116
+ result = @api.linode.disk.create(
117
+ :LinodeID => @linodeid,
118
+ :Label => "#{@swap_size}MB Swap Image",
119
+ :Type => 'swap',
120
+ :Size => @swap_size
121
+ )
122
+ @swap_diskid = result.diskid
123
+
124
+ # Create a config profiile
125
+ say "Creating a config profile..."
126
+ result = @api.linode.config.create(
127
+ :LinodeID => @linodeid,
128
+ :KernelID => @kernelid,
129
+ :Label => "#{@distribution_label} Profile",
130
+ :DiskList => [ @root_diskid, @swap_diskid ].join(',')
131
+ )
132
+ @config_id = result.configid
133
+
134
+ # Add a private IP
135
+ say "Adding a private IP..."
136
+ result = @api.linode.ip.list(:LinodeID => @linodeid)
137
+ @public_ip = result.first.ipaddress
138
+ result = @api.linode.ip.addprivate(:LinodeID => @linodeid)
139
+ result = @api.linode.ip.list(:LinodeID => @linodeid).find{|i| i.ispublic == 0 }
140
+ @private_ip = result.ipaddress
141
+
142
+ # Register IP to DNS
143
+ case @config['dns']
144
+ when 'linode'
145
+ # Set the public IP to Linode DNS Manager
146
+ say "Setting the public IP to Linode DNS Manager..."
147
+ @domainid = @api.domain.list.find{|i| i.domain == @config['fqdn']['zone'] }.domainid
148
+ @api.domain.resource.create(:DomainID => @domainid, :Type => 'A', :Name => @fqdn, :Target => @public_ip)
149
+ when 'route53'
150
+ # Set the public IP to AWS Route 53
151
+ say "Setting the public IP to AWS Route 53..."
152
+ Route53::DNSRecord.new(@fqdn, "A", "300", [@public_ip], @route53_zone).create
238
153
  end
239
154
 
240
- def setup_route53
241
- Sunzi::Dependency.load('route53')
242
- route53 = Route53::Connection.new(@config['route53']['key'], @config['route53']['secret'])
243
- @route53_zone = route53.get_zones.find{|i| i.name.sub(/\.$/,'') == @config['fqdn']['zone'] }
155
+ # Boot
156
+ say 'Done. Booting...'
157
+ @api.linode.boot(:LinodeID => @linodeid)
158
+
159
+ hash = {
160
+ :linode_id => @linodeid,
161
+ :env => @env,
162
+ :host => @host,
163
+ :fqdn => @fqdn,
164
+ :label => @label,
165
+ :group => @group,
166
+ :plan_id => @planid,
167
+ :datacenter_id => @datacenterid,
168
+ :datacenter_location => @datacenter_location,
169
+ :distribution_id => @distributionid,
170
+ :distribution_label => @distribution_label,
171
+ :kernel_id => @kernelid,
172
+ :kernel_label => @kernel_label,
173
+ :swap_size => @swap_size,
174
+ :totalhd => @totalhd,
175
+ :root_diskid => @root_diskid,
176
+ :swap_diskid => @swap_diskid,
177
+ :config_id => @config_id,
178
+ :public_ip => @public_ip,
179
+ :private_ip => @private_ip,
180
+ }
181
+
182
+ File.open("linode/instances/#{@label}.yml",'w') do |file|
183
+ file.write YAML.dump(hash)
244
184
  end
245
185
  end
186
+
187
+ def teardown(name)
188
+ unless File.exist?("linode/instances/#{name}.yml")
189
+ abort_with "#{name}.yml was not found in the instances directory."
190
+ end
191
+ @config = YAML.load(File.read('linode/linode.yml'))
192
+ setup_route53 if @config['dns'] == 'route53'
193
+
194
+ @instance = YAML.load(File.read("linode/instances/#{name}.yml"))
195
+ @api = ::Linode.new(:api_key => @config['api_key'])
196
+
197
+ # Shutdown first or disk deletion will fail
198
+ say 'shutting down...'
199
+ @api.linode.shutdown(:LinodeID => @instance[:linode_id])
200
+ sleep 10
201
+
202
+ # Delete the disks. It is required - http://www.linode.com/api/linode/linode%2Edelete
203
+ say 'deleting root disk...'
204
+ @api.linode.disk.delete(:LinodeID => @instance[:linode_id], :DiskID => @instance[:root_diskid]) rescue nil
205
+ say 'deleting swap disk...'
206
+ @api.linode.disk.delete(:LinodeID => @instance[:linode_id], :DiskID => @instance[:swap_diskid]) rescue nil
207
+ sleep 5
208
+
209
+ # Delete the instance
210
+ say 'deleting linode...'
211
+ @api.linode.delete(:LinodeID => @instance[:linode_id])
212
+
213
+ # Delete DNS record
214
+ case @config['dns']
215
+ when 'linode'
216
+ # Set the public IP to Linode DNS Manager
217
+ say "deleting the public IP to Linode DNS Manager..."
218
+ @domainid = @api.domain.list.find{|i| i.domain == @config['fqdn']['zone'] }.domainid
219
+ @resource = @api.domain.resource.list(:DomainID => @domainid).find{|i| i.target == @instance[:public_ip] }
220
+ @api.domain.resource.delete(:DomainID => @domainid, :ResourceID => @resource.resourceid)
221
+ when 'route53'
222
+ # Set the public IP to AWS Route 53
223
+ say "deleting the public IP to AWS Route 53..."
224
+ @record = @route53_zone.get_records.find{|i| i.values.first == @instance[:public_ip] }
225
+ @record.delete if @record
226
+ end
227
+
228
+ # Remove the instance config file
229
+ @cli.remove_file "linode/instances/#{name}.yml"
230
+
231
+ say 'Done.'
232
+ end
233
+
234
+ def setup_route53
235
+ Sunzi::Dependency.load('route53')
236
+ route53 = Route53::Connection.new(@config['route53']['key'], @config['route53']['secret'])
237
+ @route53_zone = route53.get_zones.find{|i| i.name.sub(/\.$/,'') == @config['fqdn']['zone'] }
238
+ end
246
239
  end
247
240
  end
248
241
  end
@@ -2,18 +2,9 @@ module Sunzi
2
2
  class Dependency
3
3
  def self.all
4
4
  {
5
- 'linode' => {
6
- :require => 'linode',
7
- :version => '>= 0.7.7',
8
- },
9
- 'highline' => {
10
- :require => 'highline',
11
- :version => '>= 1.6.11',
12
- },
13
- 'route53' => {
14
- :require => 'route53',
15
- :version => '>= 0.2.1',
16
- },
5
+ 'linode' => { :require => 'linode', :version => '>= 0.7.7' },
6
+ 'highline' => { :require => 'highline', :version => '>= 1.6.11'},
7
+ 'route53' => { :require => 'route53', :version => '>= 0.2.1' },
17
8
  }
18
9
  end
19
10
 
@@ -22,7 +13,7 @@ module Sunzi
22
13
  gem(name, all[name][:version])
23
14
  require(all[name][:require])
24
15
  rescue LoadError
25
- puts <<-EOS
16
+ Logger.error <<-EOS
26
17
  Dependency missing: #{name}
27
18
  To install the gem, issue the following command:
28
19
 
@@ -30,9 +21,8 @@ To install the gem, issue the following command:
30
21
 
31
22
  Please try again after installing the missing dependency.
32
23
  EOS
33
- exit 1
24
+ abort
34
25
  end
35
26
  end
36
-
37
27
  end
38
28
  end
@@ -0,0 +1,17 @@
1
+ module Sunzi
2
+ module Logger
3
+ class << self
4
+ def info(text)
5
+ puts text.bright
6
+ end
7
+
8
+ def success(text)
9
+ puts text.color(:green).bright
10
+ end
11
+
12
+ def error(text)
13
+ puts text.color(:red).bright
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ module Sunzi
2
+ module Utility
3
+ def abort_with(text)
4
+ Logger.error text
5
+ abort
6
+ end
7
+
8
+ def exit_with(text)
9
+ Logger.success text
10
+ exit
11
+ end
12
+
13
+ def say(text)
14
+ Logger.info text
15
+ end
16
+ end
17
+ end
data/lib/sunzi/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Sunzi
2
- VERSION = "0.4.0"
2
+ VERSION = "0.4.1"
3
3
  end
data/sunzi.gemspec CHANGED
@@ -20,4 +20,5 @@ Gem::Specification.new do |s|
20
20
 
21
21
  # s.add_development_dependency "rspec"
22
22
  s.add_runtime_dependency "thor"
23
+ s.add_runtime_dependency "rainbow"
23
24
  end
data/test/test_cli.rb CHANGED
@@ -13,4 +13,10 @@ class TestCli < Test::Unit::TestCase
13
13
  assert_equal ['root', 'example.com', '22'], @cli.parse_target('example.com')
14
14
  assert_equal ['root', '192.168.0.1', '22'], @cli.parse_target('192.168.0.1')
15
15
  end
16
+
17
+ def test_create
18
+ @cli.create 'sandbox'
19
+ assert File.exist?('sandbox/sunzi.yml')
20
+ FileUtils.rm_rf 'sandbox'
21
+ end
16
22
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sunzi
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.4.1
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-02-26 00:00:00.000000000 Z
12
+ date: 2012-02-28 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: thor
16
- requirement: &2156460860 !ruby/object:Gem::Requirement
16
+ requirement: &2164966260 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
@@ -21,7 +21,18 @@ dependencies:
21
21
  version: '0'
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *2156460860
24
+ version_requirements: *2164966260
25
+ - !ruby/object:Gem::Dependency
26
+ name: rainbow
27
+ requirement: &2164965000 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *2164965000
25
36
  description: Server provisioning utility for minimalists
26
37
  email:
27
38
  - kenn.ejima@gmail.com
@@ -41,6 +52,8 @@ files:
41
52
  - lib/sunzi/cloud/ec2.rb
42
53
  - lib/sunzi/cloud/linode.rb
43
54
  - lib/sunzi/dependency.rb
55
+ - lib/sunzi/logger.rb
56
+ - lib/sunzi/utility.rb
44
57
  - lib/sunzi/version.rb
45
58
  - lib/templates/create/remote/install.sh
46
59
  - lib/templates/create/remote/recipes/ssh_key.sh