teleport 1.0.0 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile CHANGED
@@ -1,3 +1,2 @@
1
1
  source "http://rubygems.org"
2
-
3
2
  gemspec
data/README.md CHANGED
@@ -13,7 +13,7 @@ At the moment Teleport supports **Ubuntu 10.04/10.10/11.04 with Ruby 1.8.7, 1.9.
13
13
  1. Install Teleport on your local machine.
14
14
 
15
15
  ```
16
- $ sudo gem install teleport --pre
16
+ $ sudo gem install teleport
17
17
  ```
18
18
 
19
19
  1. Create a `Telfile` config file. Here's a simple example. Note that we actually define two machines, `server_app1` and `server_db1`:
data/Rakefile CHANGED
@@ -1,10 +1,58 @@
1
1
  require "bundler"
2
- require "rake/rdoctask"
2
+ require "bundler/setup"
3
3
 
4
- Bundler::GemHelper.install_tasks
4
+ require "rake"
5
+ require "rdoc/task"
6
+ require "rspec"
7
+ require "rspec/core/rake_task"
5
8
 
6
- Rake::RDocTask.new do |rdoc|
9
+ $LOAD_PATH << File.expand_path("../lib", __FILE__)
10
+ require "teleport/version"
11
+
12
+ #
13
+ # gem
14
+ #
15
+
16
+ task :gem => :build
17
+ task :build do
18
+ system "gem build --quiet teleport.gemspec"
19
+ end
20
+
21
+ task :install => :build do
22
+ system "sudo gem install --quiet teleport-#{Teleport::VERSION}.gem"
23
+ end
24
+
25
+ task :release => :build do
26
+ system "git tag -a #{Teleport::VERSION} -m 'Tagging #{Teleport::VERSION}'"
27
+ system "git push --tags"
28
+ system "gem push teleport-#{Teleport::VERSION}.gem"
29
+ end
30
+
31
+ #
32
+ # rspec
33
+ #
34
+
35
+ RSpec::Core::RakeTask.new(:spec) do |spec|
36
+ spec.rspec_opts = %w(--color --tty)
37
+ spec.pattern = "spec/**/*_spec.rb"
38
+ end
39
+
40
+ RSpec::Core::RakeTask.new("spec:unit") do |spec|
41
+ spec.pattern = "spec/unit/**/*_spec.rb"
42
+ end
43
+
44
+ RSpec::Core::RakeTask.new("spec:end") do |spec|
45
+ spec.pattern = "spec/end*_spec.rb"
46
+ end
47
+
48
+ #
49
+ # rdoc
50
+ #
51
+
52
+ RDoc::Task.new do |rdoc|
7
53
  rdoc.rdoc_dir = "rdoc"
8
54
  rdoc.title = "teleport #{Teleport::VERSION}"
9
55
  rdoc.rdoc_files.include("lib/**/*.rb")
10
56
  end
57
+
58
+ task :default => :spec
File without changes
@@ -4,4 +4,5 @@ require "teleport/util"
4
4
  require "teleport/config"
5
5
  require "teleport/mirror"
6
6
  require "teleport/install"
7
+ require "teleport/infer"
7
8
  require "teleport/main"
@@ -2,11 +2,10 @@ module Teleport
2
2
  # This class parses Telfile, and includes DSL and the models.
3
3
  class Config
4
4
  RUBIES = ["1.9.2", "REE", "1.8.7"]
5
- PATH = "Telfile"
6
5
 
7
- attr_accessor :user, :ruby, :roles, :servers, :apt, :packages, :callbacks, :dsl
6
+ attr_accessor :user, :ruby, :ssh_options, :roles, :servers, :apt, :packages, :callbacks, :dsl
8
7
 
9
- def initialize
8
+ def initialize(file = "Telfile")
10
9
  @roles = []
11
10
  @servers = []
12
11
  @apt = []
@@ -14,7 +13,7 @@ module Teleport
14
13
  @callbacks = { }
15
14
 
16
15
  @dsl = DSL.new(self)
17
- @dsl.instance_eval(File.read(PATH), PATH)
16
+ @dsl.instance_eval(File.read(file), file)
18
17
 
19
18
  @user ||= Util.whoami
20
19
  @ruby ||= RUBIES.first
@@ -95,6 +94,12 @@ module Teleport
95
94
  @config.user = v
96
95
  end
97
96
 
97
+ def ssh_options(v)
98
+ raise "ssh_options called twice" if @config.ssh_options
99
+ raise "ssh_options must be an Array" if !v.is_a?(Array)
100
+ @config.ssh_options = v
101
+ end
102
+
98
103
  def role(name, options = {})
99
104
  raise "role #{name.inspect} defined twice" if @config.roles.any? { |i| i.name == name }
100
105
  @config.roles << Role.new(name, options)
@@ -0,0 +1,349 @@
1
+ require "set"
2
+
3
+ # Many, many thanks to Blueprint!
4
+ # https://github.com/devstructure/blueprint
5
+
6
+ module Teleport
7
+ class Infer
8
+ include Util
9
+
10
+ # Copyright 2011 DevStructure. All rights reserved.
11
+ #
12
+ # Redistribution and use in source and binary forms, with or without
13
+ # modification, are permitted provided that the following conditions are
14
+ # met:
15
+ #
16
+ # 1. Redistributions of source code must retain the above copyright
17
+ # notice, this list of conditions and the following disclaimer.
18
+ #
19
+ # 2. Redistributions in binary form must reproduce the above
20
+ # copyright notice, this list of conditions and the following
21
+ # disclaimer in the documentation and/or other materials provided
22
+ # with the distribution.
23
+ #
24
+ # THIS SOFTWARE IS PROVIDED BY DEVSTRUCTURE ``AS IS'' AND ANY EXPRESS
25
+ # OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
26
+ # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
27
+ # DISCLAIMED. IN NO EVENT SHALL DEVSTRUCTURE OR CONTRIBUTORS BE LIABLE
28
+ # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
29
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
30
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
31
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
32
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
33
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
34
+ # THE POSSIBILITY OF SUCH DAMAGE.
35
+ #
36
+ # The views and conclusions contained in the software and documentation
37
+ # are those of the authors and should not be interpreted as representing
38
+ # official policies, either expressed or implied, of DevStructure.
39
+ #
40
+ # (for MD5SUMS)
41
+
42
+ MD5SUMS = {
43
+ '/etc/adduser.conf' => ['/usr/share/adduser/adduser.conf'],
44
+ '/etc/apparmor.d/tunables/home.d/ubuntu' =>
45
+ ['2a88811f7b763daa96c20b20269294a4'],
46
+ '/etc/apt/apt.conf.d/00CDMountPoint' =>
47
+ ['cb46a4e03f8c592ee9f56c948c14ea4e'],
48
+ '/etc/apt/apt.conf.d/00trustcdrom' =>
49
+ ['a8df82e6e6774f817b500ee10202a968'],
50
+ '/etc/chatscripts/provider' => ['/usr/share/ppp/provider.chatscript'],
51
+ '/etc/default/console-setup' =>
52
+ ['0fb6cec686d0410993bdf17192bee7d6',
53
+ 'b684fd43b74ac60c6bdafafda8236ed3',
54
+ '/usr/share/console-setup/console-setup'],
55
+ '/etc/default/grub' => ['ee9df6805efb2a7d1ba3f8016754a119',
56
+ 'ad9283019e54cedfc1f58bcc5e615dce'],
57
+ '/etc/default/irqbalance' => ['7e10d364b9f72b11d7bf7bd1cfaeb0ff'],
58
+ '/etc/default/keyboard' => ['06d66484edaa2fbf89aa0c1ec4989857'],
59
+ '/etc/default/locale' => ['164aba1ef1298affaa58761647f2ceba',
60
+ '7c32189e775ac93487aa4a01dffbbf76'],
61
+ '/etc/default/rcS' => ['/usr/share/initscripts/default.rcS'],
62
+ '/etc/environment' => ['44ad415fac749e0c39d6302a751db3f2'],
63
+ '/etc/hosts.allow' => ['8c44735847c4f69fb9e1f0d7a32e94c1'],
64
+ '/etc/hosts.deny' => ['92a0a19db9dc99488f00ac9e7b28eb3d'],
65
+ '/etc/initramfs-tools/modules' =>
66
+ ['/usr/share/initramfs-tools/modules'],
67
+ '/etc/inputrc' => ['/usr/share/readline/inputrc'],
68
+ '/etc/iscsi/iscsid.conf' => ['6c6fd718faae84a4ab1b276e78fea471'],
69
+ '/etc/kernel-img.conf' => ['f1ed9c3e91816337aa7351bdf558a442'],
70
+ '/etc/ld.so.conf' => ['4317c6de8564b68d628c21efa96b37e4'],
71
+ '/etc/networks' => ['/usr/share/base-files/networks'],
72
+ '/etc/nsswitch.conf' => ['/usr/share/base-files/nsswitch.conf'],
73
+ '/etc/pam.d/common-account' => ['9d50c7dda6ba8b6a8422fd4453722324'],
74
+ '/etc/pam.d/common-auth' => ['a326c972f4f3d20e5f9e1b06eef4d620'],
75
+ '/etc/pam.d/common-password' => ['9f2fbf01b1a36a017b16ea62c7ff4c22'],
76
+ '/etc/pam.d/common-session' => ['e2b72dd3efb2d6b29698f944d8723ab1'],
77
+ '/etc/pam.d/common-session-noninteractive' =>
78
+ ['508d44b6daafbc3d6bd587e357a6ff5b'],
79
+ '/etc/ppp/chap-secrets' => ['faac59e116399eadbb37644de6494cc4'],
80
+ '/etc/ppp/pap-secrets' => ['698c4d412deedc43dde8641f84e8b2fd'],
81
+ '/etc/ppp/peers/provider' => ['/usr/share/ppp/provider.peer'],
82
+ '/etc/profile' => ['/usr/share/base-files/profile'],
83
+ '/etc/python/debian_config' => ['7f4739eb8858d231601a5ed144099ac8'],
84
+ '/etc/rc.local' => ['10fd9f051accb6fd1f753f2d48371890'],
85
+ '/etc/rsyslog.d/50-default.conf' =>
86
+ ['/usr/share/rsyslog/50-default.conf'],
87
+ '/etc/security/opasswd' => ['d41d8cd98f00b204e9800998ecf8427e'],
88
+ '/etc/sgml/xml-core.cat' => ['bcd454c9bf55a3816a134f9766f5928f'],
89
+ '/etc/shells' => ['0e85c87e09d716ecb03624ccff511760'],
90
+ '/etc/ssh/sshd_config' => ['e24f749808133a27d94fda84a89bb27b',
91
+ '8caefdd9e251b7cc1baa37874149a870'],
92
+ '/etc/sudoers' => ['02f74ccbec48997f402a063a172abb48'],
93
+ '/etc/ufw/after.rules' => ['/usr/share/ufw/after.rules'],
94
+ '/etc/ufw/after6.rules' => ['/usr/share/ufw/after6.rules'],
95
+ '/etc/ufw/before.rules' => ['/usr/share/ufw/before.rules'],
96
+ '/etc/ufw/before6.rules' => ['/usr/share/ufw/before6.rules'],
97
+ '/etc/ufw/ufw.conf' => ['/usr/share/ufw/ufw.conf']
98
+ }
99
+
100
+ NEW_FILES_WITHIN = %w(cron.d logrotate.d rsyslog.d init)
101
+ CHECKSUM_FILES = %w(bash.bashrc environment inputrc rc.local ssh/ssh_config ssh/sshd_config)
102
+
103
+ def initialize
104
+ @telfile = []
105
+
106
+ if fails?("grep -q Ubuntu /etc/lsb-release")
107
+ fatal "Sorry, --infer can only run on an Ubuntu machine."
108
+ end
109
+
110
+ append "#" * 72
111
+ append "# Telfile inferred from #{`hostname`.strip} at #{Time.now}"
112
+ append "#" * 72
113
+ append
114
+
115
+ user
116
+ ruby
117
+ apt
118
+ packages
119
+ files
120
+
121
+ banner "Done!"
122
+ $stderr.puts
123
+ @telfile.each { |i| puts i }
124
+ end
125
+
126
+ def append(s = nil)
127
+ @telfile << (s || "")
128
+ end
129
+
130
+ def user
131
+ append "user #{`whoami`.strip.inspect}"
132
+ end
133
+
134
+ def ruby
135
+ version = `ruby --version`
136
+ ruby = nil
137
+ case version
138
+ when /Ruby Enterprise Edition/ then ruby = "REE"
139
+ when /1\.8\.7/ then ruby = "1.8.7"
140
+ when /1\.9\.2/ then ruby = "1.9.2"
141
+ end
142
+ append "ruby #{ruby.inspect}" if ruby
143
+ end
144
+
145
+ def apt
146
+ banner "Calculating apt sources and keys..."
147
+ list = run_capture_lines("cat /etc/apt/sources.list /etc/apt/sources.list.d/*.list")
148
+ list = list.grep(/^deb /).sort
149
+ list.each do |line|
150
+ if line =~ /^deb http:\/\/(\S+)\s+(\S+)/
151
+ source, dist = $1, $2
152
+ file = source.chomp("/").gsub(/[^a-z0-9.-]/, "_")
153
+ file = "/var/lib/apt/lists/#{file}_dists_#{dist}_Release"
154
+ next if !File.exists?(file)
155
+
156
+ verify = run_capture("gpgv --keyring /etc/apt/trusted.gpg #{file}.gpg #{file} 2>&1")
157
+ key = verify[/key ID ([A-Z0-9]{8})$/, 1]
158
+ next if key == "437D05B5" # canonical key
159
+ append "apt #{line.inspect}, :key => #{key.inspect}"
160
+ end
161
+ end
162
+ end
163
+
164
+ def packages
165
+ banner "Looking for interesting packages..."
166
+ @packages = Apt.new.added
167
+ if !@packages.empty?
168
+ append
169
+ append "# Note: You should read this package list very carefully and remove"
170
+ append "# packages that you don't want on your server."
171
+ append
172
+ append "packages %w(#{@packages.join(" ")})"
173
+ end
174
+ end
175
+
176
+ def files
177
+ banner "Looking for interesting files..."
178
+ files = []
179
+
180
+ # read checksums from dpkg status
181
+ conf = { }
182
+ File.readlines("/var/lib/dpkg/status").each do |line|
183
+ if line =~ /^ (\S+) ([0-9a-f]{32})/
184
+ conf[$1] = $2
185
+ end
186
+ end
187
+
188
+ # look for changed conf files
189
+ $stderr.puts " scanning conf files from interesting packages..."
190
+ @packages.each do |pkg|
191
+ list = run_capture_lines("dpkg -L #{pkg}")
192
+ list = list.select { |i| i =~ /^\/etc/ }.sort
193
+ list = list.select { |i| File.file?(i) }
194
+ list = list.select { |i| conf[i] && conf[i] != md5sum(i) }
195
+ files += list
196
+ end
197
+
198
+ # look for new files in NEW_FILES_WITHIN
199
+ dirs = NEW_FILES_WITHIN.map { |i| "/etc/#{i}" }
200
+ dirs.sort.each do |dir|
201
+ $stderr.puts " scanning #{dir} for new files..."
202
+ list = Dir["#{dir}/*"].sort
203
+ list = list.select { |i| !MD5SUMS[i] }
204
+ list = list.select { |i| fails?("dpkg -S #{i}") }
205
+ files += list
206
+ end
207
+
208
+ # now look for changed files from CHECKSUM_FILES
209
+ scan = CHECKSUM_FILES.map { |i| "/etc/#{i}" }
210
+ scan = scan.select { |i| File.file?(i) }
211
+ scan.each do |i|
212
+ new_sum = md5sum(i)
213
+ if old_sum = MD5SUMS[i]
214
+ match = old_sum.any? do |sum|
215
+ sum = md5sum(sum) if sum =~ /^\//
216
+ new_sum == sum
217
+ end
218
+ files << i if !match
219
+ elsif old_sum = conf[i]
220
+ files << i if new_sum != old_sum
221
+ end
222
+ end
223
+ files = files.sort
224
+
225
+ if !files.empty?
226
+ append
227
+ append "#" * 72
228
+ append "# Also, I think these should be included in files/"
229
+ append "#" * 72
230
+ append
231
+ files.each { |i| append "# #{i}" }
232
+ append
233
+ append "# You can do that with this magical command:"
234
+ append "#"
235
+ append "# mkdir files && cd files && tar cf - #{files.join(" ")} | tar xf -"
236
+ end
237
+ end
238
+
239
+ class Apt
240
+ include Util
241
+
242
+ BLACKLIST = /^(linux-|grub-|cloud-init)/
243
+
244
+ Package = Struct.new(:name, :status, :deps, :base, :parents)
245
+
246
+ def initialize
247
+ @packages = nil
248
+ @map = nil
249
+ end
250
+
251
+ def packages
252
+ if !@packages
253
+ # run dpkg
254
+ lines = run_capture_lines("dpkg-query '-f=${Package}\t${Status}\t${Pre-Depends},${Depends},${Recommends}\t${Essential}\t${Priority}\n' -W")
255
+ @packages = lines.map do |line|
256
+ name, status, deps, essential, priority = line.split("\t")
257
+ deps = deps.gsub(/\([^)]+\)/, "")
258
+ deps = deps.split(/[,|]/)
259
+ deps = deps.map(&:strip).select { |i| !i.empty? }.sort
260
+ base = false
261
+ base = true if essential == "yes"
262
+ base = true if priority =~ /^(important|required|standard)$/
263
+ Package.new(name, status, deps, base, [])
264
+ end
265
+
266
+ # calculate ancestors
267
+ @packages.each do |pkg|
268
+ pkg.deps.each do |i|
269
+ if d = self[i]
270
+ d.parents << pkg.name
271
+ end
272
+ end
273
+ end
274
+ @packages.each do |pkg|
275
+ pkg.parents = pkg.parents.sort.uniq
276
+ end
277
+ end
278
+
279
+ @packages
280
+ end
281
+
282
+ def [](name)
283
+ if !@map
284
+ @map = { }
285
+ packages.each { |i| @map[i.name] = i }
286
+ end
287
+ @map[name]
288
+ end
289
+
290
+ def base_packages
291
+ packages.select { |i| i.base }.map(&:name)
292
+ end
293
+
294
+ def ignored_packages
295
+ list = packages.select { |i| i.base }.map(&:name)
296
+ list += %w(grub-pc installation-report language-pack-en language-pack-gnome-en linux-generic-pae linux-server os-prober ubuntu-desktop ubuntu-minimal ubuntu-standard wireless-crda)
297
+ dependencies(list)
298
+ end
299
+
300
+ def dependencies(list)
301
+ check = list
302
+ while !check.empty?
303
+ check = check.map do |i|
304
+ if pkg = self[i]
305
+ pkg.deps
306
+ end
307
+ end
308
+ check = check.compact.flatten.uniq.sort
309
+ check -= list
310
+ list += check
311
+ end
312
+ list.sort
313
+ end
314
+
315
+ def added
316
+ # calculate raw list
317
+ ignored = Set.new(ignored_packages)
318
+ list = packages.select do |i|
319
+ i.status == "install ok installed" && !ignored.include?(i.name)
320
+ end
321
+ list = list.map(&:name)
322
+
323
+ # now calculate parents
324
+ roots = []
325
+ check = list
326
+ while !check.empty?
327
+ check = check.map do |i|
328
+ if pkg = self[i]
329
+ if !pkg.parents.empty?
330
+ pkg.parents
331
+ else
332
+ roots << pkg.name
333
+ nil
334
+ end
335
+ end
336
+ end
337
+ check = check.compact.flatten.uniq.sort
338
+ check -= list
339
+ list += check
340
+ end
341
+
342
+ # blacklist
343
+ roots = roots.reject { |i| i =~ BLACKLIST }
344
+
345
+ roots.sort
346
+ end
347
+ end
348
+ end
349
+ end
@@ -51,14 +51,14 @@ module Teleport
51
51
  # do we have a server object?
52
52
  @server = @config.server(@host)
53
53
  if !@server && !@config.servers.empty?
54
- fatal "Hm. I couldn't find server #{@host.inspect} in teleport.rb."
54
+ fatal "Hm. I couldn't find server #{@host.inspect} in Telfile."
55
55
  end
56
56
 
57
57
  @role = nil
58
58
  if @server && (role_name = @server.options[:role])
59
59
  @role = @config.role(role_name)
60
60
  if !@role
61
- fatal "Hm. I couldn't find role #{role_name.inspect} in teleport.rb."
61
+ fatal "Hm. I couldn't find role #{role_name.inspect} in Telfile."
62
62
  end
63
63
  end
64
64
  end
@@ -88,7 +88,13 @@ module Teleport
88
88
  end
89
89
 
90
90
  def _hostname
91
- banner "Hostname..."
91
+ banner "Hostname..."
92
+
93
+ # ipv4?
94
+ return if @host =~ /^\d+(\.\d+){3}$/
95
+ # ipv6?
96
+ return if @host =~ /:/
97
+
92
98
  old_hostname = `hostname`.strip
93
99
  return if old_hostname == @host
94
100
 
@@ -1,5 +1,4 @@
1
- require "erb"
2
- require "getoptlong"
1
+ require "optparse"
3
2
 
4
3
  module Teleport
5
4
  # The main class for the teleport command line.
@@ -9,53 +8,83 @@ module Teleport
9
8
 
10
9
  TAR = "#{DIR}.tgz"
11
10
 
12
- attr_accessor :host, :options
13
-
14
11
  def initialize(cmd = :teleport)
15
- opts = GetoptLong.new(
16
- ["--help", "-h", GetoptLong::NO_ARGUMENT]
17
- )
18
- opts.each do |opt, arg|
19
- case opt
20
- when "--help"
21
- usage(0)
22
- end
23
- end
24
-
25
- $stderr = $stdout
12
+ cli(cmd)
26
13
 
27
- case cmd
14
+ case @options[:cmd]
28
15
  when :teleport
29
- teleport(ARGV.shift)
16
+ $stderr = $stdout
17
+ teleport
30
18
  when :install
19
+ $stderr = $stdout
31
20
  install
21
+ when :infer
22
+ infer
32
23
  end
33
24
  end
25
+
26
+ # Parse ARGV.
27
+ def cli(cmd)
28
+ @options = { }
29
+ @options[:cmd] = cmd
30
+ @options[:file] = "Telfile"
31
+
32
+ opt = OptionParser.new do |o|
33
+ o.banner = "Usage: teleport <hostname>"
34
+ o.on("-f", "--file FILE", "use this file instead of Telfile") do |f|
35
+ @options[:file] = f
36
+ end
37
+ o.on("-i", "--infer", "infer a new Telfile from YOUR machine") do |f|
38
+ @options[:cmd] = :infer
39
+ end
40
+ o.on_tail("-h", "--help", "print this help text") do
41
+ puts opt
42
+ exit(0)
43
+ end
44
+ end
45
+ begin
46
+ opt.parse!
47
+ rescue OptionParser::InvalidOption, OptionParser::MissingArgument
48
+ puts $!
49
+ puts opt
50
+ exit(1)
51
+ end
34
52
 
35
- def usage(exit_code)
36
- puts "Usage: teleport <hostname>"
37
- puts " --help print this help text"
38
- exit(exit_code)
53
+ if @options[:cmd] == :teleport
54
+ # print this error message early, to give the user a hint
55
+ # instead of complaining about command line arguments
56
+ if ARGV.length != 1
57
+ puts opt
58
+ exit(1)
59
+ end
60
+ @options[:host] = ARGV.shift
61
+ end
39
62
  end
40
63
 
64
+ # Read Telfile
41
65
  def read_config
42
- if !File.exists?(Config::PATH)
43
- fatal("Sadly, I can't find #{Config::PATH} here. Please create one.")
66
+ if !File.exists?(@options[:file])
67
+ fatal("Sadly, I can't find #{@options[:file]} here. Please create one.")
44
68
  end
45
- @config = Config.new
69
+ @config = Config.new(@options[:file])
46
70
  end
47
71
 
48
- def assemble_tgz(host)
72
+ # Assemble the the tgz before we teleport to the host
73
+ def assemble_tgz
49
74
  banner "Assembling #{TAR}..."
50
75
  rm_and_mkdir(DIR)
51
76
 
52
77
  # gem
53
78
  run("cp", ["-r", "#{File.dirname(__FILE__)}/../../lib", GEM])
79
+ # Telfile, if necessary
80
+ if @options[:file] != "Telfile"
81
+ run("cp", [@options[:file], "Telfile"])
82
+ end
54
83
  # data
55
84
  run("cp", ["-r", ".", DATA])
56
85
  # config.sh
57
86
  File.open("#{DIR}/config", "w") do |f|
58
- f.puts("CONFIG_HOST='#{host}'")
87
+ f.puts("CONFIG_HOST='#{@options[:host]}'")
59
88
  f.puts("CONFIG_RUBY='#{@config.ruby}'")
60
89
  f.puts("CONFIG_RUBYGEMS='#{RUBYGEMS}'")
61
90
  end
@@ -63,8 +92,6 @@ module Teleport
63
92
  ssh_key = "#{ENV["HOME"]}/.ssh/#{PUBKEY}"
64
93
  if File.exists?(ssh_key)
65
94
  run("cp", [ssh_key, DIR])
66
- else
67
- puts "Could not find #{ssh_key} - skipping."
68
95
  end
69
96
 
70
97
  Dir.chdir(File.dirname(DIR)) do
@@ -72,10 +99,16 @@ module Teleport
72
99
  end
73
100
  end
74
101
 
75
- def ssh_tgz(host)
102
+ # Copy the tgz to the host, then run there.
103
+ def ssh_tgz
76
104
  begin
77
- banner "scp #{TAR} to #{host}:#{TAR}..."
78
- run "scp #{TAR} #{host}:#{TAR}"
105
+ banner "scp #{TAR} to #{@options[:host]}:#{TAR}..."
106
+
107
+ args = []
108
+ args += @config.ssh_options if @config.ssh_options
109
+ args << TAR
110
+ args << "#{@options[:host]}:#{TAR}"
111
+ run("scp", args)
79
112
 
80
113
  cmd = [
81
114
  "cd /tmp",
@@ -84,27 +117,37 @@ module Teleport
84
117
  "sudo tar xfpz #{TAR}",
85
118
  "sudo #{DIR}/gem/teleport/run.sh"
86
119
  ]
87
- banner "ssh to #{host} and run..."
88
- run("ssh", [host, cmd.join(" && ")])
120
+ banner "ssh to #{@options[:host]} and run..."
121
+
122
+ args = []
123
+ args += @config.ssh_options if @config.ssh_options
124
+ args << @options[:host]
125
+ args << cmd.join(" && ")
126
+ run("ssh", args)
89
127
  rescue RunError
90
128
  fatal("Failed!")
91
129
  end
92
130
  banner "Success!"
93
131
  end
94
132
 
95
- def teleport(host)
133
+ # Teleport to the host.
134
+ def teleport
96
135
  read_config
97
- usage(1) if !host
98
- assemble_tgz(host)
99
- ssh_tgz(host)
136
+ assemble_tgz
137
+ ssh_tgz
100
138
  end
101
139
 
140
+ # We're running on the host - install!
102
141
  def install
103
142
  Dir.chdir(DATA) do
104
143
  read_config
105
144
  end
106
145
  Install.new(@config)
107
146
  end
108
-
147
+
148
+ # try to infer a new Telfile based on the current machine
149
+ def infer
150
+ Infer.new
151
+ end
109
152
  end
110
153
  end
@@ -31,8 +31,12 @@ module Teleport
31
31
  copy_metadata(path, tmp)
32
32
  path = tmp
33
33
  end
34
-
35
- cp_if_necessary(path, dst, user_for_file(dst), mode_for_file(dst))
34
+
35
+ if !File.symlink?(path)
36
+ cp_if_necessary(path, dst, user_for_file(dst), mode_for_file(dst))
37
+ else
38
+ ln_if_necessary(File.readlink(path), dst)
39
+ end
36
40
  end
37
41
 
38
42
  # Install directory from the teleport data directory into the
@@ -90,15 +90,15 @@ function install_ruby_ree() {
90
90
  #
91
91
 
92
92
  # are we on Ubuntu?
93
- if ! uname -a | grep -q Ubuntu ; then
93
+ if ! grep -q Ubuntu /etc/lsb-release ; then
94
94
  fatal "Teleport only works with Ubuntu"
95
95
  fi
96
96
 
97
97
  # which version?
98
98
  . /etc/lsb-release
99
- case $($DISTRIB_RELEASE) in
100
- 10.* ) ;; # nop
101
- 11.04) ;; # nop
99
+ case $DISTRIB_RELEASE in
100
+ 10.* ) ;; # nop
101
+ 11.04 ) ;; # nop
102
102
  *)
103
103
  banner "warning - Ubuntu $DISTRIB_RELEASE hasn't been tested with Teleport yet"
104
104
  esac
@@ -1,4 +1,5 @@
1
1
  require "cgi"
2
+ require "digest/md5"
2
3
  require "etc"
3
4
  require "fileutils"
4
5
 
@@ -56,7 +57,7 @@ module Teleport
56
57
  end
57
58
 
58
59
  # Run a command, raise an error upon failure. The output is
59
- # capture as a string and returned.
60
+ # captured as a string and returned.
60
61
  def run_capture(command, *args)
61
62
  if !args.empty?
62
63
  args = args.flatten.map { |i| shell_escape(i) }.join(" ")
@@ -72,6 +73,13 @@ module Teleport
72
73
  result
73
74
  end
74
75
 
76
+ # Run a command and split the result into lines, raise an error
77
+ # upon failure. The output is captured as an array of strings and
78
+ # returned.
79
+ def run_capture_lines(command, *args)
80
+ run_capture(command, args).split("\n")
81
+ end
82
+
75
83
  # Run a command but don't send any output to $stdout/$stderr.
76
84
  def run_quietly(command, *args)
77
85
  if !args.empty?
@@ -304,7 +312,18 @@ module Teleport
304
312
  end
305
313
  false
306
314
  end
307
-
315
+
316
+ # Calculate the md5 checksum for a file
317
+ def md5sum(path)
318
+ digest, buf = Digest::MD5.new, ""
319
+ File.open(path) do |f|
320
+ while f.read(4096, buf)
321
+ digest.update(buf)
322
+ end
323
+ end
324
+ digest.hexdigest
325
+ end
326
+
308
327
  private
309
328
 
310
329
  # Returns true if verbosity is turned on.
@@ -1,4 +1,4 @@
1
1
  module Teleport
2
2
  # Gem version
3
- VERSION = "1.0.0"
3
+ VERSION = "1.0.1"
4
4
  end
@@ -0,0 +1,39 @@
1
+ require "erb"
2
+ require "spec_helper"
3
+
4
+ describe "a new ec2 instance" do
5
+ ec2
6
+
7
+ telfile do
8
+ <<EOF
9
+ user "gub"
10
+ ruby "1.8.7"
11
+ ssh_options ["-o", "User=ubuntu", "-o", "StrictHostKeyChecking=no", "-o", "IdentityFile=#{ENV["TELEPORT_SSH_KEY"]}"]
12
+
13
+ role :master, :packages => %w(nginx)
14
+ server "#{$ec2_ip_address}", :role => :master, :packages => %w(strace)
15
+ packages %w(atop)
16
+
17
+ before_install do
18
+ puts "BEFORE_INSTALL"
19
+ end
20
+
21
+ after_install do
22
+ puts "AFTER_INSTALL"
23
+ run "touch /tmp/gub.txt"
24
+ end
25
+ EOF
26
+ end
27
+
28
+ it "installs properly" do
29
+ ARGV.clear
30
+ ARGV << $ec2_ip_address
31
+ Teleport::Main.new
32
+ end
33
+
34
+ it "installs again" do
35
+ ARGV.clear
36
+ ARGV << $ec2_ip_address
37
+ Teleport::Main.new
38
+ end
39
+ end
@@ -0,0 +1,25 @@
1
+ SUPPORT = "#{File.dirname(__FILE__)}/support"
2
+
3
+ $LOAD_PATH << "#{File.dirname(__FILE__)}/../lib"
4
+ $LOAD_PATH << File.dirname(__FILE__)
5
+ $LOAD_PATH << SUPPORT
6
+
7
+ require "awesome_print"
8
+ require "rspec"
9
+ require "teleport"
10
+
11
+ Dir["#{SUPPORT}/*.rb"].each { |i| require File.basename(i) }
12
+
13
+ TELDIRS = "#{File.dirname(__FILE__)}/teldirs"
14
+
15
+ RSpec.configure do |config|
16
+ config.extend Support::Telfile
17
+ config.extend Support::Ec2
18
+
19
+ ec2_configured = Support::Ec2.configured?
20
+ warn(Support::Ec2.message) if !ec2_configured
21
+
22
+ config.filter_run_excluding(:config => lambda { |value|
23
+ return true if value == :ec2 && !ec2_configured
24
+ })
25
+ end
@@ -0,0 +1,115 @@
1
+ require "AWS"
2
+
3
+ # spin up a fresh ec2 instance
4
+ module Support
5
+ module Ec2
6
+ AMI_10_04 = "fbbf7892"
7
+ AMI_10_10 = "08f40561"
8
+ AMI_11_04 = "68ad5201"
9
+ KEYPAIR = "teleport"
10
+ GROUP = "teleport"
11
+
12
+ AMI = "ami-#{AMI_10_04}"
13
+
14
+ def self.configured?
15
+ ENV["TELEPORT_ACCESS_KEY_ID"] && ENV["TELEPORT_SECRET_ACCESS_KEY"] && ENV["TELEPORT_SSH_KEY"]
16
+ end
17
+
18
+ def self.message
19
+ <<EOF
20
+ ------------------------------------------------------------------------
21
+ If you want to test against EC2, do the following:
22
+
23
+ 1. Create a "teleport" keypair on EC2.
24
+ 2. Set the TELEPORT_ACCESS_KEY_ID, TELEPORT_SECRET_ACCESS_KEY and
25
+ TELEPORT_SSH_KEY environment variables.
26
+
27
+ End-to-end tests that rely on EC2 will be skipped in the meantime.
28
+ ------------------------------------------------------------------------
29
+ EOF
30
+ end
31
+
32
+ #
33
+ # specs call this
34
+ #
35
+
36
+ def ec2
37
+ controller = nil
38
+ before(:all) do
39
+ if ENV["TELEPORT_IP"]
40
+ $ec2_ip_address = ENV["TELEPORT_IP"]
41
+ else
42
+ controller = Controller.new
43
+ controller.stop
44
+ $ec2_ip_address = controller.start
45
+ end
46
+ end
47
+ after(:all) do
48
+ controller.stop if controller
49
+ end
50
+ end
51
+
52
+ #
53
+ # this controller class does all the work
54
+ #
55
+
56
+ class Controller
57
+ def initialize
58
+ raise "not configured" if !Support::Ec2::configured?
59
+ @ec2 = AWS::EC2::Base.new(:access_key_id => ENV["TELEPORT_ACCESS_KEY_ID"], :secret_access_key => ENV["TELEPORT_SECRET_ACCESS_KEY"])
60
+ end
61
+
62
+ def start
63
+ puts "Running new ec2 instance..."
64
+ # setup security group and allow ssh
65
+ begin
66
+ @ec2.create_security_group(:group_name => GROUP, :group_description => GROUP)
67
+ rescue AWS::InvalidGroupDuplicate
68
+ # ignore
69
+ end
70
+ @ec2.authorize_security_group_ingress(:group_name => GROUP, :ip_protocol => "tcp", :from_port => 22, :to_port => 22)
71
+
72
+ # create the instance
73
+ @ec2.run_instances(:image_id => AMI, :instance_type => "m1.large", :key_name => KEYPAIR, :security_group => GROUP)
74
+
75
+ # wait for the new instance to start
76
+ puts "Waiting for ec2 instance to start..."
77
+ while true
78
+ sleep 3
79
+ instance = describe_instances.first
80
+ status = instance["instanceState"]["name"]
81
+ puts " #{instance["instanceId"]}: #{status}"
82
+ break if status == "running"
83
+ end
84
+
85
+ # return the ip address
86
+ ip = instance["ipAddress"]
87
+ puts " #{instance["instanceId"]}: #{ip}"
88
+ puts " sleeping to give ssh a chance to start..."
89
+ sleep 10
90
+ ip
91
+ end
92
+
93
+ def stop
94
+ puts "Terminating existing ec2 instances..."
95
+ ids = describe_instances.map { |i| i["instanceId"] }
96
+ if !ids.empty?
97
+ puts " terminate: #{ids.join(" ")}"
98
+ @ec2.terminate_instances(:instance_id => ids)
99
+ end
100
+ end
101
+
102
+ def describe_instances
103
+ list = []
104
+ hash = @ec2.describe_instances
105
+ if hash = hash["reservationSet"]
106
+ list = hash["item"].map { |i| i["instancesSet"]["item"] }.flatten
107
+ end
108
+ # cull stuff we don't care about
109
+ list = list.select { |i| i["keyName"] == KEYPAIR }
110
+ list = list.select { |i| i["instanceState"]["name"] !~ /terminated|shutting-down/ }
111
+ list
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,29 @@
1
+ # http://stackoverflow.com/questions/5118745/is-systemexit-a-special-kind-of-exception
2
+
3
+ module ExitCodeMatchers
4
+ RSpec::Matchers.define :exit_with_code do |code|
5
+ actual = nil
6
+ match do |block|
7
+ begin
8
+ block.call
9
+ rescue SystemExit => e
10
+ actual = e.status
11
+ end
12
+ actual && actual == code
13
+ end
14
+ failure_message_for_should do |block|
15
+ "expected block to call exit(#{code}) but exit" +
16
+ (actual.nil? ? " not called" : "(#{actual}) was called")
17
+ end
18
+ failure_message_for_should_not do |block|
19
+ "expected block not to call exit(#{code})"
20
+ end
21
+ description do
22
+ "expect block to call exit(#{code})"
23
+ end
24
+ end
25
+ end
26
+
27
+ RSpec.configure do |config|
28
+ config.include(ExitCodeMatchers)
29
+ end
@@ -0,0 +1,25 @@
1
+ # run inside a specific dir
2
+ module Support
3
+ module Telfile
4
+ TMP = "/tmp/teleport_spec"
5
+
6
+ def telfile(contents = nil, &block)
7
+ pwd = nil
8
+ before(:all) do
9
+ pwd = Dir.pwd
10
+ `rm -rf #{TMP} && mkdir -p #{TMP}`
11
+ Dir.chdir(TMP)
12
+ File.open("Telfile", "w") do |f|
13
+ if block
14
+ contents = block.call
15
+ end
16
+ f.puts(contents)
17
+ end
18
+ end
19
+
20
+ after(:all) do
21
+ Dir.chdir(pwd)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,64 @@
1
+ require "spec_helper"
2
+
3
+ describe Teleport::Config do
4
+ context "with a blank Telfile" do
5
+ telfile("")
6
+
7
+ let(:config) do
8
+ Teleport::Config.new
9
+ end
10
+ it "defaults to the current username" do
11
+ config.user.should == `whoami`.strip
12
+ end
13
+ it "defaults to the first vm in RUBIES" do
14
+ config.ruby.should == Teleport::Config::RUBIES.first
15
+ end
16
+ end
17
+
18
+ context "with a simple Telfile" do
19
+ telfile do
20
+ <<EOF
21
+ user "somebody"
22
+ ruby "1.8.7"
23
+
24
+ role :master, :packages => %w(nginx)
25
+ role :slave, :packages => %w(memcached)
26
+ server "one", :role => :master, :packages => %w(strace)
27
+ server "two", :role => :slave, :packages => %w(telnet)
28
+ packages %w(atop)
29
+ apt "blah blah blah", :key => "123"
30
+
31
+ before_install do
32
+ puts "before_install running"
33
+ end
34
+
35
+ after_install do
36
+ puts "after_install running"
37
+ end
38
+ EOF
39
+ end
40
+
41
+ let(:config) do
42
+ Teleport::Config.new
43
+ end
44
+ it "has the master role" do
45
+ config.role(:master).name.should == :master
46
+ config.role(:master).packages.should == %w(nginx)
47
+ end
48
+ it "has server one" do
49
+ config.server("one").name.should == "one"
50
+ config.server("one").packages.should == %w(strace)
51
+ end
52
+ it "has default packages" do
53
+ config.packages.should == %w(atop)
54
+ end
55
+ it "has callbacks" do
56
+ config.callbacks[:before_install].should_not == nil
57
+ config.callbacks[:after_install].should_not == nil
58
+ end
59
+ it "has an apt line" do
60
+ config.apt.first.line.should == "blah blah blah"
61
+ config.apt.first.options[:key].should == "123"
62
+ end
63
+ end
64
+ end
@@ -1,4 +1,5 @@
1
- $:.push File.expand_path("../lib", __FILE__)
1
+ $LOAD_PATH << File.expand_path("../lib", __FILE__)
2
+
2
3
  require "teleport/version"
3
4
 
4
5
  Gem::Specification.new do |s|
@@ -8,11 +9,17 @@ Gem::Specification.new do |s|
8
9
  s.authors = ["Adam Doppelt"]
9
10
  s.email = ["amd@gurge.com"]
10
11
  s.homepage = "http://github.com/rglabs/teleport"
11
- s.summary = %Q{Teleport - opinionated Ubuntu server setup with Ruby.}
12
- s.description = %Q{Easy Ubuntu server setup via teleportation.}
12
+ s.summary = "Teleport - opinionated Ubuntu server setup with Ruby."
13
+ s.description = "Easy Ubuntu server setup via teleportation."
13
14
 
14
15
  s.rubyforge_project = "teleport"
15
16
 
17
+ s.add_development_dependency("amazon-ec2")
18
+ s.add_development_dependency("awesome_print")
19
+ s.add_development_dependency("rake")
20
+ s.add_development_dependency("rdoc", ["~> 3.9"])
21
+ s.add_development_dependency("rspec", ["~> 2.6"])
22
+
16
23
  s.files = `git ls-files`.split("\n")
17
24
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
25
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: teleport
3
3
  version: !ruby/object:Gem::Version
4
- hash: 23
4
+ hash: 21
5
5
  prerelease:
6
6
  segments:
7
7
  - 1
8
8
  - 0
9
- - 0
10
- version: 1.0.0
9
+ - 1
10
+ version: 1.0.1
11
11
  platform: ruby
12
12
  authors:
13
13
  - Adam Doppelt
@@ -15,10 +15,81 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2011-08-03 00:00:00 -07:00
18
+ date: 2011-08-10 00:00:00 -07:00
19
19
  default_executable:
20
- dependencies: []
21
-
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: amazon-ec2
23
+ type: :development
24
+ version_requirements: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 3
30
+ segments:
31
+ - 0
32
+ version: "0"
33
+ requirement: *id001
34
+ prerelease: false
35
+ - !ruby/object:Gem::Dependency
36
+ name: awesome_print
37
+ type: :development
38
+ version_requirements: &id002 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ hash: 3
44
+ segments:
45
+ - 0
46
+ version: "0"
47
+ requirement: *id002
48
+ prerelease: false
49
+ - !ruby/object:Gem::Dependency
50
+ name: rake
51
+ type: :development
52
+ version_requirements: &id003 !ruby/object:Gem::Requirement
53
+ none: false
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ hash: 3
58
+ segments:
59
+ - 0
60
+ version: "0"
61
+ requirement: *id003
62
+ prerelease: false
63
+ - !ruby/object:Gem::Dependency
64
+ name: rdoc
65
+ type: :development
66
+ version_requirements: &id004 !ruby/object:Gem::Requirement
67
+ none: false
68
+ requirements:
69
+ - - ~>
70
+ - !ruby/object:Gem::Version
71
+ hash: 21
72
+ segments:
73
+ - 3
74
+ - 9
75
+ version: "3.9"
76
+ requirement: *id004
77
+ prerelease: false
78
+ - !ruby/object:Gem::Dependency
79
+ name: rspec
80
+ type: :development
81
+ version_requirements: &id005 !ruby/object:Gem::Requirement
82
+ none: false
83
+ requirements:
84
+ - - ~>
85
+ - !ruby/object:Gem::Version
86
+ hash: 15
87
+ segments:
88
+ - 2
89
+ - 6
90
+ version: "2.6"
91
+ requirement: *id005
92
+ prerelease: false
22
93
  description: Easy Ubuntu server setup via teleportation.
23
94
  email:
24
95
  - amd@gurge.com
@@ -38,12 +109,19 @@ files:
38
109
  - lib/teleport.rb
39
110
  - lib/teleport/config.rb
40
111
  - lib/teleport/constants.rb
112
+ - lib/teleport/infer.rb
41
113
  - lib/teleport/install.rb
42
114
  - lib/teleport/main.rb
43
115
  - lib/teleport/mirror.rb
44
116
  - lib/teleport/run.sh
45
117
  - lib/teleport/util.rb
46
118
  - lib/teleport/version.rb
119
+ - spec/end_to_end_spec.rb
120
+ - spec/spec_helper.rb
121
+ - spec/support/ec2.rb
122
+ - spec/support/exit_code.rb
123
+ - spec/support/telfile.rb
124
+ - spec/unit/teleport/config_spec.rb
47
125
  - teleport.gemspec
48
126
  has_rdoc: true
49
127
  homepage: http://github.com/rglabs/teleport