Skip to content

Commit

Permalink
Add smart proxies
Browse files Browse the repository at this point in the history
  • Loading branch information
ezr-ondrej committed Jul 28, 2021
1 parent 10fa8a8 commit 789713a
Show file tree
Hide file tree
Showing 10 changed files with 454 additions and 5 deletions.
25 changes: 25 additions & 0 deletions app/models/concerns/foreman_puppet/extensions/host.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ module Host
included do
prepend PrependedMethods

if SETTINGS[:unattended]
include ForemanPuppet::Orchestration::Puppetca
end

if ForemanPuppet.extracted_from_core?
has_one :environment, through: :puppet, class_name: 'ForemanPuppet::Environment'
else
Expand All @@ -16,6 +20,10 @@ module Host
host_classes_assoc&.instance_variable_set(:@class_name, 'ForemanPuppet::HostClass')
end

smart_proxy_reference :self => [:puppet_proxy_id, :puppet_ca_proxy_id]

before_save :check_puppet_ca_proxy_is_required?

include_in_clone puppet: %i[config_groups host_config_groups host_classes]

scoped_search relation: :environment, on: :name, complete_value: true, rename: :environment
Expand All @@ -25,6 +33,23 @@ module Host
ext_method: :search_by_puppetclass
scoped_search relation: :config_groups, on: :name, complete_value: true, rename: :config_group, only_explicit: true, operators: ['= ', '~ '],
ext_method: :search_by_config_group


apipie :class do
property :puppetca_token, 'Token::Puppetca', desc: 'Returns Puppet CA token for this host'
end

private

# fall back to our puppet proxy in case our puppet ca is not defined/used.
def check_puppet_ca_proxy_is_required?
return true if puppet_ca_proxy_id.present? || puppet_proxy_id.blank?
if puppet_proxy.has_feature?('Puppet CA')
self.puppet_ca_proxy ||= puppet_proxy
end
rescue
true # we don't want to break anything, so just skipping.
end
end

class_methods do
Expand Down
111 changes: 111 additions & 0 deletions app/models/concerns/foreman_puppet/orchestration/puppetca.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
module ForemanPuppet
module Orchestration::Puppetca
extend ActiveSupport::Concern
include Orchestration::Common

included do
attr_reader :puppetca
after_validation :initialize_puppetca, :unless => :skip_orchestration?
after_validation :queue_puppetca
before_destroy :initialize_puppetca, :queue_puppetca_destroy
end

protected

def initialize_puppetca
return unless puppetca?
return unless Setting[:manage_puppetca]
@puppetca = ProxyAPI::Puppetca.new :url => puppet_ca_proxy.url
true
rescue => e
failure _("Failed to initialize the PuppetCA proxy: %s") % e, e
end

# Removes the host's puppet certificate from the puppetmaster's CA
def delCertificate
logger.info "Remove puppet certificate for #{name}"
puppetca.del_certificate certname
end

# Empty method for rollbacks - maybe in the future we would support creating the certificates directly
def setCertificate
end

# Reset certname based on whether to use uuids or the hostname
def resetCertname
logger.info "Resetting certname for #{name}"
self.certname = Setting[:use_uuid_for_certificates] ? Foreman.uuid : hostname
end

# Adds the host's name to the autosign.conf file
def setAutosign
logger.info "Adding autosign entry for #{name}"
response = puppetca.set_autosign certname
# return if puppetca is using basic autosigning
return response if response.in? [true, false]
unless response.is_a?(Hash) && response['generated_token'].present?
logger.warn "Received an unexpected smart proxy response: #{response}"
return false
end
puppet.create_puppetca_token value: response['generated_token']
end

# Removes the host's name from the autosign.conf file
def delAutosign
logger.info "Delete the autosign entry for #{name}"
puppetca_token.destroy! if puppetca_token.present?
puppetca.del_autosign certname
end

private

def queue_puppetca
return log_orchestration_errors unless puppetca? && errors.empty?
return unless Setting[:manage_puppetca]
new_record? ? queue_puppetca_create : queue_puppetca_update
end

def queue_puppetca_certname_reset
post_queue.create(:name => _("Reset PuppetCA certname for %s") % self, :priority => 49,
:action => [self, :resetCertname])
end

def queue_puppetca_create
post_queue.create(:name => _("Cleanup PuppetCA certificates for %s") % self, :priority => 51,
:action => [self, :delCertificate])
post_queue.create(:name => _("Enable PuppetCA autosigning for %s") % self, :priority => 55,
:action => [self, :setAutosign])
end

def queue_puppetca_update
if old.build? && !build?
# Host has been built --> remove auto sign
queue_puppetca_autosign_destroy
elsif !old.build? && build?
# Host was set to build mode
# If use_uuid_for_certificates is true, reuse the certname UUID value.
# If false, then reset the certname if it does not match the hostname.
if (Setting[:use_uuid_for_certificates] ? !Foreman.is_uuid?(certname) : certname != hostname)
queue_puppetca_certname_reset
end
queue_puppetca_autosign_destroy
queue_puppetca_create
end
true
end

def queue_puppetca_destroy
return unless puppetca? && errors.empty?
return unless Setting[:manage_puppetca]
post_queue.create(:name => _("Delete PuppetCA certificates for %s") % self, :priority => 59,
:action => [self, :delCertificate])
queue_puppetca_autosign_destroy
true
end

def queue_puppetca_autosign_destroy
post_queue.create(:name => _("Disable PuppetCA autosigning for %s") % self, :priority => 50,
:action => [self, :delAutosign])
end
end
end
9 changes: 6 additions & 3 deletions app/models/foreman_puppet/host_puppet_facet.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ class HostPuppetFacet < ApplicationRecord
include Facets::Base
include ForemanPuppet::PuppetFacetCommon

has_one :puppetca_token, :foreign_key => :host_id, :dependent => :destroy, :inverse_of => :host, :class_name => 'ForemanPuppet::Token::Puppetca'
has_many :host_classes, dependent: :destroy, class_name: 'ForemanPuppet::HostClass'
has_many :puppetclasses, through: :host_classes

validates :environment_id, presence: true, unless: ->(facet) { facet.host.puppet_proxy_id.blank? }
validates :environment_id, presence: true, unless: ->(facet) { facet.puppet_proxy_id.blank? }

after_validation :ensure_puppet_associations
before_save :clear_puppetinfo, if: :environment_id_changed?
Expand All @@ -27,11 +28,13 @@ def self.populate_fields_from_facts(host, parser, type, source_proxy)

# if proxy authentication is enabled and we have no puppet proxy set and the upload came from puppet,
# use it as puppet proxy.
host.puppet_proxy ||= source_proxy
facet.puppet_proxy ||= source_proxy
end

def self.inherited_attributes(new_hostgroup, attributes)
{ 'environment_id' => new_hostgroup.puppet&.inherited_environment_id }.merge(attributes)
{ 'environment_id' => new_hostgroup.puppet&.inherited_environment_id,
'puppet_proxy_id' => new_hostgroup.puppet&.inherited_puppet_proxy_id,
'puppet_ca_proxy_id' => new_hostgroup.puppet&.inherited_puppet_ca_proxy_id }.merge(attributes)
end

def clear_puppetinfo
Expand Down
61 changes: 61 additions & 0 deletions app/models/foreman_puppet/puppet_facet_common.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,30 @@ module ForemanPuppet
module PuppetFacetCommon
extend ActiveSupport::Concern

include ::BelongsToProxies

included do
belongs_to :environment
has_many :host_config_groups, as: :host, dependent: :destroy
has_many :config_groups, through: :host_config_groups
has_many :config_group_classes, through: :config_groups
has_many :group_puppetclasses, through: :config_groups, source: :puppetclasses

belongs_to_proxy :puppet_proxy,
feature: N_('Puppet'),
label: N_('Puppet Proxy'),
description: N_('Use the Puppetserver configured on this Smart Proxy'),
api_description: N_('Puppet proxy ID')

belongs_to_proxy :puppet_ca_proxy,
feature: 'Puppet CA',
label: N_('Puppet CA Proxy'),
description: N_('Use the Puppetserver CA configured on this Smart Proxy'),
api_description: N_('Puppet CA proxy ID')

alias_method :all_puppetclasses, :classes

before_save :check_puppet_ca_proxy_is_required?
end

def parent_name
Expand Down Expand Up @@ -86,5 +102,50 @@ def available_puppetclasses
return ForemanPuppet::Puppetclass.all.authorized(:view_puppetclasses) if environment.blank?
environment.puppetclasses - parent_classes
end

def puppetca?
return false if respond_to?(:managed?) && !managed?
puppetca_exists?
end

def puppetca_exists?
!!(puppet_ca_proxy && puppet_ca_proxy.url.present?)
end

def puppet_server_uri
return unless puppet_proxy
url = puppet_proxy.setting('Puppet', 'puppet_url')
url ||= "https://#{puppet_proxy}:8140"
URI(url)
end

# The Puppet server FQDN or an empty string. Exposed as a provisioning macro
def puppet_server
puppet_server_uri.try(:host) || ''
end
alias_method :puppetmaster, :puppet_server

def puppet_ca_server_uri
return unless puppet_ca_proxy
url = puppet_ca_proxy.setting('Puppet CA', 'puppet_url')
url ||= "https://#{puppet_ca_proxy}:8140"
URI(url)
end

# The Puppet CA server FQDN or an empty string. Exposed as a provisioning
# macro.
def puppet_ca_server
puppet_ca_server_uri.try(:host) || ''
end

private

# fall back to our puppet proxy in case our puppet ca is not defined/used.
def check_puppet_ca_proxy_is_required?
return true if puppet_ca_proxy_id.present? || puppet_proxy_id.blank?
self.puppet_ca_proxy ||= puppet_proxy if puppet_proxy.has_feature?('Puppet CA')
rescue StandardError
true # we don't want to break anything, so just skipping.
end
end
end
5 changes: 5 additions & 0 deletions app/models/foreman_puppet/token/puppetca.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module ForemanPuppet
class Token::Puppetca < ::Token
validates :value, uniqueness: true
end
end
10 changes: 10 additions & 0 deletions db/migrate/20210427122719_add_puppet_ca_proxy_column.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
class AddPuppetCaProxyColumn < ActiveRecord::Migration[6.0]
def change
change_table :host_puppet_facets do |t|
t.references :puppet_ca_proxy, foreign_key: { to_table: :smart_proxies }
end
change_table :hostgroup_puppet_facets do |t|
t.references :puppet_ca_proxy, foreign_key: { to_table: :smart_proxies }
end
end
end
5 changes: 3 additions & 2 deletions lib/foreman_puppet/register.rb
Original file line number Diff line number Diff line change
Expand Up @@ -171,13 +171,14 @@
api_view list: 'foreman_puppet/api/v2/host_puppet_facets/base',
single: 'foreman_puppet/api/v2/host_puppet_facets/host_single'
template_compatibility_properties :environment, :environment_id, :environment_name,
:puppetclasses, :all_puppetclasses
:puppetclasses, :all_puppetclasses, :puppet_server, :puppet_ca_server
set_dependent_action :destroy
end
configure_hostgroup(ForemanPuppet::HostgroupPuppetFacet) do
api_view list: 'foreman_puppet/api/v2/hostgroup_puppet_facets/base',
single: 'foreman_puppet/api/v2/hostgroup_puppet_facets/hostgroup_single'
template_compatibility_properties :environment, :environment_id, :environment_name
template_compatibility_properties :environment, :environment_id, :environment_name,
:puppet_server, :puppet_ca_server, :puppetca_token
set_dependent_action :destroy
end
end
Expand Down
24 changes: 24 additions & 0 deletions test/models/foreman_puppet/host_puppet_facet_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ class HostPuppetFacetTest < ActiveSupport::TestCase
let(:config_group_diff_env) { FactoryBot.create(:config_group, :with_puppetclass, class_environments: [diff_environment]) }

describe '.populate_fields_from_facts' do
let(:puppet_proxy) { FactoryBot.create(:puppet_smart_proxy) }

test 'populate environment without any puppet info' do
h = FactoryBot.create(:host)
parser = stub(environment: environment)
Expand Down Expand Up @@ -39,6 +41,28 @@ class HostPuppetFacetTest < ActiveSupport::TestCase
HostPuppetFacet.populate_fields_from_facts(h, parser, 'puppet', FactoryBot.create(:puppet_smart_proxy))
assert_not_equal environment, h.puppet.environment
end

test 'should update puppet_proxy_id to the id of the validated proxy' do
raw = read_json_fixture('facts/facts_with_caps.json')
host = Host.import_host(raw['name'], nil)
assert HostFactImporter.new(host).import_facts(raw['facts'], puppet_proxy)
assert_equal puppet_proxy.id, Host.find_by(name: 'sinn1636.lan').puppet_proxy_id
end

test 'should not update puppet_proxy_id if it was not puppet upload' do
raw = read_json_fixture('facts/facts_with_caps.json')
host = Host.import_host(raw['name'])
assert HostFactImporter.new(host).import_facts(raw['facts'].merge(_type: 'chef'), puppet_proxy)
assert_nil host.puppet_proxy_id
end

test "shouldn't update puppet_proxy_id if it has been set" do
Host.new(name: 'sinn1636.lan', puppet_proxy_id: puppet_proxy.id).save(validate: false)
raw = read_json_fixture('facts/facts_with_certname.json')
host = Host.import_host(raw['name'])
assert HostFactImporter.new(host).import_facts(raw['facts'], puppet_proxy)
assert_equal smart_proxies(:puppetmaster).id, Host.find_by(name: 'sinn1636.lan').puppet_proxy_id
end
end

describe '#classes_in_groups' do
Expand Down
Loading

0 comments on commit 789713a

Please sign in to comment.