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.

Reply via email to