Skip to content

Commit 75520d2

Browse files
alexjfisherekohl
authored andcommitted
Add foreman_hostgroup type
1 parent bb6e115 commit 75520d2

File tree

11 files changed

+507
-52
lines changed

11 files changed

+507
-52
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,10 @@ seen in _Administer > Settings_. The `cli` provider uses `foreman-rake` to chang
8585
`foreman_smartproxy` can create and manage registered smart proxies in
8686
Foreman's database. The `rest_v3` provider uses the API with Ruby's HTTP library, OAuth and JSON.
8787

88+
`foreman_hostgroup` can be used to create and destroy hostgroups. Nested hostgroups are supported
89+
and hostgroups can be assigned to locations/organizations.
90+
The type currently doesn't support other properties such as `environment`, `puppet classes` etc.
91+
8892
## Foreman ENC via hiera
8993

9094
There is a function `foreman::enc` to retrieve the ENC data. This returns the
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
# frozen_string_literal: true
2+
3+
Puppet::Type.type(:foreman_hostgroup).provide(
4+
:rest_v3,
5+
parent: Puppet::Type.type(:foreman_resource).provider(:rest_v3)
6+
) do
7+
confine feature: %i[json oauth]
8+
9+
def initialize(value = {})
10+
super
11+
@property_flush = {}
12+
end
13+
14+
def exists?
15+
!id.nil?
16+
end
17+
18+
def create
19+
Puppet.debug("Creating Foreman Hostgroup #{resource[:name]} with parent #{resource[:parent_hostgroup]}")
20+
21+
if resource[:parent_hostgroup] && parent_hostgroup_id.nil?
22+
raise Puppet::Error,
23+
"Parent hostgroup #{resource[:parent_hostgroup]} for #{resource[:name]} not found"
24+
end
25+
26+
organization_ids = resource[:organizations]&.map { |org| organization_id(org) }
27+
location_ids = resource[:locations]&.map { |loc| location_id(loc) }
28+
29+
post_data = {
30+
hostgroup: {
31+
name: resource[:name],
32+
parent_id: parent_hostgroup_id,
33+
description: resource[:description],
34+
organization_ids: organization_ids,
35+
location_ids: location_ids
36+
}
37+
}.to_json
38+
path = 'api/v2/hostgroups'
39+
r = request(:post, path, {}, post_data)
40+
41+
return if success?(r)
42+
43+
raise Puppet::Error, "Error making POST request to Foreman at #{request_uri(path)}: #{error_message(r)}"
44+
end
45+
46+
def destroy
47+
Puppet.debug("Destroying Foreman Hostgroup #{resource[:name]} with parent #{resource[:parent_hostgroup]}")
48+
path = "api/v2/hostgroups/#{id}"
49+
r = request(:delete, path)
50+
51+
unless success?(r)
52+
error_string = "Error making DELETE request to Foreman at #{request_uri(path)}: #{error_message(r)}"
53+
raise Puppet::Error, error_string
54+
end
55+
56+
@hostgroup = nil
57+
end
58+
59+
def flush
60+
return if @property_flush.empty?
61+
62+
Puppet.debug "Calling API to update properties for #{resource[:name]}"
63+
64+
path = "api/v2/hostgroups/#{id}"
65+
r = request(:put, path, {}, { hostgroup: @property_flush }.to_json)
66+
67+
return if success?(r)
68+
69+
raise Puppet::Error, "Error making PUT request to Foreman at #{request_uri(path)}: #{error_message(r)}"
70+
end
71+
72+
# Property getters
73+
def description
74+
hostgroup ? hostgroup['description'] : nil
75+
end
76+
77+
def organizations
78+
hostgroup ? hostgroup['organizations'].map { |org| org['title'] } : nil
79+
end
80+
81+
def locations
82+
hostgroup ? hostgroup['locations'].map { |org| org['title'] } : nil
83+
end
84+
85+
# Property setters
86+
# 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()`
87+
def description=(value)
88+
@property_flush[:description] = value
89+
end
90+
91+
def organizations=(value)
92+
@property_flush[:organization_ids] = value.map { |org| organization_id(org) }
93+
end
94+
95+
def locations=(value)
96+
@property_flush[:location_ids] = value.map { |loc| location_id(loc) }
97+
end
98+
99+
private
100+
101+
def hostgroup
102+
@hostgroup ||= begin
103+
path = 'api/v2/hostgroups'
104+
search_name = resource[:name]
105+
search_title = if resource[:parent_hostgroup]
106+
"#{resource[:parent_hostgroup]}/#{resource[:name]}"
107+
else
108+
search_name
109+
end
110+
Puppet.debug("Searching for hostgroup with name #{search_name} and title #{search_title}")
111+
r = request(:get, path, search: %(title="#{search_title}" and name="#{search_name}"))
112+
113+
raise Puppet::Error, "Error making GET request to Foreman at #{request_uri(path)}: #{error_message(r)}" unless success?(r)
114+
115+
results = JSON.parse(r.body)['results']
116+
unless results.empty?
117+
raise Puppet::Error, "Too many hostgroups found when looking for hostgroup with name #{search_name} and title #{search_title}" if results.size > 1
118+
119+
get_hostgroup_by_id(results[0]['id'])
120+
end
121+
end
122+
end
123+
124+
def get_hostgroup_by_id(id)
125+
path = "api/v2/hostgroups/#{id}"
126+
r = request(:get, path)
127+
128+
raise Puppet::Error, "Error making GET request to Foreman at #{request_uri(path)}: #{error_message(r)}" unless success?(r)
129+
130+
JSON.parse(r.body)
131+
end
132+
133+
def id
134+
hostgroup ? hostgroup['id'] : nil
135+
end
136+
137+
def parent_hostgroup
138+
return nil unless resource[:parent_hostgroup]
139+
140+
@parent_hostgroup ||= begin
141+
path = 'api/v2/hostgroups'
142+
search_title = resource[:parent_hostgroup]
143+
search_name = resource[:parent_hostgroup_name] || search_title.split('/').last
144+
Puppet.debug("Searching for parent hostgroup with name #{search_name} and title #{search_title}")
145+
r = request(:get, path, search: %(title="#{search_title}" and name="#{search_name}"))
146+
147+
raise Puppet::Error, "Error making GET request to Foreman at #{request_uri(path)}: #{error_message(r)}" unless success?(r)
148+
149+
results = JSON.parse(r.body)['results']
150+
raise Puppet::Error, "Parent hostgroup #{resource[:parent_hostgroup]} for #{resource[:name]} not found" if results.empty?
151+
raise Puppet::Error, "Too many hostgroups found when looking for parent hostgroup with name #{search_name} and title #{search_title}" if results.size > 1
152+
153+
get_hostgroup_by_id(results[0]['id'])
154+
end
155+
end
156+
157+
def parent_hostgroup_id
158+
parent_hostgroup ? parent_hostgroup['id'] : nil
159+
end
160+
161+
def organization_id(organization_title)
162+
title_to_id('organizations', organization_title)
163+
end
164+
165+
def location_id(location_title)
166+
title_to_id('locations', location_title)
167+
end
168+
169+
# Returns the id of a location/organization based on its title
170+
def title_to_id(type, title)
171+
path = "api/v2/#{type}"
172+
r = request(:get, path, search: %(title="#{title}"))
173+
unless success?(r)
174+
raise Puppet::Error,
175+
"Error making GET request to Foreman at #{request_uri(path)}: #{error_message(r)}"
176+
end
177+
results = JSON.parse(r.body)['results']
178+
raise Puppet::Error, "#{title} not found in #{type}" unless results.size == 1
179+
180+
results[0]['id']
181+
end
182+
end

lib/puppet/type/foreman_host.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
Puppet::Type.newtype(:foreman_host) do
44
desc 'foreman_host creates a host in foreman.'
55

6+
instance_eval(&PuppetX::Foreman::Common::REST_API_COMMON_PARAMS)
67
instance_eval(&PuppetX::Foreman::Common::FOREMAN_HOST_PARAMS)
78

89
newparam(:facts) do
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# frozen_string_literal: true
2+
3+
require_relative '../../puppet_x/foreman/common'
4+
Puppet::Type.newtype(:foreman_hostgroup) do
5+
desc 'foreman_hostgroup manages hostgroups in foreman.'
6+
7+
instance_eval(&PuppetX::Foreman::Common::REST_API_COMMON_PARAMS)
8+
9+
def self.title_patterns
10+
[
11+
[
12+
%r{^(.+)/(.+)$},
13+
[
14+
[:parent_hostgroup],
15+
[:name]
16+
]
17+
],
18+
[
19+
%r{(.+)},
20+
[
21+
[:name]
22+
]
23+
]
24+
]
25+
end
26+
newparam(:name, namevar: true) do
27+
desc 'The name of the hostgroup.'
28+
end
29+
30+
newparam(:parent_hostgroup, namevar: true) do
31+
desc 'The full title of the parent hostgroup'
32+
end
33+
34+
newparam(:parent_hostgroup_name) do
35+
desc 'The name of the parent hostgroup. This only needs to be given if your hostgroups contain slashes!'
36+
end
37+
38+
newproperty(:description) do
39+
desc 'The hostgroup\'s `description`'
40+
end
41+
42+
newproperty(:organizations, array_matching: :all) do
43+
desc 'An array of organizations (full titles) that this hostgroup should be part of'
44+
def insync?(is) # rubocop:disable Naming/MethodParameterName
45+
is.sort == should.sort
46+
end
47+
end
48+
49+
newproperty(:locations, array_matching: :all) do
50+
desc 'An array of locations (full titles) that this hostgroup should be part of'
51+
def insync?(is) # rubocop:disable Naming/MethodParameterName
52+
is.sort == should.sort
53+
end
54+
end
55+
56+
autorequire(:foreman_hostgroup) do
57+
self[:parent_hostgroup] if self[:ensure] == :present
58+
end
59+
end

lib/puppet/type/foreman_instance_host.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
Puppet::Type.newtype(:foreman_instance_host) do
44
desc 'foreman_instance_host marks a host as belonging to the set of hosts that make up the Foreman instance/application'
55

6+
instance_eval(&PuppetX::Foreman::Common::REST_API_COMMON_PARAMS)
67
instance_eval(&PuppetX::Foreman::Common::FOREMAN_HOST_PARAMS)
78

89
autorequire(:foreman_host) do
Lines changed: 3 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,16 @@
1+
require_relative '../../puppet_x/foreman/common'
2+
13
Puppet::Type.newtype(:foreman_smartproxy) do
24
desc 'foreman_smartproxy registers a smartproxy in foreman.'
35

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

6-
ensurable
8+
instance_eval(&PuppetX::Foreman::Common::REST_API_COMMON_PARAMS)
79

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

12-
newparam(:base_url) do
13-
desc 'Foreman\'s base url.'
14-
end
15-
16-
newparam(:effective_user) do
17-
desc 'Foreman\'s effective user for the registration (usually admin).'
18-
end
19-
20-
newparam(:consumer_key) do
21-
desc 'Foreman oauth consumer_key'
22-
end
23-
24-
newparam(:consumer_secret) do
25-
desc 'Foreman oauth consumer_secret'
26-
end
27-
2814
newproperty(:features, required_features: :feature_validation, array_matching: :all) do
2915
desc 'Features expected to be enabled on the smart proxy. Setting this
3016
validates that all of the listed features are functional, according to
@@ -42,42 +28,17 @@ def is_to_s(value)
4228
alias should_to_s is_to_s
4329
end
4430

45-
newparam(:ssl_ca) do
46-
desc 'Foreman SSL CA (certificate authority) for verification'
47-
end
48-
4931
newproperty(:url) do
5032
desc 'The url of the smartproxy'
5133
isrequired
5234
newvalues(URI.regexp)
5335
end
5436

55-
newparam(:timeout) do
56-
desc "Timeout for HTTP(s) requests"
57-
58-
munge do |value|
59-
value = value.shift if value.is_a?(Array)
60-
begin
61-
value = Integer(value)
62-
rescue ArgumentError
63-
raise ArgumentError, "The timeout must be a number.", $!.backtrace
64-
end
65-
[value, 0].max
66-
end
67-
68-
defaultto 500
69-
end
70-
71-
autorequire(:anchor) do
72-
['foreman::providers::oauth']
73-
end
74-
7537
def refresh
7638
if @parameters[:ensure].retrieve == :present
7739
provider.refresh_features! if provider.respond_to?(:refresh_features!)
7840
else
7941
debug 'Skipping refresh; smart proxy is not registered'
8042
end
8143
end
82-
8344
end

lib/puppet/type/foreman_smartproxy_host.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
Puppet::Type.newtype(:foreman_smartproxy_host) do
44
desc 'foreman_smartproxy_host marks a host as a smart proxy.'
55

6+
instance_eval(&PuppetX::Foreman::Common::REST_API_COMMON_PARAMS)
67
instance_eval(&PuppetX::Foreman::Common::FOREMAN_HOST_PARAMS)
78

89
autorequire(:foreman_host) do

lib/puppet_x/foreman/common.rb

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,10 @@
11
module PuppetX
22
module Foreman
33
module Common
4-
FOREMAN_HOST_PARAMS = Proc.new do
5-
ensurable
6-
7-
newparam(:name, :namevar => true) do
8-
desc 'The name of the resource.'
9-
end
104

11-
newparam(:hostname) do
12-
desc 'The name of the host.'
13-
end
5+
# Parameters common to several types that use the rest_v3 api provider
6+
REST_API_COMMON_PARAMS = Proc.new do
7+
ensurable
148

159
newparam(:base_url) do
1610
desc 'Foreman\'s base url.'
@@ -49,7 +43,17 @@ module Common
4943
end
5044

5145
autorequire(:anchor) do
52-
['foreman::service']
46+
['foreman::service','foreman::providers::oauth']
47+
end
48+
end
49+
50+
FOREMAN_HOST_PARAMS = Proc.new do
51+
newparam(:name, :namevar => true) do
52+
desc 'The name of the resource.'
53+
end
54+
55+
newparam(:hostname) do
56+
desc 'The name of the host.'
5357
end
5458
end
5559
end

0 commit comments

Comments
 (0)