summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorJames Bence <jbence@mdsol.com>2014-09-24 11:36:03 -0700
committerJames Bence <jbence@mdsol.com>2014-09-24 11:36:03 -0700
commitd1e51961cb649a37d1366fe408b8242c5f5ebdd2 (patch)
treefb12dddeca4a278f9652bfb68c61d26c015b8556 /lib
parent6d4bca71ff197319988f716f67b426fb73662c59 (diff)
parentbed2af024937259b93bb6955ee794cf70e20693f (diff)
downloadchef-d1e51961cb649a37d1366fe408b8242c5f5ebdd2.tar.gz
Merge branch 'master' of https://github.com/opscode/chef
Diffstat (limited to 'lib')
-rw-r--r--lib/chef/exceptions.rb6
-rw-r--r--lib/chef/knife/ssl_check.rb76
-rw-r--r--lib/chef/mixin/homebrew_owner.rb58
-rw-r--r--lib/chef/mixin/windows_architecture_helper.rb16
-rw-r--r--lib/chef/platform/provider_mapping.rb4
-rw-r--r--lib/chef/platform/query_helpers.rb6
-rw-r--r--lib/chef/provider/dsc_script.rb148
-rw-r--r--lib/chef/provider/package/homebrew.rb121
-rw-r--r--lib/chef/providers.rb2
-rw-r--r--lib/chef/resource/dsc_script.rb140
-rw-r--r--lib/chef/resource/homebrew_package.rb34
-rw-r--r--lib/chef/resources.rb2
-rw-r--r--lib/chef/util/dsc/configuration_generator.rb115
-rw-r--r--lib/chef/util/dsc/lcm_output_parser.rb133
-rw-r--r--lib/chef/util/dsc/local_configuration_manager.rb137
-rw-r--r--lib/chef/util/dsc/resource_info.rb26
-rw-r--r--lib/chef/util/powershell/cmdlet.rb136
-rw-r--r--lib/chef/util/powershell/cmdlet_result.rb46
18 files changed, 1197 insertions, 9 deletions
diff --git a/lib/chef/exceptions.rb b/lib/chef/exceptions.rb
index 23e223f204..67429ac5a2 100644
--- a/lib/chef/exceptions.rb
+++ b/lib/chef/exceptions.rb
@@ -118,6 +118,10 @@ class Chef
class InvalidDataBagPath < ArgumentError; end
class DuplicateDataBagItem < RuntimeError; end
+ class PowershellCmdletException < RuntimeError; end
+
+ class CannotDetermineHomebrewOwner < Package; end
+
# A different version of a cookbook was added to a
# VersionedRecipeList than the one already there.
class CookbookVersionConflict < ArgumentError ; end
@@ -179,6 +183,8 @@ class Chef
class ChildConvergeError < RuntimeError; end
+ class NoProviderAvailable < RuntimeError; end
+
class MissingRole < RuntimeError
NULL = Object.new
diff --git a/lib/chef/knife/ssl_check.rb b/lib/chef/knife/ssl_check.rb
index e98469d5aa..f2d368ff39 100644
--- a/lib/chef/knife/ssl_check.rb
+++ b/lib/chef/knife/ssl_check.rb
@@ -106,6 +106,22 @@ class Chef
end
end
+ def verify_X509
+ cert_debug_msg = ""
+ trusted_certificates.each do |cert_name|
+ message = check_X509_certificate(cert_name)
+ unless message.nil?
+ cert_debug_msg << File.expand_path(cert_name) + ": " + message + "\n"
+ end
+ end
+
+ unless cert_debug_msg.empty?
+ debug_invalid_X509(cert_debug_msg)
+ end
+
+ true # Maybe the bad certs won't hurt...
+ end
+
def verify_cert
ui.msg("Connecting to host #{host}:#{port}")
verify_peer_socket.connect
@@ -127,6 +143,35 @@ class Chef
false
end
+ def debug_invalid_X509(cert_debug_msg)
+ ui.msg("\n#{ui.color("Configuration Info:", :bold)}\n\n")
+ debug_ssl_settings
+ debug_chef_ssl_config
+
+ ui.warn(<<-BAD_CERTS)
+There are invalid certificates in your trusted_certs_dir.
+OpenSSL will not use the following certificates when verifying SSL connections:
+
+#{cert_debug_msg}
+
+#{ui.color("TO FIX THESE WARNINGS:", :bold)}
+
+We are working on documentation for resolving common issues uncovered here.
+
+* If the certificate is generated by the server, you may try redownloading the
+server's certificate. By default, the certificate is stored in the following
+location on the host where your chef-server runs:
+
+ /var/opt/chef-server/nginx/ca/SERVER_HOSTNAME.crt
+
+Copy that file to your trusted_certs_dir (currently: #{configuration.trusted_certs_dir})
+using SSH/SCP or some other secure method, then re-run this command to confirm
+that the server's certificate is now trusted.
+
+BAD_CERTS
+ # @TODO: ^ needs URL once documentation is posted.
+ end
+
def debug_invalid_cert
noverify_socket.connect
issuer_info = noverify_socket.peer_cert.issuer
@@ -148,7 +193,7 @@ where your chef-server runs:
/var/opt/chef-server/nginx/ca/SERVER_HOSTNAME.crt
-Copy that file to you trusted_certs_dir (currently: #{configuration.trusted_certs_dir})
+Copy that file to your trusted_certs_dir (currently: #{configuration.trusted_certs_dir})
using SSH/SCP or some other secure method, then re-run this command to confirm
that the server's certificate is now trusted.
@@ -197,17 +242,36 @@ ADVICE
def run
validate_uri
- if verify_cert && verify_cert_host
+ if verify_X509 && verify_cert && verify_cert_host
ui.msg "Successfully verified certificates from `#{host}'"
else
exit 1
end
end
+ private
+ def trusted_certificates
+ if configuration.trusted_certs_dir && Dir.exist?(configuration.trusted_certs_dir)
+ Dir.glob(File.join(configuration.trusted_certs_dir, "*.{crt,pem}"))
+ else
+ []
+ end
+ end
+
+ def check_X509_certificate(cert_file)
+ store = OpenSSL::X509::Store.new
+ cert = OpenSSL::X509::Certificate.new(IO.read(File.expand_path(cert_file)))
+ begin
+ store.add_cert(cert)
+ # test if the store can verify the cert we just added
+ unless store.verify(cert) # true if verified, false if not
+ return store.error_string
+ end
+ rescue OpenSSL::X509::StoreError => e
+ return e.message
+ end
+ return nil
+ end
end
end
end
-
-
-
-
diff --git a/lib/chef/mixin/homebrew_owner.rb b/lib/chef/mixin/homebrew_owner.rb
new file mode 100644
index 0000000000..73bb22ddf5
--- /dev/null
+++ b/lib/chef/mixin/homebrew_owner.rb
@@ -0,0 +1,58 @@
+#
+# Author:: Joshua Timberman (<joshua@getchef.com>)
+# Author:: Graeme Mathieson (<mathie@woss.name>)
+#
+# Copyright 2011-2013, Opscode, Inc.
+# Copyright 2014, Chef Software, Inc <legal@getchef.com>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Ported from the homebrew cookbook's Homebrew::Mixin owner helpers
+#
+# This lives here in Chef::Mixin because Chef's namespacing makes it
+# awkward to use modules elsewhere (e.g., chef/provider/package/homebrew/owner)
+
+class Chef
+ module Mixin
+ module HomebrewOwner
+ def homebrew_owner(node)
+ @homebrew_owner ||= calculate_owner(node)
+ end
+
+ private
+
+ def calculate_owner(node)
+ owner = homebrew_owner_attr(node) || sudo_user || current_user
+ if owner == 'root'
+ raise Chef::Exceptions::CannotDetermineHomebrewOwner,
+ 'The homebrew owner is not specified and the current user is \"root\"' +
+ 'Homebrew does not support root installs, please specify the homebrew' +
+ 'owner by setting the attribute `node[\'homebrew\'][\'owner\']`.'
+ end
+ owner
+ end
+
+ def homebrew_owner_attr(node)
+ node['homebrew']['owner'] if node.attribute?('homebrew') && node['homebrew'].attribute?('owner')
+ end
+
+ def sudo_user
+ ENV['SUDO_USER']
+ end
+
+ def current_user
+ ENV['USER']
+ end
+ end
+ end
+end
diff --git a/lib/chef/mixin/windows_architecture_helper.rb b/lib/chef/mixin/windows_architecture_helper.rb
index ff118c1d3d..65ad042910 100644
--- a/lib/chef/mixin/windows_architecture_helper.rb
+++ b/lib/chef/mixin/windows_architecture_helper.rb
@@ -42,6 +42,22 @@ class Chef
is_i386_process_on_x86_64_windows?
end
+ def with_os_architecture(node)
+ wow64_redirection_state = nil
+
+ if wow64_architecture_override_required?(node, node_windows_architecture(node))
+ wow64_redirection_state = disable_wow64_file_redirection(node)
+ end
+
+ begin
+ yield
+ ensure
+ if wow64_redirection_state
+ restore_wow64_file_redirection(node, wow64_redirection_state)
+ end
+ end
+ end
+
def node_supports_windows_architecture?(node, desired_architecture)
assert_valid_windows_architecture!(desired_architecture)
return (node_windows_architecture(node) == :x86_64 ||
diff --git a/lib/chef/platform/provider_mapping.rb b/lib/chef/platform/provider_mapping.rb
index 7f79c38a6a..b10fecc53e 100644
--- a/lib/chef/platform/provider_mapping.rb
+++ b/lib/chef/platform/provider_mapping.rb
@@ -40,7 +40,7 @@ class Chef
{
:mac_os_x => {
:default => {
- :package => Chef::Provider::Package::Macports,
+ :package => Chef::Provider::Package::Homebrew,
:service => Chef::Provider::Service::Macosx,
:user => Chef::Provider::User::Dscl,
:group => Chef::Provider::Group::Dscl
@@ -48,7 +48,7 @@ class Chef
},
:mac_os_x_server => {
:default => {
- :package => Chef::Provider::Package::Macports,
+ :package => Chef::Provider::Package::Homebrew,
:service => Chef::Provider::Service::Macosx,
:user => Chef::Provider::User::Dscl,
:group => Chef::Provider::Group::Dscl
diff --git a/lib/chef/platform/query_helpers.rb b/lib/chef/platform/query_helpers.rb
index f6f5309de5..334ab278d1 100644
--- a/lib/chef/platform/query_helpers.rb
+++ b/lib/chef/platform/query_helpers.rb
@@ -45,7 +45,11 @@ class Chef
is_server_2003
end
- end
+ def supports_dsc?(node)
+ node[:languages] && node[:languages][:powershell] &&
+ node[:languages][:powershell][:version].to_i >= 4
+ end
+ end
end
end
diff --git a/lib/chef/provider/dsc_script.rb b/lib/chef/provider/dsc_script.rb
new file mode 100644
index 0000000000..5d7322842c
--- /dev/null
+++ b/lib/chef/provider/dsc_script.rb
@@ -0,0 +1,148 @@
+#
+# Author:: Adam Edwards (<adamed@getchef.com>)
+#
+# Copyright:: 2014, Chef Software, Inc.
+#
+# 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/util/powershell/cmdlet'
+require 'chef/util/dsc/configuration_generator'
+require 'chef/util/dsc/local_configuration_manager'
+
+class Chef
+ class Provider
+ class DscScript < Chef::Provider
+ def initialize(dsc_resource, run_context)
+ super(dsc_resource, run_context)
+ @dsc_resource = dsc_resource
+ @resource_converged = false
+ @operations = {
+ :set => Proc.new { |config_manager, document|
+ config_manager.set_configuration(document)
+ },
+ :test => Proc.new { |config_manager, document|
+ config_manager.test_configuration(document)
+ }}
+ end
+
+ def action_run
+ if ! @resource_converged
+ converge_by(generate_description) do
+ run_configuration(:set)
+ Chef::Log.info("DSC resource configuration completed successfully")
+ end
+ end
+ end
+
+ def load_current_resource
+ @dsc_resources_info = run_configuration(:test)
+ @resource_converged = @dsc_resources_info.all? do |resource|
+ !resource.changes_state?
+ end
+ end
+
+ def whyrun_supported?
+ true
+ end
+
+ protected
+
+ def run_configuration(operation)
+ config_directory = ::Dir.mktmpdir("chef-dsc-script")
+ configuration_data_path = get_configuration_data_path(config_directory)
+ configuration_flags = get_augmented_configuration_flags(configuration_data_path)
+
+ config_manager = Chef::Util::DSC::LocalConfigurationManager.new(@run_context.node, config_directory)
+
+ begin
+ configuration_document = generate_configuration_document(config_directory, configuration_flags)
+ @operations[operation].call(config_manager, configuration_document)
+ rescue Exception => e
+ Chef::Log.error("DSC operation failed: #{e.message.to_s}")
+ raise e
+ ensure
+ ::FileUtils.rm_rf(config_directory)
+ end
+ end
+
+ def get_augmented_configuration_flags(configuration_data_path)
+ updated_flags = nil
+ if configuration_data_path
+ updated_flags = @dsc_resource.flags.nil? ? {} : @dsc_resource.flags.dup
+ Chef::Util::PathHelper.validate_path(configuration_data_path)
+ updated_flags[:configurationdata] = configuration_data_path
+ end
+ updated_flags
+ end
+
+ def generate_configuration_document(config_directory, configuration_flags)
+ shellout_flags = {
+ :cwd => @dsc_resource.cwd,
+ :environment => @dsc_resource.environment,
+ :timeout => @dsc_resource.timeout
+ }
+
+ generator = Chef::Util::DSC::ConfigurationGenerator.new(@run_context.node, config_directory)
+
+ if @dsc_resource.command
+ generator.configuration_document_from_script_path(@dsc_resource.command, configuration_name, configuration_flags, shellout_flags)
+ else
+ # If code is also not provided, we mimic what the other script resources do (execute nothing)
+ Chef::Log.warn("Neither code or command were provided for dsc_resource[#{@dsc_resource.name}].") unless @dsc_resource.code
+ generator.configuration_document_from_script_code(@dsc_resource.code || '', configuration_flags, shellout_flags)
+ end
+ end
+
+ def get_configuration_data_path(config_directory)
+ if @dsc_resource.configuration_data_script
+ @dsc_resource.configuration_data_script
+ elsif @dsc_resource.configuration_data
+ configuration_data_path = "#{config_directory}/chef_dsc_config_data.psd1"
+ ::File.open(configuration_data_path, 'wt') do | script |
+ script.write(@dsc_resource.configuration_data)
+ end
+ configuration_data_path
+ end
+ end
+
+ def configuration_name
+ @dsc_resource.configuration_name || @dsc_resource.name
+ end
+
+ def configuration_friendly_name
+ if @dsc_resource.code
+ @dsc_resource.name
+ else
+ configuration_name
+ end
+ end
+
+ private
+
+ def generate_description
+ ["converge DSC configuration '#{configuration_friendly_name}'"] +
+ @dsc_resources_info.map do |resource|
+ if resource.changes_state?
+ # We ignore the last log message because it only contains the time it took, which looks weird
+ cleaned_messages = resource.change_log[0..-2].map { |c| c.sub(/^#{Regexp.escape(resource.name)}/, '').strip }
+ "converge DSC resource #{resource.name} by #{cleaned_messages.find_all{ |c| c != ''}.join("\n")}"
+ else
+ # This is needed because a dsc script can have resouces that are both converged and not
+ "converge DSC resource #{resource.name} by doing nothing because it is already converged"
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/provider/package/homebrew.rb b/lib/chef/provider/package/homebrew.rb
new file mode 100644
index 0000000000..d964703c87
--- /dev/null
+++ b/lib/chef/provider/package/homebrew.rb
@@ -0,0 +1,121 @@
+#
+# Author:: Joshua Timberman (<joshua@getchef.com>)
+# Author:: Graeme Mathieson (<mathie@woss.name>)
+#
+# Copyright 2011-2013, Opscode, Inc.
+# Copyright 2014, Chef Software, Inc <legal@getchef.com>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'etc'
+require 'chef/mixin/homebrew_owner'
+
+class Chef
+ class Provider
+ class Package
+ class Homebrew < Chef::Provider::Package
+ include Chef::Mixin::HomebrewOwner
+ def load_current_resource
+ self.current_resource = Chef::Resource::Package.new(new_resource.name)
+ current_resource.package_name(new_resource.package_name)
+ current_resource.version(current_installed_version)
+ Chef::Log.debug("#{new_resource} current version is #{current_resource.version}") if current_resource.version
+
+ @candidate_version = candidate_version
+
+ Chef::Log.debug("#{new_resource} candidate version is #{@candidate_version}") if @candidate_version
+
+ current_resource
+ end
+
+ def install_package(name, version)
+ unless current_resource.version == version
+ brew('install', new_resource.options, name)
+ end
+ end
+
+ def upgrade_package(name, version)
+ current_version = current_resource.version
+
+ if current_version.nil? or current_version.empty?
+ install_package(name, version)
+ elsif current_version != version
+ brew('upgrade', new_resource.options, name)
+ end
+ end
+
+ def remove_package(name, version)
+ if current_resource.version
+ brew('uninstall', new_resource.options, name)
+ end
+ end
+
+ # Homebrew doesn't really have a notion of purging, do a "force remove"
+ def purge_package(name, version)
+ new_resource.options((new_resource.options || '') << ' --force').strip
+ remove_package(name, version)
+ end
+
+ def brew(*args)
+ get_response_from_command("brew #{args.join(' ')}")
+ end
+
+ # We implement a querying method that returns the JSON-as-Hash
+ # data for a formula per the Homebrew documentation. Previous
+ # implementations of this provider in the homebrew cookbook
+ # performed a bit of magic with the load path to get this
+ # information, but that is not any more robust than using the
+ # command-line interface that returns the same thing.
+ #
+ # https://github.com/Homebrew/homebrew/wiki/Querying-Brew
+ def brew_info
+ @brew_info ||= Chef::JSONCompat.from_json(brew('info', '--json=v1', new_resource.package_name)).first
+ end
+
+ # Some packages (formula) are "keg only" and aren't linked,
+ # because multiple versions installed can cause conflicts. We
+ # handle this by using the last installed version as the
+ # "current" (as in latest). Otherwise, we will use the version
+ # that brew thinks is linked as the current version.
+ #
+ def current_installed_version
+ brew_info['keg_only'] ? brew_info['installed'].last['version'] : brew_info['linked_keg']
+ end
+
+ # Packages (formula) available to install should have a
+ # "stable" version, per the Homebrew project's acceptable
+ # formula documentation, so we will rely on that being the
+ # case. Older implementations of this provider in the homebrew
+ # cookbook would fall back to +brew_info['version']+, but the
+ # schema has changed, and homebrew is a constantly rolling
+ # forward project.
+ #
+ # https://github.com/Homebrew/homebrew/wiki/Acceptable-Formulae#stable-versions
+ def candidate_version
+ brew_info['versions']['stable']
+ end
+
+ private
+
+ def get_response_from_command(command)
+ home_dir = Etc.getpwnam(homebrew_owner(node)).dir
+
+ Chef::Log.debug "Executing '#{command}' as user '#{homebrew_owner(node)}'"
+ output = shell_out!(command, :timeout => 1800, :user => homebrew_owner(node), :environment => { 'HOME' => home_dir, 'RUBYOPT' => nil })
+ output.stdout.chomp
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/providers.rb b/lib/chef/providers.rb
index 1b0ff3ffff..fec00d0e63 100644
--- a/lib/chef/providers.rb
+++ b/lib/chef/providers.rb
@@ -24,6 +24,7 @@ require 'chef/provider/cron/solaris'
require 'chef/provider/cron/aix'
require 'chef/provider/deploy'
require 'chef/provider/directory'
+require 'chef/provider/dsc_script'
require 'chef/provider/env'
require 'chef/provider/erl_call'
require 'chef/provider/execute'
@@ -59,6 +60,7 @@ require 'chef/provider/package/easy_install'
require 'chef/provider/package/freebsd/port'
require 'chef/provider/package/freebsd/pkg'
require 'chef/provider/package/freebsd/pkgng'
+require 'chef/provider/package/homebrew'
require 'chef/provider/package/ips'
require 'chef/provider/package/macports'
require 'chef/provider/package/pacman'
diff --git a/lib/chef/resource/dsc_script.rb b/lib/chef/resource/dsc_script.rb
new file mode 100644
index 0000000000..2972ace1aa
--- /dev/null
+++ b/lib/chef/resource/dsc_script.rb
@@ -0,0 +1,140 @@
+#
+# Author:: Adam Edwards (<adamed@getchef.com>)
+# Copyright:: Copyright (c) 2014 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/exceptions'
+
+class Chef
+ class Resource
+ class DscScript < Chef::Resource
+
+ provides :dsc_script, :on_platforms => ["windows"]
+
+ def initialize(name, run_context=nil)
+ super
+ @allowed_actions.push(:run)
+ @action = :run
+ if(run_context && Chef::Platform.supports_dsc?(run_context.node))
+ @provider = Chef::Provider::DscScript
+ else
+ raise Chef::Exceptions::NoProviderAvailable,
+ "#{powershell_info_str(run_context)}\nPowershell 4.0 or higher was not detected on your system and is required to use the dsc_script resource."
+ end
+ end
+
+ def code(arg=nil)
+ if arg && command
+ raise ArgumentError, "Only one of 'code' and 'command' attributes may be specified"
+ end
+ if arg && configuration_name
+ raise ArgumentError, "The 'code' and 'command' attributes may not be used together"
+ end
+ set_or_return(
+ :code,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+
+ def configuration_name(arg=nil)
+ if arg && code
+ raise ArgumentError, "Attribute `configuration_name` may not be set if `code` is set"
+ end
+ set_or_return(
+ :configuration_name,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+
+ def command(arg=nil)
+ if arg && code
+ raise ArgumentError, "The 'code' and 'command' attributes may not be used together"
+ end
+ set_or_return(
+ :command,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+
+ def configuration_data(arg=nil)
+ if arg && configuration_data_script
+ raise ArgumentError, "The 'configuration_data' and 'configuration_data_script' attributes may not be used together"
+ end
+ set_or_return(
+ :configuration_data,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+
+ def configuration_data_script(arg=nil)
+ if arg && configuration_data
+ raise ArgumentError, "The 'configuration_data' and 'configuration_data_script' attributes may not be used together"
+ end
+ set_or_return(
+ :configuration_data_script,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+
+ def flags(arg=nil)
+ set_or_return(
+ :flags,
+ arg,
+ :kind_of => [ Hash ]
+ )
+ end
+
+ def cwd(arg=nil)
+ set_or_return(
+ :cwd,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+
+ def environment(arg=nil)
+ set_or_return(
+ :environment,
+ arg,
+ :kind_of => [ Hash ]
+ )
+ end
+
+ def timeout(arg=nil)
+ set_or_return(
+ :timeout,
+ arg,
+ :kind_of => [ Integer ]
+ )
+ end
+
+ private
+
+ def powershell_info_str(run_context)
+ if run_context && run_context.node[:languages] && run_context.node[:languages][:powershell]
+ install_info = "Powershell #{run_context.node[:languages][:powershell][:version]} was found on the system."
+ else
+ install_info = 'Powershell was not found.'
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/resource/homebrew_package.rb b/lib/chef/resource/homebrew_package.rb
new file mode 100644
index 0000000000..c3fa9ddffc
--- /dev/null
+++ b/lib/chef/resource/homebrew_package.rb
@@ -0,0 +1,34 @@
+#
+# Author:: Joshua Timberman (<joshua@getchef.com>)
+# Author:: Graeme Mathieson (<mathie@woss.name>)
+#
+# Copyright 2011-2013, Opscode, Inc.
+# Copyright 2014, Chef Software, Inc <legal@getchef.com>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/provider/package'
+require 'chef/resource/package'
+
+class Chef
+ class Resource
+ class HomebrewPackage < Chef::Resource::Package
+ def initialize(name, run_context=nil)
+ super
+ @resource_name = :homebrew_package
+ @provider = Chef::Provider::Package::Homebrew
+ end
+ end
+ end
+end
diff --git a/lib/chef/resources.rb b/lib/chef/resources.rb
index 8c2f71bd30..5b938095c6 100644
--- a/lib/chef/resources.rb
+++ b/lib/chef/resources.rb
@@ -28,6 +28,7 @@ require 'chef/resource/deploy'
require 'chef/resource/deploy_revision'
require 'chef/resource/directory'
require 'chef/resource/dpkg_package'
+require 'chef/resource/dsc_script'
require 'chef/resource/easy_install_package'
require 'chef/resource/env'
require 'chef/resource/erl_call'
@@ -39,6 +40,7 @@ require 'chef/resource/gem_package'
require 'chef/resource/git'
require 'chef/resource/group'
require 'chef/resource/http_request'
+require 'chef/resource/homebrew_package'
require 'chef/resource/ifconfig'
require 'chef/resource/link'
require 'chef/resource/log'
diff --git a/lib/chef/util/dsc/configuration_generator.rb b/lib/chef/util/dsc/configuration_generator.rb
new file mode 100644
index 0000000000..12cd5dc3a2
--- /dev/null
+++ b/lib/chef/util/dsc/configuration_generator.rb
@@ -0,0 +1,115 @@
+#
+# Author:: Adam Edwards (<adamed@getchef.com>)
+#
+# Copyright:: 2014, Chef Software, Inc.
+#
+# 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/util/powershell/cmdlet'
+
+class Chef::Util::DSC
+ class ConfigurationGenerator
+ def initialize(node, config_directory)
+ @node = node
+ @config_directory = config_directory
+ end
+
+ def configuration_document_from_script_code(code, configuration_flags, shellout_flags)
+ Chef::Log.debug("DSC: DSC code:\n '#{code}'")
+ generated_script_path = write_document_generation_script(code, 'chef_dsc')
+ begin
+ configuration_document_from_script_path(generated_script_path, 'chef_dsc', configuration_flags, shellout_flags)
+ ensure
+ ::FileUtils.rm(generated_script_path)
+ end
+ end
+
+ def configuration_document_from_script_path(script_path, configuration_name, configuration_flags, shellout_flags)
+ validate_configuration_name!(configuration_name)
+
+ document_generation_cmdlet = Chef::Util::Powershell::Cmdlet.new(
+ @node,
+ configuration_document_generation_code(script_path, configuration_name))
+
+ merged_configuration_flags = get_merged_configuration_flags!(configuration_flags, configuration_name)
+
+ document_generation_cmdlet.run!(merged_configuration_flags, shellout_flags)
+ configuration_document_location = find_configuration_document(configuration_name)
+
+ if ! configuration_document_location
+ raise RuntimeError, "No DSC configuration for '#{configuration_name}' was generated from supplied DSC script"
+ end
+
+ configuration_document = get_configuration_document(configuration_document_location)
+ ::FileUtils.rm_rf(configuration_document_location)
+ configuration_document
+ end
+
+ protected
+
+ # From PowerShell error help for the Configuration language element:
+ # Standard names may only contain letters (a-z, A-Z), numbers (0-9), and underscore (_).
+ # The name may not be null or empty, and should start with a letter.
+ def validate_configuration_name!(configuration_name)
+ if !!(configuration_name =~ /\A[A-Za-z]+[_a-zA-Z0-9]*\Z/) == false
+ raise ArgumentError, 'Configuration `#{configuration_name}` is not a valid PowerShell cmdlet name'
+ end
+ end
+
+ def get_merged_configuration_flags!(configuration_flags, configuration_name)
+ merged_configuration_flags = { :outputpath => configuration_document_directory(configuration_name) }
+ if configuration_flags
+ configuration_flags.map do | switch, value |
+ if merged_configuration_flags.key?(switch.to_s.downcase.to_sym)
+ raise ArgumentError, "The `flags` attribute for the dsc_script resource contained a command line switch :#{switch.to_s} that is disallowed."
+ end
+ merged_configuration_flags[switch.to_s.downcase.to_sym] = value
+ end
+ end
+ merged_configuration_flags
+ end
+
+ def configuration_code(code, configuration_name)
+ "$ProgressPreference = 'SilentlyContinue';Configuration '#{configuration_name}'\n{\n\tnode 'localhost'\n{\n\t#{code}\n}}\n"
+ end
+
+ def configuration_document_generation_code(configuration_script, configuration_name)
+ ". '#{configuration_script}';#{configuration_name}"
+ end
+
+ def write_document_generation_script(code, configuration_name)
+ script_path = "#{@config_directory}/chef_dsc_config.ps1"
+ ::File.open(script_path, 'wt') do | script |
+ script.write(configuration_code(code, configuration_name))
+ end
+ script_path
+ end
+
+ def find_configuration_document(configuration_name)
+ document_directory = configuration_document_directory(configuration_name)
+ document_file_name = ::Dir.entries(document_directory).find { | path | path =~ /.*.mof/ }
+ ::File.join(document_directory, document_file_name) if document_file_name
+ end
+
+ def configuration_document_directory(configuration_name)
+ ::File.join(@config_directory, configuration_name)
+ end
+
+ def get_configuration_document(document_path)
+ ::File.open(document_path, 'rb') do | file |
+ file.read
+ end
+ end
+ end
+end
diff --git a/lib/chef/util/dsc/lcm_output_parser.rb b/lib/chef/util/dsc/lcm_output_parser.rb
new file mode 100644
index 0000000000..f8f853a33a
--- /dev/null
+++ b/lib/chef/util/dsc/lcm_output_parser.rb
@@ -0,0 +1,133 @@
+#
+# Author:: Jay Mundrawala (<jdm@getchef.com>)
+#
+# Copyright:: 2014, Chef Software, Inc.
+#
+# 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/log'
+require 'chef/util/dsc/resource_info'
+
+class Chef
+ class Util
+ class DSC
+ class LocalConfigurationManager
+ module Parser
+ # Parses the output from LCM and returns a list of Chef::Util::DSC::ResourceInfo objects
+ # that describe how the resources affected the system
+ #
+ # Example:
+ # parse <<-EOF
+ # What if: [Machine]: LCM: [Start Set ]
+ # What if: [Machine]: LCM: [Start Resource ] [[File]FileToNotBeThere]
+ # What if: [Machine]: LCM: [Start Set ] [[File]FileToNotBeThere]
+ # What if: [C:\ShouldNotExist.txt] removed
+ # What if: [Machine]: LCM: [End Set ] [[File]FileToNotBeThere] in 0.1 seconds
+ # What if: [Machine]: LCM: [End Resource ] [[File]FileToNotBeThere]
+ # What if: [Machine]: LCM: [End Set ]
+ # EOF
+ #
+ # would return
+ #
+ # [
+ # Chef::Util::DSC::ResourceInfo.new(
+ # '[[File]FileToNotBeThere]',
+ # true,
+ # [
+ # '[[File]FileToNotBeThere]',
+ # '[C:\Shouldnotexist.txt]',
+ # '[[File]FileToNotBeThere] in 0.1 seconds'
+ # ]
+ # )
+ # ]
+ #
+ def self.parse(lcm_output)
+ return [] unless lcm_output
+
+ current_resource = Hash.new
+
+ resources = []
+ lcm_output.lines.each do |line|
+ op_action, op_type, info = parse_line(line)
+
+ case op_action
+ when :start
+ case op_type
+ when :set
+ if current_resource[:name]
+ current_resource[:context] = :logging
+ current_resource[:logs] = [info]
+ end
+ when :resource
+ if current_resource[:name]
+ resources.push(current_resource)
+ end
+ current_resource = {:name => info}
+ else
+ Chef::Log.debug("Ignoring op_action #{op_action}: Read line #{line}")
+ end
+ when :end
+ # Make sure we log the last line
+ if current_resource[:context] == :logging and info.include? current_resource[:name]
+ current_resource[:logs].push(info)
+ end
+ current_resource[:context] = nil
+ when :skip
+ current_resource[:skipped] = true
+ when :info
+ if current_resource[:context] == :logging
+ current_resource[:logs].push(info)
+ end
+ end
+ end
+
+ if current_resource[:name]
+ resources.push(current_resource)
+ end
+
+ build_resource_info(resources)
+ end
+
+ def self.parse_line(line)
+ if match = line.match(/^.*?:.*?:\s*LCM:\s*\[(.*?)\](.*)/)
+ # If the line looks like
+ # What If: [machinename]: LCM: [op_action op_type] message
+ # extract op_action, op_type, and message
+ operation, info = match.captures
+ op_action, op_type = operation.strip.split(' ').map {|m| m.downcase.to_sym}
+ else
+ op_action = op_type = :info
+ if match = line.match(/^.*?:.*?: \s+(.*)/)
+ info = match.captures[0]
+ else
+ info = line
+ end
+ end
+ info.strip! # Because this was formatted for humans
+ return [op_action, op_type, info]
+ end
+ private_class_method :parse_line
+
+ def self.build_resource_info(resources)
+ resources.map do |r|
+ Chef::Util::DSC::ResourceInfo.new(r[:name], !r[:skipped], r[:logs])
+ end
+ end
+ private_class_method :build_resource_info
+
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/util/dsc/local_configuration_manager.rb b/lib/chef/util/dsc/local_configuration_manager.rb
new file mode 100644
index 0000000000..4a56b6a397
--- /dev/null
+++ b/lib/chef/util/dsc/local_configuration_manager.rb
@@ -0,0 +1,137 @@
+#
+# Author:: Adam Edwards (<adamed@getchef.com>)
+#
+# Copyright:: 2014, Chef Software, Inc.
+#
+# 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/util/powershell/cmdlet'
+require 'chef/util/dsc/lcm_output_parser'
+
+class Chef::Util::DSC
+ class LocalConfigurationManager
+ def initialize(node, configuration_path)
+ @node = node
+ @configuration_path = configuration_path
+ clear_execution_time
+ end
+
+ def test_configuration(configuration_document)
+ status = run_configuration_cmdlet(configuration_document)
+ handle_what_if_exception!(status.stderr) unless status.succeeded?
+ configuration_update_required?(status.return_value)
+ end
+
+ def set_configuration(configuration_document)
+ run_configuration_cmdlet(configuration_document, true)
+ end
+
+ def last_operation_execution_time_seconds
+ if @operation_start_time && @operation_end_time
+ @operation_end_time - @operation_start_time
+ end
+ end
+
+ private
+
+ def run_configuration_cmdlet(configuration_document, apply_configuration = false)
+ Chef::Log.debug("DSC: Calling DSC Local Config Manager to #{apply_configuration ? "set" : "test"} configuration document.")
+ test_only_parameters = ! apply_configuration ? '-whatif; if (! $?) { exit 1 }' : ''
+
+ start_operation_timing
+ command_code = lcm_command_code(@configuration_path, test_only_parameters)
+ status = nil
+
+ begin
+ save_configuration_document(configuration_document)
+ cmdlet = ::Chef::Util::Powershell::Cmdlet.new(@node, "#{command_code}")
+ if apply_configuration
+ status = cmdlet.run!
+ else
+ status = cmdlet.run
+ end
+ ensure
+ end_operation_timing
+ remove_configuration_document
+ if last_operation_execution_time_seconds
+ Chef::Log.debug("DSC: DSC operation completed in #{last_operation_execution_time_seconds} seconds.")
+ end
+ end
+ Chef::Log.debug("DSC: Completed call to DSC Local Config Manager")
+ status
+ end
+
+ def lcm_command_code(configuration_path, test_only_parameters)
+ <<-EOH
+$ProgressPreference = 'SilentlyContinue';start-dscconfiguration -path #{@configuration_path} -wait -erroraction 'continue' -force #{test_only_parameters}
+EOH
+ end
+
+ def handle_what_if_exception!(what_if_exception_output)
+ if what_if_exception_output.gsub(/\s+/, ' ') =~ /A parameter cannot be found that matches parameter name 'Whatif'/i
+ # LCM returns an error if any of the resources do not support the opptional What-If
+ Chef::Log::warn("Received error while testing configuration due to resource not supporting 'WhatIf'")
+ elsif output_has_dsc_module_failure?(what_if_exception_output)
+ Chef::Log::warn("Received error while testing configuration due to a module for an imported resource possibly not being fully installed:\n#{what_if_exception_output.gsub(/\s+/, ' ')}")
+ else
+ raise Chef::Exceptions::PowershellCmdletException, "Powershell Cmdlet failed: #{what_if_exception_output.gsub(/\s+/, ' ')}"
+ end
+ end
+
+ def output_has_dsc_module_failure?(what_if_output)
+ !! (what_if_output =~ /\sCimException/ &&
+ what_if_output =~ /ProviderOperationExecutionFailure/ &&
+ what_if_output =~ /\smodule\s+is\s+installed/)
+ end
+
+ def configuration_update_required?(what_if_output)
+ Chef::Log.debug("DSC: DSC returned the following '-whatif' output from test operation:\n#{what_if_output}")
+ begin
+ Parser::parse(what_if_output)
+ rescue Chef::Util::DSC::LocalConfigurationManager::Parser => e
+ Chef::Log::warn("Could not parse LCM output: #{e}")
+ [Chef::Util::DSC::ResourceInfo.new('Unknown DSC Resources', true, ['Unknown changes because LCM output was not parsable.'])]
+ end
+ end
+
+ def save_configuration_document(configuration_document)
+ ::FileUtils.mkdir_p(@configuration_path)
+ ::File.open(configuration_document_path, 'wb') do | file |
+ file.write(configuration_document)
+ end
+ end
+
+ def remove_configuration_document
+ ::FileUtils.rm(configuration_document_path)
+ end
+
+ def configuration_document_path
+ File.join(@configuration_path,'..mof')
+ end
+
+ def clear_execution_time
+ @operation_start_time = nil
+ @operation_end_time = nil
+ end
+
+ def start_operation_timing
+ clear_execution_time
+ @operation_start_time = Time.now
+ end
+
+ def end_operation_timing
+ @operation_end_time = Time.now
+ end
+ end
+end
diff --git a/lib/chef/util/dsc/resource_info.rb b/lib/chef/util/dsc/resource_info.rb
new file mode 100644
index 0000000000..4a32451721
--- /dev/null
+++ b/lib/chef/util/dsc/resource_info.rb
@@ -0,0 +1,26 @@
+
+class Chef
+ class Util
+ class DSC
+ class ResourceInfo
+ # The name is the text following [Start Set]
+ attr_reader :name
+
+ # A list of all log messages between [Start Set] and [End Set].
+ # Each line is an element in the list.
+ attr_reader :change_log
+
+ def initialize(name, sets, change_log)
+ @name = name
+ @sets = sets
+ @change_log = change_log || []
+ end
+
+ # Does this resource change the state of the system?
+ def changes_state?
+ @sets
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/util/powershell/cmdlet.rb b/lib/chef/util/powershell/cmdlet.rb
new file mode 100644
index 0000000000..40edbb13c6
--- /dev/null
+++ b/lib/chef/util/powershell/cmdlet.rb
@@ -0,0 +1,136 @@
+#
+# Author:: Adam Edwards (<adamed@getchef.com>)
+#
+# Copyright:: 2014, Chef Software, Inc.
+#
+# 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 'mixlib/shellout'
+require 'chef/mixin/windows_architecture_helper'
+require 'chef/util/powershell/cmdlet_result'
+
+class Chef::Util::Powershell
+ class Cmdlet
+ def initialize(node, cmdlet, output_format=nil, output_format_options={})
+ @output_format = output_format
+ @node = node
+
+ case output_format
+ when nil
+ @json_format = false
+ when :json
+ @json_format = true
+ when :text
+ @json_format = false
+ when :object
+ @json_format = true
+ else
+ raise ArgumentError, "Invalid output format #{output_format.to_s} specified"
+ end
+
+ @cmdlet = cmdlet
+ @output_format_options = output_format_options
+ end
+
+ attr_reader :output_format
+
+ def run(switches={}, execution_options={}, *arguments)
+ arguments_string = arguments.join(' ')
+
+ switches_string = command_switches_string(switches)
+
+ json_depth = 5
+
+ if @json_format && @output_format_options.has_key?(:depth)
+ json_depth = @output_format_options[:depth]
+ end
+
+ json_command = @json_format ? " | convertto-json -compress -depth #{json_depth}" : ""
+ command_string = "powershell.exe -executionpolicy bypass -noprofile -noninteractive -command \"trap [Exception] {write-error -exception ($_.Exception.Message);exit 1};#{@cmdlet} #{switches_string} #{arguments_string}#{json_command}\";if ( ! $? ) { exit 1 }"
+
+ augmented_options = {:returns => [0], :live_stream => false}.merge(execution_options)
+ command = Mixlib::ShellOut.new(command_string, augmented_options)
+
+ os_architecture = "#{ENV['PROCESSOR_ARCHITEW6432']}" == 'AMD64' ? :x86_64 : :i386
+
+ status = nil
+
+ with_os_architecture(@node) do
+ status = command.run_command
+ end
+
+ CmdletResult.new(status, @output_format)
+ end
+
+ def run!(switches={}, execution_options={}, *arguments)
+ result = run(switches, execution_options, arguments)
+
+ if ! result.succeeded?
+ raise Chef::Exceptions::PowershellCmdletException, "Powershell Cmdlet failed: #{result.stderr}"
+ end
+
+ result
+ end
+
+ protected
+
+ include Chef::Mixin::WindowsArchitectureHelper
+
+ def validate_switch_name!(switch_parameter_name)
+ if !!(switch_parameter_name =~ /\A[A-Za-z]+[_a-zA-Z0-9]*\Z/) == false
+ raise ArgumentError, "`#{switch_parameter_name}` is not a valid PowerShell cmdlet switch parameter name"
+ end
+ end
+
+ def escape_parameter_value(parameter_value)
+ parameter_value.gsub(/(`|'|"|#)/,'`\1')
+ end
+
+ def escape_string_parameter_value(parameter_value)
+ "'#{escape_parameter_value(parameter_value)}'"
+ end
+
+ def command_switches_string(switches)
+ command_switches = switches.map do | switch_name, switch_value |
+ if switch_name.class != Symbol
+ raise ArgumentError, "Invalid type `#{switch_name} `for PowerShell switch '#{switch_name.to_s}'. The switch must be specified as a Symbol'"
+ end
+
+ validate_switch_name!(switch_name)
+
+ switch_argument = ''
+ switch_present = true
+
+ case switch_value
+ when Numeric
+ switch_argument = switch_value.to_s
+ when Float
+ switch_argument = switch_value.to_s
+ when FalseClass
+ switch_present = false
+ when TrueClass
+ when String
+ switch_argument = escape_string_parameter_value(switch_value)
+ else
+ raise ArgumentError, "Invalid argument type `#{switch_value.class}` specified for PowerShell switch `:#{switch_name.to_s}`. Arguments to PowerShell must be of type `String`, `Numeric`, `Float`, `FalseClass`, or `TrueClass`"
+ end
+
+ switch_present ? ["-#{switch_name.to_s.downcase}", switch_argument].join(' ').strip : ''
+ end
+
+ command_switches.join(' ')
+ end
+ end
+end
+
diff --git a/lib/chef/util/powershell/cmdlet_result.rb b/lib/chef/util/powershell/cmdlet_result.rb
new file mode 100644
index 0000000000..af7b3607cd
--- /dev/null
+++ b/lib/chef/util/powershell/cmdlet_result.rb
@@ -0,0 +1,46 @@
+#
+# Author:: Adam Edwards (<adamed@getchef.com>)
+#
+# Copyright:: Copyright (c) 2014 Chef Software, Inc.
+#
+# 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 'json'
+
+class Chef::Util::Powershell
+ class CmdletResult
+ attr_reader :output_format
+
+ def initialize(status, output_format)
+ @status = status
+ @output_format = output_format
+ end
+
+ def stderr
+ @status.stderr
+ end
+
+ def return_value
+ if output_format == :object
+ JSON.parse(@status.stdout)
+ else
+ @status.stdout
+ end
+ end
+
+ def succeeded?
+ @succeeded = @status.status.exitstatus == 0
+ end
+ end
+end