diff --git a/lib/puppet/provider/foreman_location/rest_v3.rb b/lib/puppet/provider/foreman_location/rest_v3.rb new file mode 100644 index 000000000..f1c8e5b9a --- /dev/null +++ b/lib/puppet/provider/foreman_location/rest_v3.rb @@ -0,0 +1,160 @@ +# frozen_string_literal: true + +Puppet::Type.type(:foreman_location).provide( # rubocop:disable Metrics/BlockLength + :rest_v3, + parent: Puppet::Type.type(:foreman_resource).provider(:rest_v3) +) do + desc 'foreman_location configures a location in foreman.' + confine feature: %i[json oauth] + + def initialize(value = {}) + super(value) + @property_flush = {} + end + + def location # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + @location ||= begin + loc_search = search('locations', 'name', resource[:name]).first + + return nil if loc_search.nil? + + path = "api/v2/locations/#{loc_search['id']}" + req = request(:get, path) + + unless success?(req) + raise Puppet::Error, "Error making GET request to Foreman at #{request_uri(path)}: #{error_message(req)}" + end + + result = JSON.parse(req.body) + debug("Using Foreman Location '#{resource[:name]}' with: #{result}") + result + end + end + + def id + location['id'] if location + end + + def exists? + !id.nil? + end + + def create # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + debug("Creating Foreman Location #{resource[:name]}") + + path = 'api/v2/locations' + payload = { + location: { + name: resource[:name], + parent_id: resource[:parent] ? location_id(resource[:parent]) : nil, + description: resource[:description], + ignore_types: resource[:select_all_types], + domain_ids: domain_ids(resource[:domains]), + organization_ids: organization_ids(resource[:organizations]) + } + } + req = request(:post, path, {}, payload.to_json) + + return if success?(req) + + raise Puppet::Error, "Error making POST request to Foreman at #{request_uri(path)}: #{error_message(req)}" + end + + def destroy + debug("Destroying Foreman Location #{resource[:name]}") + + path = "api/v2/locations/#{id}" + req = request(:delete, path) + + unless success?(req) + raise Puppet::Error, "Error making DELETE request to Foreman at #{request_uri(path)}: #{error_message(req)}" + end + + @location = nil + end + + def flush + return if @property_flush.empty? + + debug("Calling API to update properties for Foreman Location #{resource[:name]} with: #{@property_flush}") + + path = "api/v2/locations/#{id}" + req = request(:put, path, {}, { location: @property_flush }.to_json) + + return if success?(req) + + raise Puppet::Error, "Error making PUT request to Foreman at #{request_uri(path)}: #{error_message(req)}" + end + + def parent + location && location['parent_name'] ? locations['parent_name'].split('/')[-1] : nil + end + + def parent=(value) + if value.nil? + @property_flush[:parent_id] = nil + else + parent_id = location_id(value) + + raise Puppet::Error, "Could not find Foreman Location with name #{value} as parent." if parent_id.nil? + + @property_flush[:parent_id] = parent_id + end + end + + def description + location ? location['description'] : nil + end + + def description=(value) + @property_flush[:description] = value + end + + def select_all_types + location ? location['select_all_types'] : [] + end + + def select_all_types=(value) + @property_flush[:ignore_types] = value + end + + def domains + location ? location['domains'].reject { |d| d['inherited'] }.map { |d| d['name'] } : nil + end + + def domain_ids(domains) + return nil if domains.nil? + + domains.map do |domain| + res = search('domains', 'name', domain).first + + raise Puppet::Error, "Could not find Foreman Domain with name '#{domain}'" if res.nil? + + res['id'] + end + end + + def domains=(value) + @property_flush[:domain_ids] = domain_ids(value) + end + + def organizations + location ? location['organizations'].reject { |o| o['inherited'] }.map { |o| o['name'] } : nil + end + + def organization_ids(orgs) + return nil if orgs.nil? + + orgs.map do |organization| + org_id = organization_id(organization) + + raise Puppet::Error, "Could not find Foreman Organization with name '#{organization}'" if org_id.nil? + + org_id + end + end + + def organizations=(value) + @property_flush[:organization_ids] = organization_ids(value) + end +end diff --git a/lib/puppet/provider/foreman_organization/rest_v3.rb b/lib/puppet/provider/foreman_organization/rest_v3.rb new file mode 100644 index 000000000..7978a3d1f --- /dev/null +++ b/lib/puppet/provider/foreman_organization/rest_v3.rb @@ -0,0 +1,160 @@ +# frozen_string_literal: true + +Puppet::Type.type(:foreman_organization).provide( # rubocop:disable Metrics/BlockLength + :rest_v3, + parent: Puppet::Type.type(:foreman_resource).provider(:rest_v3) +) do + desc 'foreman_organization configures a organization in foreman.' + confine feature: %i[json oauth] + + def initialize(value = {}) + super(value) + @property_flush = {} + end + + def organization # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + @organization ||= begin + org_search = search('organizations', 'name', resource[:name]).first + + return nil if org_search.nil? + + path = "api/v2/organizations/#{org_search['id']}" + req = request(:get, path) + + unless success?(req) + raise Puppet::Error, "Error making GET request to Foreman at #{request_uri(path)}: #{error_message(req)}" + end + + result = JSON.parse(req.body) + debug("Using Foreman Organization '#{resource[:name]}' with: #{result}") + result + end + end + + def id + organization['id'] if organization + end + + def exists? + !id.nil? + end + + def create # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + debug("Creating Foreman Organization #{resource[:name]}") + + path = 'api/v2/organizations' + payload = { + organization: { + name: resource[:name], + parent_id: resource[:parent] ? organization_id(resource[:parent]) : nil, + description: resource[:description], + ignore_types: resource[:select_all_types], + domain_ids: domain_ids(resource[:domains]), + location_ids: location_ids(resource[:locations]) + } + } + req = request(:post, path, {}, payload.to_json) + + return if success?(req) + + raise Puppet::Error, "Error making POST request to Foreman at #{request_uri(path)}: #{error_message(req)}" + end + + def destroy + debug("Destroying Foreman Organization #{resource[:name]}") + + path = "api/v2/organizations/#{id}" + req = request(:delete, path) + + unless success?(req) + raise Puppet::Error, "Error making DELETE request to Foreman at #{request_uri(path)}: #{error_message(req)}" + end + + @organization = nil + end + + def flush + return if @property_flush.empty? + + debug("Calling API to update properties for Foreman Organization #{resource[:name]} with: #{@property_flush}") + + path = "api/v2/organizations/#{id}" + req = request(:put, path, {}, { organization: @property_flush }.to_json) + + return if success?(req) + + raise Puppet::Error, "Error making PUT request to Foreman at #{request_uri(path)}: #{error_message(req)}" + end + + def parent + organization && organization['parent_name'] ? organization['parent_name'].split('/')[-1] : nil + end + + def parent=(value) + if value.nil? + @property_flush[:parent_id] = nil + else + parent_id = organization_id(value) + + raise Puppet::Error, "Could not find Foreman Organzation with name #{value} as parent." if parent_id.nil? + + @property_flush[:parent_id] = parent_id + end + end + + def description + organization ? organization['description'] : nil + end + + def description=(value) + @property_flush[:description] = value + end + + def select_all_types + organization ? organization['select_all_types'] : [] + end + + def select_all_types=(value) + @property_flush[:ignore_types] = value + end + + def domains + organization ? organization['domains'].reject { |d| d['inherited'] }.map { |d| d['name'] } : nil + end + + def domain_ids(domains) + return nil if domains.nil? + + domains.map do |domain| + res = search('domains', 'name', domain).first + + raise Puppet::Error, "Could not find Foreman Domain with name '#{domain}'" if res.nil? + + res['id'] + end + end + + def domains=(value) + @property_flush[:domain_ids] = domain_ids(value) + end + + def locations + organization ? organization['locations'].reject { |l| l['inherited'] }.map { |l| l['name'] } : nil + end + + def location_ids(locations) + return nil if locations.nil? + + locations.map do |location| + location_id = location_id(location) + + raise Puppet::Error, "Could not find Foreman Location with name '#{location}'" if location_id.nil? + + location_id + end + end + + def locations=(value) + @property_flush[:location_ids] = location_ids(value) + end +end diff --git a/lib/puppet/provider/foreman_resource/rest_v3.rb b/lib/puppet/provider/foreman_resource/rest_v3.rb index 00b653bc5..ad6a29936 100644 --- a/lib/puppet/provider/foreman_resource/rest_v3.rb +++ b/lib/puppet/provider/foreman_resource/rest_v3.rb @@ -131,4 +131,25 @@ def error_message(response) JSON.parse(response.body)['error']['full_messages'].join(' ') rescue "Response: #{response.code} #{response.message}" end end + + def search(resource, key, value) + path = "api/v2/#{resource}" + req = request(:get, path, { search: %(#{key}="#{value}"), per_page: 'all' }) + + unless success?(req) + raise Puppet::Error, "Error making GET request to Foreman at #{request_uri(path)}: #{error_message(req)}" + end + + JSON.parse(req.body)['results'] + end + + def location_id(name) + res = search('locations', 'name', name).first + res.nil? ? nil : res['id'] + end + + def organization_id(name) + res = search('organizations', 'name', name).first + res.nil? ? nil : res['id'] + end end diff --git a/lib/puppet/type/foreman_location.rb b/lib/puppet/type/foreman_location.rb new file mode 100644 index 000000000..81b339f8f --- /dev/null +++ b/lib/puppet/type/foreman_location.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require 'puppet_x/foreman/common' + +Puppet::Type.newtype(:foreman_location) do + desc 'foreman_location configures a location in foreman.' + + instance_eval(&PuppetX::Foreman::Common::REST_API_COMMON_PARAMS) + + newparam(:name) do + desc 'The name of the location' + end + + newproperty(:parent) do + desc 'The name of the parent location' + end + + newproperty(:description) do + desc 'The description of this location' + end + + newproperty(:select_all_types, array_matching: :all) do + desc 'List of resource types for which to "Select All"' + end + + newproperty(:domains, array_matching: :all) do + desc 'A list of domain names to allow for this location' + end + + newproperty(:organizations, array_matching: :all) do + desc 'A list of organization names to allow for this location' + end + + autorequire(:foreman_location) do + self[:parent] if self[:ensure] == :present + end +end diff --git a/lib/puppet/type/foreman_organization.rb b/lib/puppet/type/foreman_organization.rb new file mode 100644 index 000000000..2f7567e73 --- /dev/null +++ b/lib/puppet/type/foreman_organization.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require 'puppet_x/foreman/common' + +Puppet::Type.newtype(:foreman_organization) do + desc 'foreman_organization configures a organization in foreman.' + + instance_eval(&PuppetX::Foreman::Common::REST_API_COMMON_PARAMS) + + newparam(:name) do + desc 'The name of the organization' + end + + newproperty(:parent) do + desc 'The name of the parent organization' + end + + newproperty(:description) do + desc 'The description of this organization' + end + + newproperty(:select_all_types, array_matching: :all) do + desc 'List of resource types for which to "Select All"' + end + + newproperty(:domains, array_matching: :all) do + desc 'A list of domain names to allow for this organization' + end + + newproperty(:locations, array_matching: :all) do + desc 'A list of location names to allow for this organization' + end + + autorequire(:foreman_organization) do + self[:parent] if self[:ensure] == :present + end +end diff --git a/spec/unit/foreman_location_rest_v3_spec.rb b/spec/unit/foreman_location_rest_v3_spec.rb new file mode 100644 index 000000000..1f1d1ee30 --- /dev/null +++ b/spec/unit/foreman_location_rest_v3_spec.rb @@ -0,0 +1,198 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Puppet::Type.type(:foreman_location).provider(:rest_v3) do # rubocop:disable Metrics/BlockLength + let(:basic_params) do + { + name: 'example_location', + base_url: 'https://foreman.example.com', + consumer_key: 'oauth_key', + consumer_secret: 'oauth_secret', + effective_user: 'admin' + } + end + + let(:resource) do + Puppet::Type.type(:foreman_location).new( + basic_params + ) + end + + let(:provider) do + provider = described_class.new + provider.resource = resource + provider + end + + describe '#create' do # rubocop:disable Metrics/BlockLength + let(:expected_post_data) do + { + 'location' => { + 'name' => 'example_location', + 'parent_id' => nil, + 'description' => nil, + 'ignore_types' => nil, + 'domain_ids' => nil, + 'organization_ids' => nil + } + }.to_json + end + + it 'sends POST request' do + allow(provider).to receive(:request).with(:post, 'api/v2/locations', {}, expected_post_data).and_return( + instance_double(Net::HTTPOK, code: '201', body: {}.to_json) + ) + provider.create + end + + context 'with parent set' do + let(:resource) do + Puppet::Type.type(:foreman_location).new( + basic_params.merge(parent: 'parent_location') + ) + end + let(:expected_post_data) do + { + 'location' => { + 'name' => 'example_location', + 'parent_id' => 101, + 'description' => nil, + 'ignore_types' => nil, + 'domain_ids' => nil, + 'organization_ids' => nil + } + }.to_json + end + + it 'sends POST request' do + allow(provider).to receive(:location_id).with('parent_location').and_return(101) + allow(provider).to receive(:request).with(:post, 'api/v2/locations', {}, expected_post_data).and_return( + instance_double(Net::HTTPOK, code: '201', body: {}.to_json) + ) + provider.create + end + end + + context 'with description set' do + let(:resource) do + Puppet::Type.type(:foreman_location).new( + basic_params.merge(description: 'example description') + ) + end + let(:expected_post_data) do + { + 'location' => { + 'name' => 'example_location', + 'parent_id' => nil, + 'description' => 'example description', + 'ignore_types' => nil, + 'domain_ids' => nil, + 'organization_ids' => nil + } + }.to_json + end + + it 'sends POST request' do + allow(provider).to receive(:request).with(:post, 'api/v2/locations', {}, expected_post_data).and_return( + instance_double(Net::HTTPOK, code: '201', body: {}.to_json) + ) + provider.create + end + end + + context 'with domains set' do + let(:resource) do + Puppet::Type.type(:foreman_location).new( + basic_params.merge(domains: ['example.com', 'example.org']) + ) + end + let(:expected_post_data) do + { + 'location' => { + 'name' => 'example_location', + 'parent_id' => nil, + 'description' => nil, + 'ignore_types' => nil, + 'domain_ids' => [301, 302], + 'organization_ids' => nil + } + }.to_json + end + + it 'sends POST request' do + allow(provider).to receive(:domain_ids).with(['example.com', 'example.org']).and_return([301, 302]) + allow(provider).to receive(:request).with(:post, 'api/v2/locations', {}, expected_post_data).and_return( + instance_double(Net::HTTPOK, code: '201', body: {}.to_json) + ) + provider.create + end + end + + context 'with organizations set' do + let(:resource) do + Puppet::Type.type(:foreman_location).new( + basic_params.merge(organizations: %w[org1 org2]) + ) + end + let(:expected_post_data) do + { + 'location' => { + 'name' => 'example_location', + 'parent_id' => nil, + 'description' => nil, + 'ignore_types' => nil, + 'domain_ids' => nil, + 'organization_ids' => [201, 202] + } + }.to_json + end + + it 'sends POST request' do + allow(provider).to receive(:organization_id).with('org1').and_return(201) + allow(provider).to receive(:organization_id).with('org2').and_return(202) + allow(provider).to receive(:request).with(:post, 'api/v2/locations', {}, expected_post_data).and_return( + instance_double(Net::HTTPOK, code: '201', body: {}.to_json) + ) + provider.create + end + end + + context 'with select_all_types set' do + let(:resource) do + Puppet::Type.type(:foreman_location).new( + basic_params.merge(select_all_types: %w[SmartProxies]) + ) + end + let(:expected_post_data) do + { + 'location' => { + 'name' => 'example_location', + 'parent_id' => nil, + 'description' => nil, + 'ignore_types' => %w[SmartProxies], + 'domain_ids' => nil, + 'organization_ids' => nil + } + }.to_json + end + + it 'sends POST request' do + allow(provider).to receive(:request).with(:post, 'api/v2/locations', {}, expected_post_data).and_return( + instance_double(Net::HTTPOK, code: '201', body: {}.to_json) + ) + provider.create + end + end + end + + describe '#destroy' do + it 'sends DELETE request' do + allow(provider).to receive(:id).and_return(42) + allow(provider).to receive(:request).with(:delete, 'api/v2/locations/42').and_return( + instance_double(Net::HTTPOK, code: '201', body: {}.to_json) + ) + provider.destroy + end + end +end diff --git a/spec/unit/foreman_organization_rest_v3_spec.rb b/spec/unit/foreman_organization_rest_v3_spec.rb new file mode 100644 index 000000000..3307d99bd --- /dev/null +++ b/spec/unit/foreman_organization_rest_v3_spec.rb @@ -0,0 +1,198 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Puppet::Type.type(:foreman_organization).provider(:rest_v3) do # rubocop:disable Metrics/BlockLength + let(:basic_params) do + { + name: 'example_organization', + base_url: 'https://foreman.example.com', + consumer_key: 'oauth_key', + consumer_secret: 'oauth_secret', + effective_user: 'admin' + } + end + + let(:resource) do + Puppet::Type.type(:foreman_organization).new( + basic_params + ) + end + + let(:provider) do + provider = described_class.new + provider.resource = resource + provider + end + + describe '#create' do # rubocop:disable Metrics/BlockLength + let(:expected_post_data) do + { + 'organization' => { + 'name' => 'example_organization', + 'parent_id' => nil, + 'description' => nil, + 'ignore_types' => nil, + 'domain_ids' => nil, + 'location_ids' => nil + } + }.to_json + end + + it 'sends POST request' do + allow(provider).to receive(:request).with(:post, 'api/v2/organizations', {}, expected_post_data).and_return( + instance_double(Net::HTTPOK, code: '201', body: {}.to_json) + ) + provider.create + end + + context 'with parent set' do + let(:resource) do + Puppet::Type.type(:foreman_organization).new( + basic_params.merge(parent: 'parent_organization') + ) + end + let(:expected_post_data) do + { + 'organization' => { + 'name' => 'example_organization', + 'parent_id' => 101, + 'description' => nil, + 'ignore_types' => nil, + 'domain_ids' => nil, + 'location_ids' => nil + } + }.to_json + end + + it 'sends POST request' do + allow(provider).to receive(:organization_id).with('parent_organization').and_return(101) + allow(provider).to receive(:request).with(:post, 'api/v2/organizations', {}, expected_post_data).and_return( + instance_double(Net::HTTPOK, code: '201', body: {}.to_json) + ) + provider.create + end + end + + context 'with description set' do + let(:resource) do + Puppet::Type.type(:foreman_organization).new( + basic_params.merge(description: 'example description') + ) + end + let(:expected_post_data) do + { + 'organization' => { + 'name' => 'example_organization', + 'parent_id' => nil, + 'description' => 'example description', + 'ignore_types' => nil, + 'domain_ids' => nil, + 'location_ids' => nil + } + }.to_json + end + + it 'sends POST request' do + allow(provider).to receive(:request).with(:post, 'api/v2/organizations', {}, expected_post_data).and_return( + instance_double(Net::HTTPOK, code: '201', body: {}.to_json) + ) + provider.create + end + end + + context 'with select_all_types set' do + let(:resource) do + Puppet::Type.type(:foreman_organization).new( + basic_params.merge(select_all_types: %w[SmartProxies]) + ) + end + let(:expected_post_data) do + { + 'organization' => { + 'name' => 'example_organization', + 'parent_id' => nil, + 'description' => nil, + 'ignore_types' => %w[SmartProxies], + 'domain_ids' => nil, + 'location_ids' => nil + } + }.to_json + end + + it 'sends POST request' do + allow(provider).to receive(:request).with(:post, 'api/v2/organizations', {}, expected_post_data).and_return( + instance_double(Net::HTTPOK, code: '201', body: {}.to_json) + ) + provider.create + end + end + + context 'with domains set' do + let(:resource) do + Puppet::Type.type(:foreman_organization).new( + basic_params.merge(domains: ['example.com', 'example.org']) + ) + end + let(:expected_post_data) do + { + 'organization' => { + 'name' => 'example_organization', + 'parent_id' => nil, + 'description' => nil, + 'ignore_types' => nil, + 'domain_ids' => [301, 302], + 'location_ids' => nil + } + }.to_json + end + + it 'sends POST request' do + allow(provider).to receive(:domain_ids).with(['example.com', 'example.org']).and_return([301, 302]) + allow(provider).to receive(:request).with(:post, 'api/v2/organizations', {}, expected_post_data).and_return( + instance_double(Net::HTTPOK, code: '201', body: {}.to_json) + ) + provider.create + end + end + + context 'with locations set' do + let(:resource) do + Puppet::Type.type(:foreman_organization).new( + basic_params.merge(locations: %w[loc1 loc2]) + ) + end + let(:expected_post_data) do + { + 'organization' => { + 'name' => 'example_organization', + 'parent_id' => nil, + 'description' => nil, + 'ignore_types' => nil, + 'domain_ids' => nil, + 'location_ids' => [201, 202] + } + }.to_json + end + + it 'sends POST request' do + allow(provider).to receive(:location_id).with('loc1').and_return(201) + allow(provider).to receive(:location_id).with('loc2').and_return(202) + allow(provider).to receive(:request).with(:post, 'api/v2/organizations', {}, expected_post_data).and_return( + instance_double(Net::HTTPOK, code: '201', body: {}.to_json) + ) + provider.create + end + end + end + + describe '#destroy' do + it 'sends DELETE request' do + allow(provider).to receive(:id).and_return(42) + allow(provider).to receive(:request).with(:delete, 'api/v2/organizations/42').and_return( + instance_double(Net::HTTPOK, code: '201', body: {}.to_json) + ) + provider.destroy + end + end +end