Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Manage IPA groups and IPA group memberships #11

Open
wants to merge 21 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 50 additions & 6 deletions lib/puppet/provider/ipa.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
require File.expand_path(File.join(File.dirname(__FILE__), '..', '..',
'puppet_x', 'encore', 'ipa', 'http_client'))
require File.expand_path(File.join(File.dirname(__FILE__), '..', '..',
'puppet_x', 'encore', 'ipa', 'cache'))
require 'puppet_x/encore/ipa/cache'
require 'puppet_x/encore/ipa/http_client'
require 'cgi'

# This class is a "base" provider to use to implement your own custom providers here
Expand Down Expand Up @@ -97,8 +95,24 @@ def cached_instance
# note: we explicitly do NOT cache within this method because we want to be
# able to call it both in initialize() and in flush() and return the current
# state of the resource from the API each time
def read_instance
raise NotImplementedError, 'read_instance needs to be implemented by child providers'
def read_instance(use_cache: true)
instances_hash = use_cache ? cached_all_instances : read_all_instances
if instances_hash.key?(resource[:name])
instances_hash[resource[:name]]
else
{ ensure: :absent, name: resource[:name] }
end
end

# Read all instances of this type from the API, this will then be stored in a cache
# We do it like this so that the first resource of this type takes the burden of
# reading all of the data, but the following resources are all super fast because
# they can use the global cache
#
# This should be a hash where the key is the namevar of the resource and the value
# is a hash with all of the properties of the resource set
def read_all_instances
raise NotImplementedError, 'read_all_instances needs to be implemented by child providers'
end

# this method should check resource[:ensure]
Expand All @@ -112,6 +126,18 @@ def flush_instance
raise NotImplementedError, 'flush_instance needs to be implemented by child providers'
end

# global cached instances, so we only have to read in the groups list once
def cached_all_instances
# return cache if it has been created, this means that this function will only need
# to be loaded once, returning all instances that exist of this resource in vsphere
# then, we can lookup our version by name/id/whatever. This saves a TON of processing
cached_instances = PuppetX::Encore::Ipa::Cache.instance.cached_instances[resource.type]
return cached_instances unless cached_instances.nil?

# read all instances from the API and save them in the cache
PuppetX::Encore::Ipa::Cache.instance.cached_instances[resource.type] = read_all_instances
end

def api_client
# create an HTTP client with this username/password and cache it
# be sure to use resource[:xxx] here so that we can use the parameters
Expand Down Expand Up @@ -174,4 +200,22 @@ def api_post(endpoint, body: nil, json_parse: true)
response
end
end

def get_ldap_attribute(obj, attr)
return :absent if obj[attr].nil?
# special handling for arrays
if obj[attr].is_a?(Array)
return [] if obj[attr].empty?
# de-array-ify the thing if there is only one element
return obj[attr][0] if obj[attr].size == 1
end
# either return the full array if >1 element, or return the non-array value
obj[attr]
end

def get_ldap_attribute_boolean(obj, attr)
# values can be: "TRUE", "True", or "true"
# casecmp does case insensitive comparison and returns 0 if equal
get_ldap_attribute(obj, attr).casecmp('true').zero?
end
end
106 changes: 106 additions & 0 deletions lib/puppet/provider/ipa_dns_zone/default.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
require 'puppet/provider/ipa'

Puppet::Type.type(:ipa_dns_zone).provide(:default, parent: Puppet::Provider::Ipa) do
defaultfor kernel: 'Linux'

# always need to define this in our implementation classes
mk_resource_methods

##########################
# private methods that we need to implement because we inherit from Puppet::Provider::Synapse

# Read all instances of this type from the API, this will then be stored in a cache
# We do it like this so that the first resource of this type takes the burden of
# reading all of the data, but the following resources are all super fast because
# they can use the global cache
def read_all_instances
# read all of the groups, once
body = {
'id' => 0,
'method' => 'dnszone_find/1',
'params' => [
# args (positional arguments)
[],
# options (CLI flags / options)
{
'all' => true,
# use raw here because otherwise some of the properties return these weird nested dictionaries
# only downside is that some of the keys come back with weird cases
# we'll fix this later
'raw' => true,
},
],
}
response_body = api_post('/session/json', body: body, json_parse: true)
dnszone_list = response_body['result']['result']
Puppet.debug("Got DNS Zone list: #{dnszone_list}")

instance_hash = {}
dnszone_list.each do |dnszone|
# --raw returns stuff in weird case formats
# to prevent forward compatability problems we downcase everything before we pull it out
dnszone.transform_keys!(&:downcase)
instance = {
ensure: :present,
name: get_ldap_attribute(dnszone, 'idnsname'),
allow_dynamic_update: get_ldap_attribute_boolean(dnszone, 'idnsallowdynupdate'),
allow_sync_ptr: get_ldap_attribute_boolean(dnszone, 'idnsallowsyncptr'),
}
instance_hash[instance[:name]] = instance
end
Puppet.debug("Returning group instances: #{instance_hash}")
instance_hash
end

# this method should check resource[:ensure]
# if it is :present this method should create/update the instance using the values
# in resource[:xxx] (these are the desired state values)
# else if it is :absent this method should delete the instance
#
# if you want to have access to the values before they were changed you can use
# cached_instance[:xxx] to compare against (that's why it exists)
def flush_instance
# write a single instance at a time
# we can't bulk write because instances may be written in different orders depending
# on their relationships defined in PuppetDSL
case resource[:ensure]
when :absent
body = {
'id' => 0,
'method' => 'dnszone_del/1',
'params' => [
# args (positional arguments)
[resource[:name]],
# options (CLI flags / options)
{},
],
}
api_post('/session/json', body: body)
when :present
method = if cached_instance[:ensure] == :absent
# if the group was absent, we need to add
'dnszone_add/1'
else
# if the group was present then we need to modify
'dnszone_mod/1'
end
body = {
'id' => 0,
'method' => method,
'params' => [
# args (positional arguments)
[resource[:name]],
# options (CLI flags / options)
{},
],
}
unless resource[:allow_dynamic_update].nil?
body['params'][1]['idnsallowdynupdate'] = resource[:allow_dynamic_update] ? 'TRUE' : 'FALSE'
end
unless resource[:allow_sync_ptr].nil?
body['params'][1]['idnsallowsyncptr'] = resource[:allow_sync_ptr] ? 'TRUE' : 'FALSE'
end
api_post('/session/json', body: body)
end
end
end
122 changes: 122 additions & 0 deletions lib/puppet/provider/ipa_group/default.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
require 'puppet/provider/ipa'

Puppet::Type.type(:ipa_group).provide(:default, parent: Puppet::Provider::Ipa) do
defaultfor kernel: 'Linux'

# always need to define this in our implementation classes
mk_resource_methods

##########################
# private methods that we need to implement because we inherit from Puppet::Provider::Synapse

# Read all instances of this type from the API, this will then be stored in a cache
# We do it like this so that the first resource of this type takes the burden of
# reading all of the data, but the following resources are all super fast because
# they can use the global cache
def read_all_instances
# read all of the groups, once
body = {
'id' => 0,
'method' => 'group_find/1',
'params' => [
# args (positional arguments)
[],
# options (CLI flags / options)
{
'all' => true,
},
],
}
response_body = api_post('/session/json', body: body, json_parse: true)
group_list = response_body['result']['result']
Puppet.debug("Got group list: #{group_list}")

instance_hash = {}
group_list.each do |group|
instance = {
ensure: :present,
name: get_ldap_attribute(group, 'cn'),
description: get_ldap_attribute(group, 'description'),
gid: get_ldap_attribute(group, 'gidnumber'),
group_type: :non_posix,
}

# there nothing special on a group that determines if they are non_posix other
# than the absence of the following two objectclass attributes that denote
# either posix or external group types.
# posix groups are denoted by an objectclass="posixgroup" attribute
# external groups are denoted by an objectclass="ipaexternalgroup" attribute
objectclasses = group['objectclass']
objectclasses.each do |oc|
if oc == 'posixgroup'
instance[:group_type] = :posix
break
elsif oc == 'ipaexternalgroup'
instance[:group_type] = :external
break
end
end
instance_hash[instance[:name]] = instance
end
Puppet.debug("Returning group instances: #{instance_hash}")
instance_hash
end

# this method should check resource[:ensure]
# if it is :present this method should create/update the instance using the values
# in resource[:xxx] (these are the desired state values)
# else if it is :absent this method should delete the instance
#
# if you want to have access to the values before they were changed you can use
# cached_instance[:xxx] to compare against (that's why it exists)
def flush_instance
# write a single instance at a time
# we can't bulk write because instances may be written in different orders depending
# on their relationships defined in PuppetDSL
case resource[:ensure]
when :absent
body = {
'id' => 0,
'method' => 'group_del/1',
'params' => [
# args (positional arguments)
[resource[:name]],
# options (CLI flags / options)
{},
],
}
api_post('/session/json', body: body)
when :present
method = if cached_instance[:ensure] == :absent
# if the group was absent, we need to add
'group_add/1'
else
# if the group was present then we need to modify
'group_mod/1'
end
body = {
'id' => 0,
'method' => method,
'params' => [
# args (positional arguments)
[resource[:name]],
# options (CLI flags / options)
{},
],
}
body['params'][1]['description'] = resource[:description] if resource[:description]
body['params'][1]['gidnumber'] = resource[:gid] if resource[:gid]
# can only set group "type" when adding the group
if cached_instance[:ensure] == :absent
if resource[:group_type] == :non_posix
body['params'][1]['nonposix'] = true
elsif resource[:group_type] == :external
body['params'][1]['external'] = true
end
# default is posix type, no flags need to be set
end

api_post('/session/json', body: body)
end
end
end
Loading