fabric 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,8 @@
1
+ class Grant
2
+ include DataMapper::Resource
3
+
4
+ property :id, Serial
5
+
6
+ belongs_to :user
7
+ belongs_to :role
8
+ end
@@ -0,0 +1,8 @@
1
+ class Group
2
+ include DataMapper::Resource
3
+
4
+ property :id, Serial
5
+ property :name, String
6
+
7
+ belongs_to :user
8
+ end
data/lib/fabric/key.rb ADDED
@@ -0,0 +1,8 @@
1
+ class Key
2
+ include DataMapper::Resource
3
+
4
+ property :id, Serial
5
+ property :public_key, Text
6
+
7
+ belongs_to :user
8
+ end
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
@@ -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
@@ -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,4 @@
1
+ require 'dm-core'
2
+
3
+ DataMapper.setup(:default, "sqlite3::memory:")
4
+ DataMapper.auto_migrate!
@@ -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