30
30
attr_writer :macosx_version_major
34
# JJM 2007-07-24: Not yet sure what initvars() does. I saw it in netinfo.rb
35
# I do know, however, that it makes methods "work" =)
36
# e.g. addcmd isn't available if this method call isn't present.
38
# JJM: Also, where this method is defined seems to impact the visibility
39
# of methods. If I put initvars after commands, confine and defaultfor,
40
# then getinfo is called from the parent class, not this class.
43
35
commands :dscl => "/usr/bin/dscl"
44
36
commands :dseditgroup => "/usr/sbin/dseditgroup"
45
37
commands :sw_vers => "/usr/bin/sw_vers"
80
72
:ip_address => 'IPAddress',
81
73
:members => 'GroupMembership',
84
76
@@password_hash_dir = "/var/db/shadow/hash"
87
79
# JJM Class method that provides an array of instance objects of this
89
81
# JJM: Properties are dependent on the Puppet::Type we're managine.
90
82
type_property_array = [:name] + @resource_type.validproperties
92
84
# Create a new instance of this Puppet::Type for each object present
94
86
list_all_present.collect do |name_string|
95
87
self.new(single_report(name_string, *type_property_array))
99
91
def self.get_ds_path
100
92
# JJM: 2007-07-24 This method dynamically returns the DS path we're concerned with.
101
93
# For example, if we're working with an user type, this will be /Users
102
94
# with a group type, this will be /Groups.
103
# @ds_path is an attribute of the class itself.
95
# @ds_path is an attribute of the class itself.
104
96
if defined? @ds_path
110
102
# Puppet::Type this class object is providing for.
111
103
return @resource_type.name.to_s.capitalize + "s"
114
106
def self.get_macosx_version_major
115
107
if defined? @macosx_version_major
116
108
return @macosx_version_major
119
product_version = Facter.value(:macosx_productversion)
120
if product_version.nil?
121
raise Puppet::Error, "Could not determine OS X version"
111
# Make sure we've loaded all of the facts
114
if Facter.value(:macosx_productversion_major)
115
product_version_major = Facter.value(:macosx_productversion_major)
117
# TODO: remove this code chunk once we require Facter 1.5.5 or higher.
118
Puppet.warning("DEPRECATION WARNING: Future versions of the directoryservice provider will require Facter 1.5.5 or newer.")
119
product_version = Facter.value(:macosx_productversion)
120
if product_version.nil?
121
fail("Could not determine OS X version from Facter")
123
product_version_major = product_version.scan(/(\d+)\.(\d+)./).join(".")
123
product_version_major = product_version.scan(/(\d+)\.(\d+)./).join(".")
124
125
if %w{10.0 10.1 10.2 10.3}.include?(product_version_major)
125
raise Puppet::Error, "%s is not supported by the directoryservice provider" % product_version_major
126
fail("%s is not supported by the directoryservice provider" % product_version_major)
127
128
@macosx_version_major = product_version_major
128
129
return @macosx_version_major
129
130
rescue Puppet::ExecutionFailure => detail
130
raise Puppet::Error, "Could not determine OS X version: %s" % detail
131
fail("Could not determine OS X version: %s" % detail)
134
136
def self.list_all_present
135
137
# JJM: List all objects of this Puppet::Type already present on the system.
137
139
dscl_output = execute(get_exec_preamble("-list"))
138
140
rescue Puppet::ExecutionFailure => detail
139
raise Puppet::Error, "Could not get %s list from DirectoryService" % [ @resource_type.name.to_s ]
141
fail("Could not get %s list from DirectoryService" % [ @resource_type.name.to_s ])
141
143
return dscl_output.split("\n")
144
146
def self.parse_dscl_url_data(dscl_output)
145
147
# we need to construct a Hash from the dscl -url output to match
146
148
# that returned by the dscl -plist output for 10.5+ clients.
192
194
next unless (@@ds_to_ns_attribute_map.keys.include?(ds_attribute) and type_properties.include? @@ds_to_ns_attribute_map[ds_attribute])
193
195
ds_value = input_hash[key]
194
196
case @@ds_to_ns_attribute_map[ds_attribute]
196
198
ds_value = ds_value # only members uses arrays so far
198
200
# OS X stores objects like uid/gid as strings.
199
201
# Try casting to an integer for these cases to be
200
202
# consistent with the other providers and the group type
209
211
attribute_hash[@@ds_to_ns_attribute_map[ds_attribute]] = ds_value
212
214
# NBK: need to read the existing password here as it's not actually
213
215
# stored in the user record. It is stored at a path that involves the
214
# UUID of the user record for non-Mobile local acccounts.
216
# UUID of the user record for non-Mobile local acccounts.
215
217
# Mobile Accounts are out of scope for this provider for now
216
218
if @resource_type.validproperties.include?(:password)
217
219
attribute_hash[:password] = self.get_password(attribute_hash[:guid])
219
221
return attribute_hash
222
224
def self.single_report(resource_name, *type_properties)
223
225
# JJM 2007-07-24:
224
226
# Given a the name of an object and a list of properties of that
225
227
# object, return all property values in a hash.
227
229
# This class method returns nil if the object doesn't exist
228
230
# Otherwise, it returns a hash of the object properties.
230
232
all_present_str_array = list_all_present()
232
234
# NBK: shortcut the process if the resource is missing
233
235
return nil unless all_present_str_array.include? resource_name
235
237
dscl_vector = get_exec_preamble("-read", resource_name)
237
239
dscl_output = execute(dscl_vector)
238
240
rescue Puppet::ExecutionFailure => detail
239
raise Puppet::Error, "Could not get report. command execution failed."
241
fail("Could not get report. command execution failed.")
242
244
# Two code paths is ugly, but until we can drop 10.4 support we don't
243
245
# have a lot of choice. Ultimately this should all be done using Ruby
244
246
# to access the DirectoryService APIs directly, but that's simply not
285
287
# e.g. 'dscl / -create /Users/mccune'
286
288
return command_vector
289
291
def self.set_password(resource_name, guid, password_hash)
290
292
password_hash_file = "#{@@password_hash_dir}/#{guid}"
292
294
File.open(password_hash_file, 'w') { |f| f.write(password_hash)}
293
295
rescue Errno::EACCES => detail
294
raise Puppet::Error, "Could not write to password hash file: #{detail}"
296
fail("Could not write to password hash file: #{detail}")
297
299
# NBK: For shadow hashes, the user AuthenticationAuthority must contain a value of
298
300
# ";ShadowHash;". The LKDC in 10.5 makes this more interesting though as it
299
301
# will dynamically generate ;Kerberosv5;;username@LKDC:SHA1 attributes if
302
304
# use other custom AuthenticationAuthority attributes without stomping on them.
304
306
# There is a potential problem here in that we're only doing this when setting
305
# the password, and the attribute could get modified at other times while the
307
# the password, and the attribute could get modified at other times while the
306
308
# hash doesn't change and so this doesn't get called at all... but
307
309
# without switching all the other attributes to merge instead of create I can't
308
310
# see a simple enough solution for this that doesn't modify the user record
309
311
# every single time. This should be a rather rare edge case. (famous last words)
311
313
dscl_vector = self.get_exec_preamble("-merge", resource_name)
312
314
dscl_vector << "AuthenticationAuthority" << ";ShadowHash;"
314
316
dscl_output = execute(dscl_vector)
315
317
rescue Puppet::ExecutionFailure => detail
316
raise Puppet::Error, "Could not set AuthenticationAuthority."
318
fail("Could not set AuthenticationAuthority.")
320
322
def self.get_password(guid)
321
323
password_hash = nil
322
324
password_hash_file = "#{@@password_hash_dir}/#{guid}"
323
325
if File.exists?(password_hash_file) and File.file?(password_hash_file)
324
326
if not File.readable?(password_hash_file)
325
raise Puppet::Error("Could not read password hash file at #{password_hash_file} for #{@resource[:name]}")
327
fail("Could not read password hash file at #{password_hash_file} for #{@resource[:name]}")
327
329
f = File.new(password_hash_file)
328
330
password_hash = f.read
334
336
def ensure=(ensure_value)
336
# JJM: Modeled after nameservice/netinfo.rb, we need to
337
# loop over all valid properties for the type we're managing
338
# and call the method which sets that property value
339
# Like netinfo, dscl can't create everything at once, afaik.
338
# We need to loop over all valid properties for the type we're
339
# managing and call the method which sets that property value
340
# dscl can't create everything at once unfortunately.
340
341
if ensure_value == :present
341
342
@resource.class.validproperties.each do |name|
342
343
next if name == :ensure
367
368
guid = guid_plist["dsAttrTypeStandard:#{@@ns_to_ds_attribute_map[:guid]}"][0]
368
369
self.class.set_password(@resource.name, guid, passphrase)
369
370
rescue Puppet::ExecutionFailure => detail
370
raise Puppet::Error, "Could not set %s on %s[%s]: %s" % [param, @resource.class.name, @resource.name, detail]
371
fail("Could not set %s on %s[%s]: %s" % [param, @resource.class.name, @resource.name, detail])
374
375
# NBK: we override @parent.set as we need to execute a series of commands
375
376
# to deal with array values, rather than the single command nameservice.rb
376
377
# expects to be returned by modifycmd. Thus we don't bother defining modifycmd.
378
379
def set(param, value)
379
380
self.class.validate(param, value)
380
381
current_members = @property_value_cache_hash[:members]
399
400
execute(exec_arg_vector)
400
401
rescue Puppet::ExecutionFailure => detail
401
raise Puppet::Error, "Could not set %s on %s[%s]: %s" % [param, @resource.class.name, @resource.name, detail]
402
fail("Could not set %s on %s[%s]: %s" % [param, @resource.class.name, @resource.name, detail])
406
407
# NBK: we override @parent.create as we need to execute a series of commands
407
408
# to create objects with dscl, rather than the single command nameservice.rb
408
409
# expects to be returned by addcmd. Thus we don't bother defining addcmd.
419
420
# This should be revisited if Puppet starts managing UUIDs for other platform
421
422
guid = %x{/usr/bin/uuidgen}.chomp
423
424
exec_arg_vector = self.class.get_exec_preamble("-create", @resource[:name])
424
425
exec_arg_vector << @@ns_to_ds_attribute_map[:guid] << guid
426
427
execute(exec_arg_vector)
427
428
rescue Puppet::ExecutionFailure => detail
428
raise Puppet::Error, "Could not set GeneratedUID for %s %s: %s" %
429
[@resource.class.name, @resource.name, detail]
429
fail("Could not set GeneratedUID for %s %s: %s" %
430
[@resource.class.name, @resource.name, detail])
432
433
if value = @resource.should(:password) and value != ""
433
434
self.class.set_password(@resource[:name], guid, value)
436
437
# Now we create all the standard properties
437
438
Puppet::Type.type(@resource.class.name).validproperties.each do |property|
438
439
next if property == :ensure
448
449
execute(exec_arg_vector)
449
450
rescue Puppet::ExecutionFailure => detail
450
raise Puppet::Error, "Could not create %s %s: %s" %
451
[@resource.class.name, @resource.name, detail]
451
fail("Could not create %s %s: %s" %
452
[@resource.class.name, @resource.name, detail])
458
459
def remove_unwanted_members(current_members, new_members)
459
460
current_members.each do |member|
460
461
if not new_members.include?(member)
464
465
rescue Puppet::ExecutionFailure => detail
465
raise Puppet::Error, "Could not remove %s from group: %s, %s" % [member, @resource.name, detail]
466
fail("Could not remove %s from group: %s, %s" % [member, @resource.name, detail])
471
472
def add_members(current_members, new_members)
472
473
new_members.each do |new_member|
473
474
if current_members.nil? or not current_members.include?(new_member)
477
478
rescue Puppet::ExecutionFailure => detail
478
raise Puppet::Error, "Could not add %s to group: %s, %s" % [new_member, @resource.name, detail]
479
fail("Could not add %s to group: %s, %s" % [new_member, @resource.name, detail])
485
486
# JJM: Like addcmd, only called when deleting the object itself
486
487
# Note, this isn't used to delete properties of the object,
487
488
# at least that's how I understand it...
488
489
self.class.get_exec_preamble("-delete", @resource[:name])
491
492
def getinfo(refresh = false)
493
494
# Override the getinfo method, which is also defined in nameservice.rb
494
# This method returns and sets @infohash, which looks like:
495
# (NetInfo provider, user type...)
496
# @infohash = {:comment=>"Jeff McCune", :home=>"/Users/mccune",
497
# :shell=>"/bin/zsh", :password=>"********", :uid=>502, :gid=>502,
495
# This method returns and sets @infohash
500
496
# I'm not re-factoring the name "getinfo" because this method will be
501
497
# most likely called by nameservice.rb, which I didn't write.
502
498
if refresh or (! defined?(@property_value_cache_hash) or ! @property_value_cache_hash)
503
499
# JJM 2007-07-24: OK, there's a bit of magic that's about to
504
500
# happen... Let's see how strong my grip has become... =)
506
502
# self is a provider instance of some Puppet::Type, like
507
503
# Puppet::Type::User::ProviderDirectoryservice for the case of the
508
504
# user type and this provider.
510
506
# self.class looks like "user provider directoryservice", if that
513
509
# self.class.resource_type is a reference to the Puppet::Type class,
514
510
# probably Puppet::Type::User or Puppet::Type::Group, etc...
516
512
# self.class.resource_type.validproperties is a class method,
517
513
# returning an Array of the valid properties of that specific
520
516
# So... something like [:comment, :home, :password, :shell, :uid,
521
517
# :groups, :ensure, :gid]
523
519
# Ultimately, we add :name to the list, delete :ensure from the
524
520
# list, then report on the remaining list. Pretty whacky, ehh?
525
521
type_properties = [:name] + self.class.resource_type.validproperties