changeset 388:750d36241580

Add missing dependency modules Probably required by SSH, but not obviously listed
author IBBoard <dev@ibboard.co.uk>
date Mon, 03 Jan 2022 17:15:14 +0000
parents 66c075c5f54a
children 668df4711671
files modules/common/CHANGELOG modules/common/Gemfile modules/common/LICENSE modules/common/README.md modules/common/Rakefile modules/common/checksums.json modules/common/lib/puppet/parser/functions/interface2factname.rb modules/common/lib/puppet/parser/functions/strip_file_extension.rb modules/common/manifests/init.pp modules/common/manifests/mkdir_p.pp modules/common/manifests/mkuser.pp modules/common/manifests/remove_if_empty.pp modules/common/metadata.json modules/common/spec/classes/init_spec.rb modules/common/spec/classes/mkuser_use_cases_spec.rb modules/common/spec/defines/mkdir_p_spec.rb modules/common/spec/defines/mkuser_spec.rb modules/common/spec/defines/remove_if_empty_spec.rb modules/common/spec/functions/interface2factname_spec.rb modules/common/spec/functions/strip_file_extension_spec.rb modules/common/spec/spec_helper.rb modules/common/tests/init.pp modules/sshkeys_core/CHANGELOG.md modules/sshkeys_core/CODEOWNERS modules/sshkeys_core/Gemfile modules/sshkeys_core/LICENSE modules/sshkeys_core/README.md modules/sshkeys_core/REFERENCE.md modules/sshkeys_core/Rakefile modules/sshkeys_core/appveyor.yml modules/sshkeys_core/checksums.json modules/sshkeys_core/data/common.yaml modules/sshkeys_core/hiera.yaml modules/sshkeys_core/lib/puppet/provider/ssh_authorized_key/parsed.rb modules/sshkeys_core/lib/puppet/provider/sshkey/parsed.rb modules/sshkeys_core/lib/puppet/type/ssh_authorized_key.rb modules/sshkeys_core/lib/puppet/type/sshkey.rb modules/sshkeys_core/locales/config.yaml modules/sshkeys_core/locales/ja/puppetlabs-sshkeys_core.po modules/sshkeys_core/locales/puppetlabs-sshkeys_core.pot modules/sshkeys_core/metadata.json modules/sshkeys_core/readmes/README_ja_JP.md modules/sshkeys_core/spec/acceptance/nodesets/default.yml modules/sshkeys_core/spec/acceptance/tests/resource/ssh_authorized_key/create_spec.rb modules/sshkeys_core/spec/acceptance/tests/resource/ssh_authorized_key/destroy_spec.rb modules/sshkeys_core/spec/acceptance/tests/resource/ssh_authorized_key/modify_spec.rb modules/sshkeys_core/spec/acceptance/tests/resource/sshkey/create_spec.rb modules/sshkeys_core/spec/default_facts.yml modules/sshkeys_core/spec/fixtures/integration/provider/sshkey/sample modules/sshkeys_core/spec/fixtures/unit/provider/sshkey/parsed/sample modules/sshkeys_core/spec/fixtures/unit/provider/sshkey/parsed/sample_with_blank_lines modules/sshkeys_core/spec/fixtures/unit/type/user/authorized_keys modules/sshkeys_core/spec/integration/provider/ssh_authorized_key_spec.rb modules/sshkeys_core/spec/integration/provider/sshkey_spec.rb modules/sshkeys_core/spec/integration/type/user_spec.rb modules/sshkeys_core/spec/lib/puppet_spec/compiler.rb modules/sshkeys_core/spec/lib/puppet_spec/files.rb modules/sshkeys_core/spec/spec_helper.rb modules/sshkeys_core/spec/spec_helper_acceptance.rb modules/sshkeys_core/spec/spec_helper_local.rb modules/sshkeys_core/spec/unit/provider/sshkey/parsed_spec.rb modules/sshkeys_core/spec/unit/type/ssh_authorized_key_spec.rb modules/sshkeys_core/spec/unit/type/sshkey_spec.rb modules/sshkeys_core/spec/unit/type/user_spec.rb
diffstat 64 files changed, 5220 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/common/CHANGELOG	Mon Jan 03 17:15:14 2022 +0000
@@ -0,0 +1,22 @@
+1.0.0 - 2013-05-24
+
+Add spec tests and become viable for first major release.
+
+Albin Gustavsson
+  2ef8ddb Removed some commented out code from the spec tests
+  d8fd795 Added spec testing for functions and defines
+  558a99f Added spec testing for common::mkuser
+
+Garrett Honeycutt
+  21c7d9a Ensure all modules are listed in README
+  49f726d Add facter to .gemfile
+  fd2a905 Test for both attributes of mkdir_p with a variable
+  81067a2 Travis - fix allowed failure for 2.7.x
+  556d1be Travis - test against Puppet 3.2.1 and display graphic in README
+  676bdeb Travis - remove 2.6.9 testing and test 3.x
+  c64244c Add travis-ci.org support
+  419e530 Split mkuser testing into its own file and clarify usernames.
+  93beb4f Add spec tests
+
+0.0.1 - 2013-05-12 Garrett Honeycutt <code@garretthoneycutt.com>
+* Rework of some old code such that no action is taken by default
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/common/Gemfile	Mon Jan 03 17:15:14 2022 +0000
@@ -0,0 +1,41 @@
+source ENV['GEM_SOURCE'] || 'https://rubygems.org'
+
+if puppetversion = ENV['PUPPET_GEM_VERSION']
+  gem 'puppet', puppetversion, :require => false
+else
+  gem 'puppet', :require => false
+end
+
+gem 'facter', '>= 2.2.0'
+gem 'rspec-puppet', '~> 2.0'
+gem 'puppet-lint'
+gem 'puppet-lint-absolute_classname-check'
+gem 'puppet-lint-alias-check'
+gem 'puppet-lint-empty_string-check'
+gem 'puppet-lint-file_ensure-check'
+gem 'puppet-lint-file_source_rights-check'
+gem 'puppet-lint-leading_zero-check'
+gem 'puppet-lint-spaceship_operator_without_tag-check'
+gem 'puppet-lint-trailing_comma-check'
+gem 'puppet-lint-undef_in_function-check'
+gem 'puppet-lint-unquoted_string-check'
+gem 'puppet-lint-variable_contains_upcase'
+
+gem 'rspec',              '~> 2.0'   if RUBY_VERSION >= '1.8.7' && RUBY_VERSION < '1.9'
+gem 'rake',               '~> 10.0'  if RUBY_VERSION >= '1.8.7' && RUBY_VERSION < '1.9'
+gem 'json',               '<= 1.8'   if RUBY_VERSION < '2.0.0'
+gem 'json_pure',          '<= 2.0.1' if RUBY_VERSION < '2.0.0'
+gem 'metadata-json-lint', '0.0.11'   if RUBY_VERSION <= '1.9.3'
+gem 'metadata-json-lint'             if RUBY_VERSION > '1.9.3'
+gem 'public_suffix',      '~> 1.1.0' if RUBY_VERSION < '2.1.1' && RUBY_VERSION >= '1.9'
+gem 'public_suffix',      '1.3.0'    if RUBY_VERSION < '1.9'
+
+gem 'puppetlabs_spec_helper', '2.0.2',    :require => false if RUBY_VERSION >= '1.8.7' && RUBY_VERSION < '1.9'
+gem 'puppetlabs_spec_helper', '>= 2.0.0', :require => false if RUBY_VERSION >= '1.9'
+gem 'parallel_tests',         '<= 2.9.0', :require => false if RUBY_VERSION < '2.0.0' && RUBY_VERSION >= '1.9'
+gem 'parallel_tests',         '1.0.9',    :require => false if RUBY_VERSION < '1.9'
+gem 'parallel',               '1.3.3.1',  :require => false if RUBY_VERSION < '1.9'
+
+if puppetversion && puppetversion < '5.0' && RUBY_VERSION >= '2.1.9'
+  gem 'semantic_puppet', :require => false
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/common/LICENSE	Mon Jan 03 17:15:14 2022 +0000
@@ -0,0 +1,13 @@
+Copyright (C) 2007-2020 Garrett Honeycutt <code@garretthoneycutt.com>
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/common/README.md	Mon Jan 03 17:15:14 2022 +0000
@@ -0,0 +1,377 @@
+# puppet-module-common #
+
+[![Build Status](
+https://api.travis-ci.org/ghoneycutt/puppet-module-common.png?branch=master)](https://travis-ci.org/ghoneycutt/puppet-module-common)
+
+common module to be applied to **ALL** nodes
+
+# Compatibility #
+
+Module is generic enough to work on any system, though the individual modules that it could potentially include could be very platform specific.
+
+===
+
+# Common class #
+Optionally include classes that are common to all systems, such as `dnsclient`, `ntp`, `puppet::agent`, and `vim`. By default we do not take any action, so you must enable the classes. This should be done in Hiera such as the following example. Ideally you would do this in your least specific level of hiera (often times labeled as 'common' or 'global') and potentially override at other levels.
+
+<pre>
+---
+common::manage_root_password: true
+common::enable_dnsclient: true
+common::enable_ntp: true
+common::enable_puppet_agent: true
+common::enable_vim: true
+</pre>
+
+## Parameters for class `common`##
+
+users
+-----
+Hash of users to ensure with common::mkusers
+
+- *Default*: undef
+
+groups
+------
+Hash of groups to ensure
+
+- *Default*: undef
+
+manage_root_password
+--------------------
+
+- *Default*: false
+
+root_password
+-------------
+
+- *Default*: MD5 crypt of `puppet`
+
+create_opt_lsb_provider_name_dir
+--------------------------------
+Boolean to ensure `/opt/${lsb_provider_name}`
+
+- *Default*: false
+
+lsb_provider_name
+-----------------
+LSB Provider Name as assigned by LANANA - [http://www.lanana.org/lsbreg/providers/index.html](http://www.lanana.org/lsbreg/providers/index.html)
+
+- *Default*: `UNSET`
+
+enable_dnsclient
+----------------
+Boolean to include ghoneycutt/dnsclient
+
+- *Default*: false
+
+enable_hosts
+------------
+Boolean to include ghoneycutt/hosts
+
+- *Default*: false
+
+enable_inittab
+--------------
+Boolean to include ghoneycutt/inittab
+
+- *Default*: false
+
+enable_mailaliases
+------------------
+Boolean to include ghoneycutt/mailaliases
+
+- *Default*: false
+
+enable_motd
+-----------
+Boolean to include ghoneycutt/motd
+
+- *Default*: false
+
+enable_network
+--------------
+Boolean to include ghoneycutt/network
+
+- *Default*: false
+
+enable_nsswitch
+---------------
+Boolean to include ghoneycutt/nsswitch
+
+- *Default*: false
+
+enable_ntp
+----------
+Boolean to include ghoneycutt/ntp
+
+- *Default*: false
+
+enable_pam
+----------
+Boolean to include ghoneycutt/pam
+
+- *Default*: false
+
+enable_puppet_agent
+-------------------
+Boolean to include ghoneycutt/puppet::agent
+
+- *Default*: false
+
+enable_rsyslog
+--------------
+Boolean to include ghoneycutt/rsyslog
+
+- *Default*: false
+
+enable_selinux
+--------------
+Boolean to include ghoneycutt/selinux
+
+- *Default*: false
+
+enable_ssh
+----------
+Boolean to include ghoneycutt/ssh
+
+- *Default*: false
+
+enable_utils
+------------
+Boolean to include ghoneycutt/utils
+
+- *Default*: false
+
+enable_vim
+----------
+Boolean to include ghoneycutt/vim
+
+- *Default*: false
+
+enable_wget
+-----------
+Boolean to include ghoneycutt/wget
+
+- *Default*: false
+
+### includes classes based on `osfamily` fact ###
+
+enable_debian
+-----------
+Boolean to include ghoneycutt/debian
+
+- *Default*: false
+
+enable_redhat
+-----------
+Boolean to include ghoneycutt/redhat
+
+- *Default*: false
+
+enable_solaris
+-----------
+Boolean to include ghoneycutt/solaris
+
+- *Default*: false
+
+enable_suse
+-----------
+Boolean to include ghoneycutt/suse
+
+- *Default*: false
+
+===
+
+# common::mkdir_p define #
+Provide `mkdir -p` functionality for a directory.
+
+Used in conjunction with a file resource.
+
+## Example usage: ##
+<pre>
+common::mkdir_p { '/some/dir/structure': }
+
+file { '/some/dir/structure':
+  ensure  => directory,
+  require => Common::Mkdir_p['/some/dir/structure'],
+}
+</pre>
+
+## Parameters for `common::mkdir_p` define ##
+
+None.
+
+===
+
+# common::remove_if_empty define #
+Removes a file if it exists and is empty.
+
+## Example usage: ##
+<pre>
+common::remove_if_empty { '/path/to/potentially_empty_file': }
+</pre>
+
+## Parameters for `common::remove_if_empty` define ##
+
+None.
+
+===
+
+# common::mkuser define #
+Ensures user/groups
+
+## Usage ##
+You can specify hash each for users and groups and use Hiera to manage them.
+
+This example uses the YAML backend, though that is not mandatory.
+
+In Hiera's hierarchy add two levels, `users`, and `groups` such as the following example.
+
+`hiera.yaml`
+<pre>
+---
+:backends:
+  - yaml
+:hierarchy:
+  - fqdn/%{fqdn}
+  - users
+  - groups
+  - %{environment}
+  - common
+:yaml:
+  :datadir:
+</pre>
+
+`users.yaml`
+<pre>
+---
+common::users:
+  gh:
+    uid: "30000"
+    comment: "Garrett Honeycutt"
+    groups: admin
+    ssh_auth_key: ssh-public-key
+</pre>
+
+`groups.yaml`
+<pre>
+---
+common::groups:
+  admin:
+    gid: "32000"
+</pre>
+
+
+## Parameters for `common::mkuser` define ##
+
+uid
+---
+String - UID of user
+
+- *Required*
+
+gid
+---
+String - GID of user
+
+- *Default*: `$uid`
+
+name
+----
+String - username
+
+group
+-----
+String - group name of user
+
+- *Default*: `$name`
+
+shell
+-----
+String - user's shell
+
+- *Default*: '/bin/bash'
+
+home
+------
+String - home directory
+
+- *Default*: `/home/${username}`
+
+ensure
+------
+Present or Absent
+
+- *Default*: present
+
+managehome
+----------
+Boolean for manage home attribute of user resource
+
+- *Default*: true
+
+manage_dotssh
+-------------
+Boolean to optionally create `~/.ssh` directory
+
+- *Default*: true
+
+comment
+-------
+String - GECOS field for passed
+
+- *Default*: 'created via puppet'
+
+groups
+------
+Array - additional groups the user should be associated with
+
+- *Default*: undef
+
+password
+--------
+String - password crypt for user
+
+- *Default*: '!!'
+
+mode
+----
+String - mode of home directory
+
+- *Default*: 0700
+
+ssh_auth_key
+-----------------
+String - The ssh key for the user
+
+- *Default*: undef
+
+ssh_auth_key_type
+-----------------
+String - Anything that the ssh_authorized_key resource can take for the type attribute, such as `ssh-dss` or `ssh-rsa`.
+
+- *Default*: 'ssh-dss'
+
+purge_ssh_keys
+-----------------
+Boolean - Purge any keys that aren’t managed as ssh_authorized_key resources. As this parameter was introduced with Puppet 3.6,
+it will only work with Puppet >= 3.6. On earlier version this parameter will be silently ignored.
+
+
+- *Default*: false
+
+===
+
+# Functions #
+
+## interface2factname() ##
+Takes one argument, the interface name, and returns it formatted for use with facter.
+
+Example: `interface2factname('bond0:0')` would return `ipaddress_bond0_0`.
+
+## strip_file_extension() ##
+Takes two arguments, a file name which can include the path, and the extension to be removed. Returns the file name without the extension as a string.
+
+Example: `strip_file_extension('myapp.war','war')` would return `myapp`.
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/common/Rakefile	Mon Jan 03 17:15:14 2022 +0000
@@ -0,0 +1,16 @@
+require 'puppetlabs_spec_helper/rake_tasks'
+require 'puppet-lint/tasks/puppet-lint'
+PuppetLint.configuration.send('disable_80chars')
+PuppetLint.configuration.send('disable_140chars')
+PuppetLint.configuration.relative = true
+PuppetLint.configuration.ignore_paths = ['spec/**/*.pp', 'pkg/**/*.pp', 'vendor/**/*.pp']
+
+desc 'Validate manifests, templates, and ruby files'
+task :validate do
+  Dir['spec/**/*.rb', 'lib/**/*.rb'].each do |ruby_file|
+    sh "ruby -c #{ruby_file}" unless ruby_file =~ /spec\/fixtures/
+  end
+  Dir['files/**/*.sh'].each do |shell_script|
+    sh "bash -n #{shell_script}"
+  end
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/common/checksums.json	Mon Jan 03 17:15:14 2022 +0000
@@ -0,0 +1,23 @@
+{
+  "CHANGELOG": "9d9dbf369b52edf7b6264fa51e59ed2e",
+  "Gemfile": "abaa884ee8a4c85f81ac9e7820d4424d",
+  "LICENSE": "62044e17123096d332a8be8ffdbaa01a",
+  "README.md": "8c5408dad2bea307610e131a7ab97bcb",
+  "Rakefile": "36b2f69bac69998352ee24ce8b8270a5",
+  "lib/puppet/parser/functions/interface2factname.rb": "fb5ab2ed1ffe64de903a34b337a4102d",
+  "lib/puppet/parser/functions/strip_file_extension.rb": "7b9d678e8346e044ab05f208e602ffb0",
+  "manifests/init.pp": "4f5b33941d784c1ce1e923e404032e8b",
+  "manifests/mkdir_p.pp": "cb3b9916ad43f66b8734249323bffc87",
+  "manifests/mkuser.pp": "7e7b652cd0b89c50c07d10952f9be6c4",
+  "manifests/remove_if_empty.pp": "305f39a2cc3bd2653cb8ca6520e31788",
+  "metadata.json": "0d0fe8b97d8af745fc0a492fb2553f64",
+  "spec/classes/init_spec.rb": "25be7248db2412adf4f5b1b9fb968251",
+  "spec/classes/mkuser_use_cases_spec.rb": "7201f2bec9cdac5826b285241140da5b",
+  "spec/defines/mkdir_p_spec.rb": "3a6b8db7f2860eefbf31ef22400ea84a",
+  "spec/defines/mkuser_spec.rb": "d1c659770f960997f58c32c1683d51da",
+  "spec/defines/remove_if_empty_spec.rb": "6e18465a4f78263d4ca47c04f41497ef",
+  "spec/functions/interface2factname_spec.rb": "afee2b300871c5ead9e5bddb55bfa9b6",
+  "spec/functions/strip_file_extension_spec.rb": "58809e0c56e0e8237c9010d306081831",
+  "spec/spec_helper.rb": "63a6a4e0cb806d5f46095edb8bb4c08f",
+  "tests/init.pp": "c1c9f68a35b8bd5a6147341c0eed884b"
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/common/lib/puppet/parser/functions/interface2factname.rb	Mon Jan 03 17:15:14 2022 +0000
@@ -0,0 +1,14 @@
+module Puppet::Parser::Functions
+  newfunction(:interface2factname, :type => :rvalue, :doc => <<-EOS
+    Takes one argument, the interface name, and returns it formatted for use
+    with facter. Example: interface2factname(bond0:0) would return 'ipaddress_bond0_0'
+    EOS
+  ) do |args|
+
+    raise(Puppet::ParseError, "interface2factname(): Wrong number of arguments " +
+      "given (#{args.size} for 1)") if args.size != 1
+
+    interface = "ipaddress_#{args[0]}"
+    interface.gsub(/[^a-z0-9_]/i, '_')
+  end
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/common/lib/puppet/parser/functions/strip_file_extension.rb	Mon Jan 03 17:15:14 2022 +0000
@@ -0,0 +1,23 @@
+module Puppet::Parser::Functions
+  newfunction(:strip_file_extension, :type => :rvalue, :doc => <<-EOS
+    Takes two arguments, a file name which can include the path, and the
+    extension to be removed. Returns the file name without the extension
+    as a string.
+    EOS
+  ) do |args|
+
+    raise(Puppet::ParseError, "strip_file_extension(): Wrong number of arguments " +
+      "given (#{args.size} for 2)") if args.size != 2
+
+    filename = args[0]
+
+    # allow the extension to optionally start with a period.
+    if args[1] =~ /^\./
+      extension = args[1]
+    else
+      extension = ".#{args[1]}"
+    end
+
+    File.basename(filename,extension)
+  end
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/common/manifests/init.pp	Mon Jan 03 17:15:14 2022 +0000
@@ -0,0 +1,312 @@
+# == Class: common
+#
+# This class is applied to *ALL* nodes
+#
+# === Copyright
+#
+# Copyright 2013 GH Solutions, LLC
+#
+class common (
+  $users                            = undef,
+  $groups                           = undef,
+  $manage_root_password             = false,
+  $root_password                    = '$1$cI5K51$dexSpdv6346YReZcK2H1k.', # puppet
+  $create_opt_lsb_provider_name_dir = false,
+  $lsb_provider_name                = 'UNSET',
+  $enable_dnsclient                 = false,
+  $enable_hosts                     = false,
+  $enable_inittab                   = false,
+  $enable_mailaliases               = false,
+  $enable_motd                      = false,
+  $enable_network                   = false,
+  $enable_nsswitch                  = false,
+  $enable_ntp                       = false,
+  $enable_pam                       = false,
+  $enable_puppet_agent              = false,
+  $enable_rsyslog                   = false,
+  $enable_selinux                   = false,
+  $enable_ssh                       = false,
+  $enable_utils                     = false,
+  $enable_vim                       = false,
+  $enable_wget                      = false,
+  # include classes based on osfamily fact
+  $enable_debian                    = false,
+  $enable_redhat                    = false,
+  $enable_solaris                   = false,
+  $enable_suse                      = false,
+) {
+
+  # validate type and convert string to boolean if necessary
+  if is_string($enable_dnsclient) {
+    $dnsclient_enabled = str2bool($enable_dnsclient)
+  } else {
+    $dnsclient_enabled = $enable_dnsclient
+  }
+  if $dnsclient_enabled == true {
+    include ::dnsclient
+  }
+
+  # validate type and convert string to boolean if necessary
+  if is_string($enable_hosts) {
+    $hosts_enabled = str2bool($enable_hosts)
+  } else {
+    $hosts_enabled = $enable_hosts
+  }
+  if $hosts_enabled == true {
+    include ::hosts
+  }
+
+  # validate type and convert string to boolean if necessary
+  if is_string($enable_inittab) {
+    $inittab_enabled = str2bool($enable_inittab)
+  } else {
+    $inittab_enabled = $enable_inittab
+  }
+  if $inittab_enabled == true {
+    include ::inittab
+  }
+
+  # validate type and convert string to boolean if necessary
+  if is_string($enable_mailaliases) {
+    $mailaliases_enabled = str2bool($enable_mailaliases)
+  } else {
+    $mailaliases_enabled = $enable_mailaliases
+  }
+  if $mailaliases_enabled == true {
+    include ::mailaliases
+  }
+
+  # validate type and convert string to boolean if necessary
+  if is_string($enable_mailaliases) {
+    $motd_enabled = str2bool($enable_motd)
+  } else {
+    $motd_enabled = $enable_motd
+  }
+  if $motd_enabled == true {
+    include ::motd
+  }
+
+  # validate type and convert string to boolean if necessary
+  if is_string($enable_network) {
+    $network_enabled = str2bool($enable_network)
+  } else {
+    $network_enabled = $enable_network
+  }
+  if $network_enabled == true {
+    include ::network
+  }
+
+  # validate type and convert string to boolean if necessary
+  if is_string($enable_nsswitch) {
+    $nsswitch_enabled = str2bool($enable_nsswitch)
+  } else {
+    $nsswitch_enabled = $enable_nsswitch
+  }
+  if $nsswitch_enabled == true {
+    include ::nsswitch
+  }
+
+  # validate type and convert string to boolean if necessary
+  if is_string($enable_ntp) {
+    $ntp_enabled = str2bool($enable_ntp)
+  } else {
+    $ntp_enabled = $enable_ntp
+  }
+  if $ntp_enabled == true {
+    include ::ntp
+  }
+
+  # validate type and convert string to boolean if necessary
+  if is_string($enable_pam) {
+    $pam_enabled = str2bool($enable_pam)
+  } else {
+    $pam_enabled = $enable_pam
+  }
+  if $pam_enabled == true {
+    include ::pam
+  }
+
+  # validate type and convert string to boolean if necessary
+  if is_string($enable_puppet_agent) {
+    $puppet_agent_enabled = str2bool($enable_puppet_agent)
+  } else {
+    $puppet_agent_enabled = $enable_puppet_agent
+  }
+  if $puppet_agent_enabled == true {
+    include ::puppet::agent
+  }
+
+  # validate type and convert string to boolean if necessary
+  if is_string($enable_rsyslog) {
+    $rsyslog_enabled = str2bool($enable_rsyslog)
+  } else {
+    $rsyslog_enabled = $enable_rsyslog
+  }
+  if $rsyslog_enabled == true {
+    include ::rsyslog
+  }
+
+  # validate type and convert string to boolean if necessary
+  if is_string($enable_selinux) {
+    $selinux_enabled = str2bool($enable_selinux)
+  } else {
+    $selinux_enabled = $enable_selinux
+  }
+  if $selinux_enabled == true {
+    include ::selinux
+  }
+
+  # validate type and convert string to boolean if necessary
+  if is_string($enable_ssh) {
+    $ssh_enabled = str2bool($enable_ssh)
+  } else {
+    $ssh_enabled = $enable_ssh
+  }
+  if $ssh_enabled == true {
+    include ::ssh
+  }
+
+  # validate type and convert string to boolean if necessary
+  if is_string($enable_utils) {
+    $utils_enabled = str2bool($enable_utils)
+  } else {
+    $utils_enabled = $enable_utils
+  }
+  if $utils_enabled == true {
+    include ::utils
+  }
+
+  # validate type and convert string to boolean if necessary
+  if is_string($enable_vim) {
+    $vim_enabled = str2bool($enable_vim)
+  } else {
+    $vim_enabled = $enable_vim
+  }
+  if $vim_enabled == true {
+    include ::vim
+  }
+
+  # validate type and convert string to boolean if necessary
+  if is_string($enable_wget) {
+    $wget_enabled = str2bool($enable_wget)
+  } else {
+    $wget_enabled = $enable_wget
+  }
+  if $wget_enabled == true {
+    include ::wget
+  }
+
+  # only allow supported OS's
+  case $::osfamily {
+    'debian': {
+      # validate type and convert string to boolean if necessary
+      if is_string($enable_debian) {
+        $debian_enabled = str2bool($enable_debian)
+      } else {
+        $debian_enabled = $enable_debian
+      }
+      if $debian_enabled == true {
+        include ::debian
+      }
+    }
+    'redhat': {
+      # validate type and convert string to boolean if necessary
+      if is_string($enable_redhat) {
+        $redhat_enabled = str2bool($enable_redhat)
+      } else {
+        $redhat_enabled = $enable_redhat
+      }
+      if $redhat_enabled == true {
+        include ::redhat
+      }
+    }
+    'solaris': {
+      # validate type and convert string to boolean if necessary
+      if is_string($enable_solaris) {
+        $solaris_enabled = str2bool($enable_solaris)
+      } else {
+        $solaris_enabled = $enable_solaris
+      }
+      if $solaris_enabled == true {
+        include ::solaris
+      }
+    }
+    'suse': {
+      # validate type and convert string to boolean if necessary
+      if is_string($enable_suse) {
+        $suse_enabled = str2bool($enable_suse)
+      } else {
+        $suse_enabled = $enable_suse
+      }
+      if $suse_enabled == true {
+        include ::suse
+      }
+    }
+    default: {
+      fail("Supported OS families are Debian, RedHat, Solaris, and Suse. Detected osfamily is ${::osfamily}.")
+    }
+  }
+
+  # validate type and convert string to boolean if necessary
+  if is_string($manage_root_password) {
+    $manage_root_password_real = str2bool($manage_root_password)
+  } else {
+    $manage_root_password_real = $manage_root_password
+  }
+
+  if $manage_root_password_real == true {
+
+    # validate root_password - fail if not a string
+    if !is_string($root_password) {
+      fail('common::root_password is not a string.')
+    }
+
+    user { 'root':
+      password => $root_password,
+    }
+  }
+
+  # validate type and convert string to boolean if necessary
+  if is_string($create_opt_lsb_provider_name_dir) {
+    $create_opt_lsb_provider_name_dir_real = str2bool($create_opt_lsb_provider_name_dir)
+  } else {
+    $create_opt_lsb_provider_name_dir_real = $create_opt_lsb_provider_name_dir
+  }
+
+  if $create_opt_lsb_provider_name_dir_real == true {
+
+    # validate lsb_provider_name - fail if not a string
+    if !is_string($lsb_provider_name) {
+      fail('common::lsb_provider_name is not a string.')
+    }
+
+    if $lsb_provider_name != 'UNSET' {
+
+      # basic filesystem requirements
+      file { "/opt/${lsb_provider_name}":
+        ensure => directory,
+        owner  => 'root',
+        group  => 'root',
+        mode   => '0755',
+      }
+    }
+  }
+
+  if $users != undef {
+
+    # Create virtual user resources
+    create_resources('@common::mkuser',$common::users)
+
+    # Collect all virtual users
+    Common::Mkuser <||> # lint:ignore:spaceship_operator_without_tag
+  }
+
+  if $groups != undef {
+
+    # Create virtual group resources
+    create_resources('@group',$common::groups)
+
+    # Collect all virtual groups
+    Group <||> # lint:ignore:spaceship_operator_without_tag
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/common/manifests/mkdir_p.pp	Mon Jan 03 17:15:14 2022 +0000
@@ -0,0 +1,25 @@
+# == Define: common::mkdir_p
+#
+# Provide `mkdir -p` functionality for a directory
+#
+# Idea is to use this mkdir_p in conjunction with a file resource
+#
+# Example usage:
+#
+#  common::mkdir_p { '/some/dir/structure': }
+#
+#  file { '/some/dir/structure':
+#    ensure  => directory,
+#    require => Common::Mkdir_p['/some/dir/structure'],
+#  }
+#
+define common::mkdir_p () {
+
+  validate_absolute_path($name)
+
+  exec { "mkdir_p-${name}":
+    command => "mkdir -p ${name}",
+    unless  => "test -d ${name}",
+    path    => '/bin:/usr/bin',
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/common/manifests/mkuser.pp	Mon Jan 03 17:15:14 2022 +0000
@@ -0,0 +1,203 @@
+# == Define: common::mkuser
+#
+# mkuser creates a user/group that can be realized in the module that employs it
+#
+# Copyright 2007-2013 Garrett Honeycutt
+# contact@garretthoneycutt.com - Licensed GPLv2
+#
+# Parameters:
+#   $uid               - UID of user
+#   $gid               - GID of user, defaults to UID
+#   $group             - group name of user, defaults to username
+#   $shell             - user's shell, defaults to '/bin/bash'
+#   $home              - home directory, defaults to /home/<username>
+#   $ensure            - present by default
+#   $managehome        - true by default
+#   $manage_dotssh     - true by default. creates ~/.ssh
+#   $comment           - comment field for passwd
+#   $groups            - additional groups the user should be associated with
+#   $password          - defaults to '!!'
+#   $mode              - mode of home directory, defaults to 0700
+#   $ssh_auth_key      - ssh key of the user
+#   $ssh_auth_key_type - defaults to 'ssh-dss'
+#
+# Actions: creates a user/group
+#
+# Requires:
+#   $uid
+#
+# Sample Usage:
+#   # create apachehup user and realize it
+#   @mkuser { 'apachehup':
+#       uid        => '32001',
+#       home       => '/home/apachehup',
+#       comment    => 'Apache Restart User',
+#   } # @mkuser
+#
+#   realize Common::Mkuser[apachehup]
+#
+define common::mkuser (
+  $uid,
+  $gid               = undef,
+  $group             = undef,
+  $shell             = undef,
+  $home              = undef,
+  $ensure            = 'present',
+  $managehome        = true,
+  $manage_dotssh     = true,
+  $comment           = 'created via puppet',
+  $groups            = undef,
+  $password          = undef,
+  $mode              = undef,
+  $ssh_auth_key      = undef,
+  $create_group      = true,
+  $ssh_auth_key_type = undef,
+  $purge_ssh_keys    = undef,
+) {
+
+  if $shell {
+    $myshell = $shell
+  } else {
+    $myshell = '/bin/bash'
+  }
+
+  # if gid is unspecified, match with uid
+  if $gid {
+    $mygid = $gid
+  } else {
+    $mygid = $uid
+  } # fi $gid
+
+  # if groups is unspecified, match with name
+  if $groups {
+    $mygroups = $groups
+  } else {
+    $mygroups = $name
+  }
+
+  # if group is unspecified, use the username
+  if $group {
+    $mygroup = $group
+  } else {
+    $mygroup = $name
+  }
+
+  if $password {
+    $mypassword = $password
+  } else {
+    $mypassword = '!!'
+  }
+
+  # if home is unspecified, use /home/<username>
+  if $home {
+    $myhome = $home
+  } else {
+    $myhome = "/home/${name}"
+  }
+
+  # if mode is unspecified, use 0700, which is the default when you enable the
+  # managehome attribute.
+  if $mode {
+    $mymode = $mode
+  } else {
+    $mymode = '0700'
+  }
+
+  if $purge_ssh_keys != undef {
+    $mypurgekey = str2bool($purge_ssh_keys)
+    validate_bool($mypurgekey)
+  } else {
+    $mypurgekey = false
+  }
+
+  if versioncmp("${::puppetversion}", '3.6') >= 0 { # lint:ignore:only_variable_string
+    User {
+      purge_ssh_keys => $mypurgekey,
+    }
+  }
+
+  # ensure managehome is boolean
+  if is_bool($managehome){
+    $my_managehome = $managehome
+  } elsif is_string($managehome) {
+    $my_managehome = str2bool($managehome)
+  } else {
+    fail("${name}::managehome must be boolean or string.")
+  }
+
+  # create user
+  user { $name:
+    ensure     => $ensure,
+    uid        => $uid,
+    gid        => $mygid,
+    shell      => $myshell,
+    groups     => $mygroups,
+    password   => $mypassword,
+    managehome => $my_managehome,
+    home       => $myhome,
+    comment    => $comment,
+  } # user
+
+  if $create_group {
+    # If the group is not already defined, ensure its existence
+    if !defined(Group[$name]) {
+      group { $name:
+        ensure => $ensure,
+        gid    => $mygid,
+        name   => $mygroup,
+      }
+    }
+  }
+
+  # If managing home, then set the mode of the home directory. This allows for
+  # modes other than 0700 for $HOME.
+  if $my_managehome == true {
+
+    common::mkdir_p { $myhome: }
+
+    file { $myhome:
+      owner   => $name,
+      group   => $mygroup,
+      mode    => $mymode,
+      require => Common::Mkdir_p[$myhome],
+    }
+
+    # ensure manage_dotssh is boolean
+    if is_bool($manage_dotssh){
+      $my_manage_dotssh = $manage_dotssh
+    } elsif is_string($manage_dotssh) {
+      $my_manage_dotssh = str2bool($manage_dotssh)
+    } else {
+      fail("${name}::manage_dotssh must be boolean or string.")
+    }
+
+    # create ~/.ssh
+    if $my_manage_dotssh == true {
+      file { "${myhome}/.ssh":
+        ensure  => directory,
+        mode    => '0700',
+        owner   => $name,
+        group   => $name,
+        require => User[$name],
+      }
+    }
+  }
+
+  # if ssh_auth_key_type is unspecified, use ssh-dss
+  if $ssh_auth_key_type {
+    $my_ssh_auth_key_type = $ssh_auth_key_type
+  } else {
+    $my_ssh_auth_key_type = 'ssh-dss'
+  }
+
+  # if we specify a key, then it should be present
+  if $ssh_auth_key {
+    ssh_authorized_key { $name:
+      ensure  => present,
+      user    => $name,
+      key     => $ssh_auth_key,
+      type    => $my_ssh_auth_key_type,
+      require => File["${myhome}/.ssh"],
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/common/manifests/remove_if_empty.pp	Mon Jan 03 17:15:14 2022 +0000
@@ -0,0 +1,18 @@
+# == Define: common::remove_if_empty
+#
+# Removes a file if it exists and is empty.
+#
+# Example usage:
+#
+#  common::remove_if_empty { '/path/to/potentially_empty_file': }
+#
+define common::remove_if_empty () {
+
+  validate_absolute_path($name)
+
+  exec { "remove_if_empty-${name}":
+    command => "rm -f ${name}",
+    unless  => "test -f ${name}; if [ $? == '0' ]; then test -s ${name}; fi",
+    path    => '/bin:/usr/bin:/sbin:/usr/sbin',
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/common/metadata.json	Mon Jan 03 17:15:14 2022 +0000
@@ -0,0 +1,57 @@
+{
+  "name": "ghoneycutt-common",
+  "version": "1.10.0",
+  "author": "ghoneycutt",
+  "summary": "common module to be applied to all nodes",
+  "license": "Apache-2.0",
+  "source": "git://github.com/ghoneycutt/puppet-module-common.git",
+  "project_page": "https://github.com/ghoneycutt/puppet-module-common",
+  "issues_url": "https://github.com/ghoneycutt/puppet-module-common/issues",
+  "dependencies": [
+    {
+      "name": "puppetlabs/sshkeys_core",
+      "version_requirement": ">= 1.0.1 <2.0.0"
+    },
+    {
+      "name": "puppetlabs/stdlib",
+      "version_requirement": ">= 4.6.0 < 7.0.0"
+    }
+  ],
+  "data_provider": null,
+  "description": "Provide ability to include modules that are common to all nodes.",
+  "requirements": [
+    {
+      "name": "puppet",
+      "version_requirement": ">= 3.0.0 < 7.0.0"
+    }
+  ],
+  "operatingsystem_support": [
+    {
+      "operatingsystem": "Debian"
+    },
+    {
+      "operatingsystem": "RedHat"
+    },
+    {
+      "operatingsystem": "CentOS"
+    },
+    {
+      "operatingsystem": "OracleLinux"
+    },
+    {
+      "operatingsystem": "Scientific"
+    },
+    {
+      "operatingsystem": "Solaris"
+    },
+    {
+      "operatingsystem": "SLES"
+    },
+    {
+      "operatingsystem": "SLED"
+    },
+    {
+      "operatingsystem": "Ubuntu"
+    }
+  ]
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/common/spec/classes/init_spec.rb	Mon Jan 03 17:15:14 2022 +0000
@@ -0,0 +1,88 @@
+require 'spec_helper'
+describe 'common' do
+
+  describe 'class common' do
+
+    context 'default options with supported OS' do
+      let(:facts) { { :osfamily => 'RedHat' } }
+
+      it { should contain_class('common') }
+    end
+
+    context 'default options with unsupported osfamily, Gentoo, should fail' do
+      let(:facts) { { :osfamily => 'Gentoo' } }
+      it do
+        expect {
+          should contain_class('common')
+        }.to raise_error(Puppet::Error,/Supported OS families are Debian, RedHat, Solaris, and Suse\. Detected osfamily is Gentoo\./)
+      end
+    end
+
+    describe 'managing root password' do
+      context 'manage_root_password => true with default root_password' do
+        let(:facts) { { :osfamily => 'RedHat' } }
+        let(:params) { { :manage_root_password => true } }
+
+        it { should contain_class('common') }
+
+        it {
+          should contain_user('root').with({
+            'password' => '$1$cI5K51$dexSpdv6346YReZcK2H1k.',
+          })
+        }
+      end
+
+      context 'manage_root_password => true and root_password => foo' do
+        let(:facts) { { :osfamily => 'RedHat' } }
+        let(:params) do
+          { :manage_root_password => true,
+            :root_password        => 'foo',
+          }
+        end
+
+        it { should contain_class('common') }
+
+        it {
+          should contain_user('root').with({
+            'password' => 'foo',
+          })
+        }
+      end
+    end
+
+    describe 'managing /opt/$lanana' do
+      context 'create_opt_lsb_provider_name_dir => true and lsb_provider_name => UNSET [default]' do
+        let(:facts) { { :osfamily => 'RedHat' } }
+        let(:params) do
+          { :create_opt_lsb_provider_name_dir => true,
+            :lsb_provider_name => 'UNSET',
+          }
+        end
+
+        it { should contain_class('common') }
+
+        it { should_not contain_file('/opt/UNSET') }
+      end
+
+      context 'create_opt_lsb_provider_name_dir => true and lsb_provider_name => foo' do
+        let(:facts) { { :osfamily => 'RedHat' } }
+        let(:params) do
+          { :create_opt_lsb_provider_name_dir => true,
+            :lsb_provider_name                => 'foo',
+          }
+        end
+
+        it { should contain_class('common') }
+
+        it {
+          should contain_file('/opt/foo').with({
+            'ensure' => 'directory',
+            'owner'  => 'root',
+            'group'  => 'root',
+            'mode'   => '0755',
+          })
+        }
+      end
+    end
+  end
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/common/spec/classes/mkuser_use_cases_spec.rb	Mon Jan 03 17:15:14 2022 +0000
@@ -0,0 +1,338 @@
+require 'spec_helper'
+
+clientversion = `facter puppetversion`
+
+describe 'common' do
+  let(:facts) do
+    { :osfamily      => 'RedHat',
+      :puppetversion => clientversion,
+    }
+  end
+
+  context 'one user with default values' do
+    let(:facts) do
+      { :osfamily      => 'RedHat',
+        :puppetversion => clientversion,
+      }
+    end
+    let(:params) do
+      { :users => {
+          'alice' => {
+            'uid' => 1000,
+          }
+        }
+      }
+    end
+
+    it {
+      should contain_user('alice').with({
+        'uid'        => '1000',
+        'gid'        => '1000',
+        'shell'      => '/bin/bash',
+        'home'       => '/home/alice',
+        'ensure'     => 'present',
+        'groups'     => 'alice',
+        'password'   => '!!',
+        'managehome' => 'true',
+        'comment'    => 'created via puppet',
+      })
+    }
+
+    it {
+      should contain_file('/home/alice').with({
+        'owner'   => 'alice',
+        'mode'    => '0700',
+        'require' => 'Common::Mkdir_p[/home/alice]',
+      })
+    }
+
+    it {
+      should contain_file('/home/alice/.ssh').with({
+        'ensure'  => 'directory',
+        'mode'    => '0700',
+        'owner'   => 'alice',
+        'group'   => 'alice',
+        'require' => 'User[alice]',
+      })
+    }
+
+    it { should contain_common__mkdir_p('/home/alice') }
+
+    it {
+      should contain_group('alice').with({
+        'ensure' => 'present',
+        'gid'    => 1000,
+        'name'   => 'alice',
+      })
+    }
+
+    it { should_not contain_ssh_authorized_key('alice') }
+  end
+
+  context 'one user with custom values' do
+    let(:facts) do
+      { :osfamily      => 'RedHat',
+        :puppetversion => clientversion,
+      }
+    end
+    let(:params) do
+      { :users =>  {
+          'myuser' => {
+            'uid'      => 2000,
+            'group'    => 'superusers',
+            'gid'      => 2000,
+            'shell'    => '/bin/zsh',
+            'home'     => '/home/superu',
+            'groups'   => ['superusers', 'development', 'admins'],
+            'password' => 'puppet',
+            'mode'     => '0701',
+            'comment'  => 'a puppet master',
+          }
+        }
+      }
+    end
+
+    it {
+      should contain_user('myuser').with({
+        'uid'      => '2000',
+        'gid'      => '2000',
+        'shell'    => '/bin/zsh',
+        'home'     => '/home/superu',
+        'groups'   => ['superusers', 'development', 'admins'],
+        'password' => 'puppet',
+        'comment'  => 'a puppet master',
+      })
+    }
+
+    it {
+      should contain_file('/home/superu').with({
+        'owner'   => 'myuser',
+        'mode'    => '0701',
+        'require' => 'Common::Mkdir_p[/home/superu]',
+      })
+    }
+
+    it {
+      should contain_file('/home/superu/.ssh').with({
+        'ensure'  => 'directory',
+        'mode'    => '0700',
+        'owner'   => 'myuser',
+        'group'   => 'myuser',
+        'require' => 'User[myuser]',
+      })
+    }
+
+    it { should contain_common__mkdir_p('/home/superu') }
+
+    it { should_not contain_ssh_authorized_key('myuser') }
+  end
+
+  context 'two users with default values' do
+    let(:facts) do
+      { :osfamily      => 'RedHat',
+        :puppetversion => clientversion,
+      }
+    end
+    let(:params) do
+      { :users => {
+         'alice' => {
+           'uid' => 1000,
+         },
+         'bob' => {
+           'uid' => 1001,
+         }
+        }
+      }
+    end
+
+    it {
+      should contain_user('alice').with({
+        'uid'        => 1000,
+        'gid'        => 1000,
+        'shell'      => '/bin/bash',
+        'home'       => '/home/alice',
+        'ensure'     => 'present',
+        'managehome' => true,
+        'groups'     => 'alice',
+        'password'   => '!!',
+        'comment'    => 'created via puppet',
+      })
+    }
+
+    it {
+      should contain_user('bob').with({
+        'uid'        => 1001,
+        'gid'        => 1001,
+        'shell'      => '/bin/bash',
+        'home'       => '/home/bob',
+        'ensure'     => 'present',
+        'managehome' => true,
+        'groups'     => 'bob',
+        'password'   => '!!',
+        'comment'    => 'created via puppet',
+      })
+    }
+
+    it { should contain_common__mkdir_p('/home/alice') }
+    it { should contain_common__mkdir_p('/home/bob') }
+
+    it {
+      should contain_file('/home/alice').with({
+        'owner'   => 'alice',
+        'mode'    => '0700',
+        'require' => 'Common::Mkdir_p[/home/alice]',
+      })
+    }
+
+    it {
+      should contain_file('/home/bob').with({
+        'owner'   => 'bob',
+        'mode'    => '0700',
+        'require' => 'Common::Mkdir_p[/home/bob]',
+      })
+    }
+
+    it {
+      should contain_file('/home/alice/.ssh').with({
+        'ensure'  => 'directory',
+        'mode'    => '0700',
+        'owner'   => 'alice',
+        'group'   => 'alice',
+        'require' => 'User[alice]',
+      })
+    }
+
+    it {
+      should contain_file('/home/bob/.ssh').with({
+        'ensure'  => 'directory',
+        'mode'    => '0700',
+        'owner'   => 'bob',
+        'group'   => 'bob',
+        'require' => 'User[bob]',
+      })
+    }
+
+    it {
+      should contain_group('alice').with({
+        'ensure' => 'present',
+        'gid'    => 1000,
+        'name'   => 'alice',
+      })
+    }
+
+    it {
+      should contain_group('bob').with({
+        'ensure' => 'present',
+        'gid'    => 1001,
+        'name'   => 'bob',
+      })
+    }
+
+    ['alice','bob'].each do |name|
+      it { should_not contain_ssh_authorized_key(name) }
+    end
+  end
+
+  context 'do not manage home' do
+    let(:facts) do
+      { :osfamily      => 'RedHat',
+        :puppetversion => clientversion,
+      }
+    end
+    let(:params) do
+      { :users => {
+          'alice' => {
+            'uid'        => 1000,
+            'managehome' => false
+          }
+        }
+      }
+    end
+
+    it { should_not contain_file('/home/alice') }
+
+    it { should_not contain_common__mkdir_p('/home/alice') }
+
+    it { should contain_user('alice').with_managehome(false) }
+  end
+
+  context 'do not manage dotssh' do
+    let(:facts) do
+      { :osfamily      => 'RedHat',
+        :puppetversion => clientversion,
+      }
+    end
+    let(:params) do
+      { :users => {
+        'alice' => {
+          'uid'           => 1000,
+          'manage_dotssh' => false
+        }
+      }
+    }
+    end
+
+    it { should_not contain_file('/home/alice/.ssh') }
+
+    it { should_not contain_ssh_authorized_key('alice') }
+  end
+
+  describe 'with ssh_auth_key parameter specified' do
+    context 'with defaults for ssh_auth_key_type parameter' do
+      let(:facts) do
+        { :osfamily      => 'RedHat',
+          :puppetversion => clientversion,
+        }
+      end
+      let(:params) do
+        {
+          :users => {
+            'alice' => {
+              'uid'          => 1000,
+              'ssh_auth_key' => 'AAAB3NzaC1yc2EAAAABIwAAAQEArGElx46pD6NNnlxVaTbp0ZJMgBKCmbTCT3RaeCk0ZUJtQ8wkcwTtqIXmmiuFsynUT0DFSd8UIodnBOPqitimmooAVAiAi30TtJVzADfPScMiUnBJKZajIBkEMkwUcqsfh630jyBvLPE/kyQcxbEeGtbu1DG3monkeymanOBW1AKc5o+cJLXcInLnbowMG7NXzujT3BRYn/9s5vtT1V9cuZJs4XLRXQ50NluxJI7sVfRPVvQI9EMbTS4AFBXUej3yfgaLSV+nPZC/lmJ2gR4t/tKvMFF9m16f8IcZKK7o0rK7v81G/tREbOT5YhcKLK+0wBfR6RsmHzwy4EddZloyLQ==',
+            }
+          }
+        }
+      end
+
+      it {
+        should contain_ssh_authorized_key('alice').with({
+          'ensure'  => 'present',
+          'user'    => 'alice',
+          'key'     => 'AAAB3NzaC1yc2EAAAABIwAAAQEArGElx46pD6NNnlxVaTbp0ZJMgBKCmbTCT3RaeCk0ZUJtQ8wkcwTtqIXmmiuFsynUT0DFSd8UIodnBOPqitimmooAVAiAi30TtJVzADfPScMiUnBJKZajIBkEMkwUcqsfh630jyBvLPE/kyQcxbEeGtbu1DG3monkeymanOBW1AKc5o+cJLXcInLnbowMG7NXzujT3BRYn/9s5vtT1V9cuZJs4XLRXQ50NluxJI7sVfRPVvQI9EMbTS4AFBXUej3yfgaLSV+nPZC/lmJ2gR4t/tKvMFF9m16f8IcZKK7o0rK7v81G/tREbOT5YhcKLK+0wBfR6RsmHzwy4EddZloyLQ==',
+          'type'    => 'ssh-dss',
+          'require' => 'File[/home/alice/.ssh]',
+        })
+      }
+    end
+
+    context 'with ssh_auth_key_type parameter specified' do
+      let(:facts) do
+        { :osfamily      => 'RedHat',
+          :puppetversion => clientversion,
+        }
+      end
+      let(:params) do
+        {
+          :users => {
+            'alice' => {
+              'uid'               => 1000,
+              'ssh_auth_key'      => 'AAAB3NzaC1yc2EAAAABIwAAAQEArGElx46pD6NNnlxVaTbp0ZJMgBKCmbTCT3RaeCk0ZUJtQ8wkcwTtqIXmmiuFsynUT0DFSd8UIodnBOPqitimmooAVAiAi30TtJVzADfPScMiUnBJKZajIBkEMkwUcqsfh630jyBvLPE/kyQcxbEeGtbu1DG3monkeymanOBW1AKc5o+cJLXcInLnbowMG7NXzujT3BRYn/9s5vtT1V9cuZJs4XLRXQ50NluxJI7sVfRPVvQI9EMbTS4AFBXUej3yfgaLSV+nPZC/lmJ2gR4t/tKvMFF9m16f8IcZKK7o0rK7v81G/tREbOT5YhcKLK+0wBfR6RsmHzwy4EddZloyLQ==',
+              'ssh_auth_key_type' => 'ssh-rsa',
+            }
+          }
+        }
+      end
+
+      it {
+        should contain_ssh_authorized_key('alice').with({
+          'ensure'  => 'present',
+          'user'    => 'alice',
+          'key'     => 'AAAB3NzaC1yc2EAAAABIwAAAQEArGElx46pD6NNnlxVaTbp0ZJMgBKCmbTCT3RaeCk0ZUJtQ8wkcwTtqIXmmiuFsynUT0DFSd8UIodnBOPqitimmooAVAiAi30TtJVzADfPScMiUnBJKZajIBkEMkwUcqsfh630jyBvLPE/kyQcxbEeGtbu1DG3monkeymanOBW1AKc5o+cJLXcInLnbowMG7NXzujT3BRYn/9s5vtT1V9cuZJs4XLRXQ50NluxJI7sVfRPVvQI9EMbTS4AFBXUej3yfgaLSV+nPZC/lmJ2gR4t/tKvMFF9m16f8IcZKK7o0rK7v81G/tREbOT5YhcKLK+0wBfR6RsmHzwy4EddZloyLQ==',
+          'type'    => 'ssh-rsa',
+          'require' => 'File[/home/alice/.ssh]',
+        })
+      }
+    end
+  end
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/common/spec/defines/mkdir_p_spec.rb	Mon Jan 03 17:15:14 2022 +0000
@@ -0,0 +1,27 @@
+require 'spec_helper'
+
+describe 'common::mkdir_p' do
+  context 'should create new directory' do
+    let(:title) { '/some/dir/structure' }
+
+    it {
+      should contain_exec('mkdir_p-/some/dir/structure').with({
+        'command' => 'mkdir -p /some/dir/structure',
+        'unless'  => 'test -d /some/dir/structure',
+      })
+    }
+  end
+
+  context 'should fail with a path that is not absolute' do
+    let(:title) { 'not/a/valid/absolute/path' }
+
+    it do
+      expect {
+        should contain_exec('mkdir_p-not/a/valid/absolute/path').with({
+          'command' => 'mkdir -p not/a/valid/absolute/path',
+          'unless'  => 'test -d not/a/valid/absolute/path',
+        })
+      }.to raise_error(Puppet::Error)
+    end
+  end
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/common/spec/defines/mkuser_spec.rb	Mon Jan 03 17:15:14 2022 +0000
@@ -0,0 +1,283 @@
+require 'spec_helper'
+
+clientversion = `facter puppetversion`
+
+describe 'common::mkuser' do
+  let(:title) { 'alice' }
+  let(:facts) do
+    { :osfamily      => 'RedHat',
+      :puppetversion => clientversion,
+    }
+  end
+
+  context 'user alice with default values' do
+    let(:params) { { :uid => 1000 } }
+
+    it do
+      should contain_user('alice').with({
+        'uid'        => '1000',
+        'gid'        => '1000',
+        'shell'      => '/bin/bash',
+        'home'       => '/home/alice',
+        'ensure'     => 'present',
+        'groups'     => 'alice',
+        'password'   => '!!',
+        'managehome' => 'true',
+        'comment'    => 'created via puppet',
+      })
+    end
+
+    it do
+      should contain_file('/home/alice').with({
+        'owner'   => 'alice',
+        'group'   => 'alice',
+        'mode'    => '0700',
+        'require' => 'Common::Mkdir_p[/home/alice]',
+      })
+    end
+
+    it do
+      should contain_file('/home/alice/.ssh').with({
+        'ensure'  => 'directory',
+        'mode'    => '0700',
+        'owner'   => 'alice',
+        'group'   => 'alice',
+        'require' => 'User[alice]',
+      })
+    end
+
+    it { should contain_common__mkdir_p('/home/alice') }
+
+    it do
+      should contain_group('alice').with({
+        'ensure' => 'present',
+        'gid'    => 1000,
+        'name'   => 'alice',
+      })
+    end
+
+    it { should_not contain_ssh_authorized_key('alice') }
+  end
+
+  context 'user alice with custom values' do
+    let(:params) do
+      {
+        'uid'      => 2000,
+        'group'    => 'superusers',
+        'gid'      => 2000,
+        'shell'    => '/bin/zsh',
+        'home'     => '/home/superu',
+        'groups'   => %w(superusers development admins),
+        'password' => 'puppet',
+        'mode'     => '0701',
+        'comment'  => 'a puppet master',
+      }
+    end
+
+    it do
+      should contain_user('alice').with({
+        'uid'      => '2000',
+        'gid'      => '2000',
+        'shell'    => '/bin/zsh',
+        'home'     => '/home/superu',
+        'groups'   => %w(superusers development admins),
+        'password' => 'puppet',
+        'comment'  => 'a puppet master',
+      })
+    end
+
+    it do
+      should contain_file('/home/superu').with({
+        'owner'   => 'alice',
+        'group'   => 'superusers',
+        'mode'    => '0701',
+        'require' => 'Common::Mkdir_p[/home/superu]',
+      })
+    end
+
+    it do
+      should contain_file('/home/superu/.ssh').with({
+        'ensure'  => 'directory',
+        'mode'    => '0700',
+        'owner'   => 'alice',
+        'group'   => 'alice',
+        'require' => 'User[alice]',
+      })
+    end
+
+    it { should contain_common__mkdir_p('/home/superu') }
+
+    it { should_not contain_ssh_authorized_key('myuser') }
+  end
+
+  context 'do not manage home' do
+    let(:params) do
+      {
+        'uid'        => 1000,
+        'managehome' => false
+      }
+    end
+
+    it { should_not contain_file('/home/alice') }
+
+    it { should_not contain_common__mkdir_p('/home/alice') }
+
+    it { should contain_user('alice').with_managehome(false) }
+  end
+
+  context 'do not manage dotssh' do
+    let(:params) do
+      {
+        'uid'           => 1000,
+        'manage_dotssh' => false
+      }
+    end
+
+    it { should_not contain_file('/home/alice/.ssh') }
+
+    it { should_not contain_ssh_authorized_key('alice') }
+  end
+
+  describe 'with ssh_auth_key parameter specified' do
+    context 'with defaults for ssh_auth_key_type parameter' do
+      let(:params) do
+        {
+          'uid'          => 1000,
+          'ssh_auth_key' => 'AAAB3NzaC1yc2EAAAABIwAAAQEArGElx46pD6NNnlxVaTbp0ZJMgBKCmbTCT3RaeCk0ZUJtQ8wkcwTtqIXmmiuFsynUT0DFSd8UIodnBOPqitimmooAVAiAi30TtJVzADfPScMiUnBJKZajIBkEMkwUcqsfh630jyBvLPE/kyQcxbEeGtbu1DG3monkeymanOBW1AKc5o+cJLXcInLnbowMG7NXzujT3BRYn/9s5vtT1V9cuZJs4XLRXQ50NluxJI7sVfRPVvQI9EMbTS4AFBXUej3yfgaLSV+nPZC/lmJ2gR4t/tKvMFF9m16f8IcZKK7o0rK7v81G/tREbOT5YhcKLK+0wBfR6RsmHzwy4EddZloyLQ==',
+        }
+      end
+
+      it do
+        should contain_ssh_authorized_key('alice').with({
+          'ensure'  => 'present',
+          'user'    => 'alice',
+          'key'     => 'AAAB3NzaC1yc2EAAAABIwAAAQEArGElx46pD6NNnlxVaTbp0ZJMgBKCmbTCT3RaeCk0ZUJtQ8wkcwTtqIXmmiuFsynUT0DFSd8UIodnBOPqitimmooAVAiAi30TtJVzADfPScMiUnBJKZajIBkEMkwUcqsfh630jyBvLPE/kyQcxbEeGtbu1DG3monkeymanOBW1AKc5o+cJLXcInLnbowMG7NXzujT3BRYn/9s5vtT1V9cuZJs4XLRXQ50NluxJI7sVfRPVvQI9EMbTS4AFBXUej3yfgaLSV+nPZC/lmJ2gR4t/tKvMFF9m16f8IcZKK7o0rK7v81G/tREbOT5YhcKLK+0wBfR6RsmHzwy4EddZloyLQ==',
+          'type'    => 'ssh-dss',
+          'require' => 'File[/home/alice/.ssh]',
+        })
+      end
+    end
+
+    context 'with ssh_auth_key_type parameter specified' do
+      let(:params) do
+        {
+          'uid'               => 1000,
+          'ssh_auth_key'      => 'AAAB3NzaC1yc2EAAAABIwAAAQEArGElx46pD6NNnlxVaTbp0ZJMgBKCmbTCT3RaeCk0ZUJtQ8wkcwTtqIXmmiuFsynUT0DFSd8UIodnBOPqitimmooAVAiAi30TtJVzADfPScMiUnBJKZajIBkEMkwUcqsfh630jyBvLPE/kyQcxbEeGtbu1DG3monkeymanOBW1AKc5o+cJLXcInLnbowMG7NXzujT3BRYn/9s5vtT1V9cuZJs4XLRXQ50NluxJI7sVfRPVvQI9EMbTS4AFBXUej3yfgaLSV+nPZC/lmJ2gR4t/tKvMFF9m16f8IcZKK7o0rK7v81G/tREbOT5YhcKLK+0wBfR6RsmHzwy4EddZloyLQ==',
+          'ssh_auth_key_type' => 'ssh-rsa',
+        }
+      end
+
+      it do
+        should contain_ssh_authorized_key('alice').with({
+          'ensure'  => 'present',
+          'user'    => 'alice',
+          'key'     => 'AAAB3NzaC1yc2EAAAABIwAAAQEArGElx46pD6NNnlxVaTbp0ZJMgBKCmbTCT3RaeCk0ZUJtQ8wkcwTtqIXmmiuFsynUT0DFSd8UIodnBOPqitimmooAVAiAi30TtJVzADfPScMiUnBJKZajIBkEMkwUcqsfh630jyBvLPE/kyQcxbEeGtbu1DG3monkeymanOBW1AKc5o+cJLXcInLnbowMG7NXzujT3BRYn/9s5vtT1V9cuZJs4XLRXQ50NluxJI7sVfRPVvQI9EMbTS4AFBXUej3yfgaLSV+nPZC/lmJ2gR4t/tKvMFF9m16f8IcZKK7o0rK7v81G/tREbOT5YhcKLK+0wBfR6RsmHzwy4EddZloyLQ==',
+          'type'    => 'ssh-rsa',
+          'require' => 'File[/home/alice/.ssh]',
+        })
+      end
+    end
+  end
+
+  # purge_ssh_keys was introduced with Puppet 3.6.0
+  # we need to know which version of Puppet is running this test
+  # to decide which results we need to expect
+  # dirty trick to get the running version of Puppet:
+  clientversion = `facter puppetversion`
+  # test environments contains no facts, we need to set it as fact
+
+  describe "with purge_ssh_keys running on Puppet version #{clientversion}" do
+    let(:facts) do
+      {
+        :osfamily      => 'RedHat',
+        :puppetversion => clientversion,
+      }
+    end
+
+    context 'set to undef/nil' do
+      let(:params) { { :uid => 1000 } }
+
+      if clientversion.to_f >= 3.6
+        it { should contain_user('alice').with_purge_ssh_keys(false) }
+      else
+        it { should contain_user('alice').without_purge_ssh_keys }
+      end
+    end
+
+    context 'set to true' do
+      let(:params) do
+        {
+          'uid'            => 1000,
+          'purge_ssh_keys' => true,
+        }
+      end
+
+      if clientversion.to_f >= 3.6
+        it { should contain_user('alice').with_purge_ssh_keys(true) }
+      else
+        it { should contain_user('alice').without_purge_ssh_keys }
+      end
+    end
+
+    context 'set to false' do
+      let(:params) do
+        {
+          'uid'            => 1000,
+          'purge_ssh_keys' => false,
+        }
+      end
+
+      if clientversion.to_f >= 3.6
+        it { should contain_user('alice').with_purge_ssh_keys(false) }
+      else
+        it { should contain_user('alice').without_purge_ssh_keys }
+      end
+    end
+  end
+
+  describe 'variable type and content validations' do
+    # set needed custom facts and variables
+    let(:facts) do
+      {
+        :osfamily      => 'RedHat',
+        :puppetversion => clientversion,
+      }
+    end
+    let(:validation_params) do
+      {
+        :uid => 1000,
+      }
+    end
+
+    validations = {
+      'bool_stringified' => {
+        :name    => %w(managehome manage_dotssh purge_ssh_keys),
+        :valid   => [true, false, 'true', 'false'],
+        :invalid => ['invalid', %w(array), { 'ha' => 'sh' }, 3, 2.42, nil],
+        :message => '(str2bool|must be boolean or string)',
+      },
+    }
+
+    validations.sort.each do |type, var|
+      var[:name].each do |var_name|
+        var[:valid].each do |valid|
+          context "with #{var_name} (#{type}) set to valid #{valid} (as #{valid.class})" do
+            let(:params) { validation_params.merge({ :"#{var_name}" => valid, }) }
+            it { should compile }
+          end
+        end
+
+        var[:invalid].each do |invalid|
+          context "with #{var_name} (#{type}) set to invalid #{invalid} (as #{invalid.class})" do
+            let(:params) { validation_params.merge({ :"#{var_name}" => invalid, }) }
+            it 'should fail' do
+              expect do
+                should contain_class(subject)
+              end.to raise_error(Puppet::Error, /#{var[:message]}/)
+            end
+          end
+        end
+      end # var[:name].each
+    end # validations.sort.each
+  end # describe 'variable type and content validations'
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/common/spec/defines/remove_if_empty_spec.rb	Mon Jan 03 17:15:14 2022 +0000
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+describe 'common::remove_if_empty' do
+  context 'should create new directory' do
+    let(:title) { '/some/dir/structure' }
+
+    it {
+      should contain_exec('remove_if_empty-/some/dir/structure').with({
+        'command' => 'rm -f /some/dir/structure',
+        'unless'  => 'test -f /some/dir/structure; if [ $? == \'0\' ]; then test -s /some/dir/structure; fi',
+        'path'    => '/bin:/usr/bin:/sbin:/usr/sbin',
+      })
+    }
+  end
+
+  context 'should fail with a path that is not absolute' do
+    let(:title) { 'not/a/valid/absolute/path' }
+
+    it do
+      expect {
+        should contain_exec('remove_if_empty-not/a/valid/absolute/path')
+      }.to raise_error(Puppet::Error)
+    end
+  end
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/common/spec/functions/interface2factname_spec.rb	Mon Jan 03 17:15:14 2022 +0000
@@ -0,0 +1,19 @@
+require 'spec_helper'
+
+describe 'interface2factname' do
+
+  describe 'should return correct results' do
+
+    it 'should run with eth0' do
+      should run.with_params('eth0').and_return('ipaddress_eth0')
+    end
+
+    it 'should run with bond0:0' do
+      should run.with_params('bond0:0').and_return('ipaddress_bond0_0')
+    end
+
+    it 'should run with bond0:1' do
+      should run.with_params('bond0:1').and_return('ipaddress_bond0_1')
+    end
+  end
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/common/spec/functions/strip_file_extension_spec.rb	Mon Jan 03 17:15:14 2022 +0000
@@ -0,0 +1,15 @@
+require 'spec_helper'
+
+describe 'strip_file_extension' do
+
+  describe 'strips file extensions' do
+
+    it 'from single files' do
+      should run.with_params('puppet.conf', 'conf').and_return('puppet')
+    end
+
+    it 'and removes full path' do
+      should run.with_params('/etc/puppet/puppet.conf', 'conf').and_return('puppet')
+    end
+  end
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/common/spec/spec_helper.rb	Mon Jan 03 17:15:14 2022 +0000
@@ -0,0 +1,16 @@
+require 'puppetlabs_spec_helper/module_spec_helper'
+
+RSpec.configure do |config|
+  config.hiera_config = 'spec/fixtures/hiera/hiera.yaml'
+  config.before :each do
+    # Ensure that we don't accidentally cache facts and environment between
+    # test cases.  This requires each example group to explicitly load the
+    # facts being exercised with something like
+    # Facter.collection.loader.load(:ipaddress)
+    Facter.clear
+    Facter.clear_messages
+  end
+  config.default_facts = {
+    :environment => 'rp_env',
+  }
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/common/tests/init.pp	Mon Jan 03 17:15:14 2022 +0000
@@ -0,0 +1,11 @@
+# The baseline for module testing used by Puppet Labs is that each manifest
+# should have a corresponding test manifest that declares that class or defined
+# type.
+#
+# Tests are then run by using puppet apply --noop (to check for compilation errors
+# and view a log of events) or by fully applying the test in a virtual environment
+# (to compare the resulting system state to the desired state).
+#
+# Learn more about module testing here: http://docs.puppetlabs.com/guides/tests_smoke.html
+#
+include ::common
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/sshkeys_core/CHANGELOG.md	Mon Jan 03 17:15:14 2022 +0000
@@ -0,0 +1,47 @@
+# Change log
+
+All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org).
+
+## [1.0.3](https://github.com/puppetlabs/puppetlabs-sshkeys_core/tree/1.0.3) (2019-10-31)
+
+[Full Changelog](https://github.com/puppetlabs/puppetlabs-sshkeys_core/compare/1.0.2...1.0.3)
+
+### Added
+
+- \(MODULES-9578\) Create ssh\_authorized\_key in root path [\#20](https://github.com/puppetlabs/puppetlabs-sshkeys_core/pull/20) ([GabrielNagy](https://github.com/GabrielNagy))
+
+## [1.0.2](https://github.com/puppetlabs/puppetlabs-sshkeys_core/tree/1.0.2) (2019-01-11)
+
+[Full Changelog](https://github.com/puppetlabs/puppetlabs-sshkeys_core/compare/1.0.1...1.0.2)
+
+### Added
+
+- \(maint\) add LICENSE file [\#16](https://github.com/puppetlabs/puppetlabs-sshkeys_core/pull/16) ([melissa](https://github.com/melissa))
+- \(L10n\) Update Japanese translations [\#13](https://github.com/puppetlabs/puppetlabs-sshkeys_core/pull/13) ([melissa](https://github.com/melissa))
+
+### Fixed
+
+- ssh\_authorized\_key: Fix invalid 'options' error [\#10](https://github.com/puppetlabs/puppetlabs-sshkeys_core/pull/10) ([natemccurdy](https://github.com/natemccurdy))
+
+## [1.0.1](https://github.com/puppetlabs/puppetlabs-sshkeys_core/tree/1.0.1) (2018-08-17)
+
+[Full Changelog](https://github.com/puppetlabs/puppetlabs-sshkeys_core/compare/1.0.0...1.0.1)
+
+### Added
+
+- \(PUP-9053\) Enable localization and bump puppet version to at least 6 [\#7](https://github.com/puppetlabs/puppetlabs-sshkeys_core/pull/7) ([melissa](https://github.com/melissa))
+- \(maint\) Import missed User type integration test from puppet repo [\#6](https://github.com/puppetlabs/puppetlabs-sshkeys_core/pull/6) ([jhelwig](https://github.com/jhelwig))
+- \(maint\) Import the User type unit tests specific to ssh\_authorized\_keys [\#5](https://github.com/puppetlabs/puppetlabs-sshkeys_core/pull/5) ([jhelwig](https://github.com/jhelwig))
+- Install module on all hosts, not just those with default role [\#4](https://github.com/puppetlabs/puppetlabs-sshkeys_core/pull/4) ([joshcooper](https://github.com/joshcooper))
+
+## [1.0.0](https://github.com/puppetlabs/puppetlabs-sshkeys_core/tree/1.0.0) (2018-07-17)
+
+[Full Changelog](https://github.com/puppetlabs/puppetlabs-sshkeys_core/compare/d1719de1d77b9c139b1b5f5832330807c0fe11fe...1.0.0)
+
+### Added
+
+- Initial release of the extracted sshkeys module [\#1](https://github.com/puppetlabs/puppetlabs-sshkeys_core/pull/1) ([jhelwig](https://github.com/jhelwig))
+
+
+
+\* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)*
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/sshkeys_core/CODEOWNERS	Mon Jan 03 17:15:14 2022 +0000
@@ -0,0 +1,1 @@
+* @puppetlabs/team-coremunity
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/sshkeys_core/Gemfile	Mon Jan 03 17:15:14 2022 +0000
@@ -0,0 +1,83 @@
+source ENV['GEM_SOURCE'] || 'https://rubygems.org'
+
+def location_for(place_or_version, fake_version = nil)
+  git_url_regex = %r{\A(?<url>(https?|git)[:@][^#]*)(#(?<branch>.*))?}
+  file_url_regex = %r{\Afile:\/\/(?<path>.*)}
+
+  if place_or_version && (git_url = place_or_version.match(git_url_regex))
+    [fake_version, { git: git_url[:url], branch: git_url[:branch], require: false }].compact
+  elsif place_or_version && (file_url = place_or_version.match(file_url_regex))
+    ['>= 0', { path: File.expand_path(file_url[:path]), require: false }]
+  else
+    [place_or_version, { require: false }]
+  end
+end
+
+ruby_version_segments = Gem::Version.new(RUBY_VERSION.dup).segments
+minor_version = ruby_version_segments[0..1].join('.')
+
+group :development do
+  gem "fast_gettext", '1.1.0',                                   require: false if Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new('2.1.0')
+  gem "fast_gettext",                                            require: false if Gem::Version.new(RUBY_VERSION.dup) >= Gem::Version.new('2.1.0')
+  gem "json_pure", '<= 2.0.1',                                   require: false if Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new('2.0.0')
+  gem "json", '= 1.8.1',                                         require: false if Gem::Version.new(RUBY_VERSION.dup) == Gem::Version.new('2.1.9')
+  gem "json", '= 2.0.4',                                         require: false if Gem::Requirement.create('~> 2.4.2').satisfied_by?(Gem::Version.new(RUBY_VERSION.dup))
+  gem "json", '= 2.1.0',                                         require: false if Gem::Requirement.create(['>= 2.5.0', '< 2.7.0']).satisfied_by?(Gem::Version.new(RUBY_VERSION.dup))
+  gem "rb-readline", '= 0.5.5',                                  require: false, platforms: [:mswin, :mingw, :x64_mingw]
+  gem "puppet-module-posix-default-r#{minor_version}", '~> 0.3', require: false, platforms: [:ruby]
+  gem "puppet-module-posix-dev-r#{minor_version}", '~> 0.3',     require: false, platforms: [:ruby]
+  gem "puppet-module-win-default-r#{minor_version}", '~> 0.3',   require: false, platforms: [:mswin, :mingw, :x64_mingw]
+  gem "puppet-module-win-dev-r#{minor_version}", '~> 0.3',       require: false, platforms: [:mswin, :mingw, :x64_mingw]
+  gem "puppet-strings",                                          require: false
+  gem "github_changelog_generator",                              require: false, git: 'https://github.com/skywinder/github-changelog-generator', ref: '20ee04ba1234e9e83eb2ffb5056e23d641c7a018' if Gem::Version.new(RUBY_VERSION.dup) >= Gem::Version.new('2.2.2')
+end
+group :system_tests do
+  gem "puppet-module-posix-system-r#{minor_version}",                            require: false, platforms: [:ruby]
+  gem "puppet-module-win-system-r#{minor_version}",                              require: false, platforms: [:mswin, :mingw, :x64_mingw]
+  gem "beaker", *location_for(ENV['BEAKER_VERSION'] || '~> 3.13')
+  gem "beaker-abs", *location_for(ENV['BEAKER_ABS_VERSION'] || '~> 0.1')
+  gem "beaker-pe",                                                               require: false
+  gem "beaker-hostgenerator"
+  gem "beaker-rspec"
+end
+
+puppet_version = ENV['PUPPET_GEM_VERSION']
+facter_version = ENV['FACTER_GEM_VERSION']
+hiera_version = ENV['HIERA_GEM_VERSION']
+
+gems = {}
+
+gems['puppet'] = location_for(puppet_version)
+
+# If facter or hiera versions have been specified via the environment
+# variables
+
+gems['facter'] = location_for(facter_version) if facter_version
+gems['hiera'] = location_for(hiera_version) if hiera_version
+
+if Gem.win_platform? && puppet_version =~ %r{^(file:///|git://)}
+  # If we're using a Puppet gem on Windows which handles its own win32-xxx gem
+  # dependencies (>= 3.5.0), set the maximum versions (see PUP-6445).
+  gems['win32-dir'] =      ['<= 0.4.9', require: false]
+  gems['win32-eventlog'] = ['<= 0.6.5', require: false]
+  gems['win32-process'] =  ['<= 0.7.5', require: false]
+  gems['win32-security'] = ['<= 0.2.5', require: false]
+  gems['win32-service'] =  ['0.8.8', require: false]
+end
+
+gems.each do |gem_name, gem_params|
+  gem gem_name, *gem_params
+end
+
+# Evaluate Gemfile.local and ~/.gemfile if they exist
+extra_gemfiles = [
+  "#{__FILE__}.local",
+  File.join(Dir.home, '.gemfile'),
+]
+
+extra_gemfiles.each do |gemfile|
+  if File.file?(gemfile) && File.readable?(gemfile)
+    eval(File.read(gemfile), binding)
+  end
+end
+# vim: syntax=ruby
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/sshkeys_core/LICENSE	Mon Jan 03 17:15:14 2022 +0000
@@ -0,0 +1,201 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/sshkeys_core/README.md	Mon Jan 03 17:15:14 2022 +0000
@@ -0,0 +1,62 @@
+
+# sshkeys_core
+
+## Table of Contents
+
+1. [Description](#description)
+2. [Usage - Configuration options and additional functionality](#usage)
+3. [Reference - User documentation](#reference)
+4. [Development - Guide for contributing to the module](#development)
+
+<a id="description"></a>
+## Description
+
+Manage SSH `authorized_keys`, and `ssh_known_hosts` files.
+
+<a id="usage"></a>
+## Usage
+
+To manage an authorized key for a user, use the following code:
+
+```
+ssh_authorized_key { 'nick@magpie.example.com':
+  ensure => present,
+  user   => 'nick',
+  type   => 'ssh-rsa',
+  key    => 'AAAAB3Nza[...]qXfdaQ==',
+}
+```
+
+To manage a known hosts file entry, use the following code:
+
+```
+sshkey { 'github.com':
+  ensure => present,
+  type   => 'ssh-rsa',
+  key    => 'AAAAB3Nza[...]UFFAaQ==',
+}
+```
+<a id="reference"></a>
+## Reference
+
+Please see REFERENCE.md for the reference documentation.
+
+This module is documented using Puppet Strings.
+
+For a quick primer on how Strings works, please see [this blog post](https://puppet.com/blog/using-puppet-strings-generate-great-documentation-puppet-modules) or the [README.md](https://github.com/puppetlabs/puppet-strings/blob/master/README.md) for Puppet Strings.
+
+To generate documentation locally, run the following command:
+```
+bundle install
+bundle exec puppet strings generate ./lib/**/*.rb
+```
+This command will create a browsable `\_index.html` file in the `doc` directory. The references available here are all generated from YARD-style comments embedded in the code base. When any development happens on this module, the impacted documentation should also be updated.
+
+<a id="development"></a>
+## Development
+
+Puppet Labs modules on the Puppet Forge are open projects, and community contributions are essential for keeping them great. We can't access the huge number of platforms and myriad of hardware, software, and deployment configurations that Puppet is intended to serve.
+
+We want to keep it as easy as possible to contribute changes so that our modules work in your environment. There are a few guidelines that we need contributors to follow so that we can have a chance of keeping on top of things.
+
+For more information, see our [module contribution guide.](https://puppet.com/docs/puppet/latest/contributing.html)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/sshkeys_core/REFERENCE.md	Mon Jan 03 17:15:14 2022 +0000
@@ -0,0 +1,193 @@
+# Reference
+
+## Resource types
+* [`ssh_authorized_key`](#ssh_authorized_key): Manages SSH authorized keys. Currently only type 2 keys are supported.  In their native habitat, SSH keys usually appear as a single long lin
+* [`sshkey`](#sshkey): Installs and manages ssh host keys.  By default, this type will install keys into `/etc/ssh/ssh_known_hosts`. To manage ssh keys in a differe
+## Resource types
+
+### ssh_authorized_key
+
+Manages SSH authorized keys. Currently only type 2 keys are supported.
+
+In their native habitat, SSH keys usually appear as a single long line, in
+the format `<TYPE> <KEY> <NAME/COMMENT>`. This resource type requires you
+to split that line into several attributes. Thus, a key that appears in
+your `~/.ssh/id_rsa.pub` file like this...
+
+    ssh-rsa AAAAB3Nza[...]qXfdaQ== nick@magpie.example.com
+
+...would translate to the following resource:
+
+    ssh_authorized_key { 'nick@magpie.example.com':
+      ensure => present,
+      user   => 'nick',
+      type   => 'ssh-rsa',
+      key    => 'AAAAB3Nza[...]qXfdaQ==',
+    }
+
+To ensure that only the currently approved keys are present, you can purge
+unmanaged SSH keys on a per-user basis. Do this with the `user` resource
+type's `purge_ssh_keys` attribute:
+
+    user { 'nick':
+      ensure         => present,
+      purge_ssh_keys => true,
+    }
+
+This will remove any keys in `~/.ssh/authorized_keys` that aren't being
+managed with `ssh_authorized_key` resources. See the documentation of the
+`user` type for more details.
+
+**Autorequires:** If Puppet is managing the user account in which this
+SSH key should be installed, the `ssh_authorized_key` resource will autorequire
+that user.
+
+
+#### Properties
+
+The following properties are available in the `ssh_authorized_key` type.
+
+##### `ensure`
+
+Valid values: present, absent
+
+The basic property that the resource should be in.
+
+Default value: present
+
+##### `type`
+
+Valid values: ssh-dss, ssh-rsa, ecdsa-sha2-nistp256, ecdsa-sha2-nistp384, ecdsa-sha2-nistp521, ssh-ed25519, dsa, ed25519, rsa
+
+Aliases: "dsa"=>"ssh-dss", "ed25519"=>"ssh-ed25519", "rsa"=>"ssh-rsa"
+
+The encryption type used.
+
+##### `key`
+
+The public key itself; generally a long string of hex characters. The `key`
+attribute may not contain whitespace.
+
+Make sure to omit the following in this attribute (and specify them in
+other attributes):
+
+* Key headers, such as 'ssh-rsa' --- put these in the `type` attribute.
+* Key identifiers / comments, such as 'joe@joescomputer.local' --- put these in
+  the `name` attribute/resource title.
+
+##### `user`
+
+The user account in which the SSH key should be installed. The resource
+will autorequire this user if it is being managed as a `user` resource.
+
+##### `target`
+
+The absolute filename in which to store the SSH key. This
+property is optional and should be used only in cases where keys
+are stored in a non-standard location, for instance when not in
+`~user/.ssh/authorized_keys`. The parent directory must be present
+if the target is in a privileged path.
+
+Default value: absent
+
+##### `options`
+
+Key options; see sshd(8) for possible values. Multiple values
+should be specified as an array. For example, you could use the
+following to install a SSH CA that allows someone with the
+'superuser' principal to log in as root
+
+    ssh_authorized_key { 'Company SSH CA':
+      ensure  => present,
+      user    => 'root',
+      type    => 'ssh-ed25519',
+      key     => 'AAAAC3NzaC[...]CeA5kG',
+      options => [ 'cert-authority', 'principals="superuser"' ],
+    }
+
+#### Parameters
+
+The following parameters are available in the `ssh_authorized_key` type.
+
+##### `name`
+
+namevar
+
+The SSH key comment. This can be anything, and doesn't need to match
+the original comment from the `.pub` file.
+
+Due to internal limitations, this must be unique across all user accounts;
+if you want to specify one key for multiple users, you must use a different
+comment for each instance.
+
+##### `drop_privileges`
+
+Whether to drop privileges when writing the key file. This is
+useful for creating files in paths not writable by the target user. Note
+the possible security implications of managing file ownership and
+permissions as a privileged user.
+
+Default value: `true`
+
+### sshkey
+
+Installs and manages ssh host keys.  By default, this type will
+install keys into `/etc/ssh/ssh_known_hosts`. To manage ssh keys in a
+different `known_hosts` file, such as a user's personal `known_hosts`,
+pass its path to the `target` parameter. See the `ssh_authorized_key`
+type to manage authorized keys.
+
+
+#### Properties
+
+The following properties are available in the `sshkey` type.
+
+##### `ensure`
+
+Valid values: present, absent
+
+The basic property that the resource should be in.
+
+Default value: present
+
+##### `type`
+
+Valid values: ssh-dss, ssh-ed25519, ssh-rsa, ecdsa-sha2-nistp256, ecdsa-sha2-nistp384, ecdsa-sha2-nistp521, dsa, ed25519, rsa
+
+Aliases: "dsa"=>"ssh-dss", "ed25519"=>"ssh-ed25519", "rsa"=>"ssh-rsa"
+
+The encryption type used.  Probably ssh-dss or ssh-rsa.
+
+##### `key`
+
+The key itself; generally a long string of uuencoded characters. The `key`
+attribute may not contain whitespace.
+
+Make sure to omit the following in this attribute (and specify them in
+other attributes):
+
+* Key headers, such as 'ssh-rsa' --- put these in the `type` attribute.
+* Key identifiers / comments, such as 'joescomputer.local' --- put these in
+  the `name` attribute/resource title.
+
+##### `host_aliases`
+
+Any aliases the host might have.  Multiple values must be
+specified as an array.
+
+##### `target`
+
+The file in which to store the ssh key.  Only used by
+the `parsed` provider.
+
+#### Parameters
+
+The following parameters are available in the `sshkey` type.
+
+##### `name`
+
+namevar
+
+The host name that the key is associated with.
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/sshkeys_core/Rakefile	Mon Jan 03 17:15:14 2022 +0000
@@ -0,0 +1,87 @@
+require 'puppet_litmus/rake_tasks' if Bundler.rubygems.find_name('puppet_litmus').any?
+require 'puppetlabs_spec_helper/rake_tasks'
+require 'puppet-syntax/tasks/puppet-syntax'
+require 'puppet_blacksmith/rake_tasks' if Bundler.rubygems.find_name('puppet-blacksmith').any?
+require 'github_changelog_generator/task' if Bundler.rubygems.find_name('github_changelog_generator').any?
+require 'puppet-strings/tasks' if Bundler.rubygems.find_name('puppet-strings').any?
+require 'puppet-lint/tasks/puppet-lint'
+
+def changelog_user
+  return unless Rake.application.top_level_tasks.include? "changelog"
+  returnVal = nil || JSON.load(File.read('metadata.json'))['author']
+  raise "unable to find the changelog_user in .sync.yml, or the author in metadata.json" if returnVal.nil?
+  puts "GitHubChangelogGenerator user:#{returnVal}"
+  returnVal
+end
+
+def changelog_project
+  return unless Rake.application.top_level_tasks.include? "changelog"
+
+  returnVal = nil
+  returnVal ||= begin
+    metadata_source = JSON.load(File.read('metadata.json'))['source']
+    metadata_source_match = metadata_source && metadata_source.match(%r{.*\/([^\/]*?)(?:\.git)?\Z})
+
+    metadata_source_match && metadata_source_match[1]
+  end
+
+  raise "unable to find the changelog_project in .sync.yml or calculate it from the source in metadata.json" if returnVal.nil?
+
+  puts "GitHubChangelogGenerator project:#{returnVal}"
+  returnVal
+end
+
+def changelog_future_release
+  return unless Rake.application.top_level_tasks.include? "changelog"
+  returnVal = "%s" % JSON.load(File.read('metadata.json'))['version']
+  raise "unable to find the future_release (version) in metadata.json" if returnVal.nil?
+  puts "GitHubChangelogGenerator future_release:#{returnVal}"
+  returnVal
+end
+
+PuppetLint.configuration.send('disable_relative')
+
+if Bundler.rubygems.find_name('github_changelog_generator').any?
+  GitHubChangelogGenerator::RakeTask.new :changelog do |config|
+    raise "Set CHANGELOG_GITHUB_TOKEN environment variable eg 'export CHANGELOG_GITHUB_TOKEN=valid_token_here'" if Rake.application.top_level_tasks.include? "changelog" and ENV['CHANGELOG_GITHUB_TOKEN'].nil?
+    config.user = "#{changelog_user}"
+    config.project = "#{changelog_project}"
+    config.future_release = "#{changelog_future_release}"
+    config.exclude_labels = ['maintenance']
+    config.header = "# Change log\n\nAll notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org)."
+    config.add_pr_wo_labels = true
+    config.issues = false
+    config.merge_prefix = "### UNCATEGORIZED PRS; GO LABEL THEM"
+    config.configure_sections = {
+      "Changed" => {
+        "prefix" => "### Changed",
+        "labels" => ["backwards-incompatible"],
+      },
+      "Added" => {
+        "prefix" => "### Added",
+        "labels" => ["feature", "enhancement"],
+      },
+      "Fixed" => {
+        "prefix" => "### Fixed",
+        "labels" => ["bugfix"],
+      },
+    }
+  end
+else
+  desc 'Generate a Changelog from GitHub'
+  task :changelog do
+    raise <<EOM
+The changelog tasks depends on unreleased features of the github_changelog_generator gem.
+Please manually add it to your .sync.yml for now, and run `pdk update`:
+---
+Gemfile:
+  optional:
+    ':development':
+      - gem: 'github_changelog_generator'
+        git: 'https://github.com/skywinder/github-changelog-generator'
+        ref: '20ee04ba1234e9e83eb2ffb5056e23d641c7a018'
+        condition: "Gem::Version.new(RUBY_VERSION.dup) >= Gem::Version.new('2.2.2')"
+EOM
+  end
+end
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/sshkeys_core/appveyor.yml	Mon Jan 03 17:15:14 2022 +0000
@@ -0,0 +1,56 @@
+---
+version: 1.1.x.{build}
+branches:
+  only:
+    - master
+    - release
+skip_commits:
+  message: /^\(?doc\)?.*/
+clone_depth: 10
+init:
+  - SET
+  - 'mkdir C:\ProgramData\PuppetLabs\code && exit 0'
+  - 'mkdir C:\ProgramData\PuppetLabs\facter && exit 0'
+  - 'mkdir C:\ProgramData\PuppetLabs\hiera && exit 0'
+  - 'mkdir C:\ProgramData\PuppetLabs\puppet\var && exit 0'
+environment:
+  matrix:
+    -
+      RUBY_VERSION: 24-x64
+      CHECK: syntax lint metadata_lint check:symlinks check:git_ignore check:dot_underscore check:test_file rubocop
+    -
+      PUPPET_GEM_VERSION: ~> 5.0
+      RUBY_VERSION: 24
+      CHECK: parallel_spec
+    -
+      PUPPET_GEM_VERSION: ~> 5.0
+      RUBY_VERSION: 24-x64
+      CHECK: parallel_spec
+    -
+      PUPPET_GEM_VERSION: ~> 6.0
+      RUBY_VERSION: 25
+      CHECK: parallel_spec
+    -
+      PUPPET_GEM_VERSION: ~> 6.0
+      RUBY_VERSION: 25-x64
+      CHECK: parallel_spec
+matrix:
+  fast_finish: true
+install:
+  - set PATH=C:\Ruby%RUBY_VERSION%\bin;%PATH%
+  - bundle install --jobs 4 --retry 2 --without system_tests
+  - type Gemfile.lock
+build: off
+test_script:
+  - bundle exec puppet -V
+  - ruby -v
+  - gem -v
+  - bundle -v
+  - bundle exec rake %CHECK%
+notifications:
+  - provider: Email
+    to:
+      - nobody@nowhere.com
+    on_build_success: false
+    on_build_failure: false
+    on_build_status_changed: false
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/sshkeys_core/checksums.json	Mon Jan 03 17:15:14 2022 +0000
@@ -0,0 +1,43 @@
+{
+  "CHANGELOG.md": "5ae725b37a147d11da824eba5b690019",
+  "CODEOWNERS": "2cca06ca8543bbcc3198f68456d6626e",
+  "Gemfile": "a20dbee236fb0f1b506364848d1b4637",
+  "LICENSE": "86d3f3a95c324c9479bd8986968f4327",
+  "README.md": "c0171a40b42e7b26a87caaa8637e5a2b",
+  "REFERENCE.md": "7e4c9f5fb2bb689367c46bf68d5a1481",
+  "Rakefile": "dd1a30e7c4c65a364d277285097a51f2",
+  "appveyor.yml": "4aa690537aadcdd9341e0e3cca957d38",
+  "data/common.yaml": "6105347ebb9825ac754615ca55ff3b0c",
+  "hiera.yaml": "b13568f83ed354ead5c75d742cdd1b5a",
+  "lib/puppet/provider/ssh_authorized_key/parsed.rb": "d30bafeb8c266977f59ae5fba0a7872b",
+  "lib/puppet/provider/sshkey/parsed.rb": "49d22c9b64c4d341a20eaecc5ae029e6",
+  "lib/puppet/type/ssh_authorized_key.rb": "01c88ca745f1fc4207827c8d8cedf3a9",
+  "lib/puppet/type/sshkey.rb": "0eb31725dec58f60d98d771742f7b1c5",
+  "locales/config.yaml": "a2bf6ca78a05f7a56058fb79ac9f4eb5",
+  "locales/ja/puppetlabs-sshkeys_core.po": "53239708ecec192c6bd4f15edfe4177a",
+  "locales/puppetlabs-sshkeys_core.pot": "5dbcac178c88d8fa9a82a37e51d1624c",
+  "metadata.json": "6557c88dc1631d790d22ad968d7ccc3f",
+  "readmes/README_ja_JP.md": "0bbf1883434f0d236e2b616dedda134e",
+  "spec/acceptance/nodesets/default.yml": "6508d8ea6b0a00c3725c58dd18e05f5d",
+  "spec/acceptance/tests/resource/ssh_authorized_key/create_spec.rb": "a19f2c289c66e3181ee49090b4f0c184",
+  "spec/acceptance/tests/resource/ssh_authorized_key/destroy_spec.rb": "2bf57394fb1d6312a369e797e4cf6f5c",
+  "spec/acceptance/tests/resource/ssh_authorized_key/modify_spec.rb": "bc98ba0ebbe994c940a38dae0798c0df",
+  "spec/acceptance/tests/resource/sshkey/create_spec.rb": "4e306ff8a334c30703ac7ea25a01a77f",
+  "spec/default_facts.yml": "973bd7e9b429cde3c8d45d57c2fa21ed",
+  "spec/fixtures/integration/provider/sshkey/sample": "50407b47528358f655803fa152377979",
+  "spec/fixtures/unit/provider/sshkey/parsed/sample": "50407b47528358f655803fa152377979",
+  "spec/fixtures/unit/provider/sshkey/parsed/sample_with_blank_lines": "e1d1405df598ac0d8a994ae87ad1afe8",
+  "spec/fixtures/unit/type/user/authorized_keys": "a5bd8a51e666038a8f6969e332cdee66",
+  "spec/integration/provider/ssh_authorized_key_spec.rb": "0ff1ac722907a8e4804e677ef427d013",
+  "spec/integration/provider/sshkey_spec.rb": "9d59dee31da486d8b75782c0b044515c",
+  "spec/integration/type/user_spec.rb": "57ff7af0313eb18b8fdd60ce429f9086",
+  "spec/lib/puppet_spec/compiler.rb": "c1000cfbcb0fbc70f964ddd517d48853",
+  "spec/lib/puppet_spec/files.rb": "c41d5ddfe76b1f1e34498da61a59baa8",
+  "spec/spec_helper.rb": "80a9262fe27b07bccc4cac930a88146e",
+  "spec/spec_helper_acceptance.rb": "04ab9740aa9bd39929675b6c85612ea9",
+  "spec/spec_helper_local.rb": "b8cd2faec0f145710e7ceef3d95978c8",
+  "spec/unit/provider/sshkey/parsed_spec.rb": "ce8a6c6669960109d7cd3bcd84b3ca10",
+  "spec/unit/type/ssh_authorized_key_spec.rb": "faa91dba4dc16b36cba4396636052489",
+  "spec/unit/type/sshkey_spec.rb": "0697f1345dde41d907ee3b1edb904f4f",
+  "spec/unit/type/user_spec.rb": "03e01198feefb3406a809c0c5031871e"
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/sshkeys_core/data/common.yaml	Mon Jan 03 17:15:14 2022 +0000
@@ -0,0 +1,1 @@
+---
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/sshkeys_core/hiera.yaml	Mon Jan 03 17:15:14 2022 +0000
@@ -0,0 +1,21 @@
+---
+version: 5
+
+defaults:  # Used for any hierarchy level that omits these keys.
+  datadir: data         # This path is relative to hiera.yaml's directory.
+  data_hash: yaml_data  # Use the built-in YAML backend.
+
+hierarchy:
+  - name: "osfamily/major release"
+    paths:
+      - "os/%{facts.os.family}/%{facts.os.release.major}.yaml"
+        # Used for Solaris
+      - "os/%{facts.os.family}/%{facts.kernelrelease}.yaml"
+        # Used to distinguish between Debian and Ubuntu
+      - "os/%{facts.os.name}/%{facts.os.release.major}.yaml"
+  - name: "osfamily"
+    paths:
+      - "os/%{facts.os.family}.yaml"
+      - "os/%{facts.os.name}.yaml"
+  - name: 'common'
+    path: 'common.yaml'
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/sshkeys_core/lib/puppet/provider/ssh_authorized_key/parsed.rb	Mon Jan 03 17:15:14 2022 +0000
@@ -0,0 +1,138 @@
+require 'puppet/provider/parsedfile'
+
+Puppet::Type.type(:ssh_authorized_key).provide(
+  :parsed,
+  parent: Puppet::Provider::ParsedFile,
+  filetype: :flat,
+  default_target: '',
+) do
+  desc 'Parse and generate authorized_keys files for SSH.'
+
+  text_line :comment, match: %r{^\s*#}
+  text_line :blank, match: %r{^\s*$}
+
+  record_line :parsed,
+              fields: ['options', 'type', 'key', 'name'],
+              optional: ['options'],
+              rts: %r{^\s+},
+              match: Puppet::Type.type(:ssh_authorized_key).keyline_regex,
+              post_parse: proc { |h|
+                h[:name] = '' if h[:name] == :absent
+                h[:options] ||= [:absent]
+                h[:options] = Puppet::Type::Ssh_authorized_key::ProviderParsed.parse_options(h[:options]) if h[:options].is_a? String
+              },
+              pre_gen: proc { |h|
+                # if this name was generated, don't write it back to disk
+                h[:name] = '' if h[:unnamed]
+                h[:options] = [] if h[:options].include?(:absent)
+                h[:options] = h[:options].join(',')
+              }
+
+  record_line :key_v1,
+              fields: ['options', 'bits', 'exponent', 'modulus', 'name'],
+              optional: ['options'],
+              rts: %r{^\s+},
+              match: %r{^(?:(.+) )?(\d+) (\d+) (\d+)(?: (.+))?$}
+
+  def dir_perm
+    0o700
+  end
+
+  def file_perm
+    0o600
+  end
+
+  def group_writable_perm
+    0o020
+  end
+
+  def group_writable?(path)
+    path.stat.mode & group_writable_perm != 0
+  end
+
+  def trusted_path
+    # return if the parent directory does not exist
+    return false unless Puppet::FileSystem.dir_exist?(target)
+    path = Puppet::FileSystem.pathname(target).dirname
+    until path.dirname.root?
+      path = path.realpath if path.symlink?
+      # do not trust if path is world or group writable
+      if path.stat.uid != Process.euid || path.world_writable? || group_writable?(path)
+        Puppet.debug('Path untrusted, will attempt to write as the target user')
+        return false
+      end
+      path = path.dirname
+    end
+    Puppet.debug('Path trusted, writing the file as the current user')
+  end
+
+  def flush
+    raise Puppet::Error, 'Cannot write SSH authorized keys without user'    unless @resource.should(:user)
+    raise Puppet::Error, "User '#{@resource.should(:user)}' does not exist" unless Puppet::Util.uid(@resource.should(:user))
+    # ParsedFile usually calls backup_target much later in the flush process,
+    # but our SUID makes that fail to open filebucket files for writing.
+    # Fortunately, there's already logic to make sure it only ever happens once,
+    # so calling it here suppresses the later attempt by our superclass's flush method.
+    self.class.backup_target(target)
+
+    # attempt to create the file as the specified user if we're not dropping privileges
+    if @resource[:drop_privileges]
+      Puppet::Util::SUIDManager.asuser(@resource.should(:user)) do
+        unless Puppet::FileSystem.exist?(dir = File.dirname(target))
+          Puppet.debug "Creating #{dir} as #{@resource.should(:user)}"
+          Dir.mkdir(dir, dir_perm)
+        end
+        super
+
+        File.chmod(file_perm, target)
+      end
+    # to avoid race conditions when handling permissions as a privileged user
+    # (CVE-2011-3870) we use the trusted_path method to ensure the entire
+    # directory structure is "safe" to write in
+    else
+      raise Puppet::Error, 'drop_privileges is false but the target path is not trusted' unless trusted_path
+      super
+
+      uid = Puppet::Util.uid(@resource.should(:user))
+      gid = Puppet::Util.gid(@resource.should(:user))
+      File.open(target) do |target|
+        target.chown(uid, gid)
+        target.chmod(file_perm)
+      end
+    end
+  end
+
+  # Parse sshv2 option strings, which is a comma-separated list of
+  # either key="values" elements or bare-word elements
+  def self.parse_options(options)
+    result = []
+    scanner = StringScanner.new(options)
+    until scanner.eos?
+      scanner.skip(%r{[ \t]*})
+      # scan a long option
+      out = scanner.scan(%r{[-a-z0-9A-Z_]+=\".*?[^\\]\"}) || scanner.scan(%r{[-a-z0-9A-Z_]+})
+
+      # found an unscannable token, let's abort
+      break unless out
+
+      result << out
+
+      # eat a comma
+      scanner.skip(%r{[ \t]*,[ \t]*})
+    end
+    result
+  end
+
+  def self.prefetch_hook(records)
+    name_index = 0
+    records.each do |record|
+      next unless record[:record_type] == :parsed && record[:name].empty?
+      record[:unnamed] = true
+      # Generate a unique ID for unnamed keys, in case they need purging.
+      # If you change this, you have to keep
+      # Puppet::Type::User#unknown_keys_in_file in sync! (PUP-3357)
+      record[:name] = "#{record[:target]}:unnamed-#{name_index += 1}"
+      Puppet.debug("generating name for on-disk ssh_authorized_key #{record[:key]}: #{record[:name]}")
+    end
+  end
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/sshkeys_core/lib/puppet/provider/sshkey/parsed.rb	Mon Jan 03 17:15:14 2022 +0000
@@ -0,0 +1,49 @@
+require 'puppet/provider/parsedfile'
+
+Puppet::Type.type(:sshkey).provide(
+  :parsed,
+  parent: Puppet::Provider::ParsedFile,
+  filetype: :flat,
+) do
+  desc 'Parse and generate host-wide known hosts files for SSH.'
+
+  text_line :comment, match: %r{^#}
+  text_line :blank, match: %r{^\s*$}
+
+  record_line :parsed, fields: ['name', 'type', 'key'],
+                       post_parse: proc { |hash|
+                                     names = hash[:name].split(',', -1)
+                                     hash[:name] = names.shift
+                                     hash[:host_aliases] = names
+                                   },
+                       pre_gen: proc { |hash|
+                                  if hash[:host_aliases]
+                                    hash[:name] = [hash[:name], hash[:host_aliases]].flatten.join(',')
+                                    hash.delete(:host_aliases)
+                                  end
+                                }
+
+  # Make sure to use mode 644 if ssh_known_hosts is newly created
+  def self.default_mode
+    0o644
+  end
+
+  def self.default_target
+    case Facter.value(:operatingsystem)
+    when 'Darwin'
+      # Versions 10.11 and up use /etc/ssh/ssh_known_hosts
+      version = Facter.value(:macosx_productversion_major)
+      if version
+        if Puppet::Util::Package.versioncmp(version, '10.11') >= 0
+          '/etc/ssh/ssh_known_hosts'
+        else
+          '/etc/ssh_known_hosts'
+        end
+      else
+        '/etc/ssh_known_hosts'
+      end
+    else
+      '/etc/ssh/ssh_known_hosts'
+    end
+  end
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/sshkeys_core/lib/puppet/type/ssh_authorized_key.rb	Mon Jan 03 17:15:14 2022 +0000
@@ -0,0 +1,167 @@
+require 'puppet/parameter/boolean'
+
+module Puppet
+  Type.newtype(:ssh_authorized_key) do
+    @doc = "Manages SSH authorized keys. Currently only type 2 keys are supported.
+
+      In their native habitat, SSH keys usually appear as a single long line, in
+      the format `<TYPE> <KEY> <NAME/COMMENT>`. This resource type requires you
+      to split that line into several attributes. Thus, a key that appears in
+      your `~/.ssh/id_rsa.pub` file like this...
+
+          ssh-rsa AAAAB3Nza[...]qXfdaQ== nick@magpie.example.com
+
+      ...would translate to the following resource:
+
+          ssh_authorized_key { 'nick@magpie.example.com':
+            ensure => present,
+            user   => 'nick',
+            type   => 'ssh-rsa',
+            key    => 'AAAAB3Nza[...]qXfdaQ==',
+          }
+
+      To ensure that only the currently approved keys are present, you can purge
+      unmanaged SSH keys on a per-user basis. Do this with the `user` resource
+      type's `purge_ssh_keys` attribute:
+
+          user { 'nick':
+            ensure         => present,
+            purge_ssh_keys => true,
+          }
+
+      This will remove any keys in `~/.ssh/authorized_keys` that aren't being
+      managed with `ssh_authorized_key` resources. See the documentation of the
+      `user` type for more details.
+
+      **Autorequires:** If Puppet is managing the user account in which this
+      SSH key should be installed, the `ssh_authorized_key` resource will autorequire
+      that user."
+
+    ensurable
+
+    newparam(:name) do
+      desc "The SSH key comment. This can be anything, and doesn't need to match
+        the original comment from the `.pub` file.
+
+        Due to internal limitations, this must be unique across all user accounts;
+        if you want to specify one key for multiple users, you must use a different
+        comment for each instance."
+
+      isnamevar
+    end
+
+    newparam(:drop_privileges, boolean: true, parent: Puppet::Parameter::Boolean) do
+      desc "Whether to drop privileges when writing the key file. This is
+        useful for creating files in paths not writable by the target user. Note
+        the possible security implications of managing file ownership and
+        permissions as a privileged user."
+
+      defaultto true
+    end
+
+    newproperty(:type) do
+      desc 'The encryption type used.'
+
+      newvalues :'ssh-dss', :'ssh-rsa', :'ecdsa-sha2-nistp256', :'ecdsa-sha2-nistp384', :'ecdsa-sha2-nistp521', :'ssh-ed25519'
+
+      aliasvalue(:dsa, :'ssh-dss')
+      aliasvalue(:ed25519, :'ssh-ed25519')
+      aliasvalue(:rsa, :'ssh-rsa')
+    end
+
+    newproperty(:key) do
+      desc "The public key itself; generally a long string of hex characters. The `key`
+        attribute may not contain whitespace.
+
+        Make sure to omit the following in this attribute (and specify them in
+        other attributes):
+
+        * Key headers, such as 'ssh-rsa' --- put these in the `type` attribute.
+        * Key identifiers / comments, such as 'joe@joescomputer.local' --- put these in
+          the `name` attribute/resource title."
+
+      validate do |value|
+        raise Puppet::Error, _('Key must not contain whitespace: %{value}') % { value: value } if value =~ %r{\s}
+      end
+    end
+
+    newproperty(:user) do
+      desc "The user account in which the SSH key should be installed. The resource
+        will autorequire this user if it is being managed as a `user` resource."
+    end
+
+    newproperty(:target) do
+      desc "The absolute filename in which to store the SSH key. This
+        property is optional and should be used only in cases where keys
+        are stored in a non-standard location, for instance when not in
+        `~user/.ssh/authorized_keys`. The parent directory must be present
+        if the target is in a privileged path."
+
+      defaultto :absent
+
+      def should
+        return super if defined?(@should) && @should[0] != :absent
+
+        return nil unless resource[:user]
+
+        begin
+          return File.expand_path("~#{resource[:user]}/.ssh/authorized_keys")
+        rescue
+          Puppet.debug 'The required user is not yet present on the system'
+          return nil
+        end
+      end
+
+      def insync?(is)
+        is == should
+      end
+    end
+
+    newproperty(:options, array_matching: :all) do
+      desc "Key options; see sshd(8) for possible values. Multiple values
+        should be specified as an array. For example, you could use the
+        following to install a SSH CA that allows someone with the
+        'superuser' principal to log in as root
+
+             ssh_authorized_key { 'Company SSH CA':
+               ensure  => present,
+               user    => 'root',
+               type    => 'ssh-ed25519',
+               key     => 'AAAAC3NzaC[...]CeA5kG',
+               options => [ 'cert-authority', 'principals=\"superuser\"' ],
+             }"
+
+      defaultto { :absent }
+
+      validate do |value|
+        unless value == :absent || value =~ %r{^[-a-z0-9A-Z_]+(?:=\".*?\")?$}
+          raise(
+            Puppet::Error,
+            _("Option %{value} is not valid. A single option must either be of the form 'option' or 'option=\"value\"'. Multiple options must be provided as an array") % { value: value },
+          )
+        end
+      end
+    end
+
+    autorequire(:user) do
+      should(:user) if should(:user)
+    end
+
+    validate do
+      # Go ahead if target attribute is defined
+      return if @parameters[:target].shouldorig[0] != :absent
+
+      # Go ahead if user attribute is defined
+      return if @parameters.include?(:user)
+
+      # If neither target nor user is defined, this is an error
+      raise Puppet::Error, _("Attribute 'user' or 'target' is mandatory")
+    end
+
+    # regular expression suitable for use by a ParsedFile based provider
+    REGEX = %r{^(?:(.+)\s+)?(ssh-dss|ssh-ed25519|ssh-rsa|ecdsa-sha2-nistp256|ecdsa-sha2-nistp384|ecdsa-sha2-nistp521)\s+([^ ]+)\s*(.*)$}
+    def self.keyline_regex
+      REGEX
+    end
+  end
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/sshkeys_core/lib/puppet/type/sshkey.rb	Mon Jan 03 17:15:14 2022 +0000
@@ -0,0 +1,85 @@
+module Puppet
+  Type.newtype(:sshkey) do
+    @doc = "Installs and manages ssh host keys.  By default, this type will
+      install keys into `/etc/ssh/ssh_known_hosts`. To manage ssh keys in a
+      different `known_hosts` file, such as a user's personal `known_hosts`,
+      pass its path to the `target` parameter. See the `ssh_authorized_key`
+      type to manage authorized keys."
+
+    ensurable
+
+    newproperty(:type) do
+      desc 'The encryption type used.  Probably ssh-dss or ssh-rsa.'
+
+      newvalues :'ssh-dss', :'ssh-ed25519', :'ssh-rsa', :'ecdsa-sha2-nistp256', :'ecdsa-sha2-nistp384', :'ecdsa-sha2-nistp521'
+
+      aliasvalue(:dsa, :'ssh-dss')
+      aliasvalue(:ed25519, :'ssh-ed25519')
+      aliasvalue(:rsa, :'ssh-rsa')
+    end
+
+    newproperty(:key) do
+      desc "The key itself; generally a long string of uuencoded characters. The `key`
+        attribute may not contain whitespace.
+
+        Make sure to omit the following in this attribute (and specify them in
+        other attributes):
+
+        * Key headers, such as 'ssh-rsa' --- put these in the `type` attribute.
+        * Key identifiers / comments, such as 'joescomputer.local' --- put these in
+          the `name` attribute/resource title."
+    end
+
+    # FIXME: This should automagically check for aliases to the hosts, just
+    # to see if we can automatically glean any aliases.
+    newproperty(:host_aliases) do
+      desc 'Any aliases the host might have.  Multiple values must be
+        specified as an array.'
+
+      attr_accessor :meta
+
+      def insync?(is)
+        is == @should
+      end
+
+      # We actually want to return the whole array here, not just the first
+      # value.
+      def should
+        defined?(@should) ? @should : nil
+      end
+
+      validate do |value|
+        if value =~ %r{\s}
+          raise Puppet::Error, _('Aliases cannot include whitespace')
+        end
+        if value =~ %r{,}
+          raise Puppet::Error, _('Aliases must be provided as an array, not a comma-separated list')
+        end
+      end
+    end
+
+    newparam(:name) do
+      desc 'The host name that the key is associated with.'
+
+      isnamevar
+
+      validate do |value|
+        raise Puppet::Error, _('Resourcename cannot include whitespaces') if value =~ %r{\s}
+        raise Puppet::Error, _('No comma in resourcename allowed. If you want to specify aliases use the host_aliases property') if value.include?(',')
+      end
+    end
+
+    newproperty(:target) do
+      desc "The file in which to store the ssh key.  Only used by
+        the `parsed` provider."
+
+      defaultto do
+        if @resource.class.defaultprovider.ancestors.include?(Puppet::Provider::ParsedFile)
+          @resource.class.defaultprovider.default_target
+        else
+          nil
+        end
+      end
+    end
+  end
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/sshkeys_core/locales/config.yaml	Mon Jan 03 17:15:14 2022 +0000
@@ -0,0 +1,24 @@
+# This is the project-specific configuration file for setting up
+# fast_gettext for your project.
+gettext:
+  # This is used for the name of the .pot and .po files; they will be
+  # called <project_name>.pot?
+  project_name: puppetlabs-sshkeys_core
+  # This is used in comments in the .pot and .po files to indicate what
+  # project the files belong to and should bea little more desctiptive than
+  # <project_name>
+  package_name: puppetlabs-sshkeys_core
+  # The locale that the default messages in the .pot file are in
+  default_locale: en
+  # The email used for sending bug reports.
+  bugs_address: docs@puppet.com
+  # The holder of the copyright.
+  copyright_holder: Puppet, Inc.
+  # This determines which comments in code should be eligible for translation.
+  # Any comments that start with this string will be externalized. (Leave
+  # empty to include all.)
+  comments_tag: TRANSLATOR
+  # Patterns for +Dir.glob+ used to find all files that might contain
+  # translatable content, relative to the project root directory
+  source_files:
+      - './lib/**/*.rb'
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/sshkeys_core/locales/ja/puppetlabs-sshkeys_core.po	Mon Jan 03 17:15:14 2022 +0000
@@ -0,0 +1,56 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2018 Puppet, Inc.
+# This file is distributed under the same license as the puppetlabs-sshkeys_core package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2018.
+# 
+# Translators:
+# Erwin Hom <erwin.hom@puppet.com>, 2018
+# 
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: puppetlabs-sshkeys_core 1.0.0-8-g39a613b\n"
+"\n"
+"Report-Msgid-Bugs-To: docs@puppet.com\n"
+"POT-Creation-Date: 2018-08-17 15:12-0700\n"
+"PO-Revision-Date: 2018-10-24 00:48+0000\n"
+"Last-Translator: Erwin Hom <erwin.hom@puppet.com>, 2018\n"
+"Language-Team: Japanese (Japan) (https://www.transifex.com/puppet/teams/41915/ja_JP/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: ja_JP\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: ../lib/puppet/type/ssh_authorized_key.rb:73
+msgid "Key must not contain whitespace: %{value}"
+msgstr "キーに空白を含めることはできません: %{value}"
+
+#: ../lib/puppet/type/ssh_authorized_key.rb:118
+msgid ""
+"Option %{value} is not valid. A single option must either be of the form "
+"'option' or 'option=\"value\". Multiple options must be provided as an array"
+msgstr ""
+"オプション%{value}は有効ではありません。単一オプションの場合は、'option'または'option=\"value\"のいずれかの形にする必要があります。複数オプションの場合は、配列として指定する必要があります。"
+
+#: ../lib/puppet/type/ssh_authorized_key.rb:136
+msgid "Attribute 'user' or 'target' is mandatory"
+msgstr "属性'user'または'target'は必須です。"
+
+#: ../lib/puppet/type/sshkey.rb:53
+msgid "Aliases cannot include whitespace"
+msgstr "別名に空白を含めることはできません。"
+
+#: ../lib/puppet/type/sshkey.rb:56
+msgid "Aliases must be provided as an array, not a comma-separated list"
+msgstr "別名はカンマ区切りリストではなく、配列として指定する必要があります。"
+
+#: ../lib/puppet/type/sshkey.rb:67
+msgid "Resourcename cannot include whitespaces"
+msgstr "リソース名に空白を含めることはできません。"
+
+#: ../lib/puppet/type/sshkey.rb:68
+msgid ""
+"No comma in resourcename allowed. If you want to specify aliases use the "
+"host_aliases property"
+msgstr "リソース名にカンマは使えません。別名を指定したい場合は、host_aliasesプロパティを使用してください。"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/sshkeys_core/locales/puppetlabs-sshkeys_core.pot	Mon Jan 03 17:15:14 2022 +0000
@@ -0,0 +1,47 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2018 Puppet, Inc.
+# This file is distributed under the same license as the puppetlabs-sshkeys_core package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2018.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: puppetlabs-sshkeys_core \n"
+"Report-Msgid-Bugs-To: docs@puppet.com\n"
+"POT-Creation-Date: 2018-10-31 16:52+0000\n"
+"PO-Revision-Date: 2018-10-31 16:52+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
+
+#: ../lib/puppet/type/ssh_authorized_key.rb:73
+msgid "Key must not contain whitespace: %{value}"
+msgstr ""
+
+#: ../lib/puppet/type/ssh_authorized_key.rb:118
+msgid "Option %{value} is not valid. A single option must either be of the form 'option' or 'option=\"value\"'. Multiple options must be provided as an array"
+msgstr ""
+
+#: ../lib/puppet/type/ssh_authorized_key.rb:136
+msgid "Attribute 'user' or 'target' is mandatory"
+msgstr ""
+
+#: ../lib/puppet/type/sshkey.rb:53
+msgid "Aliases cannot include whitespace"
+msgstr ""
+
+#: ../lib/puppet/type/sshkey.rb:56
+msgid "Aliases must be provided as an array, not a comma-separated list"
+msgstr ""
+
+#: ../lib/puppet/type/sshkey.rb:67
+msgid "Resourcename cannot include whitespaces"
+msgstr ""
+
+#: ../lib/puppet/type/sshkey.rb:68
+msgid "No comma in resourcename allowed. If you want to specify aliases use the host_aliases property"
+msgstr ""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/sshkeys_core/metadata.json	Mon Jan 03 17:15:14 2022 +0000
@@ -0,0 +1,93 @@
+{
+  "name": "puppetlabs-sshkeys_core",
+  "version": "1.0.3",
+  "author": "puppetlabs",
+  "summary": "Manage SSH authorized keys, and known hosts.",
+  "license": "Apache-2.0",
+  "source": "https://github.com/puppetlabs/puppetlabs-sshkeys_core",
+  "project_page": "https://github.com/puppetlabs/puppetlabs-sshkeys_core",
+  "issues_url": "https://tickets.puppetlabs.com/project/MODULES",
+  "dependencies": [
+  
+  ],
+  "data_provider": null,
+  "operatingsystem_support": [
+    {
+      "operatingsystem": "CentOS",
+      "operatingsystemrelease": [
+        "7"
+      ]
+    },
+    {
+      "operatingsystem": "OracleLinux",
+      "operatingsystemrelease": [
+        "7"
+      ]
+    },
+    {
+      "operatingsystem": "RedHat",
+      "operatingsystemrelease": [
+        "7"
+      ]
+    },
+    {
+      "operatingsystem": "Scientific",
+      "operatingsystemrelease": [
+        "7"
+      ]
+    },
+    {
+      "operatingsystem": "Debian",
+      "operatingsystemrelease": [
+        "8"
+      ]
+    },
+    {
+      "operatingsystem": "Ubuntu",
+      "operatingsystemrelease": [
+        "16.04"
+      ]
+    },
+    {
+      "operatingsystem": "Fedora",
+      "operatingsystemrelease": [
+        "25"
+      ]
+    },
+    {
+      "operatingsystem": "Darwin",
+      "operatingsystemrelease": [
+        "16"
+      ]
+    },
+    {
+      "operatingsystem": "SLES",
+      "operatingsystemrelease": [
+        "12"
+      ]
+    },
+    {
+      "operatingsystem": "Solaris",
+      "operatingsystemrelease": [
+        "11"
+      ]
+    },
+    {
+      "operatingsystem": "windows",
+      "operatingsystemrelease": [
+        "2008 R2",
+        "2012 R2",
+        "10"
+      ]
+    }
+  ],
+  "requirements": [
+    {
+      "name": "puppet",
+      "version_requirement": ">= 6.0.0 < 7.0.0"
+    }
+  ],
+  "pdk-version": "1.14.0",
+  "template-url": "https://github.com/puppetlabs/pdk-templates#1.14.0",
+  "template-ref": "1.14.0-0-g1bf3a4e"
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/sshkeys_core/readmes/README_ja_JP.md	Mon Jan 03 17:15:14 2022 +0000
@@ -0,0 +1,62 @@
+
+# sshkeys_core
+
+## 目次
+
+1. [説明](#description)
+2. [使用 - 設定オプションと追加機能](#usage)
+3. [リファレンス - ユーザマニュアル](#reference)
+4. [開発 - モジュール貢献についてのガイド](#development)
+
+<a id="description"></a>
+## 説明
+
+SSH `authorized_keys`、および`ssh_known_hosts`ファイルを管理します。
+
+<a id="usage"></a>
+## 使用
+
+ユーザの認証されたキーを管理するには、以下のコードを使用します。
+
+```
+ssh_authorized_key { 'nick@magpie.example.com':
+  ensure => present,
+  user   => 'nick',
+  type   => 'ssh-rsa',
+  key    => 'AAAAB3Nza[...]qXfdaQ==',
+}
+```
+
+既知のホストファイルのエントリを管理するには、以下のコードを使用します。
+
+```
+sshkey { 'github.com':
+  ensure => present,
+  type   => 'ssh-rsa',
+  key    => 'AAAAB3Nza[...]UFFAaQ==',
+}
+```
+<a id="reference"></a>
+## リファレンス
+
+リファレンス文書については、REFERENCE.mdを参照してください。
+
+このモジュールは、Puppet Stringsを用いて文書化されています。
+
+Stringsの仕組みの簡単な概要については、Puppet Stringsに関する[こちらのブログ記事](https://puppet.com/blog/using-puppet-strings-generate-great-documentation-puppet-modules)または[README.md](https://github.com/puppetlabs/puppet-strings/blob/master/README.md)を参照してください。
+
+文書をローカルで作成するには、以下のコマンドを実行します。
+```
+bundle install
+bundle exec puppet strings generate ./lib/**/*.rb
+```
+このコマンドにより、閲覧可能な`\_index.html`ファイルが`doc`ディレクトリに作成されます。ここで利用可能なリファレンスはすべて、コードベースに埋め込まれたYARD形式のコメントから生成されます。このモジュールに関して何らかの開発をする場合は、影響を受ける文書も更新する必要があります。
+
+<a id="development"></a>
+## 開発
+
+Puppet ForgeのPuppet Labsモジュールは、オープンプロジェクトです。プロジェクトをさらに発展させるには、コミュニティへの貢献が不可欠です。Puppetが役立つ可能性のある膨大な数のプラットフォーム、無数のハードウェア、ソフトウェア、デプロイメント構成に我々がアクセスすることはできません。
+
+弊社は、できるだけ変更に貢献しやすくして、弊社のモジュールがユーザの環境で機能する状態を維持したいと考えています。弊社では、状況を把握できるよう、貢献者に従っていただくべきいくつかのガイドラインを設けています。
+
+詳細については、[モジュール貢献ガイド](https://docs.puppetlabs.com/forge/contributing.html)を参照してください。
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/sshkeys_core/spec/acceptance/nodesets/default.yml	Mon Jan 03 17:15:14 2022 +0000
@@ -0,0 +1,19 @@
+---
+HOSTS:
+  ubuntu1604-64-1:
+    pe_dir:
+    pe_ver:
+    pe_upgrade_dir:
+    pe_upgrade_ver:
+    hypervisor: vmpooler
+    platform: ubuntu-16.04-amd64
+    packaging_platform: ubuntu-16.04-amd64
+    template: ubuntu-1604-x86_64
+    roles:
+      - agent
+      - default
+CONFIG:
+  type: agent
+  nfs_server: none
+  consoleport: 443
+pooling_api: http://vmpooler.delivery.puppetlabs.net/
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/sshkeys_core/spec/acceptance/tests/resource/ssh_authorized_key/create_spec.rb	Mon Jan 03 17:15:14 2022 +0000
@@ -0,0 +1,88 @@
+require 'spec_helper_acceptance'
+
+RSpec.context 'ssh_authorized_key: Create' do
+  test_name 'should create an entry for an SSH authorized key'
+
+  let(:auth_keys) { '~/.ssh/authorized_keys' }
+  let(:name) { "pl#{rand(999_999).to_i}" }
+  let(:custom_key_directory) { "/etc/ssh_authorized_keys_#{name}" }
+  let(:custom_key) { "#{custom_key_directory}/authorized_keys_#{name}" }
+
+  before(:each) do
+    posix_agents.each do |agent|
+      on(agent, "cp -a #{auth_keys} /tmp/auth_keys", acceptable_exit_codes: [0, 1])
+      on(agent, "rm -f #{auth_keys}")
+    end
+  end
+
+  after(:each) do
+    posix_agents.each do |agent|
+      # (teardown) restore the #{auth_keys} file
+      on(agent, "mv /tmp/auth_keys #{auth_keys}", acceptable_exit_codes: [0, 1])
+    end
+  end
+
+  posix_agents.each do |agent|
+    it "#{agent} should create an entry for an SSH authorized key" do
+      args = ['ensure=present',
+              'user=$LOGNAME',
+              "type='rsa'",
+              "key='mykey'"]
+      on(agent, puppet_resource('ssh_authorized_key', name.to_s, args))
+
+      on(agent, "cat #{auth_keys}") do |_res|
+        fail_test "didn't find the ssh_authorized_key for #{name}" unless stdout.include? name.to_s
+      end
+    end
+
+    it "#{agent} should create an entry for an SSH authorized key in a custom location" do
+      on(agent, "mkdir #{custom_key_directory}")
+      args = ['ensure=present',
+              'user=$LOGNAME',
+              "type='rsa'",
+              "key='mykey'",
+              "target='#{custom_key}'"]
+      on(agent, puppet_resource('ssh_authorized_key', name.to_s, args))
+
+      on(agent, "cat #{custom_key}") do |_res|
+        fail_test "didn't find the ssh_authorized_key for #{name}" unless stdout.include? name.to_s
+      end
+      on(agent, "rm -rf #{custom_key_directory}")
+    end
+
+    it "#{agent} should fail if target user doesn't have permissions for symlinked path" do
+      # create a dummy user
+      on(agent, puppet_resource('user', 'testuser', 'ensure=present', 'managehome=true'))
+
+      on(agent, "mkdir #{custom_key_directory}")
+
+      # as the user, symlink an owned directory to something inside /root
+      on(agent, puppet_resource('file', '/home/testuser/tmp', ['ensure=/etc', 'owner=testuser']))
+      args = ['ensure=present',
+              'user=testuser',
+              "type='rsa'",
+              "key='mykey'",
+              'drop_privileges=false',
+              "target=/home/testuser/tmp/ssh_authorized_keys_#{name}/authorized_keys_#{name}"]
+      on(agent, puppet_resource('ssh_authorized_key', name.to_s, args)) do |_res|
+        fail_test unless stderr =~ %r{the target path is not trusted}
+      end
+      on(agent, "rm -rf #{custom_key_directory}")
+
+      # purge the user
+      on(agent, puppet_resource('user', 'testuser', 'ensure=absent'))
+    end
+
+    it "#{agent} should not create directories for SSH authorized key in a custom location" do
+      args = ['ensure=present',
+              'user=$LOGNAME',
+              "type='rsa'",
+              "key='mykey'",
+              'drop_privileges=false',
+              "target='#{custom_key}'"]
+      on(agent, puppet_resource('ssh_authorized_key', name.to_s, args), acceptable_exit_codes: [0, 1]) do |_res|
+        fail_test unless stderr =~ %r{the target path is not trusted}
+      end
+    end
+  end
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/sshkeys_core/spec/acceptance/tests/resource/ssh_authorized_key/destroy_spec.rb	Mon Jan 03 17:15:14 2022 +0000
@@ -0,0 +1,55 @@
+require 'spec_helper_acceptance'
+
+RSpec.context 'sshkeys: Destroy' do
+  confine :except, platform: ['windows']
+
+  let(:auth_keys) { '~/.ssh/authorized_keys' }
+  let(:name) { "pl#{rand(999_999).to_i}" }
+  let(:custom_key_directory) { "/etc/ssh_authorized_keys_#{name}" }
+  let(:custom_key) { "#{custom_key_directory}/authorized_keys_#{name}" }
+
+  before(:each) do
+    posix_agents.each do |agent|
+      on(agent, "cp -a #{auth_keys} /tmp/auth_keys", acceptable_exit_codes: [0, 1])
+      on(agent, "rm -f #{auth_keys}")
+      on(agent, "echo '' >> #{auth_keys} && echo 'ssh-rsa mykey #{name}' >> #{auth_keys}")
+    end
+  end
+
+  after(:each) do
+    posix_agents.each do |agent|
+      # (teardown) restore the #{auth_keys} file
+      on(agent, "mv /tmp/auth_keys #{auth_keys}", acceptable_exit_codes: [0, 1])
+    end
+  end
+
+  posix_agents.each do |agent|
+    it "#{agent} should delete an entry for an SSH authorized key" do
+      args = ['ensure=absent',
+              'user=$LOGNAME',
+              "type='rsa'",
+              "key='mykey'"]
+      on(agent, puppet_resource('ssh_authorized_key', name.to_s, args))
+
+      on(agent, "cat #{auth_keys}") do |_res|
+        expect(stdout).not_to include(name.to_s)
+      end
+    end
+
+    it "#{agent} should delete an entry for an SSH authorized key in a custom location" do
+      on(agent, "mkdir #{custom_key_directory}")
+      on(agent, "echo '' >> #{custom_key} && echo 'ssh-rsa mykey #{name}' >> #{custom_key}")
+      args = ['ensure=absent',
+              'user=$LOGNAME',
+              "type='rsa'",
+              "key='mykey'",
+              "target='#{custom_key}'"]
+      on(agent, puppet_resource('ssh_authorized_key', name.to_s, args))
+
+      on(agent, "cat #{custom_key}") do |_res|
+        expect(stdout).not_to include(name.to_s)
+      end
+      on(agent, "rm -rf #{custom_key_directory}")
+    end
+  end
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/sshkeys_core/spec/acceptance/tests/resource/ssh_authorized_key/modify_spec.rb	Mon Jan 03 17:15:14 2022 +0000
@@ -0,0 +1,55 @@
+require 'spec_helper_acceptance'
+
+RSpec.context 'sshkeys: Modify' do
+  let(:auth_keys) { '~/.ssh/authorized_keys' }
+  let(:name) { "pl#{rand(999_999).to_i}" }
+  let(:custom_key_directory) { "/etc/ssh_authorized_keys_#{name}" }
+  let(:custom_key) { "#{custom_key_directory}/authorized_keys_#{name}" }
+
+  before(:each) do
+    posix_agents.each do |agent|
+      on(agent, "cp -a #{auth_keys} /tmp/auth_keys", acceptable_exit_codes: [0, 1])
+      on(agent, "rm -f #{auth_keys}")
+      on(agent, "echo '' >> #{auth_keys} && echo 'ssh-rsa mykey #{name}' >> #{auth_keys}")
+    end
+  end
+
+  after(:each) do
+    posix_agents.each do |agent|
+      # (teardown) restore the #{auth_keys} file
+      on(agent, "mv /tmp/auth_keys #{auth_keys}", acceptable_exit_codes: [0, 1])
+    end
+  end
+
+  posix_agents.each do |agent|
+    it "#{agent} should update an entry for an SSH authorized key" do
+      args = ['ensure=present',
+              'user=$LOGNAME',
+              "type='rsa'",
+              "key='mynewshinykey'"]
+      on(agent, puppet_resource('ssh_authorized_key', name.to_s, args))
+
+      on(agent, "cat #{auth_keys}") do |_res|
+        expect(stdout).to include("mynewshinykey #{name}")
+        expect(stdout).not_to include("mykey #{name}")
+      end
+    end
+
+    it "#{agent} should update an entry for an SSH authorized key in a custom location" do
+      on(agent, "mkdir #{custom_key_directory}")
+      on(agent, "echo '' >> #{custom_key} && echo 'ssh-rsa mykey #{name}' >> #{custom_key}")
+      args = ['ensure=present',
+              'user=$LOGNAME',
+              "type='rsa'",
+              "key='mynewshinykey'",
+              "target='#{custom_key}'"]
+      on(agent, puppet_resource('ssh_authorized_key', name.to_s, args))
+
+      on(agent, "cat #{custom_key}") do |_res|
+        expect(stdout).to include("mynewshinykey #{name}")
+        expect(stdout).not_to include("mykey #{name}")
+      end
+      on(agent, "rm -rf #{custom_key_directory}")
+    end
+  end
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/sshkeys_core/spec/acceptance/tests/resource/sshkey/create_spec.rb	Mon Jan 03 17:15:14 2022 +0000
@@ -0,0 +1,81 @@
+require 'spec_helper_acceptance'
+
+RSpec.context 'sshkeys: Create' do
+  let(:keyname) { "pl#{rand(999_999).to_i}" }
+
+  # FIXME: This is bletcherous
+  let(:macos_version) { fact_on(agent, 'os.macosx.version.major') }
+  let(:ssh_known_hosts) do
+    if ['10.9', '10.10'].include? macos_version
+      '/etc/ssh_known_hosts'
+    else
+      '/etc/ssh/ssh_known_hosts'
+    end
+  end
+
+  before(:each) do
+    osx_agents.each do |agent|
+      # The 'cp' might fail because the source file doesn't exist
+      on(
+        agent,
+        "cp -fv #{ssh_known_hosts} /tmp/ssh_known_hosts",
+        acceptable_exit_codes: [0, 1],
+      )
+    end
+  end
+
+  after(:each) do
+    osx_agents.each do |agent|
+      # Is it present?
+      rc = on(
+        agent,
+        '[ -e /tmp/ssh_known_hosts ]',
+        accept_all_exit_codes: true,
+      )
+      if rc.exit_code == 0
+        # It's present, so restore the original
+        on(
+          agent,
+          "mv -fv /tmp/ssh_known_hosts #{ssh_known_hosts}",
+          accept_all_exit_codes: true,
+        )
+      else
+        # It's missing, which means there wasn't one to backup; just
+        # delete the one we laid down
+        on(
+          agent,
+          "rm -fv #{ssh_known_hosts}",
+          accept_all_exit_codes: true,
+        )
+      end
+    end
+  end
+
+  osx_agents.each do |agent|
+    it "#{agent} should add an SSH key to the correct ssh_known_hosts file on OS X/macOS (PUP-5508)" do
+      # Is it even there?
+      rc = on(
+        agent,
+        "[ ! -e #{ssh_known_hosts} ]",
+        acceptable_exit_codes: [0, 1],
+      )
+      if rc.exit_code == 1
+        # If it's there, it should be empty
+        on(agent, "cat #{ssh_known_hosts}") do |_res|
+          expect(stdout).to be_empty
+        end
+      end
+
+      args = [
+        'ensure=present',
+        'key=how_about_the_key_of_c',
+        'type=ssh-rsa',
+      ]
+      on(agent, puppet_resource('sshkey', keyname.to_s, args))
+
+      on(agent, "cat #{ssh_known_hosts}") do |_rc|
+        expect(stdout).to include(keyname.to_s)
+      end
+    end
+  end
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/sshkeys_core/spec/default_facts.yml	Mon Jan 03 17:15:14 2022 +0000
@@ -0,0 +1,8 @@
+# Use default_module_facts.yml for module specific facts.
+#
+# Facts specified here will override the values provided by rspec-puppet-facts.
+---
+ipaddress: "172.16.254.254"
+ipaddress6: "FE80:0000:0000:0000:AAAA:AAAA:AAAA"
+is_pe: false
+macaddress: "AA:AA:AA:AA:AA:AA"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/sshkeys_core/spec/fixtures/integration/provider/sshkey/sample	Mon Jan 03 17:15:14 2022 +0000
@@ -0,0 +1,21 @@
+hosting2.planetargon.com,64.34.164.77 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAy+f2t52cDMrYkgEKQ6juqfMf/a0nDFry3JAzl+SAWQ0gTklVxNcVbfHx2pkZk66EBGQfrK33Bx1BflZ/iEDyiCwmzVtNba0X9A6ELYjB9WSkWdIqZCfPlKZMu9N//aZ6+3SDVuz/BVFsAVmtqQ4Let2QjOFiSIKXrtPqWvVT/MM=
+kirby.madstop.com,192.168.0.5 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAw9iHuAa/wepHoUzWqsvhQvSkpE4K7agrdLOWHM9mvyRQ2X3HVq5GqzAvWu4J+f0FvcLPwA9tivpxt1oSt5MOtvDM6HoM+8m3P4daBp0nlNaYR8/vHCAmX6N3RyM8FWfp+VqWyux1SooQwxYxVFy86G78ApTqNsZ+p7cHmnBYqk0=
+fedora1,192.168.0.51 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAyz1rKcApU4//j8CHYKexq4qnq2WVqLPrZYGnlij1t7FscLiDVKvBuRHVkfyTNIjAM/t7tM1Dj+FuD4iWziCmf7RO9q4wI5y/1zgCiSUegnZVSmH2yxnWGMdHGpXOkN3NXcpy6jylxyBo0M7T22PSezCxyUVfMclesjOEO1jETd0=
+kirby ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAw9iHuAa/wepHoUzWqsvhQvSkpE4K7agrdLOWHM9mvyRQ2X3HVq5GqzAvWu4J+f0FvcLPwA9tivpxt1oSt5MOtvDM6HoM+8m3P4daBp0nlNaYR8/vHCAmX6N3RyM8FWfp+VqWyux1SooQwxYxVFy86G78ApTqNsZ+p7cHmnBYqk0=
+sol10b,192.168.0.50 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAs37kiDwKxWqi6EfSdKwRaZXBwh2doOARRqZzyitBaPwESy26DwTx+xdQ2rwB4V2k1WIec+1f3bgTS2ArH75dQSPyba2HKqxaSRBd3Zh4z23+uUxpupEyoRdW1HolMOvuoceheSMsruiuYcuiyct41d4c/Qmr51Dv04Doi00k6Ws=
+piratehaven.org,64.81.59.88 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEA7TYRRkle0wDNohZ0TNZN6R1Zp0svxgX+GJ9umI5yNM1bMxUTgeNRh5nIvZg1HgD1WRXQ57dSxxLzbvRyAqc245g6S8eWWCtenvOFLl0rOF5D3VxbQuw79sOe8/Ac8TC+c8RuWB7aaxpwL5Rv9xfDeazOtoKXj7+uwQW1PUmTaEM=
+atalanta,192.168.0.10 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAysniuWiJH6OQLXl63XXcS1b/hP2lAgSz0IutjQ6ZUfBrt1BZ8udEgSh57w5QDLsZ1lNvND61u5cy6iDKXI5TIQY4DvUmsoFZhyr4iYJbtT/h6UJSyaZtEnA7ZMRjGhUIMOn+mjbj7Z3zmJMhxtImK3Xo3O2fJ1hnK4jsBwQPSLc=
+pixie,192.168.0.9 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAzDp588CvET6w11LB2s/vPjc4tX9+u46iYJgNFfhzxrXYMVv4GF7d30IXB5+Hwyi2FOQIG1+h0kUXVGWEv64rAFBT7pD2KMFX0lcDERV4avqT0TRDIIA5OqFOhq9Ff+kOmHS2cB7eFyR5vqbN4ujOnJGTmru9dcqyL+2AcFekvh0=
+culain,192.168.0.3 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAvJ/IORhQMdAsJ7LB1sI2LlWUHc7HPTCDiEgJ96ij3jFvqaIiYieRFaNkxbbk75mPkj3vIqWIgAsAtHmKX4wDikNG/gyjs4WM4cWFKtl2jiVhqpoxqqCaVxs6Ex+vpKuKhQR6SzFBFDlBZYP9an6DPu1msTLT8/hZH2WwswVmpyU=
+cs312-server.een.orst.edu,128.193.40.66 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEA+t3hsOiyp1mztt013bLdJqIAFCo8lxlz86MYEPz/mADHzWLs3Xv7xpAUv/E8pRbhEOzXo84EddORRBXz6DgVyMah8+v/u3IOkpXuZI0Iu1n5hZyf2l5DGEyGecr3oqqjUdMuM9HeXFLnqXJI3hDE7civBtqf5AJSol+TCcipeE8=
+freebsd1,192.168.0.52 ssh-dss AAAAB3NzaC1kc3MAAACBAJSiOyQhYlKAi0FDLKy42VzLDq6yJWXGXVCLSfgWyVx7QCq/3+W3C1dtHuAvjbypcjqqvsuGGITgQ1Y6B/+76n5d7FyQnj4SFZ5drOBn/TvslXhrS/Ok5KCcndfNAa+EyMnSZJ21jhoRjZftY4lmb4hy6fEF3RvjuOdf1qBN5FWpAAAAFQDcsWF0zELAW6YUlSjAyO0T0lfPbwAAAIAlOLdOd/WszzVaplCOnH5vF6LWfr6BosZKDkFi0mv6Ec636YGaj4AMxK8sRPusHv6sVByN17ntIJnLo2XD1SuoH28bZ0ZnPIdVnd0l1KqsOCuuow9LZYJUihezoUuYuTvij1jZdrwltuPNJTVLYtsZDnKE5plw+Tjzeb7ImjbXGwAAAIBT40olg3fxhRDiZECle0WK7GitgXCB3njs+4dba8VwveEJb9UuulMc1eR+zQiJR96IUBagC9NiLvUGS1IfiIHpT4FA8O/MK1W9SgxXB9d39Nk/9l8dH3U/fLnbC/hYVo8bN0or/mKxcxQMkdBwpPlWAbELRftod2BkkkvgfQdc+g==
+192.168.0.2 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgwCZ+qxpMMRJ3otGsjaYeKTKf6tuCZyK1cD+ns9Eu7V0ZJLJ/LLMxduu7n4H/ufGI5rGV5axzgx8yZhjDRzsrGjLAQYsqlomMkf901YQI6UuieSA4MZa5MDkq/Jt6Vx1kEGTpkgrfw9kRMX5BngECt1QKY4xTgC7Ex+WlFvZwk+tRUT3
+openbsd1,192.168.0.54 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAvgRVrtJk0fBg9YsLf7rWR1X32ZjFcva5XBvenDlHruObaHzuGXyyr6iOCAEOc7eCZjlPBYrGZ2potqyk8HlBOHXr1cCBf49t4yAt8KgKswtzWlgdbU1UEwllSRVOpzqULAT0smv/jfaIZdvRoN1rLriqtpn4bQL//ZOHiyXCwdlJ+di8Mza2L8KZ5T75hwIFBhrgL12xfymRp3v+Iy21MjjzsF3pROIkZ7icitNqQF55U9vsHaA37vG8FepVkO10bYYP7IHPZaBPBMPx7qPyRgRDJahEUGBnkIkzwJHEmA+7YMiNTZu6MigezD+CIqY1xOi/eXZwObLwLKXo7eRmpw==
+192.168.0.60 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAufcvE7s1eRwhUwMBfZ6uFNxkdSdzjSDEn3vjByOjG/eraNhnYAW3rxV7WIf2pEa6JSOMrE1mqsEL75xEtpXlzC949Ysz4+1OSHY52KonoFm/a+FbmbFp81TVuVPYaLoeWN27STiJh+puC5spkIZe0laqT1GU13M4gj6B+j3NLhU=
+rh3a,192.168.0.55 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAufcvE7s1eRwhUwMBfZ6uFNxkdSdzjSDEn3vjByOjG/eraNhnYAW3rxV7WIf2pEa6JSOMrE1mqsEL75xEtpXlzC949Ysz4+1OSHY52KonoFm/a+FbmbFp81TVuVPYaLoeWN27STiJh+puC5spkIZe0laqT1GU13M4gj6B+j3NLhU=
+tsetse,192.168.0.7 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAwCDGn82BEMSCfcE2LZKcwdAoyIBC+u2twVwWNRm3KzyrJMQ+RbTQo4AwGOdyr/QYh6KaxTKTSoDtiBLr132uenMQKwF47gCKMA1T47uiy+TBUehgOGwOxteSYP/pQRpKXFmhOppSPyDPQVq234XvANeJp0iT8ZKEhF2FsWTs6sM=
+config.sage.org,131.106.3.205 ssh-dss AAAAB3NzaC1kc3MAAACBAL2akEcIfQsfm3zCd2hD6PgH3kWA/tqX/qbrLhL/ipX7iqK/y282GMClZQSQjc1YNi9virvLzb6u/gdZxicZ8K5O3FaJrULQJOZaP62SOHk5CUSHVnvpMCaCnbwB6gEHa2LeMWStcEfWW+g1CC2hzPJw16/S5GISGXbyanO02MnXAAAAFQDomwx/OCjTmmQljMTU5rgNn2E4gwAAAIBmtMSfcs2Tq5iFFKK5cahluv047vVNfXqYIAkeJniceL0Et16MKfuzadpq0H9ocxQYW/5Ir9nUULrdxBUN9LxNKq15/uWkJC9QCSh8PysgvFnjVZeCODua/dn6eZTZnY9DZ3S6v1pT8CP6uWr5fmZJ8FKJGrC3gYX4y1V1ZTCVewAAAIB6e7RCST6vkTS5rgn5wGbrfLK5ad+PW+2i66k77Zv1pjtfRz+0oejBjwJDPNVSc2YNEl7X0nEEMNjo/a5x8Ls+nVqhzJA+NXIwS1e/moKbXFGewW5HAtxd79gtInC8dEIO7hmnWnqF1dBkRHXg1YffYkHrMVJBxpzagw7nYa0BBQ==
+rh3b,192.168.0.56 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAufcvE7s1eRwhUwMBfZ6uFNxkdSdzjSDEn3vjByOjG/eraNhnYAW3rxV7WIf2pEa6JSOMrE1mqsEL75xEtpXlzC949Ysz4+1OSHY52KonoFm/a+FbmbFp81TVuVPYaLoeWN27STiJh+puC5spkIZe0laqT1GU13M4gj6B+j3NLhU=
+centos1,192.168.0.57 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEA0DXqYF+3Lf68GkWBAjjKBb6UITNnzm4wiDi/AGjv5+DoVXqDcqHvZ8rZFAMgUe1dVob4pWT2ZWLHW0gicoJCdr4UQbPXlWz1F62z8fo2PRRPlG6KN1wmF7pnyml8jr0wBX8lQZJsMqi4InGozf7wFHLH/7DNGRK3MD6tSp3Z4is=
+doorstop.cafes.net,205.241.238.186 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEApJKeB9/bN5t55zLETHs0MVo/vEkfQ3EzY7178GKLI/yiOFmcn+NvUvUtCQK/xKpod813LBHCODxZPG1Kb0SjlaC/EkFEenb74LNu0o1qXa1GWh3wfaIm0JRNjXqPqxAWTlMs43O2HXwOwmLVhl7SSP3xtTw6h9gREbVKmfBaRdsRfzD0etfz1NCnmGh/1Sh9+j4eeS+IBtwoR5JVhZVhuofHCqs5HZ8gLDgfn8HXP7pMbLkx54cf1R/tmFmn9JGLdTPtEGcSIiu7414XSbfChSC83rGZCDPKHq7ZodiE8GpbWLBnyPXi2AYxTPM7aZMqitIHv3MWf5suV0q0WLGdnQ==
+host.domain.com,host1.domain.com,192.168.0.1 dss thisismykey1
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/sshkeys_core/spec/fixtures/unit/provider/sshkey/parsed/sample	Mon Jan 03 17:15:14 2022 +0000
@@ -0,0 +1,21 @@
+hosting2.planetargon.com,64.34.164.77 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAy+f2t52cDMrYkgEKQ6juqfMf/a0nDFry3JAzl+SAWQ0gTklVxNcVbfHx2pkZk66EBGQfrK33Bx1BflZ/iEDyiCwmzVtNba0X9A6ELYjB9WSkWdIqZCfPlKZMu9N//aZ6+3SDVuz/BVFsAVmtqQ4Let2QjOFiSIKXrtPqWvVT/MM=
+kirby.madstop.com,192.168.0.5 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAw9iHuAa/wepHoUzWqsvhQvSkpE4K7agrdLOWHM9mvyRQ2X3HVq5GqzAvWu4J+f0FvcLPwA9tivpxt1oSt5MOtvDM6HoM+8m3P4daBp0nlNaYR8/vHCAmX6N3RyM8FWfp+VqWyux1SooQwxYxVFy86G78ApTqNsZ+p7cHmnBYqk0=
+fedora1,192.168.0.51 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAyz1rKcApU4//j8CHYKexq4qnq2WVqLPrZYGnlij1t7FscLiDVKvBuRHVkfyTNIjAM/t7tM1Dj+FuD4iWziCmf7RO9q4wI5y/1zgCiSUegnZVSmH2yxnWGMdHGpXOkN3NXcpy6jylxyBo0M7T22PSezCxyUVfMclesjOEO1jETd0=
+kirby ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAw9iHuAa/wepHoUzWqsvhQvSkpE4K7agrdLOWHM9mvyRQ2X3HVq5GqzAvWu4J+f0FvcLPwA9tivpxt1oSt5MOtvDM6HoM+8m3P4daBp0nlNaYR8/vHCAmX6N3RyM8FWfp+VqWyux1SooQwxYxVFy86G78ApTqNsZ+p7cHmnBYqk0=
+sol10b,192.168.0.50 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAs37kiDwKxWqi6EfSdKwRaZXBwh2doOARRqZzyitBaPwESy26DwTx+xdQ2rwB4V2k1WIec+1f3bgTS2ArH75dQSPyba2HKqxaSRBd3Zh4z23+uUxpupEyoRdW1HolMOvuoceheSMsruiuYcuiyct41d4c/Qmr51Dv04Doi00k6Ws=
+piratehaven.org,64.81.59.88 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEA7TYRRkle0wDNohZ0TNZN6R1Zp0svxgX+GJ9umI5yNM1bMxUTgeNRh5nIvZg1HgD1WRXQ57dSxxLzbvRyAqc245g6S8eWWCtenvOFLl0rOF5D3VxbQuw79sOe8/Ac8TC+c8RuWB7aaxpwL5Rv9xfDeazOtoKXj7+uwQW1PUmTaEM=
+atalanta,192.168.0.10 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAysniuWiJH6OQLXl63XXcS1b/hP2lAgSz0IutjQ6ZUfBrt1BZ8udEgSh57w5QDLsZ1lNvND61u5cy6iDKXI5TIQY4DvUmsoFZhyr4iYJbtT/h6UJSyaZtEnA7ZMRjGhUIMOn+mjbj7Z3zmJMhxtImK3Xo3O2fJ1hnK4jsBwQPSLc=
+pixie,192.168.0.9 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAzDp588CvET6w11LB2s/vPjc4tX9+u46iYJgNFfhzxrXYMVv4GF7d30IXB5+Hwyi2FOQIG1+h0kUXVGWEv64rAFBT7pD2KMFX0lcDERV4avqT0TRDIIA5OqFOhq9Ff+kOmHS2cB7eFyR5vqbN4ujOnJGTmru9dcqyL+2AcFekvh0=
+culain,192.168.0.3 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAvJ/IORhQMdAsJ7LB1sI2LlWUHc7HPTCDiEgJ96ij3jFvqaIiYieRFaNkxbbk75mPkj3vIqWIgAsAtHmKX4wDikNG/gyjs4WM4cWFKtl2jiVhqpoxqqCaVxs6Ex+vpKuKhQR6SzFBFDlBZYP9an6DPu1msTLT8/hZH2WwswVmpyU=
+cs312-server.een.orst.edu,128.193.40.66 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEA+t3hsOiyp1mztt013bLdJqIAFCo8lxlz86MYEPz/mADHzWLs3Xv7xpAUv/E8pRbhEOzXo84EddORRBXz6DgVyMah8+v/u3IOkpXuZI0Iu1n5hZyf2l5DGEyGecr3oqqjUdMuM9HeXFLnqXJI3hDE7civBtqf5AJSol+TCcipeE8=
+freebsd1,192.168.0.52 ssh-dss AAAAB3NzaC1kc3MAAACBAJSiOyQhYlKAi0FDLKy42VzLDq6yJWXGXVCLSfgWyVx7QCq/3+W3C1dtHuAvjbypcjqqvsuGGITgQ1Y6B/+76n5d7FyQnj4SFZ5drOBn/TvslXhrS/Ok5KCcndfNAa+EyMnSZJ21jhoRjZftY4lmb4hy6fEF3RvjuOdf1qBN5FWpAAAAFQDcsWF0zELAW6YUlSjAyO0T0lfPbwAAAIAlOLdOd/WszzVaplCOnH5vF6LWfr6BosZKDkFi0mv6Ec636YGaj4AMxK8sRPusHv6sVByN17ntIJnLo2XD1SuoH28bZ0ZnPIdVnd0l1KqsOCuuow9LZYJUihezoUuYuTvij1jZdrwltuPNJTVLYtsZDnKE5plw+Tjzeb7ImjbXGwAAAIBT40olg3fxhRDiZECle0WK7GitgXCB3njs+4dba8VwveEJb9UuulMc1eR+zQiJR96IUBagC9NiLvUGS1IfiIHpT4FA8O/MK1W9SgxXB9d39Nk/9l8dH3U/fLnbC/hYVo8bN0or/mKxcxQMkdBwpPlWAbELRftod2BkkkvgfQdc+g==
+192.168.0.2 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgwCZ+qxpMMRJ3otGsjaYeKTKf6tuCZyK1cD+ns9Eu7V0ZJLJ/LLMxduu7n4H/ufGI5rGV5axzgx8yZhjDRzsrGjLAQYsqlomMkf901YQI6UuieSA4MZa5MDkq/Jt6Vx1kEGTpkgrfw9kRMX5BngECt1QKY4xTgC7Ex+WlFvZwk+tRUT3
+openbsd1,192.168.0.54 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAvgRVrtJk0fBg9YsLf7rWR1X32ZjFcva5XBvenDlHruObaHzuGXyyr6iOCAEOc7eCZjlPBYrGZ2potqyk8HlBOHXr1cCBf49t4yAt8KgKswtzWlgdbU1UEwllSRVOpzqULAT0smv/jfaIZdvRoN1rLriqtpn4bQL//ZOHiyXCwdlJ+di8Mza2L8KZ5T75hwIFBhrgL12xfymRp3v+Iy21MjjzsF3pROIkZ7icitNqQF55U9vsHaA37vG8FepVkO10bYYP7IHPZaBPBMPx7qPyRgRDJahEUGBnkIkzwJHEmA+7YMiNTZu6MigezD+CIqY1xOi/eXZwObLwLKXo7eRmpw==
+192.168.0.60 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAufcvE7s1eRwhUwMBfZ6uFNxkdSdzjSDEn3vjByOjG/eraNhnYAW3rxV7WIf2pEa6JSOMrE1mqsEL75xEtpXlzC949Ysz4+1OSHY52KonoFm/a+FbmbFp81TVuVPYaLoeWN27STiJh+puC5spkIZe0laqT1GU13M4gj6B+j3NLhU=
+rh3a,192.168.0.55 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAufcvE7s1eRwhUwMBfZ6uFNxkdSdzjSDEn3vjByOjG/eraNhnYAW3rxV7WIf2pEa6JSOMrE1mqsEL75xEtpXlzC949Ysz4+1OSHY52KonoFm/a+FbmbFp81TVuVPYaLoeWN27STiJh+puC5spkIZe0laqT1GU13M4gj6B+j3NLhU=
+tsetse,192.168.0.7 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAwCDGn82BEMSCfcE2LZKcwdAoyIBC+u2twVwWNRm3KzyrJMQ+RbTQo4AwGOdyr/QYh6KaxTKTSoDtiBLr132uenMQKwF47gCKMA1T47uiy+TBUehgOGwOxteSYP/pQRpKXFmhOppSPyDPQVq234XvANeJp0iT8ZKEhF2FsWTs6sM=
+config.sage.org,131.106.3.205 ssh-dss AAAAB3NzaC1kc3MAAACBAL2akEcIfQsfm3zCd2hD6PgH3kWA/tqX/qbrLhL/ipX7iqK/y282GMClZQSQjc1YNi9virvLzb6u/gdZxicZ8K5O3FaJrULQJOZaP62SOHk5CUSHVnvpMCaCnbwB6gEHa2LeMWStcEfWW+g1CC2hzPJw16/S5GISGXbyanO02MnXAAAAFQDomwx/OCjTmmQljMTU5rgNn2E4gwAAAIBmtMSfcs2Tq5iFFKK5cahluv047vVNfXqYIAkeJniceL0Et16MKfuzadpq0H9ocxQYW/5Ir9nUULrdxBUN9LxNKq15/uWkJC9QCSh8PysgvFnjVZeCODua/dn6eZTZnY9DZ3S6v1pT8CP6uWr5fmZJ8FKJGrC3gYX4y1V1ZTCVewAAAIB6e7RCST6vkTS5rgn5wGbrfLK5ad+PW+2i66k77Zv1pjtfRz+0oejBjwJDPNVSc2YNEl7X0nEEMNjo/a5x8Ls+nVqhzJA+NXIwS1e/moKbXFGewW5HAtxd79gtInC8dEIO7hmnWnqF1dBkRHXg1YffYkHrMVJBxpzagw7nYa0BBQ==
+rh3b,192.168.0.56 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAufcvE7s1eRwhUwMBfZ6uFNxkdSdzjSDEn3vjByOjG/eraNhnYAW3rxV7WIf2pEa6JSOMrE1mqsEL75xEtpXlzC949Ysz4+1OSHY52KonoFm/a+FbmbFp81TVuVPYaLoeWN27STiJh+puC5spkIZe0laqT1GU13M4gj6B+j3NLhU=
+centos1,192.168.0.57 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEA0DXqYF+3Lf68GkWBAjjKBb6UITNnzm4wiDi/AGjv5+DoVXqDcqHvZ8rZFAMgUe1dVob4pWT2ZWLHW0gicoJCdr4UQbPXlWz1F62z8fo2PRRPlG6KN1wmF7pnyml8jr0wBX8lQZJsMqi4InGozf7wFHLH/7DNGRK3MD6tSp3Z4is=
+doorstop.cafes.net,205.241.238.186 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEApJKeB9/bN5t55zLETHs0MVo/vEkfQ3EzY7178GKLI/yiOFmcn+NvUvUtCQK/xKpod813LBHCODxZPG1Kb0SjlaC/EkFEenb74LNu0o1qXa1GWh3wfaIm0JRNjXqPqxAWTlMs43O2HXwOwmLVhl7SSP3xtTw6h9gREbVKmfBaRdsRfzD0etfz1NCnmGh/1Sh9+j4eeS+IBtwoR5JVhZVhuofHCqs5HZ8gLDgfn8HXP7pMbLkx54cf1R/tmFmn9JGLdTPtEGcSIiu7414XSbfChSC83rGZCDPKHq7ZodiE8GpbWLBnyPXi2AYxTPM7aZMqitIHv3MWf5suV0q0WLGdnQ==
+host.domain.com,host1.domain.com,192.168.0.1 dss thisismykey1
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/sshkeys_core/spec/fixtures/unit/provider/sshkey/parsed/sample_with_blank_lines	Mon Jan 03 17:15:14 2022 +0000
@@ -0,0 +1,8 @@
+# A comment
+
+# ... and another after a blank line. The following line includes three whitespace characters.
+   
+hosting2.planetargon.com,64.34.164.77 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAy+f2t52cDMrYkgEKQ6juqfMf/a0nDFry3JAzl+SAWQ0gTklVxNcVbfHx2pkZk66EBGQfrK33Bx1BflZ/                   iEDyiCwmzVtNba0X9A6ELYjB9WSkWdIqZCfPlKZMu9N//aZ6+3SDVuz/BVFsAVmtqQ4Let2QjOFiSIKXrtPqWvVT/MM=
+
+will.isawesome.com,192.168.0.5 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAw9iHuAa/wepHoUzWqsvhQvSkpE4K7agrdLOWHM9mvyRQ2X3HVq5GqzAvWu4J+f0FvcLPwA9tivpxt1oSt5MOtvDM6HoM+8m3P4daBp0nlNaYR8/vHCAmX6N3RyM8FWfp+VqWyux1SooQwxYxVFy86G78ApTqNsZ+p7cHmnBYqk0=
+will.isgreat.com,192.168.0.6 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAw9iHuAa/wepHoUzWqsvhQvSkpE4K7agrdLOWHM9mvyRQ2X3HVq5GqzAvWu4J+f0FvcLPwA9tivpxt1oSt5MOtvDM6HoM+8m3P4daBp0nlNaYR8/vHCAmX6N3RyM8FWfp+VqWyux1SooQwxYxVFy86G78ApTqNsZ+p7cHmnBasd=
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/sshkeys_core/spec/fixtures/unit/type/user/authorized_keys	Mon Jan 03 17:15:14 2022 +0000
@@ -0,0 +1,6 @@
+# fixture for testing ssh key purging
+
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDTXvM7AslzjNUYrPLiNVBsF5VnqL2RmqrkzscdVdHzVxvieNwmLGeUkg8EfXPiz7j5F/Lr0J8oItTCWzyN2KmM+DhUMjvP4AbELO/VYbnVrZICRiUNYSO3EN9/uapKAuiev88d7ynbonCU0VZoTPg/ug4OondOrLCtcGri5ltF+mausGfAYiFAQVEWqXV+1tyejoawJ884etb3n4ilpsrH9JK6AtOkEWVD3TDrNi29O1mQQ/Cn88g472zAJ+DhsIn+iehtfX5nmOtDNN/1t1bGMIBzkSYEAYwUiRJbRXvbobT7qKZQPA3dh0m8AYQS5/hd4/c4pmlxL8kgr24SnBY5 key1 name
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDTXvM7AslzjNUYrPLiNVBsF5VnqL2RmqrkzscdVdHzVxvieNwmLGeUkg8EfXPiz7j5F/Lr0J8oItTCWzyN2KmM+DhUMjvP4AbELO/VYbnVrZICRiUNYSO3EN9/uapKAuiev88d7ynbonCU0VZoTPg/ug4OondOrLCtcGri5ltF+mausGfAYiFAQVEWqXV+1tyejoawJ884etb3n4ilpsrH9JK6AtOkEWVD3TDrNi29O1mQQ/Cn88g472zAJ+DhsIn+iehtfX5nmOtDNN/1t1bGMIBzkSYEAYwUiRJbRXvbobT7qKZQPA3dh0m8AYQS5/hd4/c4pmlxL8kgr24SnBY5 keyname2
+#ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDTXvM7AslzjNUYrPLiNVBsF5VnqL2RmqrkzscdVdHzVxvieNwmLGeUkg8EfXPiz7j5F/Lr0J8oItTCWzyN2KmM+DhUMjvP4AbELO/VYbnVrZICRiUNYSO3EN9/uapKAuiev88d7ynbonCU0VZoTPg/ug4OondOrLCtcGri5ltF+mausGfAYiFAQVEWqXV+1tyejoawJ884etb3n4ilpsrH9JK6AtOkEWVD3TDrNi29O1mQQ/Cn88g472zAJ+DhsIn+iehtfX5nmOtDNN/1t1bGMIBzkSYEAYwUiRJbRXvbobT7qKZQPA3dh0m8AYQS5/hd4/c4pmlxL8kgr24SnBY5 keyname3
+ssh-rsa KEY-WITH-NO-NAME
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/sshkeys_core/spec/integration/provider/ssh_authorized_key_spec.rb	Mon Jan 03 17:15:14 2022 +0000
@@ -0,0 +1,220 @@
+require 'spec_helper'
+require 'puppet/file_bucket/dipper'
+
+describe Puppet::Type.type(:ssh_authorized_key).provider(:parsed), unless: Puppet.features.microsoft_windows? do
+  include PuppetSpec::Files
+
+  let :fake_userfile do
+    tmpfile('authorized_keys.user')
+  end
+
+  let :fake_rootfile do
+    tmpfile('authorized_keys.root')
+  end
+
+  # rubocop:disable Metrics/LineLength
+  let :sample_rsa_keys do
+    [
+      'AAAAB3NzaC1yc2EAAAADAQABAAAAgQCi18JBZOq10X3w4f67nVhO0O3s5Y1vHH4UgMSM3ZnQwbC5hjGyYSi9UULOoQQoQynI/a0I9NL423/Xk/XJVIKCHcS8q6V2Wmjd+fLNelOjxxoW6mbIytEt9rDvwgq3Mof3/m21L3t2byvegR00a+ikKbmInPmKwjeWZpexCIsHzQ==', # 1024 bit
+      'AAAAB3NzaC1yc2EAAAADAQABAAAAgQDLClyvi3CsJw5Id6khZs2/+s11qOH4Gdp6iDioDsrIp0m8kSiPr71VGyQYAfPzzvHemHS7Xg0NkG1Kc8u9tRqBQfTvz7ubq0AT/g01+4P2hQ/soFkuwlUG/HVnnaYb6N0Qp5SHWvD5vBE2nFFQVpP5GrSctPtHSjzJq/i+6LYhmQ==', # 1024 bit
+      'AAAAB3NzaC1yc2EAAAADAQABAAABAQDLygAO6txXkh9FNV8xSsBkATeqLbHzS7sFjGI3gt0Dx6q3LjyKwbhQ1RLf28kd5G6VWiXmClU/RtiPdUz8nrGuun++2mrxzrXrvpR9dq1lygLQ2wn2cI35dN5bjRMtXy3decs6HUhFo9MoNwX250rUWfdCyNPhGIp6OOfmjdy+UeLGNxq9wDx6i4bT5tVVSqVRtsEfw9+ICXchzl85QudjneVVpP+thriPZXfXA5eaGwAo/dmoKOIhUwF96gpdLqzNtrGQuxPbV80PTbGv9ZtAtTictxaDz8muXO7he9pXmchUpxUKtMFjHkL0FAZ9tRPmv3RA30sEr2fZ8+LKvnE50w0' # 2048 Bit
+    ]
+  end
+  # rubocop:enable Metrics/LineLength
+
+  # rubocop:disable Metrics/LineLength
+  let :sample_dsa_keys do
+    [
+      'AAAAB3NzaC1kc3MAAACBAOPck2O8MIDSqxPSnvENt6tzRrKJ5oOhB6Nc6oEcWm+VEH1gvuxdiRqwoMgRwyEf1yUd+UAcLw3a6Jn+EtFyEBN/5WF+4Tt4xTxZ0Pfik2Wc5uqHbQ2dkmOoXiAOYPiD3JUQ1Xwm/J0CgetjitoLfzAGdCNhMqguqAuHcVJ78ZZbAAAAFQCIBKFYZ+I18I+dtgteirXh+VVEEwAAAIEAs1yvQ/wnLLrRCM660pF4kBiw3D6dJfMdCXWQpn0hZmkBQSIzZv4Wuk3giei5luxscDxNc+y3CTXtnyG4Kt1Yi2sOdvhRI3rX8tD+ejn8GHazM05l5VIo9uu4AQPIE32iV63IqgApSBbJ6vDJW91oDH0J492WdLCar4BS/KE3cRwAAACBAN0uSDyJqYLRsfYcFn4HyVf6TJxQm1IcwEt6GcJVzgjri9VtW7FqY5iBqa9B9Zdh5XXAYJ0XLsWQCcrmMHM2XGHGpA4gL9VlCJ/0QvOcXxD2uK7IXwAVUA7g4V4bw8EVnFv2Flufozhsp+4soo1xiYc5jiFVHwVlk21sMhAtKAeF' # 1024 Bit
+    ]
+  end
+  # rubocop:enable Metrics/LineLength
+
+  let :sample_lines do
+    [
+      "ssh-rsa #{sample_rsa_keys[1]} root@someotherhost",
+      "ssh-dss #{sample_dsa_keys[0]} root@anywhere",
+      "ssh-rsa #{sample_rsa_keys[2]} paul",
+      "ssh-rsa #{sample_rsa_keys[2]} dummy",
+    ]
+  end
+
+  let :dummy do
+    Puppet::Type.type(:ssh_authorized_key).new(
+      name: 'dummy',
+      target: fake_userfile,
+      user: 'nobody',
+      ensure: :absent,
+    )
+  end
+
+  before :each do
+    File.stubs(:chown)
+    File.stubs(:chmod)
+    Puppet::Util::SUIDManager.stubs(:asuser).yields
+  end
+
+  after :each do
+    described_class.clear # Work around bug #6628
+  end
+
+  def create_fake_key(username, content)
+    filename = ((username == :root) ? fake_rootfile : fake_userfile)
+    File.open(filename, 'w') do |f|
+      content.each do |line|
+        f.puts line
+      end
+    end
+  end
+
+  def check_fake_key(username, expected_content)
+    filename = ((username == :root) ? fake_rootfile : fake_userfile)
+    content = File.readlines(filename).map(&:chomp).sort.reject { |x| x =~ %r{^# HEADER:} }
+    expect(content.join("\n")).to eq(expected_content.sort.join("\n"))
+  end
+
+  def run_in_catalog(*resources)
+    Puppet::FileBucket::Dipper.any_instance.stubs(:backup) # rubocop:disable RSpec/AnyInstance
+    catalog = Puppet::Resource::Catalog.new
+    catalog.host_config = false
+    resources.each do |resource|
+      resource.expects(:err).never
+      catalog.add_resource(resource)
+    end
+    catalog.apply
+  end
+
+  it 'does not complain about empty lines and comments' do
+    described_class.expects(:flush).never
+    sample = ['', sample_lines[0], '   ', sample_lines[1], '# just a comment', '#and another']
+    create_fake_key(:user, sample)
+    run_in_catalog(dummy)
+    check_fake_key(:user, sample)
+  end
+
+  it 'keeps empty lines and comments when modifying a file' do
+    create_fake_key(:user, ['', sample_lines[0], '   ', sample_lines[3], '# just a comment', '#and another'])
+    run_in_catalog(dummy)
+    check_fake_key(:user, ['', sample_lines[0], '   ', '# just a comment', '#and another'])
+  end
+
+  describe 'when managing one resource' do
+    describe 'with ensure set to absent' do
+      let :resource do
+        Puppet::Type.type(:ssh_authorized_key).new(
+          name: 'root@hostname',
+          type: :rsa,
+          key: sample_rsa_keys[0],
+          target: fake_rootfile,
+          user: 'root',
+          ensure: :absent,
+        )
+      end
+
+      it "does not modify root's keyfile if resource is currently not present" do
+        create_fake_key(:root, sample_lines)
+        run_in_catalog(resource)
+        check_fake_key(:root, sample_lines)
+      end
+
+      it "remove the key from root's keyfile if resource is currently present" do
+        create_fake_key(:root, sample_lines + ["ssh-rsa #{sample_rsa_keys[0]} root@hostname"])
+        run_in_catalog(resource)
+        check_fake_key(:root, sample_lines)
+      end
+    end
+
+    describe 'when ensure is present' do
+      let :resource do
+        Puppet::Type.type(:ssh_authorized_key).new(
+          name: 'root@hostname',
+          type: :rsa,
+          key: sample_rsa_keys[0],
+          target: fake_rootfile,
+          user: 'root',
+          ensure: :present,
+        )
+      end
+
+      # just a dummy so the parsedfile provider is aware
+      # of the user's authorized_keys file
+
+      it 'adds the key if it is not present' do
+        create_fake_key(:root, sample_lines)
+        run_in_catalog(resource)
+        check_fake_key(:root, sample_lines + ["ssh-rsa #{sample_rsa_keys[0]} root@hostname"])
+      end
+
+      it 'modifies the type if type is out of sync' do
+        create_fake_key(:root, sample_lines + ["ssh-dss #{sample_rsa_keys[0]} root@hostname"])
+        run_in_catalog(resource)
+        check_fake_key(:root, sample_lines + ["ssh-rsa #{sample_rsa_keys[0]} root@hostname"])
+      end
+
+      it 'modifies the key if key is out of sync' do
+        create_fake_key(:root, sample_lines + ["ssh-rsa #{sample_rsa_keys[1]} root@hostname"])
+        run_in_catalog(resource)
+        check_fake_key(:root, sample_lines + ["ssh-rsa #{sample_rsa_keys[0]} root@hostname"])
+      end
+
+      it 'removes the key from old file if target is out of sync' do
+        create_fake_key(:user, [sample_lines[0], "ssh-rsa #{sample_rsa_keys[0]} root@hostname"])
+        create_fake_key(:root, [sample_lines[1], sample_lines[2]])
+        run_in_catalog(resource, dummy)
+        check_fake_key(:user, [sample_lines[0]])
+        # check_fake_key(:root, [ sample_lines[1], sample_lines[2], "ssh-rsa #{sample_rsa_keys[0]} root@hostname" ])
+      end
+
+      it 'adds the key to new file if target is out of sync' do
+        create_fake_key(:user, [sample_lines[0], "ssh-rsa #{sample_rsa_keys[0]} root@hostname"])
+        create_fake_key(:root, [sample_lines[1], sample_lines[2]])
+        run_in_catalog(resource, dummy)
+        # check_fake_key(:user, [ sample_lines[0] ])
+        check_fake_key(:root, [sample_lines[1], sample_lines[2], "ssh-rsa #{sample_rsa_keys[0]} root@hostname"])
+      end
+
+      it 'modifies options if options are out of sync' do
+        resource[:options] = ['from="*.domain1,host1.domain2"', 'no-port-forwarding', 'no-pty']
+        create_fake_key(:root, sample_lines + ["from=\"*.false,*.false2\",no-port-forwarding,no-pty ssh-rsa #{sample_rsa_keys[0]} root@hostname"])
+        run_in_catalog(resource)
+        check_fake_key(:root, sample_lines + ["from=\"*.domain1,host1.domain2\",no-port-forwarding,no-pty ssh-rsa #{sample_rsa_keys[0]} root@hostname"])
+      end
+    end
+  end
+
+  describe 'when managing two resource' do
+    let :examples do
+      resources = []
+      resources << Puppet::Type.type(:ssh_authorized_key).new(
+        name: 'root@hostname',
+        type: :rsa,
+        key: sample_rsa_keys[0],
+        target: fake_rootfile,
+        user: 'root',
+        ensure: :present,
+      )
+      resources << Puppet::Type.type(:ssh_authorized_key).new(
+        name: 'user@hostname',
+        key: sample_rsa_keys[1],
+        type: :rsa,
+        target: fake_userfile,
+        user: 'nobody',
+        ensure: :present,
+      )
+      resources
+    end
+
+    describe 'and both keys are absent' do
+      before :each do
+        create_fake_key(:root, sample_lines)
+        create_fake_key(:user, sample_lines)
+      end
+
+      it 'adds both keys' do
+        run_in_catalog(*examples)
+        check_fake_key(:root, sample_lines + ["ssh-rsa #{sample_rsa_keys[0]} root@hostname"])
+        check_fake_key(:user, sample_lines + ["ssh-rsa #{sample_rsa_keys[1]} user@hostname"])
+      end
+    end
+  end
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/sshkeys_core/spec/integration/provider/sshkey_spec.rb	Mon Jan 03 17:15:14 2022 +0000
@@ -0,0 +1,156 @@
+require 'spec_helper'
+require 'puppet/file_bucket/dipper'
+require 'puppet_spec/files'
+require 'puppet_spec/compiler'
+
+describe Puppet::Type.type(:sshkey).provider(:parsed), unless: Puppet.features.microsoft_windows? do
+  include PuppetSpec::Files
+  include PuppetSpec::Compiler
+
+  let(:sshkey_file) { tmpfile('sshkey_integration_specs') }
+  let(:type_under_test) { 'sshkey' }
+
+  before :each do
+    # Don't backup to filebucket
+    Puppet::FileBucket::Dipper.any_instance.stubs(:backup) # rubocop:disable RSpec/AnyInstance
+    # We don't want to execute anything
+    described_class.stubs(:filetype)
+                   .returns Puppet::Util::FileType::FileTypeFlat
+
+    FileUtils.cp(my_fixture('sample'), sshkey_file)
+  end
+
+  after :each do
+    # sshkey provider class
+    described_class.clear
+  end
+
+  describe 'when managing a ssh known hosts file it...' do
+    let(:host_alias) { 'r0ckdata.com' }
+    let(:invalid_type) { 'ssh-er0ck' }
+    let(:sshkey_name) { 'kirby.madstop.com' }
+    let(:super_unique) { 'my.super.unique.host' }
+
+    it 'creates a new known_hosts file with mode 0644' do
+      target   = tmpfile('ssh_known_hosts')
+      manifest = "#{type_under_test} { '#{super_unique}':
+      ensure => 'present',
+      type   => 'rsa',
+      key    => 'TESTKEY',
+      target => '#{target}' }"
+      apply_with_error_check(manifest)
+      expect_file_mode(target, '644')
+    end
+
+    it 'creates an SSH host key entry (ensure present)' do
+      manifest = "#{type_under_test} { '#{super_unique}':
+      ensure => 'present',
+      type   => 'rsa',
+      key    => 'mykey',
+      target => '#{sshkey_file}' }"
+      apply_with_error_check(manifest)
+      expect(File.read(sshkey_file)).to match(%r{#{super_unique}.*mykey})
+    end
+
+    it 'deletes an entry for an SSH host key' do
+      manifest = "#{type_under_test} { '#{sshkey_name}':
+      ensure => 'absent',
+      target => '#{sshkey_file}' }"
+      apply_with_error_check(manifest)
+      expect(File.read(sshkey_file)).not_to match(%r{#{sshkey_name}.*Yqk0=})
+    end
+
+    it 'updates an entry for an SSH host key' do
+      manifest = "#{type_under_test} { '#{sshkey_name}':
+      ensure => 'present',
+      type   => 'rsa',
+      key    => 'mynewshinykey',
+      target => '#{sshkey_file}' }"
+      apply_with_error_check(manifest)
+      expect(File.read(sshkey_file)).to match(%r{#{sshkey_name}.*mynewshinykey})
+      expect(File.read(sshkey_file)).not_to match(%r{#{sshkey_name}.*Yqk0=})
+    end
+
+    # test all key types
+    types = [
+      'ssh-dss',     'dsa',
+      'ssh-ed25519', 'ed25519',
+      'ssh-rsa',     'rsa',
+      'ecdsa-sha2-nistp256',
+      'ecdsa-sha2-nistp384',
+      'ecdsa-sha2-nistp521'
+    ]
+    # these types are treated as aliases for sshkey <ahem> type
+    #   so they are populated as the *values* below
+    aliases = {
+      'dsa'     => 'ssh-dss',
+      'ed25519' => 'ssh-ed25519',
+      'rsa'     => 'ssh-rsa',
+    }
+    types.each do |type|
+      it "should update an entry with #{type} type" do
+        manifest = "#{type_under_test} { '#{sshkey_name}':
+        ensure => 'present',
+        type   => '#{type}',
+        key    => 'mynewshinykey',
+        target => '#{sshkey_file}' }"
+
+        apply_with_error_check(manifest)
+        if aliases.key?(type)
+          full_type = aliases[type]
+          expect(File.read(sshkey_file)).to match(%r{#{sshkey_name}.*#{full_type}.*mynew})
+        else
+          expect(File.read(sshkey_file)).to match(%r{#{sshkey_name}.*#{type}.*mynew})
+        end
+      end
+    end
+
+    # test unknown key type fails
+    it 'raises an error with an unknown type' do
+      manifest = "#{type_under_test} { '#{sshkey_name}':
+      ensure => 'present',
+      type   => '#{invalid_type}',
+      key    => 'mynewshinykey',
+      target => '#{sshkey_file}' }"
+      expect {
+        apply_compiled_manifest(manifest)
+      }.to raise_error(Puppet::ResourceError, %r{Invalid value "#{invalid_type}"})
+    end
+
+    # single host_alias
+    it 'updates an entry with a single new host_alias' do
+      manifest = "#{type_under_test} { '#{sshkey_name}':
+      ensure       => 'present',
+      host_aliases => '#{host_alias}',
+      target       => '#{sshkey_file}' }"
+      apply_with_error_check(manifest)
+      expect(File.read(sshkey_file)).to match(%r{#{sshkey_name},#{host_alias}\s})
+      expect(File.read(sshkey_file)).not_to match(%r{#{sshkey_name}\s})
+    end
+
+    # array host_alias
+    it 'updates an entry with multiple new host_aliases' do
+      manifest = "#{type_under_test} { '#{sshkey_name}':
+      ensure       => 'present',
+      host_aliases => [ 'r0ckdata.com', 'erict.net' ],
+      target       => '#{sshkey_file}' }"
+      apply_with_error_check(manifest)
+      expect(File.read(sshkey_file)).to match(%r{#{sshkey_name},r0ckdata\.com,erict\.net\s})
+      expect(File.read(sshkey_file)).not_to match(%r{#{sshkey_name}\s})
+    end
+
+    # puppet resource sshkey
+    it 'fetches an entry from resources' do
+      resource_app = Puppet::Application[:resource]
+      resource_app.preinit
+      resource_app.command_line
+                  .stubs(:args)
+                  .returns([type_under_test, sshkey_name, "target=#{sshkey_file}"])
+
+      resource_app.expects(:puts).with do |args|
+        expect(args).to match(%r{#{sshkey_name}})
+      end
+      resource_app.main
+    end
+  end
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/sshkeys_core/spec/integration/type/user_spec.rb	Mon Jan 03 17:15:14 2022 +0000
@@ -0,0 +1,89 @@
+require 'spec_helper'
+require 'puppet_spec/files'
+require 'puppet_spec/compiler'
+require 'puppet/provider/parsedfile'
+
+# parsedfile provider implements prefetch
+Puppet::Type.newtype(:prefetchable_test) do
+  newparam(:name, isnamevar: true)
+end
+Puppet::Type.type(:prefetchable_test).provide(:parsed, parent: Puppet::Provider::ParsedFile, filetype: :flat) do
+end
+
+describe 'Puppet::Type.type(:user) (integration)', unless: Puppet.features.microsoft_windows? do
+  include PuppetSpec::Files
+  include PuppetSpec::Compiler
+
+  context 'when set to purge ssh keys from a file' do
+    # different UTF-8 widths
+    # 1-byte A
+    # 2-byte ۿ - http://www.fileformat.info/info/unicode/char/06ff/index.htm - 0xDB 0xBF / 219 191
+    # 3-byte ᚠ - http://www.fileformat.info/info/unicode/char/16A0/index.htm - 0xE1 0x9A 0xA0 / 225 154 160
+    # 4-byte 𠜎 - http://www.fileformat.info/info/unicode/char/2070E/index.htm - 0xF0 0xA0 0x9C 0x8E / 240 160 156 142
+    let(:mixed_utf8) { "A\u06FF\u16A0\u{2070E}" } # Aۿᚠ𠜎
+
+    let(:tempfile) do
+      file_containing('user_spec', <<-EOF)
+        # comment #{mixed_utf8}
+        ssh-rsa KEY-DATA key-name
+        ssh-rsa KEY-DATA key name
+        EOF
+    end
+    # must use an existing user, or the generated key resource
+    # will fail on account of an invalid user for the key
+    # - root should be a safe default
+    let(:manifest) { "user { 'root': purge_ssh_keys => '#{tempfile}' }" }
+
+    it 'purges authorized ssh keys' do
+      apply_compiled_manifest(manifest)
+      expect(File.read(tempfile, encoding: Encoding::UTF_8)).not_to match(%r{key-name})
+    end
+
+    it 'purges keys with spaces in the comment string' do
+      apply_compiled_manifest(manifest)
+      expect(File.read(tempfile, encoding: Encoding::UTF_8)).not_to match(%r{key name})
+    end
+
+    context 'with other prefetching resources evaluated first' do
+      let(:provider) { Puppet::Type.type(:prefetchable_test).provider(:parsed) }
+      let(:manifest) do
+        "
+          prefetchable_test { 'test':
+            before => User[root]
+          }
+          user { 'root':
+            purge_ssh_keys => '#{tempfile}'
+          }
+        "
+      end
+
+      before(:each) do
+        provider.default_target = tmpfile('prefetchable')
+      end
+
+      after(:each) do
+        provider.clear
+      end
+
+      it 'purges authorized ssh keys' do
+        apply_compiled_manifest(manifest)
+        expect(File.read(tempfile, encoding: Encoding::UTF_8)).not_to match(%r{key-name})
+      end
+    end
+
+    context 'with multiple unnamed keys' do
+      let(:tempfile) do
+        file_containing('user_spec', <<-EOF)
+          # comment #{mixed_utf8}
+          ssh-rsa KEY-DATA1
+          ssh-rsa KEY-DATA2
+          EOF
+      end
+
+      it 'purges authorized ssh keys' do
+        apply_compiled_manifest(manifest)
+        expect(File.read(tempfile, encoding: Encoding::UTF_8)).not_to match(%r{KEY-DATA})
+      end
+    end
+  end
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/sshkeys_core/spec/lib/puppet_spec/compiler.rb	Mon Jan 03 17:15:14 2022 +0000
@@ -0,0 +1,116 @@
+module PuppetSpec::Compiler
+  module_function
+
+  def compile_to_catalog(string, node = Puppet::Node.new('test'))
+    Puppet[:code] = string
+    # see lib/puppet/indirector/catalog/compiler.rb#filter
+    Puppet::Parser::Compiler.compile(node).filter { |r| r.virtual? }
+  end
+
+  # Does not removed virtual resources in compiled catalog (i.e. keeps unrealized)
+  def compile_to_catalog_unfiltered(string, node = Puppet::Node.new('test'))
+    Puppet[:code] = string
+    # see lib/puppet/indirector/catalog/compiler.rb#filter
+    Puppet::Parser::Compiler.compile(node)
+  end
+
+  def compile_to_ral(manifest, node = Puppet::Node.new('test'))
+    catalog = compile_to_catalog(manifest, node)
+    ral = catalog.to_ral
+    ral.finalize
+    ral
+  end
+
+  def compile_to_relationship_graph(manifest, prioritizer = Puppet::Graph::SequentialPrioritizer.new)
+    ral = compile_to_ral(manifest)
+    graph = Puppet::Graph::RelationshipGraph.new(prioritizer)
+    graph.populate_from(ral)
+    graph
+  end
+
+  def apply_compiled_manifest(manifest, prioritizer = Puppet::Graph::SequentialPrioritizer.new)
+    args = []
+    if Puppet.version.to_f < 5.0
+      args << 'apply'
+    end
+    catalog = compile_to_ral(manifest)
+    if block_given?
+      catalog.resources.each { |res| yield res }
+    end
+    transaction = Puppet::Transaction.new(catalog,
+                                          Puppet::Transaction::Report.new(*args),
+                                          prioritizer)
+    transaction.evaluate
+    transaction.report.finalize_report
+
+    transaction
+  end
+
+  def apply_with_error_check(manifest)
+    apply_compiled_manifest(manifest) do |res|
+      res.expects(:err).never
+    end
+  end
+
+  def order_resources_traversed_in(relationships)
+    order_seen = []
+    relationships.traverse { |resource| order_seen << resource.ref }
+    order_seen
+  end
+
+  def collect_notices(code, node = Puppet::Node.new('foonode'))
+    Puppet[:code] = code
+    compiler = Puppet::Parser::Compiler.new(node)
+    node.environment.check_for_reparse
+    logs = []
+    Puppet::Util::Log.with_destination(Puppet::Test::LogCollector.new(logs)) do
+      yield(compiler)
+    end
+    logs = logs.select { |log| log.level == :notice }.map { |log| log.message }
+    logs
+  end
+
+  def eval_and_collect_notices(code, node = Puppet::Node.new('foonode'), topscope_vars = {})
+    collect_notices(code, node) do |compiler|
+      unless topscope_vars.empty?
+        scope = compiler.topscope
+        topscope_vars.each { |k, v| scope.setvar(k, v) }
+      end
+      if block_given?
+        compiler.compile do |catalog|
+          yield(compiler.topscope, catalog)
+          catalog
+        end
+      else
+        compiler.compile
+      end
+    end
+  end
+
+  # Compiles a catalog, and if source is given evaluates it and returns its result.
+  # The catalog is returned if no source is given.
+  # Topscope variables are set before compilation
+  # Uses a created node 'testnode' if none is given.
+  # (Parameters given by name)
+  #
+  def evaluate(code: 'undef', source: nil, node: Puppet::Node.new('testnode'), variables: {})
+    source_location = caller(0..0).first
+    Puppet[:code] = code
+    compiler = Puppet::Parser::Compiler.new(node)
+    unless variables.empty?
+      scope = compiler.topscope
+      variables.each { |k, v| scope.setvar(k, v) }
+    end
+
+    if source.nil?
+      compiler.compile
+      # see lib/puppet/indirector/catalog/compiler.rb#filter
+      return compiler.filter { |r| r.virtual? }
+    end
+
+    # evaluate given source is the context of the compiled state and return its result
+    compiler.compile do |_catalog|
+      Puppet::Pops::Parser::EvaluatingParser.singleton.evaluate_string(compiler.topscope, source, source_location)
+    end
+  end
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/sshkeys_core/spec/lib/puppet_spec/files.rb	Mon Jan 03 17:15:14 2022 +0000
@@ -0,0 +1,126 @@
+require 'fileutils'
+require 'tempfile'
+require 'tmpdir'
+require 'pathname'
+
+# A support module for testing files.
+module PuppetSpec::Files
+  @global_tempfiles = []
+
+  def self.cleanup
+    until @global_tempfiles.empty?
+      path = @global_tempfiles.pop
+      begin
+        Dir.unstub(:entries)
+        FileUtils.rm_rf path, secure: true
+      rescue Errno::ENOENT # rubocop:disable Lint/HandleExceptions
+        # nothing to do
+      end
+    end
+  end
+
+  def make_absolute(path)
+    PuppetSpec::Files.make_absolute(path)
+  end
+
+  def self.make_absolute(path)
+    path = File.expand_path(path)
+    path[0] = 'c' if Puppet.features.microsoft_windows?
+    path
+  end
+
+  def tmpfile(name, dir = nil)
+    PuppetSpec::Files.tmpfile(name, dir)
+  end
+
+  def self.tmpfile(name, dir = nil)
+    # Generate a temporary file, just for the name...
+    source = dir ? Tempfile.new(name, dir) : Tempfile.new(name)
+    path = Puppet::FileSystem.expand_path(source.path.encode(Encoding::UTF_8))
+    source.close!
+
+    record_tmp(File.expand_path(path))
+
+    path
+  end
+
+  def file_containing(name, contents)
+    PuppetSpec::Files.file_containing(name, contents)
+  end
+
+  def self.file_containing(name, contents)
+    file = tmpfile(name)
+    File.open(file, 'wb') { |f| f.write(contents) }
+    file
+  end
+
+  def script_containing(name, contents)
+    PuppetSpec::Files.script_containing(name, contents)
+  end
+
+  def self.script_containing(name, contents)
+    file = tmpfile(name)
+    if Puppet.features.microsoft_windows?
+      file += '.bat'
+      text = contents[:windows]
+    else
+      text = contents[:posix]
+    end
+    File.open(file, 'wb') { |f| f.write(text) }
+    Puppet::FileSystem.chmod(0o755, file)
+    file
+  end
+
+  def tmpdir(name)
+    PuppetSpec::Files.tmpdir(name)
+  end
+
+  def self.tmpdir(name)
+    dir = Puppet::FileSystem.expand_path(Dir.mktmpdir(name).encode!(Encoding::UTF_8))
+
+    record_tmp(dir)
+
+    dir
+  end
+
+  def dir_containing(name, contents_hash)
+    PuppetSpec::Files.dir_containing(name, contents_hash)
+  end
+
+  def self.dir_containing(name, contents_hash)
+    dir_contained_in(tmpdir(name), contents_hash)
+  end
+
+  def dir_contained_in(dir, contents_hash)
+    PuppetSpec::Files.dir_contained_in(dir, contents_hash)
+  end
+
+  def self.dir_contained_in(dir, contents_hash)
+    contents_hash.each do |k, v|
+      if v.is_a?(Hash)
+        Dir.mkdir(tmp = File.join(dir, k))
+        dir_contained_in(tmp, v)
+      else
+        file = File.join(dir, k)
+        File.open(file, 'wb') { |f| f.write(v) }
+      end
+    end
+    dir
+  end
+
+  def self.record_tmp(tmp)
+    # ...record it for cleanup,
+    @global_tempfiles ||= []
+    @global_tempfiles << tmp
+  end
+
+  def expect_file_mode(file, mode)
+    actual_mode = '%o' % Puppet::FileSystem.stat(file).mode
+    target_mode = if Puppet.features.microsoft_windows?
+                    mode
+                  else
+                    '10' + '%04i' % mode.to_i
+                  end
+    expect(actual_mode).to eq(target_mode)
+  end
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/sshkeys_core/spec/spec_helper.rb	Mon Jan 03 17:15:14 2022 +0000
@@ -0,0 +1,54 @@
+require 'puppetlabs_spec_helper/module_spec_helper'
+require 'rspec-puppet-facts'
+
+require 'spec_helper_local' if File.file?(File.join(File.dirname(__FILE__), 'spec_helper_local.rb'))
+
+include RspecPuppetFacts
+
+default_facts = {
+  puppetversion: Puppet.version,
+  facterversion: Facter.version,
+}
+
+default_fact_files = [
+  File.expand_path(File.join(File.dirname(__FILE__), 'default_facts.yml')),
+  File.expand_path(File.join(File.dirname(__FILE__), 'default_module_facts.yml')),
+]
+
+default_fact_files.each do |f|
+  next unless File.exist?(f) && File.readable?(f) && File.size?(f)
+
+  begin
+    default_facts.merge!(YAML.safe_load(File.read(f), [], [], true))
+  rescue => e
+    RSpec.configuration.reporter.message "WARNING: Unable to load #{f}: #{e}"
+  end
+end
+
+# read default_facts and merge them over what is provided by facterdb
+default_facts.each do |fact, value|
+  add_custom_fact fact, value
+end
+
+RSpec.configure do |c|
+  c.default_facts = default_facts
+  c.before :each do
+    # set to strictest setting for testing
+    # by default Puppet runs at warning level
+    Puppet.settings[:strict] = :warning
+  end
+  c.filter_run_excluding(bolt: true) unless ENV['GEM_BOLT']
+  c.after(:suite) do
+  end
+end
+
+# Ensures that a module is defined
+# @param module_name Name of the module
+def ensure_module_defined(module_name)
+  module_name.split('::').reduce(Object) do |last_module, next_module|
+    last_module.const_set(next_module, Module.new) unless last_module.const_defined?(next_module, false)
+    last_module.const_get(next_module, false)
+  end
+end
+
+# 'spec_overrides' from sync.yml will appear below this line
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/sshkeys_core/spec/spec_helper_acceptance.rb	Mon Jan 03 17:15:14 2022 +0000
@@ -0,0 +1,25 @@
+require 'beaker-rspec'
+require 'beaker/module_install_helper'
+require 'beaker/puppet_install_helper'
+
+def beaker_opts
+  { debug: true, trace: true, expect_failures: true, acceptable_exit_codes: (0...256) }
+end
+
+def posix_agents
+  agents.reject { |agent| agent['platform'].include?('windows') }
+end
+
+def osx_agents
+  agents.select { |agent| agent['platform'].include?('osx') }
+end
+
+RSpec.configure do |c|
+  c.before :suite do
+    unless ENV['BEAKER_provision'] == 'no'
+      run_puppet_install_helper
+      install_module_on(hosts)
+      install_module_dependencies_on(hosts)
+    end
+  end
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/sshkeys_core/spec/spec_helper_local.rb	Mon Jan 03 17:15:14 2022 +0000
@@ -0,0 +1,17 @@
+dir = File.expand_path(File.dirname(__FILE__))
+$LOAD_PATH.unshift File.join(dir, 'lib')
+
+# Container for various Puppet-specific RSpec helpers.
+module PuppetSpec
+end
+
+require 'puppet_spec/files'
+
+RSpec.configure do |config|
+  config.before :each do |_test|
+    base = PuppetSpec::Files.tmpdir('tmp_settings')
+    Puppet[:vardir] = File.join(base, 'var')
+
+    FileUtils.mkdir_p Puppet[:statedir]
+  end
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/sshkeys_core/spec/unit/provider/sshkey/parsed_spec.rb	Mon Jan 03 17:15:14 2022 +0000
@@ -0,0 +1,94 @@
+require 'spec_helper'
+
+describe 'sshkey parsed provider' do
+  subject { provider }
+
+  let(:type) { Puppet::Type.type(:sshkey) }
+  let(:provider) { type.provider(:parsed) }
+
+  after :each do
+    subject.clear
+  end
+
+  def key
+    'AAAAB3NzaC1yc2EAAAABIwAAAQEAzwHhxXvIrtfIwrudFqc8yQcIfMudrgpnuh1F3AV6d2BrLgu/yQE7W5UyJMUjfj427sQudRwKW45O0Jsnr33F4mUw+GIMlAAmp9g24/OcrTiB8ZUKIjoPy/cO4coxGi8/NECtRzpD/ZUPFh6OEpyOwJPMb7/EC2Az6Otw4StHdXUYw22zHazBcPFnv6zCgPx1hA7QlQDWTu4YcL0WmTYQCtMUb3FUqrcFtzGDD0ytosgwSd+JyN5vj5UwIABjnNOHPZ62EY1OFixnfqX/+dUwrFSs5tPgBF/KkC6R7tmbUfnBON6RrGEmu+ajOTOLy23qUZB4CQ53V7nyAWhzqSK+hw==' # rubocop:disable Metrics/LineLength
+  end
+
+  it 'parses the name from the first field' do
+    expect(subject.parse_line('test ssh-rsa ' + key)[:name]).to eq('test')
+  end
+
+  it 'parses the first component of the first field as the name' do
+    expect(subject.parse_line('test,alias ssh-rsa ' + key)[:name]).to eq('test')
+  end
+
+  it 'parses host_aliases from the remaining components of the first field' do
+    expect(subject.parse_line('test,alias ssh-rsa ' + key)[:host_aliases]).to eq(['alias'])
+  end
+
+  it 'parses multiple host_aliases' do
+    expect(subject.parse_line('test,alias1,alias2,alias3 ssh-rsa ' + key)[:host_aliases]).to eq(['alias1', 'alias2', 'alias3'])
+  end
+
+  it 'does not drop an empty host_alias' do
+    expect(subject.parse_line('test,alias, ssh-rsa ' + key)[:host_aliases]).to eq(['alias', ''])
+  end
+
+  it 'recognises when there are no host aliases' do
+    expect(subject.parse_line('test ssh-rsa ' + key)[:host_aliases]).to eq([])
+  end
+
+  context 'with the sample file' do
+    ['sample', 'sample_with_blank_lines'].each do |sample_file|
+      let(:fixture) { my_fixture(sample_file) }
+
+      before(:each) { subject.stubs(:default_target).returns(fixture) }
+
+      it 'parses to records on prefetch' do
+        expect(subject.target_records(fixture)).to be_empty
+        subject.prefetch
+
+        records = subject.target_records(fixture)
+        expect(records).to be_an Array
+        expect(records).to(be_all { |x| expect(x).to be_an(Hash) })
+      end
+
+      it 'reconstitutes the file from records' do
+        subject.prefetch
+        records = subject.target_records(fixture)
+        text = subject.to_file(records).gsub(%r{^# HEADER.+\n}, '')
+
+        oldlines = File.readlines(fixture).map(&:chomp)
+        newlines = text.chomp.split("\n")
+        expect(oldlines.length).to eq(newlines.length)
+
+        oldlines.zip(newlines).each do |old, new|
+          expect(old.gsub(%r{\s+}, '')).to eq(new.gsub(%r{\s+}, ''))
+        end
+      end
+    end
+  end
+
+  context 'default ssh_known_hosts target path' do
+    ['9.10', '9.11', '10.10'].each do |version|
+      it 'is `/etc/ssh_known_hosts` when OSX version 10.10 or older`' do
+        Facter.expects(:value).with(:operatingsystem).returns('Darwin')
+        Facter.expects(:value).with(:macosx_productversion_major).returns(version)
+        expect(subject.default_target).to eq('/etc/ssh_known_hosts')
+      end
+    end
+
+    ['10.11', '10.13', '11.0', '11.11'].each do |version|
+      it 'is `/etc/ssh/ssh_known_hosts` when OSX version 10.11 or newer`' do
+        Facter.expects(:value).with(:operatingsystem).returns('Darwin')
+        Facter.expects(:value).with(:macosx_productversion_major).returns(version)
+        expect(subject.default_target).to eq('/etc/ssh/ssh_known_hosts')
+      end
+    end
+
+    it 'is `/etc/ssh/ssh_known_hosts` on other operating systems' do
+      Facter.expects(:value).with(:operatingsystem).returns('RedHat')
+      expect(subject.default_target).to eq('/etc/ssh/ssh_known_hosts')
+    end
+  end
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/sshkeys_core/spec/unit/type/ssh_authorized_key_spec.rb	Mon Jan 03 17:15:14 2022 +0000
@@ -0,0 +1,262 @@
+require 'spec_helper'
+
+describe Puppet::Type.type(:ssh_authorized_key), unless: Puppet.features.microsoft_windows? do
+  include PuppetSpec::Files
+
+  before(:each) do
+    provider_class = stub 'provider_class', name: 'fake', suitable?: true, supports_parameter?: true
+    described_class.stubs(:defaultprovider).returns(provider_class)
+    described_class.stubs(:provider).returns(provider_class)
+
+    provider = stub 'provider', class: provider_class, file_path: make_absolute('/tmp/whatever'), clear: nil
+    provider_class.stubs(:new).returns(provider)
+  end
+
+  it 'has :name as its namevar' do
+    expect(described_class.key_attributes).to eq [:name]
+  end
+
+  describe 'when validating attributes' do
+    [:name, :provider, :drop_privileges].each do |param|
+      it "has a #{param} parameter" do
+        expect(described_class.attrtype(param)).to eq :param
+      end
+    end
+
+    [:type, :key, :user, :target, :options, :ensure].each do |property|
+      it "has a #{property} property" do
+        expect(described_class.attrtype(property)).to eq :property
+      end
+    end
+  end
+
+  describe 'when validating values' do
+    describe 'for name' do
+      it 'supports valid names' do
+        described_class.new(name: 'username', ensure: :present, user: 'nobody')
+        described_class.new(name: 'username@hostname', ensure: :present, user: 'nobody')
+      end
+
+      it 'supports whitespace' do
+        described_class.new(name: 'my test', ensure: :present, user: 'nobody')
+      end
+    end
+
+    describe 'for ensure' do
+      it 'supports :present' do
+        described_class.new(name: 'whev', ensure: :present, user: 'nobody')
+      end
+
+      it 'supports :absent' do
+        described_class.new(name: 'whev', ensure: :absent, user: 'nobody')
+      end
+
+      it 'nots support other values' do
+        expect { described_class.new(name: 'whev', ensure: :foo, user: 'nobody') }.to raise_error(Puppet::Error, %r{Invalid value})
+      end
+    end
+
+    describe 'for drop_privileges' do
+      it 'uses true as a default value' do
+        expect(described_class.new(name: 'whev', user: 'nobody')[:drop_privileges]).to eq true
+      end
+
+      [true, :true, 'true', :yes, 'yes'].each do |value|
+        it "supports #{value} and returns a boolean true" do
+          expect(described_class.new(name: 'whev', user: 'nobody', drop_privileges: value)[:drop_privileges]).to eq true
+        end
+      end
+
+      [false, :false, 'false', :no, 'no'].each do |value|
+        it "supports #{value} and returns a boolean false" do
+          expect(described_class.new(name: 'whev', user: 'nobody', drop_privileges: value)[:drop_privileges]).to eq false
+        end
+      end
+
+      it 'raises an exception on something else' do
+        expect { described_class.new(name: 'whev', user: 'nobody', drop_privileges: 'nope') }.to raise_error(Puppet::Error, %r{Invalid value})
+      end
+    end
+
+    describe 'for type' do
+      [
+        :'ssh-dss', :dsa,
+        :'ssh-rsa', :rsa,
+        :'ecdsa-sha2-nistp256',
+        :'ecdsa-sha2-nistp384',
+        :'ecdsa-sha2-nistp521',
+        :ed25519, :'ssh-ed25519'
+      ].each do |keytype|
+        it "supports #{keytype}" do
+          described_class.new(name: 'whev', type: keytype, user: 'nobody')
+        end
+      end
+
+      it 'aliases :rsa to :ssh-rsa' do
+        key = described_class.new(name: 'whev', type: :rsa, user: 'nobody')
+        expect(key.should(:type)).to eq :'ssh-rsa'
+      end
+
+      it 'aliases :dsa to :ssh-dss' do
+        key = described_class.new(name: 'whev', type: :dsa, user: 'nobody')
+        expect(key.should(:type)).to eq :'ssh-dss'
+      end
+
+      it "doesn't support values other than ssh-dss, ssh-rsa, dsa, rsa" do
+        expect { described_class.new(name: 'whev', type: :something) }.to raise_error(Puppet::Error, %r{Invalid value})
+      end
+    end
+
+    describe 'for key' do
+      # rubocop:disable Metrics/LineLength
+      it 'supports a valid key like a 1024 bit rsa key' do
+        expect { described_class.new(name: 'whev', type: :rsa, user: 'nobody', key: 'AAAAB3NzaC1yc2EAAAADAQABAAAAgQDCPfzW2ry7XvMc6E5Kj2e5fF/YofhKEvsNMUogR3PGL/HCIcBlsEjKisrY0aYgD8Ikp7ZidpXLbz5dBsmPy8hJiBWs5px9ZQrB/EOQAwXljvj69EyhEoGawmxQMtYw+OAIKHLJYRuk1QiHAMHLp5piqem8ZCV2mLb9AsJ6f7zUVw==') }.not_to raise_error
+      end
+      # rubocop:enable Metrics/LineLength
+
+      # rubocop:disable Metrics/LineLength
+      it 'supports a valid key like a 4096 bit rsa key' do
+        expect { described_class.new(name: 'whev', type: :rsa, user: 'nobody', key: 'AAAAB3NzaC1yc2EAAAADAQABAAACAQDEY4pZFyzSfRc9wVWI3DfkgT/EL033UZm/7x1M+d+lBD00qcpkZ6CPT7lD3Z+vylQlJ5S8Wcw6C5Smt6okZWY2WXA9RCjNJMIHQbJAzwuQwgnwU/1VMy9YPp0tNVslg0sUUgpXb13WW4mYhwxyGmIVLJnUrjrQmIFhtfHsJAH8ZVqCWaxKgzUoC/YIu1u1ScH93lEdoBPLlwm6J0aiM7KWXRb7Oq1nEDZtug1zpX5lhgkQWrs0BwceqpUbY+n9sqeHU5e7DCyX/yEIzoPRW2fe2Gx1Iq6JKM/5NNlFfaW8rGxh3Z3S1NpzPHTRjw8js3IeGiV+OPFoaTtM1LsWgPDSBlzIdyTbSQR7gKh0qWYCNV/7qILEfa0yIFB5wIo4667iSPZw2pNgESVtenm8uXyoJdk8iWQ4mecdoposV/znknNb2GPgH+n/2vme4btZ0Sl1A6rev22GQjVgbWOn8zaDglJ2vgCN1UAwmq41RXprPxENGeLnWQppTnibhsngu0VFllZR5kvSIMlekLRSOFLFt92vfd+tk9hZIiKm9exxcbVCGGQPsf6dZ27rTOmg0xM2Sm4J6RRKuz79HQgA4Eg18+bqRP7j/itb89DmtXEtoZFAsEJw8IgIfeGGDtHTkfAlAC92mtK8byeaxGq57XCTKbO/r5gcOMElZHy1AcB8kw==') }.not_to raise_error # rubocop:disable Metrics/LineLength
+      end
+      # rubocop:enable Metrics/LineLength
+
+      # rubocop:disable Metrics/LineLength
+      it 'supports a valid key like a 1024 bit dsa key' do
+        expect { described_class.new(name: 'whev', type: :dsa, user: 'nobody', key: 'AAAAB3NzaC1kc3MAAACBAI80iR78QCgpO4WabVqHHdEDigOjUEHwIjYHIubR/7u7DYrXY+e+TUmZ0CVGkiwB/0yLHK5dix3Y/bpj8ZiWCIhFeunnXccOdE4rq5sT2V3l1p6WP33RpyVYbLmeuHHl5VQ1CecMlca24nHhKpfh6TO/FIwkMjghHBfJIhXK+0w/AAAAFQDYzLupuMY5uz+GVrcP+Kgd8YqMmwAAAIB3SVN71whLWjFPNTqGyyIlMy50624UfNOaH4REwO+Of3wm/cE6eP8n75vzTwQGBpJX3BPaBGW1S1Zp/DpTOxhCSAwZzAwyf4WgW7YyAOdxN3EwTDJZeyiyjWMAOjW9/AOWt9gtKg0kqaylbMHD4kfiIhBzo31ZY81twUzAfN7angAAAIBfva8sTSDUGKsWWIXkdbVdvM4X14K4gFdy0ZJVzaVOtZ6alysW6UQypnsl6jfnbKvsZ0tFgvcX/CPyqNY/gMR9lyh/TCZ4XQcbqeqYPuceGehz+jL5vArfqsW2fJYFzgCcklmr/VxtP5h6J/T0c9YcDgc/xIfWdZAlznOnphI/FA==') }.not_to raise_error # rubocop:disable Metrics/LineLength
+      end
+      # rubocop:enable Metrics/LineLength
+
+      it "doesn't support whitespaces" do
+        expect { described_class.new(name: 'whev', type: :rsa, user: 'nobody', key: 'AAA FA==') }.to raise_error(Puppet::Error, %r{Key must not contain whitespace})
+      end
+    end
+
+    describe 'for options' do
+      it 'supports flags as options' do
+        expect { described_class.new(name: 'whev', type: :rsa, user: 'nobody', options: 'cert-authority') }.not_to raise_error
+        expect { described_class.new(name: 'whev', type: :rsa, user: 'nobody', options: 'no-port-forwarding') }.not_to raise_error
+      end
+
+      it 'supports key-value pairs as options' do
+        expect { described_class.new(name: 'whev', type: :rsa, user: 'nobody', options: 'command="command"') }.not_to raise_error
+      end
+
+      it 'supports key-value pairs where value consist of multiple items' do
+        expect { described_class.new(name: 'whev', type: :rsa, user: 'nobody', options: 'from="*.domain1,host1.domain2"') }.not_to raise_error
+      end
+
+      it 'supports environments as options' do
+        expect { described_class.new(name: 'whev', type: :rsa, user: 'nobody', options: 'environment="NAME=value"') }.not_to raise_error
+      end
+
+      it 'supports multiple options as an array' do
+        expect { described_class.new(name: 'whev', type: :rsa, user: 'nobody', options: ['cert-authority', 'environment="NAME=value"']) }.not_to raise_error
+      end
+
+      it "doesn't support a comma separated list" do
+        expect { described_class.new(name: 'whev', type: :rsa, user: 'nobody', options: 'cert-authority,no-port-forwarding') }.to raise_error(Puppet::Error, %r{must be provided as an array})
+      end
+
+      it 'uses :absent as a default value' do
+        expect(described_class.new(name: 'whev', type: :rsa, user: 'nobody').should(:options)).to eq [:absent]
+      end
+
+      it 'property should return well formed string of arrays from is_to_s' do
+        resource = described_class.new(name: 'whev', type: :rsa, user: 'nobody', options: ['a', 'b', 'c'])
+        str = (Puppet.version.to_f < 5.0) ? ['a', 'b', 'c'] : "['a', 'b', 'c']"
+        expect(resource.property(:options).is_to_s(['a', 'b', 'c'])).to eq(str)
+      end
+
+      it 'property should return well formed string of arrays from should_to_s' do
+        resource = described_class.new(name: 'whev', type: :rsa, user: 'nobody', options: ['a', 'b', 'c'])
+        str = (Puppet.version.to_f < 5.0) ? 'a b c' : "['a', 'b', 'c']"
+        expect(resource.property(:options).should_to_s(['a', 'b', 'c'])).to eq(str)
+      end
+    end
+
+    describe 'for user' do
+      it 'supports present users' do
+        described_class.new(name: 'whev', type: :rsa, user: 'root')
+      end
+
+      it 'supports absent users' do
+        described_class.new(name: 'whev', type: :rsa, user: 'ihopeimabsent')
+      end
+    end
+
+    describe 'for target' do
+      it 'supports absolute paths' do
+        described_class.new(name: 'whev', type: :rsa, target: '/tmp/here')
+      end
+
+      it "uses the user's path if not explicitly specified" do
+        expect(described_class.new(name: 'whev', user: 'root').should(:target)).to eq File.expand_path('~root/.ssh/authorized_keys')
+      end
+
+      it "doesn't consider the user's path if explicitly specified" do
+        expect(described_class.new(name: 'whev', user: 'root', target: '/tmp/here').should(:target)).to eq '/tmp/here'
+      end
+
+      it 'informs about an absent user' do
+        Puppet::Log.level = :debug
+        logs = []
+        Puppet::Util::Log.with_destination(Puppet::Test::LogCollector.new(logs)) do
+          described_class.new(name: 'whev', user: 'idontexist').should(:target)
+        end
+        expect(logs.map(&:message)).to include('The required user is not yet present on the system')
+      end
+    end
+  end
+
+  describe 'when neither user nor target is specified' do
+    it 'raises an error' do
+      expect {
+        described_class.new(
+          name: 'Test',
+          key: 'AAA',
+          type: 'ssh-rsa',
+          ensure: :present,
+        )
+      }.to raise_error(Puppet::Error, %r{user.*or.*target.*mandatory})
+    end
+  end
+
+  describe 'when both target and user are specified' do
+    it 'uses target' do
+      resource = described_class.new(
+        name: 'Test',
+        user: 'root',
+        target: '/tmp/blah',
+      )
+      expect(resource.should(:target)).to eq '/tmp/blah'
+    end
+  end
+
+  describe 'when user is specified' do
+    it 'determines target' do
+      resource = described_class.new(
+        name: 'Test',
+        user: 'root',
+      )
+      target = File.expand_path('~root/.ssh/authorized_keys')
+      expect(resource.should(:target)).to eq target
+    end
+
+    # Bug #2124 - ssh_authorized_key always changes target if target is not defined
+    it "doesn't raise spurious change events" do
+      resource = described_class.new(name: 'Test', user: 'root')
+      target = File.expand_path('~root/.ssh/authorized_keys')
+      expect(resource.property(:target).safe_insync?(target)).to eq true
+    end
+  end
+
+  describe 'when calling validate' do
+    it "doesn't crash on a non-existent user" do
+      resource = described_class.new(
+        name: 'Test',
+        user: 'ihopesuchuserdoesnotexist',
+      )
+      resource.validate
+    end
+  end
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/sshkeys_core/spec/unit/type/sshkey_spec.rb	Mon Jan 03 17:15:14 2022 +0000
@@ -0,0 +1,72 @@
+require 'spec_helper'
+
+describe Puppet::Type.type(:sshkey) do
+  it 'uses :name as its namevar' do
+    expect(described_class.key_attributes).to eq [:name]
+  end
+
+  describe 'when validating attributes' do
+    [:name, :provider].each do |param|
+      it "has a #{param} parameter" do
+        expect(described_class.attrtype(param)).to eq :param
+      end
+    end
+
+    [:host_aliases, :ensure, :key, :type].each do |property|
+      it "has a #{property} property" do
+        expect(described_class.attrtype(property)).to eq :property
+      end
+    end
+  end
+
+  describe 'when validating values' do
+    [
+      :'ssh-dss', :dsa,
+      :'ssh-rsa', :rsa,
+      :'ecdsa-sha2-nistp256',
+      :'ecdsa-sha2-nistp384',
+      :'ecdsa-sha2-nistp521',
+      :'ssh-ed25519', :ed25519
+    ].each do |keytype|
+      it "supports #{keytype} as a type value" do
+        described_class.new(name: 'foo', type: keytype)
+      end
+    end
+
+    it 'aliases :rsa to :ssh-rsa' do
+      key = described_class.new(name: 'foo', type: :rsa)
+      expect(key.should(:type)).to eq :'ssh-rsa'
+    end
+
+    it 'aliases :dsa to :ssh-dss' do
+      key = described_class.new(name: 'foo', type: :dsa)
+      expect(key.should(:type)).to eq :'ssh-dss'
+    end
+
+    it "doesn't support values other than ssh-dss, ssh-rsa, dsa, rsa for type" do
+      expect {
+        described_class.new(name: 'whev', type: :'ssh-dsa')
+      }.to raise_error(Puppet::Error, %r{Invalid value.*ssh-dsa})
+    end
+
+    it 'accepts one host_alias' do
+      described_class.new(name: 'foo', host_aliases: 'foo.bar.tld')
+    end
+
+    it 'accepts multiple host_aliases as an array' do
+      described_class.new(name: 'foo', host_aliases: ['foo.bar.tld', '10.0.9.9'])
+    end
+
+    it "doesn't accept spaces in any host_alias" do
+      expect {
+        described_class.new(name: 'foo', host_aliases: ['foo.bar.tld', 'foo bar'])
+      }.to raise_error(Puppet::Error, %r{cannot include whitespace})
+    end
+
+    it "doesn't accept aliases in the resourcename" do
+      expect {
+        described_class.new(name: 'host,host.domain,ip')
+      }.to raise_error(Puppet::Error, %r{No comma in resourcename})
+    end
+  end
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/sshkeys_core/spec/unit/type/user_spec.rb	Mon Jan 03 17:15:14 2022 +0000
@@ -0,0 +1,145 @@
+# encoding: UTF-8
+
+require 'spec_helper'
+
+describe Puppet::Type.type(:user) do
+  let(:provider_class) do
+    described_class.provide(:simple) do
+      has_features :manages_expiry, :manages_password_age, :manages_passwords, :manages_solaris_rbac, :manages_shell
+      mk_resource_methods
+
+      def create; end
+
+      def delete; end
+
+      def exists?
+        get(:ensure) != :absent
+      end
+
+      def flush; end
+
+      def self.instances
+        []
+      end
+    end
+  end
+
+  before :each do
+    described_class.stubs(:defaultprovider).returns provider_class
+  end
+
+  describe 'when purging ssh keys' do
+    it 'does not accept a keyfile with a relative path' do
+      expect {
+        described_class.new(name: 'a', purge_ssh_keys: 'keys')
+      }.to raise_error(Puppet::Error, %r{Paths to keyfiles must be absolute, not keys})
+    end
+
+    context 'with a home directory specified' do
+      it 'accepts true' do
+        described_class.new(name: 'a', home: '/tmp', purge_ssh_keys: true)
+      end
+
+      it 'accepts the ~ wildcard' do
+        described_class.new(name: 'a', home: '/tmp', purge_ssh_keys: '~/keys')
+      end
+
+      it 'accepts the %h wildcard' do
+        described_class.new(name: 'a', home: '/tmp', purge_ssh_keys: '%h/keys')
+      end
+
+      it 'raises when given a relative path' do
+        expect {
+          described_class.new(name: 'a', home: '/tmp', purge_ssh_keys: 'keys')
+        }.to raise_error(Puppet::Error, %r{Paths to keyfiles must be absolute})
+      end
+    end
+
+    context 'with no home directory specified' do
+      it 'does not accept true' do
+        expect {
+          described_class.new(name: 'a', purge_ssh_keys: true)
+        }.to raise_error(Puppet::Error, %r{purge_ssh_keys can only be true for users with a defined home directory})
+      end
+
+      it 'does not accept the ~ wildcard' do
+        expect {
+          described_class.new(name: 'a', purge_ssh_keys: '~/keys')
+        }.to raise_error(Puppet::Error, %r{meta character ~ or %h only allowed for users with a defined home directory})
+      end
+
+      it 'does not accept the %h wildcard' do
+        expect {
+          described_class.new(name: 'a', purge_ssh_keys: '%h/keys')
+        }.to raise_error(Puppet::Error, %r{meta character ~ or %h only allowed for users with a defined home directory})
+      end
+    end
+
+    context 'with a valid parameter' do
+      subject do
+        res = described_class.new(name: 'test', purge_ssh_keys: paths)
+        res.catalog = Puppet::Resource::Catalog.new
+        res
+      end
+
+      let(:paths) do
+        ['/dev/null', '/tmp/keyfile'].map { |path| File.expand_path(path) }
+      end
+
+      it 'does not just return from generate' do
+        subject.expects :find_unmanaged_keys
+        subject.generate
+      end
+
+      it 'checks each keyfile for readability' do
+        paths.each do |path|
+          File.expects(:readable?).with(path)
+        end
+        subject.generate
+      end
+    end
+
+    describe 'generated keys' do
+      subject do
+        res = described_class.new(name: 'test_user_name', purge_ssh_keys: purge_param)
+        res.catalog = Puppet::Resource::Catalog.new
+        res
+      end
+
+      context 'when purging is disabled' do
+        let(:purge_param) { false }
+
+        it 'has an empty generate' do
+          expect(subject.generate).to be_empty
+        end
+      end
+
+      context 'when purging is enabled' do
+        let(:purge_param) { File.expand_path(my_fixture('authorized_keys')) }
+        let(:resources) { subject.generate }
+
+        it 'contains a resource for each key' do
+          names = resources.map { |res| res.name }
+          expect(names).to include('key1 name')
+          expect(names).to include('keyname2')
+        end
+
+        it 'does not include keys in comment lines' do
+          names = resources.map { |res| res.name }
+          expect(names).not_to include('keyname3')
+        end
+
+        it 'generates names for unnamed keys' do
+          names = resources.map { |res| res.name }
+          fixture_path = File.expand_path(File.join(my_fixture_dir, 'authorized_keys'))
+          expect(names).to include("#{fixture_path}:unnamed-1")
+        end
+
+        it 'has a value for the user property on each resource' do
+          resource_users = resources.map { |res| res[:user] }.reject { |user_name| user_name == 'test_user_name' }
+          expect(resource_users).to be_empty
+        end
+      end
+    end
+  end
+end