fabric 0.2.0
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/.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
|