teleport 1.0.0 → 1.0.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/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