teleport 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ rdoc
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Adam Doppelt
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,62 @@
1
+ ## Welcome to Teleport
2
+
3
+ Teleport is a lightweight way to set up Ubuntu machines. The name derives from the mechanism that teleport uses to setup the target machine - it copies itself onto the target machine via ssh and then runs itself there. In effect, it "teleports" to the target. This design makes it possible for teleport to bootstrap itself onto a fresh machine. There's no need to install ruby or anything else by hand.
4
+
5
+ Teleport strives to be **idempotent** - you can run it repeatedly without changing the result. In other words, as you build up your teleport config file you can generally run it over and over again without fear of breaking the target machine.
6
+
7
+ Teleport is great for managing a small number of hosted machines, either dedicated or in the cloud. Due to it's opinionated nature and limited scope you may find that it works better for you than other, more complicated tools.
8
+
9
+ At the moment Teleport supports **Ubuntu 10.04/10.10/11.04 with Ruby 1.8.7, 1.9.2, or [REE](http://www.rubyenterpriseedition.com/)**.
10
+
11
+ ## Getting Started
12
+
13
+ 1. Install Teleport on your local machine.
14
+
15
+ ```
16
+ $ sudo gem install teleport --pre
17
+ ```
18
+
19
+ 1. Create a `Telfile` config file. Here's a simple example. Note that we actually define two machines, `server_app1` and `server_db1`:
20
+
21
+ ```
22
+ $ mkdir ~/teleport
23
+ $ cd ~/teleport
24
+ ```
25
+
26
+ Put this into `~/teleport/Telfile`:
27
+
28
+ ``` ruby
29
+ user "admin"
30
+ ruby "1.9.2"
31
+ apt "deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen", :key => "7F0CAB10"
32
+ role :app, :packages => [:memcached]
33
+ role :db, :packages => [:mongodb-10gen]
34
+ server "server_app1", :role => :app
35
+ server "server_db1", :role => :db
36
+ packages [:atop, :emacs, :gcc]
37
+ ```
38
+
39
+ 1. You'll want to copy files to your new machines too. Put the files into your teleport directory. For example, maybe you want to automatically have your `.bashrc` and `.emacs` files copied to your new server. You'll want the `memcached` and `mongodb` config files too. Here's what your teleport directory should look like:
40
+
41
+ ```
42
+ Telfile
43
+ files/home/admin/.bashrc
44
+ files/home/admin/.emacs
45
+ files_app/etc/default/memcached
46
+ files_app/etc/memcached.conf
47
+ files_db/etc/mongodb.conf
48
+ ```
49
+
50
+ 1. Now run Teleport:
51
+
52
+ ```
53
+ $ teleport server_app1
54
+ ```
55
+
56
+ Teleport will ssh to the machine and set it up per your instructions.
57
+
58
+ ## Full Documentation
59
+
60
+ Full docs are in the wiki:
61
+
62
+ https://github.com/rglabs/teleport/wiki
@@ -0,0 +1,10 @@
1
+ require "bundler"
2
+ require "rake/rdoctask"
3
+
4
+ Bundler::GemHelper.install_tasks
5
+
6
+ Rake::RDocTask.new do |rdoc|
7
+ rdoc.rdoc_dir = "rdoc"
8
+ rdoc.title = "teleport #{Teleport::VERSION}"
9
+ rdoc.rdoc_files.include("lib/**/*.rb")
10
+ end
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # in lieu of -w, since we're using env to startup
4
+ $VERBOSE = true
5
+
6
+ require "teleport"
7
+ Teleport::Main.new
@@ -0,0 +1,7 @@
1
+ require "teleport/constants"
2
+ require "teleport/version"
3
+ require "teleport/util"
4
+ require "teleport/config"
5
+ require "teleport/mirror"
6
+ require "teleport/install"
7
+ require "teleport/main"
@@ -0,0 +1,129 @@
1
+ module Teleport
2
+ # This class parses Telfile, and includes DSL and the models.
3
+ class Config
4
+ RUBIES = ["1.9.2", "REE", "1.8.7"]
5
+ PATH = "Telfile"
6
+
7
+ attr_accessor :user, :ruby, :roles, :servers, :apt, :packages, :callbacks, :dsl
8
+
9
+ def initialize
10
+ @roles = []
11
+ @servers = []
12
+ @apt = []
13
+ @packages = []
14
+ @callbacks = { }
15
+
16
+ @dsl = DSL.new(self)
17
+ @dsl.instance_eval(File.read(PATH), PATH)
18
+
19
+ @user ||= Util.whoami
20
+ @ruby ||= RUBIES.first
21
+ end
22
+
23
+ def role(n)
24
+ @roles.find { |i| i.name == n.to_sym }
25
+ end
26
+
27
+ def server(n)
28
+ @servers.find { |i| i.name == n.to_s }
29
+ end
30
+
31
+ # The model for role in the Telfile.
32
+ class Role
33
+ attr_reader :name, :options, :packages
34
+
35
+ def initialize(name, options)
36
+ raise "role name must be a sym" if !name.is_a?(Symbol)
37
+ raise "role options must be a hash" if !options.is_a?(Hash)
38
+
39
+ @name, @options, @packages = name, options, []
40
+ if p = @options.delete(:packages)
41
+ raise "role :packages must be an array" if !p.is_a?(Array)
42
+ @packages = p
43
+ end
44
+ end
45
+ end
46
+
47
+ # The model for server in the Telfile.
48
+ class Server
49
+ attr_reader :name, :options, :packages
50
+
51
+ def initialize(name, options)
52
+ raise "server name must be a string" if !name.is_a?(String)
53
+ raise "server options must be a hash" if !options.is_a?(Hash)
54
+ raise "server :role must be a sym" if !options[:role].is_a?(Symbol)
55
+
56
+ @name, @options, @packages = name, options, []
57
+ if p = @options.delete(:packages)
58
+ raise "server :packages must be an array" if !p.is_a?(Array)
59
+ @packages = p
60
+ end
61
+ end
62
+ end
63
+
64
+ # The model for an apt line in the Telfile.
65
+ class Apt
66
+ attr_reader :line, :options
67
+
68
+ def initialize(line, options)
69
+ raise "apt line must be a string" if !line.is_a?(String)
70
+ raise "apt options must be a hash" if !options.is_a?(Hash)
71
+ @line, @options = line, options
72
+
73
+ if k = @options[:key]
74
+ raise "apt :key must be an String" if !k.is_a?(String)
75
+ end
76
+ end
77
+ end
78
+
79
+ # DSL used when parsing Telfile.
80
+ class DSL
81
+ def initialize(config)
82
+ @config = config
83
+ end
84
+
85
+ def ruby(v)
86
+ raise "ruby called twice" if @config.ruby
87
+ raise "ruby must be a string" if !v.is_a?(String)
88
+ raise "don't recognize ruby #{v.inspect}." if !Config::RUBIES.include?(v)
89
+ @config.ruby = v
90
+ end
91
+
92
+ def user(v)
93
+ raise "user called twice" if @config.user
94
+ raise "user must be a string" if !v.is_a?(String)
95
+ @config.user = v
96
+ end
97
+
98
+ def role(name, options = {})
99
+ raise "role #{name.inspect} defined twice" if @config.roles.any? { |i| i.name == name }
100
+ @config.roles << Role.new(name, options)
101
+ end
102
+
103
+ def server(name, options = {})
104
+ raise "server #{name.inspect} defined twice" if @config.servers.any? { |i| i.name == name }
105
+ @config.servers << Server.new(name, options)
106
+ end
107
+
108
+ def apt(line, options = {})
109
+ @config.apt << Apt.new(line, options)
110
+ end
111
+
112
+ def packages(*list)
113
+ @config.packages += list.flatten
114
+ end
115
+
116
+ %w(install user packages files).each do |op|
117
+ %w(before after).each do |before_after|
118
+ callback = "#{before_after}_#{op}".to_sym
119
+ define_method(callback) do |&block|
120
+ if @config.callbacks[callback]
121
+ raise "you already defined the #{callback} callback"
122
+ end
123
+ @config.callbacks[callback] = block
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,19 @@
1
+ module Constants
2
+ # The temp directory that teleport uses on the target machine.
3
+ DIR = "/tmp/_teleported"
4
+
5
+ # The teleport gem within our temp directory.
6
+ GEM = "#{DIR}/gem"
7
+
8
+ # Directory where user install data (Telfile and friends) is stored.
9
+ DATA = "#{DIR}/data"
10
+
11
+ # Directory where user install files are stored.
12
+ FILES = "#{DIR}/data/files"
13
+
14
+ # Name of the public key to install on the target machine.
15
+ PUBKEY = "id_teleport.pub"
16
+
17
+ # Minimum version of rubygems to install.
18
+ RUBYGEMS = "1.8.5"
19
+ end
@@ -0,0 +1,227 @@
1
+ module Teleport
2
+ # Class that performs the install on the target machine.
3
+ class Install
4
+ include Constants
5
+ include Util
6
+ include Mirror
7
+
8
+ def initialize(config)
9
+ @config = config
10
+ run_verbose!
11
+ _read_config
12
+
13
+ # setup @config constants
14
+ Config::DSL.const_set("HOST", @host)
15
+ Config::DSL.const_set("USER", @config.user)
16
+ Config::DSL.const_set("ROLE", @role && @role.name)
17
+
18
+ # add mixins
19
+ @config.dsl.extend(Mirror)
20
+ @config.dsl.extend(Util)
21
+ @config.dsl.run_verbose!
22
+
23
+ _with_callback(:install) do
24
+ _gems
25
+ _hostname
26
+ _with_callback(:user) do
27
+ _create_user
28
+ end
29
+ _apt
30
+ _with_callback(:packages) do
31
+ _packages
32
+ end
33
+ _with_callback(:files) do
34
+ _files
35
+ end
36
+ end
37
+ end
38
+
39
+ protected
40
+
41
+ def _read_config
42
+ # read DIR/config to get CONFIG_HOST (and set @host)
43
+ config_file = { }
44
+ File.readlines("config").each do |i|
45
+ if i =~ /CONFIG_([^=]+)='([^']*)'/
46
+ config_file[$1.downcase.to_sym] = $2
47
+ end
48
+ end
49
+ @host = config_file[:host]
50
+
51
+ # do we have a server object?
52
+ @server = @config.server(@host)
53
+ if !@server && !@config.servers.empty?
54
+ fatal "Hm. I couldn't find server #{@host.inspect} in teleport.rb."
55
+ end
56
+
57
+ @role = nil
58
+ if @server && (role_name = @server.options[:role])
59
+ @role = @config.role(role_name)
60
+ if !@role
61
+ fatal "Hm. I couldn't find role #{role_name.inspect} in teleport.rb."
62
+ end
63
+ end
64
+ end
65
+
66
+ def _gems
67
+ banner "Gems..."
68
+
69
+ # update rubygems if necessary
70
+ gem_version = `gem --version`.strip.split(".").map(&:to_i)
71
+ if (gem_version <=> RUBYGEMS.split(".").map(&:to_i)) == -1
72
+ banner "Upgrading rubygems..."
73
+ run "gem update --system"
74
+ end
75
+
76
+ # uninstall all gems except for bundler
77
+ gems = `gem list`.split("\n").map { |i| i.split.first }
78
+ gems.delete("bundler")
79
+ if !gems.empty?
80
+ banner "Uninstalling #{gems.length} system gems..."
81
+ gems.each do |i|
82
+ run "gem uninstall -aIx #{i}"
83
+ end
84
+ end
85
+
86
+ # install bundler
87
+ gem_if_necessary("bundler")
88
+ end
89
+
90
+ def _hostname
91
+ banner "Hostname..."
92
+ old_hostname = `hostname`.strip
93
+ return if old_hostname == @host
94
+
95
+ puts "setting hostname to #{@host} (it was #{old_hostname})..."
96
+ File.open("/etc/hostname", "w") do |f|
97
+ f.write @host
98
+ end
99
+ run "hostname -F /etc/hostname"
100
+
101
+ puts "adding #{@host} to /etc/hosts ..."
102
+ _rewrite("/etc/hosts") do |fout|
103
+ hosts = File.read("/etc/hosts")
104
+
105
+ # old_hostname => @host
106
+ hosts.gsub!(_etc_hosts_regex(old_hostname), "\\1#{@host}\\2")
107
+ if hosts !~ _etc_hosts_regex(@host)
108
+ # not found? append to localhost
109
+ hosts.gsub!(_etc_hosts_regex("localhost"), "\\1localhost #{@host}\\2")
110
+ if hosts !~ _etc_hosts_regex(@host)
111
+ puts " Hm. I couldn't add it, unfortunately. You'll have to do it manually."
112
+ end
113
+ end
114
+
115
+ fout.write hosts
116
+ end
117
+ end
118
+
119
+ def _create_user
120
+ user = @config.user
121
+
122
+ banner "Creating #{user} account..."
123
+ # create the account
124
+ if !File.directory?("/home/#{user}")
125
+ run "useradd --create-home --shell /bin/bash --groups adm #{user}"
126
+ end
127
+
128
+ # try to sudo - if it fails, add user to sudoers
129
+ if fails?("sudo -u #{user} sudo -n echo gub")
130
+ puts "setting up sudoers..."
131
+ File.open("/etc/sudoers", "a") do |f|
132
+ f.puts <<EOF
133
+ # added by teleport
134
+ #{user} ALL=(ALL) NOPASSWD: ALL
135
+ EOF
136
+ end
137
+ end
138
+
139
+ # ssh key, if present
140
+ if File.exists?(PUBKEY)
141
+ authorized_keys = "/home/#{user}/.ssh/authorized_keys"
142
+ if !File.exists?(authorized_keys)
143
+ puts "adding #{PUBKEY} to authorized_keys..."
144
+ mkdir_if_necessary(File.dirname(authorized_keys), user, 0700)
145
+ cp(PUBKEY, authorized_keys, user, 0600)
146
+ end
147
+ end
148
+ end
149
+
150
+ def _apt
151
+ return if @config.apt.empty?
152
+ banner "Apt..."
153
+
154
+ dirty = false
155
+
156
+ # keys
157
+ keys = @config.apt.map { |i| i.options[:key] }.compact
158
+ keys.each do |i|
159
+ if fails?("apt-key list | grep #{i}")
160
+ run "apt-key adv --keyserver keyserver.ubuntu.com --recv #{i}"
161
+ dirty = true
162
+ end
163
+ end
164
+
165
+ # teleport.list
166
+ apt = @config.apt.sort_by { |i| i.line }
167
+ rewrite = _rewrite("/etc/apt/sources.list.d/teleport.list") do |f|
168
+ f.puts "# Generated by teleport"
169
+ apt.each do |i|
170
+ f.puts i.line
171
+ end
172
+ end
173
+
174
+ if dirty || rewrite
175
+ run "apt-get update"
176
+ end
177
+ end
178
+
179
+ def _packages
180
+ banner "Packages..."
181
+ list = @config.packages
182
+ list += @role.packages if @role
183
+ list += @server.packages if @server
184
+ list.sort.each { |i| package_if_necessary(i) }
185
+ end
186
+
187
+ def _files
188
+ banner "Files..."
189
+ files = ["files"]
190
+ files << "files_#{@role.name}" if @role
191
+ files.each do |i|
192
+ install_dir(i) if File.exists?("#{DATA}/#{i}")
193
+ end
194
+ end
195
+
196
+ protected
197
+
198
+ def _with_callback(op, &block)
199
+ if before = @config.callbacks["before_#{op}".to_sym]
200
+ before.call
201
+ end
202
+ yield
203
+ if after = @config.callbacks["after_#{op}".to_sym]
204
+ after.call
205
+ end
206
+ end
207
+
208
+ def _rewrite(path, &block)
209
+ tmp = "#{path}.tmp"
210
+ begin
211
+ File.open(tmp, "w") { |f| yield(f) }
212
+ if !File.exists?(path) || different?(path, tmp)
213
+ copy_perms(path, tmp) if File.exists?(path)
214
+ mv(tmp, path)
215
+ return true
216
+ end
217
+ ensure
218
+ File.unlink(tmp) if File.exists?(tmp)
219
+ end
220
+ false
221
+ end
222
+
223
+ def _etc_hosts_regex(host)
224
+ /^([^#]+[ \t])#{Regexp.escape(host)}([ \t]|$)/
225
+ end
226
+ end
227
+ end
@@ -0,0 +1,110 @@
1
+ require "erb"
2
+ require "getoptlong"
3
+
4
+ module Teleport
5
+ # The main class for the teleport command line.
6
+ class Main
7
+ include Constants
8
+ include Util
9
+
10
+ TAR = "#{DIR}.tgz"
11
+
12
+ attr_accessor :host, :options
13
+
14
+ 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
26
+
27
+ case cmd
28
+ when :teleport
29
+ teleport(ARGV.shift)
30
+ when :install
31
+ install
32
+ end
33
+ end
34
+
35
+ def usage(exit_code)
36
+ puts "Usage: teleport <hostname>"
37
+ puts " --help print this help text"
38
+ exit(exit_code)
39
+ end
40
+
41
+ def read_config
42
+ if !File.exists?(Config::PATH)
43
+ fatal("Sadly, I can't find #{Config::PATH} here. Please create one.")
44
+ end
45
+ @config = Config.new
46
+ end
47
+
48
+ def assemble_tgz(host)
49
+ banner "Assembling #{TAR}..."
50
+ rm_and_mkdir(DIR)
51
+
52
+ # gem
53
+ run("cp", ["-r", "#{File.dirname(__FILE__)}/../../lib", GEM])
54
+ # data
55
+ run("cp", ["-r", ".", DATA])
56
+ # config.sh
57
+ File.open("#{DIR}/config", "w") do |f|
58
+ f.puts("CONFIG_HOST='#{host}'")
59
+ f.puts("CONFIG_RUBY='#{@config.ruby}'")
60
+ f.puts("CONFIG_RUBYGEMS='#{RUBYGEMS}'")
61
+ end
62
+ # keys
63
+ ssh_key = "#{ENV["HOME"]}/.ssh/#{PUBKEY}"
64
+ if File.exists?(ssh_key)
65
+ run("cp", [ssh_key, DIR])
66
+ else
67
+ puts "Could not find #{ssh_key} - skipping."
68
+ end
69
+
70
+ Dir.chdir(File.dirname(DIR)) do
71
+ run("tar", ["cfpz", TAR, File.basename(DIR)])
72
+ end
73
+ end
74
+
75
+ def ssh_tgz(host)
76
+ begin
77
+ banner "scp #{TAR} to #{host}:#{TAR}..."
78
+ run "scp #{TAR} #{host}:#{TAR}"
79
+
80
+ cmd = [
81
+ "cd /tmp",
82
+ "(sudo -n echo gub > /dev/null 2> /dev/null || (echo `whoami` could not sudo. && exit 1))",
83
+ "sudo rm -rf #{DIR}",
84
+ "sudo tar xfpz #{TAR}",
85
+ "sudo #{DIR}/gem/teleport/run.sh"
86
+ ]
87
+ banner "ssh to #{host} and run..."
88
+ run("ssh", [host, cmd.join(" && ")])
89
+ rescue RunError
90
+ fatal("Failed!")
91
+ end
92
+ banner "Success!"
93
+ end
94
+
95
+ def teleport(host)
96
+ read_config
97
+ usage(1) if !host
98
+ assemble_tgz(host)
99
+ ssh_tgz(host)
100
+ end
101
+
102
+ def install
103
+ Dir.chdir(DATA) do
104
+ read_config
105
+ end
106
+ Install.new(@config)
107
+ end
108
+
109
+ end
110
+ end
@@ -0,0 +1,103 @@
1
+ require "erb"
2
+
3
+ module Teleport
4
+ # Helper module for recursively mirroring files from a src to a dst.
5
+ module Mirror
6
+ include Constants
7
+
8
+ # Install file from the teleport data directory into the normal
9
+ # filesystem. Path can use a few different formats:
10
+ #
11
+ # * #{DATA}/xyz - Full path into the data directory
12
+ # * files/xyz - Path into the #{DATA}/files directory
13
+ # * files_role/xyz - Path into a role directory
14
+ # * xyz - Assumed to be #{DATA}/files/xyz
15
+ #
16
+ # Note that the path can be an erb file. For example,
17
+ # "etc/hosts.erb" will be installed as "etc/hosts".
18
+ #
19
+ # Returns true if the file was installed, false if the file had
20
+ # previously been installed and no change was required.
21
+ def install_file(path)
22
+ path, dst = path_to_src(path), path_to_dst(path)
23
+
24
+ # run erb if necessary
25
+ if path =~ /#{DATA}\/(.*)\.erb$/
26
+ tmp = "#{DATA}/#{$1.gsub('/', '_')}"
27
+ dst = dst.gsub(/\.erb$/, "")
28
+ File.open(tmp, "w") do |f|
29
+ f.write ERB.new(File.read(path)).result(binding)
30
+ end
31
+ copy_metadata(path, tmp)
32
+ path = tmp
33
+ end
34
+
35
+ cp_if_necessary(path, dst, user_for_file(dst), mode_for_file(dst))
36
+ end
37
+
38
+ # Install directory from the teleport data directory into the
39
+ # normal filesystem. Path can use a few different formats:
40
+ #
41
+ # * #{DATA}/xyz - Full path into the data directory
42
+ # * files/xyz - Path into the #{DATA}/files directory
43
+ # * files_role/xyz - Path into a role directory
44
+ # * xyz - Assumed to be #{DATA}/files/xyz
45
+ #
46
+ # Returns true if the dir was installed, false if the dir had
47
+ # previously been installed and no change was required.
48
+ def install_dir(path)
49
+ dirty = false
50
+
51
+ path, dst = path_to_src(path), path_to_dst(path)
52
+ mkdir_if_necessary(dst, user_for_file(dst)) if !dst.empty?
53
+
54
+ files = Dir.new(path).to_a.sort
55
+ files.delete_if { |i| i == "." || i == ".." || i =~ /^.#/ }
56
+ files.each do |i|
57
+ i = "#{path}/#{i}"
58
+ if File.directory?(i)
59
+ dirty = install_dir(i) || dirty
60
+ else
61
+ dirty = install_file(i) || dirty
62
+ end
63
+ end
64
+
65
+ dirty
66
+ end
67
+
68
+ protected
69
+
70
+ def normalize_path(path)
71
+ case path
72
+ when /^#{DATA}/
73
+ # already absolute - do nothing
74
+ when /^files/
75
+ path = "#{DATA}/#{path}"
76
+ else
77
+ path = "#{DATA}/files/#{path}"
78
+ end
79
+ path
80
+ end
81
+
82
+ def path_to_src(path)
83
+ normalize_path(path)
84
+ end
85
+
86
+ def path_to_dst(path)
87
+ path = normalize_path(path)
88
+ path = path[%r{#{DATA}/files[^/]*(.*)}, 1]
89
+ path
90
+ end
91
+
92
+ def user_for_file(f)
93
+ f[%r{^/home/([^/]+)}, 1] || "root"
94
+ end
95
+
96
+ def mode_for_file(f)
97
+ case f
98
+ when %r{sudoers} then 0440
99
+ when %r{/\.ssh/} then 0400
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,123 @@
1
+ #!/bin/bash
2
+
3
+ #
4
+ # This script runs on the target machine. First it installs ruby if
5
+ # necessary, then it runs teleport.
6
+ #
7
+
8
+ # bail on errors
9
+ set -eu
10
+
11
+
12
+
13
+ #
14
+ # functions
15
+ #
16
+
17
+ function banner() {
18
+ printf '\e[1;37;43m[%s] %-72s\e[0m\n' `date '+%H:%M:%S'` "run.sh: $1"
19
+ }
20
+
21
+ function fatal() {
22
+ printf '\e[1;37;41m[%s] %-72s\e[0m\n' `date '+%H:%M:%S'` "run.sh: error - $1"
23
+ exit 1
24
+ }
25
+
26
+ function install_ruby() {
27
+ banner "apt-get update / upgrade..."
28
+ sudo apt-get update
29
+ sudo apt-get -y upgrade
30
+ sudo apt-get install -y wget libreadline5-dev
31
+
32
+ banner "installing Ruby $CONFIG_RUBY..."
33
+ case $CONFIG_RUBY in
34
+ 1.8.7 ) install_ruby_187 ;;
35
+ 1.9.2 ) install_ruby_192 ;;
36
+ REE ) install_ruby_ree ;;
37
+ * ) fatal "unknown ruby ($CONFIG_RUBY)" ;;
38
+ esac
39
+ }
40
+
41
+ function install_ruby_187() {
42
+ sudo apt-get -y install irb libopenssl-ruby libreadline-ruby rdoc ri ruby ruby-dev
43
+
44
+ wget http://production.cf.rubygems.org/rubygems/rubygems-$CONFIG_RUBYGEMS.tgz
45
+ tar xfpz rubygems-$CONFIG_RUBYGEMS.tgz
46
+ (cd rubygems-$CONFIG_RUBYGEMS ; ruby setup.rb)
47
+ ln -s /usr/bin/gem1.8 /usr/bin/gem
48
+ }
49
+
50
+ function install_ruby_192() {
51
+ local patch=p180
52
+
53
+ # see http://threebrothers.org/brendan/blog/ruby-1-9-2-on-ubuntu-11-04/
54
+ sudo apt-get install -y bison build-essential checkinstall libffi5 libssl-dev libyaml-dev zlib1g-dev
55
+
56
+ wget http://ftp.ruby-lang.org/pub/ruby/1.9/ruby-1.9.2-$patch.tar.gz
57
+ tar xvzf ruby-1.9.2-$patch.tar.gz
58
+
59
+ cd ruby-1.9.2-$patch
60
+ ./configure --prefix=/usr/local \
61
+ --program-suffix=1.9.2 \
62
+ --with-ruby-version=1.9.2 \
63
+ --disable-install-doc
64
+ make
65
+ sudo checkinstall -D -y \
66
+ --fstrans=no \
67
+ --nodoc \
68
+ --pkgname="ruby1.9.2" \
69
+ --pkgversion="1.9.2-$patch" \
70
+ --provides="ruby"
71
+ cd ..
72
+
73
+ sudo update-alternatives --install /usr/local/bin/ruby ruby /usr/local/bin/ruby1.9.2 500 \
74
+ --slave /usr/local/bin/ri ri /usr/local/bin/ri1.9.2 \
75
+ --slave /usr/local/bin/irb irb /usr/local/bin/irb1.9.2 \
76
+ --slave /usr/local/bin/gem gem /usr/local/bin/gem1.9.2 \
77
+ --slave /usr/local/bin/erb erb /usr/local/bin/erb1.9.2 \
78
+ --slave /usr/local/bin/rdoc rdoc /usr/local/bin/rdoc1.9.2
79
+ }
80
+
81
+ function install_ruby_ree() {
82
+ local ree="ruby-enterprise_1.8.7-2011.03_${ARCH}_ubuntu10.04.deb"
83
+ wget http://rubyenterpriseedition.googlecode.com/files/$ree
84
+ sudo dpkg -i $ree
85
+ }
86
+
87
+
88
+ #
89
+ # main
90
+ #
91
+
92
+ # are we on Ubuntu?
93
+ if ! uname -a | grep -q Ubuntu ; then
94
+ fatal "Teleport only works with Ubuntu"
95
+ fi
96
+
97
+ # which version?
98
+ . /etc/lsb-release
99
+ case $($DISTRIB_RELEASE) in
100
+ 10.* ) ;; # nop
101
+ 11.04) ;; # nop
102
+ *)
103
+ banner "warning - Ubuntu $DISTRIB_RELEASE hasn't been tested with Teleport yet"
104
+ esac
105
+
106
+ # which architecture?
107
+ if [ $(uname -m) == "x86_64" ] ; then
108
+ ARCH=amd64
109
+ else
110
+ ARCH=i386
111
+ fi
112
+
113
+ # read our config
114
+ cd /tmp/_teleported
115
+ source ./config
116
+
117
+ # do we need to install ruby?
118
+ if ! which ruby > /dev/null ; then
119
+ install_ruby
120
+ fi
121
+
122
+ # run teleport!
123
+ ruby -I gem -r teleport -e "Teleport::Main.new(:install)"
@@ -0,0 +1,319 @@
1
+ require "cgi"
2
+ require "etc"
3
+ require "fileutils"
4
+
5
+ module Teleport
6
+ # Helper module for executing commands and printing stuff
7
+ # out.
8
+ #
9
+ # The general idea is to only print commands that are actually
10
+ # interesting. For example, mkdir_if_necessary won't print anything
11
+ # if the directory already exists. That way we can scan teleport
12
+ # output and see what changes were made without getting lost in
13
+ # repetitive commands that had no actual effect.
14
+ module Util
15
+ class RunError < StandardError ; end
16
+
17
+ extend self
18
+
19
+ RESET = "\e[0m"
20
+ RED = "\e[1;37;41m"
21
+ GREEN = "\e[1;37;42m"
22
+ YELLOW = "\e[1;37;43m"
23
+ BLUE = "\e[1;37;44m"
24
+ MAGENTA = "\e[1;37;45m"
25
+ CYAN = "\e[1;37;46m"
26
+
27
+ #
28
+ # running commands
29
+ #
30
+
31
+ # Make all commands echo before running.
32
+ def run_verbose!
33
+ @run_verbose = true
34
+ end
35
+
36
+ # Run a command, raise an error upon failure. Output goes to
37
+ # $stdout/$stderr.
38
+ def run(command, args = nil)
39
+ line = nil
40
+ if args
41
+ args = args.map(&:to_s)
42
+ line = "#{command} #{args.join(" ")}"
43
+ vputs line
44
+ system(command, *args)
45
+ else
46
+ line = command
47
+ vputs line
48
+ system(command)
49
+ end
50
+ if $? != 0
51
+ if $?.termsig == Signal.list["INT"]
52
+ raise "#{line} interrupted"
53
+ end
54
+ raise RunError, "#{line} failed : #{$?.to_i / 256}"
55
+ end
56
+ end
57
+
58
+ # Run a command, raise an error upon failure. The output is
59
+ # capture as a string and returned.
60
+ def run_capture(command, *args)
61
+ if !args.empty?
62
+ args = args.flatten.map { |i| shell_escape(i) }.join(" ")
63
+ command = "#{command} #{args}"
64
+ end
65
+ result = `#{command}`
66
+ if $? != 0
67
+ if $?.termsig == Signal.list["INT"]
68
+ raise "#{command} interrupted"
69
+ end
70
+ raise RunError, "#{command} failed : #{$?.to_i / 256} #{result.inspect}"
71
+ end
72
+ result
73
+ end
74
+
75
+ # Run a command but don't send any output to $stdout/$stderr.
76
+ def run_quietly(command, *args)
77
+ if !args.empty?
78
+ args = args.flatten.map { |i| shell_escape(i) }.join(" ")
79
+ command = "#{command} #{args}"
80
+ end
81
+ run("#{command} > /dev/null 2> /dev/null")
82
+ end
83
+
84
+ # Run a command, return true if it succeeds.
85
+ def succeeds?(command)
86
+ system("#{command} > /dev/null 2> /dev/null")
87
+ $? == 0
88
+ end
89
+
90
+ # Run a command, return true if it fails.
91
+ def fails?(command)
92
+ !succeeds?(command)
93
+ end
94
+
95
+ # Escape some text for the shell and enclose it in single quotes
96
+ # if necessary.
97
+ def shell_escape(s)
98
+ s = s.to_s
99
+ if s !~ /^[0-9A-Za-z+,.\/:=@_-]+$/
100
+ s = s.gsub("'") { "'\\''" }
101
+ s = "'#{s}'"
102
+ end
103
+ s
104
+ end
105
+
106
+ # Like mkdir -p. Optionally, set the owner and mode.
107
+ def mkdir(dir, owner = nil, mode = nil)
108
+ FileUtils.mkdir_p(dir, :verbose => verbose?)
109
+ chmod(dir, mode) if mode
110
+ chown(dir, owner) if owner
111
+ end
112
+
113
+ # mkdir only if the directory doesn't already exist. Optionally,
114
+ # set the owner and mode.
115
+ def mkdir_if_necessary(dir, owner = nil, mode = nil)
116
+ mkdir(dir, owner, mode) if !(File.exists?(dir) || File.symlink?(dir))
117
+ end
118
+
119
+ # rm a dir and recreate it.
120
+ def rm_and_mkdir(dir)
121
+ raise "don't do this" if dir == ""
122
+ run "rm -rf #{dir} && mkdir -p #{dir}"
123
+ end
124
+
125
+ # Are two files different?
126
+ def different?(a, b)
127
+ !FileUtils.compare_file(a, b)
128
+ end
129
+
130
+ # Copy perms from src file to dst.
131
+ def copy_perms(src, dst)
132
+ stat = File.stat(src)
133
+ File.chmod(stat.mode, dst)
134
+ end
135
+
136
+ # Copy perms and timestamps from src file to dst.
137
+ def copy_metadata(src, dst)
138
+ stat = File.stat(src)
139
+ File.chmod(stat.mode, dst)
140
+ File.utime(stat.atime, stat.mtime, dst)
141
+ end
142
+
143
+ # Copy file or dir from src to dst. Optionally, set the mode and
144
+ # owner of dst.
145
+ def cp(src, dst, owner = nil, mode = nil)
146
+ FileUtils.cp_r(src, dst, :preserve => true, :verbose => verbose?)
147
+ if owner && !File.symlink?(dst)
148
+ chown(dst, owner)
149
+ end
150
+ if mode
151
+ chmod(dst, mode)
152
+ end
153
+ end
154
+
155
+ # Copy file or dir from src to dst, but create the dst directory
156
+ # first if necessary. Optionally, set the mode and owner of dst.
157
+ def cp_with_mkdir(src, dst, owner = nil, mode = nil)
158
+ mkdir_if_necessary(File.dirname(dst))
159
+ cp(src, dst, owner, mode)
160
+ end
161
+
162
+ # Copy file or dir from src to dst, but ONLY if dst doesn't exist
163
+ # or has different contents than src. Optionally, set the mode and
164
+ # owner of dst.
165
+ def cp_if_necessary(src, dst, owner = nil, mode = nil)
166
+ if !File.exists?(dst) || different?(src, dst)
167
+ cp(src, dst, owner, mode)
168
+ true
169
+ end
170
+ end
171
+
172
+ # Move src to dst. Because this uses FileUtils, it works even if
173
+ # dst is on a different partition.
174
+ def mv(src, dst)
175
+ FileUtils.mv(src, dst, :verbose => verbose?)
176
+ end
177
+
178
+ # Move src to dst, but create the dst directory first if
179
+ # necessary.
180
+ def mv_with_mkdir(src, dst)
181
+ mkdir_if_necessary(File.dirname(dst))
182
+ mv(src, dst)
183
+ end
184
+
185
+ # Chown file to be owned by user.
186
+ def chown(file, user)
187
+ user = user.to_s
188
+ # who is the current owner?
189
+ @uids ||= {}
190
+ @uids[user] ||= Etc.getpwnam(user).uid
191
+ uid = @uids[user]
192
+ if File.stat(file).uid != uid
193
+ run "chown #{user}:#{user} '#{file}'"
194
+ end
195
+ end
196
+
197
+ # Chmod file to a new mode.
198
+ def chmod(file, mode)
199
+ if File.stat(file).mode != mode
200
+ FileUtils.chmod(mode, file, :verbose => verbose?)
201
+ end
202
+ end
203
+
204
+ # rm a file
205
+ def rm(file)
206
+ FileUtils.rm(file, :force => true, :verbose => verbose?)
207
+ end
208
+
209
+ # rm a file, but only if it exists.
210
+ def rm_if_necessary(file)
211
+ if File.exists?(file)
212
+ rm(file)
213
+ true
214
+ end
215
+ end
216
+
217
+ # Create a symlink from src to dst.
218
+ def ln(src, dst)
219
+ FileUtils.ln_sf(src, dst, :verbose => verbose?)
220
+ end
221
+
222
+ # Create a symlink from src to dst, but only if it hasn't already
223
+ # been created.
224
+ def ln_if_necessary(src, dst)
225
+ ln = false
226
+ if !File.symlink?(dst)
227
+ ln = true
228
+ elsif File.readlink(dst) != src
229
+ rm(dst)
230
+ ln = true
231
+ end
232
+ if ln
233
+ ln(src, dst)
234
+ true
235
+ end
236
+ end
237
+
238
+ # A nice printout in green.
239
+ def banner(s, color = GREEN)
240
+ s = "#{s} ".ljust(72, " ")
241
+ $stderr.write "#{color}[#{Time.new.strftime('%H:%M:%S')}] #{s}#{RESET}\n"
242
+ $stderr.flush
243
+ end
244
+
245
+ # Print a warning in yellow.
246
+ def warning(msg)
247
+ banner("Warning: #{msg}", YELLOW)
248
+ end
249
+
250
+ # Print a fatal error in red, then exit.
251
+ def fatal(msg)
252
+ banner(msg, RED)
253
+ exit(1)
254
+ end
255
+
256
+ # Who owns this process?
257
+ def whoami
258
+ @whoami ||= Etc.getpwuid(Process.uid).name
259
+ end
260
+
261
+ # Returns true if the pkg is installed.
262
+ def package_is_installed?(pkg)
263
+ succeeds?("dpkg-query -f='${Status}' -W #{pkg} | grep 'install ok installed' 2> /dev/null")
264
+ end
265
+
266
+ # Install pkg if necessary.
267
+ def package_if_necessary(pkg)
268
+ if !package_is_installed?(pkg)
269
+ banner "#{pkg}..."
270
+ run "apt-get -y install #{pkg}"
271
+ end
272
+ end
273
+
274
+ # Install gem if necessary.
275
+ def gem_if_necessary(gem)
276
+ grep = args = nil
277
+ if gem =~ /(.*)-(\d+\.\d+\.\d+)$/
278
+ gem, version = $1, $2
279
+ grep = "^#{gem}.*#{version}"
280
+ args = " --version #{version}"
281
+ else
282
+ grep = "^#{gem}"
283
+ end
284
+ if fails?("gem list #{gem} | grep '#{grep}'")
285
+ banner "#{gem}..."
286
+ run "gem install #{gem} #{args} --no-rdoc --no-ri"
287
+ return true
288
+ end
289
+ false
290
+ end
291
+
292
+ # Returns true if the pidfile exists and that process id exists as
293
+ # well.
294
+ def process_by_pid?(pidfile)
295
+ begin
296
+ if File.exists?(pidfile)
297
+ pid = File.read(pidfile).to_i
298
+ if pid != 0
299
+ Process.kill(0, pid)
300
+ return true
301
+ end
302
+ end
303
+ rescue Errno::ENOENT, Errno::ESRCH
304
+ end
305
+ false
306
+ end
307
+
308
+ private
309
+
310
+ # Returns true if verbosity is turned on.
311
+ def verbose?
312
+ @run_verbose ||= nil
313
+ end
314
+
315
+ def vputs(s)
316
+ $stderr.puts s if verbose?
317
+ end
318
+ end
319
+ end
@@ -0,0 +1,4 @@
1
+ module Teleport
2
+ # Gem version
3
+ VERSION = "1.0.0"
4
+ end
@@ -0,0 +1,20 @@
1
+ $:.push File.expand_path("../lib", __FILE__)
2
+ require "teleport/version"
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = "teleport"
6
+ s.version = Teleport::VERSION
7
+ s.platform = Gem::Platform::RUBY
8
+ s.authors = ["Adam Doppelt"]
9
+ s.email = ["amd@gurge.com"]
10
+ 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.}
13
+
14
+ s.rubyforge_project = "teleport"
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib"]
20
+ end
metadata ADDED
@@ -0,0 +1,83 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: teleport
3
+ version: !ruby/object:Gem::Version
4
+ hash: 23
5
+ prerelease:
6
+ segments:
7
+ - 1
8
+ - 0
9
+ - 0
10
+ version: 1.0.0
11
+ platform: ruby
12
+ authors:
13
+ - Adam Doppelt
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-08-03 00:00:00 -07:00
19
+ default_executable:
20
+ dependencies: []
21
+
22
+ description: Easy Ubuntu server setup via teleportation.
23
+ email:
24
+ - amd@gurge.com
25
+ executables:
26
+ - teleport
27
+ extensions: []
28
+
29
+ extra_rdoc_files: []
30
+
31
+ files:
32
+ - .gitignore
33
+ - Gemfile
34
+ - LICENSE
35
+ - README.md
36
+ - Rakefile
37
+ - bin/teleport
38
+ - lib/teleport.rb
39
+ - lib/teleport/config.rb
40
+ - lib/teleport/constants.rb
41
+ - lib/teleport/install.rb
42
+ - lib/teleport/main.rb
43
+ - lib/teleport/mirror.rb
44
+ - lib/teleport/run.sh
45
+ - lib/teleport/util.rb
46
+ - lib/teleport/version.rb
47
+ - teleport.gemspec
48
+ has_rdoc: true
49
+ homepage: http://github.com/rglabs/teleport
50
+ licenses: []
51
+
52
+ post_install_message:
53
+ rdoc_options: []
54
+
55
+ require_paths:
56
+ - lib
57
+ required_ruby_version: !ruby/object:Gem::Requirement
58
+ none: false
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ hash: 3
63
+ segments:
64
+ - 0
65
+ version: "0"
66
+ required_rubygems_version: !ruby/object:Gem::Requirement
67
+ none: false
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ hash: 3
72
+ segments:
73
+ - 0
74
+ version: "0"
75
+ requirements: []
76
+
77
+ rubyforge_project: teleport
78
+ rubygems_version: 1.5.2
79
+ signing_key:
80
+ specification_version: 3
81
+ summary: Teleport - opinionated Ubuntu server setup with Ruby.
82
+ test_files: []
83
+