From ec6aff9036dd732b69e747f8afb72002c133796a Mon Sep 17 00:00:00 2001 From: Charlie Sharpsteen Date: Sun, 4 Oct 2015 12:07:31 -0700 Subject: [PATCH 01/13] Add abstract capability for getting facts from VMs This commit adds a new guest capability, `pebuild_facts`, which loads facts from guest VMs. If Facter is installed, it will be used to generate a full list of facts. If Facter is not installed, a basic set of info is computed about the `os` and `architecture`, which should be sufficient to drive a PE installation. --- lib/pe_build/cap/facts/base.rb | 105 +++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 lib/pe_build/cap/facts/base.rb diff --git a/lib/pe_build/cap/facts/base.rb b/lib/pe_build/cap/facts/base.rb new file mode 100644 index 0000000..29ff788 --- /dev/null +++ b/lib/pe_build/cap/facts/base.rb @@ -0,0 +1,105 @@ +require 'json' + +# Base class for retrieving facts from guest VMs +# +# This class implements a Guest Capability for Fact retrieval. Facter will be +# queried, if installed. Otherwise, a minimal set of base facts will be +# returned by {#basic_facts}. +# +# @abstract Subclass and override {#architecture}, {#os_info} and +# {#release_info} to implement for a particular guest operating system. +# +# @since 0.13.0 +class PEBuild::Cap::Facts::Base + + # Retrieve facts from a guest VM + # + # See {#load_facts} for implementation details. + # + # @return [Hash] A hash of facts. + def self.pebuild_facts(machine) + new(machine).load_facts + end + + attr_reader :machine + + def initialize(machine) + @machine = machine + end + + # Load Facts from the guest VM + # + # @return [Hash] A hash of facts from Facter, if installed. + # @return [Hash] A hash containing the results of {#basic_facts} if + # Facter is not installed. + def load_facts + # TODO: Facter might be located in several places that aren't on the + # default PATH. Fix that. + if machine.communicate.test('facter --version') + facts = JSON.load(sudo('facter --json')[:stdout]) + else + # Facter isn't installed yet, so we gather a minimal set of info. + facts = basic_facts + end + + # JSON.load can do funny things. Sort by top-level key. + Hash[facts.sort] + end + + # Determine basic info about a guest + # + # This function returns a minimal set of basic facts which should be + # sufficient to determine what software to install on the guest. + # + # @return [Hash] A hash containing the `architecture` and `os` facts. + def basic_facts + { + 'architecture' => architecture, + 'os' => { + 'release' => release_info + }.update(os_info) + } + end + + # Returns the native architecture of the OS + # + # @return [String] An architecture, such as `i386` or `x86_64`. + def architecture + raise NotImplementedError + end + + # Returns info about the OS type + # + # @return [Hash] A hash containing the `family` of the operating system and, + # optionally, the `name`. + def os_info + raise NotImplementedError + end + + # Returns info about the OS version + # + # @return [Hash] A hash containing the `full` version strying of the + # operating system and, optionally, the `minor` and `major` + # release versions. + def release_info + raise NotImplementedError + end + + private + + def sudo(cmd) + stdout = '' + stderr = '' + + retval = machine.communicate.sudo(cmd) do |type, data| + if type == :stderr + stderr << data.chomp + else + stdout << data.chomp + end + end + + {:stdout => stdout, :stderr => stderr, :retval => retval} + end + +end From 057c78d72becfdef59e014826663deee8411ffa9 Mon Sep 17 00:00:00 2001 From: Charlie Sharpsteen Date: Sun, 4 Oct 2015 12:31:10 -0700 Subject: [PATCH 02/13] Add Facts capabiliy for RedHat guests --- lib/pe_build/cap.rb | 4 ++++ lib/pe_build/cap/facts/posix.rb | 20 +++++++++++++++++ lib/pe_build/cap/facts/redhat.rb | 37 ++++++++++++++++++++++++++++++++ lib/pe_build/plugin.rb | 7 ++++++ 4 files changed, 68 insertions(+) create mode 100644 lib/pe_build/cap/facts/posix.rb create mode 100644 lib/pe_build/cap/facts/redhat.rb diff --git a/lib/pe_build/cap.rb b/lib/pe_build/cap.rb index 9e07248..a5e3ebe 100644 --- a/lib/pe_build/cap.rb +++ b/lib/pe_build/cap.rb @@ -17,6 +17,10 @@ class DetectFailed < Vagrant::Errors::VagrantError require 'pe_build/cap/detect_installer/solaris' end + module Facts + require 'pe_build/cap/facts/redhat' + end + module RunInstall require 'pe_build/cap/run_install/posix' require 'pe_build/cap/run_install/windows' diff --git a/lib/pe_build/cap/facts/posix.rb b/lib/pe_build/cap/facts/posix.rb new file mode 100644 index 0000000..a74e43a --- /dev/null +++ b/lib/pe_build/cap/facts/posix.rb @@ -0,0 +1,20 @@ +require_relative 'base' + +# Base class for retrieving facts from POSIX +# +# @abstract Subclass and override {#os_info} and {#release_info} to implement +# for a particular POSIX system. +# +# @since 0.13.0 +class PEBuild::Cap::Facts::POSIX < PEBuild::Cap::Facts::Base + + # (see PEBuild::Cap::Facts::Base#architecture) + # + # This method is a concrete implementation which uses `uname -m`. + # + # @see PEBuild::Cap::Facts::Base#architecture + def architecture + sudo('uname -m')[:stdout] + end + +end diff --git a/lib/pe_build/cap/facts/redhat.rb b/lib/pe_build/cap/facts/redhat.rb new file mode 100644 index 0000000..6c7651c --- /dev/null +++ b/lib/pe_build/cap/facts/redhat.rb @@ -0,0 +1,37 @@ +require_relative 'posix' + +# Facts implementation for RedHat guests +# +# @since 0.13.0 +class PEBuild::Cap::Facts::RedHat < PEBuild::Cap::Facts::POSIX + + # (see PEBuild::Cap::Facts::Base#os_info) + # + # Currently returns `family` as `RedHat`. + # @todo Implement `name` detection (RHEL, CentOS, Sci. Linux, etc.). + # + # @see PEBuild::Cap::Facts::Base#os_info + def os_info + { + 'family' => 'RedHat' + } + end + + # (see PEBuild::Cap::Facts::Base#release_info) + # + # Reads `/etc/redhat-release` and generates a `full` version along with + # `major` and `minor` components. + # + # @see PEBuild::Cap::Facts::Base#release_info + def release_info + release_file = sudo('cat /etc/redhat-release')[:stdout] + version = release_file.match(/release (\d+\.\d+)/)[1] + + { + 'major' => version.split('.', 2)[0], + 'minor' => version.split('.', 2)[1], + 'full' => version + } + end + +end diff --git a/lib/pe_build/plugin.rb b/lib/pe_build/plugin.rb index 255cce5..949e03a 100644 --- a/lib/pe_build/plugin.rb +++ b/lib/pe_build/plugin.rb @@ -92,6 +92,13 @@ class Plugin < Vagrant.plugin('2') PEBuild::Cap::RunInstall::Windows end + # Retrieve Facts + guest_capability('redhat', 'pebuild_facts') do + require_relative 'cap' + PEBuild::Cap::Facts::RedHat + end + + # internal action hooks action_hook('PE Build: initialize build dir') do |hook| From 42e22bd37e3fa7b98c331a7511d45bc6acda320a Mon Sep 17 00:00:00 2001 From: Charlie Sharpsteen Date: Sun, 4 Oct 2015 12:33:33 -0700 Subject: [PATCH 03/13] Add Facts capabiliy for Windows guests --- lib/pe_build/cap.rb | 1 + lib/pe_build/cap/facts/windows.rb | 58 +++++++++++++++++++++++++++++++ lib/pe_build/plugin.rb | 6 ++++ 3 files changed, 65 insertions(+) create mode 100644 lib/pe_build/cap/facts/windows.rb diff --git a/lib/pe_build/cap.rb b/lib/pe_build/cap.rb index a5e3ebe..c0cbfb3 100644 --- a/lib/pe_build/cap.rb +++ b/lib/pe_build/cap.rb @@ -19,6 +19,7 @@ class DetectFailed < Vagrant::Errors::VagrantError module Facts require 'pe_build/cap/facts/redhat' + require 'pe_build/cap/facts/windows' end module RunInstall diff --git a/lib/pe_build/cap/facts/windows.rb b/lib/pe_build/cap/facts/windows.rb new file mode 100644 index 0000000..2d0179c --- /dev/null +++ b/lib/pe_build/cap/facts/windows.rb @@ -0,0 +1,58 @@ +require_relative 'base' + +# Facts implementation for Windows guests +# +# @since 0.13.0 +class PEBuild::Cap::Facts::Windows < PEBuild::Cap::Facts::Base + + # (see PEBuild::Cap::Facts::Base#architecture) + # + # Looks at the default pointer size for integers and returns `x86` or `x64`. + # + # @see PEBuild::Cap::Facts::Base#architecture + def architecture + sudo('if ([System.IntPtr]::Size -eq 4) { "x86" } else { "x64" }')[:stdout] + end + + # (see PEBuild::Cap::Facts::Base#os_info) + # + # Currently returns `family` as `Windows`. + # + # @see PEBuild::Cap::Facts::Base#os_info + def os_info + { + 'family' => 'Windows', + } + end + + # (see PEBuild::Cap::Facts::Base#release_info) + # + # Queries WMI and generates a `full` version. + # + # @see PEBuild::Cap::Facts::Base#release_info + def release_info + version = sudo('(Get-WmiObject -Class Win32_OperatingSystem).Version')[:stdout] + producttype = sudo('(Get-WmiObject -Class Win32_OperatingSystem).Producttype')[:stdout] + + # Cribbed from Facter 2.4. + # + # NOTE: Currently doesn't support XP/Server 2003 or Windows 10. + name = case version + when /^6\.3/ + producttype == 1 ? "8.1" : "2012 R2" + when /^6\.2/ + producttype == 1 ? "8" : "2012" + when /^6\.1/ + producttype == 1 ? "7" : "2008 R2" + when /^6\.0/ + producttype == 1 ? "Vista" : "2008" + else + version # Default to the raw version number. + end + + { + 'full' => name + } + end + +end diff --git a/lib/pe_build/plugin.rb b/lib/pe_build/plugin.rb index 949e03a..bf5502a 100644 --- a/lib/pe_build/plugin.rb +++ b/lib/pe_build/plugin.rb @@ -99,6 +99,12 @@ class Plugin < Vagrant.plugin('2') end + guest_capability('windows', 'pebuild_facts') do + require_relative 'cap' + PEBuild::Cap::Facts::Windows + end + + # internal action hooks action_hook('PE Build: initialize build dir') do |hook| From eab2090c7740aee5e85d6326b6d2711384748f36 Mon Sep 17 00:00:00 2001 From: Charlie Sharpsteen Date: Sun, 4 Oct 2015 12:41:47 -0700 Subject: [PATCH 04/13] Add Facts capabiliy for Debian guests --- lib/pe_build/cap.rb | 1 + lib/pe_build/cap/facts/debian.rb | 37 ++++++++++++++++++++++++++++++++ lib/pe_build/plugin.rb | 4 ++++ 3 files changed, 42 insertions(+) create mode 100644 lib/pe_build/cap/facts/debian.rb diff --git a/lib/pe_build/cap.rb b/lib/pe_build/cap.rb index c0cbfb3..af48732 100644 --- a/lib/pe_build/cap.rb +++ b/lib/pe_build/cap.rb @@ -19,6 +19,7 @@ class DetectFailed < Vagrant::Errors::VagrantError module Facts require 'pe_build/cap/facts/redhat' + require 'pe_build/cap/facts/debian' require 'pe_build/cap/facts/windows' end diff --git a/lib/pe_build/cap/facts/debian.rb b/lib/pe_build/cap/facts/debian.rb new file mode 100644 index 0000000..ea6cd8e --- /dev/null +++ b/lib/pe_build/cap/facts/debian.rb @@ -0,0 +1,37 @@ +require_relative 'posix' + +# Facts implementation for Debian guests +# +# @since 0.13.0 +class PEBuild::Cap::Facts::Debian < PEBuild::Cap::Facts::POSIX + + # (see PEBuild::Cap::Facts::Base#os_info) + # + # Returns `family` as `Debian` and `name` as `Debian`. + # + # @see PEBuild::Cap::Facts::Base#os_info + def os_info + { + 'name' => 'Debian', + 'family' => 'Debian' + } + end + + # (see PEBuild::Cap::Facts::Base#release_info) + # + # Reads `/etc/debian_version` and generates a `full` version along with + # `major` and `minor` components. + # + # @see PEBuild::Cap::Facts::Base#release_info + def release_info + release_file = sudo('cat /etc/debian_version')[:stdout] + version = release_file.match(/(\d+\.\d+)/)[1] + + { + 'major' => version.split('.', 2)[0], + 'minor' => version.split('.', 2)[1], + 'full' => version + } + end + +end diff --git a/lib/pe_build/plugin.rb b/lib/pe_build/plugin.rb index bf5502a..238324c 100644 --- a/lib/pe_build/plugin.rb +++ b/lib/pe_build/plugin.rb @@ -98,6 +98,10 @@ class Plugin < Vagrant.plugin('2') PEBuild::Cap::Facts::RedHat end + guest_capability('debian', 'pebuild_facts') do + require_relative 'cap' + PEBuild::Cap::Facts::Debian + end guest_capability('windows', 'pebuild_facts') do require_relative 'cap' From 7d9f0e8ccc059f4dd1c6afa80cd49a49647271f9 Mon Sep 17 00:00:00 2001 From: Charlie Sharpsteen Date: Sun, 4 Oct 2015 12:45:22 -0700 Subject: [PATCH 05/13] Add Facts capabiliy for Ubuntu guests --- lib/pe_build/cap.rb | 1 + lib/pe_build/cap/facts/ubuntu.rb | 35 ++++++++++++++++++++++++++++++++ lib/pe_build/plugin.rb | 5 +++++ 3 files changed, 41 insertions(+) create mode 100644 lib/pe_build/cap/facts/ubuntu.rb diff --git a/lib/pe_build/cap.rb b/lib/pe_build/cap.rb index af48732..e3af19e 100644 --- a/lib/pe_build/cap.rb +++ b/lib/pe_build/cap.rb @@ -20,6 +20,7 @@ class DetectFailed < Vagrant::Errors::VagrantError module Facts require 'pe_build/cap/facts/redhat' require 'pe_build/cap/facts/debian' + require 'pe_build/cap/facts/ubuntu' require 'pe_build/cap/facts/windows' end diff --git a/lib/pe_build/cap/facts/ubuntu.rb b/lib/pe_build/cap/facts/ubuntu.rb new file mode 100644 index 0000000..2bc907e --- /dev/null +++ b/lib/pe_build/cap/facts/ubuntu.rb @@ -0,0 +1,35 @@ +require_relative 'posix' + +# Facts implementation for Ubuntu guests +# +# @since 0.13.0 +class PEBuild::Cap::Facts::Ubuntu < PEBuild::Cap::Facts::POSIX + + # (see PEBuild::Cap::Facts::Base#os_info) + # + # Returns `family` as `Debian` and `name` as `Ubuntu`. + # + # @see PEBuild::Cap::Facts::Base#os_info + def os_info + { + 'name' => 'Ubuntu', + 'family' => 'Debian' + } + end + + + # (see PEBuild::Cap::Facts::Base#release_info) + # + # Reads `/etc/issue` and generates a `full` version. + # + # @see PEBuild::Cap::Facts::Base#release_info + def release_info + release_file = sudo('cat /etc/issue')[:stdout] + version = release_file.match(/Ubuntu (\d{2}\.\d{2})/)[1] + + { + 'full' => version + } + end + +end diff --git a/lib/pe_build/plugin.rb b/lib/pe_build/plugin.rb index 238324c..c26dcee 100644 --- a/lib/pe_build/plugin.rb +++ b/lib/pe_build/plugin.rb @@ -103,6 +103,11 @@ class Plugin < Vagrant.plugin('2') PEBuild::Cap::Facts::Debian end + guest_capability('ubuntu', 'pebuild_facts') do + require_relative 'cap' + PEBuild::Cap::Facts::Ubuntu + end + guest_capability('windows', 'pebuild_facts') do require_relative 'cap' PEBuild::Cap::Facts::Windows From b5f29d4204b06a9b8327348ad5122a08d2671ccd Mon Sep 17 00:00:00 2001 From: Charlie Sharpsteen Date: Sun, 4 Oct 2015 12:49:03 -0700 Subject: [PATCH 06/13] Add Facts capabiliy for Solaris guests --- lib/pe_build/cap.rb | 1 + lib/pe_build/cap/facts/solaris.rb | 46 +++++++++++++++++++++++++++++++ lib/pe_build/plugin.rb | 7 +++++ 3 files changed, 54 insertions(+) create mode 100644 lib/pe_build/cap/facts/solaris.rb diff --git a/lib/pe_build/cap.rb b/lib/pe_build/cap.rb index e3af19e..74e8b21 100644 --- a/lib/pe_build/cap.rb +++ b/lib/pe_build/cap.rb @@ -21,6 +21,7 @@ module Facts require 'pe_build/cap/facts/redhat' require 'pe_build/cap/facts/debian' require 'pe_build/cap/facts/ubuntu' + require 'pe_build/cap/facts/solaris' require 'pe_build/cap/facts/windows' end diff --git a/lib/pe_build/cap/facts/solaris.rb b/lib/pe_build/cap/facts/solaris.rb new file mode 100644 index 0000000..c510a3e --- /dev/null +++ b/lib/pe_build/cap/facts/solaris.rb @@ -0,0 +1,46 @@ +require_relative 'posix' + +# Facts implementation for Solaris guests +# +# @since 0.13.0 +class PEBuild::Cap::Facts::Solaris < PEBuild::Cap::Facts::POSIX + + # (see PEBuild::Cap::Facts::Base#os_info) + # + # Currently returns `family` as `Solaris`. + # + # @see PEBuild::Cap::Facts::Base#os_info + def os_info + { + 'family' => 'Solaris' + } + end + + # (see PEBuild::Cap::Facts::Base#release_info) + # + # Reads `/etc/release` and generates a `full` version along with a + # `major` component. + # + # @todo Capture full version string. I.E 11.2, 10u11, etc and add `minor` + # component. + # + # @see PEBuild::Cap::Facts::Base#release_info + def release_info + release_file = sudo('cat /etc/release')[:stdout] + + # Cribbed from Facter 2.4. + if match = release_file.match(/\s+s(\d+)[sx]?(_u\d+)?.*(?:SPARC|X86)/) + version = match.captures.join('') + elsif match = release_file.match(/Solaris ([0-9\.]+(?:\s*[0-9\.\/]+))\s*(?:SPARC|X86)/) + version = match.captures.first + else + version = sudo('uname -v')[:stdout] + end + + { + 'major' => version.scan(/\d+/).first, + 'full' => version + } + end + +end diff --git a/lib/pe_build/plugin.rb b/lib/pe_build/plugin.rb index c26dcee..c6af602 100644 --- a/lib/pe_build/plugin.rb +++ b/lib/pe_build/plugin.rb @@ -108,6 +108,13 @@ class Plugin < Vagrant.plugin('2') PEBuild::Cap::Facts::Ubuntu end + [:solaris, :solaris11].each do |os| + guest_capability(os, 'pebuild_facts') do + require_relative 'cap' + PEBuild::Cap::Facts::Solaris + end + end + guest_capability('windows', 'pebuild_facts') do require_relative 'cap' PEBuild::Cap::Facts::Windows From 68722818436b584c556ac2646a91f44c099a6e64 Mon Sep 17 00:00:00 2001 From: Charlie Sharpsteen Date: Sun, 4 Oct 2015 12:53:07 -0700 Subject: [PATCH 07/13] Add Facts capabiliy for SUSE guests --- lib/pe_build/cap.rb | 1 + lib/pe_build/cap/facts/suse.rb | 38 ++++++++++++++++++++++++++++++++++ lib/pe_build/plugin.rb | 5 +++++ 3 files changed, 44 insertions(+) create mode 100644 lib/pe_build/cap/facts/suse.rb diff --git a/lib/pe_build/cap.rb b/lib/pe_build/cap.rb index 74e8b21..de7d8d0 100644 --- a/lib/pe_build/cap.rb +++ b/lib/pe_build/cap.rb @@ -21,6 +21,7 @@ module Facts require 'pe_build/cap/facts/redhat' require 'pe_build/cap/facts/debian' require 'pe_build/cap/facts/ubuntu' + require 'pe_build/cap/facts/suse' require 'pe_build/cap/facts/solaris' require 'pe_build/cap/facts/windows' end diff --git a/lib/pe_build/cap/facts/suse.rb b/lib/pe_build/cap/facts/suse.rb new file mode 100644 index 0000000..4e4f93b --- /dev/null +++ b/lib/pe_build/cap/facts/suse.rb @@ -0,0 +1,38 @@ +require_relative 'posix' + +# Facts implementation for SUSE guests +# +# @since 0.13.0 +class PEBuild::Cap::Facts::SUSE < PEBuild::Cap::Facts::POSIX + + # (see PEBuild::Cap::Facts::Base#os_info) + # + # Returns `family` as `SUSE` and `name` as `SLES`. + # + # @see PEBuild::Cap::Facts::Base#os_info + def os_info + { + 'name' => 'SLES', + 'family' => 'SUSE' + } + end + + # (see PEBuild::Cap::Facts::Base#release_info) + # + # Reads `/etc/SuSE-release` and generates a `full` version along with + # `major` and `minor` components. + # + # @see PEBuild::Cap::Facts::Base#release_info + def release_info + release_file = sudo('cat /etc/SuSE-release')[:stdout] + major = release_file.match(/VERSION\s*=\s*(\d+)/)[1] + minor = release_file.match(/PATCHLEVEL\s*=\s*(\d+)/)[1] + + { + 'major' => major, + 'minor' => minor, + 'full' => [major, minor].join('.') + } + end + +end diff --git a/lib/pe_build/plugin.rb b/lib/pe_build/plugin.rb index c6af602..d4512ed 100644 --- a/lib/pe_build/plugin.rb +++ b/lib/pe_build/plugin.rb @@ -108,6 +108,11 @@ class Plugin < Vagrant.plugin('2') PEBuild::Cap::Facts::Ubuntu end + guest_capability('suse', 'pebuild_facts') do + require_relative 'cap' + PEBuild::Cap::Facts::SUSE + end + [:solaris, :solaris11].each do |os| guest_capability(os, 'pebuild_facts') do require_relative 'cap' From ecc15e88878f4f255e5da62981e6892f880f583f Mon Sep 17 00:00:00 2001 From: Charlie Sharpsteen Date: Sun, 4 Oct 2015 15:57:49 -0700 Subject: [PATCH 08/13] Search POSIX systems for Facter --- lib/pe_build/cap/facts/base.rb | 17 +++++++++++++---- lib/pe_build/cap/facts/posix.rb | 16 ++++++++++++++++ 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/lib/pe_build/cap/facts/base.rb b/lib/pe_build/cap/facts/base.rb index 29ff788..06804d7 100644 --- a/lib/pe_build/cap/facts/base.rb +++ b/lib/pe_build/cap/facts/base.rb @@ -27,16 +27,18 @@ def initialize(machine) @machine = machine end + def facter_path + @facter_path ||= find_facter + end + # Load Facts from the guest VM # # @return [Hash] A hash of facts from Facter, if installed. # @return [Hash] A hash containing the results of {#basic_facts} if # Facter is not installed. def load_facts - # TODO: Facter might be located in several places that aren't on the - # default PATH. Fix that. - if machine.communicate.test('facter --version') - facts = JSON.load(sudo('facter --json')[:stdout]) + unless facter_path.nil? + facts = JSON.load(sudo("#{facter_path} --json")[:stdout]) else # Facter isn't installed yet, so we gather a minimal set of info. facts = basic_facts @@ -87,6 +89,13 @@ def release_info private + # Override this method to implement a more sophisticated search for the + # Facter executable. + def find_facter + return 'facter' if @machine.communicate.test('facter --version') + return nil + end + def sudo(cmd) stdout = '' stderr = '' diff --git a/lib/pe_build/cap/facts/posix.rb b/lib/pe_build/cap/facts/posix.rb index a74e43a..a6c7eba 100644 --- a/lib/pe_build/cap/facts/posix.rb +++ b/lib/pe_build/cap/facts/posix.rb @@ -17,4 +17,20 @@ def architecture sudo('uname -m')[:stdout] end + private + + def find_facter + paths = %w[ + /opt/puppetlabs/bin/facter + /opt/puppet/bin/facter + /usr/local/bin/facter + ] + + paths.each do |path| + return path if @machine.communicate.test("#{path} --version") + end + + return nil + end + end From bf5bcc6393cc22b9af83982b65f60ca9f0455e0f Mon Sep 17 00:00:00 2001 From: Charlie Sharpsteen Date: Sun, 4 Oct 2015 16:35:46 -0700 Subject: [PATCH 09/13] Retrieve facts using Puppet This commit switches fact retrieval from using `facter` to `puppet facts`. This smooths over any differences between Facter versions and allows us to capture things like the Puppet version number and agent certname in addition to custom facts. --- lib/pe_build/cap/facts/base.rb | 27 ++++++++++++++++----------- lib/pe_build/cap/facts/posix.rb | 6 +++--- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/lib/pe_build/cap/facts/base.rb b/lib/pe_build/cap/facts/base.rb index 06804d7..6bb53d8 100644 --- a/lib/pe_build/cap/facts/base.rb +++ b/lib/pe_build/cap/facts/base.rb @@ -2,8 +2,8 @@ # Base class for retrieving facts from guest VMs # -# This class implements a Guest Capability for Fact retrieval. Facter will be -# queried, if installed. Otherwise, a minimal set of base facts will be +# This class implements a Guest Capability for Fact retrieval. Puppet will be +# queried, if installed. Otherwise, a minimal set of base facts willPuppetbe # returned by {#basic_facts}. # # @abstract Subclass and override {#architecture}, {#os_info} and @@ -27,20 +27,24 @@ def initialize(machine) @machine = machine end - def facter_path - @facter_path ||= find_facter + def puppet_path + @puppet_path ||= find_puppet end # Load Facts from the guest VM # - # @return [Hash] A hash of facts from Facter, if installed. + # @return [Hash] A hash of facts from Puppet, if installed. # @return [Hash] A hash containing the results of {#basic_facts} if # Facter is not installed. def load_facts - unless facter_path.nil? - facts = JSON.load(sudo("#{facter_path} --json")[:stdout]) + unless puppet_path.nil? + certname = sudo("#{puppet_path} agent --configprint certname")[:stdout].chomp + raw_facts = JSON.load(sudo("#{puppet_path} facts find --render-as json --terminus facter #{certname}")[:stdout]) + facts = raw_facts['values'] + # Keep the certname of the agent. + facts['certname'] = certname else - # Facter isn't installed yet, so we gather a minimal set of info. + # Puppet isn't installed yet, so we gather a minimal set of info. facts = basic_facts end @@ -90,12 +94,13 @@ def release_info private # Override this method to implement a more sophisticated search for the - # Facter executable. - def find_facter - return 'facter' if @machine.communicate.test('facter --version') + # Puppet executable. + def find_puppet + return 'puppet' if @machine.communicate.test('puppet --version') return nil end + # TODO: Split this out into a shared module. def sudo(cmd) stdout = '' stderr = '' diff --git a/lib/pe_build/cap/facts/posix.rb b/lib/pe_build/cap/facts/posix.rb index a6c7eba..28e5d88 100644 --- a/lib/pe_build/cap/facts/posix.rb +++ b/lib/pe_build/cap/facts/posix.rb @@ -21,9 +21,9 @@ def architecture def find_facter paths = %w[ - /opt/puppetlabs/bin/facter - /opt/puppet/bin/facter - /usr/local/bin/facter + /opt/puppetlabs/bin/puppet + /opt/puppet/bin/puppet + /usr/local/bin/puppet ] paths.each do |path| From 68b14956c52b0f072afe24f65d74267fa4c64da6 Mon Sep 17 00:00:00 2001 From: Charlie Sharpsteen Date: Sun, 4 Oct 2015 14:03:18 -0700 Subject: [PATCH 10/13] Add command for retrieving VM facts The `vagrant pe-build facts` command prints facts for fact info for every running VM, or an optional list of target running VMs. --- lib/pe_build/command.rb | 1 + lib/pe_build/command/base.rb | 3 +- lib/pe_build/command/facts.rb | 63 +++++++++++++++++++++++++++++++++++ templates/locales/en.yml | 6 ++++ 4 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 lib/pe_build/command/facts.rb diff --git a/lib/pe_build/command.rb b/lib/pe_build/command.rb index 41937a8..04fc066 100644 --- a/lib/pe_build/command.rb +++ b/lib/pe_build/command.rb @@ -6,5 +6,6 @@ module Command require 'pe_build/command/base' require 'pe_build/command/copy' require 'pe_build/command/list' + require 'pe_build/command/facts' end end diff --git a/lib/pe_build/command/base.rb b/lib/pe_build/command/base.rb index a333fac..c635ade 100644 --- a/lib/pe_build/command/base.rb +++ b/lib/pe_build/command/base.rb @@ -3,7 +3,7 @@ class PEBuild::Command::Base < Vagrant.plugin(2, :command) def self.synopsis - 'list and download PE installers' + 'Commands related to PE Installation' end def initialize(argv, env) @@ -13,6 +13,7 @@ def initialize(argv, env) @subcommands = { 'list' => PEBuild::Command::List, 'copy' => PEBuild::Command::Copy, + 'facts' => PEBuild::Command::Facts, } end diff --git a/lib/pe_build/command/facts.rb b/lib/pe_build/command/facts.rb new file mode 100644 index 0000000..9d58e99 --- /dev/null +++ b/lib/pe_build/command/facts.rb @@ -0,0 +1,63 @@ +require 'json' + +class PEBuild::Command::Facts < Vagrant.plugin(2, :command) + + def self.synopsis + 'Load facts from running VMs' + end + + def execute + argv = parse_options(parser) + argv.shift # Remove 'facts' subcommand. + + running = @env.active_machines.map do |name, provider| + @env.machine(name, provider) + end.select do |vm| + begin + vm.communicate.ready? + rescue Vagrant::Errors::VagrantError + # WinRM will raise an error if the VM isn't running instead of + # returning false (GH-6356). + false + end + end + + # Filter the list of VMs for inspection down to just those passed on the + # command line. Warn if the user passed a VM that was not running. + unless argv.empty? + running_vms = running.map {|vm| vm.name.to_s} + argv.each do |name| + @env.ui.warn I18n.t('pebuild.command.facts.vm_not_running', :vm_name => name) unless running_vms.include? name + end + + running.select! {|vm| argv.include? vm.name.to_s} + end + + running.each do |vm| + facts = vm.guest.capability(:pebuild_facts) + + @env.ui.machine('guest-facts', facts, {:target => vm.name.to_s}) + @env.ui.info(JSON.pretty_generate(facts)) + end + + return 0 + end + + private + + def parser + OptionParser.new do |o| + o.banner = <<-BANNER + Usage: vagrant pe-build facts [vm-name] + BANNER + + o.separator '' + + o.on('-h', '--help', 'Display this help') do + puts o + exit(0) + end + end + end + +end diff --git a/templates/locales/en.yml b/templates/locales/en.yml index b94c1c6..f78d5a4 100644 --- a/templates/locales/en.yml +++ b/templates/locales/en.yml @@ -1,5 +1,11 @@ en: pebuild: + command: + facts: + vm_not_running: |- + Could not establish a connection to %{vm_name} in order to retrieve + facts. This usually means the machine is either shut down, + or has not been created. archive: no_installer_source: |- The Puppet Enterprise installer %{filename} From 16983827beae286855e8a304211e3814e93a8351 Mon Sep 17 00:00:00 2001 From: Charlie Sharpsteen Date: Tue, 6 Oct 2015 09:08:11 -0700 Subject: [PATCH 11/13] Add pe_agent provisioner The new `pe_agent` provisioner performs a [Simplified Agent install][1] for PE 2015.x and newer. [1]: http://docs.puppetlabs.com/pe/2015.2/install_agents.html#using-the-puppet-agent-package-installation-script --- lib/pe_build/config.rb | 1 + lib/pe_build/config/pe_agent.rb | 68 +++++++++++++++++++++++++ lib/pe_build/config_builder.rb | 1 + lib/pe_build/config_builder/pe_agent.rb | 24 +++++++++ lib/pe_build/plugin.rb | 10 ++++ lib/pe_build/provisioner/pe_agent.rb | 55 ++++++++++++++++++++ spec/unit/config/pe_agent_spec.rb | 66 ++++++++++++++++++++++++ spec/unit/provisioner/pe_agent_spec.rb | 64 +++++++++++++++++++++++ templates/locales/en.yml | 13 +++++ 9 files changed, 302 insertions(+) create mode 100644 lib/pe_build/config/pe_agent.rb create mode 100644 lib/pe_build/config_builder/pe_agent.rb create mode 100644 lib/pe_build/provisioner/pe_agent.rb create mode 100644 spec/unit/config/pe_agent_spec.rb create mode 100644 spec/unit/provisioner/pe_agent_spec.rb diff --git a/lib/pe_build/config.rb b/lib/pe_build/config.rb index b9c4b5a..880d18c 100644 --- a/lib/pe_build/config.rb +++ b/lib/pe_build/config.rb @@ -4,5 +4,6 @@ module PEBuild module Config require 'pe_build/config/global' require 'pe_build/config/pe_bootstrap' + require 'pe_build/config/pe_agent' end end diff --git a/lib/pe_build/config/pe_agent.rb b/lib/pe_build/config/pe_agent.rb new file mode 100644 index 0000000..e1b3805 --- /dev/null +++ b/lib/pe_build/config/pe_agent.rb @@ -0,0 +1,68 @@ +# Configuration for PE Agent provisioners +# +# @since 0.13.0 +class PEBuild::Config::PEAgent < Vagrant.plugin('2', :config) + # The minimum PE Version supported by this provisioner. + MINIMUM_VERSION = '2015.2.0' + + # @!attribute master + # @return [String] The DNS hostname of the Puppet master for this node. + attr_accessor :master + + # @!attribute version + # @return [String] The version of PE to install. May be either a version + # string of the form `x.y.x[-optional-arbitrary-stuff]` or the string + # `current`. Defaults to `current`. + attr_accessor :version + + def initialize + @master = UNSET_VALUE + @version = UNSET_VALUE + end + + def finalize! + @master = nil if @master == UNSET_VALUE + @version = 'current' if @version == UNSET_VALUE + end + + def validate(machine) + errors = _detected_errors + + if @master.nil? + errors << I18n.t('pebuild.config.pe_agent.errors.no_master') + end + + validate_version!(errors, machine) + + {'pe_agent provisioner' => errors} + end + + private + + def validate_version!(errors, machine) + pe_version_regex = %r[\d+\.\d+\.\d+[\w-]*] + + if @version.kind_of? String + return if version == 'current' + if version.match(pe_version_regex) + unless PEBuild::Util::VersionString.compare(@version, MINIMUM_VERSION) > 0 + errors << I18n.t( + 'pebuild.config.pe_agent.errors.version_too_old', + :version => @version, + :minimum_version => MINIMUM_VERSION + ) + end + + return + end + end + + # If we end up here, the version was not a string that matched 'current' or + # the regex. Mutate the error array. + errors << I18n.t( + 'pebuild.config.pe_agent.errors.malformed_version', + :version => @version, + :version_class => @version.class + ) + end +end diff --git a/lib/pe_build/config_builder.rb b/lib/pe_build/config_builder.rb index 504d630..19dd7b3 100644 --- a/lib/pe_build/config_builder.rb +++ b/lib/pe_build/config_builder.rb @@ -2,5 +2,6 @@ module PEBuild module ConfigBuilder require 'pe_build/config_builder/global' require 'pe_build/config_builder/pe_bootstrap' + require 'pe_build/config_builder/pe_agent' end end diff --git a/lib/pe_build/config_builder/pe_agent.rb b/lib/pe_build/config_builder/pe_agent.rb new file mode 100644 index 0000000..b909e29 --- /dev/null +++ b/lib/pe_build/config_builder/pe_agent.rb @@ -0,0 +1,24 @@ +require 'config_builder/model' + +# @since 0.13.0 +class PEBuild::ConfigBuilder::PEAgent < ::ConfigBuilder::Model::Base + # @!attribute master + # @return [String] The DNS hostname of the Puppet master for this node. + def_model_attribute :master + # @!attribute version + # @return [String] The version of PE to install. May be either a version + # string of the form `x.y.x[-optional-arbitrary-stuff]` or the string + # `current`. Defaults to `current`. + def_model_attribute :version + + def to_proc + Proc.new do |vm_config| + vm_config.provision :pe_agent do |config| + with_attr(:master) {|val| config.master = val } + with_attr(:version) {|val| config.version = val } + end + end + end + + ::ConfigBuilder::Model::Provisioner.register('pe_agent', self) +end diff --git a/lib/pe_build/plugin.rb b/lib/pe_build/plugin.rb index d4512ed..714e938 100644 --- a/lib/pe_build/plugin.rb +++ b/lib/pe_build/plugin.rb @@ -22,6 +22,11 @@ class Plugin < Vagrant.plugin('2') PEBuild::Config::PEBootstrap end + config(:pe_agent, :provisioner) do + require_relative 'config' + PEBuild::Config::PEAgent + end + config(:pe_build) do require_relative 'config' PEBuild::Config::Global @@ -32,6 +37,11 @@ class Plugin < Vagrant.plugin('2') PEBuild::Provisioner::PEBootstrap end + provisioner(:pe_agent) do + require_relative 'provisioner/pe_agent' + PEBuild::Provisioner::PEAgent + end + command(:'pe-build') do require_relative 'command' PEBuild::Command::Base diff --git a/lib/pe_build/provisioner/pe_agent.rb b/lib/pe_build/provisioner/pe_agent.rb new file mode 100644 index 0000000..f0e1235 --- /dev/null +++ b/lib/pe_build/provisioner/pe_agent.rb @@ -0,0 +1,55 @@ +module PEBuild + module Provisioner + # Provision PE agents using simplified install + # + # @since 0.13.0 + class PEAgent < Vagrant.plugin('2', :provisioner) + attr_reader :facts + attr_reader :agent_version + + def provision + provision_init! + + unless agent_version.nil? + machine.ui.info I18n.t( + 'pebuild.provisioner.pe_agent.already_installed', + :version => agent_version + ) + return + end + + # TODO add a provisioning method that ensures the master VM is + # configured to serve packages for a given Agent version and + # architecture. + + # TODO Wrap in a method that handles windows VMs (by calling pe_bootstrap). + provision_posix_agent + end + + private + + # Set data items that are only available at provision time + def provision_init! + @facts = machine.guest.capability(:pebuild_facts) + @agent_version = facts['puppetversion'] + end + + # Execute a Vagrant shell provisioner to provision POSIX agents + # + # Performs a `curl | bash` installation. + def provision_posix_agent + shell_config = Vagrant.plugin('2').manager.provisioner_configs[:shell].new + shell_config.privileged = true + # TODO: Extend to allow passing agent install options. + shell_config.inline = <<-EOS +curl -k -tlsv1 -s https://#{config.master}:8140/packages/#{config.version}/install.bash | bash + EOS + shell_config.finalize! + + shell_provisioner = Vagrant.plugin('2').manager.provisioners[:shell].new(machine, shell_config) + shell_provisioner.provision + end + + end + end +end diff --git a/spec/unit/config/pe_agent_spec.rb b/spec/unit/config/pe_agent_spec.rb new file mode 100644 index 0000000..95575d3 --- /dev/null +++ b/spec/unit/config/pe_agent_spec.rb @@ -0,0 +1,66 @@ +require 'spec_helper' + +require 'pe_build/config' + +describe PEBuild::Config::PEAgent do + let(:machine) { double('machine') } + + describe 'hostname' do + it 'must be set' do + subject.finalize! + + errors = subject.validate(machine) + + expect(errors['pe_agent provisioner'].to_s).to match(/No master hostname has been configured/) + end + end + + describe 'version' do + before(:each) { subject.master = 'master.company.com' } + + it 'defaults to "current"' do + subject.finalize! + + expect(subject.version).to eq 'current' + end + + it 'may be of the form x.y.z' do + subject.version = '2015.2.1' + + subject.finalize! + errors = subject.validate(machine) + + expect(errors['pe_agent provisioner']).to be_empty + end + + it 'may be of the form x.y.z[-other-arbitrary-stuff]' do + subject.version = '2015.2.1-r42-gsomesha' + + subject.finalize! + errors = subject.validate(machine) + + expect(errors['pe_agent provisioner']).to be_empty + end + + it 'may not be x.y' do + subject.version = '2015.2' + + subject.finalize! + errors = subject.validate(machine) + + # Casting the array to a string and using a regex matcher gives a nice + # diff in the case of failure. + expect(errors['pe_agent provisioner'].to_s).to match(/The agent version.*is invalid./) + end + + it "must be greater than #{PEBuild::Config::PEAgent::MINIMUM_VERSION}" do + subject.version = '3.8.2' + + subject.finalize! + errors = subject.validate(machine) + + expect(errors['pe_agent provisioner'].to_s).to match(/The agent version.*is too old./) + end + end + +end diff --git a/spec/unit/provisioner/pe_agent_spec.rb b/spec/unit/provisioner/pe_agent_spec.rb new file mode 100644 index 0000000..abe8bec --- /dev/null +++ b/spec/unit/provisioner/pe_agent_spec.rb @@ -0,0 +1,64 @@ +require 'spec_helper' + +require 'pe_build/provisioner/pe_agent' + +describe PEBuild::Provisioner::PEAgent do + include_context 'vagrant-unit' + + subject { described_class.new(machine, config) } + + let(:test_env) do + env = isolated_environment + env.vagrantfile("") + + env + end + + let(:env) { test_env.create_vagrant_env } + let(:machine) { env.machine(env.machine_names[0], :dummy) } + let(:config) { PEBuild::Config::PEAgent.new } + # Mock the communicator to prevent SSH commands from being executed. + let(:communicator) { double('communicator') } + # Mock the guest operating system. + let(:guest) { double('guest') } + # Mock Vagrant IO. + let(:ui) { double('ui') } + + before(:each) do + allow(machine).to receive(:communicate).and_return(communicator) + allow(machine).to receive(:guest).and_return(guest) + allow(machine).to receive(:ui).and_return(ui) + + config.finalize! + # Skip provision-time inspection of machines. + allow(subject).to receive(:provision_init!) + end + + after(:each) { test_env.close } + + context 'when an agent is installed' do + before(:each) do + allow(subject).to receive(:agent_version).and_return('1.0.0') + end + + it 'logs a message and returns early' do + expect(ui).to receive(:info).with(/Puppet agent .* is already installed/) + expect(subject).to_not receive(:provision_posix_agent) + + subject.provision + end + end + + context 'when an agent is not installed' do + before(:each) do + allow(subject).to receive(:agent_version).and_return(nil) + end + + it 'invokes the agent provisioner' do + expect(subject).to receive(:provision_posix_agent) + + subject.provision + end + end + +end diff --git a/templates/locales/en.yml b/templates/locales/en.yml index f78d5a4..fa7a34d 100644 --- a/templates/locales/en.yml +++ b/templates/locales/en.yml @@ -27,6 +27,9 @@ en: unset_version: |- The Puppet Enterprise version must be set either on the global pe_build config object or specified on a per-provisioner basis, but both were unset. + pe_agent: + already_installed: |- + Puppet agent %{version} is already installed, skipping installation. transfer: open_uri: download_failed: |- @@ -66,6 +69,16 @@ en: Autosign must be one of %{autosign_classes}, but was of type %{autosign}. invalid_answer_extras: |- Answer_extras must be an Array, got a %{class}. + pe_agent: + errors: + no_master: |- + No master hostname has been configured for this agent. + malformed_version: |- + The agent version "%{version}":%{version_class} is invalid; it must be a String of + the format "x.y.z[-optional-stuff]" or "current". + version_too_old: |- + The agent version %{version} is too old; pe_agent can only provision versions + newer than %{minimum_version}. cap: run_install: already_installed: |- From 75c01da7cd2d7e44f0a86afa943a134bea26f610 Mon Sep 17 00:00:00 2001 From: Charlie Sharpsteen Date: Thu, 8 Oct 2015 08:57:49 -0700 Subject: [PATCH 12/13] Provision agent repositories on master VMs This patch adds a `master_vm` setting to the configuration of the `pe_agent` provisioner. If set, this setting will be interpreted as the name of another Vagrant machine which hosts the PE master. Setting `master_vm` will enable the following tasks during agent installation: - The `hostname` of the master machine will be used as a default for the `master` setting of the `pe_agent` provisioner. - The `pe_agent` provisioner will be able to ensure the correct `pe_repos` are configured on the master before attempting agent installation. --- lib/pe_build/config/pe_agent.rb | 10 +++- lib/pe_build/config_builder/pe_agent.rb | 2 + lib/pe_build/provisioner/pe_agent.rb | 73 ++++++++++++++++++++++++- lib/pe_build/util/machine_comms.rb | 51 +++++++++++++++++ lib/pe_build/util/pe_packaging.rb | 57 +++++++++++++++++++ spec/unit/config/pe_agent_spec.rb | 16 +++++- spec/unit/provisioner/pe_agent_spec.rb | 41 ++++++++++---- templates/locales/en.yml | 13 ++++- 8 files changed, 244 insertions(+), 19 deletions(-) create mode 100644 lib/pe_build/util/machine_comms.rb create mode 100644 lib/pe_build/util/pe_packaging.rb diff --git a/lib/pe_build/config/pe_agent.rb b/lib/pe_build/config/pe_agent.rb index e1b3805..5378111 100644 --- a/lib/pe_build/config/pe_agent.rb +++ b/lib/pe_build/config/pe_agent.rb @@ -7,8 +7,14 @@ class PEBuild::Config::PEAgent < Vagrant.plugin('2', :config) # @!attribute master # @return [String] The DNS hostname of the Puppet master for this node. + # If {#master_vm} is set, the hostname of that machine will be used + # as a default. attr_accessor :master + # @!attribute master + # @return [String] The name of a Vagrant VM to use as the master. + attr_accessor :master_vm + # @!attribute version # @return [String] The version of PE to install. May be either a version # string of the form `x.y.x[-optional-arbitrary-stuff]` or the string @@ -17,18 +23,20 @@ class PEBuild::Config::PEAgent < Vagrant.plugin('2', :config) def initialize @master = UNSET_VALUE + @master_vm = UNSET_VALUE @version = UNSET_VALUE end def finalize! @master = nil if @master == UNSET_VALUE + @master_vm = nil if @master_vm == UNSET_VALUE @version = 'current' if @version == UNSET_VALUE end def validate(machine) errors = _detected_errors - if @master.nil? + if @master.nil? && @master_vm.nil? errors << I18n.t('pebuild.config.pe_agent.errors.no_master') end diff --git a/lib/pe_build/config_builder/pe_agent.rb b/lib/pe_build/config_builder/pe_agent.rb index b909e29..1250a6e 100644 --- a/lib/pe_build/config_builder/pe_agent.rb +++ b/lib/pe_build/config_builder/pe_agent.rb @@ -5,6 +5,7 @@ class PEBuild::ConfigBuilder::PEAgent < ::ConfigBuilder::Model::Base # @!attribute master # @return [String] The DNS hostname of the Puppet master for this node. def_model_attribute :master + def_model_attribute :master_vm # @!attribute version # @return [String] The version of PE to install. May be either a version # string of the form `x.y.x[-optional-arbitrary-stuff]` or the string @@ -15,6 +16,7 @@ def to_proc Proc.new do |vm_config| vm_config.provision :pe_agent do |config| with_attr(:master) {|val| config.master = val } + with_attr(:master_vm){|val| config.master_vm = val } with_attr(:version) {|val| config.version = val } end end diff --git a/lib/pe_build/provisioner/pe_agent.rb b/lib/pe_build/provisioner/pe_agent.rb index f0e1235..d6449fd 100644 --- a/lib/pe_build/provisioner/pe_agent.rb +++ b/lib/pe_build/provisioner/pe_agent.rb @@ -1,9 +1,15 @@ +require 'pe_build/util/pe_packaging' +require 'pe_build/util/machine_comms' + module PEBuild module Provisioner # Provision PE agents using simplified install # # @since 0.13.0 class PEAgent < Vagrant.plugin('2', :provisioner) + include ::PEBuild::Util::PEPackaging + include ::PEBuild::Util::MachineComms + attr_reader :facts attr_reader :agent_version @@ -18,12 +24,15 @@ def provision return end - # TODO add a provisioning method that ensures the master VM is - # configured to serve packages for a given Agent version and - # architecture. + # FIXME: Not necessary if the agent is running Windows. + unless config.master_vm.nil? + provision_pe_repo + end # TODO Wrap in a method that handles windows VMs (by calling pe_bootstrap). provision_posix_agent + + # TODO Sign agent cert, if master_vm is available. end private @@ -32,6 +41,62 @@ def provision def provision_init! @facts = machine.guest.capability(:pebuild_facts) @agent_version = facts['puppetversion'] + + # Resolve the master_vm setting to a Vagrant machine reference. + unless config.master_vm.nil? + vm_def = machine.env.active_machines.find {|vm| vm[0].to_s == config.master_vm.to_s} + + unless vm_def.nil? + config.master_vm = machine.env.machine(*vm_def) + config.master ||= config.master_vm.config.vm.hostname.to_s + end + end + end + + # Ensure a master VM is able to serve agent packages + # + # This method inspects the master VM and ensures it is configured to + # serve packages for the agent's architecture. + def provision_pe_repo + # This method will raise an error if commands can't be run on the + # master VM. + ensure_reachable(config.master_vm) + + platform = platform_tag(facts) + # Transform the platform_tag into a Puppet class name. + pe_repo_platform = platform.gsub('-', '_').gsub('.', '') + # TODO: Support PE 3.x + platform_repo = "/opt/puppetlabs/server/data/packages/public/current/#{platform}" + + # Print a message and return if the agent repositories exist on the + # master. + if config.master_vm.communicate.test("[ -e #{platform_repo} ]") + config.master_vm.ui.info I18n.t( + 'pebuild.provisioner.pe_agent.pe_repo_present', + :vm_name => config.master_vm.name, + :platform_tag => platform + ) + return + end + + config.master_vm.ui.info I18n.t( + 'pebuild.provisioner.pe_agent.adding_pe_repo', + :vm_name => config.master_vm.name, + :platform_tag => platform + ) + + shell_config = Vagrant.plugin('2').manager.provisioner_configs[:shell].new + shell_config.privileged = true + # TODO: Extend to configuring agent repos which are older than the + # master. + # TODO: Extend to PE 3.x masters. + shell_config.inline = <<-EOS +/opt/puppetlabs/bin/puppet apply -e 'include pe_repo::platform::#{pe_repo_platform}' + EOS + shell_config.finalize! + + shell_provisioner = Vagrant.plugin('2').manager.provisioners[:shell].new(config.master_vm, shell_config) + shell_provisioner.provision end # Execute a Vagrant shell provisioner to provision POSIX agents @@ -46,6 +111,8 @@ def provision_posix_agent EOS shell_config.finalize! + machine.ui.info "Running: #{shell_config.inline}" + shell_provisioner = Vagrant.plugin('2').manager.provisioners[:shell].new(machine, shell_config) shell_provisioner.provision end diff --git a/lib/pe_build/util/machine_comms.rb b/lib/pe_build/util/machine_comms.rb new file mode 100644 index 0000000..aae33ad --- /dev/null +++ b/lib/pe_build/util/machine_comms.rb @@ -0,0 +1,51 @@ +require 'vagrant/errors' + +module PEBuild + module Util + # Utilities related to Vagrant Machine communications + # + # This module provides general-purpose utility functions for communicating + # with Vagrant machines. + # + # @since 0.13.0 + module MachineComms + + # Determine if commands can be executed on a Vagrant machine + # + # @param machine [Vagrant::Machine] A Vagrant machine. + # + # @return [true] If the machine can accept communication. + # @return [false] If the machine cannot accept communication. + def is_reachable?(machine) + begin + machine.communicate.ready? + rescue Vagrant::Errors::VagrantError + # WinRM will raise an error if the VM isn't running instead of + # returning false (GH-6356). + false + end + end + module_function :is_reachable? + + class MachineNotReachable < ::Vagrant::Errors::VagrantError + error_key(:machine_not_reachable, 'pebuild.errors') + end + + # Raise an error if Vagrant commands cannot be executed on a machine + # + # This function raises an error if a given vagrant machine is not ready + # for communication. + # + # @param machine [Vagrant::Machine] A Vagrant machine. + # + # @return [void] If the machine can accept communication. + # @raise [MachineNotReachable] If the machine cannot accept + # communication. + def ensure_reachable(machine) + raise MachineNotReachable, :vm_name => machine.name.to_s unless is_reachable?(machine) + end + module_function :ensure_reachable + + end + end +end diff --git a/lib/pe_build/util/pe_packaging.rb b/lib/pe_build/util/pe_packaging.rb new file mode 100644 index 0000000..2269da2 --- /dev/null +++ b/lib/pe_build/util/pe_packaging.rb @@ -0,0 +1,57 @@ +module PEBuild + module Util + # Utilities related to PE Packages + # + # This module provides general-purpose utility functions for working with + # PE packages. + # + # @since 0.13.0 + module PEPackaging + + # Determine package tag from Facts + # + # The `platform_tag` is a `os-version-archtecture` value that is used in + # many PE package filenames and repostiory names. + # + # @param facts [Hash] A hash of facts which includes `architecture` + # and `os` values. + # + # @return [String] A string representing the platform tag. + def platform_tag(facts) + case facts['os']['family'].downcase + when 'redhat' + # TODO: Fedora might be in here. + os = 'el' + version = facts['os']['release']['major'] + arch = facts['architecture'] + when 'windows' + os = 'windows' + version = nil + arch = (facts['architecture'] == 'x64' ? 'x86_64' : 'i386') + when 'debian' + case os = facts['os']['name'].downcase + when 'debian' + version = facts['os']['release']['major'] + when 'ubuntu' + version = facts['os']['release']['full'] + end + # TODO: Add "unknown debian" error. + arch = (facts['architecture'] == 'x86_64' ? 'amd64' : 'i386') + when 'solaris' + os = 'solaris' + version = facts['os']['release']['major'] + arch = (facts['architecture'].match(/^i\d+/) ? 'i386' : 'sparc') + when 'suse' + os = 'sles' + version = facts['os']['release']['major'] + arch = facts['architecture'] + end + # TODO: Add "unknown os" error. + + [os, version, arch].join('-').downcase + end + module_function :platform_tag + + end + end +end diff --git a/spec/unit/config/pe_agent_spec.rb b/spec/unit/config/pe_agent_spec.rb index 95575d3..7faf6ef 100644 --- a/spec/unit/config/pe_agent_spec.rb +++ b/spec/unit/config/pe_agent_spec.rb @@ -5,13 +5,23 @@ describe PEBuild::Config::PEAgent do let(:machine) { double('machine') } - describe 'hostname' do - it 'must be set' do + describe 'master' do + it 'must be set if master_vm is nil' do + subject.master_vm = nil subject.finalize! errors = subject.validate(machine) - expect(errors['pe_agent provisioner'].to_s).to match(/No master hostname has been configured/) + expect(errors['pe_agent provisioner'].to_s).to match(/No master or master_vm setting has been configured/) + end + + it 'may be unset if master_vm is not nil' do + subject.master_vm = 'master' + subject.finalize! + + errors = subject.validate(machine) + + expect(errors['pe_agent provisioner']).to be_empty end end diff --git a/spec/unit/provisioner/pe_agent_spec.rb b/spec/unit/provisioner/pe_agent_spec.rb index abe8bec..777c0ec 100644 --- a/spec/unit/provisioner/pe_agent_spec.rb +++ b/spec/unit/provisioner/pe_agent_spec.rb @@ -5,29 +5,36 @@ describe PEBuild::Provisioner::PEAgent do include_context 'vagrant-unit' - subject { described_class.new(machine, config) } + subject { described_class.new(agent_vm, config) } let(:test_env) do env = isolated_environment - env.vagrantfile("") + env.vagrantfile <<-EOF +Vagrant.configure('2') do |config| + config.vm.define :agent_vm + config.vm.define :master_vm +end +EOF env end let(:env) { test_env.create_vagrant_env } - let(:machine) { env.machine(env.machine_names[0], :dummy) } + let(:agent_vm) { env.machine(:agent_vm, :dummy) } + let(:master_vm) { env.machine(:master_vm, :dummy) } let(:config) { PEBuild::Config::PEAgent.new } # Mock the communicator to prevent SSH commands from being executed. - let(:communicator) { double('communicator') } - # Mock the guest operating system. - let(:guest) { double('guest') } + let(:agent_comm) { double('agent_comm') } + let(:master_comm) { double('master_comm') } # Mock Vagrant IO. - let(:ui) { double('ui') } + let(:agent_ui) { double('agent_ui') } + let(:master_ui) { double('master_ui') } before(:each) do - allow(machine).to receive(:communicate).and_return(communicator) - allow(machine).to receive(:guest).and_return(guest) - allow(machine).to receive(:ui).and_return(ui) + allow(agent_vm).to receive(:communicate).and_return(agent_comm) + allow(agent_vm).to receive(:ui).and_return(agent_ui) + allow(master_vm).to receive(:communicate).and_return(master_comm) + allow(master_vm).to receive(:ui).and_return(master_ui) config.finalize! # Skip provision-time inspection of machines. @@ -42,7 +49,7 @@ end it 'logs a message and returns early' do - expect(ui).to receive(:info).with(/Puppet agent .* is already installed/) + expect(agent_ui).to receive(:info).with(/Puppet agent .* is already installed/) expect(subject).to_not receive(:provision_posix_agent) subject.provision @@ -54,6 +61,18 @@ allow(subject).to receive(:agent_version).and_return(nil) end + context 'when master_vm is set' do + before(:each) do + allow(config).to receive(:master_vm).and_return(master_vm) + end + + it 'raises an error if the master_vm is unreachable' do + allow(master_comm).to receive(:ready?).and_return(false) + + expect { subject.provision }.to raise_error(::PEBuild::Util::MachineComms::MachineNotReachable) + end + end + it 'invokes the agent provisioner' do expect(subject).to receive(:provision_posix_agent) diff --git a/templates/locales/en.yml b/templates/locales/en.yml index fa7a34d..d03cdad 100644 --- a/templates/locales/en.yml +++ b/templates/locales/en.yml @@ -6,6 +6,11 @@ en: Could not establish a connection to %{vm_name} in order to retrieve facts. This usually means the machine is either shut down, or has not been created. + errors: + machine_not_reachable: |- + The specified Vagrant machine, %{vm_name}, is not able to recieve + communication. This typically means the machine has not been created, + is shut down, or there is an issue with the SSH or WinRM communicator. archive: no_installer_source: |- The Puppet Enterprise installer %{filename} @@ -30,6 +35,12 @@ en: pe_agent: already_installed: |- Puppet agent %{version} is already installed, skipping installation. + pe_repo_present: |- + The Puppet Server on %{vm_name} is configured with Agent repositories for: + %{platform_tag} + adding_pe_repo: |- + Configuring Puppet Server on %{vm_name} with Agent repositories for: + %{platform_tag} transfer: open_uri: download_failed: |- @@ -72,7 +83,7 @@ en: pe_agent: errors: no_master: |- - No master hostname has been configured for this agent. + No master or master_vm setting has been configured for this agent. malformed_version: |- The agent version "%{version}":%{version_class} is invalid; it must be a String of the format "x.y.z[-optional-stuff]" or "current". From 125c435e55061465c1fca08037fe3430ab1315dc Mon Sep 17 00:00:00 2001 From: Charlie Sharpsteen Date: Mon, 12 Oct 2015 11:07:57 -0700 Subject: [PATCH 13/13] Update acceptance tests for pe_agent This patch updates the acceptance suite to provision CentOS PE 2015.x masters with `pe_bootstrap` and Ubuntu 2015.x agents with `pe_agent`. Acceptance setup is also expanded to include support for both CentOS and Ubuntu agent platforms. Because of the expanded platform support, other tests are also updated to reflect the change in Vagrant box names. --- .../pe_build/pe_bootstrap_2015x_spec.rb | 27 ++++----- acceptance/pe_build/pe_bootstrap_3x_spec.rb | 9 ++- .../pe_build/pe_bootstrap_latest_spec.rb | 9 ++- .../skeletons/2015x_acceptance/Vagrantfile | 60 +++++-------------- acceptance/skeletons/pe_build/Vagrantfile | 2 +- tasks/acceptance.rake | 3 +- vagrant-spec.config.example.rb | 2 +- 7 files changed, 45 insertions(+), 67 deletions(-) diff --git a/acceptance/pe_build/pe_bootstrap_2015x_spec.rb b/acceptance/pe_build/pe_bootstrap_2015x_spec.rb index d20487d..9df072c 100644 --- a/acceptance/pe_build/pe_bootstrap_2015x_spec.rb +++ b/acceptance/pe_build/pe_bootstrap_2015x_spec.rb @@ -1,7 +1,7 @@ shared_examples 'provider/provisioner/pe_bootstrap/2015x' do |provider, options| - if !File.file?(options[:box]) + if options[:boxes].empty? raise ArgumentError, - "A box file must be downloaded for provider: #{provider}. Try: rake acceptance:setup" + "Box files must be downloaded for provider: #{provider}. Try: rake acceptance:setup" end include_context 'acceptance' @@ -17,7 +17,10 @@ # The skelton sets up a Vagrantfile which expects the OS under test to be # available as `box`. environment.skeleton('2015x_acceptance') - assert_execute('vagrant', 'box', 'add', 'box', options[:box]) + options[:boxes].each do |box| + name = File.basename(box).split('-').first + assert_execute('vagrant', 'box', 'add', name, box) + end end after(:each) do @@ -25,21 +28,17 @@ assert_execute('vagrant', 'destroy', '--force', log: false) end - context 'when installing PE 2015.2.0' do - it 'provisions with pe_build' do - result = assert_execute('vagrant', 'up', "--provider=#{provider}", 'pe-201520-master', 'pe-201520-agent') - end - end - - context 'when installing PE 2015.2.1' do - it 'provisions with pe_build' do - assert_execute('vagrant', 'up', "--provider=#{provider}", 'pe-201521-master', 'pe-201521-agent') + context 'when installing PE 2015.2.x' do + it 'provisions masters with pe_bootstrap and agents with pe_agent' do + assert_execute('vagrant', 'up', "--provider=#{provider}", 'pe-20152-master') + assert_execute('vagrant', 'up', "--provider=#{provider}", 'pe-20152-agent') end end context 'when installing PE 2015.latest' do - it 'provisions with pe_build' do - assert_execute('vagrant', 'up', "--provider=#{provider}", 'pe-2015latest-master', 'pe-2015latest-agent') + it 'provisions masters with pe_bootstrap and agents with pe_agent' do + assert_execute('vagrant', 'up', "--provider=#{provider}", 'pe-2015latest-master') + assert_execute('vagrant', 'up', "--provider=#{provider}", 'pe-2015latest-agent') end end end diff --git a/acceptance/pe_build/pe_bootstrap_3x_spec.rb b/acceptance/pe_build/pe_bootstrap_3x_spec.rb index eadc230..5abca2a 100644 --- a/acceptance/pe_build/pe_bootstrap_3x_spec.rb +++ b/acceptance/pe_build/pe_bootstrap_3x_spec.rb @@ -1,7 +1,7 @@ shared_examples 'provider/provisioner/pe_bootstrap/3x' do |provider, options| - if !File.file?(options[:box]) + if options[:boxes].empty? raise ArgumentError, - "A box file must be downloaded for provider: #{provider}. Try: rake acceptance:setup" + "Box files must be downloaded for provider: #{provider}. Try: rake acceptance:setup" end include_context 'acceptance' @@ -17,7 +17,10 @@ # The skelton sets up a Vagrantfile which expects the OS under test to be # available as `box`. environment.skeleton('pe_build') - assert_execute('vagrant', 'box', 'add', 'box', options[:box]) + options[:boxes].each do |box| + name = File.basename(box).split('-').first + assert_execute('vagrant', 'box', 'add', name, box) + end end after(:each) do diff --git a/acceptance/pe_build/pe_bootstrap_latest_spec.rb b/acceptance/pe_build/pe_bootstrap_latest_spec.rb index d431508..b1f6d29 100644 --- a/acceptance/pe_build/pe_bootstrap_latest_spec.rb +++ b/acceptance/pe_build/pe_bootstrap_latest_spec.rb @@ -1,7 +1,7 @@ shared_examples 'provider/provisioner/pe_bootstrap/latest' do |provider, options| - if !File.file?(options[:box]) + if options[:boxes].empty? raise ArgumentError, - "A box file must be downloaded for provider: #{provider}. Try: rake acceptance:setup" + "Box files must be downloaded for provider: #{provider}. Try: rake acceptance:setup" end include_context 'acceptance' @@ -19,7 +19,10 @@ before(:each) do environment.skeleton('pe_build') - assert_execute('vagrant', 'box', 'add', 'box', options[:box]) + options[:boxes].each do |box| + name = File.basename(box).split('-').first + assert_execute('vagrant', 'box', 'add', name, box) + end end after(:each) do diff --git a/acceptance/skeletons/2015x_acceptance/Vagrantfile b/acceptance/skeletons/2015x_acceptance/Vagrantfile index c696fcf..b707b2e 100644 --- a/acceptance/skeletons/2015x_acceptance/Vagrantfile +++ b/acceptance/skeletons/2015x_acceptance/Vagrantfile @@ -7,44 +7,16 @@ end Vagrant.configure('2') do |config| config.pe_build.download_root = ENV['PE_BUILD_DOWNLOAD_ROOT'] - # This is the box name used by the setup helpers in the acceptance tests. - config.vm.box = 'box' - config.vm.provision :shell, :inline => 'service iptables stop' - config.vm.define 'pe-201520-master' do |node| + config.vm.define 'pe-20152-master' do |node| node.vm.hostname = 'pe-201520-master.pe-bootstrap.vlan' + node.vm.box = 'centos' # All-in-one master nodes need a generous amount of RAM for all the Java. set_resources node, 4096, 1 + node.vm.provision :shell, :inline => 'service iptables stop' node.vm.network 'private_network', :ip => '10.20.1.100' - node.vm.provision :shell, :inline => 'echo "10.20.1.101 pe-201520-agent.pe-bootstrap.vlan" >> /etc/hosts' - - node.vm.provision :pe_bootstrap do |p| - p.version = '2015.2.0' - p.role = :master - end - end - - config.vm.define 'pe-201520-agent' do |node| - node.vm.hostname = 'pe-201520-agent.pe-bootstrap.vlan' - - node.vm.network 'private_network', :ip => '10.20.1.101' - node.vm.provision :shell, :inline => 'echo "10.20.1.100 pe-201520-master.pe-bootstrap.vlan" >> /etc/hosts' - - node.vm.provision :pe_bootstrap do |p| - p.version = '2015.2.0' - p.role = :agent - p.master = 'pe-201520-master.pe-bootstrap.vlan' - end - end - - config.vm.define 'pe-201521-master' do |node| - node.vm.hostname = 'pe-201521-master.pe-bootstrap.vlan' - # All-in-one master nodes need a generous amount of RAM for all the Java. - set_resources node, 4096, 1 - - node.vm.network 'private_network', :ip => '10.20.1.102' - node.vm.provision :shell, :inline => 'echo "10.20.1.103 pe-201521-agent.pe-bootstrap.vlan" >> /etc/hosts' + node.vm.provision :shell, :inline => 'echo "10.20.1.101 pe-20152-agent.pe-bootstrap.vlan" >> /etc/hosts' node.vm.provision :pe_bootstrap do |p| p.version = '2015.2.1' @@ -52,23 +24,24 @@ Vagrant.configure('2') do |config| end end - config.vm.define 'pe-201521-agent' do |node| - node.vm.hostname = 'pe-201521-agent.pe-bootstrap.vlan' + config.vm.define 'pe-20152-agent' do |node| + node.vm.hostname = 'pe-20152-agent.pe-bootstrap.vlan' + node.vm.box = 'ubuntu' - node.vm.network 'private_network', :ip => '10.20.1.103' - node.vm.provision :shell, :inline => 'echo "10.20.1.102 pe-201521-master.pe-bootstrap.vlan" >> /etc/hosts' + node.vm.network 'private_network', :ip => '10.20.1.101' + node.vm.provision :shell, :inline => 'echo "10.20.1.100 pe-20152-master.pe-bootstrap.vlan" >> /etc/hosts' - node.vm.provision :pe_bootstrap do |p| - p.version = '2015.2.1' - p.role = :agent - p.master = 'pe-201521-master.pe-bootstrap.vlan' + node.vm.provision :pe_agent do |p| + p.master_vm = 'pe-20152-master' end end config.vm.define 'pe-2015latest-master' do |node| node.vm.hostname = 'pe-2015latest-master.pe-bootstrap.vlan' + node.vm.box = 'centos' # All-in-one master nodes need a generous amount of RAM for all the Java. set_resources node, 4096, 1 + node.vm.provision :shell, :inline => 'service iptables stop' node.vm.network 'private_network', :ip => '10.20.1.104' node.vm.provision :shell, :inline => 'echo "10.20.1.105 pe-2015latest-agent.pe-bootstrap.vlan" >> /etc/hosts' @@ -81,14 +54,13 @@ Vagrant.configure('2') do |config| config.vm.define 'pe-2015latest-agent' do |node| node.vm.hostname = 'pe-2015latest-agent.pe-bootstrap.vlan' + node.vm.box = 'ubuntu' node.vm.network 'private_network', :ip => '10.20.1.105' node.vm.provision :shell, :inline => 'echo "10.20.1.104 pe-2015latest-master.pe-bootstrap.vlan" >> /etc/hosts' - node.vm.provision :pe_bootstrap do |p| - p.version_file = 'LATEST' - p.role = :agent - p.master = 'pe-2015latest-master.pe-bootstrap.vlan' + node.vm.provision :pe_agent do |p| + p.master_vm = 'pe-2015latest-master' end end end diff --git a/acceptance/skeletons/pe_build/Vagrantfile b/acceptance/skeletons/pe_build/Vagrantfile index cba646a..9aa88a4 100644 --- a/acceptance/skeletons/pe_build/Vagrantfile +++ b/acceptance/skeletons/pe_build/Vagrantfile @@ -8,7 +8,7 @@ end Vagrant.configure('2') do |config| config.pe_build.download_root = ENV['PE_BUILD_DOWNLOAD_ROOT'] # This is the box name used by the setup helpers in the acceptance tests. - config.vm.box = 'box' + config.vm.box = 'centos' config.vm.define 'pe-3x' do |node| node.vm.hostname = 'pe-3x.pe-bootstrap.vlan' diff --git a/tasks/acceptance.rake b/tasks/acceptance.rake index 6be7277..77e7df5 100644 --- a/tasks/acceptance.rake +++ b/tasks/acceptance.rake @@ -1,7 +1,8 @@ namespace :acceptance do ARTIFACT_DIR = File.join('acceptance', 'artifacts') TEST_BOXES = { - 'virtualbox.box' => 'https://s3.amazonaws.com/puppetlabs-vagrantcloud/centos-6.6-x86_64-virtualbox-nocm-1.0.1.box' + 'centos-virtualbox.box' => 'https://s3.amazonaws.com/puppetlabs-vagrantcloud/centos-6.6-x86_64-virtualbox-nocm-1.0.2.box', + 'ubuntu-virtualbox.box' => 'https://s3.amazonaws.com/puppetlabs-vagrantcloud/ubuntu-14.04-x86_64-virtualbox-nocm-1.0.2.box' } directory ARTIFACT_DIR diff --git a/vagrant-spec.config.example.rb b/vagrant-spec.config.example.rb index 4bf8036..a190894 100644 --- a/vagrant-spec.config.example.rb +++ b/vagrant-spec.config.example.rb @@ -10,7 +10,7 @@ c.skeleton_paths = [(acceptance_dir + 'skeletons').to_s] c.provider 'virtualbox', - box: (acceptance_dir + 'artifacts' + 'virtualbox.box').to_s, + boxes: Dir[acceptance_dir + 'artifacts' + '*-virtualbox.box'], # This folder should be filled with PE tarballs for CentOS. archive_path: (acceptance_dir + 'artifacts' + 'pe_archives').to_s, pe_latest: '2015.2.0',