automateit 0.71021 → 0.71030

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,40 @@
1
+ # == AccountManager::PasswdExpect
2
+ #
3
+ # An AccountManager driver for the +passwd+ command found on Unix-like systems
4
+ # using the +expect+ program as a wrapper because the Ruby PTY implementation
5
+ # is unreliable.
6
+ class ::AutomateIt::AccountManager::PasswdExpect < ::AutomateIt::AccountManager::BaseDriver
7
+ depends_on :programs => %w(passwd expect)
8
+
9
+ def suitability(method, *args) # :nodoc:
10
+ # Level must be higher than PasswdPTY
11
+ return available? ? 9 : 0
12
+ end
13
+
14
+ # See AccountManager#passwd
15
+ def passwd(user, password, opts={})
16
+ _passwd_helper(user, password, opts) do |user, password, opts|
17
+ log.silence(Logger::WARN) do
18
+ interpreter.mktemp do |filename|
19
+ # Script derived from /usr/share/doc/expect/examples/autopasswd
20
+ interpreter.render(:text => <<-HERE, :to => filename)
21
+ set password "#{password}"
22
+ spawn passwd "#{user}"
23
+ expect "assword:"
24
+ sleep 0.1
25
+ send "$password\\r"
26
+ expect "assword:"
27
+ sleep 0.1
28
+ send "$password\\r"
29
+ expect eof
30
+ HERE
31
+
32
+ cmd = "expect #{filename} #{user} #{password}"
33
+ cmd << " > /dev/null" if opts[:quiet]
34
+ return(interpreter.sh cmd)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+
@@ -0,0 +1,67 @@
1
+ # == AccountManager::PasswdPTY
2
+ #
3
+ # An AccountManager driver for +passwd+ command found on Unix-like systems
4
+ # using the Ruby PTY implementation.
5
+ #
6
+ # *WARNING*: The Ruby PTY module is unreliable or unavailable on most
7
+ # platforms. It may hang indefinitely or report incorrect results. Every
8
+ # attempt has been made to work around these problems, but this is a low-level
9
+ # problem. You are strongly encouraged to install the +expect+ program, which
10
+ # works flawlessly. Once the +expect+ program is installed, passwords will be
11
+ # changed using the AccountManager::PasswdExpect driver, which works properly.
12
+ class ::AutomateIt::AccountManager::PasswdPTY < ::AutomateIt::AccountManager::BaseDriver
13
+ depends_on \
14
+ :programs => %w(passwd),
15
+ :libraries => %w(open3 expect pty)
16
+
17
+ def suitability(method, *args) # :nodoc:
18
+ # Level must be higher than Linux
19
+ return available? ? 3 : 0
20
+ end
21
+
22
+ # See AccountManager#passwd
23
+ def passwd(user, password, opts={})
24
+ log.info(PERROR+"Setting password with flaky Ruby PTY, which hangs or fails randomly. Install 'expect' (http://expect.nist.gov/) for reliable operation.")
25
+ _passwd_helper(user, password, opts) do
26
+ log.silence(Logger::WARN) do
27
+ interpreter.mktemp do |filename|
28
+ tries = 5
29
+ exitstatus = nil
30
+ begin
31
+ exitstruct = _passwd_raw(user, password, opts)
32
+ if exitstatus and not exitstruct.exitstatus.zero?
33
+ # FIXME AccountManager::Linux#passwd -- The `passwd` command randomly returns exit status 10 even when it succeeds. What does this mean and how to deal with it?! Temporary workaround is to throw an error and force a retry.
34
+ raise Errno::EPIPE.new("bad exitstatus %s" % exitstruct.exitstatus)
35
+ end
36
+ rescue Errno::EPIPE => e
37
+ # FIXME AccountManager::Linux#passwd -- EPIPE exception randomly thrown even when `passwd` succeeds. How to eliminate it? How to differentiate between this false error and a real one?
38
+ if tries <= 0
39
+ raise e
40
+ else
41
+ tries -= 1
42
+ retry
43
+ end
44
+ end
45
+ return exitstruct.exitstatus.zero?
46
+ end
47
+ end
48
+ end
49
+
50
+ end
51
+
52
+ def _passwd_raw(user, password, opts={})
53
+ quiet = (opts[:quiet] or not log.info?)
54
+
55
+ require 'open4'
56
+ return Open4::popen4("passwd %s 2>&1" % user) do |pid, sin, sout, serr|
57
+ $expect_verbose = ! quiet
58
+ 2.times do
59
+ sout.expect(/:/)
60
+ sleep 0.1 # Reduce chance of passwd thinking we're a robot :(
61
+ sin.puts password
62
+ puts "*" * 12 unless quiet
63
+ end
64
+ end
65
+ end
66
+ protected :_passwd_raw
67
+ end
@@ -0,0 +1,126 @@
1
+ # == AccountManager::POSIX
2
+ #
3
+ # A POSIX driver for the AccountManager.
4
+ class ::AutomateIt::AccountManager::POSIX < ::AutomateIt::AccountManager::Etc
5
+ depends_on :programs => %w(useradd usermod userdel groupadd groupmod groupdel)
6
+
7
+ def suitability(method, *args) # :nodoc:
8
+ # Level must be higher than Portable
9
+ return available? ? 2 : 0
10
+ end
11
+
12
+ #.......................................................................
13
+
14
+ # See AccountManager#add_user
15
+ def add_user(username, opts={})
16
+ return _add_user_helper(username, opts) do |username, opts|
17
+ cmd = "useradd"
18
+ cmd << " -c #{opts[:description] || username}"
19
+ cmd << " -d #{opts[:home]}" if opts[:home]
20
+ cmd << " -m" unless opts[:create_home] == false
21
+ cmd << " -G #{opts[:groups].join(',')}" if opts[:groups]
22
+ cmd << " -s #{opts[:shell] || "/bin/bash"}"
23
+ cmd << " -u #{opts[:uid]}" if opts[:uid]
24
+ cmd << " -g #{opts[:gid]}" if opts[:gid]
25
+ cmd << " #{username} < /dev/null"
26
+ cmd << " > /dev/null 2>&1 | grep -v blocks" if opts[:quiet]
27
+ interpreter.sh(cmd)
28
+ end
29
+ end
30
+
31
+ # TODO AccountManager#update_user -- implement
32
+ ### def update_user(username, opts={}) dispatch(username, opts) end
33
+
34
+ # See AccountManager#remove_user
35
+ def remove_user(username, opts={})
36
+ return _remove_user_helper(username, opts) do |username, opts|
37
+ # Options: -r -- remove the home directory and mail spool
38
+ cmd = "userdel"
39
+ cmd << " -r" unless opts[:remove_home] == false
40
+ cmd << " #{username}"
41
+ cmd << " > /dev/null" if opts[:quiet]
42
+ interpreter.sh(cmd)
43
+ end
44
+ end
45
+
46
+ # See AccountManager#add_groups_to_user
47
+ def add_groups_to_user(groups, username)
48
+ return _add_groups_to_user_helper(groups, username) do |missing, username|
49
+ targets = (groups_for_user(username) + missing).uniq
50
+
51
+ cmd = "usermod -G #{targets.join(',')} #{username}"
52
+ interpreter.sh(cmd)
53
+ end
54
+ end
55
+
56
+ # See AccountManager#remove_groups_from_user
57
+ def remove_groups_from_user(groups, username)
58
+ return _remove_groups_from_user_helper(groups, username) do |present, username|
59
+ matches = (groups_for_user(username) - [groups].flatten).uniq
60
+ cmd = "usermod -G #{matches.join(',')} #{username}"
61
+ interpreter.sh(cmd)
62
+ end
63
+ end
64
+
65
+ #.......................................................................
66
+
67
+ # See AccountManager#add_group
68
+ def add_group(groupname, opts={})
69
+ modified = false
70
+ unless has_group?(groupname)
71
+ modified = true
72
+
73
+ cmd = "groupadd"
74
+ cmd << " -g #{opts[:gid]}" if opts[:gid]
75
+ cmd << " #{groupname}"
76
+ interpreter.sh(cmd)
77
+
78
+ manager.invalidate(:groups)
79
+ end
80
+
81
+ if opts[:members]
82
+ modified = true
83
+ add_users_to_group(opts[:members], groupname)
84
+ end
85
+
86
+ return modified ? groups[groupname] : false
87
+ end
88
+
89
+ # TODO AccountManager#update_group -- implement
90
+ ### def update_group(groupname, opts={}) dispatch(groupname, opts) end
91
+
92
+ # See AccountManager#remove_group
93
+ def remove_group(groupname, opts={})
94
+ return false unless has_group?(groupname)
95
+ cmd = "groupdel #{groupname}"
96
+ interpreter.sh(cmd)
97
+
98
+ manager.invalidate(:groups)
99
+
100
+ return true
101
+ end
102
+
103
+ # See AccountManager#add_users_to_group
104
+ def add_users_to_group(users, groupname)
105
+ _add_users_to_group_helper(users, groupname) do |missing, groupname|
106
+ for username in missing
107
+ targets = (groups_for_user(username) + [groupname]).uniq
108
+ cmd = "usermod -G #{targets.join(',')} #{username}"
109
+ interpreter.sh(cmd)
110
+ end
111
+ end
112
+ end
113
+
114
+ # See AccountManager#remove_users_from_group
115
+ def remove_users_from_group(users, groupname)
116
+ _remove_users_from_group_helper(users, groupname) do |present, groupname|
117
+ u2g = users_to_groups
118
+ for username in present
119
+ user_groups = u2g[username]
120
+ # FIXME tries to include non-present groups, should use some variant of present
121
+ cmd = "usermod -G #{(user_groups.to_a-[groupname]).join(',')} #{username}"
122
+ interpreter.sh(cmd)
123
+ end
124
+ end
125
+ end
126
+ end
@@ -51,6 +51,37 @@ module AutomateIt
51
51
  #
52
52
  # Hello world!
53
53
  #
54
+ # === Partitioning recipes
55
+ #
56
+ # You should split up your recipe code into different recipe files. This will
57
+ # improve the clarity of your code because each file can perform one task,
58
+ # and you'll also be able to easily execute a specific recipe.
59
+ #
60
+ # For example, you can use a task-specific <tt>recipes/postgresql.rb</tt> to
61
+ # set up the PostgreSQL database server, and a <tt>recipes/apache.rb</tt> to
62
+ # setup the Apache web server.
63
+ #
64
+ # === Running recipes from other recipes
65
+ #
66
+ # You can run one recipe from another. It's a good idea to create a top-level
67
+ # recipe that invokes the other recipes. This lets you run a single recipe
68
+ # that will in turn run all your other recipes in the correct order, such as
69
+ # setting up the database server before the web server so that websites.
70
+ #
71
+ # For example, consider a <tt>recipes/all.rb</tt> file with these lines:
72
+ #
73
+ # invoke 'postgresql' if tagged? :postgresql_server
74
+ # invoke 'nginx' if tagged? :nginx_server
75
+ # invoke 'apache' if tagged? :apache_server
76
+ #
77
+ # The first line above checks to see if the current host has the
78
+ # <tt>postgresql_server</tt> tag, and if it does, invokes the
79
+ # <tt>recipes/postgresql.rb</tt> recipe.
80
+ #
81
+ # You must run recipes from other recipes using AutomateIt's +invoke+ method
82
+ # and not Ruby's +require+, because the +invoke+ passes along the AutomateIt
83
+ # interpreter to the other recipes so they can continue execution.
84
+ #
54
85
  # === Using project libraries
55
86
  #
56
87
  # Any files ending with <tt>.rb</tt> that you put into the project's
@@ -133,8 +164,10 @@ module AutomateIt
133
164
  # aifield -p /tmp/hello_project greeting
134
165
  #
135
166
  # The <tt>-p</tt> specifies the project path (its an alias for
136
- # <tt>--project</tt>). More commands are available. You can see the
137
- # documentation and examples for these commands by running:
167
+ # <tt>--project</tt>).
168
+ #
169
+ # More commands are available. For documentation and examples run the
170
+ # following commands from the Unix shell:
138
171
  #
139
172
  # aifield --help
140
173
  # aitag --help
@@ -151,21 +184,21 @@ module AutomateIt
151
184
  # If you want to share a project between different hosts, you're responsible for distributing the files between them. This isn't a big deal though because these are just text files and your OS has dozens of excellent ways to distribute these.
152
185
  #
153
186
  # Common approaches to distribution:
154
- # * *Shared directory*: Your hosts mount a shared network directory (e.g., +nfs+ or +smb+) with your project. This is very easy if your hosts already have a shared directory, but can be a nuisance otherwise because it opens potential security holes and risks having you hosts hang if the master goes offline.
155
- # * *Client pull*: Your hosts download the latest copy of your project from a master repository using a remote copy tool (e.g., +rsync+) or a revision control system (e.g., +cvs+, +svn+, +hg+). This is a safe, simple and secure option.
156
- # * *Server push*: You have a master push out the project files to clients using a remote copy tool. This can be awkward and time-consuming because the server must go through a list of all hosts and copy files to them individually.
187
+ # * <b>Shared directory</b>: Your hosts mount a shared network directory (e.g., +nfs+ or +smb+) with your project. This is very easy if your hosts already have a shared directory, but can be a nuisance otherwise because it opens potential security holes and risks having you hosts hang if the master goes offline.
188
+ # * <b>Client pull</b>: Your hosts download the latest copy of your project from a master repository using a remote copy tool (e.g., +rsync+) or a revision control system (e.g., +cvs+, +svn+, +hg+). This is a safe, simple and secure option.
189
+ # * <b>Server push</b>: You have a master push out the project files to clients using a remote copy tool and then invoke +automateit+ on them via SSH. This can be awkward and time-consuming because the server must go through a list of all hosts and copy files to them individually.
157
190
  #
158
191
  # An example of a complete solution for distributing system configuration management files:
159
- # * Setup an +svn+ or +hg+ repository to store your project and create a special account for the hosts to use to checkout code.
160
- # * Write a wrapper script for running the recipes, for example, write a "/usr/bin/myautomateit" shell script like:
192
+ # 1. Setup an +svn+ or other version control repository to store your project and create a special account for the hosts to use to checkout code.
193
+ # 2. Write a wrapper script for running the recipes, for example, write a "/usr/bin/myautomateit" shell script like:
161
194
  #
162
195
  # #!/bin/sh
163
196
  # cd /var/local/myautomateit
164
197
  # svn update --quiet
165
198
  # automateit recipe/default.rb
166
- # * Run this wrapper once an hour using cron so that your systems are always up to date. AutomateIt only prints output when it makes a change, so cron will only email you when you commit new code to the repository and the hosts make changes.
167
- # * If you need to run a recipe on the machine right now, SSH into it and run the wrapper.
168
- # * If you need to run the script early on a bunch of machines and don't want to manually SSH into each one, you can leverage the +aitag+ (see <tt>aitag --help</tt>) to execute a Unix command across multiple systems. For example, you could use a Unix shell command like this to execute the wrapper on all hosts tagged with +apache_servers+:
199
+ # 3. Run this wrapper once an hour using cron so that your systems are always up to date. AutomateIt only prints output when it makes a change, so cron will only email you when you commit new code to the repository and the hosts make changes.
200
+ # 4. If you need to run a recipe on the machine right now, SSH into it and run the wrapper.
201
+ # 5. If you need to run the script early on a bunch of machines and don't want to manually SSH into each one, you can leverage the +aitag+ (see <tt>aitag --help</tt>) to execute a Unix command across multiple systems. For example, you could use a Unix shell command like this to execute the wrapper on all hosts tagged with +apache_servers+:
169
202
  #
170
203
  # for host in `aitag -p /var/local/myautomateit -w apache_server`; do
171
204
  # echo "# $host"
@@ -1,7 +1,7 @@
1
1
  # See AutomateIt::Interpreter for usage information.
2
2
  module AutomateIt # :nodoc:
3
3
  # AutomateIt version
4
- VERSION=Gem::Version.new("0.71021")
4
+ VERSION=Gem::Version.new("0.71030")
5
5
 
6
6
  # Instantiates a new Interpreter. See documentation for
7
7
  # Interpreter#setup.
@@ -7,22 +7,65 @@ elsif not INTERPRETER.superuser?
7
7
  elsif not INTERPRETER.account_manager.available?(:add_user)
8
8
  puts "NOTE: Can't find AccountManager for this platform, #{__FILE__}"
9
9
  else
10
- describe "AutomateIt::AccountManager" do
10
+ describe AutomateIt::AccountManager do
11
11
  before(:all) do
12
+ ### @independent = true
13
+ @independent = false
14
+
15
+ ### @a = AutomateIt.new(:verbosity => Logger::INFO)
12
16
  @a = AutomateIt.new(:verbosity => Logger::WARN)
13
17
  @m = @a.account_manager
18
+ @quiet = ! @a.log.info?
14
19
 
15
- @username = "automateit_testuser"
16
- @groupname = "automateit_testgroup"
20
+ # Some OSes are limited to 8 character names :(
21
+ @username = "aitestus"
22
+ @groupname = "aitestgr"
17
23
 
18
- raise "User named '#{@username}' found. If this isn't a real user, delete it so that the test can contineu. If this is a real user, change the spec to test with a user that shouldn't exist." if @m.users[@username]
19
- raise "Group named '#{@groupname}' found. If this isn't a real group, delete it so that the test can contineu. If this is a real group, change the spec to test with a group that shouldn't exist." if @m.groups[@groupname]
24
+ begin
25
+ raise "User named '#{@username}' found. If this isn't a real user, delete it so that the test can contineu. If this is a real user, change the spec to test with a user that shouldn't exist." if @m.users[@username]
26
+ raise "Group named '#{@groupname}' found. If this isn't a real group, delete it so that the test can contineu. If this is a real group, change the spec to test with a group that shouldn't exist." if @m.groups[@groupname]
27
+ rescue Exception => e
28
+ @fail = true
29
+ raise e
30
+ end
20
31
  end
21
32
 
22
33
  after(:all) do
23
- @m.remove_user(@username, :quiet => true)
24
- @m.remove_group(@username, :quiet => true)
25
- @m.remove_group(@groupname, :quiet => true)
34
+ unless @fail
35
+ @m.remove_user(@username, :quiet => true)
36
+ @m.remove_group(@username, :quiet => true)
37
+ @m.remove_group(@groupname, :quiet => true)
38
+ end
39
+ end
40
+
41
+ after(:each) do
42
+ unless @fail
43
+ if @independent
44
+ @m.remove_user(@username, :quiet => true)
45
+ @m.remove_group(@username, :quiet => true)
46
+ @m.remove_group(@groupname, :quiet => true)
47
+ end
48
+ end
49
+ end
50
+
51
+ def add_user(opts={})
52
+ # SunOS /home entries don't exist until you add them to auto_home, so
53
+ # work around this by using a directory we know can be used
54
+ home = INTERPRETER.tagged?(:sunos) ? "/var/tmp/#{@username}" : nil
55
+
56
+ defaults = { :passwd => "asdf", :shell => "/bin/false", :home => home,
57
+ :quiet => @quiet }
58
+
59
+ return @m.add_user(@username, defaults.merge(opts))
60
+ end
61
+
62
+ def add_group
63
+ return @m.add_group(@groupname)
64
+ end
65
+
66
+ def add_user_with_group
67
+ add_user
68
+ return @m.add_group(@groupname, :members => @username)
26
69
  end
27
70
 
28
71
  it "should find root user" do
@@ -36,27 +79,29 @@ else
36
79
  end
37
80
 
38
81
  it "should create a user" do
39
- entry = @m.add_user(@username, :passwd => "asdf", :shell => "/bin/false")
82
+ entry = add_user
40
83
 
41
84
  entry.should_not be_nil
42
85
  entry.name.should == @username
43
- # Leaves behind user for further tests
44
86
  end
45
87
 
46
88
  it "should have a user after one is created" do
47
- # Depends on user to be created by previous tests
89
+ add_user if @independent
90
+
48
91
  @m.has_user?(@username).should be_true
49
92
  end
50
93
 
51
94
  it "should query user data by name" do
52
- # Depends on user to be created by previous tests
95
+ add_user if @independent
96
+
53
97
  entry = @m.users[@username]
54
98
  entry.should_not be_nil
55
99
  entry.name.should == @username
56
100
  end
57
101
 
58
102
  it "should query user data by id" do
59
- # Depends on user to be created by previous tests
103
+ add_user if @independent
104
+
60
105
  uid = @m.users[@username].uid
61
106
 
62
107
  entry = @m.users[uid]
@@ -69,12 +114,14 @@ else
69
114
  end
70
115
 
71
116
  it "should create user group" do
72
- # Depends on user to be created by previous tests
117
+ add_user if @independent
118
+
73
119
  @m.groups[@username].should_not be_nil
74
120
  end
75
121
 
76
122
  it "should not re-add an existing user" do
77
- # Depends on user to be created by previous tests
123
+ add_user if @independent
124
+
78
125
  @m.add_user(@username).should be_false
79
126
  end
80
127
 
@@ -83,23 +130,28 @@ else
83
130
  end
84
131
 
85
132
  it "should add a group" do
86
- entry = @m.add_group(@groupname)
133
+ entry = add_group
134
+
87
135
  entry.should_not be_nil
88
136
  entry.name.should == @groupname
89
- # Leaves behind group for further tests
90
137
  end
91
138
 
92
139
  it "should not re-add a group" do
140
+ add_group if @independent
141
+
93
142
  @m.add_group(@groupname).should be_false
94
143
  end
95
144
 
96
145
  it "should query group data by name" do
146
+ add_group if @independent
147
+
97
148
  entry = @m.groups[@groupname]
98
149
  entry.should_not be_nil
99
150
  entry.name.should == @groupname
100
151
  end
101
152
 
102
153
  it "should query group data by id" do
154
+ add_group if @independent
103
155
  gid = @m.groups[@groupname].gid
104
156
 
105
157
  entry = @m.groups[gid]
@@ -112,7 +164,8 @@ else
112
164
  end
113
165
 
114
166
  it "should remove a group" do
115
- # Depends on group to be created by previous tests
167
+ add_group if @independent
168
+
116
169
  @m.remove_group(@groupname).should be_true
117
170
  end
118
171
 
@@ -124,41 +177,53 @@ else
124
177
  @m.users_for_group(@groupname).should == []
125
178
  end
126
179
 
127
- it "should add a group with members" do
128
- # Depends on user to be created by previous tests
129
- @m.add_group(@groupname, :members => @username)
130
- # Leaves behind group for further tests
131
- end
180
+ it "should add a group with members" do
181
+ add_user_with_group.should_not be_nil
182
+ end
132
183
 
133
- it "should query users in a group" do
134
- # Depends on group to be created by previous tests
135
- @m.users_for_group(@groupname).should == [@username]
136
- end
184
+ it "should query users in a group" do
185
+ add_user_with_group if @independent
137
186
 
138
- it "should query groups for a user" do
139
- # Depends on user to be created by previous tests
140
- # Depends on group to be created by previous tests
141
- @m.groups_for_user(@username).should include(@groupname)
142
- end
187
+ @m.users_for_group(@groupname).should == [@username]
188
+ end
143
189
 
144
- it "should remove users from a group" do
145
- # Depends on user to be created by previous tests
146
- # Depends on group to be created by previous tests
147
- @m.remove_users_from_group(@username, @groupname).should == [@username]
148
- end
190
+ it "should query groups for a user" do
191
+ add_user_with_group if @independent
192
+
193
+ @m.groups_for_user(@username).should include(@groupname)
194
+ end
195
+
196
+ it "should remove users from a group" do
197
+ add_user_with_group if @independent
198
+
199
+ @m.remove_users_from_group(@username, @groupname).should == [@username]
200
+ end
149
201
 
150
202
  it "should add groups to a user" do
151
- # Depends on user to be created by previous tests
203
+ add_user if @independent
204
+ add_group if @independent
205
+
152
206
  @m.add_groups_to_user(@groupname, @username).should == [@groupname]
207
+
208
+ end
209
+
210
+ it "should add users to group" do
211
+ @m.remove_groups_from_user(@groupname, @username) unless @independent
212
+ add_user if @independent
213
+ add_group if @independent
214
+
215
+ @m.add_users_to_group(@username, @groupname).should == [@username]
153
216
  end
154
217
 
155
218
  it "should remove groups from user" do
156
- # Depends on user to be created by previous tests
219
+ add_user_with_group if @independent
220
+
157
221
  @m.remove_groups_from_user(@groupname, @username).should == [@groupname]
158
222
  end
159
223
 
160
224
  it "should remove a group with members" do
161
- # Depends on group to be created by previous tests
225
+ add_group if @independent
226
+
162
227
  @m.remove_group(@groupname).should be_true
163
228
  end
164
229
 
@@ -189,7 +254,7 @@ else
189
254
  end
190
255
 
191
256
  it "should change password" do
192
- # Depends on user to be created by previous tests
257
+ add_user if @independent
193
258
  pass = "automateit"
194
259
 
195
260
  # TODO This isn't portable
@@ -201,18 +266,38 @@ else
201
266
  end
202
267
 
203
268
  before = extract_pwent(@username)
204
- @m.passwd(@username, pass).should be_true
269
+ @m.passwd(@username, pass, :quiet => @quiet).should be_true
205
270
  after = extract_pwent(@username)
206
271
  before.should_not eql(after)
207
272
  end
208
273
 
209
274
  it "should remove a user" do
210
- # Depends on user to be created by previous tests
275
+ add_user if @independent
211
276
  @m.remove_user(@username, :quiet => true).should be_true
212
277
  end
213
278
 
214
279
  it "should not remove a non-existent user" do
215
280
  @m.remove_user(@username).should be_false
216
281
  end
282
+
283
+ it "should add user with multiple groups" do
284
+ # Find the first few users
285
+ groups_expected = []
286
+ size = 3
287
+ Etc.group do |group|
288
+ groups_expected << group.name
289
+ break if groups_expected.size >= size
290
+ end
291
+
292
+ # Create a user
293
+ (user = add_user(:groups => groups_expected)).should_not be_true
294
+
295
+ # Make sure they have the right number of groups
296
+ groups_found = @m.groups_for_user(@username)
297
+ for group in groups_expected
298
+ ### puts "%s : %s" % [group, groups_found.include?(group)]
299
+ groups_found.should include(group)
300
+ end
301
+ end
217
302
  end
218
303
  end