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.
@@ -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