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

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