diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index adadf988..f7076fec 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by # `rubocop --auto-gen-config` -# on 2021-06-09 10:26:01 UTC using RuboCop version 1.16.0. +# on 2021-07-28 22:19:11 UTC using RuboCop version 1.16.0. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new @@ -18,7 +18,7 @@ Lint/RequireParentheses: Exclude: - 'app/controllers/concerns/foreman_puppet/extensions/hosts_controller_extensions.rb' -# Offense count: 42 +# Offense count: 44 # Configuration parameters: IgnoredMethods, CountRepeatedAttributes. Metrics/AbcSize: Max: 41 @@ -34,17 +34,17 @@ Metrics/BlockLength: Metrics/ClassLength: Max: 250 -# Offense count: 16 +# Offense count: 18 # Configuration parameters: IgnoredMethods. Metrics/CyclomaticComplexity: Max: 17 -# Offense count: 38 +# Offense count: 39 # Configuration parameters: CountComments, CountAsOne, ExcludedMethods, IgnoredMethods. Metrics/MethodLength: Max: 23 -# Offense count: 3 +# Offense count: 4 # Configuration parameters: CountComments, CountAsOne. Metrics/ModuleLength: Max: 171 @@ -54,16 +54,17 @@ Metrics/ModuleLength: Metrics/ParameterLists: MaxOptionalParameters: 4 -# Offense count: 15 +# Offense count: 17 # Configuration parameters: IgnoredMethods. Metrics/PerceivedComplexity: Max: 18 -# Offense count: 1 +# Offense count: 6 # Configuration parameters: EnforcedStyle, IgnoredPatterns. # SupportedStyles: snake_case, camelCase Naming/MethodName: Exclude: + - 'app/models/concerns/foreman_puppet/orchestration/puppetca.rb' - 'app/models/foreman_puppet/environment.rb' # Offense count: 2 @@ -96,7 +97,7 @@ Rails/SkipsModelValidations: Exclude: - 'test/controllers/foreman_puppet/api/v2/environments_controller_test.rb' -# Offense count: 198 +# Offense count: 213 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle. # SupportedStyles: always, always_true, never @@ -130,7 +131,7 @@ Style/SymbolProc: Exclude: - 'app/views/foreman_puppet/api/v2/import_puppetclasses/show.json.rabl' -# Offense count: 272 +# Offense count: 279 # Cop supports --auto-correct. # Configuration parameters: AutoCorrect, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns. # URISchemes: http, https diff --git a/app/models/concerns/foreman_puppet/extensions/host.rb b/app/models/concerns/foreman_puppet/extensions/host.rb index d962f577..95ad45a3 100644 --- a/app/models/concerns/foreman_puppet/extensions/host.rb +++ b/app/models/concerns/foreman_puppet/extensions/host.rb @@ -6,6 +6,8 @@ module Host included do prepend PrependedMethods + include ForemanPuppet::Orchestration::Puppetca if SETTINGS[:unattended] + if ForemanPuppet.extracted_from_core? has_one :environment, through: :puppet, class_name: 'ForemanPuppet::Environment' else @@ -16,6 +18,10 @@ module Host host_classes_assoc&.instance_variable_set(:@class_name, 'ForemanPuppet::HostClass') end + smart_proxy_reference self: %i[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 @@ -25,6 +31,20 @@ 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? + 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 class_methods do diff --git a/app/models/concerns/foreman_puppet/orchestration/puppetca.rb b/app/models/concerns/foreman_puppet/orchestration/puppetca.rb new file mode 100644 index 00000000..dded1a45 --- /dev/null +++ b/app/models/concerns/foreman_puppet/orchestration/puppetca.rb @@ -0,0 +1,112 @@ +module ForemanPuppet + module Orchestration + module 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 StandardError => 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. + queue_puppetca_certname_reset if Setting[:use_uuid_for_certificates] ? !Foreman.is_uuid?(certname) : certname != hostname + 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 +end diff --git a/app/models/foreman_puppet/host_puppet_facet.rb b/app/models/foreman_puppet/host_puppet_facet.rb index 9855c95e..3dfd92a3 100644 --- a/app/models/foreman_puppet/host_puppet_facet.rb +++ b/app/models/foreman_puppet/host_puppet_facet.rb @@ -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? @@ -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 diff --git a/app/models/foreman_puppet/puppet_facet_common.rb b/app/models/foreman_puppet/puppet_facet_common.rb index 66e3bc3e..448e555a 100644 --- a/app/models/foreman_puppet/puppet_facet_common.rb +++ b/app/models/foreman_puppet/puppet_facet_common.rb @@ -4,6 +4,8 @@ module ForemanPuppet module PuppetFacetCommon extend ActiveSupport::Concern + include ::BelongsToProxies + included do belongs_to :environment has_many :host_config_groups, as: :host, dependent: :destroy @@ -11,7 +13,21 @@ module PuppetFacetCommon 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 @@ -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 diff --git a/app/models/foreman_puppet/token/puppetca.rb b/app/models/foreman_puppet/token/puppetca.rb new file mode 100644 index 00000000..2f9cc32c --- /dev/null +++ b/app/models/foreman_puppet/token/puppetca.rb @@ -0,0 +1,7 @@ +module ForemanPuppet + module Token + class Puppetca < ::Token + validates :value, uniqueness: true + end + end +end diff --git a/db/migrate/20210427122719_add_puppet_ca_proxy_column.rb b/db/migrate/20210427122719_add_puppet_ca_proxy_column.rb new file mode 100644 index 00000000..72294753 --- /dev/null +++ b/db/migrate/20210427122719_add_puppet_ca_proxy_column.rb @@ -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 diff --git a/lib/foreman_puppet/register.rb b/lib/foreman_puppet/register.rb index bffc6272..7d5c73cc 100644 --- a/lib/foreman_puppet/register.rb +++ b/lib/foreman_puppet/register.rb @@ -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 diff --git a/test/models/foreman_puppet/host_puppet_facet_test.rb b/test/models/foreman_puppet/host_puppet_facet_test.rb index 86e0e398..4c150fdb 100644 --- a/test/models/foreman_puppet/host_puppet_facet_test.rb +++ b/test/models/foreman_puppet/host_puppet_facet_test.rb @@ -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) @@ -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 diff --git a/test/models/foreman_puppet/orchestration/puppet_ca_orchestration_test.rb b/test/models/foreman_puppet/orchestration/puppet_ca_orchestration_test.rb new file mode 100644 index 00000000..42f3d333 --- /dev/null +++ b/test/models/foreman_puppet/orchestration/puppet_ca_orchestration_test.rb @@ -0,0 +1,185 @@ +require 'test_helper' + +class PuppetCaOrchestrationTest < ActiveSupport::TestCase + def setup + users(:one).roles << Role.find_by(name: 'Manager') + User.current = users(:one) + disable_orchestration + Setting[:manage_puppetca] = true + skip_without_unattended + end + + def teardown + User.current = nil + end + + context 'a host with puppetca orchestration' do + context 'when entering build mode on creation' do + let(:host) { FactoryBot.create(:host, :managed, :with_puppet_ca, build: true) } + + test 'should queue puppetca autosigning' do + assert_valid host + tasks = host.post_queue.all.sort.map(&:name) + assert_equal tasks[0], "Cleanup PuppetCA certificates for #{host}" + assert_equal tasks[1], "Enable PuppetCA autosigning for #{host}" + assert_equal 2, tasks.size + end + + test 'should use the hostname for autosigning on setting' do + Setting[:use_uuid_for_certificates] = false + assert_valid host + assert_equal host.certname, host.hostname + assert host.send(:initialize_puppetca) + host.puppetca.expects(:set_autosign).with(host.hostname).returns(true) + assert host.send(:setAutosign) + end + + test 'should use a uuid for autosigning on setting' do + Setting[:use_uuid_for_certificates] = true + assert_valid host + assert Foreman.is_uuid?(host.certname) + assert host.send(:initialize_puppetca) + host.puppetca.expects(:set_autosign).with(host.certname).returns(true) + assert host.send(:setAutosign) + end + end + + context 'when reentering build mode' do + let(:host) { FactoryBot.create(:host, :managed, :with_puppet_ca, build: false) } + + setup do + @host = host + @host.post_queue.clear + @host.build = true + @host.save! + end + + test 'should queue puppetca autosigning' do + tasks = @host.post_queue.all.sort.map(&:name) + assert_equal tasks[0], "Disable PuppetCA autosigning for #{host}" + assert_equal tasks[1], "Cleanup PuppetCA certificates for #{host}" + assert_equal tasks[2], "Enable PuppetCA autosigning for #{host}" + assert_equal 3, tasks.size + end + end + + context 'when reentering build mode after certname setting was changed' do + let(:host) { FactoryBot.create(:host, :managed, :with_puppet_ca, build: false) } + + test 'should reset certname when changing from hostname to uuid' do + assert_valid host + host.post_queue.clear + Setting[:use_uuid_for_certificates] = true + host.build = true + host.save! + tasks = host.post_queue.all.sort.map(&:name) + assert_equal tasks[0], "Disable PuppetCA autosigning for #{host}" + assert_equal tasks[1], "Cleanup PuppetCA certificates for #{host}" + assert_equal tasks[2], "Enable PuppetCA autosigning for #{host}" + assert_equal 3, tasks.size + # Foreman updates the certname automatically in this case + assert Foreman.is_uuid?(host.certname) + end + + test 'should reset certname when changing from uuid to hostname' do + Setting[:use_uuid_for_certificates] = true + assert_valid host + host.post_queue.clear + Setting[:use_uuid_for_certificates] = false + host.build = true + host.save! + tasks = host.post_queue.all.sort.map(&:name) + assert_equal tasks[0], "Reset PuppetCA certname for #{host}" + assert_equal tasks[1], "Disable PuppetCA autosigning for #{host}" + assert_equal tasks[2], "Cleanup PuppetCA certificates for #{host}" + assert_equal tasks[3], "Enable PuppetCA autosigning for #{host}" + assert_equal 4, tasks.size + end + end + + context 'when host leaves build mode' do + let(:host) { FactoryBot.create(:host, :managed, :with_puppet_ca, build: true) } + + setup do + @host = host + @host.post_queue.clear + @host.build = false + @host.save! + end + + test 'should remove autosign entry for host' do + tasks = @host.post_queue.all.sort.map(&:name) + assert_equal tasks[0], "Disable PuppetCA autosigning for #{host}" + assert_equal 1, tasks.size + end + end + + context 'when host is updated' do + let(:host) { FactoryBot.create(:host, :managed, :with_puppet_ca, build: false) } + + test 'should not queue anything if build mode is not changed' do + assert_valid host + host.post_queue.clear + host.comment = 'updated' + host.save! + assert_equal 0, host.post_queue.all.size + end + end + + context 'when host gets destroyed' do + let(:host) { FactoryBot.create(:host, :managed, :with_puppet_ca, build: false) } + + test 'should queue puppetca destroy' do + assert_valid host + host.post_queue.clear + host.send(:queue_puppetca_destroy) + tasks = host.post_queue.all.sort.map(&:name) + assert_equal tasks[0], "Disable PuppetCA autosigning for #{host}" + assert_equal tasks[1], "Delete PuppetCA certificates for #{host}" + assert_equal 2, tasks.size + end + end + + context 'handles smart proxy responses correctly' do + let(:host) { FactoryBot.create(:host, :managed, :with_puppet_ca, build: true) } + + setup do + @host = host + @host.send(:initialize_puppetca) + end + + test 'when it uses basic autosigning' do + @host.puppetca.stubs(:set_autosign).with(@host.certname).returns(true) + assert @host.send(:setAutosign) + assert_nil @host.puppetca_token + end + + test 'when autosigning fails' do + @host.puppetca.stubs(:set_autosign).with(@host.certname).returns(false) + assert_not @host.send(:setAutosign) + assert_nil @host.puppetca_token + end + + test 'when using token based autosigning' do + spresponse = { 'generated_token' => 'foo42' } + @host.puppetca.stubs(:set_autosign).with(@host.certname).returns(spresponse) + assert @host.send(:setAutosign) + assert_valid @host.puppetca_token + assert_equal('foo42', @host.puppetca_token.value) + end + + test 'when it gets an invalid hash response' do + spresponse = { 'not_a_token' => '' } + @host.puppetca.stubs(:set_autosign).with(@host.certname).returns(spresponse) + assert_not @host.send(:setAutosign) + assert_nil @host.puppetca_token + end + + test 'when it gets an invalid nil response' do + @host.puppetca.stubs(:set_autosign).with(@host.certname).returns(nil) + assert_not @host.send(:setAutosign) + assert_nil @host.puppetca_token + end + end + end +end diff --git a/test/models/foreman_puppet/token/puppetca_test.rb b/test/models/foreman_puppet/token/puppetca_test.rb new file mode 100644 index 00000000..2f5fc254 --- /dev/null +++ b/test/models/foreman_puppet/token/puppetca_test.rb @@ -0,0 +1,26 @@ +require 'test_puppet_helper' + +module ForemanPuppet + module Token + class PuppetcaTest < ActiveSupport::TestCase + should validate_uniqueness_of(:value) + + let(:host) { FactoryBot.create(:host) } + + test 'a host can create a puppetca-token' do + host.create_puppetca_token value: 'foo.bar.baz' + assert_instance_of Token::Puppetca, host.puppetca_token + assert_equal Token::Puppetca.first.host_id, host.id + assert_equal 'foo.bar.baz', host.puppetca_token.value + end + + test 'a host can delete its puppetca-token' do + host.create_puppetca_token value: 'aaaa' + assert_equal('aaaa', host.puppetca_token.value) + host.puppetca_token = nil + assert_nil host.puppetca_token + assert_equal([], Token::Puppetca.all) + end + end + end +end