anjou 0.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.
- checksums.yaml +7 -0
- data/.gitignore +23 -0
- data/.rspec +1 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +30 -0
- data/Rakefile +9 -0
- data/anjou.gemspec +30 -0
- data/lib/anjou.rb +10 -0
- data/lib/anjou/authorized_keys.rb +25 -0
- data/lib/anjou/ec2.rb +146 -0
- data/lib/anjou/instance_user_data.rb +76 -0
- data/lib/anjou/launch_instance.rb +84 -0
- data/lib/anjou/user_home.rb +75 -0
- data/lib/anjou/version.rb +3 -0
- data/scripts/create-user.sh +3 -0
- data/scripts/first-boot/append/install-motd.sh +13 -0
- data/scripts/first-boot/optional/install-emacs.sh +3 -0
- data/scripts/first-boot/optional/install-mosh.sh +5 -0
- data/scripts/first-boot/optional/install-ruby.sh +6 -0
- data/scripts/first-boot/optional/install-wemux.sh +13 -0
- data/scripts/first-boot/prepend/install-git.sh +3 -0
- data/scripts/first-boot/prepend/install-ubuntu-updates.sh +5 -0
- data/scripts/mount-user-volume.sh +16 -0
- data/spec/install_script_helpers.rb +22 -0
- data/spec/lib/anjou/authorized_keys_spec.rb +45 -0
- data/spec/lib/anjou/instance_user_data_spec.rb +35 -0
- data/spec/spec_helper.rb +11 -0
- metadata +188 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: f9a15edb4af49d3d799f55b5748292f65b6ae3ba
|
4
|
+
data.tar.gz: b47599d0848bfea7b65d2c0239f3ced13cff5f4f
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 241ee6a63f6f680279f191ea3331ded3783160be2c8485603e5fe146358a98e48d9fa1115d05486cb96bfdd1d7ef92f2e969b2230bba251326e4098818d6cb5d
|
7
|
+
data.tar.gz: e6787c8cfdfb9272f6ea330263e56bc174bcdf74f58310ada152bc052882db3652694d3d1ffc9c8662f3c23e55e5837f7d151453e96c6bfbd97925ffeb2ee0e8
|
data/.gitignore
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
.bundle
|
4
|
+
.config
|
5
|
+
.yardoc
|
6
|
+
Gemfile.lock
|
7
|
+
InstalledFiles
|
8
|
+
_yardoc
|
9
|
+
coverage
|
10
|
+
doc/
|
11
|
+
lib/bundler/man
|
12
|
+
pkg
|
13
|
+
rdoc
|
14
|
+
spec/reports
|
15
|
+
test/tmp
|
16
|
+
test/version_tmp
|
17
|
+
tmp
|
18
|
+
|
19
|
+
# added by coy
|
20
|
+
.coy
|
21
|
+
|
22
|
+
# added by coy
|
23
|
+
credentials
|
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--require spec_helper
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Joel Helbling
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# Anjou
|
2
|
+
|
3
|
+
TODO: Write a gem description
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'anjou'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install anjou
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
TODO: Write usage instructions here
|
22
|
+
|
23
|
+
## Contributing
|
24
|
+
|
25
|
+
1. Fork it
|
26
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
27
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
28
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
29
|
+
5. Create new Pull Request
|
30
|
+
7. Count your money
|
data/Rakefile
ADDED
data/anjou.gemspec
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'anjou/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "anjou"
|
8
|
+
spec.version = Anjou::VERSION
|
9
|
+
spec.authors = ["Joel Helbling"]
|
10
|
+
spec.email = ["joel@joelhelbling.com"]
|
11
|
+
spec.description = %q{Remote pair programming made super easy}
|
12
|
+
spec.summary = %q{Anjou brings you, your stuff, some other programmer and their stuff all together on a pair programming machine.}
|
13
|
+
spec.homepage = ""
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_dependency "aws-sdk", "~> 1.32.0"
|
22
|
+
spec.add_dependency "net-sftp", "~> 2.1.2"
|
23
|
+
|
24
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
25
|
+
spec.add_development_dependency "rspec", "~> 2.13.0"
|
26
|
+
spec.add_development_dependency "pry"
|
27
|
+
spec.add_development_dependency "rake"
|
28
|
+
spec.add_development_dependency "coy"
|
29
|
+
spec.add_development_dependency "fakefs", "~> 0.5.0"
|
30
|
+
end
|
data/lib/anjou.rb
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
require "anjou/version"
|
2
|
+
require_relative "anjou/authorized_keys"
|
3
|
+
require_relative "anjou/ec2"
|
4
|
+
require_relative "anjou/instance_user_data"
|
5
|
+
require_relative "anjou/launch_instance"
|
6
|
+
require_relative "anjou/user_home"
|
7
|
+
|
8
|
+
module Anjou
|
9
|
+
# Your code goes here...
|
10
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module Anjou
|
5
|
+
class AuthorizedKeys
|
6
|
+
attr_reader :keys
|
7
|
+
|
8
|
+
def initialize(github_username)
|
9
|
+
uri = URI.parse("https://api.github.com/users/#{github_username}/keys")
|
10
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
11
|
+
http.use_ssl = true
|
12
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
13
|
+
data = http.get(uri.request_uri)
|
14
|
+
@keys = JSON.parse data.body
|
15
|
+
raise "Response from GitHub: #{@keys['message']}" if @keys.kind_of? Hash
|
16
|
+
end
|
17
|
+
|
18
|
+
def contents
|
19
|
+
@keys.map{|item| item['key']}.join("\n")
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
|
data/lib/anjou/ec2.rb
ADDED
@@ -0,0 +1,146 @@
|
|
1
|
+
require 'aws-sdk'
|
2
|
+
require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'credentials', 'anjou'))
|
3
|
+
|
4
|
+
module Anjou
|
5
|
+
class EC2
|
6
|
+
attr_reader :api
|
7
|
+
|
8
|
+
KEY_ID = Anjou::AWS::ACCESS_KEY_ID
|
9
|
+
SECRET_KEY = Anjou::AWS::SECRET_ACCESS_KEY
|
10
|
+
KEY_PAIR_NAME = Anjou::AWS::KEY_PAIR_NAME
|
11
|
+
USER_VOL_SIZE = 1 #Gb
|
12
|
+
DEFAULT_ZONE = "us-east-1d"
|
13
|
+
DEFAULT_AMI = 'ami-ad184ac4' # Ubuntu Server 13.10 64bit
|
14
|
+
DEFAULT_INSTANCE_TYPE = 't1.micro'
|
15
|
+
DEFAULT_USER_DATA = "#!/bin/sh\n\necho \"Welcome to Anjou!\n\n\" >> /etc/motd\n"
|
16
|
+
DEFAULT_SNAPSHOT_NAME = 'anjou-generic'
|
17
|
+
SECURITY_GROUP_NAME = 'Anjou'
|
18
|
+
|
19
|
+
def initialize(access_key_id=KEY_ID, secret_access_key=SECRET_KEY)
|
20
|
+
@api = ::AWS::EC2.new(
|
21
|
+
access_key_id: access_key_id,
|
22
|
+
secret_access_key: secret_access_key )
|
23
|
+
end
|
24
|
+
|
25
|
+
def create_instance(
|
26
|
+
username: nil,
|
27
|
+
ami: DEFAULT_AMI,
|
28
|
+
key_name: KEY_PAIR_NAME,
|
29
|
+
zone: DEFAULT_ZONE,
|
30
|
+
instance_type: DEFAULT_INSTANCE_TYPE,
|
31
|
+
user_data: DEFAULT_USER_DATA
|
32
|
+
)
|
33
|
+
key_pair = key_pair_for key_name
|
34
|
+
raise "Unable to start instance: no such key pair exists! (key_name: #{key_name})" unless key_pair && key_pair.exists?
|
35
|
+
@api.instances.create(
|
36
|
+
image_id: ami,
|
37
|
+
key_pair: key_pair,
|
38
|
+
availability_zone: zone,
|
39
|
+
instance_type: instance_type,
|
40
|
+
user_data: user_data,
|
41
|
+
security_groups: security_groups
|
42
|
+
).tap do |instance|
|
43
|
+
if username
|
44
|
+
instance.tags.Name = name_tag_for username
|
45
|
+
instance.tags.owner = username
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def instance_for(username)
|
51
|
+
@api.instances.tagged('owner').select do |instance|
|
52
|
+
instance.tags.to_a.include? ['owner', username]
|
53
|
+
end.select{|i| i.status != :terminated }.last
|
54
|
+
end
|
55
|
+
|
56
|
+
def instance_status_for(username)
|
57
|
+
instance_for(username).status
|
58
|
+
end
|
59
|
+
|
60
|
+
def key_pair_for(key_name)
|
61
|
+
@api.key_pairs.select{ |kp| kp.name == key_name }.last
|
62
|
+
end
|
63
|
+
|
64
|
+
def create_user_volume(username: nil, user_vol_size: USER_VOL_SIZE, zone: DEFAULT_ZONE, snapshot: DEFAULT_SNAPSHOT_NAME)
|
65
|
+
|
66
|
+
aws_snapshot = @api.snapshots.tagged('Name').select do |snp|
|
67
|
+
snp.tags.to_a.include? ['Name', snapshot]
|
68
|
+
end.last
|
69
|
+
|
70
|
+
@api.volumes.create(
|
71
|
+
size: user_vol_size,
|
72
|
+
availability_zone: zone,
|
73
|
+
snapshot: aws_snapshot
|
74
|
+
).tap do |volume|
|
75
|
+
if username
|
76
|
+
volume.tags.Name = name_tag_for username
|
77
|
+
volume.tags.owner = username
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def delete_user_volume(username)
|
83
|
+
detach(username).delete
|
84
|
+
end
|
85
|
+
|
86
|
+
def user_volume_for(username)
|
87
|
+
@api.volumes.tagged('owner').select do |vol|
|
88
|
+
vol.tags.to_a.include? ['owner', username]
|
89
|
+
end.first
|
90
|
+
end
|
91
|
+
|
92
|
+
def user_volume_status_for(username)
|
93
|
+
user_volume_for(username).status
|
94
|
+
end
|
95
|
+
|
96
|
+
#@depricated: this method violates Anjou::EC2's "thin wrapper" contract.
|
97
|
+
# It's a useful method, but it should be implemented elsewhere.
|
98
|
+
def wait_for_user_volume(username, status: :available, timeout: 15)
|
99
|
+
seconds = 0
|
100
|
+
while user_volume_status_for(username) != status
|
101
|
+
raise "Timeout: After #{timeout} seconds, user volume for #{username} is still not #{status}" if seconds >= timeout
|
102
|
+
seconds += sleep 1
|
103
|
+
end
|
104
|
+
status
|
105
|
+
end
|
106
|
+
|
107
|
+
def attach(username, instance, device=nil)
|
108
|
+
volume = user_volume_for(username)
|
109
|
+
guard_volume_available(volume, username)
|
110
|
+
device ||= next_device_for(instance)
|
111
|
+
volume.attach_to instance, device
|
112
|
+
end
|
113
|
+
|
114
|
+
def detach(username)
|
115
|
+
user_volume_for(username).tap do |volume|
|
116
|
+
volume.attachments.each do |attachment|
|
117
|
+
attachment.delete(force: true)
|
118
|
+
end
|
119
|
+
sleep 1 until volume.status == :available
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def next_device_for(instance)
|
124
|
+
'/dev/sda' + (instance.attachments.keys.size + 1).to_s
|
125
|
+
end
|
126
|
+
|
127
|
+
def security_groups
|
128
|
+
@api.security_groups.to_a.select{ |sg| sg.name == 'Anjou' }
|
129
|
+
end
|
130
|
+
|
131
|
+
private
|
132
|
+
|
133
|
+
def name_tag_for(username)
|
134
|
+
"anjou-#{username || :generic}"
|
135
|
+
end
|
136
|
+
|
137
|
+
def guard_volume_available(volume, username)
|
138
|
+
volume.status.tap do |status|
|
139
|
+
unless status == :available
|
140
|
+
raise "user volume for \"#{username}\" is not available (status: #{status})"
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
@@ -0,0 +1,76 @@
|
|
1
|
+
module Anjou
|
2
|
+
class InstanceUserData
|
3
|
+
PREPENDS = 'scripts/first-boot/prepend'
|
4
|
+
OPTIONALS = 'scripts/first-boot/optional'
|
5
|
+
APPENDS = 'scripts/first-boot/append'
|
6
|
+
|
7
|
+
def self.render_mime(*scripts)
|
8
|
+
self.new(scripts.flatten).render_mime
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize(scripts=[])
|
12
|
+
@scripts = scripts
|
13
|
+
@script_sequence = 0
|
14
|
+
end
|
15
|
+
|
16
|
+
def render_mime
|
17
|
+
generate_mime
|
18
|
+
mime_sections.join("\n")
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_s
|
22
|
+
render_mime
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def generate_mime
|
28
|
+
add_to PREPENDS, [ 'install-ubuntu-updates', 'install-git' ]
|
29
|
+
add_to OPTIONALS, @scripts
|
30
|
+
add_to APPENDS, [ 'install-motd' ]
|
31
|
+
end
|
32
|
+
|
33
|
+
def add_to(path, scripts)
|
34
|
+
scripts.each do |script|
|
35
|
+
filename = "#{script}.sh"
|
36
|
+
filepath = "#{path}/#{filename}"
|
37
|
+
mime_sections << file_part(filename, File.read(filepath))
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def boundary
|
42
|
+
@boundary ||= "~~Anjou::UserData~~"
|
43
|
+
end
|
44
|
+
|
45
|
+
def mime_sections
|
46
|
+
@mime_sections ||= [ message_header ]
|
47
|
+
end
|
48
|
+
|
49
|
+
def message_header
|
50
|
+
<<-HEADER
|
51
|
+
Content-Type: multipart/mixed; boundary=#{boundary}
|
52
|
+
MIME-Version: 1.0
|
53
|
+
|
54
|
+
HEADER
|
55
|
+
end
|
56
|
+
|
57
|
+
def file_part(filename, file_contents)
|
58
|
+
[ part_header(filename), file_contents ].join
|
59
|
+
end
|
60
|
+
|
61
|
+
def file_sequence
|
62
|
+
"%03d" % ( @script_sequence += 1 )
|
63
|
+
end
|
64
|
+
|
65
|
+
def part_header(filename, content_type: 'text/x-shellscript')
|
66
|
+
<<-PART
|
67
|
+
--#{boundary}
|
68
|
+
Content-Type: #{content_type}
|
69
|
+
MIME-Version: 1.0
|
70
|
+
Content-Disposition: attachment; filename="#{file_sequence}-#{filename}"
|
71
|
+
|
72
|
+
PART
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'anjou'
|
2
|
+
|
3
|
+
module Anjou
|
4
|
+
class LaunchInstance
|
5
|
+
LAUNCH_TIMEOUT_SECONDS = 60 * 5
|
6
|
+
|
7
|
+
class << self
|
8
|
+
|
9
|
+
def create_and_launch_instance(users=[], host_user: nil)
|
10
|
+
host_user ||= users.first
|
11
|
+
users << host_user unless users.include? host_user
|
12
|
+
|
13
|
+
guard_enough users
|
14
|
+
|
15
|
+
ensure_volumes_for users
|
16
|
+
|
17
|
+
ensure_authorized_keys_for users
|
18
|
+
|
19
|
+
user_data = Anjou::InstanceUserData.render_mime 'install-ruby', 'install-mosh'
|
20
|
+
|
21
|
+
create_instance_for(host_user, user_data).tap do |instance|
|
22
|
+
attach users, instance
|
23
|
+
|
24
|
+
users.each do |user|
|
25
|
+
home = Anjou::UserHome.new user, instance.dns_name
|
26
|
+
puts "Creating login for #{user}..."
|
27
|
+
home.create_linux_user
|
28
|
+
puts "Mounting home directory for #{user}..."
|
29
|
+
home.mount_home_dir
|
30
|
+
puts "Installing authorized_keys for #{user}..."
|
31
|
+
home.install_authorized_keys @authorized_keys[user]
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def ec2
|
40
|
+
@@ec2 ||= Anjou::EC2.new
|
41
|
+
end
|
42
|
+
|
43
|
+
def guard_enough users
|
44
|
+
raise "You must provide at least one user for this pairing workstation!" unless users.size > 0
|
45
|
+
end
|
46
|
+
|
47
|
+
def create_instance_for host_user, user_data
|
48
|
+
puts "Creating Anjou instance for host user #{host_user}..."
|
49
|
+
instance = ec2.create_instance username: host_user, user_data: user_data
|
50
|
+
|
51
|
+
timeout_seconds = LAUNCH_TIMEOUT_SECONDS
|
52
|
+
until instance.status == :running do
|
53
|
+
raise "instance failed to start after #{LAUNCH_TIMEOUT_SECONDS}" if timeout_seconds <= 0
|
54
|
+
sleep 1; timeout_seconds -= 1
|
55
|
+
end
|
56
|
+
puts "Anjou instance started at #{instance.dns_name} :)"
|
57
|
+
instance
|
58
|
+
end
|
59
|
+
|
60
|
+
def ensure_volumes_for users
|
61
|
+
users.each do |user|
|
62
|
+
unless ec2.user_volume_for(user)
|
63
|
+
puts "Creating user volume for #{user}"
|
64
|
+
ec2.create_user_volume(username: user)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def ensure_authorized_keys_for users
|
70
|
+
@authorized_keys = {}
|
71
|
+
users.each do |user|
|
72
|
+
@authorized_keys[user] = Anjou::AuthorizedKeys.new(user)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def attach users, instance
|
77
|
+
users.each do |user|
|
78
|
+
puts "Attaching user volume for #{user} to #{instance.dns_name}"
|
79
|
+
ec2.attach user, instance
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'net/sftp'
|
2
|
+
|
3
|
+
module Anjou
|
4
|
+
class UserHome
|
5
|
+
ANJOU_LOGIN = 'ubuntu'
|
6
|
+
SSH_READY_TIMEOUT = 300
|
7
|
+
SSH_READY_SLEEP_INCREMENT = 2
|
8
|
+
|
9
|
+
def initialize(username, hostname)
|
10
|
+
@username = username
|
11
|
+
@hostname = hostname
|
12
|
+
end
|
13
|
+
|
14
|
+
def create_linux_user
|
15
|
+
ssh_do "sudo adduser --disabled-password --gecos '#{@username}' #{@username}"
|
16
|
+
ssh_do "echo '#{@username} ALL=(ALL) NOPASSWD:ALL' > /tmp/sudoize-#{@username}"
|
17
|
+
ssh_do "sudo chown root:root /tmp/sudoize-#{@username}"
|
18
|
+
ssh_do "sudo chmod 440 /tmp/sudoize-#{@username}"
|
19
|
+
ssh_do "sudo mv /tmp/sudoize-#{@username} /etc/sudoers.d/"
|
20
|
+
end
|
21
|
+
|
22
|
+
def mount_home_dir
|
23
|
+
volume = api.user_volume_for @username
|
24
|
+
device = volume.attachments.to_a.first.device.gsub(/sda/, "xvda")
|
25
|
+
ssh_do "sudo mount #{device} /home/#{@username}"
|
26
|
+
end
|
27
|
+
|
28
|
+
def install_authorized_keys(authorized_keys=Anjou::AuthorizedKeys.new(@username))
|
29
|
+
if ssh_do("sudo ls /home/#@username/.ssh").include? 'authorized_keys'
|
30
|
+
puts " ...on second thought, skipping this since #@username already has one..."
|
31
|
+
else
|
32
|
+
Net::SSH.start(@hostname, ANJOU_LOGIN) do |ssh|
|
33
|
+
ssh.sftp.connect do |sftp|
|
34
|
+
sftp.file.open("#{@username}-authorized_keys", 'w') do |fh|
|
35
|
+
fh.write authorized_keys.contents
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
ssh_do "sudo mv ~#{ANJOU_LOGIN}/#@username-authorized_keys ~#@username/.ssh/authorized_keys"
|
40
|
+
ssh_do "sudo chmod 0600 ~#@username/.ssh/authorized_keys"
|
41
|
+
end
|
42
|
+
ssh_do "sudo chown -R #@username:#@username /home/#@username"
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def api
|
48
|
+
@api ||= Anjou::EC2.new
|
49
|
+
end
|
50
|
+
|
51
|
+
def ssh_do(cmd)
|
52
|
+
ensure_ssh_ready
|
53
|
+
`#{ssh_cmd} "#{cmd}"`
|
54
|
+
end
|
55
|
+
|
56
|
+
def ensure_ssh_ready
|
57
|
+
timeout = 0
|
58
|
+
until ssh_ready? do
|
59
|
+
raise "SSH connection not ready after #{timeout} attempts." if timeout >= (SSH_READY_TIMEOUT / SSH_READY_SLEEP_INCREMENT)
|
60
|
+
puts " ...connection not ready, waiting a bit..."
|
61
|
+
sleep SSH_READY_SLEEP_INCREMENT
|
62
|
+
timeout += 1
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def ssh_ready?
|
67
|
+
@ssh_ready ||= `#{ssh_cmd} ls /var/lib/cloud/instance/* 2>&1`.include? 'boot-finished'
|
68
|
+
end
|
69
|
+
|
70
|
+
def ssh_cmd
|
71
|
+
@ssh_cmd ||= "ssh -o 'StrictHostKeyChecking no' #{ANJOU_LOGIN}@#{@hostname}"
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
#!/bin/sh
|
2
|
+
|
3
|
+
apt-get -q -y install tmux
|
4
|
+
git clone git://github.com/zolrath/wemux.git /usr/local/share/wemux
|
5
|
+
ln -s /usr/local/share/wemux/wemux /usr/local/bin/wemux
|
6
|
+
cp /usr/local/share/wemux/wemux.conf.example /usr/local/etc/wemux.conf
|
7
|
+
|
8
|
+
# need a way to add participants as hosts...
|
9
|
+
sed -i 's/host_list=(change_this)/host_list=(joelhelbling ubuntu anjouhost)/g' /usr/local/etc/wemux.conf
|
10
|
+
|
11
|
+
wemux start
|
12
|
+
chmod 1777 /tmp/wemux-wemux
|
13
|
+
|
@@ -0,0 +1,22 @@
|
|
1
|
+
|
2
|
+
module InstallScriptHelpers
|
3
|
+
|
4
|
+
def make_scripts_dir
|
5
|
+
Dir.mkdir 'scripts'
|
6
|
+
Dir.mkdir 'scripts/first-boot'
|
7
|
+
Dir.mkdir 'scripts/first-boot/prepend'
|
8
|
+
Dir.mkdir 'scripts/first-boot/optional'
|
9
|
+
Dir.mkdir 'scripts/first-boot/append'
|
10
|
+
end
|
11
|
+
|
12
|
+
def make_install_script(package, subdir = :optional)
|
13
|
+
File.open("scripts/first-boot/#{subdir}/install-#{package}.sh", 'w') do |fh|
|
14
|
+
fh.write <<-SHELL
|
15
|
+
#!/bin/sh
|
16
|
+
|
17
|
+
apt-get -q -y install #{package}
|
18
|
+
SHELL
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'anjou/authorized_keys'
|
2
|
+
|
3
|
+
module Anjou
|
4
|
+
describe AuthorizedKeys do
|
5
|
+
let(:gh_user) { 'foogoo' }
|
6
|
+
subject { described_class.new(gh_user) }
|
7
|
+
|
8
|
+
let(:key1) { "ssh-rsa AABBCC==" }
|
9
|
+
let(:key2) { "ssh-dss AABBCCDD=" }
|
10
|
+
let(:key3) { "ssh-rsa ZZYYXX=" }
|
11
|
+
let(:gh_response) do
|
12
|
+
"[{\"id\":101,\"key\":\"#{key1}\"}," +
|
13
|
+
"{\"id\":102,\"key\":\"#{key2}\"}," +
|
14
|
+
"{\"id\":103,\"key\":\"#{key3}\"}]"
|
15
|
+
end
|
16
|
+
let(:expected_authorized_keys) do
|
17
|
+
[key1, key2, key3].join("\n")
|
18
|
+
end
|
19
|
+
|
20
|
+
let(:data) { double }
|
21
|
+
before do
|
22
|
+
Net::HTTP.any_instance.stub(:get).and_return(data)
|
23
|
+
data.stub(:body).and_return(gh_response)
|
24
|
+
end
|
25
|
+
|
26
|
+
its(:keys) { should have(3).items }
|
27
|
+
its(:contents) { should == expected_authorized_keys }
|
28
|
+
|
29
|
+
context 'when user is not on GitHub' do
|
30
|
+
subject { described_class }
|
31
|
+
let(:gh_response) do
|
32
|
+
<<-RESPONSE
|
33
|
+
{
|
34
|
+
"message": "Not Found",
|
35
|
+
"documentation_url": "http://developer.github.com/v3"
|
36
|
+
}
|
37
|
+
RESPONSE
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'throws an error' do
|
41
|
+
expect { described_class.new gh_user }.to raise_error /Not Found/
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'anjou/instance_user_data'
|
2
|
+
|
3
|
+
module Anjou
|
4
|
+
describe InstanceUserData do
|
5
|
+
it { should respond_to :render_mime }
|
6
|
+
|
7
|
+
describe "#render_mime", :fakefs do
|
8
|
+
before do
|
9
|
+
make_scripts_dir
|
10
|
+
make_install_script 'ubuntu-updates', :prepend
|
11
|
+
make_install_script 'git', :prepend
|
12
|
+
make_install_script 'foo'
|
13
|
+
make_install_script 'bar'
|
14
|
+
make_install_script 'baz'
|
15
|
+
make_install_script 'motd', :append
|
16
|
+
end
|
17
|
+
|
18
|
+
context "for a subset of existing scripts" do
|
19
|
+
subject { described_class.new ["install-foo", "install-bar"] }
|
20
|
+
its(:render_mime) { should include("apt-get -q -y install foo") }
|
21
|
+
its(:render_mime) { should include("install bar") }
|
22
|
+
its(:render_mime) { should_not include("install baz") }
|
23
|
+
end
|
24
|
+
|
25
|
+
context "for a non-existant script" do
|
26
|
+
subject { described_class.new ["install-bop"] }
|
27
|
+
it "throws an error" do
|
28
|
+
expect{ subject.render_mime }.to raise_error
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
end # describe InstanceUserData
|
35
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'fakefs/spec_helpers'
|
2
|
+
require File.join(File.dirname(__FILE__), 'install_script_helpers')
|
3
|
+
|
4
|
+
$LOAD_PATH.unshift(File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib')))
|
5
|
+
|
6
|
+
RSpec.configure do |cfg|
|
7
|
+
cfg.treat_symbols_as_metadata_keys_with_true_values = true
|
8
|
+
cfg.include FakeFS::SpecHelpers, fakefs: true
|
9
|
+
cfg.include InstallScriptHelpers, fakefs: true
|
10
|
+
end
|
11
|
+
|
metadata
ADDED
@@ -0,0 +1,188 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: anjou
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Joel Helbling
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-02-08 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: aws-sdk
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 1.32.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 1.32.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: net-sftp
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 2.1.2
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 2.1.2
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: bundler
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.3'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.3'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 2.13.0
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 2.13.0
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: pry
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rake
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: coy
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: fakefs
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: 0.5.0
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: 0.5.0
|
125
|
+
description: Remote pair programming made super easy
|
126
|
+
email:
|
127
|
+
- joel@joelhelbling.com
|
128
|
+
executables: []
|
129
|
+
extensions: []
|
130
|
+
extra_rdoc_files: []
|
131
|
+
files:
|
132
|
+
- ".gitignore"
|
133
|
+
- ".rspec"
|
134
|
+
- Gemfile
|
135
|
+
- LICENSE.txt
|
136
|
+
- README.md
|
137
|
+
- Rakefile
|
138
|
+
- anjou.gemspec
|
139
|
+
- lib/anjou.rb
|
140
|
+
- lib/anjou/authorized_keys.rb
|
141
|
+
- lib/anjou/ec2.rb
|
142
|
+
- lib/anjou/instance_user_data.rb
|
143
|
+
- lib/anjou/launch_instance.rb
|
144
|
+
- lib/anjou/user_home.rb
|
145
|
+
- lib/anjou/version.rb
|
146
|
+
- scripts/create-user.sh
|
147
|
+
- scripts/first-boot/append/install-motd.sh
|
148
|
+
- scripts/first-boot/optional/install-emacs.sh
|
149
|
+
- scripts/first-boot/optional/install-mosh.sh
|
150
|
+
- scripts/first-boot/optional/install-ruby.sh
|
151
|
+
- scripts/first-boot/optional/install-wemux.sh
|
152
|
+
- scripts/first-boot/prepend/install-git.sh
|
153
|
+
- scripts/first-boot/prepend/install-ubuntu-updates.sh
|
154
|
+
- scripts/mount-user-volume.sh
|
155
|
+
- spec/install_script_helpers.rb
|
156
|
+
- spec/lib/anjou/authorized_keys_spec.rb
|
157
|
+
- spec/lib/anjou/instance_user_data_spec.rb
|
158
|
+
- spec/spec_helper.rb
|
159
|
+
homepage: ''
|
160
|
+
licenses:
|
161
|
+
- MIT
|
162
|
+
metadata: {}
|
163
|
+
post_install_message:
|
164
|
+
rdoc_options: []
|
165
|
+
require_paths:
|
166
|
+
- lib
|
167
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
168
|
+
requirements:
|
169
|
+
- - ">="
|
170
|
+
- !ruby/object:Gem::Version
|
171
|
+
version: '0'
|
172
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
173
|
+
requirements:
|
174
|
+
- - ">="
|
175
|
+
- !ruby/object:Gem::Version
|
176
|
+
version: '0'
|
177
|
+
requirements: []
|
178
|
+
rubyforge_project:
|
179
|
+
rubygems_version: 2.4.3
|
180
|
+
signing_key:
|
181
|
+
specification_version: 4
|
182
|
+
summary: Anjou brings you, your stuff, some other programmer and their stuff all together
|
183
|
+
on a pair programming machine.
|
184
|
+
test_files:
|
185
|
+
- spec/install_script_helpers.rb
|
186
|
+
- spec/lib/anjou/authorized_keys_spec.rb
|
187
|
+
- spec/lib/anjou/instance_user_data_spec.rb
|
188
|
+
- spec/spec_helper.rb
|