Skip to content

Commit

Permalink
Add foreman_hostgroup type
Browse files Browse the repository at this point in the history
  • Loading branch information
alexjfisher authored and ekohl committed Jan 10, 2022
1 parent bb6e115 commit 75520d2
Show file tree
Hide file tree
Showing 11 changed files with 507 additions and 52 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ seen in _Administer > Settings_. The `cli` provider uses `foreman-rake` to chang
`foreman_smartproxy` can create and manage registered smart proxies in
Foreman's database. The `rest_v3` provider uses the API with Ruby's HTTP library, OAuth and JSON.

`foreman_hostgroup` can be used to create and destroy hostgroups. Nested hostgroups are supported
and hostgroups can be assigned to locations/organizations.
The type currently doesn't support other properties such as `environment`, `puppet classes` etc.

## Foreman ENC via hiera

There is a function `foreman::enc` to retrieve the ENC data. This returns the
Expand Down
182 changes: 182 additions & 0 deletions lib/puppet/provider/foreman_hostgroup/rest_v3.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
# frozen_string_literal: true

Puppet::Type.type(:foreman_hostgroup).provide(
:rest_v3,
parent: Puppet::Type.type(:foreman_resource).provider(:rest_v3)
) do
confine feature: %i[json oauth]

def initialize(value = {})
super
@property_flush = {}
end

def exists?
!id.nil?
end

def create
Puppet.debug("Creating Foreman Hostgroup #{resource[:name]} with parent #{resource[:parent_hostgroup]}")

if resource[:parent_hostgroup] && parent_hostgroup_id.nil?
raise Puppet::Error,
"Parent hostgroup #{resource[:parent_hostgroup]} for #{resource[:name]} not found"
end

organization_ids = resource[:organizations]&.map { |org| organization_id(org) }
location_ids = resource[:locations]&.map { |loc| location_id(loc) }

post_data = {
hostgroup: {
name: resource[:name],
parent_id: parent_hostgroup_id,
description: resource[:description],
organization_ids: organization_ids,
location_ids: location_ids
}
}.to_json
path = 'api/v2/hostgroups'
r = request(:post, path, {}, post_data)

return if success?(r)

raise Puppet::Error, "Error making POST request to Foreman at #{request_uri(path)}: #{error_message(r)}"
end

def destroy
Puppet.debug("Destroying Foreman Hostgroup #{resource[:name]} with parent #{resource[:parent_hostgroup]}")
path = "api/v2/hostgroups/#{id}"
r = request(:delete, path)

unless success?(r)
error_string = "Error making DELETE request to Foreman at #{request_uri(path)}: #{error_message(r)}"
raise Puppet::Error, error_string
end

@hostgroup = nil
end

def flush
return if @property_flush.empty?

Puppet.debug "Calling API to update properties for #{resource[:name]}"

path = "api/v2/hostgroups/#{id}"
r = request(:put, path, {}, { hostgroup: @property_flush }.to_json)

return if success?(r)

raise Puppet::Error, "Error making PUT request to Foreman at #{request_uri(path)}: #{error_message(r)}"
end

# Property getters
def description
hostgroup ? hostgroup['description'] : nil
end

def organizations
hostgroup ? hostgroup['organizations'].map { |org| org['title'] } : nil
end

def locations
hostgroup ? hostgroup['locations'].map { |org| org['title'] } : nil
end

# Property setters
# If one of more properties is being modified then group all of these updates in @property_flush so that we can update them in a single API call in `flush()`
def description=(value)
@property_flush[:description] = value
end

def organizations=(value)
@property_flush[:organization_ids] = value.map { |org| organization_id(org) }
end

def locations=(value)
@property_flush[:location_ids] = value.map { |loc| location_id(loc) }
end

private

def hostgroup
@hostgroup ||= begin
path = 'api/v2/hostgroups'
search_name = resource[:name]
search_title = if resource[:parent_hostgroup]
"#{resource[:parent_hostgroup]}/#{resource[:name]}"
else
search_name
end
Puppet.debug("Searching for hostgroup with name #{search_name} and title #{search_title}")
r = request(:get, path, search: %(title="#{search_title}" and name="#{search_name}"))

raise Puppet::Error, "Error making GET request to Foreman at #{request_uri(path)}: #{error_message(r)}" unless success?(r)

results = JSON.parse(r.body)['results']
unless results.empty?
raise Puppet::Error, "Too many hostgroups found when looking for hostgroup with name #{search_name} and title #{search_title}" if results.size > 1

get_hostgroup_by_id(results[0]['id'])
end
end
end

def get_hostgroup_by_id(id)
path = "api/v2/hostgroups/#{id}"
r = request(:get, path)

raise Puppet::Error, "Error making GET request to Foreman at #{request_uri(path)}: #{error_message(r)}" unless success?(r)

JSON.parse(r.body)
end

def id
hostgroup ? hostgroup['id'] : nil
end

def parent_hostgroup
return nil unless resource[:parent_hostgroup]

@parent_hostgroup ||= begin
path = 'api/v2/hostgroups'
search_title = resource[:parent_hostgroup]
search_name = resource[:parent_hostgroup_name] || search_title.split('/').last
Puppet.debug("Searching for parent hostgroup with name #{search_name} and title #{search_title}")
r = request(:get, path, search: %(title="#{search_title}" and name="#{search_name}"))

raise Puppet::Error, "Error making GET request to Foreman at #{request_uri(path)}: #{error_message(r)}" unless success?(r)

results = JSON.parse(r.body)['results']
raise Puppet::Error, "Parent hostgroup #{resource[:parent_hostgroup]} for #{resource[:name]} not found" if results.empty?
raise Puppet::Error, "Too many hostgroups found when looking for parent hostgroup with name #{search_name} and title #{search_title}" if results.size > 1

get_hostgroup_by_id(results[0]['id'])
end
end

def parent_hostgroup_id
parent_hostgroup ? parent_hostgroup['id'] : nil
end

def organization_id(organization_title)
title_to_id('organizations', organization_title)
end

def location_id(location_title)
title_to_id('locations', location_title)
end

# Returns the id of a location/organization based on its title
def title_to_id(type, title)
path = "api/v2/#{type}"
r = request(:get, path, search: %(title="#{title}"))
unless success?(r)
raise Puppet::Error,
"Error making GET request to Foreman at #{request_uri(path)}: #{error_message(r)}"
end
results = JSON.parse(r.body)['results']
raise Puppet::Error, "#{title} not found in #{type}" unless results.size == 1

results[0]['id']
end
end
1 change: 1 addition & 0 deletions lib/puppet/type/foreman_host.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
Puppet::Type.newtype(:foreman_host) do
desc 'foreman_host creates a host in foreman.'

instance_eval(&PuppetX::Foreman::Common::REST_API_COMMON_PARAMS)
instance_eval(&PuppetX::Foreman::Common::FOREMAN_HOST_PARAMS)

newparam(:facts) do
Expand Down
59 changes: 59 additions & 0 deletions lib/puppet/type/foreman_hostgroup.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# frozen_string_literal: true

require_relative '../../puppet_x/foreman/common'
Puppet::Type.newtype(:foreman_hostgroup) do
desc 'foreman_hostgroup manages hostgroups in foreman.'

instance_eval(&PuppetX::Foreman::Common::REST_API_COMMON_PARAMS)

def self.title_patterns
[
[
%r{^(.+)/(.+)$},
[
[:parent_hostgroup],
[:name]
]
],
[
%r{(.+)},
[
[:name]
]
]
]
end
newparam(:name, namevar: true) do
desc 'The name of the hostgroup.'
end

newparam(:parent_hostgroup, namevar: true) do
desc 'The full title of the parent hostgroup'
end

newparam(:parent_hostgroup_name) do
desc 'The name of the parent hostgroup. This only needs to be given if your hostgroups contain slashes!'
end

newproperty(:description) do
desc 'The hostgroup\'s `description`'
end

newproperty(:organizations, array_matching: :all) do
desc 'An array of organizations (full titles) that this hostgroup should be part of'
def insync?(is) # rubocop:disable Naming/MethodParameterName
is.sort == should.sort
end
end

newproperty(:locations, array_matching: :all) do
desc 'An array of locations (full titles) that this hostgroup should be part of'
def insync?(is) # rubocop:disable Naming/MethodParameterName
is.sort == should.sort
end
end

autorequire(:foreman_hostgroup) do
self[:parent_hostgroup] if self[:ensure] == :present
end
end
1 change: 1 addition & 0 deletions lib/puppet/type/foreman_instance_host.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
Puppet::Type.newtype(:foreman_instance_host) do
desc 'foreman_instance_host marks a host as belonging to the set of hosts that make up the Foreman instance/application'

instance_eval(&PuppetX::Foreman::Common::REST_API_COMMON_PARAMS)
instance_eval(&PuppetX::Foreman::Common::FOREMAN_HOST_PARAMS)

autorequire(:foreman_host) do
Expand Down
45 changes: 3 additions & 42 deletions lib/puppet/type/foreman_smartproxy.rb
Original file line number Diff line number Diff line change
@@ -1,30 +1,16 @@
require_relative '../../puppet_x/foreman/common'

Puppet::Type.newtype(:foreman_smartproxy) do
desc 'foreman_smartproxy registers a smartproxy in foreman.'

feature :feature_validation, "Enabled features can be validated", methods: [:features, :features=]

ensurable
instance_eval(&PuppetX::Foreman::Common::REST_API_COMMON_PARAMS)

newparam(:name, :namevar => true) do
desc 'The name of the smartproxy.'
end

newparam(:base_url) do
desc 'Foreman\'s base url.'
end

newparam(:effective_user) do
desc 'Foreman\'s effective user for the registration (usually admin).'
end

newparam(:consumer_key) do
desc 'Foreman oauth consumer_key'
end

newparam(:consumer_secret) do
desc 'Foreman oauth consumer_secret'
end

newproperty(:features, required_features: :feature_validation, array_matching: :all) do
desc 'Features expected to be enabled on the smart proxy. Setting this
validates that all of the listed features are functional, according to
Expand All @@ -42,42 +28,17 @@ def is_to_s(value)
alias should_to_s is_to_s
end

newparam(:ssl_ca) do
desc 'Foreman SSL CA (certificate authority) for verification'
end

newproperty(:url) do
desc 'The url of the smartproxy'
isrequired
newvalues(URI.regexp)
end

newparam(:timeout) do
desc "Timeout for HTTP(s) requests"

munge do |value|
value = value.shift if value.is_a?(Array)
begin
value = Integer(value)
rescue ArgumentError
raise ArgumentError, "The timeout must be a number.", $!.backtrace
end
[value, 0].max
end

defaultto 500
end

autorequire(:anchor) do
['foreman::providers::oauth']
end

def refresh
if @parameters[:ensure].retrieve == :present
provider.refresh_features! if provider.respond_to?(:refresh_features!)
else
debug 'Skipping refresh; smart proxy is not registered'
end
end

end
1 change: 1 addition & 0 deletions lib/puppet/type/foreman_smartproxy_host.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
Puppet::Type.newtype(:foreman_smartproxy_host) do
desc 'foreman_smartproxy_host marks a host as a smart proxy.'

instance_eval(&PuppetX::Foreman::Common::REST_API_COMMON_PARAMS)
instance_eval(&PuppetX::Foreman::Common::FOREMAN_HOST_PARAMS)

autorequire(:foreman_host) do
Expand Down
24 changes: 14 additions & 10 deletions lib/puppet_x/foreman/common.rb
Original file line number Diff line number Diff line change
@@ -1,16 +1,10 @@
module PuppetX
module Foreman
module Common
FOREMAN_HOST_PARAMS = Proc.new do
ensurable

newparam(:name, :namevar => true) do
desc 'The name of the resource.'
end

newparam(:hostname) do
desc 'The name of the host.'
end
# Parameters common to several types that use the rest_v3 api provider
REST_API_COMMON_PARAMS = Proc.new do
ensurable

newparam(:base_url) do
desc 'Foreman\'s base url.'
Expand Down Expand Up @@ -49,7 +43,17 @@ module Common
end

autorequire(:anchor) do
['foreman::service']
['foreman::service','foreman::providers::oauth']
end
end

FOREMAN_HOST_PARAMS = Proc.new do
newparam(:name, :namevar => true) do
desc 'The name of the resource.'
end

newparam(:hostname) do
desc 'The name of the host.'
end
end
end
Expand Down
Loading

0 comments on commit 75520d2

Please sign in to comment.