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 +40 -13
- data/bin/sunzi +4 -0
- data/lib/sunzi.rb +3 -0
- data/lib/sunzi/cli.rb +76 -70
- data/lib/sunzi/cloud/base.rb +14 -23
- data/lib/sunzi/cloud/ec2.rb +5 -7
- data/lib/sunzi/cloud/linode.rb +224 -231
- data/lib/sunzi/dependency.rb +5 -15
- data/lib/sunzi/logger.rb +17 -0
- data/lib/sunzi/utility.rb +17 -0
- data/lib/sunzi/version.rb +1 -1
- data/sunzi.gemspec +1 -0
- data/test/test_cli.rb +6 -0
- metadata +17 -4
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.
|
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
|
-
|
24
|
+
```bash
|
25
|
+
gem install sunzi
|
26
|
+
```
|
25
27
|
|
26
28
|
Go to your project directory, then:
|
27
29
|
|
28
|
-
|
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
|
36
|
+
Go into the `sunzi` directory, then run `sunzi deploy`:
|
33
37
|
|
34
|
-
|
35
|
-
|
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,
|
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 `
|
98
|
+
With `sunzi.yml`:
|
79
99
|
|
80
100
|
```yaml
|
81
|
-
|
82
|
-
|
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
|
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
|
-
|
172
|
+
```bash
|
173
|
+
sunzi deploy localhost:2222
|
174
|
+
```
|
data/bin/sunzi
CHANGED
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
|
-
|
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
|
-
|
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
|
-
|
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') ]
|
data/lib/sunzi/cloud/base.rb
CHANGED
@@ -2,36 +2,27 @@ Sunzi::Dependency.load('highline')
|
|
2
2
|
|
3
3
|
module Sunzi
|
4
4
|
module Cloud
|
5
|
-
class Base
|
6
|
-
include
|
5
|
+
class Base
|
6
|
+
include Sunzi::Utility
|
7
7
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
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(
|
19
|
+
def initialize(cli)
|
20
|
+
@cli = cli
|
27
21
|
@ui = HighLine.new
|
28
|
-
super
|
29
22
|
end
|
30
23
|
|
31
|
-
|
32
|
-
|
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
|
data/lib/sunzi/cloud/ec2.rb
CHANGED
@@ -1,14 +1,12 @@
|
|
1
1
|
module Sunzi
|
2
2
|
module Cloud
|
3
3
|
class EC2 < Base
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
end
|
4
|
+
def setup
|
5
|
+
Logger.error 'EC2 is not implemented yet!'
|
6
|
+
end
|
8
7
|
|
9
|
-
|
10
|
-
|
11
|
-
end
|
8
|
+
def teardown(target)
|
9
|
+
Logger.error 'EC2 is not implemented yet!'
|
12
10
|
end
|
13
11
|
end
|
14
12
|
end
|
data/lib/sunzi/cloud/linode.rb
CHANGED
@@ -6,243 +6,236 @@ YAML::ENGINE.yamler = 'syck'
|
|
6
6
|
module Sunzi
|
7
7
|
module Cloud
|
8
8
|
class Linode < Base
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
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
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
@
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
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
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
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
|
data/lib/sunzi/dependency.rb
CHANGED
@@ -2,18 +2,9 @@ module Sunzi
|
|
2
2
|
class Dependency
|
3
3
|
def self.all
|
4
4
|
{
|
5
|
-
'linode' =>
|
6
|
-
|
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
|
-
|
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
|
-
|
24
|
+
abort
|
34
25
|
end
|
35
26
|
end
|
36
|
-
|
37
27
|
end
|
38
28
|
end
|
data/lib/sunzi/logger.rb
ADDED
data/lib/sunzi/version.rb
CHANGED
data/sunzi.gemspec
CHANGED
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.
|
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-
|
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: &
|
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: *
|
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
|