Specific providers should be created for AIX to manage users and groups. AIX bases the authentication management on a set of commands: mkuser, rmuser, chuser, lsuser, mkgroup, rmgroup, chgroup, lsgroup, etc.
This commit implements such providers. Notes:: - AIX users can have expiry date defined with minute granularity, but puppet does not allow it. There is a ticket open for that (#5431) - AIX maximum password age is in WEEKs, not days. - I force the compat IA module. TODO:: - Add new AIX specific attributes, specilly registry and SYSTEM. Signed-off-by: Hector Rivas Gandara <[email protected]> --- Local-branch: feature/master/5432 lib/puppet/provider/aixobject.rb | 282 ++++++++++++++++++++++++++++++++++++ lib/puppet/provider/group/aix.rb | 76 ++++++++++ lib/puppet/provider/user/aix.rb | 294 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 652 insertions(+), 0 deletions(-) create mode 100755 lib/puppet/provider/aixobject.rb create mode 100755 lib/puppet/provider/group/aix.rb create mode 100755 lib/puppet/provider/user/aix.rb diff --git a/lib/puppet/provider/aixobject.rb b/lib/puppet/provider/aixobject.rb new file mode 100755 index 0000000..98ac13b --- /dev/null +++ b/lib/puppet/provider/aixobject.rb @@ -0,0 +1,282 @@ +# +# Common code for AIX providers +# +# Author:: Hector Rivas Gandara <[email protected]> +# +# +class Puppet::Provider::AixObject < Puppet::Provider + desc "User management for AIX! Users are managed with mkuser, rmuser, chuser, lsuser" + + # Constants + # Loadable AIX I/A module for users and groups. By default we manage compat. + # TODO:: add a type parameter to change this + class << self + attr_accessor :ia_module + end + + + # AIX attributes to properties mapping. Subclasses should rewrite them + # It is a list with of hash + # :aix_attr AIX command attribute name + # :puppet_prop Puppet propertie name + # :to Method to adapt puppet property to aix command value. Optional. + # :from Method to adapt aix command value to puppet property. Optional + class << self + attr_accessor :attribute_mapping + end + + # Provider must implement these functions. + def lscmd(val...@resource[:name]) + raise Puppet::Error, "Method not defined #[email protected]} #[email protected]}: #{detail}" + end + + def addcmd(extra_attrs = []) + raise Puppet::Error, "Method not defined #[email protected]} #[email protected]}: #{detail}" + end + + def modifycmd(attributes_hash) + raise Puppet::Error, "Method not defined #[email protected]} #[email protected]}: #{detail}" + end + + def deletecmd + raise Puppet::Error, "Method not defined #[email protected]} #[email protected]}: #{detail}" + end + + # attribute_mapping class variable, + class << self + attr_accessor :attribute_mapping + end + def self.attribute_mapping_to + if ! @attribute_mapping_to + @attribute_mapping_to = {} + attribute_mapping.each { |elem| + attribute_mapping_to[elem[:puppet_prop]] = { + :key => elem[:aix_attr], + :method => elem[:to] + } + } + end + @attribute_mapping_to + end + def self.attribute_mapping_from + if ! @attribute_mapping_from + @attribute_mapping_from = {} + attribute_mapping.each { |elem| + attribute_mapping_from[elem[:aix_attr]] = { + :key => elem[:puppet_prop], + :method => elem[:from] + } + } + end + @attribute_mapping_from + end + + # This functions translates a key and value using the given mapping. + # Mapping can be nil (no translation) or a hash with this format + # {:key => new_key, :method => translate_method} + # It returns a list [key, value] + def self.translate_attr(key, value, mapping) + return [key, value] unless mapping + return nil unless mapping[key] + + if mapping[key][:method] + new_value = method(mapping[key][:method]).call(value) + else + new_value = value + end + [mapping[key][:key], new_value] + end + + #----- + # Convert a pair key-value using the + + # Parse AIX command attributes (string) and return provider hash + # If a mapping is provided, the keys are translated as defined in the + # mapping hash. Only values included in mapping will be added + # NOTE: it will ignore the items not including '=' + def self.attr2hash(str, mapping=attribute_mapping_from) + properties = {} + attrs = [] + if !str or (attrs = str.split()[0..-1]).empty? + return nil + end + + attrs.each { |i| + if i.include? "=" # Ignore if it does not include '=' + (key_str, val) = i.split('=') + # Check the key + if !key_str or key_str.empty? + info "Empty key in string 'i'?" + continue + end + key = key_str.to_sym + + if ret = self.translate_attr(key, val, mapping) + new_key = ret[0] + new_val = ret[1] + + properties[new_key] = new_val + end + end + } + properties.empty? ? nil : properties + end + + # Convert the provider properties to AIX command attributes (string) + def self.hash2attr(hash, mapping=attribute_mapping_to) + return "" unless hash + attr_list = [] + hash.each {|key, val| + + if ret = self.translate_attr(key, val, mapping) + new_key = ret[0] + new_val = ret[1] + + # Arrays are separated by commas + if new_val.is_a? Array + value = new_val.join(",") + else + value = new_val.to_s + end + + attr_list << (new_key.to_s + "=" + value ) + end + } + attr_list + end + + # Retrieve what we can about our object + def getinfo(refresh = false) + if @objectinfo.nil? or refresh == true + # Execute lsuser, split all attributes and add them to a dict. + begin + attrs = execute(self.lscmd).split("\n")[0] + @objectinfo = self.class.attr2hash(attrs) + rescue Puppet::ExecutionFailure => detail + # Print error if needed + Puppet.debug "aix.getinfo(): Could not find #[email protected]} #[email protected]}: #{detail}" \ + unless detail.to_s.include? "User \"#[email protected]}\" does not exist." + end + end + @objectinfo + end + + #------------- + # Provider API + # ------------ + + # Clear out the cached values. + def flush + @property_hash.clear if @property_hash + @object_info.clear if @object_info + end + + # Check that the user exists + def exists? + !!getinfo(true) # !! => converts to bool + end + + #- **ensure** + # The basic state that the object should be in. Valid values are + # `present`, `absent`, `role`. + # From ensurable: exists?, create, delete + def ensure + if exists? + :present + else + :absent + end + end + + # Return all existing instances + # The method for returning a list of provider instances. Note that it returns + # providers, preferably with values already filled in, not resources. + def self.instances + objects = [] + execute(lscmd("ALL")).each { |entry| + objects << new(:name => entry.split(" ")[0], :ensure => :present) + } + objects + end + + def create + if exists? + info "already exists" + # The object already exists + return nil + end + + begin + execute(self.addcmd) + rescue Puppet::ExecutionFailure => detail + raise Puppet::Error, "Could not create #[email protected]} #[email protected]}: #{detail}" + end + end + + def delete + unless exists? + info "already absent" + # the object already doesn't exist + return nil + end + + begin + execute(self.deletecmd) + rescue Puppet::ExecutionFailure => detail + raise Puppet::Error, "Could not delete #[email protected]} #[email protected]}: #{detail}" + end + end + + #-------------------------------- + # Call this method when the object is initialized, + # create getter/setter methods for each property our resource type supports. + # If setter or getter already defined it will not be overwritten + def self.mk_resource_methods + [resource_type.validproperties, resource_type.parameters].flatten.each do |prop| + next if prop == :ensure + define_method(prop) { get(prop) || :absent} unless public_method_defined?(prop) + define_method(prop.to_s + "=") { |*vals| set(prop, *vals) } unless public_method_defined?(prop.to_s + "=") + end + end + # + + # Define the needed getters and setters as soon as we know the resource type + def self.resource_type=(resource_type) + super + mk_resource_methods + end + + # Retrieve a specific value by name. + def get(param) + (hash = getinfo(false)) ? hash[param] : nil + end + + # Set a property. + def set(param, value) + @property_hash[symbolize(param)] = value + # If value does not change, do not update. + if value == getinfo()[param.to_sym] + return + end + + #self.class.validate(param, value) + cmd = modifycmd({param => value}) + begin + execute(cmd) + rescue Puppet::ExecutionFailure => detail + raise Puppet::Error, "Could not set #{param} on #[email protected]}[#{@resource.name}]: #{detail}" + end + + # Refresh de info. + hash = getinfo(true) + end + + def initialize(resource) + super + @objectinfo = nil + # FIXME: Initiallize this properly. + self.class.ia_module="compat" + end + +end +#end diff --git a/lib/puppet/provider/group/aix.rb b/lib/puppet/provider/group/aix.rb new file mode 100755 index 0000000..bcb81e1 --- /dev/null +++ b/lib/puppet/provider/group/aix.rb @@ -0,0 +1,76 @@ +# +# Group Puppet provider for AIX. It uses standard commands to manage groups: +# mkgroup, rmgroup, lsgroup, chgroup +# +# Author:: Hector Rivas Gandara <[email protected]> +# +require 'puppet/provider/aixobject' + +Puppet::Type.type(:group).provide :aix, :parent => Puppet::Provider::AixObject do + desc "Group management for AIX! Users are managed with mkgroup, rmgroup, lsgroup, chgroup" + + # Constants + # Default extra attributes to add when element is created + # registry=compat: Needed if you are using LDAP by default. + @DEFAULT_EXTRA_ATTRS = [ "registry=compat", ] + + + # This will the the default provider for this platform + defaultfor :operatingsystem => :aix + confine :operatingsystem => :aix + + # Provider features + has_features :manages_members + + # Commands that manage the element + commands :list => "/usr/sbin/lsgroup" + commands :add => "/usr/bin/mkgroup" + commands :delete => "/usr/sbin/rmgroup" + commands :modify => "/usr/bin/chgroup" + + # AIX attributes to properties mapping. + # + # Valid attributes to be managed by this provider. + # It is a list with of hash + # :aix_attr AIX command attribute name + # :puppet_prop Puppet propertie name + # :to Method to adapt puppet property to aix command value. Optional. + # :from Method to adapt aix command value to puppet property. Optional + self.attribute_mapping = [ + #:name => :name, + {:aix_attr => :id, :puppet_prop => :gid }, + {:aix_attr => :users, :puppet_prop => :members, + :from => :users_from_attr}, + ] + + #-------------- + # Command lines + def lscmd(val...@resource[:name]) + [self.class.command(:list), "-R", self.class.ia_module , value] + end + + def addcmd(extra_attrs = []) + # Here we use the @resource.to_hash to get the list of provided parameters + # Puppet does not call to self.<parameter>= method if it does not exists. + # + # It gets an extra list of arguments to add to the user. + [self.class.command(:add), "-R", self.class.ia_module ]+ + self.class.hash2attr(@resource.to_hash) + + extra_attrs + [...@resource[:name]] + end + + def modifycmd(hash = property_hash) + [self.class.command(:modify), "-R", self.class.ia_module ]+ + self.class.hash2attr(hash) + [...@resource[:name]] + end + + def deletecmd + [self.class.command(:delete),"-R", self.class.ia_module, @resource[:name]] + end + + # Force convert users it a list. + def self.users_from_attr(value) + (value.is_a? String) ? value.split(',') : value + end + +end diff --git a/lib/puppet/provider/user/aix.rb b/lib/puppet/provider/user/aix.rb new file mode 100755 index 0000000..ba95dbc --- /dev/null +++ b/lib/puppet/provider/user/aix.rb @@ -0,0 +1,294 @@ +# +# User Puppet provider for AIX. It uses standar commands to manage users: +# mkuser, rmuser, lsuser, chuser +# +# Notes: +# - AIX users can have expiry date defined with minute granularity, +# but puppet does not allow it. There is a ticket open for that (#5431) +# - AIX maximum password age is in WEEKs, not days +# - I force the compat IA module. +# +# See http://projects.puppetlabs.com/projects/puppet/wiki/Development_Provider_Development +# for more information +# +# Author:: Hector Rivas Gandara <[email protected]> +# +# TODO:: +# - Add new AIX specific attributes, specilly registry and SYSTEM. +# +require 'puppet/provider/aixobject' +require 'tempfile' +require 'date' + +Puppet::Type.type(:user).provide :aix, :parent => Puppet::Provider::AixObject do + desc "User management for AIX! Users are managed with mkuser, rmuser, chuser, lsuser" + + # Constants + # Default extra attributes to add when element is created + # registry=compat SYSTEM=compat: Needed if you are using LDAP by default. + @DEFAULT_EXTRA_ATTRS = [ "registry=compat", " SYSTEM=compat" ] + + # This will the the default provider for this platform + defaultfor :operatingsystem => :aix + confine :operatingsystem => :aix + + # Commands that manage the element + commands :lsgroup => "/usr/sbin/lsgroup" + + commands :list => "/usr/sbin/lsuser" + commands :add => "/usr/bin/mkuser" + commands :delete => "/usr/sbin/rmuser" + commands :modify => "/usr/bin/chuser" + commands :chpasswd => "/bin/chpasswd" + + # Provider features + has_features :manages_homedir, :manages_passwords, :manages_expiry, :manages_password_age + + # Attribute verification (TODO) + #verify :gid, "GID must be an string or int of a valid group" do |value| + # value.is_a? String || value.is_a? Integer + #end + # + #verify :groups, "Groups must be comma-separated" do |value| + # value !~ /\s/ + #end + + # AIX attributes to properties mapping. + # + # Valid attributes to be managed by this provider. + # It is a list with of hash + # :aix_attr AIX command attribute name + # :puppet_prop Puppet propertie name + # :to Method to adapt puppet property to aix command value. Optional. + # :from Method to adapt aix command value to puppet property. Optional + self.attribute_mapping = [ + #:name => :name, + {:aix_attr => :pgrp, :puppet_prop => :gid, + :to => :gid_to_attr, :from => :gid_from_attr}, + {:aix_attr => :id, :puppet_prop => :uid}, + {:aix_attr => :groups, :puppet_prop => :groups}, + {:aix_attr => :home, :puppet_prop => :home}, + {:aix_attr => :shell, :puppet_prop => :shell}, + {:aix_attr => :expires, :puppet_prop => :expiry, + :to => :expiry_to_attr, :from => :expiry_from_attr}, + {:aix_attr => :maxage, :puppet_prop => :password_max_age}, + {:aix_attr => :minage, :puppet_prop => :password_min_age}, + ] + + #-------------- + # Command lines + + def self.lsgroupscmd(val...@resource[:name]) + [command(:lsgroup),"-R", ia_module, "-a", "id", value] + end + + def lscmd(val...@resource[:name]) + [self.class.command(:list), "-R", self.class.ia_module , value] + end + + def addcmd(extra_attrs = []) + # Here we use the @resource.to_hash to get the list of provided parameters + # Puppet does not call to self.<parameter>= method if it does not exists. + # + # It gets an extra list of arguments to add to the user. + [self.class.command(:add), "-R", self.class.ia_module ]+ + self.class.hash2attr(@resource.to_hash) + + extra_attrs + [...@resource[:name]] + end + + def modifycmd(hash = property_hash) + [self.class.command(:modify), "-R", self.class.ia_module ]+ + self.class.hash2attr(hash) + [...@resource[:name]] + end + + def deletecmd + [self.class.command(:delete),"-R", self.class.ia_module, @resource[:name]] + end + + #-------------- + # We overwrite the create function to change the password after creation. + def create + super + # Reset the password if needed + self.password = @resource[:password] if @resource[:password] + end + + + # Get the groupname from its id + def self.groupname_by_id(gid) + groupname=nil + execute(lsgroupscmd("ALL")).each { |entry| + attrs = attr2hash(entry, nil) + if attrs and attrs.include? :id and gid == attrs[:id].to_i + groupname = entry.split(" ")[0] + end + } + groupname + end + + # Get the groupname from its id + def self.groupid_by_name(groupname) + attrs = attr2hash(execute(lsgroupscmd(groupname)).split("\n")[0], nil) + attrs ? attrs[:id].to_i : nil + end + + # Check that a group exists and is valid + def self.verify_group(value) + if value.is_a? Integer or value.is_a? Fixnum + groupname = self.groupname_by_id(value) + raise ArgumentError, "AIX group must be a valid existing group" unless groupname + else + raise ArgumentError, "AIX group must be a valid existing group" unless groupid_by_name(value) + groupname = value + end + groupname + end + + # The user's primary group. Can be specified numerically or by name. + def self.gid_to_attr(value) + verify_group(value) + end + + def self.gid_from_attr(value) + groupid_by_name(value) + end + + # The expiry date for this user. Must be provided in + # a zero padded YYYY-MM-DD HH:MM format + def self.expiry_to_attr(value) + # For chuser the expires parameter is a 10-character string in the MMDDhhmmyy format + # that is,"%m%d%H%M%y" + newdate = '0' + if value.is_a? String and value!="0000-00-00" + d = DateTime.parse(value, "%Y-%m-%d %H:%M") + newdate = d.strftime("%m%d%H%M%y") + end + newdate + end + + def self.expiry_from_attr(value) + if value =~ /(..)(..)(..)(..)(..)/ + #d= DateTime.parse("20#{$5}-#{$1}-#{$2} #{$3}:#{$4}") + #expiry_date = d.strftime("%Y-%m-%d %H:%M") + #expiry_date = d.strftime("%Y-%m-%d") + expiry_date = "20#{$5}-#{$1}-#{$2}" + else + Puppet.warn("Could not convert AIX expires date '#{value}' on #[email protected]}[#{@resource.name}]") \ + unless value == '0' + expiry_date = :absent + end + expiry_date + end + + #-------------------------------- + # Getter and Setter + # When the provider is initialized, create getter/setter methods for each + # property our resource type supports. + # If setter or getter already defined it will not be overwritten + + #- **password** + # The user's password, in whatever encrypted format the local machine + # requires. Be sure to enclose any value that includes a dollar sign ($) + # in single quotes ('). Requires features manages_passwords. + # + # Retrieve the password parsing directly the /etc/security/passwd + def password + password = :absent + user = @resource[:name] + f = File.open("/etc/security/passwd", 'r') + # Skip to the user + f.each { |l| break if l =~ /^#{user}:\s*$/ } + if ! f.eof? + f.each { |l| + # If there is a new user stanza, stop + break if l =~ /^\S*:\s*$/ + # If the password= entry is found, return it + if l =~ /^\s*password\s*=\s*(.*)$/ + password = $1; break; + end + } + end + f.close() + return password + end + + def password=(value) + user = @resource[:name] + + # Puppet execute does not support strings as input, only files. + tmpfile = Tempfile.new('puppet_#{user}_pw') + tmpfile << "#{user}:#{value}\n" + tmpfile.close() + + # Options '-e', '-c', use encrypted password and clear flags + # Must receibe "user:enc_password" as input + # command, arguments = {:failonfail => true, :combine => true} + cmd = [self.class.command(:chpasswd),"-R", ia_module, + '-e', '-c', user] + begin + execute(cmd, {:failonfail => true, :combine => true, :stdinfile => tmpfile.path }) + rescue Puppet::ExecutionFailure => detail + raise Puppet::Error, "Could not set #{param} on #[email protected]}[#{@resource.name}]: #{detail}" + ensure + tmpfile.delete() + end + end + + #- **comment** + # A description of the user. Generally is a user's full name. + #def comment=(value) + #end + # + #def comment + #end + # UNSUPPORTED + #- **profile_membership** + # Whether specified roles should be treated as the only roles + # of which the user is a member or whether they should merely + # be treated as the minimum membership list. Valid values are + # `inclusive`, `minimum`. + # UNSUPPORTED + #- **profiles** + # The profiles the user has. Multiple profiles should be + # specified as an array. Requires features manages_solaris_rbac. + # UNSUPPORTED + #- **project** + # The name of the project associated with a user Requires features + # manages_solaris_rbac. + # UNSUPPORTED + #- **role_membership** + # Whether specified roles should be treated as the only roles + # of which the user is a member or whether they should merely + # be treated as the minimum membership list. Valid values are + # `inclusive`, `minimum`. + # UNSUPPORTED + #- **roles** + # The roles the user has. Multiple roles should be + # specified as an array. Requires features manages_solaris_rbac. + # UNSUPPORTED + #- **key_membership** + # Whether specified key value pairs should be treated as the only + # attributes + # of the user or whether they should merely + # be treated as the minimum list. Valid values are `inclusive`, + # `minimum`. + # UNSUPPORTED + #- **keys** + # Specify user attributes in an array of keyvalue pairs Requires features + # manages_solaris_rbac. + # UNSUPPORTED + #- **allowdupe** + # Whether to allow duplicate UIDs. Valid values are `true`, `false`. + # UNSUPPORTED + #- **auths** + # The auths the user has. Multiple auths should be + # specified as an array. Requires features manages_solaris_rbac. + # UNSUPPORTED + #- **auth_membership** + # Whether specified auths should be treated as the only auths + # of which the user is a member or whether they should merely + # be treated as the minimum membership list. Valid values are + # `inclusive`, `minimum`. + # UNSUPPORTED + +end -- 1.7.1 ______________________________________________________________________ This email has been scanned by the MessageLabs Email Security System. For more information please visit http://www.messagelabs.com/email ______________________________________________________________________ -- You received this message because you are subscribed to the Google Groups "Puppet Developers" group. To post to this group, send email to [email protected]. To unsubscribe from this group, send email to [email protected]. For more options, visit this group at http://groups.google.com/group/puppet-dev?hl=en.
