chef 10.24.4 → 10.26.0.beta.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/distro/common/html/chef-client.8.html +3 -3
- data/distro/common/html/chef-expander.8.html +3 -3
- data/distro/common/html/chef-expanderctl.8.html +3 -3
- data/distro/common/html/chef-server-webui.8.html +3 -3
- data/distro/common/html/chef-server.8.html +3 -3
- data/distro/common/html/chef-solo.8.html +3 -3
- data/distro/common/html/chef-solr.8.html +3 -3
- data/distro/common/html/knife-bootstrap.1.html +3 -3
- data/distro/common/html/knife-client.1.html +3 -3
- data/distro/common/html/knife-configure.1.html +3 -3
- data/distro/common/html/knife-cookbook-site.1.html +3 -3
- data/distro/common/html/knife-cookbook.1.html +3 -3
- data/distro/common/html/knife-data-bag.1.html +3 -3
- data/distro/common/html/knife-environment.1.html +3 -3
- data/distro/common/html/knife-exec.1.html +3 -3
- data/distro/common/html/knife-index.1.html +3 -3
- data/distro/common/html/knife-node.1.html +3 -3
- data/distro/common/html/knife-role.1.html +3 -3
- data/distro/common/html/knife-search.1.html +3 -3
- data/distro/common/html/knife-ssh.1.html +3 -3
- data/distro/common/html/knife-status.1.html +3 -3
- data/distro/common/html/knife-tag.1.html +3 -3
- data/distro/common/html/knife.1.html +3 -3
- data/distro/common/html/shef.1.html +3 -3
- data/distro/common/man/man1/knife-bootstrap.1 +1 -1
- data/distro/common/man/man1/knife-client.1 +1 -1
- data/distro/common/man/man1/knife-configure.1 +1 -1
- data/distro/common/man/man1/knife-cookbook-site.1 +1 -1
- data/distro/common/man/man1/knife-cookbook.1 +1 -1
- data/distro/common/man/man1/knife-data-bag.1 +1 -1
- data/distro/common/man/man1/knife-environment.1 +1 -1
- data/distro/common/man/man1/knife-exec.1 +1 -1
- data/distro/common/man/man1/knife-index.1 +1 -1
- data/distro/common/man/man1/knife-node.1 +1 -1
- data/distro/common/man/man1/knife-role.1 +1 -1
- data/distro/common/man/man1/knife-search.1 +1 -1
- data/distro/common/man/man1/knife-ssh.1 +1 -1
- data/distro/common/man/man1/knife-status.1 +1 -1
- data/distro/common/man/man1/knife-tag.1 +1 -1
- data/distro/common/man/man1/knife.1 +1 -1
- data/distro/common/man/man1/shef.1 +1 -1
- data/distro/common/man/man8/chef-client.8 +1 -1
- data/distro/common/man/man8/chef-expander.8 +1 -1
- data/distro/common/man/man8/chef-expanderctl.8 +1 -1
- data/distro/common/man/man8/chef-server-webui.8 +1 -1
- data/distro/common/man/man8/chef-server.8 +1 -1
- data/distro/common/man/man8/chef-solo.8 +1 -1
- data/distro/common/man/man8/chef-solr.8 +1 -1
- data/lib/chef/application.rb +0 -7
- data/lib/chef/application/client.rb +8 -3
- data/lib/chef/application/windows_service.rb +1 -4
- data/lib/chef/client.rb +1 -1
- data/lib/chef/cookbook_uploader.rb +10 -1
- data/lib/chef/knife/cookbook_upload.rb +3 -9
- data/lib/chef/platform.rb +18 -5
- data/lib/chef/provider/user/solaris.rb +90 -0
- data/lib/chef/provider/user/useradd.rb +38 -29
- data/lib/chef/providers.rb +1 -0
- data/lib/chef/resource.rb +10 -1
- data/lib/chef/shef/shef_session.rb +2 -2
- data/lib/chef/version.rb +1 -1
- data/spec/unit/application/client_spec.rb +36 -1
- data/spec/unit/knife/cookbook_upload_spec.rb +9 -5
- data/spec/unit/platform_spec.rb +2 -1
- data/spec/unit/provider/user/solaris_spec.rb +414 -0
- data/spec/unit/provider/user/useradd_spec.rb +1 -1
- data/spec/unit/shef/shef_session_spec.rb +16 -3
- metadata +7 -5
data/lib/chef/platform.rb
CHANGED
|
@@ -73,6 +73,14 @@ class Chef
|
|
|
73
73
|
:mdadm => Chef::Provider::Mdadm
|
|
74
74
|
}
|
|
75
75
|
},
|
|
76
|
+
:gcel => {
|
|
77
|
+
:default => {
|
|
78
|
+
:package => Chef::Provider::Package::Apt,
|
|
79
|
+
:service => Chef::Provider::Service::Debian,
|
|
80
|
+
:cron => Chef::Provider::Cron,
|
|
81
|
+
:mdadm => Chef::Provider::Mdadm
|
|
82
|
+
}
|
|
83
|
+
},
|
|
76
84
|
:linaro => {
|
|
77
85
|
:default => {
|
|
78
86
|
:package => Chef::Provider::Package::Apt,
|
|
@@ -245,7 +253,8 @@ class Chef
|
|
|
245
253
|
:service => Chef::Provider::Service::Solaris,
|
|
246
254
|
:package => Chef::Provider::Package::Ips,
|
|
247
255
|
:cron => Chef::Provider::Cron::Solaris,
|
|
248
|
-
:group => Chef::Provider::Group::Usermod
|
|
256
|
+
:group => Chef::Provider::Group::Usermod,
|
|
257
|
+
:user => Chef::Provider::User::Solaris,
|
|
249
258
|
}
|
|
250
259
|
},
|
|
251
260
|
:solaris2 => {
|
|
@@ -253,19 +262,22 @@ class Chef
|
|
|
253
262
|
:service => Chef::Provider::Service::Solaris,
|
|
254
263
|
:package => Chef::Provider::Package::Ips,
|
|
255
264
|
:cron => Chef::Provider::Cron::Solaris,
|
|
256
|
-
:group => Chef::Provider::Group::Usermod
|
|
265
|
+
:group => Chef::Provider::Group::Usermod,
|
|
266
|
+
:user => Chef::Provider::User::Solaris
|
|
257
267
|
},
|
|
258
268
|
"5.9" => {
|
|
259
269
|
:service => Chef::Provider::Service::Solaris,
|
|
260
270
|
:package => Chef::Provider::Package::Solaris,
|
|
261
271
|
:cron => Chef::Provider::Cron::Solaris,
|
|
262
|
-
:group => Chef::Provider::Group::Usermod
|
|
272
|
+
:group => Chef::Provider::Group::Usermod,
|
|
273
|
+
:user => Chef::Provider::User::Solaris
|
|
263
274
|
},
|
|
264
275
|
"5.10" => {
|
|
265
276
|
:service => Chef::Provider::Service::Solaris,
|
|
266
277
|
:package => Chef::Provider::Package::Solaris,
|
|
267
278
|
:cron => Chef::Provider::Cron::Solaris,
|
|
268
|
-
:group => Chef::Provider::Group::Usermod
|
|
279
|
+
:group => Chef::Provider::Group::Usermod,
|
|
280
|
+
:user => Chef::Provider::User::Solaris
|
|
269
281
|
}
|
|
270
282
|
},
|
|
271
283
|
:smartos => {
|
|
@@ -273,7 +285,8 @@ class Chef
|
|
|
273
285
|
:service => Chef::Provider::Service::Solaris,
|
|
274
286
|
:package => Chef::Provider::Package::SmartOS,
|
|
275
287
|
:cron => Chef::Provider::Cron::Solaris,
|
|
276
|
-
:group => Chef::Provider::Group::Usermod
|
|
288
|
+
:group => Chef::Provider::Group::Usermod,
|
|
289
|
+
:user => Chef::Provider::User::Solaris
|
|
277
290
|
}
|
|
278
291
|
},
|
|
279
292
|
:netbsd => {
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Author:: Stephen Nelson-Smith (<sns@opscode.com>)
|
|
3
|
+
# Author:: Jon Ramsey (<jonathon.ramsey@gmail.com>)
|
|
4
|
+
# Copyright:: Copyright (c) 2012 Opscode, Inc.
|
|
5
|
+
# License:: Apache License, Version 2.0
|
|
6
|
+
#
|
|
7
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
8
|
+
# you may not use this file except in compliance with the License.
|
|
9
|
+
# You may obtain a copy of the License at
|
|
10
|
+
#
|
|
11
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
#
|
|
13
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
14
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
15
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
16
|
+
# See the License for the specific language governing permissions and
|
|
17
|
+
# limitations under the License.
|
|
18
|
+
|
|
19
|
+
class Chef
|
|
20
|
+
class Provider
|
|
21
|
+
class User
|
|
22
|
+
class Solaris < Chef::Provider::User::Useradd
|
|
23
|
+
UNIVERSAL_OPTIONS = [[:comment, "-c"], [:gid, "-g"], [:shell, "-s"], [:uid, "-u"]]
|
|
24
|
+
|
|
25
|
+
attr_writer :password_file
|
|
26
|
+
|
|
27
|
+
def initialize(new_resource, run_context)
|
|
28
|
+
@password_file = "/etc/shadow"
|
|
29
|
+
super
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def create_user
|
|
33
|
+
super
|
|
34
|
+
manage_password
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def manage_user
|
|
38
|
+
manage_password
|
|
39
|
+
super
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def manage_password
|
|
45
|
+
if @current_resource.password != @new_resource.password && @new_resource.password
|
|
46
|
+
Chef::Log.debug("#{@new_resource} setting password to #{@new_resource.password}")
|
|
47
|
+
write_shadow_file
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def write_shadow_file
|
|
52
|
+
buffer = Tempfile.new("shadow", "/etc")
|
|
53
|
+
::File.open(@password_file) do |shadow_file|
|
|
54
|
+
shadow_file.each do |entry|
|
|
55
|
+
user = entry.split(":").first
|
|
56
|
+
if user == @new_resource.username
|
|
57
|
+
buffer.write(updated_password(entry))
|
|
58
|
+
else
|
|
59
|
+
buffer.write(entry)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
buffer.close
|
|
64
|
+
|
|
65
|
+
# FIXME: mostly duplicates code with file provider deploying a file
|
|
66
|
+
mode = ::File.stat(@password_file).mode & 07777
|
|
67
|
+
uid = ::File.stat(@password_file).uid
|
|
68
|
+
gid = ::File.stat(@password_file).gid
|
|
69
|
+
|
|
70
|
+
FileUtils.chown uid, gid, buffer.path
|
|
71
|
+
FileUtils.chmod mode, buffer.path
|
|
72
|
+
|
|
73
|
+
FileUtils.mv buffer.path, @password_file
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def updated_password(entry)
|
|
77
|
+
fields = entry.split(":")
|
|
78
|
+
fields[1] = @new_resource.password
|
|
79
|
+
fields[2] = days_since_epoch
|
|
80
|
+
fields.join(":")
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def days_since_epoch
|
|
84
|
+
(Time.now.to_i / 86400).floor
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
7
|
# you may not use this file except in compliance with the License.
|
|
8
8
|
# You may obtain a copy of the License at
|
|
9
|
-
#
|
|
9
|
+
#
|
|
10
10
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
-
#
|
|
11
|
+
#
|
|
12
12
|
# Unless required by applicable law or agreed to in writing, software
|
|
13
13
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
14
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
@@ -21,7 +21,7 @@ require 'chef/provider/user'
|
|
|
21
21
|
|
|
22
22
|
class Chef
|
|
23
23
|
class Provider
|
|
24
|
-
class User
|
|
24
|
+
class User
|
|
25
25
|
class Useradd < Chef::Provider::User
|
|
26
26
|
UNIVERSAL_OPTIONS = [[:comment, "-c"], [:gid, "-g"], [:password, "-p"], [:shell, "-s"], [:uid, "-u"]]
|
|
27
27
|
|
|
@@ -32,21 +32,23 @@ class Chef
|
|
|
32
32
|
end
|
|
33
33
|
run_command(:command => command)
|
|
34
34
|
end
|
|
35
|
-
|
|
35
|
+
|
|
36
36
|
def manage_user
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
if universal_options != ""
|
|
38
|
+
command = compile_command("usermod") do |u|
|
|
39
|
+
u << universal_options
|
|
40
|
+
end
|
|
41
|
+
run_command(:command => command)
|
|
39
42
|
end
|
|
40
|
-
run_command(:command => command)
|
|
41
43
|
end
|
|
42
|
-
|
|
44
|
+
|
|
43
45
|
def remove_user
|
|
44
46
|
command = "userdel"
|
|
45
47
|
command << " -r" if managing_home_dir?
|
|
46
48
|
command << " #{@new_resource.username}"
|
|
47
49
|
run_command(:command => command)
|
|
48
50
|
end
|
|
49
|
-
|
|
51
|
+
|
|
50
52
|
def check_lock
|
|
51
53
|
status = popen4("passwd -S #{@new_resource.username}") do |pid, stdin, stdout, stderr|
|
|
52
54
|
status_line = stdout.gets.split(' ')
|
|
@@ -80,11 +82,11 @@ class Chef
|
|
|
80
82
|
|
|
81
83
|
@locked
|
|
82
84
|
end
|
|
83
|
-
|
|
85
|
+
|
|
84
86
|
def lock_user
|
|
85
87
|
run_command(:command => "usermod -L #{@new_resource.username}")
|
|
86
88
|
end
|
|
87
|
-
|
|
89
|
+
|
|
88
90
|
def unlock_user
|
|
89
91
|
run_command(:command => "usermod -U #{@new_resource.username}")
|
|
90
92
|
end
|
|
@@ -94,29 +96,36 @@ class Chef
|
|
|
94
96
|
base_command << " #{@new_resource.username}"
|
|
95
97
|
base_command
|
|
96
98
|
end
|
|
97
|
-
|
|
99
|
+
|
|
98
100
|
def universal_options
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
opts << " #{option} '#{@new_resource.send(field)}'"
|
|
101
|
+
@universal_options ||=
|
|
102
|
+
begin
|
|
103
|
+
opts = ''
|
|
104
|
+
# magic allows UNIVERSAL_OPTIONS to be overridden in a subclass
|
|
105
|
+
self.class::UNIVERSAL_OPTIONS.each do |field, option|
|
|
106
|
+
update_options(field, option, opts)
|
|
106
107
|
end
|
|
108
|
+
if updating_home?
|
|
109
|
+
if managing_home_dir?
|
|
110
|
+
Chef::Log.debug("#{@new_resource} managing the users home directory")
|
|
111
|
+
opts << " -m -d '#{@new_resource.home}'"
|
|
112
|
+
else
|
|
113
|
+
Chef::Log.debug("#{@new_resource} setting home to #{@new_resource.home}")
|
|
114
|
+
opts << " -d '#{@new_resource.home}'"
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
opts << " -o" if @new_resource.non_unique || @new_resource.supports[:non_unique]
|
|
118
|
+
opts
|
|
107
119
|
end
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
opts << " -d '#{@new_resource.home}'"
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def update_options(field, option, opts)
|
|
123
|
+
if @current_resource.send(field) != @new_resource.send(field)
|
|
124
|
+
if @new_resource.send(field)
|
|
125
|
+
Chef::Log.debug("#{@new_resource} setting #{field} to #{@new_resource.send(field)}")
|
|
126
|
+
opts << " #{option} '#{@new_resource.send(field)}'"
|
|
116
127
|
end
|
|
117
128
|
end
|
|
118
|
-
opts << " -o" if @new_resource.non_unique || @new_resource.supports[:non_unique]
|
|
119
|
-
opts
|
|
120
129
|
end
|
|
121
130
|
|
|
122
131
|
def useradd_options
|
data/lib/chef/providers.rb
CHANGED
|
@@ -82,6 +82,7 @@ require 'chef/provider/user/dscl'
|
|
|
82
82
|
require 'chef/provider/user/pw'
|
|
83
83
|
require 'chef/provider/user/useradd'
|
|
84
84
|
require 'chef/provider/user/windows'
|
|
85
|
+
require 'chef/provider/user/solaris'
|
|
85
86
|
|
|
86
87
|
require 'chef/provider/group/aix'
|
|
87
88
|
require 'chef/provider/group/dscl'
|
data/lib/chef/resource.rb
CHANGED
|
@@ -146,6 +146,15 @@ F
|
|
|
146
146
|
include Chef::Mixin::ConvertToClassName
|
|
147
147
|
include Chef::Mixin::Deprecation
|
|
148
148
|
|
|
149
|
+
if Module.method(:const_defined?).arity == 1
|
|
150
|
+
def self.strict_const_defined?(const)
|
|
151
|
+
const_defined?(const)
|
|
152
|
+
end
|
|
153
|
+
else
|
|
154
|
+
def self.strict_const_defined?(const)
|
|
155
|
+
const_defined?(const, false)
|
|
156
|
+
end
|
|
157
|
+
end
|
|
149
158
|
|
|
150
159
|
# Set or return the list of "state attributes" implemented by the Resource
|
|
151
160
|
# subclass. State attributes are attributes that describe the desired state
|
|
@@ -731,7 +740,7 @@ F
|
|
|
731
740
|
|
|
732
741
|
# Add log entry if we override an existing light-weight resource.
|
|
733
742
|
class_name = convert_to_class_name(rname)
|
|
734
|
-
if Chef::Resource.
|
|
743
|
+
if Chef::Resource.strict_const_defined?(class_name)
|
|
735
744
|
Chef::Log.info("#{class_name} light-weight resource already initialized -- overriding!")
|
|
736
745
|
old_class = Chef::Resource.send(:remove_const, class_name)
|
|
737
746
|
Chef::Resource.resource_classes.delete(old_class)
|
|
@@ -202,8 +202,8 @@ module Shef
|
|
|
202
202
|
Chef::Cookbook::FileVendor.on_create { |manifest| Chef::Cookbook::RemoteFileVendor.new(manifest, Chef::REST.new(Chef::Config[:server_url])) }
|
|
203
203
|
cookbook_hash = @client.sync_cookbooks
|
|
204
204
|
cookbook_collection = Chef::CookbookCollection.new(cookbook_hash)
|
|
205
|
-
@run_context = Chef::RunContext.new(node, cookbook_collection, @events)
|
|
206
|
-
@run_context.load(
|
|
205
|
+
@run_context = Chef::RunContext.new(node, cookbook_collection, @events)
|
|
206
|
+
@run_context.load(@node.run_list.expand(@node.chef_environment))
|
|
207
207
|
@run_status.run_context = run_context
|
|
208
208
|
end
|
|
209
209
|
|
data/lib/chef/version.rb
CHANGED
|
@@ -19,7 +19,7 @@ require 'spec_helper'
|
|
|
19
19
|
|
|
20
20
|
describe Chef::Application::Client, "reconfigure" do
|
|
21
21
|
before do
|
|
22
|
-
@original_config = Chef::Config.configuration
|
|
22
|
+
@original_config = Chef::Config.configuration.dup
|
|
23
23
|
|
|
24
24
|
@app = Chef::Application::Client.new
|
|
25
25
|
@app.stub!(:configure_opt_parser).and_return(true)
|
|
@@ -134,3 +134,38 @@ describe Chef::Application::Client, "setup_application" do
|
|
|
134
134
|
Chef::Config[:solo] = false
|
|
135
135
|
end
|
|
136
136
|
end
|
|
137
|
+
|
|
138
|
+
describe Chef::Application::Client, "run_application", :unix_only do
|
|
139
|
+
before do
|
|
140
|
+
Chef::Config[:daemonize] = false
|
|
141
|
+
Chef::Config[:interval] = 10
|
|
142
|
+
|
|
143
|
+
@pipe = IO.pipe
|
|
144
|
+
@app = Chef::Application::Client.new
|
|
145
|
+
@app.stub(:run_chef_client) do
|
|
146
|
+
@pipe[1].puts 'started'
|
|
147
|
+
sleep 1
|
|
148
|
+
@pipe[1].puts 'finished'
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
it "should exit gracefully when sent SIGTERM" do
|
|
153
|
+
pid = fork do
|
|
154
|
+
begin
|
|
155
|
+
@app.run_application
|
|
156
|
+
rescue SystemExit # expected
|
|
157
|
+
rescue Exception => e
|
|
158
|
+
$stderr.puts e
|
|
159
|
+
$stderr.puts e.backtrace
|
|
160
|
+
ensure
|
|
161
|
+
exit!
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
IO.select([@pipe[0]], nil, nil, 10).should_not be_nil
|
|
165
|
+
@pipe[0].gets.should == "started\n"
|
|
166
|
+
Process.kill("TERM", pid)
|
|
167
|
+
Process.wait
|
|
168
|
+
IO.select([@pipe[0]], nil, nil, 0).should_not be_nil
|
|
169
|
+
@pipe[0].gets.should == "finished\n"
|
|
170
|
+
end
|
|
171
|
+
end
|
|
@@ -41,7 +41,8 @@ describe Chef::Knife::CookbookUpload do
|
|
|
41
41
|
|
|
42
42
|
describe 'run' do
|
|
43
43
|
before(:each) do
|
|
44
|
-
@
|
|
44
|
+
@cookbook_uploader = stub(:upload_cookbooks => nil)
|
|
45
|
+
Chef::CookbookUploader.stub(:new => @cookbook_uploader)
|
|
45
46
|
Chef::CookbookVersion.stub(:list_all_versions).and_return({})
|
|
46
47
|
end
|
|
47
48
|
|
|
@@ -125,8 +126,11 @@ describe Chef::Knife::CookbookUpload do
|
|
|
125
126
|
|
|
126
127
|
describe 'when a frozen cookbook exists on the server' do
|
|
127
128
|
it 'should fail to replace it' do
|
|
128
|
-
|
|
129
|
-
@
|
|
129
|
+
exception = Chef::Exceptions::CookbookFrozen.new
|
|
130
|
+
@cookbook_uploader.should_receive(:upload_cookbooks).
|
|
131
|
+
and_raise(exception)
|
|
132
|
+
@knife.ui.stub(:error)
|
|
133
|
+
@knife.ui.should_receive(:error).with(exception)
|
|
130
134
|
lambda { @knife.run }.should raise_error(SystemExit)
|
|
131
135
|
end
|
|
132
136
|
|
|
@@ -140,5 +144,5 @@ describe Chef::Knife::CookbookUpload do
|
|
|
140
144
|
lambda { @knife.run }.should raise_error(SystemExit)
|
|
141
145
|
end
|
|
142
146
|
end
|
|
143
|
-
end
|
|
144
|
-
end
|
|
147
|
+
end # run
|
|
148
|
+
end
|
data/spec/unit/platform_spec.rb
CHANGED
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Author:: Adam Jacob (<adam@opscode.com>)
|
|
3
|
+
# Author:: Daniel DeLeo (<dan@opscode.com>)
|
|
4
|
+
# Copyright:: Copyright (c) 2008, 2010 Opscode, Inc.
|
|
5
|
+
#
|
|
6
|
+
# License:: Apache License, Version 2.0
|
|
7
|
+
#
|
|
8
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
9
|
+
# you may not use this file except in compliance with the License.
|
|
10
|
+
# You may obtain a copy of the License at
|
|
11
|
+
#
|
|
12
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
13
|
+
#
|
|
14
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
15
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
16
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
17
|
+
# See the License for the specific language governing permissions and
|
|
18
|
+
# limitations under the License.
|
|
19
|
+
#
|
|
20
|
+
|
|
21
|
+
require 'spec_helper'
|
|
22
|
+
|
|
23
|
+
describe Chef::Provider::User::Solaris do
|
|
24
|
+
before(:each) do
|
|
25
|
+
@node = Chef::Node.new
|
|
26
|
+
@events = Chef::EventDispatch::Dispatcher.new
|
|
27
|
+
@run_context = Chef::RunContext.new(@node, {}, @events)
|
|
28
|
+
|
|
29
|
+
@new_resource = Chef::Resource::User.new("adam", @run_context)
|
|
30
|
+
@new_resource.comment "Adam Jacob"
|
|
31
|
+
@new_resource.uid 1000
|
|
32
|
+
@new_resource.gid 1000
|
|
33
|
+
@new_resource.home "/home/adam"
|
|
34
|
+
@new_resource.shell "/usr/bin/zsh"
|
|
35
|
+
@new_resource.password "abracadabra"
|
|
36
|
+
@new_resource.system false
|
|
37
|
+
@new_resource.manage_home false
|
|
38
|
+
@new_resource.non_unique false
|
|
39
|
+
@current_resource = Chef::Resource::User.new("adam", @run_context)
|
|
40
|
+
@current_resource.comment "Adam Jacob"
|
|
41
|
+
@current_resource.uid 1000
|
|
42
|
+
@current_resource.gid 1000
|
|
43
|
+
@current_resource.home "/home/adam"
|
|
44
|
+
@current_resource.shell "/usr/bin/zsh"
|
|
45
|
+
@current_resource.password "abracadabra"
|
|
46
|
+
@current_resource.system false
|
|
47
|
+
@current_resource.manage_home false
|
|
48
|
+
@current_resource.non_unique false
|
|
49
|
+
@current_resource.supports({:manage_home => false, :non_unique => false})
|
|
50
|
+
@provider = Chef::Provider::User::Solaris.new(@new_resource, @run_context)
|
|
51
|
+
@provider.current_resource = @current_resource
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
describe "when setting option" do
|
|
55
|
+
field_list = {
|
|
56
|
+
'comment' => "-c",
|
|
57
|
+
'gid' => "-g",
|
|
58
|
+
'uid' => "-u",
|
|
59
|
+
'shell' => "-s"
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
field_list.each do |attribute, option|
|
|
63
|
+
it "should check for differences in #{attribute} between the new and current resources" do
|
|
64
|
+
@current_resource.should_receive(attribute)
|
|
65
|
+
@new_resource.should_receive(attribute)
|
|
66
|
+
@provider.universal_options
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it "should set the option for #{attribute} if the new resources #{attribute} is not nil" do
|
|
70
|
+
@new_resource.stub!(attribute).and_return("hola")
|
|
71
|
+
@provider.universal_options.should eql(" #{option} 'hola'")
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
it "should set the option for #{attribute} if the new resources #{attribute} is not nil, without homedir management" do
|
|
75
|
+
@new_resource.stub!(:supports).and_return({:manage_home => false,
|
|
76
|
+
:non_unique => false})
|
|
77
|
+
@new_resource.stub!(attribute).and_return("hola")
|
|
78
|
+
@provider.universal_options.should eql(" #{option} 'hola'")
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
it "should set the option for #{attribute} if the new resources #{attribute} is not nil, without homedir management (using real attributes)" do
|
|
82
|
+
@new_resource.stub!(:manage_home).and_return(false)
|
|
83
|
+
@new_resource.stub!(:non_unique).and_return(false)
|
|
84
|
+
@new_resource.stub!(attribute).and_return("hola")
|
|
85
|
+
@provider.universal_options.should eql(" #{option} 'hola'")
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
it "should combine all the possible options" do
|
|
90
|
+
match_string = ""
|
|
91
|
+
field_list.sort{ |a,b| a[0] <=> b[0] }.each do |attribute, option|
|
|
92
|
+
@new_resource.stub!(attribute).and_return("hola")
|
|
93
|
+
match_string << " #{option} 'hola'"
|
|
94
|
+
end
|
|
95
|
+
@provider.universal_options.should eql(match_string)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
describe "when we want to set a password" do
|
|
99
|
+
before do
|
|
100
|
+
@new_resource.password "hocus-pocus"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
it "should use its own shadow file writer to set the password" do
|
|
104
|
+
@provider.should_receive(:write_shadow_file)
|
|
105
|
+
@provider.stub!(:run_command).and_return(true)
|
|
106
|
+
@provider.manage_user
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
it "should write out a modified version of the password file" do
|
|
110
|
+
password_file = Tempfile.new("shadow")
|
|
111
|
+
password_file.puts "adam:existingpassword:15441::::::"
|
|
112
|
+
password_file.close
|
|
113
|
+
@provider.password_file = password_file.path
|
|
114
|
+
@provider.stub!(:run_command).and_return(true)
|
|
115
|
+
# may not be able to write to /etc for tests...
|
|
116
|
+
temp_file = Tempfile.new("shadow")
|
|
117
|
+
Tempfile.stub!(:new).with("shadow", "/etc").and_return(temp_file)
|
|
118
|
+
@new_resource.password "verysecurepassword"
|
|
119
|
+
@provider.manage_user
|
|
120
|
+
::File.open(password_file.path, "r").read.should =~ /adam:verysecurepassword:/
|
|
121
|
+
password_file.unlink
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
describe "when we want to create a system user" do
|
|
126
|
+
before do
|
|
127
|
+
@new_resource.manage_home(true)
|
|
128
|
+
@new_resource.non_unique(false)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
it "should set useradd -r" do
|
|
132
|
+
@new_resource.system(true)
|
|
133
|
+
@provider.useradd_options.should == " -r"
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
describe "when the resource has a different home directory and supports home directory management" do
|
|
138
|
+
before do
|
|
139
|
+
@new_resource.stub!(:home).and_return("/wowaweea")
|
|
140
|
+
@new_resource.stub!(:supports).and_return({:manage_home => true,
|
|
141
|
+
:non_unique => false})
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
it "should set -m -d /homedir" do
|
|
145
|
+
@provider.universal_options.should == " -m -d '/wowaweea'"
|
|
146
|
+
@provider.useradd_options.should == ""
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
describe "when the resource has a different home directory and supports home directory management (using real attributes)" do
|
|
151
|
+
before do
|
|
152
|
+
@new_resource.stub!(:home).and_return("/wowaweea")
|
|
153
|
+
@new_resource.stub!(:manage_home).and_return(true)
|
|
154
|
+
@new_resource.stub!(:non_unique).and_return(false)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
it "should set -m -d /homedir" do
|
|
158
|
+
@provider.universal_options.should eql(" -m -d '/wowaweea'")
|
|
159
|
+
@provider.useradd_options.should == ""
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
describe "when the resource supports non_unique ids" do
|
|
164
|
+
before do
|
|
165
|
+
@new_resource.stub!(:supports).and_return({:manage_home => false,
|
|
166
|
+
:non_unique => true})
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
it "should set -m -o" do
|
|
170
|
+
@provider.universal_options.should eql(" -o")
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
describe "when the resource supports non_unique ids (using real attributes)" do
|
|
175
|
+
before do
|
|
176
|
+
@new_resource.stub!(:manage_home).and_return(false)
|
|
177
|
+
@new_resource.stub!(:non_unique).and_return(true)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
it "should set -m -o" do
|
|
181
|
+
@provider.universal_options.should eql(" -o")
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
describe "when creating a user" do
|
|
187
|
+
before(:each) do
|
|
188
|
+
@current_resource = Chef::Resource::User.new(@new_resource.name, @run_context)
|
|
189
|
+
@current_resource.username(@new_resource.username)
|
|
190
|
+
@provider.current_resource = @current_resource
|
|
191
|
+
@provider.new_resource.manage_home true
|
|
192
|
+
@provider.new_resource.home "/Users/mud"
|
|
193
|
+
@provider.new_resource.gid '23'
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
it "runs useradd with the computed command options" do
|
|
197
|
+
command = "useradd -c 'Adam Jacob' -g '23' -s '/usr/bin/zsh' -u '1000' -m -d '/Users/mud' adam"
|
|
198
|
+
@provider.should_receive(:run_command).with({ :command => command }).and_return(true)
|
|
199
|
+
@provider.should_receive(:manage_password).and_return(nil)
|
|
200
|
+
@provider.create_user
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
describe "and home is not specified for new system user resource" do
|
|
204
|
+
|
|
205
|
+
before do
|
|
206
|
+
@provider.new_resource.system true
|
|
207
|
+
# there is no public API to set attribute's value to nil
|
|
208
|
+
@provider.new_resource.instance_variable_set("@home", nil)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
it "should not include -m or -d in the command options" do
|
|
212
|
+
command = "useradd -c 'Adam Jacob' -g '23' -s '/usr/bin/zsh' -u '1000' -r adam"
|
|
213
|
+
@provider.should_receive(:run_command).with({ :command => command }).and_return(true)
|
|
214
|
+
@provider.should_receive(:manage_password).and_return(nil)
|
|
215
|
+
@provider.create_user
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
describe "when managing a user" do
|
|
223
|
+
before(:each) do
|
|
224
|
+
@provider.new_resource.manage_home true
|
|
225
|
+
@provider.new_resource.home "/Users/mud"
|
|
226
|
+
@provider.new_resource.gid '23'
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# CHEF-3423, -m must come before the username
|
|
230
|
+
it "runs usermod with the computed command options" do
|
|
231
|
+
@provider.should_receive(:run_command).with({ :command => "usermod -g '23' -m -d '/Users/mud' adam" }).and_return(true)
|
|
232
|
+
@provider.manage_user
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
it "does not set the -r option to usermod" do
|
|
236
|
+
@new_resource.system(true)
|
|
237
|
+
@provider.should_receive(:run_command).with({ :command => "usermod -g '23' -m -d '/Users/mud' adam" }).and_return(true)
|
|
238
|
+
@provider.manage_user
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
it "CHEF-3429: does not set -m if we aren't changing the home directory" do
|
|
242
|
+
@provider.should_receive(:updating_home?).and_return(false)
|
|
243
|
+
@provider.should_receive(:run_command).with({ :command => "usermod -g '23' adam" }).and_return(true)
|
|
244
|
+
@provider.manage_user
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
describe "when removing a user" do
|
|
249
|
+
|
|
250
|
+
it "should run userdel with the new resources user name" do
|
|
251
|
+
@provider.should_receive(:run_command).with({ :command => "userdel #{@new_resource.username}" }).and_return(true)
|
|
252
|
+
@provider.remove_user
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
it "should run userdel with the new resources user name and -r if manage_home is true" do
|
|
256
|
+
@new_resource.stub!(:supports).and_return({ :manage_home => true,
|
|
257
|
+
:non_unique => false})
|
|
258
|
+
@provider.should_receive(:run_command).with({ :command => "userdel -r #{@new_resource.username}"}).and_return(true)
|
|
259
|
+
@provider.remove_user
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
it "should run userdel with the new resources user name if non_unique is true" do
|
|
263
|
+
@new_resource.stub!(:supports).and_return({ :manage_home => false,
|
|
264
|
+
:non_unique => true})
|
|
265
|
+
@provider.should_receive(:run_command).with({ :command => "userdel #{@new_resource.username}"}).and_return(true)
|
|
266
|
+
@provider.remove_user
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
describe "when checking the lock" do
|
|
271
|
+
before(:each) do
|
|
272
|
+
# @node = Chef::Node.new
|
|
273
|
+
# @new_resource = mock("Chef::Resource::User",
|
|
274
|
+
# :nil_object => true,
|
|
275
|
+
# :username => "adam"
|
|
276
|
+
# )
|
|
277
|
+
@status = mock("Status", :exitstatus => 0)
|
|
278
|
+
#@provider = Chef::Provider::User::Useradd.new(@node, @new_resource)
|
|
279
|
+
@provider.stub!(:popen4).and_return(@status)
|
|
280
|
+
@stdin = mock("STDIN", :nil_object => true)
|
|
281
|
+
@stdout = mock("STDOUT", :nil_object => true)
|
|
282
|
+
@stdout.stub!(:gets).and_return("root P 09/02/2008 0 99999 7 -1")
|
|
283
|
+
@stderr = mock("STDERR", :nil_object => true)
|
|
284
|
+
@pid = mock("PID", :nil_object => true)
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
it "should call passwd -S to check the lock status" do
|
|
288
|
+
@provider.should_receive(:popen4).with("passwd -S #{@new_resource.username}").and_return(@status)
|
|
289
|
+
@provider.check_lock
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
it "should get the first line of passwd -S STDOUT" do
|
|
293
|
+
@provider.should_receive(:popen4).and_yield(@pid, @stdin, @stdout, @stderr).and_return(@status)
|
|
294
|
+
@stdout.should_receive(:gets).and_return("root P 09/02/2008 0 99999 7 -1")
|
|
295
|
+
@provider.check_lock
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
it "should return false if status begins with P" do
|
|
299
|
+
@provider.should_receive(:popen4).and_yield(@pid, @stdin, @stdout, @stderr).and_return(@status)
|
|
300
|
+
@provider.check_lock.should eql(false)
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
it "should return false if status begins with N" do
|
|
304
|
+
@stdout.stub!(:gets).and_return("root N")
|
|
305
|
+
@provider.should_receive(:popen4).and_yield(@pid, @stdin, @stdout, @stderr).and_return(@status)
|
|
306
|
+
@provider.check_lock.should eql(false)
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
it "should return true if status begins with L" do
|
|
310
|
+
@stdout.stub!(:gets).and_return("root L")
|
|
311
|
+
@provider.should_receive(:popen4).and_yield(@pid, @stdin, @stdout, @stderr).and_return(@status)
|
|
312
|
+
@provider.check_lock.should eql(true)
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
it "should raise a Chef::Exceptions::User if passwd -S fails on anything other than redhat/centos" do
|
|
316
|
+
@node.automatic_attrs[:platform] = 'ubuntu'
|
|
317
|
+
@status.should_receive(:exitstatus).and_return(1)
|
|
318
|
+
lambda { @provider.check_lock }.should raise_error(Chef::Exceptions::User)
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
['redhat', 'centos'].each do |os|
|
|
322
|
+
it "should not raise a Chef::Exceptions::User if passwd -S exits with 1 on #{os} and the passwd package is version 0.73-1" do
|
|
323
|
+
@node.automatic_attrs[:platform] = os
|
|
324
|
+
@stdout.stub!(:gets).and_return("passwd-0.73-1\n")
|
|
325
|
+
@status.should_receive(:exitstatus).twice.and_return(1)
|
|
326
|
+
@provider.should_receive(:popen4).with("passwd -S #{@new_resource.username}")
|
|
327
|
+
@provider.should_receive(:popen4).with("rpm -q passwd").and_yield(@pid, @stdin, @stdout, @stderr).and_return(@status)
|
|
328
|
+
lambda { @provider.check_lock }.should_not raise_error(Chef::Exceptions::User)
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
it "should raise a Chef::Exceptions::User if passwd -S exits with 1 on #{os} and the passwd package is not version 0.73-1" do
|
|
332
|
+
@node.automatic_attrs[:platform] = os
|
|
333
|
+
@stdout.stub!(:gets).and_return("passwd-0.73-2\n")
|
|
334
|
+
@status.should_receive(:exitstatus).twice.and_return(1)
|
|
335
|
+
@provider.should_receive(:popen4).with("passwd -S #{@new_resource.username}")
|
|
336
|
+
@provider.should_receive(:popen4).with("rpm -q passwd").and_yield(@pid, @stdin, @stdout, @stderr).and_return(@status)
|
|
337
|
+
lambda { @provider.check_lock }.should raise_error(Chef::Exceptions::User)
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
it "should raise a Chef::Exceptions::User if passwd -S exits with something other than 0 or 1 on #{os}" do
|
|
341
|
+
@node.automatic_attrs[:platform] = os
|
|
342
|
+
@status.should_receive(:exitstatus).twice.and_return(2)
|
|
343
|
+
lambda { @provider.check_lock }.should raise_error(Chef::Exceptions::User)
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
describe "when locking the user" do
|
|
349
|
+
it "should run usermod -L with the new resources username" do
|
|
350
|
+
@provider.should_receive(:run_command).with({ :command => "usermod -L #{@new_resource.username}"})
|
|
351
|
+
@provider.lock_user
|
|
352
|
+
end
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
describe "when unlocking the user" do
|
|
356
|
+
it "should run usermod -L with the new resources username" do
|
|
357
|
+
@provider.should_receive(:run_command).with({ :command => "usermod -U #{@new_resource.username}"})
|
|
358
|
+
@provider.unlock_user
|
|
359
|
+
end
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
describe "when checking if home needs updating" do
|
|
363
|
+
[
|
|
364
|
+
{
|
|
365
|
+
"action" => "should return false if home matches",
|
|
366
|
+
"current_resource_home" => [ "/home/laurent" ],
|
|
367
|
+
"new_resource_home" => [ "/home/laurent" ],
|
|
368
|
+
"expected_result" => false
|
|
369
|
+
},
|
|
370
|
+
{
|
|
371
|
+
"action" => "should return true if home doesn't match",
|
|
372
|
+
"current_resource_home" => [ "/home/laurent" ],
|
|
373
|
+
"new_resource_home" => [ "/something/else" ],
|
|
374
|
+
"expected_result" => true
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
"action" => "should return false if home only differs by trailing slash",
|
|
378
|
+
"current_resource_home" => [ "/home/laurent" ],
|
|
379
|
+
"new_resource_home" => [ "/home/laurent/", "/home/laurent" ],
|
|
380
|
+
"expected_result" => false
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
"action" => "should return false if home is an equivalent path",
|
|
384
|
+
"current_resource_home" => [ "/home/laurent" ],
|
|
385
|
+
"new_resource_home" => [ "/home/./laurent", "/home/laurent" ],
|
|
386
|
+
"expected_result" => false
|
|
387
|
+
},
|
|
388
|
+
].each do |home_check|
|
|
389
|
+
it home_check["action"] do
|
|
390
|
+
@provider.current_resource.home home_check["current_resource_home"].first
|
|
391
|
+
@current_home_mock = mock("Pathname")
|
|
392
|
+
@provider.new_resource.home home_check["new_resource_home"].first
|
|
393
|
+
@new_home_mock = mock("Pathname")
|
|
394
|
+
|
|
395
|
+
Pathname.should_receive(:new).with(@current_resource.home).and_return(@current_home_mock)
|
|
396
|
+
@current_home_mock.should_receive(:cleanpath).and_return(home_check["current_resource_home"].last)
|
|
397
|
+
Pathname.should_receive(:new).with(@new_resource.home).and_return(@new_home_mock)
|
|
398
|
+
@new_home_mock.should_receive(:cleanpath).and_return(home_check["new_resource_home"].last)
|
|
399
|
+
|
|
400
|
+
@provider.updating_home?.should == home_check["expected_result"]
|
|
401
|
+
end
|
|
402
|
+
end
|
|
403
|
+
it "should return true if the current home does not exist but a home is specified by the new resource" do
|
|
404
|
+
@new_resource = Chef::Resource::User.new("adam", @run_context)
|
|
405
|
+
@current_resource = Chef::Resource::User.new("adam", @run_context)
|
|
406
|
+
@provider = Chef::Provider::User::Solaris.new(@new_resource, @run_context)
|
|
407
|
+
@provider.current_resource = @current_resource
|
|
408
|
+
@current_resource.home nil
|
|
409
|
+
@new_resource.home "/home/kitten"
|
|
410
|
+
|
|
411
|
+
@provider.updating_home?.should == true
|
|
412
|
+
end
|
|
413
|
+
end
|
|
414
|
+
end
|