sunzi 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -27,7 +27,7 @@ Go to your project directory, then:
27
27
 
28
28
  $ sunzi create
29
29
 
30
- It generates a `sunzi` folder along with subdirectories and templates. Inside `sunzi`, there's `attributes.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 `attributes.yml`.
30
+ 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
31
 
32
32
  Go into the `sunzi` directory, then run the `sunzi deploy`:
33
33
 
@@ -38,7 +38,7 @@ Now, what it actually does is:
38
38
 
39
39
  1. SSH to `example.com` and login as `root`
40
40
  1. Transfer the content of the `remote` directory to the remote server and extract in `$HOME/sunzi`
41
- 1. Run `install.sh` in the remote server
41
+ 1. Run `install.sh` on the remote server
42
42
 
43
43
  As you can see, what you need to do is edit `install.sh` and add some shell commands. That's it.
44
44
 
@@ -61,13 +61,13 @@ sunzi/
61
61
  How do you pass dynamic values to a recipe?
62
62
  -------------------------------------------
63
63
 
64
- In the compile phase, `attributes.yml` are split into multiple files, one per attribute. We use filesystem as a sort of key-value storage so that it's easy to use from shell scripts.
64
+ In the compile phase, attributes defined in `sunzi.yml` are split into multiple files, one per attribute. We use filesystem as a sort of key-value storage so that it's easy to use from shell scripts.
65
65
 
66
66
  The convention for argument passing to a recipe is to use `$1`, `$2`, etc. and put a comment line for each argument.
67
67
 
68
68
  For instance, given a recipe `greeting.sh`:
69
69
 
70
- ```
70
+ ```bash
71
71
  # Greeting
72
72
  # $1: Name for goodbye
73
73
  # $2: Name for hello
@@ -77,14 +77,14 @@ echo "Goodbye $1, Hello $2!"
77
77
 
78
78
  With `attributes.yml`:
79
79
 
80
- ```
80
+ ```yaml
81
81
  goodbye: Chef
82
82
  hello: Sunzi
83
83
  ```
84
84
 
85
85
  Then, include the recipe in `install.sh`:
86
86
 
87
- ```
87
+ ```bash
88
88
  source recipes/greeting.sh $(cat attributes/goodbye) $(cat attributes/hello)
89
89
  ```
90
90
 
@@ -97,16 +97,26 @@ Goodbye Chef, Hello Sunzi!
97
97
  Remote Recipes
98
98
  --------------
99
99
 
100
- Recipes can be retrieved remotely via HTTP. Put a URL in `recipes.yml`, and Sunzi automatically loads the content and put it into the `remote/recipes` folder.
100
+ Recipes can be retrieved remotely via HTTP. Put a URL in the recipes section of `sunzi.yml`, and Sunzi will automatically load the content and put it into the `remote/recipes` folder in the compile phase.
101
101
 
102
- For instance, if you have the following line in `recipes.yml`,
102
+ For instance, if you have the following line in `sunzi.yml`,
103
103
 
104
- ```
105
- rvm: https://raw.github.com/kenn/sunzi-recipes/master/ruby/rvm.sh
104
+ ```yaml
105
+ recipes:
106
+ rvm: https://raw.github.com/kenn/sunzi-recipes/master/ruby/rvm.sh
106
107
  ```
107
108
 
108
109
  `rvm.sh` will be available and you can refer to that recipe by `source recipes/rvm.sh`.
109
110
 
111
+ Cloud Support
112
+ -------------
113
+
114
+ You can setup a new VM / teardown an existing VM interactively. Use `sunzi setup` and `sunzi teardown` for that.
115
+
116
+ The following screenshot says it all.
117
+
118
+ ![Sunzi for Linode](http://farm8.staticflickr.com/7210/6783789868_ab89010d5c.jpg)
119
+
110
120
  Vagrant
111
121
  -------
112
122
 
@@ -124,7 +134,7 @@ end
124
134
 
125
135
  with `chpasswd.sh`:
126
136
 
127
- ```
137
+ ```bash
128
138
  #!/bin/bash
129
139
 
130
140
  sudo echo 'root:vagrant' | /usr/sbin/chpasswd
data/bin/sunzi CHANGED
@@ -1,3 +1,4 @@
1
1
  #!/usr/bin/env ruby
2
+ Signal.trap(:INT) { abort "\nAborting." }
2
3
  require File.expand_path('../../lib/sunzi',__FILE__)
3
4
  Sunzi::Cli.start
@@ -1,7 +1,16 @@
1
1
  LIB_PATH = File.join(File.dirname(__FILE__), 'sunzi')
2
2
 
3
+ require 'thor'
4
+ require 'yaml'
5
+
3
6
  module Sunzi
4
- autoload :Base, File.join(LIB_PATH, 'base')
5
7
  autoload :Cli, File.join(LIB_PATH, 'cli')
8
+ autoload :Dependency, File.join(LIB_PATH, 'dependency')
6
9
  autoload :Version, File.join(LIB_PATH, 'version')
10
+
11
+ module Cloud
12
+ autoload :Base, File.join(LIB_PATH, 'cloud', 'base')
13
+ autoload :Linode, File.join(LIB_PATH, 'cloud', 'linode')
14
+ autoload :EC2, File.join(LIB_PATH, 'cloud', 'ec2')
15
+ end
7
16
  end
@@ -1,6 +1,3 @@
1
- require 'thor'
2
- require 'yaml'
3
- require 'fileutils'
4
1
  require 'open3'
5
2
 
6
3
  module Sunzi
@@ -23,9 +20,9 @@ module Sunzi
23
20
  empty_directory project
24
21
  empty_directory "#{project}/remote"
25
22
  empty_directory "#{project}/remote/recipes"
26
- template "templates/sunzi.yml", "#{project}/sunzi.yml"
27
- template "templates/remote/install.sh", "#{project}/remote/install.sh"
28
- template "templates/remote/recipes/ssh_key.sh", "#{project}/remote/recipes/ssh_key.sh"
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"
29
26
  end
30
27
 
31
28
  desc "deploy example.com (or user@example.com:2222)", "Deploy sunzi project"
@@ -87,6 +84,16 @@ module Sunzi
87
84
  end
88
85
  end
89
86
 
87
+ desc "setup [linode|ec2]", "Setup a new VM"
88
+ def setup(target)
89
+ Cloud::Base.choose(target).setup
90
+ end
91
+
92
+ desc "teardown [linode|ec2] [name]", "Teardown an existing VM"
93
+ def teardown(target, name)
94
+ Cloud::Base.choose(target).teardown(name)
95
+ end
96
+
90
97
  no_tasks do
91
98
  def parse_target(target)
92
99
  target.match(/(.*@)?(.*?)(:.*)?$/)
@@ -0,0 +1,38 @@
1
+ Sunzi::Dependency.load('highline')
2
+
3
+ module Sunzi
4
+ module Cloud
5
+ class Base < Thor
6
+ include Thor::Actions
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
23
+ end
24
+ end
25
+
26
+ def initialize(*args)
27
+ @ui = HighLine.new
28
+ super
29
+ end
30
+
31
+ no_tasks do
32
+ def ask(question, answer_type, &details)
33
+ @ui.ask(@ui.color(question, :green, :bold), answer_type, &details)
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,15 @@
1
+ module Sunzi
2
+ module Cloud
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
8
+
9
+ def teardown(target)
10
+ say shell.set_color('EC2 is not implemented yet!', :red, true)
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,248 @@
1
+ Sunzi::Dependency.load('linode')
2
+
3
+ # Workaround the bug: https://github.com/rick/linode/issues/12
4
+ YAML::ENGINE.yamler = 'syck'
5
+
6
+ module Sunzi
7
+ module Cloud
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
190
+ end
191
+
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)
238
+ end
239
+
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'] }
244
+ end
245
+ end
246
+ end
247
+ end
248
+ end
@@ -0,0 +1,38 @@
1
+ module Sunzi
2
+ class Dependency
3
+ def self.all
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
+ },
17
+ }
18
+ end
19
+
20
+ def self.load(name)
21
+ begin
22
+ gem(name, all[name][:version])
23
+ require(all[name][:require])
24
+ rescue LoadError
25
+ puts <<-EOS
26
+ Dependency missing: #{name}
27
+ To install the gem, issue the following command:
28
+
29
+ gem install #{name} -v '#{all[name][:version]}'
30
+
31
+ Please try again after installing the missing dependency.
32
+ EOS
33
+ exit 1
34
+ end
35
+ end
36
+
37
+ end
38
+ end
@@ -1,3 +1,3 @@
1
1
  module Sunzi
2
- VERSION = "0.3.0"
2
+ VERSION = "0.4.0"
3
3
  end
@@ -0,0 +1,34 @@
1
+ ---
2
+ api_key: your_api_key
3
+ root_pass: your_root_password
4
+ root_sshkey_path: ~/.ssh/id_rsa.pub
5
+
6
+ # payment_term must be 1, 12 or 24
7
+ payment_term: 1
8
+
9
+ # Add / remove environments
10
+ environments:
11
+ - production
12
+ - staging
13
+ fqdn:
14
+ zone: example.com
15
+ production: %{host}.example.com
16
+ staging: %{host}.staging.example.com
17
+ label:
18
+ production: example-%{host}
19
+ staging: example-staging-%{host}
20
+ group:
21
+ production: example
22
+ staging: example-staging
23
+
24
+ # Filter out large lists by keyword
25
+ distributions_filter: debian
26
+ kernels_filter: latest
27
+
28
+ # dns takes either "linode" or "route53"
29
+ dns: linode
30
+
31
+ # only used when route53 is chosen for DNS
32
+ route53:
33
+ key: your_aws_key
34
+ secret: your_aws_secret
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.3.0
4
+ version: 0.4.0
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-25 00:00:00.000000000 Z
12
+ date: 2012-02-26 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: thor
16
- requirement: &2152751900 !ruby/object:Gem::Requirement
16
+ requirement: &2156460860 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
@@ -21,7 +21,7 @@ dependencies:
21
21
  version: '0'
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *2152751900
24
+ version_requirements: *2156460860
25
25
  description: Server provisioning utility for minimalists
26
26
  email:
27
27
  - kenn.ejima@gmail.com
@@ -36,12 +36,16 @@ files:
36
36
  - Rakefile
37
37
  - bin/sunzi
38
38
  - lib/sunzi.rb
39
- - lib/sunzi/base.rb
40
39
  - lib/sunzi/cli.rb
40
+ - lib/sunzi/cloud/base.rb
41
+ - lib/sunzi/cloud/ec2.rb
42
+ - lib/sunzi/cloud/linode.rb
43
+ - lib/sunzi/dependency.rb
41
44
  - lib/sunzi/version.rb
42
- - lib/templates/remote/install.sh
43
- - lib/templates/remote/recipes/ssh_key.sh
44
- - lib/templates/sunzi.yml
45
+ - lib/templates/create/remote/install.sh
46
+ - lib/templates/create/remote/recipes/ssh_key.sh
47
+ - lib/templates/create/sunzi.yml
48
+ - lib/templates/setup/linode/linode.yml
45
49
  - sunzi.gemspec
46
50
  - test/test_cli.rb
47
51
  homepage: http://github.com/kenn/sunzi
@@ -1,9 +0,0 @@
1
- require 'yaml'
2
-
3
- module Sunzi
4
- class Base
5
- def initialize(project)
6
- # Do something
7
- end
8
- end
9
- end