diff options
author | James Bence <jbence@mdsol.com> | 2014-09-24 11:36:03 -0700 |
---|---|---|
committer | James Bence <jbence@mdsol.com> | 2014-09-24 11:36:03 -0700 |
commit | d1e51961cb649a37d1366fe408b8242c5f5ebdd2 (patch) | |
tree | fb12dddeca4a278f9652bfb68c61d26c015b8556 /lib | |
parent | 6d4bca71ff197319988f716f67b426fb73662c59 (diff) | |
parent | bed2af024937259b93bb6955ee794cf70e20693f (diff) | |
download | chef-d1e51961cb649a37d1366fe408b8242c5f5ebdd2.tar.gz |
Merge branch 'master' of https://github.com/opscode/chef
Diffstat (limited to 'lib')
-rw-r--r-- | lib/chef/exceptions.rb | 6 | ||||
-rw-r--r-- | lib/chef/knife/ssl_check.rb | 76 | ||||
-rw-r--r-- | lib/chef/mixin/homebrew_owner.rb | 58 | ||||
-rw-r--r-- | lib/chef/mixin/windows_architecture_helper.rb | 16 | ||||
-rw-r--r-- | lib/chef/platform/provider_mapping.rb | 4 | ||||
-rw-r--r-- | lib/chef/platform/query_helpers.rb | 6 | ||||
-rw-r--r-- | lib/chef/provider/dsc_script.rb | 148 | ||||
-rw-r--r-- | lib/chef/provider/package/homebrew.rb | 121 | ||||
-rw-r--r-- | lib/chef/providers.rb | 2 | ||||
-rw-r--r-- | lib/chef/resource/dsc_script.rb | 140 | ||||
-rw-r--r-- | lib/chef/resource/homebrew_package.rb | 34 | ||||
-rw-r--r-- | lib/chef/resources.rb | 2 | ||||
-rw-r--r-- | lib/chef/util/dsc/configuration_generator.rb | 115 | ||||
-rw-r--r-- | lib/chef/util/dsc/lcm_output_parser.rb | 133 | ||||
-rw-r--r-- | lib/chef/util/dsc/local_configuration_manager.rb | 137 | ||||
-rw-r--r-- | lib/chef/util/dsc/resource_info.rb | 26 | ||||
-rw-r--r-- | lib/chef/util/powershell/cmdlet.rb | 136 | ||||
-rw-r--r-- | lib/chef/util/powershell/cmdlet_result.rb | 46 |
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 |