sunzi 0.4.0 → 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
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