diff options
-rw-r--r-- | knife/knife.gemspec | 1 | ||||
-rw-r--r-- | knife/lib/chef/knife/bootstrap/templates/windows-chef-client-msi.erb | 10 | ||||
-rw-r--r-- | knife/lib/chef/knife/core/windows_bootstrap_context.rb | 94 | ||||
-rw-r--r-- | knife/lib/constants.rb | 28 | ||||
-rw-r--r-- | knife/lib/exceptions.rb | 565 | ||||
-rw-r--r-- | knife/lib/json_compat.rb | 63 | ||||
-rw-r--r-- | knife/lib/mixin/powershell_exec.rb | 128 | ||||
-rw-r--r-- | knife/lib/powershell.rb | 84 | ||||
-rw-r--r-- | knife/lib/pwsh.rb | 71 | ||||
-rw-r--r-- | lib/chef/dsl/chef_vault.rb | 4 |
10 files changed, 1008 insertions, 40 deletions
diff --git a/knife/knife.gemspec b/knife/knife.gemspec index d121343211..98f4a15806 100644 --- a/knife/knife.gemspec +++ b/knife/knife.gemspec @@ -32,6 +32,7 @@ Gem::Specification.new do |s| # disabling this until we get get it to compile on RHEL 7 # s.add_dependency "x25519", ">= 1.0.9" # ed25519 KEX module. 1.0.9+ required to resolve sigill failures s.add_dependency "highline", ">= 1.6.9", "< 3" # Used in UI to present a list, no other usage. + s.add_dependency "ffi-libarchive", "~> 1.0", ">= 1.0.3" # archive_file resource s.add_dependency "tty-prompt", "~> 0.21" # knife ui.ask prompt s.add_dependency "tty-screen", "~> 0.6" # knife list diff --git a/knife/lib/chef/knife/bootstrap/templates/windows-chef-client-msi.erb b/knife/lib/chef/knife/bootstrap/templates/windows-chef-client-msi.erb index 7aa7be49f8..877d6d5c64 100644 --- a/knife/lib/chef/knife/bootstrap/templates/windows-chef-client-msi.erb +++ b/knife/lib/chef/knife/bootstrap/templates/windows-chef-client-msi.erb @@ -32,9 +32,6 @@ @echo Existing directory found, skipping creation. ) -> <%= bootstrap_directory %>\wget.vbs ( - <%= win_wget %> -) > <%= bootstrap_directory %>\wget.ps1 ( <%= win_wget_ps %> @@ -111,14 +108,15 @@ goto chef_installed :chef_installed @echo Checking for existing <%= ChefUtils::Dist::Infra::PRODUCT %> installation WHERE <%= ChefUtils::Dist::Infra::CLIENT %> >nul 2>nul +<%# If !ERRORLEVEL!==0 ( @echo Existing <%= ChefUtils::Dist::Infra::PRODUCT %> installation detected, skipping download - goto key_create +<%# goto key_create ) else ( @echo No existing installation of <%= ChefUtils::Dist::Infra::PRODUCT %> detected goto install -) - +<%# ) +%> :install @rem If user has provided the custom installation command, execute it <% if @config[:bootstrap_install_command] %> diff --git a/knife/lib/chef/knife/core/windows_bootstrap_context.rb b/knife/lib/chef/knife/core/windows_bootstrap_context.rb index f93e6572e3..4d79abe81e 100644 --- a/knife/lib/chef/knife/core/windows_bootstrap_context.rb +++ b/knife/lib/chef/knife/core/windows_bootstrap_context.rb @@ -15,10 +15,13 @@ # See the License for the specific language governing permissions and # limitations under the License. # - +require "pry" require_relative "bootstrap_context" require "chef-config/path_helper" unless defined?(ChefConfig::PathHelper) require "chef-utils/dist" unless defined?(ChefUtils::Dist) +require "mixin/powershell_exec" +# require "chef/mixin/powershell_exec" + class Chef class Knife @@ -30,6 +33,8 @@ class Chef # * @run_list - the run list for the node to bootstrap # class WindowsBootstrapContext < BootstrapContext + # include Chef::Mixin::PowershellExec + include Mixin::PowershellExec attr_accessor :config attr_accessor :chef_config attr_accessor :secret @@ -322,7 +327,10 @@ class Chef private def install_command(executor_quote) - "msiexec /qn /log #{executor_quote}%CHEF_CLIENT_MSI_LOG_PATH%#{executor_quote} /i #{executor_quote}%LOCAL_DESTINATION_MSI_PATH%#{executor_quote}" + # "msiexec /qn /log #{executor_quote}%CHEF_CLIENT_MSI_LOG_PATH%#{executor_quote} /i #{executor_quote}%LOCAL_DESTINATION_MSI_PATH%#{executor_quote}" + binding.pry + powershell_exec("echo hello") + "$a = (Start-Process -FilePath \"msiexec\" -Wait -ArgumentList \"/qn /log #{executor_quote}%CHEF_CLIENT_MSI_LOG_PATH%#{executor_quote} /i #{executor_quote}%LOCAL_DESTINATION_MSI_PATH%#{executor_quote} -Passthru).ExitCode" end # Returns a string for copying the trusted certificates on the workstation to the system being bootstrapped @@ -364,39 +372,57 @@ class Chef # need to be double quoted, schtasks allows the use of single # quotes that will later be converted to double quotes command = install_command("'") + binding.pry <<~EOH - @set MSIERRORCODE=!ERRORLEVEL! - @if ERRORLEVEL 1 ( - @echo WARNING: Failed to install #{ChefUtils::Dist::Infra::PRODUCT} MSI package in remote context with status code !MSIERRORCODE!. - @echo WARNING: This may be due to a defect in operating system update KB2918614: http://support.microsoft.com/kb/2918614 - @set OLDLOGLOCATION="%CHEF_CLIENT_MSI_LOG_PATH%-fail.log" - @move "%CHEF_CLIENT_MSI_LOG_PATH%" "!OLDLOGLOCATION!" > NUL - @echo WARNING: Saving installation log of failure at !OLDLOGLOCATION! - @echo WARNING: Retrying installation with local context... - @schtasks /create /f /sc once /st 00:00:00 /tn chefclientbootstraptask /ru SYSTEM /rl HIGHEST /tr \"cmd /c #{command} & sleep 2 & waitfor /s %computername% /si chefclientinstalldone\" - - @if ERRORLEVEL 1 ( - @echo ERROR: Failed to create #{ChefUtils::Dist::Infra::PRODUCT} installation scheduled task with status code !ERRORLEVEL! > "&2" - ) else ( - @echo Successfully created scheduled task to install #{ChefUtils::Dist::Infra::PRODUCT}. - @schtasks /run /tn chefclientbootstraptask - @if ERRORLEVEL 1 ( - @echo ERROR: Failed to execute #{ChefUtils::Dist::Infra::PRODUCT} installation scheduled task with status code !ERRORLEVEL!. > "&2" - ) else ( - @echo Successfully started #{ChefUtils::Dist::Infra::PRODUCT} installation scheduled task. - @echo Waiting for installation to complete -- this may take a few minutes... - waitfor chefclientinstalldone /t 600 - if ERRORLEVEL 1 ( - @echo ERROR: Timed out waiting for #{ChefUtils::Dist::Infra::PRODUCT} package to install - ) else ( - @echo Finished waiting for #{ChefUtils::Dist::Infra::PRODUCT} package to install. - ) - @schtasks /delete /f /tn chefclientbootstraptask > NUL - ) - ) - ) else ( - @echo Successfully installed #{ChefUtils::Dist::Infra::PRODUCT} package. - ) + $MSIERRORCODE = $a + + + Write-Warning "WARNING: Failed to install #{ChefUtils::Dist::Infra::PRODUCT} MSI package in remote context with status code $MSIERRORCODE." + Write-Warning "WARNING: This may be due to a defect in operating system update KB2918614: http://support.microsoft.com/kb/2918614" + $OLDLOGLOCATION="%CHEF_CLIENT_MSI_LOG_PATH%-fail.log" + Move-Item -Path "{CHEF_CLIENT_MSI_LOG_PATH}" -Destination "{OLDLOGLOCATION}" + Write-Warning WARNING: "Saving installation log of failure at ${OLDLOGLOCATION}" + Write-Warning WARNING: "Retrying installation with local context..." + + $command = "{CHEF_CLIENT_MSI_LOG_PATH}" + Write-Warning WARNING: #####################command $OLDLOGLOCATION ################### + New-Event -SourceIdentifier chefclientinstalldone -Sender $computername + $actions = (New-ScheduledTaskAction -Execute $command), (New-ScheduledTaskAction -Execute Start-Sleep 2) + $trigger = New-ScheduledTaskTrigger -Once -At '0:00 AM' + $principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -RunLevel Highest + $task = Register-ScheduledTask -TaskName "chefclientbootstraptask" -Trigger $trigger -Principal $principal -Action $actions + Wait-Event -SourceIdentifier chefclientinstalldone -Timeout 600 + + if(ERRORLEVEL -eq 1) + { + Write-Host "ERROR: Failed to create #{ChefUtils::Dist::Infra::PRODUCT} installation scheduled task with status code ${ERRORLEVEL} > "&2"" + } + else { + Write-Host "Successfully created scheduled task to install #{ChefUtils::Dist::Infra::PRODUCT}." + Start-ScheduledTask -TaskName "chefclientbootstraptask" + } + if(ERRORLEVEL -eq 1) + { + Write-Host "ERROR: Failed to execute #{ChefUtils::Dist::Infra::PRODUCT} installation scheduled task with status code ${ERRORLEVEL}. > "&2"" + } + else + { + Write-Host "Successfully started #{ChefUtils::Dist::Infra::PRODUCT} installation scheduled task." + Write-Host "Waiting for installation to complete -- this may take a few minutes..." + waitfor chefclientinstalldone /t 600 + if(ERRORLEVEL -eq 1) + { + Write-Host "ERROR: Timed out waiting for #{ChefUtils::Dist::Infra::PRODUCT} package to install" + } + else { + Write-Host "Finished waiting for #{ChefUtils::Dist::Infra::PRODUCT} package to install." + } + Unregister-ScheduledTask -TaskName 'chefclientbootstraptask' -Confirm:$false + } + } + + Write-Host "Successfully installed #{ChefUtils::Dist::Infra::PRODUCT} package." + EOH end end diff --git a/knife/lib/constants.rb b/knife/lib/constants.rb new file mode 100644 index 0000000000..0c78c9ee19 --- /dev/null +++ b/knife/lib/constants.rb @@ -0,0 +1,28 @@ +# +# Author:: John Keiser <jkeiser@chef.io> +# Copyright:: Copyright (c) Chef Software Inc. +# License:: Apache License, Version 2.0 +# +# 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. + +class Chef + NOT_PASSED = Object.new + def NOT_PASSED.to_s + "NOT_PASSED" + end + + def NOT_PASSED.inspect + to_s + end + NOT_PASSED.freeze +end diff --git a/knife/lib/exceptions.rb b/knife/lib/exceptions.rb new file mode 100644 index 0000000000..ffdbdcbaba --- /dev/null +++ b/knife/lib/exceptions.rb @@ -0,0 +1,565 @@ +# +# Author:: Adam Jacob (<adam@chef.io>) +# Author:: Seth Falcon (<seth@chef.io>) +# Author:: Kyle Goodwin (<kgoodwin@primerevenue.com>) +# Copyright:: Copyright (c) Chef Software Inc. +# License:: Apache License, Version 2.0 +# +# 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. + +require "chef-config/exceptions" +require "chef-utils/dist" unless defined?(ChefUtils::Dist) +require_relative "constants" + +class Chef + # == Chef::Exceptions + # Chef's custom exceptions are all contained within the Chef::Exceptions + # namespace. + class Exceptions + + ConfigurationError = ChefConfig::ConfigurationError + + # Backcompat with Chef::ShellOut code: + require "mixlib/shellout/exceptions" + + def self.const_missing(const_name) + if const_name == :ShellCommandFailed + Chef::Log.warn("Chef::Exceptions::ShellCommandFailed is deprecated, use Mixlib::ShellOut::ShellCommandFailed") + called_from = caller[0..3].inject("Called from:\n") { |msg, trace_line| msg << " #{trace_line}\n" } + Chef::Log.warn(called_from) + Mixlib::ShellOut::ShellCommandFailed + else + super + end + end + + class Application < RuntimeError; end + class SigInt < RuntimeError; end + class SigTerm < RuntimeError; end + class Cron < RuntimeError; end + class WindowsEnv < RuntimeError; end + class Exec < RuntimeError; end + class Execute < RuntimeError; end + class ErlCall < RuntimeError; end + class FileNotFound < RuntimeError; end + class Package < RuntimeError; end + class Service < RuntimeError; end + class Script < RuntimeError; end + class Route < RuntimeError; end + class SearchIndex < RuntimeError; end + class Override < RuntimeError; end + class UnsupportedAction < RuntimeError; end + class MissingLibrary < RuntimeError; end + + class CannotDetermineNodeName < RuntimeError + def initialize + super "Unable to determine node name: configure node_name or configure the system's hostname and fqdn" + end + end + + class User < RuntimeError; end + class Group < RuntimeError; end + class Link < RuntimeError; end + class Mount < RuntimeError; end + class Reboot < Exception; end # rubocop:disable Lint/InheritException + class RebootPending < Exception; end # rubocop:disable Lint/InheritException + class RebootFailed < Mixlib::ShellOut::ShellCommandFailed; end + class ClientUpgraded < Exception; end # rubocop:disable Lint/InheritException + class PrivateKeyMissing < RuntimeError; end + class CannotWritePrivateKey < RuntimeError; end + class RoleNotFound < RuntimeError; end + class DuplicateRole < RuntimeError; end + class ValidationFailed < ArgumentError; end + class CannotValidateStaticallyError < ArgumentError; end + class InvalidPrivateKey < ArgumentError; end + class MissingKeyAttribute < ArgumentError; end + class KeyCommandInputError < ArgumentError; end + + class BootstrapCommandInputError < ArgumentError + def initialize + super "You cannot pass both --json-attributes and --json-attribute-file. Please pass one or none." + end + end + + class InvalidKeyArgument < ArgumentError; end + class InvalidKeyAttribute < ArgumentError; end + class InvalidUserAttribute < ArgumentError; end + class InvalidClientAttribute < ArgumentError; end + class RedirectLimitExceeded < RuntimeError; end + class AmbiguousRunlistSpecification < ArgumentError; end + class CookbookFrozen < ArgumentError; end + class CookbookNotFound < RuntimeError; end + class OnlyApiVersion0SupportedForAction < RuntimeError; end + # Cookbook loader used to raise an argument error when cookbook not found. + # for back compat, need to raise an error that inherits from ArgumentError + class CookbookNotFoundInRepo < ArgumentError; end + class CookbookMergingError < RuntimeError; end + class RecipeNotFound < ArgumentError; end + # AttributeNotFound really means the attribute file could not be found + class AttributeNotFound < RuntimeError; end + # NoSuchAttribute is raised on access by node.read!("foo", "bar") when node["foo"]["bar"] does not exist. + class NoSuchAttribute < RuntimeError; end + # AttributeTypeMismatch is raised by node.write!("foo", "bar", "baz") when e.g. node["foo"] = "bar" (overwriting String with Hash) + class AttributeTypeMismatch < RuntimeError; end + class MissingCookbookDependency < StandardError; end # CHEF-5120 + class InvalidCommandOption < RuntimeError; end + class CommandTimeout < RuntimeError; end + class RequestedUIDUnavailable < RuntimeError; end + class InvalidHomeDirectory < ArgumentError; end + class DsclCommandFailed < RuntimeError; end + class PlistUtilCommandFailed < RuntimeError; end + class UserIDNotFound < ArgumentError; end + class GroupIDNotFound < ArgumentError; end + class ConflictingMembersInGroup < ArgumentError; end + class InvalidResourceReference < RuntimeError; end + class ResourceNotFound < RuntimeError; end + class ProviderNotFound < RuntimeError; end + NoProviderAvailable = ProviderNotFound + class VerificationNotFound < RuntimeError; end + class InvalidEventType < ArgumentError; end + class MultipleIdentityError < RuntimeError; end + # Used in Resource::ActionClass#load_current_resource to denote that + # the resource doesn't actually exist (for example, the file does not exist) + class CurrentValueDoesNotExist < RuntimeError; end + + # Can't find a Resource of this type that is valid on this platform. + class NoSuchResourceType < NameError + def initialize(short_name, node) + super "Cannot find a resource for #{short_name} on #{node[:platform]} version #{node[:platform_version]} with target_mode? #{Chef::Config.target_mode?}" + end + end + + class InvalidPolicybuilderCall < ArgumentError; end + + class InvalidResourceSpecification < ArgumentError; end + class SolrConnectionError < RuntimeError; end + class IllegalChecksumRevert < RuntimeError; end + class CookbookVersionNameMismatch < ArgumentError; end + class MissingParentDirectory < RuntimeError; end + class UnresolvableGitReference < RuntimeError; end + class InvalidRemoteGitReference < RuntimeError; end + class InvalidEnvironmentRunListSpecification < ArgumentError; end + class InvalidDataBagItemID < ArgumentError; end + class InvalidDataBagName < ArgumentError; end + class EnclosingDirectoryDoesNotExist < ArgumentError; end + # Errors originating from calls to the Win32 API + class Win32APIError < RuntimeError; end + + # Thrown when Win32 API layer binds to non-existent Win32 function. Occurs + # when older versions of Windows don't support newer Win32 API functions. + class Win32APIFunctionNotImplemented < NotImplementedError; end # rubocop:disable Lint/InheritException + # Attempting to run windows code on a not-windows node + class Win32NotWindows < RuntimeError; end + class WindowsNotAdmin < RuntimeError; end + # Attempting to access a 64-bit only resource on a 32-bit Windows system + class Win32ArchitectureIncorrect < RuntimeError; end + class ObsoleteDependencySyntax < ArgumentError; end + class InvalidDataBagPath < ArgumentError; end + class DuplicateDataBagItem < RuntimeError; end + + class PowershellCmdletException < RuntimeError; end + class LCMParser < RuntimeError; end + + class CannotDetermineHomebrewOwner < Package; end + class CannotDetermineWindowsInstallerType < Package; end + class NoWindowsPackageSource < Package; end + + # for example, if both recipes/default.yml, recipes/default.yaml are present + class AmbiguousYAMLFile < RuntimeError; end + + # Can not create staging file during file deployment + class FileContentStagingError < RuntimeError + def initialize(errors) + super "Staging tempfile can not be created during file deployment.\n Errors: #{errors.join('\n')}!" + end + end + + # A different version of a cookbook was added to a + # VersionedRecipeList than the one already there. + class CookbookVersionConflict < ArgumentError; end + + # does not follow X.Y.Z format. ArgumentError? + class InvalidPlatformVersion < ArgumentError; end + class InvalidCookbookVersion < ArgumentError; end + + # version constraint should be a string or array, or it doesn't + # match OP VERSION. ArgumentError? + class InvalidVersionConstraint < ArgumentError; end + + # Version constraints are not allowed in chef-solo + class IllegalVersionConstraint < NotImplementedError; end # rubocop:disable Lint/InheritException + + class MetadataNotValid < StandardError; end + + class MetadataNotFound < StandardError + attr_reader :install_path + attr_reader :cookbook_name + + def initialize(install_path, cookbook_name) + @install_path = install_path + @cookbook_name = cookbook_name + super "No metadata.rb or metadata.json found for cookbook #{@cookbook_name} in #{@install_path}" + end + end + + # File operation attempted but no permissions to perform it + class InsufficientPermissions < RuntimeError; end + + # Ifconfig failed + class Ifconfig < RuntimeError; end + + # Invalid "source" parameter to a remote_file resource + class InvalidRemoteFileURI < ArgumentError; end + + # Node::Attribute computes the merged version of of attributes + # and makes it read-only. Attempting to modify a read-only + # attribute will cause this error. + class ImmutableAttributeModification < NoMethodError + def initialize + super "Node attributes are read-only when you do not specify which precedence level to set. " + + %q{To set an attribute use code like `node.default["key"] = "value"'} + end + end + + # Merged node attributes are invalidated when the component + # attributes are updated. Attempting to read from a stale copy + # of merged attributes will trigger this error. + class StaleAttributeRead < StandardError; end + + # Registry Helper throws the following errors + class Win32RegArchitectureIncorrect < Win32ArchitectureIncorrect; end + class Win32RegHiveMissing < ArgumentError; end + class Win32RegKeyMissing < RuntimeError; end + class Win32RegValueMissing < RuntimeError; end + class Win32RegDataMissing < RuntimeError; end + class Win32RegValueExists < RuntimeError; end + class Win32RegNoRecursive < ArgumentError; end + class Win32RegTypeDoesNotExist < ArgumentError; end + class Win32RegBadType < ArgumentError; end + class Win32RegBadValueSize < ArgumentError; end + class Win32RegTypesMismatch < ArgumentError; end + + # incorrect input for registry_key create action throws following error + class RegKeyValuesTypeMissing < ArgumentError; end + class RegKeyValuesDataMissing < ArgumentError; end + + class InvalidEnvironmentPath < ArgumentError; end + class EnvironmentNotFound < RuntimeError; end + + # File-like resource found a non-file (socket, pipe, directory, etc) at its destination + class FileTypeMismatch < RuntimeError; end + + # File (or descendent) resource configured to manage symlink source, but + # the symlink that is there either loops or points to a nonexistent file + class InvalidSymlink < RuntimeError; end + + class ChildConvergeError < RuntimeError; end + + class DeprecatedFeatureError < RuntimeError + def initialize(message) + super("#{message} (raising error due to treat_deprecation_warnings_as_errors being set)") + end + end + + class MissingRole < RuntimeError + attr_reader :expansion + + def initialize(message_or_expansion = NOT_PASSED) + @expansion = nil + case message_or_expansion + when NOT_PASSED + super() + when String + super + when RunList::RunListExpansion + @expansion = message_or_expansion + missing_roles = @expansion.errors.join(", ") + super("The expanded run list includes nonexistent roles: #{missing_roles}") + end + end + + end + + class Secret + class RetrievalError < RuntimeError; end + class ConfigurationInvalid < RuntimeError; end + class FetchFailed < RuntimeError; end + class MissingSecretName < RuntimeError; end + class InvalidSecretName < RuntimeError; end + + class InvalidFetcherService < RuntimeError + def initialize(given, fetcher_service_names) + super("#{given} is not a supported secrets service. Supported services are: :#{fetcher_service_names.join(" :")}") + end + end + + class MissingFetcher < RuntimeError + def initialize(fetcher_service_names) + super("No secret service provided. Supported services are: :#{fetcher_service_names.join(" :")}") + end + end + + class Azure + class IdentityNotFound < RuntimeError + def initialize + super("The managed identity could not be found. This could mean one of the following things:\n\n" \ + " 1. The VM has no system or user assigned identities.\n" \ + " 2. The managed identity object_id or client_id that was specified is not assigned to the VM.\n") + end + end + end + end + + # Exception class for collecting multiple failures. Used when running + # delayed notifications so that chef can process each delayed + # notification even if chef client or other notifications fail. + class MultipleFailures < StandardError + def initialize(*args) + super + @all_failures = [] + end + + def message + base = "Multiple failures occurred:\n" + @all_failures.inject(base) do |message, (location, error)| + message << "* #{error.class} occurred in #{location}: #{error.message}\n" + end + end + + def client_run_failure(exception) + set_backtrace(exception.backtrace) + @all_failures << [ "#{ChefUtils::Dist::Infra::PRODUCT} run", exception ] + end + + def notification_failure(exception) + @all_failures << [ "delayed notification", exception ] + end + + def raise! + unless empty? + raise for_raise + end + end + + def empty? + @all_failures.empty? + end + + def for_raise + if @all_failures.size == 1 + @all_failures[0][1] + else + self + end + end + end + + class CookbookVersionSelection + + # Compound exception: In run_list expansion and resolution, + # run_list items referred to cookbooks that don't exist and/or + # have no versions available. + class InvalidRunListItems < StandardError + attr_reader :non_existent_cookbooks + attr_reader :cookbooks_with_no_matching_versions + + def initialize(message, non_existent_cookbooks, cookbooks_with_no_matching_versions) + super(message) + + @non_existent_cookbooks = non_existent_cookbooks + @cookbooks_with_no_matching_versions = cookbooks_with_no_matching_versions + end + + def to_json(*a) + result = { + "message" => message, + "non_existent_cookbooks" => non_existent_cookbooks, + "cookbooks_with_no_versions" => cookbooks_with_no_matching_versions, + } + Chef::JSONCompat.to_json(result, *a) + end + end + + # In run_list expansion and resolution, a constraint was + # unsatisfiable. + # + # This exception may not be the complete error report. If you + # resolve the misconfiguration represented by this exception and + # re-solve, you may get another exception + class UnsatisfiableRunListItem < StandardError + attr_reader :run_list_item + attr_reader :non_existent_cookbooks, :most_constrained_cookbooks + + # most_constrained_cookbooks: if I were to remove constraints + # regarding these cookbooks, I would get a solution or move on + # to the next error (deeper in the graph). An item in this list + # may be unsatisfiable, but when resolved may also reveal + # further unsatisfiable constraints; this condition would not be + # reported. + def initialize(message, run_list_item, non_existent_cookbooks, most_constrained_cookbooks) + super(message) + + @run_list_item = run_list_item + @non_existent_cookbooks = non_existent_cookbooks + @most_constrained_cookbooks = most_constrained_cookbooks + end + + def to_json(*a) + result = { + "message" => message, + "unsatisfiable_run_list_item" => run_list_item, + "non_existent_cookbooks" => non_existent_cookbooks, + "most_constrained_cookbooks" => most_constrained_cookbooks, + } + Chef::JSONCompat.to_json(result, *a) + end + end + + end # CookbookVersionSelection + + # When the server sends a redirect, RFC 2616 states a user-agent should + # not follow it with a method other than GET or HEAD, unless a specific + # action is taken by the user. A redirect received as response to a + # non-GET and non-HEAD request will thus raise an InvalidRedirect. + class InvalidRedirect < StandardError; end + + # Raised when the content length of a download does not match the content + # length declared in the http response. + class ContentLengthMismatch < RuntimeError + def initialize(response_length, content_length) + super <<~EOF + Response body length #{response_length} does not match HTTP Content-Length header #{content_length}. + This error is most often caused by network issues (proxies, etc) outside of #{ChefUtils::Dist::Infra::CLIENT}. + EOF + end + end + + class UnsupportedPlatform < RuntimeError + def initialize(platform) + super "This functionality is not supported on platform #{platform}." + end + end + + # Raised when Chef::Config[:run_lock_timeout] is set and some other client run fails + # to release the run lock before Chef::Config[:run_lock_timeout] seconds pass. + class RunLockTimeout < RuntimeError + def initialize(duration, blocking_pid) + super "Unable to acquire lock. Waited #{duration} seconds for #{blocking_pid} to release." + end + end + + class ChecksumMismatch < RuntimeError + def initialize(res_cksum, cont_cksum) + super "Checksum on resource (#{res_cksum}...) does not match checksum on content (#{cont_cksum}...)" + end + end + + class BadProxyURI < RuntimeError; end + + # Raised by Chef::JSONCompat + class JSON + class EncodeError < RuntimeError; end + class ParseError < RuntimeError; end + end + + class InvalidSearchQuery < ArgumentError; end + + # Raised by Chef::ProviderResolver + class AmbiguousProviderResolution < RuntimeError + def initialize(resource, classes) + super "Found more than one provider for #{resource.resource_name} resource: #{classes}" + end + end + + # If a converge fails, we want to wrap the output from those errors into 1 error so we can + # see both issues in the output. It is possible that nil will be provided. You must call `fill_backtrace` + # to correctly populate the backtrace with the wrapped backtraces. + class RunFailedWrappingError < RuntimeError + attr_reader :wrapped_errors + + def initialize(*errors) + errors = errors.compact + output = "Found #{errors.size} errors, they are stored in the backtrace" + @wrapped_errors = errors + super output + end + + def fill_backtrace + backtrace = [] + wrapped_errors.each_with_index do |e, i| + backtrace << "#{i + 1}) #{e.class} - #{e.message}" + backtrace += e.backtrace if e.backtrace + backtrace << "" unless i == wrapped_errors.length - 1 + end + set_backtrace(backtrace) + end + end + + class PIDFileLockfileMatch < RuntimeError + def initialize + super "PID file and lockfile are not permitted to match. Specify a different location with --pid or --lockfile" + end + end + + class CookbookChefVersionMismatch < RuntimeError + def initialize(chef_version, cookbook_name, cookbook_version, *constraints) + constraint_str = constraints.map { |c| c.requirement.as_list.to_s }.join(", ") + super "Cookbook '#{cookbook_name}' version '#{cookbook_version}' depends on #{ChefUtils::Dist::Infra::PRODUCT} version #{constraint_str}, but the running #{ChefUtils::Dist::Infra::PRODUCT} version is #{chef_version}" + end + end + + class CookbookOhaiVersionMismatch < RuntimeError + def initialize(ohai_version, cookbook_name, cookbook_version, *constraints) + constraint_str = constraints.map { |c| c.requirement.as_list.to_s }.join(", ") + super "Cookbook '#{cookbook_name}' version '#{cookbook_version}' depends on ohai version #{constraint_str}, but the running ohai version is #{ohai_version}" + end + end + + class MultipleDscResourcesFound < RuntimeError + attr_reader :resources_found + + def initialize(resources_found) + @resources_found = resources_found + matches_info = @resources_found.each do |r| + if r["Module"].nil? + "Resource #{r["Name"]} was found in #{r["Module"]["Name"]}" + else + "Resource #{r["Name"]} is a binary resource" + end + end + super "Found multiple resources matching #{matches_info[0]["Module"]["Name"]}:\n#{(matches_info.map { |f| f["Module"]["Version"] }).uniq.join("\n")}" + end + end + + # exception specific to invalid usage of 'dsc_resource' resource + class DSCModuleNameMissing < ArgumentError; end + + class GemRequirementConflict < RuntimeError + def initialize(gem_name, option, value1, value2) + super "Conflicting requirements for gem '#{gem_name}': Both #{value1.inspect} and #{value2.inspect} given for option #{option.inspect}" + end + end + + class UnifiedModeImmediateSubscriptionEarlierResource < RuntimeError + def initialize(notification) + super "immediate subscription from #{notification.resource} resource cannot be setup to #{notification.notifying_resource} resource, which has already fired while in unified mode" + end + end + + class UnifiedModeBeforeSubscriptionEarlierResource < RuntimeError + def initialize(notification) + super "before subscription from #{notification.resource} resource cannot be setup to #{notification.notifying_resource} resource, which has already fired while in unified mode" + end + end + end +end diff --git a/knife/lib/json_compat.rb b/knife/lib/json_compat.rb new file mode 100644 index 0000000000..2c143ab445 --- /dev/null +++ b/knife/lib/json_compat.rb @@ -0,0 +1,63 @@ +# +# Author:: Tim Hinderliter (<tim@chef.io>) +# Copyright:: Copyright (c) Chef Software Inc. +# License:: Apache License, Version 2.0 +# +# 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. + +# Wrapper class for interacting with JSON. + +autoload :FFI_Yajl, "ffi_yajl" +require_relative "exceptions" +# We're requiring this to prevent breaking consumers using Hash.to_json +require "json" unless defined?(JSON) + +class Chef + class JSONCompat + + class << self + + def parse(source, opts = {}) + FFI_Yajl::Parser.parse(source, opts) + rescue FFI_Yajl::ParseError => e + raise Chef::Exceptions::JSON::ParseError, e.message + end + + def from_json(source, opts = {}) + obj = parse(source, opts) + + # JSON gem requires top level object to be a Hash or Array (otherwise + # you get the "must contain two octets" error). Yajl doesn't impose the + # same limitation. For compatibility, we re-impose this condition. + unless obj.is_a?(Hash) || obj.is_a?(Array) + raise Chef::Exceptions::JSON::ParseError, "Top level JSON object must be a Hash or Array. (actual: #{obj.class})" + end + + obj + end + + def to_json(obj, opts = nil) + FFI_Yajl::Encoder.encode(obj, opts) + rescue FFI_Yajl::EncodeError => e + raise Chef::Exceptions::JSON::EncodeError, e.message + end + + def to_json_pretty(obj, opts = nil) + options_map = { pretty: true } + options_map[:indent] = opts[:indent] if opts.respond_to?(:key?) && opts.key?(:indent) + to_json(obj, options_map).chomp + end + + end + end +end diff --git a/knife/lib/mixin/powershell_exec.rb b/knife/lib/mixin/powershell_exec.rb new file mode 100644 index 0000000000..bbf8ae1a69 --- /dev/null +++ b/knife/lib/mixin/powershell_exec.rb @@ -0,0 +1,128 @@ +# +# Author:: Stuart Preston (<stuart@chef.io>) +# Copyright:: Copyright (c) Chef Software Inc. +# License:: Apache License, Version 2.0 +# +# 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. + +require_relative "../powershell" +require_relative "../pwsh" + +# The powershell_exec mixin provides in-process access to the PowerShell engine. +# +# powershell_exec is initialized with a string that should be set to the script +# to run and also takes an optional interpreter argument which must be either +# :powershell (Windows PowerShell which is the default) or :pwsh (PowerShell +# Core). It will return a Chef::PowerShell object that provides 5 methods: +# +# .result - returns a hash representing the results returned by executing the +# PowerShell script block +# .verbose - this is an array of string containing any messages written to the +# PowerShell verbose stream during execution +# .errors - this is an array of string containing any messages written to the +# PowerShell error stream during execution +# .error? - returns true if there were error messages written to the PowerShell +# error stream during execution +# .error! - raise Chef::PowerShell::CommandFailed if there was an error +# +# Some examples of usage: +# +# > powershell_exec("(Get-Item c:\\windows\\system32\\w32time.dll).VersionInfo" +# ).result["FileVersion"] +# => "10.0.14393.0 (rs1_release.160715-1616)" +# +# > powershell_exec("(get-process ruby).Mainmodule").result["FileName"] +# => C:\\opscode\\chef\\embedded\\bin\\ruby.exe" +# +# > powershell_exec("$a = $true; $a").result +# => true +# +# > powershell_exec("$PSVersionTable", :pwsh).result["PSEdition"] +# => "Core" +# +# > powershell_exec("not-found").errors +# => ["ObjectNotFound: (not-found:String) [], CommandNotFoundException: The +# term 'not-found' is not recognized as the name of a cmdlet, function, script +# file, or operable program. Check the spelling of the name, or if a path was +# included, verify that the path is correct and try again. (at <ScriptBlock>, +# <No file>: line 1)"] +# +# > powershell_exec("not-found").error? +# => true +# +# > powershell_exec("get-item c:\\notfound -erroraction stop") +# WIN32OLERuntimeError: (in OLE method `ExecuteScript': ) +# OLE error code:80131501 in System.Management.Automation +# The running command stopped because the preference variable +# "ErrorActionPreference" or common parameter is set to Stop: Cannot find +# path 'C:\notfound' because it does not exist. +# +# *Why use this and not powershell_out?* Startup time to invoke the PowerShell +# engine is much faster (over 7X faster in tests) than writing the PowerShell +# to disk, shelling out to powershell.exe and retrieving the .stdout or .stderr +# methods afterwards. Additionally we are able to have a higher fidelity +# conversation with PowerShell because we are now working with the objects that +# are returned by the script, rather than having to parse the stdout of +# powershell.exe to get a result. +# +# *How does this work?* In .NET terms, when you run a PowerShell script block +# through the engine, behind the scenes you get a Collection<PSObject> returned +# and simply we are serializing this, adding any errors that were generated to +# a custom JSON string transferred in memory to Ruby. The easiest way to +# develop for this approach is to imagine that the last thing that happens in +# your PowerShell script block is "ConvertTo-Json". That's exactly what we are +# doing here behind the scenes. +# +# There are a handful of current limitations with this approach: +# - Windows UAC elevation is controlled by the token assigned to the account +# that Ruby.exe is running under. +# - Terminating errors will result in a WIN32OLERuntimeError and typically are +# handled as an exception. +# - There are no return/error codes, as we are not shelling out to +# powershell.exe but calling a method inline, no errors codes are returned. +# - There is no settable timeout on powershell_exec method execution. +# - It is not possible to impersonate another user running powershell, the +# credentials of the user running Chef Client are used. +# + +class Chef + module Mixin + module PowershellExec + # Run a command under PowerShell via a managed (.NET) API. + # + # Requires: .NET Framework 4.0 or higher on the target machine. + # + # @param script [String] script to run + # @param interpreter [Symbol] the interpreter type, `:powershell` or `:pwsh` + # @return [Chef::PowerShell] output + def powershell_exec(script, interpreter = :powershell) + case interpreter + when :powershell + Chef::PowerShell.new(script) + when :pwsh + Chef::Pwsh.new(script) + else + raise ArgumentError, "Expected interpreter of :powershell or :pwsh" + end + end + + # The same as the #powershell_exec method except this will raise + # Chef::PowerShell::CommandFailed if the command fails + def powershell_exec!(script, interpreter = :powershell) + cmd = powershell_exec(script, interpreter) + cmd.error! + cmd + end + end + end +end diff --git a/knife/lib/powershell.rb b/knife/lib/powershell.rb new file mode 100644 index 0000000000..52ab66667f --- /dev/null +++ b/knife/lib/powershell.rb @@ -0,0 +1,84 @@ +# +# Author:: Stuart Preston (<stuart@chef.io>) +# Copyright:: Copyright (c) Chef Software Inc. +# License:: Apache License, Version 2.0 +# +# 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. + +require "ffi" #unless defined?(FFI) +require_relative "json_compat" +require "pry" + +class Chef + class PowerShell + extend FFI::Library + ffi_lib FFI::Library::LIBC + + + attr_reader :result + attr_reader :errors + attr_reader :verbose + + # Run a command under PowerShell via FFI + # This implementation requires the managed dll and native wrapper to be in the library search + # path on Windows (i.e. c:\windows\system32 or in the same location as ruby.exe). + # + # Requires: .NET Framework 4.0 or higher on the target machine. + # + # @param script [String] script to run + # @return [Object] output + def initialize(script) + binding.pry + # This Powershell DLL source lives here: https://github.com/chef/chef-powershell-shim + # Every merge into that repo triggers a Habitat build and promotion. Running + # the rake :update_chef_exec_dll task in this (chef/chef) repo will pull down + # the built packages and copy the binaries to distro/ruby_bin_folder. Bundle install + # ensures that the correct architecture binaries are installed into the path. + @dll ||= "Chef.PowerShell.Wrapper.dll" + exec(script) + end + + # + # Was there an error running the command + # + # @return [Boolean] + # + def error? + return true if errors.count > 0 + + false + end + + class CommandFailed < RuntimeError; end + + # + # @raise [Chef::PowerShell::CommandFailed] raise if the command failed + # + def error! + raise Chef::PowerShell::CommandFailed, "Unexpected exit in PowerShell command: #{@errors}" if error? + end + + protected + + def exec(script) + binding.pry + FFI.ffi_lib @dll + FFI.attach_function :execute_powershell, :ExecuteScript, [:string], :pointer + execution = FFI.execute_powershell(script).read_utf16string + hashed_outcome = Chef::JSONCompat.parse(execution) + @result = Chef::JSONCompat.parse(hashed_outcome["result"]) + @errors = hashed_outcome["errors"] + @verbose = hashed_outcome["verbose"] + end + end +end diff --git a/knife/lib/pwsh.rb b/knife/lib/pwsh.rb new file mode 100644 index 0000000000..3d067eb0d6 --- /dev/null +++ b/knife/lib/pwsh.rb @@ -0,0 +1,71 @@ +# +# Author:: Matt Wrock (<mwrock@chef.io>) +# Copyright:: Copyright (c) Chef Software Inc. +# License:: Apache License, Version 2.0 +# +# 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. + +class Chef + class Pwsh < Chef::PowerShell + + # Run a command under pwsh (powershell core) via FFI + # This implementation requires the managed dll, native wrapper and a + # published, self contained dotnet core directory tree to exist in the + # bindir directory. + # + # @param script [String] script to run + # @return [Object] output + def initialize(script) + @dll = Pwsh.dll + super + end + + protected + + def exec(script) + # Note that we need to override the location of the shared dotnet core library + # location. With most .net core applications, you can simply publish them as a + # "self-contained" application allowing consumers of the application to run them + # and use its own stand alone version of the .net core runtime. However because + # this is simply a dll and not an exe, it will look for the runtime in the shared + # .net core installation folder. By setting DOTNET_MULTILEVEL_LOOKUP to 0 we can + # override that folder's location with DOTNET_ROOT. To avoid the possibility of + # interfering with other .net core processes that might rely on the common shared + # location, we revert these variables after the script completes. + original_dml = ENV["DOTNET_MULTILEVEL_LOOKUP"] + original_dotnet_root = ENV["DOTNET_ROOT"] + original_dotnet_root_x86 = ENV["DOTNET_ROOT(x86)"] + + ENV["DOTNET_MULTILEVEL_LOOKUP"] = "0" + ENV["DOTNET_ROOT"] = RbConfig::CONFIG["bindir"] + ENV["DOTNET_ROOT(x86)"] = RbConfig::CONFIG["bindir"] + + super + ensure + ENV["DOTNET_MULTILEVEL_LOOKUP"] = original_dml + ENV["DOTNET_ROOT"] = original_dotnet_root + ENV["DOTNET_ROOT(x86)"] = original_dotnet_root_x86 + end + + def self.dll + # This Powershell DLL source lives here: https://github.com/chef/chef-powershell-shim + # Every merge into that repo triggers a Habitat build and promotion. Running + # the rake :update_chef_exec_dll task in this (chef/chef) repo will pull down + # the built packages and copy the binaries to distro/ruby_bin_folder. Bundle install + # ensures that the correct architecture binaries are installed into the path. + # Also note that the version of pwsh is determined by which assemblies the dll was + # built with. To update powershell, those dependencies must be bumped. + @dll ||= Dir.glob("#{RbConfig::CONFIG["bindir"]}/**/Chef.PowerShell.Wrapper.Core.dll").last + end + end +end diff --git a/lib/chef/dsl/chef_vault.rb b/lib/chef/dsl/chef_vault.rb index 3411f79e41..ffd2538771 100644 --- a/lib/chef/dsl/chef_vault.rb +++ b/lib/chef/dsl/chef_vault.rb @@ -19,6 +19,7 @@ autoload :ChefVault, "chef-vault" require_relative "data_query" +require "pry" class Chef module DSL @@ -37,6 +38,7 @@ class Chef # @param [String] bag Name of the data bag to load from. # @param [String] id Identifier of the data bag item to load. def chef_vault_item(bag, id) + binding.pry if ::ChefVault::Item.vault?(bag, id) ::ChefVault::Item.load(bag, id) elsif node["chef-vault"]["databag_fallback"] @@ -56,6 +58,7 @@ class Chef # @param [String] bag Name of the data bag to load from. # @return [Array] def chef_vault(bag) + binding.pry raise "'#{bag}' is not a vault" unless Chef::DataBag.list.include? bag pattern = Regexp.new(/_keys$/).freeze @@ -74,6 +77,7 @@ class Chef # @param [String] id Identifier of the data bag item to load. # @return [Hash] def chef_vault_item_for_environment(bag, id) + binding.pry item = chef_vault_item(bag, id) return {} unless item[node.chef_environment] |