fabric 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.gitignore +26 -0
- data/Gemfile +18 -0
- data/LICENSE +20 -0
- data/README.rdoc +91 -0
- data/Rakefile +8 -0
- data/VERSION +1 -0
- data/bin/fab +7 -0
- data/fabric.gemspec +97 -0
- data/features/support/env.rb +6 -0
- data/lib/extensions/narrator.rb +19 -0
- data/lib/fabric-test.rb +10 -0
- data/lib/fabric.rb +57 -0
- data/lib/fabric/account.rb +36 -0
- data/lib/fabric/capture.rb +21 -0
- data/lib/fabric/grant.rb +8 -0
- data/lib/fabric/group.rb +8 -0
- data/lib/fabric/key.rb +8 -0
- data/lib/fabric/map.rb +102 -0
- data/lib/fabric/role.rb +24 -0
- data/lib/fabric/server.rb +148 -0
- data/lib/fabric/user.rb +33 -0
- data/lib/initialisers/data_mapper.rb +4 -0
- data/spec/classes/account_spec.rb +92 -0
- data/spec/classes/capture_spec.rb +28 -0
- data/spec/classes/map_spec.rb +212 -0
- data/spec/classes/role_spec.rb +4 -0
- data/spec/classes/server_spec.rb +104 -0
- data/spec/classes/user_spec.rb +34 -0
- data/spec/spec.opts +5 -0
- data/spec/spec_helper.rb +27 -0
- data/tasks/features.rake +15 -0
- data/tasks/jeweler.rake +14 -0
- data/tasks/rdoc.rake +9 -0
- data/tasks/spec.rake +15 -0
- data/tasks/test.rake +6 -0
- data/test.yml.example +5 -0
- metadata +116 -0
data/lib/fabric/grant.rb
ADDED
data/lib/fabric/group.rb
ADDED
data/lib/fabric/key.rb
ADDED
data/lib/fabric/map.rb
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
class Map
|
2
|
+
include DataMapper::Resource
|
3
|
+
|
4
|
+
property :id, Serial
|
5
|
+
property :key_repository, String, :default => 'keys'
|
6
|
+
|
7
|
+
has n, :roles
|
8
|
+
has n, :users
|
9
|
+
|
10
|
+
belongs_to :parent, :model => Map, :required => false
|
11
|
+
|
12
|
+
after :create do
|
13
|
+
self.users += self.parent.users if self.parent
|
14
|
+
end
|
15
|
+
|
16
|
+
after :key_repository= do |*args|
|
17
|
+
unless File.exists?(self.expanded_key_repository_path)
|
18
|
+
raise LoadError, "Could not load key repository - #{self.expanded_key_repository_path}"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.draw(opts = {})
|
23
|
+
map = Map.create(opts)
|
24
|
+
yield map if block_given?
|
25
|
+
map.read!
|
26
|
+
|
27
|
+
map
|
28
|
+
end
|
29
|
+
|
30
|
+
def read!
|
31
|
+
self.roles.all.each do |role|
|
32
|
+
role.update_user_access!
|
33
|
+
end
|
34
|
+
true
|
35
|
+
end
|
36
|
+
|
37
|
+
def key_repository(repo = nil)
|
38
|
+
if repo
|
39
|
+
self.key_repository = repo
|
40
|
+
end
|
41
|
+
|
42
|
+
@key_repository
|
43
|
+
end
|
44
|
+
|
45
|
+
def expanded_key_repository_path
|
46
|
+
File.join(Fabric.options(:map_root), self.key_repository)
|
47
|
+
# self.key_repository
|
48
|
+
end
|
49
|
+
|
50
|
+
def role(name, *hosts)
|
51
|
+
role = self.roles.create(:name => name.to_s)
|
52
|
+
hosts.each do |host|
|
53
|
+
role.servers.create(:host => host)
|
54
|
+
end
|
55
|
+
|
56
|
+
role.save
|
57
|
+
end
|
58
|
+
|
59
|
+
def user(*names)
|
60
|
+
names.each do |name|
|
61
|
+
user = self.users.create(:name => name.to_s)
|
62
|
+
self.load_keys_from_repository(user)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def grant(user_opts, role_opts, opts = {})
|
67
|
+
users = load_by_attribute(:users, :name, user_opts)
|
68
|
+
roles = load_by_attribute(:roles, :name, role_opts)
|
69
|
+
|
70
|
+
roles.each do |role|
|
71
|
+
role.users += users
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def namespace(&block)
|
76
|
+
self.class.draw(:parent => self, &block)
|
77
|
+
end
|
78
|
+
|
79
|
+
protected
|
80
|
+
def load_keys_from_repository(user)
|
81
|
+
path = File.join(self.expanded_key_repository_path, "#{user.name}.pub")
|
82
|
+
narrate "Looking for key for #{user.name} in #{path}"
|
83
|
+
|
84
|
+
if File.exists?(path)
|
85
|
+
user.keys.create(:public_key => File.read(path))
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def load_association(klass, opts = {})
|
90
|
+
association = self.send(klass.to_sym).all(opts)
|
91
|
+
raise DataMapper::ObjectNotFoundError if association.empty?
|
92
|
+
association
|
93
|
+
end
|
94
|
+
|
95
|
+
def load_by_attribute(klass, attribute, opts = {})
|
96
|
+
if opts == :all
|
97
|
+
load_association(klass)
|
98
|
+
else
|
99
|
+
load_association(klass, attribute.to_sym => opts)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
data/lib/fabric/role.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
class Role
|
2
|
+
include DataMapper::Resource
|
3
|
+
|
4
|
+
property :id, Serial
|
5
|
+
property :name, String
|
6
|
+
|
7
|
+
has n, :servers
|
8
|
+
has n, :grants
|
9
|
+
has n, :users, :through => :grants
|
10
|
+
|
11
|
+
belongs_to :map, :required => false
|
12
|
+
|
13
|
+
def narrate_as
|
14
|
+
"role - #{self.name}"
|
15
|
+
end
|
16
|
+
|
17
|
+
def update_user_access!
|
18
|
+
narrate "Updating role"
|
19
|
+
self.servers.each do |server|
|
20
|
+
server.install_accounts!
|
21
|
+
server.remove_dead_accounts
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
require 'net/ssh'
|
2
|
+
|
3
|
+
class Server
|
4
|
+
include DataMapper::Resource
|
5
|
+
|
6
|
+
property :id, Serial
|
7
|
+
property :host, Text
|
8
|
+
property :runner, Text
|
9
|
+
|
10
|
+
belongs_to :role, :required => false
|
11
|
+
|
12
|
+
has n, :captures
|
13
|
+
|
14
|
+
after :create do
|
15
|
+
self.add_captures :output, :errors
|
16
|
+
end
|
17
|
+
|
18
|
+
def narrate_as
|
19
|
+
self.host
|
20
|
+
end
|
21
|
+
|
22
|
+
def execute_command(command)
|
23
|
+
self.clear_captures!
|
24
|
+
|
25
|
+
narrate "executing: #{command}"
|
26
|
+
|
27
|
+
self.connection.open_channel do |channel, success|
|
28
|
+
# Works without this with passwordless sudo locally - with it on, errors end up in on_data, not on_extended_data, annoyingly.
|
29
|
+
channel.request_pty
|
30
|
+
|
31
|
+
channel.exec command do |ch, success|
|
32
|
+
|
33
|
+
# "on_extended_data" is called when the process writes something to stderr
|
34
|
+
ch.on_extended_data do |c, type, data|
|
35
|
+
self.capture :errors, data
|
36
|
+
end
|
37
|
+
|
38
|
+
# "on_data" is called when the process writes something to stdout
|
39
|
+
ch.on_data do |c, data|
|
40
|
+
self.capture :output, data
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
self.connection.loop
|
46
|
+
end
|
47
|
+
|
48
|
+
def output
|
49
|
+
self.captures.first(:name => 'output').contents
|
50
|
+
end
|
51
|
+
|
52
|
+
def errors
|
53
|
+
self.captures.first(:name => 'errors').contents
|
54
|
+
end
|
55
|
+
|
56
|
+
def accounts
|
57
|
+
return [] unless self.role
|
58
|
+
@accounts ||= self.role.users.collect { |user| Account.create(:user => user, :server => self) }
|
59
|
+
end
|
60
|
+
|
61
|
+
def should_account_exist_for?(user)
|
62
|
+
self.accounts.include?(user)
|
63
|
+
end
|
64
|
+
|
65
|
+
def account_exists_for?(user)
|
66
|
+
self.execute_command("id #{user.name}")
|
67
|
+
|
68
|
+
if self.output =~ /uid=/
|
69
|
+
true
|
70
|
+
else
|
71
|
+
false
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def delete_account_for(user)
|
76
|
+
narrate "Deleting dead account for #{user.name}"
|
77
|
+
self.execute_command("sudo /usr/sbin/userdel --remove #{user.name}")
|
78
|
+
end
|
79
|
+
|
80
|
+
def install_accounts!
|
81
|
+
narrate "Installing accounts"
|
82
|
+
self.accounts.each do |account|
|
83
|
+
account.add_user
|
84
|
+
account.add_ssh_directory
|
85
|
+
account.write_ssh_key
|
86
|
+
account.add_to_groups
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def remove_dead_accounts
|
91
|
+
accounts_to_remove.each do |user|
|
92
|
+
self.delete_account_for(user)
|
93
|
+
end
|
94
|
+
narrate "Dead accounts removed"
|
95
|
+
end
|
96
|
+
|
97
|
+
def accounts_to_add
|
98
|
+
self.role.users.select do |user|
|
99
|
+
not self.account_exists_for?(user)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def accounts_to_remove
|
104
|
+
narrate "Checking for dead accounts"
|
105
|
+
users = User.all
|
106
|
+
|
107
|
+
users.reject do |user|
|
108
|
+
self.should_account_exist_for?(user) and self.account_exists_for?(user)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def create_group(group)
|
113
|
+
self.execute_command("sudo /usr/sbin/groupadd #{group.name}")
|
114
|
+
end
|
115
|
+
|
116
|
+
def is_running_plesk?
|
117
|
+
self.execute_command("if [ -d /usr/local/psa ]; then echo 'plesk'; else echo 'not plesk'; fi")
|
118
|
+
|
119
|
+
case self.output.strip
|
120
|
+
when 'plesk' then true
|
121
|
+
when 'not plesk' then false
|
122
|
+
else raise "Couldn't detect if this server is running plesk."
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
protected
|
127
|
+
attr_writer :connection
|
128
|
+
|
129
|
+
def connection
|
130
|
+
@connection ||= Net::SSH.start(self.host, self.runner)
|
131
|
+
end
|
132
|
+
|
133
|
+
def add_captures(*names)
|
134
|
+
names.each do |name|
|
135
|
+
self.captures.create(:name => name)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def capture(capture, data)
|
140
|
+
self.captures.first(:name => capture.to_s).append(data)
|
141
|
+
end
|
142
|
+
|
143
|
+
def clear_captures!
|
144
|
+
self.captures.each do |capture|
|
145
|
+
capture.clear!
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
data/lib/fabric/user.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
class User
|
2
|
+
include DataMapper::Resource
|
3
|
+
|
4
|
+
property :id, Serial
|
5
|
+
property :name, String
|
6
|
+
|
7
|
+
has n, :keys
|
8
|
+
has n, :groups
|
9
|
+
has n, :grants
|
10
|
+
|
11
|
+
belongs_to :map, :required => false
|
12
|
+
|
13
|
+
def authorized_keys_file
|
14
|
+
authorized_keys = self.keys.inject('') do |authorized_keys, key|
|
15
|
+
authorized_keys << key.public_key
|
16
|
+
end
|
17
|
+
|
18
|
+
raise "User #{self.name} has a blank SSH key - this is not permitted" if authorized_keys.blank?
|
19
|
+
authorized_keys
|
20
|
+
end
|
21
|
+
|
22
|
+
def authorized_keys_file_path
|
23
|
+
"/home/#{self.name}/.ssh/authorized_keys"
|
24
|
+
end
|
25
|
+
|
26
|
+
def home_directory_path
|
27
|
+
"/home/#{self.name}/"
|
28
|
+
end
|
29
|
+
|
30
|
+
def ssh_config_directory_path
|
31
|
+
"/home/#{self.name}/.ssh/"
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../spec_helper.rb'
|
2
|
+
|
3
|
+
describe Account do
|
4
|
+
before(:each) do
|
5
|
+
@account = Account.create(
|
6
|
+
:server => Server.create(:host => TEST_SERVER),
|
7
|
+
:user => User.create(:name => TEST_USER)
|
8
|
+
)
|
9
|
+
|
10
|
+
@server = @account.server
|
11
|
+
@user = @account.user
|
12
|
+
end
|
13
|
+
|
14
|
+
after(:each) do
|
15
|
+
@server.execute_command("sudo /usr/sbin/userdel --remove #{TEST_USER}")
|
16
|
+
end
|
17
|
+
|
18
|
+
describe "with a user" do
|
19
|
+
it "should create a user on a server" do
|
20
|
+
@account.add_user
|
21
|
+
@server.execute_command("id #{@user.name}")
|
22
|
+
@server.output.should_not == ''
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should ensure that user's home directory has the correct permissions" do
|
26
|
+
@account.add_user
|
27
|
+
@server.execute_command("sudo ls -la /home | grep #{@user.name}")
|
28
|
+
@server.output.should =~ /^drwxr-xr-x/
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
describe "with a user in a group" do
|
33
|
+
before(:each) do
|
34
|
+
@account.add_user
|
35
|
+
@user.groups.new(:name => TEST_GROUP)
|
36
|
+
@server.execute_command("sudo /usr/sbin/groupadd #{TEST_GROUP}")
|
37
|
+
end
|
38
|
+
|
39
|
+
after(:each) do
|
40
|
+
@server.execute_command("sudo /usr/sbin/groupdel #{TEST_GROUP}")
|
41
|
+
end
|
42
|
+
|
43
|
+
it "should add the user to that group" do
|
44
|
+
@account.add_to_groups
|
45
|
+
|
46
|
+
@server.execute_command("groups #{@user.name}")
|
47
|
+
@server.output.should =~ / ?#{TEST_GROUP} ?/
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
describe "with a user with an ssh key" do
|
52
|
+
before(:each) do
|
53
|
+
@account.add_user
|
54
|
+
@account.add_ssh_directory
|
55
|
+
@key_text = 'this is a key'
|
56
|
+
@user.keys.create(:public_key => @key_text)
|
57
|
+
end
|
58
|
+
|
59
|
+
describe "on a server" do
|
60
|
+
before(:each) do
|
61
|
+
@account.write_ssh_key
|
62
|
+
end
|
63
|
+
|
64
|
+
it "should have the correct permissions" do
|
65
|
+
@server.execute_command("sudo ls -la /home/#{@user.name}/.ssh/authorized_keys")
|
66
|
+
@server.output.should =~ /^-rw-------/
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
describe "to be added" do
|
71
|
+
it "should add that key" do
|
72
|
+
@account.write_ssh_key
|
73
|
+
@server.execute_command("sudo cat /home/#{@user.name}/.ssh/authorized_keys")
|
74
|
+
@server.output.strip.should == @key_text
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
describe "to be updated" do
|
79
|
+
before(:each) do
|
80
|
+
@account.write_ssh_key
|
81
|
+
@updated_key = 'this is also a key'
|
82
|
+
@user.keys.first.public_key = @updated_key
|
83
|
+
end
|
84
|
+
|
85
|
+
it "should update that key" do
|
86
|
+
@account.write_ssh_key
|
87
|
+
@server.execute_command("sudo cat /home/#{@user.name}/.ssh/authorized_keys")
|
88
|
+
@server.output.strip.should == @updated_key
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../spec_helper.rb'
|
2
|
+
|
3
|
+
describe Capture do
|
4
|
+
it "should be blank when just created" do
|
5
|
+
Capture.create.contents.should == ''
|
6
|
+
end
|
7
|
+
|
8
|
+
it "should append data to existing" do
|
9
|
+
capture = Capture.create
|
10
|
+
data1 = 'hello'
|
11
|
+
data2 = 'world'
|
12
|
+
|
13
|
+
capture.append(data1)
|
14
|
+
capture.contents.should == data1
|
15
|
+
|
16
|
+
capture.append(data2)
|
17
|
+
capture.contents.should == data1 + data2
|
18
|
+
end
|
19
|
+
|
20
|
+
it "can clear its data" do
|
21
|
+
capture = Capture.create
|
22
|
+
capture.append('hello world')
|
23
|
+
|
24
|
+
capture.clear!
|
25
|
+
|
26
|
+
capture.contents.should == ''
|
27
|
+
end
|
28
|
+
end
|