diff options
author | tyler-ball <tyleraball@gmail.com> | 2014-12-29 15:00:52 -0800 |
---|---|---|
committer | tyler-ball <tyleraball@gmail.com> | 2014-12-29 15:00:52 -0800 |
commit | 43eba1778fab8bc440f97824af7227476457a65c (patch) | |
tree | 5e967abff042f8a7279c3ad049b54cf7a009f6cb | |
parent | 4a91e5e40972f0be7e71aba4615b1f76affeafc6 (diff) | |
parent | b7b7dad4e476b3fde67f0d9881e15efe7e5b60ac (diff) | |
download | chef-43eba1778fab8bc440f97824af7227476457a65c.tar.gz |
Merging master to audit-mode
-rw-r--r-- | CHANGELOG.md | 1 | ||||
-rw-r--r-- | DOC_CHANGES.md | 6 | ||||
-rw-r--r-- | RELEASE_NOTES.md | 4 | ||||
-rw-r--r-- | lib/chef/application/windows_service_manager.rb | 10 | ||||
-rw-r--r-- | lib/chef/http.rb | 2 | ||||
-rw-r--r-- | lib/chef/knife/search.rb | 8 | ||||
-rw-r--r-- | lib/chef/provider/service.rb | 2 | ||||
-rw-r--r-- | lib/chef/provider/service/windows.rb | 100 | ||||
-rw-r--r-- | lib/chef/resource/windows_service.rb | 18 | ||||
-rw-r--r-- | lib/chef/search/query.rb | 153 | ||||
-rw-r--r-- | spec/functional/knife/ssh_spec.rb | 2 | ||||
-rw-r--r-- | spec/functional/resource/windows_service_spec.rb | 98 | ||||
-rw-r--r-- | spec/functional/win32/service_manager_spec.rb | 60 | ||||
-rw-r--r-- | spec/support/chef_helpers.rb | 6 | ||||
-rw-r--r-- | spec/support/shared/functional/win32_service.rb | 60 | ||||
-rw-r--r-- | spec/unit/http_spec.rb | 7 | ||||
-rw-r--r-- | spec/unit/provider/service/windows_spec.rb | 94 | ||||
-rw-r--r-- | spec/unit/search/query_spec.rb | 90 |
18 files changed, 512 insertions, 209 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ff00ab99e..0ea4b68264 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ Typo fixes * [**Tim Smith**](https://github.com/tas50) Typo fixes +* [Pull 2505](https://github.com/opscode/chef/pull/2505) Make Chef handle URIs in a case-insensitive manner ### Chef Contributions * ruby 1.9.3 support is dropped diff --git a/DOC_CHANGES.md b/DOC_CHANGES.md index 9a6c78a524..0c82661f34 100644 --- a/DOC_CHANGES.md +++ b/DOC_CHANGES.md @@ -6,6 +6,10 @@ Example Doc Change: Description of the required change. --> +### Chef now handles URI Schemes in a case insensitive manner + +Previously, when a URI scheme contained all uppercase letters, Chef would reject the URI as invalid. In compliance with RFC3986, Chef now treats URI schemes in a case insensitive manner. This applies to all resources which accept URIs such as remote_file etc. + ### Experimental Audit Mode Feature There is a new command_line flag provided for `chef-client`: `--audit-mode`. This accepts 1 of 3 arguments: @@ -28,4 +32,4 @@ The `--audit-mode` flag should be a link to the documentation for that flag #### Editors node 2 This probably only needs to be a bullet point added to http://docs.getchef.com/nodes.html#about-why-run-mode under the -`certain assumptions` section +`certain assumptions` section
\ No newline at end of file diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 0c73b7f7c8..43c8f06d93 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -60,6 +60,10 @@ More information about the audit mode can be found in its The package resource on OpenBSD is wired up to use the new OpenBSD package provider to install via pkg_add on OpenBSD systems. +## Case Insensitive URI Handling + +Previously, when a URI scheme contained all uppercase letters, Chef would reject the URI as invalid. In compliance with RFC3986, Chef now treats URI schemes in a case insensitive manner. + # Chef Client Release Notes 12.0.0: # Internal API Changes in this Release diff --git a/lib/chef/application/windows_service_manager.rb b/lib/chef/application/windows_service_manager.rb index 30810c51f2..de8ed657c2 100644 --- a/lib/chef/application/windows_service_manager.rb +++ b/lib/chef/application/windows_service_manager.rb @@ -16,7 +16,9 @@ # limitations under the License. # -require 'win32/service' +if RUBY_PLATFORM =~ /mswin|mingw32|windows/ + require 'win32/service' +end require 'chef/config' require 'mixlib/cli' @@ -88,6 +90,8 @@ class Chef @service_display_name = service_options[:service_display_name] @service_description = service_options[:service_description] @service_file_path = service_options[:service_file_path] + @service_start_name = service_options[:run_as_user] + @password = service_options[:run_as_password] end def run(params = ARGV) @@ -116,7 +120,9 @@ class Chef # and we don't want that, so we need to override the service type. :service_type => ::Win32::Service::SERVICE_WIN32_OWN_PROCESS, :start_type => ::Win32::Service::SERVICE_AUTO_START, - :binary_path_name => cmd + :binary_path_name => cmd, + :service_start_name => @service_start_name, + :password => @password, ) puts "Service '#{@service_name}' has successfully been installed." end diff --git a/lib/chef/http.rb b/lib/chef/http.rb index 8d00a38dc1..5e52337aff 100644 --- a/lib/chef/http.rb +++ b/lib/chef/http.rb @@ -203,7 +203,7 @@ class Chef def create_url(path) return path if path.is_a?(URI) - if path =~ /^(http|https):\/\// + if path =~ /^(http|https):\/\//i URI.parse(path) elsif path.nil? or path.empty? URI.parse(@url) diff --git a/lib/chef/knife/search.rb b/lib/chef/knife/search.rb index 34d12168b6..caca99b4d8 100644 --- a/lib/chef/knife/search.rb +++ b/lib/chef/knife/search.rb @@ -53,7 +53,7 @@ class Chef :short => "-R INT", :long => "--rows INT", :description => "The number of rows to return", - :default => 1000, + :default => nil, :proc => lambda { |i| i.to_i } option :run_list, @@ -92,9 +92,9 @@ class Chef result_count = 0 search_args = Hash.new - search_args[:sort] = config[:sort] - search_args[:start] = config[:start] - search_args[:rows] = config[:rows] + search_args[:sort] = config[:sort] if config[:sort] + search_args[:start] = config[:start] if config[:start] + search_args[:rows] = config[:rows] if config[:rows] if config[:filter_result] search_args[:filter_result] = create_result_filter(config[:filter_result]) elsif (not ui.config[:attribute].nil?) && (not ui.config[:attribute].empty?) diff --git a/lib/chef/provider/service.rb b/lib/chef/provider/service.rb index 968f9bff9c..75da2ddb31 100644 --- a/lib/chef/provider/service.rb +++ b/lib/chef/provider/service.rb @@ -150,7 +150,7 @@ class Chef end def reload_service - raise Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :restart" + raise Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :reload" end protected diff --git a/lib/chef/provider/service/windows.rb b/lib/chef/provider/service/windows.rb index d4c272354e..ba53f0a3c3 100644 --- a/lib/chef/provider/service/windows.rb +++ b/lib/chef/provider/service/windows.rb @@ -20,6 +20,7 @@ require 'chef/provider/service/simple' if RUBY_PLATFORM =~ /mswin|mingw32|windows/ + require 'chef/win32/error' require 'win32/service' end @@ -29,6 +30,7 @@ class Chef::Provider::Service::Windows < Chef::Provider::Service provides :windows_service, os: "windows" include Chef::Mixin::ShellOut + include Chef::ReservedNames::Win32::API::Error rescue LoadError #Win32::Service.get_start_type AUTO_START = 'auto start' @@ -67,6 +69,22 @@ class Chef::Provider::Service::Windows < Chef::Provider::Service def start_service if Win32::Service.exists?(@new_resource.service_name) + # reconfiguration is idempotent, so just do it. + new_config = { + service_name: @new_resource.service_name, + service_start_name: @new_resource.run_as_user, + password: @new_resource.run_as_password, + }.reject { |k,v| v.nil? || v.length == 0 } + + Win32::Service.configure(new_config) + Chef::Log.info "#{@new_resource} configured with #{new_config.inspect}" + + # it would be nice to check if the user already has the logon privilege, but that turns out to be + # nontrivial. + if new_config.has_key?(:service_start_name) + grant_service_logon(new_config[:service_start_name]) + end + state = current_state if state == RUNNING Chef::Log.debug "#{@new_resource} already started - nothing to do" @@ -79,7 +97,17 @@ class Chef::Provider::Service::Windows < Chef::Provider::Service shell_out!(@new_resource.start_command) else spawn_command_thread do - Win32::Service.start(@new_resource.service_name) + begin + Win32::Service.start(@new_resource.service_name) + rescue SystemCallError => ex + if ex.errno == ERROR_SERVICE_LOGON_FAILED + Chef::Log.error ex.message + raise Chef::Exceptions::Service, + "Service #{@new_resource} did not start due to a logon failure (error #{ERROR_SERVICE_LOGON_FAILED}): possibly the specified user '#{@new_resource.run_as_user}' does not have the 'log on as a service' privilege, or the password is incorrect." + else + raise ex + end + end end wait_for_state(RUNNING) end @@ -209,6 +237,76 @@ class Chef::Provider::Service::Windows < Chef::Provider::Service end private + def make_policy_text(username) + text = <<-EOS +[Unicode] +Unicode=yes +[Privilege Rights] +SeServiceLogonRight = \\\\#{canonicalize_username(username)},*S-1-5-80-0 +[Version] +signature="$CHICAGO$" +Revision=1 +EOS + end + + def grant_logfile_name(username) + Chef::Util::PathHelper.canonical_path("#{Dir.tmpdir}/logon_grant-#{clean_username_for_path(username)}-#{$$}.log", prefix=false) + end + + def grant_policyfile_name(username) + Chef::Util::PathHelper.canonical_path("#{Dir.tmpdir}/service_logon_policy-#{clean_username_for_path(username)}-#{$$}.inf", prefix=false) + end + + def grant_dbfile_name(username) + "#{ENV['TEMP']}\\secedit.sdb" + end + + def grant_service_logon(username) + logfile = grant_logfile_name(username) + policy_file = ::File.new(grant_policyfile_name(username), 'w') + policy_text = make_policy_text(username) + dbfile = grant_dbfile_name(username) # this is just an audit file. + + begin + Chef::Log.debug "Policy file text:\n#{policy_text}" + policy_file.puts(policy_text) + policy_file.close # need to flush the buffer. + + # it would be nice to do this with APIs instead, but the LSA_* APIs are + # particularly onerous and life is short. + cmd = %Q{secedit.exe /configure /db "#{dbfile}" /cfg "#{policy_file.path}" /areas USER_RIGHTS SECURITYPOLICY SERVICES /log "#{logfile}"} + Chef::Log.debug "Granting logon-as-service privilege with: #{cmd}" + runner = shell_out(cmd) + + if runner.exitstatus != 0 + Chef::Log.fatal "Logon-as-service grant failed with output: #{runner.stdout}" + raise Chef::Exceptions::Service, <<-EOS +Logon-as-service grant failed with policy file #{policy_file.path}. +You can look at #{logfile} for details, or do `secedit /analyze #{dbfile}`. +The failed command was `#{cmd}`. +EOS + end + + Chef::Log.info "Grant logon-as-service to user '#{username}' successful." + + ::File.delete(dbfile) rescue nil + ::File.delete(policy_file) + ::File.delete(logfile) rescue nil # logfile is not always present at end. + end + true + end + + # remove characters that make for broken or wonky filenames. + def clean_username_for_path(username) + username.gsub(/[\/\\. ]+/, '_') + end + + # the security policy file only seems to accept \\username, so fix .\username or .\\username. + # TODO: this probably has to be fixed to handle various valid Windows names correctly. + def canonicalize_username(username) + username.sub(/^\.?\\+/, '') + end + def current_state Win32::Service.status(@new_resource.service_name).current_state end diff --git a/lib/chef/resource/windows_service.rb b/lib/chef/resource/windows_service.rb index 2aec4d6304..8090adceb0 100644 --- a/lib/chef/resource/windows_service.rb +++ b/lib/chef/resource/windows_service.rb @@ -37,6 +37,8 @@ class Chef @resource_name = :windows_service @allowed_actions.push(:configure_startup) @startup_type = :automatic + @run_as_user = "" + @run_as_password = "" end def startup_type(arg=nil) @@ -48,6 +50,22 @@ class Chef :equal_to => [ :automatic, :manual, :disabled ] ) end + + def run_as_user(arg=nil) + set_or_return( + :run_as_user, + arg, + :kind_of => [ String ] + ) + end + + def run_as_password(arg=nil) + set_or_return( + :run_as_password, + arg, + :kind_of => [ String ] + ) + end end end end diff --git a/lib/chef/search/query.rb b/lib/chef/search/query.rb index cc43efe1b1..8656e810db 100644 --- a/lib/chef/search/query.rb +++ b/lib/chef/search/query.rb @@ -17,52 +17,47 @@ # require 'chef/config' -require 'uri' -require 'chef/rest' -require 'chef/node' -require 'chef/role' -require 'chef/data_bag' -require 'chef/data_bag_item' require 'chef/exceptions' +require 'chef/rest' + +require 'uri' class Chef class Search class Query attr_accessor :rest + attr_reader :config - def initialize(url=nil) - @rest = Chef::REST.new(url ||Chef::Config[:chef_server_url]) + def initialize(url=nil, config:Chef::Config) + @config = config + @url = url end + def rest + @rest ||= Chef::REST.new(@url || @config[:chef_server_url]) + end - # This search is only kept for backwards compatibility, since the results of the - # new filtered search method will be in a slightly different format + # Backwards compatability for cookbooks. + # This can be removed in Chef > 12. def partial_search(type, query='*:*', *args, &block) - Chef::Log.warn("DEPRECATED: The 'partial_search' api is deprecated, please use the search api with 'filter_result'") - # accept both types of args - if args.length == 1 && args[0].is_a?(Hash) - args_hash = args[0].dup - # partial_search implemented in the partial search cookbook uses the - # arg hash :keys instead of :filter_result to filter returned data - args_hash[:filter_result] = args_hash[:keys] + Chef::Log.warn(<<-WARNDEP) +DEPRECATED: The 'partial_search' API is deprecated and will be removed in +future releases. Please use 'search' with a :filter_result argument to get +partial search data. +WARNDEP + + if !args.empty? && args.first.is_a?(Hash) + # partial_search uses :keys instead of :filter_result for + # result filtering. + args_h = args.first.dup + args_h[:filter_result] = args_h[:keys] + args_h.delete(:keys) + + search(type, query, args_h, &block) else - args_hash = {} - args_hash[:sort] = args[0] if args.length >= 1 - args_hash[:start] = args[1] if args.length >= 2 - args_hash[:rows] = args[2] if args.length >= 3 + search(type, query, *args, &block) end - - unless block.nil? - raw_results = search(type,query,args_hash) - else - raw_results = search(type,query,args_hash,&block) - end - results = Array.new - raw_results[0].each do |r| - results << r["data"] - end - return results end # @@ -87,87 +82,71 @@ class Chef # def search(type, query='*:*', *args, &block) validate_type(type) - validate_args(args) - scrubbed_args = Hash.new + args_h = hashify_args(*args) + response = call_rest_service(type, query: query, **args_h) - # argify everything - if args[0].kind_of?(Hash) - scrubbed_args = args[0] + if block + response["rows"].each { |row| block.call(row) if row } + unless (response["start"] + response["rows"].length) >= response["total"] + args_h[:start] = response["start"] + (args_h[:rows] || 0) + search(type, query, args_h, &block) + end + true else - # This api will be deprecated in a future release - scrubbed_args = { :sort => args[0], :start => args[1], :rows => args[2] } + [ response["rows"], response["start"], response["total"] ] end - - # set defaults, if they haven't been set yet. - scrubbed_args[:sort] ||= 'X_CHEF_id_CHEF_X asc' - scrubbed_args[:start] ||= 0 - scrubbed_args[:rows] ||= 1000 - - do_search(type, query, scrubbed_args, &block) - end - - def list_indexes - @rest.get_rest("search") end private def validate_type(t) unless t.kind_of?(String) || t.kind_of?(Symbol) msg = "Invalid search object type #{t.inspect} (#{t.class}), must be a String or Symbol." + - "Useage: search(:node, QUERY, [OPTIONAL_ARGS])" + - " `knife search environment QUERY (options)`" + "Usage: search(:node, QUERY[, OPTIONAL_ARGS])" + + " `knife search environment QUERY (options)`" raise Chef::Exceptions::InvalidSearchQuery, msg end end - def validate_args(a) - max_args = 3 - raise Chef::Exceptions::InvalidSearchQuery, "Too many arguments! (#{a.size} for <= #{max_args})" if a.size > max_args + def hashify_args(*args) + return Hash.new if args.empty? + return args.first if args.first.is_a?(Hash) + + args_h = Hash.new + args_h[:sort] = args[0] if args[0] + args_h[:start] = args[1] if args[1] + args_h[:rows] = args[2] + args_h[:filter_result] = args[3] + args_h end def escape(s) s && URI.escape(s.to_s) end - # new search api that allows for a cleaner implementation of things like return filters - # (formerly known as 'partial search'). - # Also args should never be nil, but that is required for Ruby 1.8 compatibility - def do_search(type, query="*:*", args=nil, &block) - query_string = create_query_string(type, query, args) - response = call_rest_service(query_string, args) - unless block.nil? - response["rows"].each { |rowset| block.call(rowset) unless rowset.nil?} - unless (response["start"] + response["rows"].length) >= response["total"] - args[:start] = response["start"] + args[:rows] - do_search(type, query, args, &block) - end - true - else - [ response["rows"], response["start"], response["total"] ] - end + def create_query_string(type, query, rows, start, sort) + qstr = "search/#{type}?q=#{escape(query)}" + qstr += "&sort=#{escape(sort)}" if sort + qstr += "&start=#{escape(start)}" if start + qstr += "&rows=#{escape(rows)}" if rows + qstr end - # create the full rest url string - def create_query_string(type, query, args) - # create some default variables just so we don't break backwards compatibility - sort = args[:sort] - start = args[:start] - rows = args[:rows] + def call_rest_service(type, query:'*:*', rows:nil, start:0, sort:'X_CHEF_id_CHEF_X asc', filter_result:nil) + query_string = create_query_string(type, query, rows, start, sort) - return "search/#{type}?q=#{escape(query)}&sort=#{escape(sort)}&start=#{escape(start)}&rows=#{escape(rows)}" - end - - def call_rest_service(query_string, args) - if args.key?(:filter_result) - response = @rest.post_rest(query_string, args[:filter_result]) - response_rows = response['rows'].map { |row| row['data'] } + if filter_result + response = rest.post_rest(query_string, filter_result) + # response returns rows in the format of + # { "url" => url_to_node, "data" => filter_result_hash } + response['rows'].map! { |row| row['data'] } else - response = @rest.get_rest(query_string) - response_rows = response['rows'] + response = rest.get_rest(query_string) end - return response + + response end + end end end diff --git a/spec/functional/knife/ssh_spec.rb b/spec/functional/knife/ssh_spec.rb index cde702e8b2..5b8ad6f368 100644 --- a/spec/functional/knife/ssh_spec.rb +++ b/spec/functional/knife/ssh_spec.rb @@ -260,7 +260,7 @@ describe Chef::Knife::Ssh do Chef::Config[:client_key] = nil Chef::Config[:chef_server_url] = 'http://localhost:9000' - @api.get("/search/node?q=*:*&sort=X_CHEF_id_CHEF_X%20asc&start=0&rows=1000", 200) { + @api.get("/search/node?q=*:*&sort=X_CHEF_id_CHEF_X%20asc&start=0", 200) { %({"total":1, "start":0, "rows":[{"name":"i-xxxxxxxx", "json_class":"Chef::Node", "automatic":{"fqdn":"the.fqdn", "ec2":{"public_hostname":"the_public_hostname"}},"recipes":[]}]}) } end diff --git a/spec/functional/resource/windows_service_spec.rb b/spec/functional/resource/windows_service_spec.rb new file mode 100644 index 0000000000..29d1fc42c3 --- /dev/null +++ b/spec/functional/resource/windows_service_spec.rb @@ -0,0 +1,98 @@ +# +# Author:: Chris Doherty (<cdoherty@chef.io>) +# 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 'spec_helper' + +describe Chef::Resource::WindowsService, :windows_only, :system_windows_service_gem_only do + + include_context "using Win32::Service" + + let(:username) { "service_spec_user"} + let(:qualified_username) { ".\\#{username}"} + let(:password) { "1a2b3c4X!&narf"} + + let(:user_resource) { + r = Chef::Resource::User.new(username, run_context) + r.username(username) + r.password(password) + r.comment("temp spec user") + r + } + + let(:global_service_file_path) { + "#{ENV['WINDIR']}\\temp\\#{File.basename(test_service[:service_file_path])}" + } + + let(:service_params) { + + id = "#{$$}_#{rand(1000)}" + + test_service.merge( { + run_as_user: qualified_username, + run_as_password: password, + service_name: "spec_service_#{id}", + service_display_name: "windows_service spec #{id}}", + service_description: "Test service for running the windows_service functional spec.", + service_file_path: global_service_file_path, + } ) + } + + let(:manager) { + Chef::Application::WindowsServiceManager.new(service_params) + } + + let(:service_resource) { + r = Chef::Resource::WindowsService.new(service_params[:service_name], run_context) + [:run_as_user, :run_as_password].each { |prop| r.send(prop, service_params[prop]) } + r + } + + before { + user_resource.run_action(:create) + + # the service executable has to be outside the current user's home + # directory in order for the logon user to execute it. + FileUtils::copy_file(test_service[:service_file_path], global_service_file_path) + + # if you don't make the file executable by the service user, you'll get + # the not-very-helpful "service did not respond fast enough" error. + + # #mode may break in a post-Windows 8.1 release, and have to be replaced + # with the rights stuff in the file resource. + file = Chef::Resource::File.new(global_service_file_path, run_context) + file.mode("0777") + + file.run_action(:create) + + manager.run(%w{--action install}) + } + + after { + user_resource.run_action(:remove) + manager.run(%w{--action uninstall}) + File.delete(global_service_file_path) + } + + describe "logon as a service" do + it "successfully runs a service as another user" do + service_resource.run_action(:start) + end + + it "raises an exception when it can't grant the logon privilege" + end +end diff --git a/spec/functional/win32/service_manager_spec.rb b/spec/functional/win32/service_manager_spec.rb index fd21e7d82e..d2474deace 100644 --- a/spec/functional/win32/service_manager_spec.rb +++ b/spec/functional/win32/service_manager_spec.rb @@ -24,7 +24,7 @@ end # # ATTENTION: # This test creates a windows service for testing purposes and runs it -# as Local System on windows boxes. +# as Local System (or an otherwise specified user) on windows boxes. # This test will fail if you run the tests inside a Windows VM by # sharing the code from your host since Local System account by # default can't see the mounted partitions. @@ -35,61 +35,7 @@ end describe "Chef::Application::WindowsServiceManager", :windows_only, :system_windows_service_gem_only do - # Some helper methods. - - def test_service_exists? - ::Win32::Service.exists?("spec-service") - end - - def test_service_state - ::Win32::Service.status("spec-service").current_state - end - - def service_manager - Chef::Application::WindowsServiceManager.new(test_service) - end - - def cleanup - # Uninstall if the test service is installed. - if test_service_exists? - - # We can only uninstall when the service is stopped. - if test_service_state != "stopped" - ::Win32::Service.send("stop", "spec-service") - while test_service_state != "stopped" - sleep 1 - end - end - - ::Win32::Service.delete("spec-service") - end - - # Delete the test_service_file if it exists - if File.exists?(test_service_file) - File.delete(test_service_file) - end - - end - - - # Definition for the test-service - - let(:test_service) { - { - :service_name => "spec-service", - :service_display_name => "Spec Test Service", - :service_description => "Service for testing Chef::Application::WindowsServiceManager.", - :service_file_path => File.expand_path(File.join(File.dirname(__FILE__), '../../support/platforms/win32/spec_service.rb')) - } - } - - # Test service creates a file for us to verify that it is running. - # Since our test service is running as Local System we should look - # for the file it creates under SYSTEM temp directory - - let(:test_service_file) { - "#{ENV['SystemDrive']}\\windows\\temp\\spec_service_file" - } + include_context "using Win32::Service" context "with invalid service definition" do it "throws an error when initialized with no service definition" do @@ -190,7 +136,7 @@ describe "Chef::Application::WindowsServiceManager", :windows_only, :system_wind ["pause", "resume"].each do |action| it "#{action} => should raise error" do - expect {service_manager.run(["-a", action])}.to raise_error(::Win32::Service::Error) + expect { service_manager.run(["-a", action]) }.to raise_error(SystemCallError) end end diff --git a/spec/support/chef_helpers.rb b/spec/support/chef_helpers.rb index 237543748c..851b1dce0a 100644 --- a/spec/support/chef_helpers.rb +++ b/spec/support/chef_helpers.rb @@ -67,15 +67,15 @@ end # win32/service gem. windows_service_manager tests create a windows # service that starts with the system ruby and requires this gem. def system_windows_service_gem? - windows_service_gem_check_command = "ruby -e 'require \"win32/daemon\"' > /dev/null 2>&1" + windows_service_gem_check_command = %q{ruby -r "win32/daemon" -e ":noop"} if defined?(Bundler) Bundler.with_clean_env do # This returns true if the gem can be loaded - system windows_service_gem_check_command + system(windows_service_gem_check_command) end else # This returns true if the gem can be loaded - system windows_service_gem_check_command + system(windows_service_gem_check_command) end end diff --git a/spec/support/shared/functional/win32_service.rb b/spec/support/shared/functional/win32_service.rb new file mode 100644 index 0000000000..7dd1920418 --- /dev/null +++ b/spec/support/shared/functional/win32_service.rb @@ -0,0 +1,60 @@ + +require 'chef/application/windows_service_manager' + +shared_context "using Win32::Service" do + # Some helper methods. + + def test_service_exists? + ::Win32::Service.exists?("spec-service") + end + + def test_service_state + ::Win32::Service.status("spec-service").current_state + end + + def service_manager + Chef::Application::WindowsServiceManager.new(test_service) + end + + def cleanup + # Uninstall if the test service is installed. + if test_service_exists? + + # We can only uninstall when the service is stopped. + if test_service_state != "stopped" + ::Win32::Service.send("stop", "spec-service") + while test_service_state != "stopped" + sleep 1 + end + end + + ::Win32::Service.delete("spec-service") + end + + # Delete the test_service_file if it exists + if File.exists?(test_service_file) + File.delete(test_service_file) + end + + end + + + # Definition for the test-service + + let(:test_service) { + { + :service_name => "spec-service", + :service_display_name => "Spec Test Service", + :service_description => "Service for testing Chef::Application::WindowsServiceManager.", + :service_file_path => File.expand_path(File.join(File.dirname(__FILE__), '../../platforms/win32/spec_service.rb')) + } + } + + # Test service creates a file for us to verify that it is running. + # Since our test service is running as Local System we should look + # for the file it creates under SYSTEM temp directory + + let(:test_service_file) { + "#{ENV['SystemDrive']}\\windows\\temp\\spec_service_file" + } +end diff --git a/spec/unit/http_spec.rb b/spec/unit/http_spec.rb index 60d36eb4a0..ddfc56583d 100644 --- a/spec/unit/http_spec.rb +++ b/spec/unit/http_spec.rb @@ -44,6 +44,13 @@ describe Chef::HTTP do expect(http.create_url('///api/endpoint?url=http://foo.bar')).to eql(URI.parse('http://www.getchef.com/organization/org/api/endpoint?url=http://foo.bar')) end + # As per: https://github.com/opscode/chef/issues/2500 + it 'should treat scheme part of the URI in a case-insensitive manner' do + http = Chef::HTTP.allocate # Calling Chef::HTTP::new sets @url, don't want that. + expect { http.create_url('HTTP://www1.chef.io/') }.not_to raise_error + expect(http.create_url('HTTP://www2.chef.io/')).to eql(URI.parse('http://www2.chef.io/')) + end + end # create_url describe "head" do diff --git a/spec/unit/provider/service/windows_spec.rb b/spec/unit/provider/service/windows_spec.rb index e4b0714d22..784a2232b2 100644 --- a/spec/unit/provider/service/windows_spec.rb +++ b/spec/unit/provider/service/windows_spec.rb @@ -18,6 +18,7 @@ # require 'spec_helper' +require 'mixlib/shellout' describe Chef::Provider::Service::Windows, "load_current_resource" do before(:each) do @@ -38,6 +39,7 @@ describe Chef::Provider::Service::Windows, "load_current_resource" do allow(Win32::Service).to receive(:config_info).with(@new_resource.service_name).and_return( double("ConfigStruct", :start_type => "auto start")) allow(Win32::Service).to receive(:exists?).and_return(true) + allow(Win32::Service).to receive(:configure).and_return(Win32::Service) end it "should set the current resources service name to the new resources service name" do @@ -131,6 +133,26 @@ describe Chef::Provider::Service::Windows, "load_current_resource" do expect(@new_resource.updated_by_last_action?).to be_falsey end + describe "running as a different account" do + let(:old_run_as_user) { @new_resource.run_as_user } + let(:old_run_as_password) { @new_resource.run_as_password } + + before { + @new_resource.run_as_user(".\\wallace") + @new_resource.run_as_password("Wensleydale") + } + + after { + @new_resource.run_as_user(old_run_as_user) + @new_resource.run_as_password(old_run_as_password) + } + + it "should call #grant_service_logon if the :run_as_user and :run_as_password attributes are present" do + expect(Win32::Service).to receive(:start) + expect(@provider).to receive(:grant_service_logon).and_return(true) + @provider.start_service + end + end end @@ -364,4 +386,76 @@ describe Chef::Provider::Service::Windows, "load_current_resource" do expect { @provider.send(:set_startup_type, :fire_truck) }.to raise_error(Chef::Exceptions::ConfigurationError) end end + + shared_context "testing private methods" do + + let(:private_methods) { + described_class.private_instance_methods + } + + before { + described_class.send(:public, *private_methods) + } + + after { + described_class.send(:private, *private_methods) + } + end + + describe "grant_service_logon" do + include_context "testing private methods" + + let(:username) { "unit_test_user" } + let(:success_string) { "The task has completed successfully.\r\nSee logfile etc." } + let(:failure_string) { "Look on my works, ye Mighty, and despair!" } + let(:command) { + dbfile = @provider.grant_dbfile_name(username) + policyfile = @provider.grant_policyfile_name(username) + logfile = @provider.grant_logfile_name(username) + + %Q{secedit.exe /configure /db "#{dbfile}" /cfg "#{policyfile}" /areas USER_RIGHTS SECURITYPOLICY SERVICES /log "#{logfile}"} + } + let(:shellout_env) { {:environment=>{"LC_ALL"=>"en_US.UTF-8"}} } + + before { + expect_any_instance_of(described_class).to receive(:shell_out).with(command).and_call_original + expect_any_instance_of(Mixlib::ShellOut).to receive(:run_command).and_return(nil) + } + + after { + # only needed for the second test. + ::File.delete(@provider.grant_policyfile_name(username)) rescue nil + ::File.delete(@provider.grant_logfile_name(username)) rescue nil + ::File.delete(@provider.grant_dbfile_name(username)) rescue nil + } + + it "calls Mixlib::Shellout with the correct command string" do + expect_any_instance_of(Mixlib::ShellOut).to receive(:exitstatus).and_return(0) + expect(@provider.grant_service_logon(username)).to equal true + end + + it "raises an exception when the grant command fails" do + expect_any_instance_of(Mixlib::ShellOut).to receive(:exitstatus).and_return(1) + expect_any_instance_of(Mixlib::ShellOut).to receive(:stdout).and_return(failure_string) + expect { @provider.grant_service_logon(username) }.to raise_error(Chef::Exceptions::Service) + end + end + + describe "cleaning usernames" do + include_context "testing private methods" + + it "correctly reformats usernames to create valid filenames" do + expect(@provider.clean_username_for_path("\\\\problem username/oink.txt")).to eq("_problem_username_oink_txt") + expect(@provider.clean_username_for_path("boring_username")).to eq("boring_username") + end + + it "correctly reformats usernames for the policy file" do + expect(@provider.canonicalize_username(".\\maryann")).to eq("maryann") + expect(@provider.canonicalize_username("maryann")).to eq("maryann") + + expect(@provider.canonicalize_username("\\\\maryann")).to eq("maryann") + expect(@provider.canonicalize_username("mydomain\\\\maryann")).to eq("mydomain\\\\maryann") + expect(@provider.canonicalize_username("\\\\mydomain\\\\maryann")).to eq("mydomain\\\\maryann") + end + end end diff --git a/spec/unit/search/query_spec.rb b/spec/unit/search/query_spec.rb index d4ff9e4367..2fb197b183 100644 --- a/spec/unit/search/query_spec.rb +++ b/spec/unit/search/query_spec.rb @@ -24,7 +24,7 @@ describe Chef::Search::Query do let(:query) { Chef::Search::Query.new } shared_context "filtered search" do - let(:query_string) { "search/node?q=platform:rhel&sort=X_CHEF_id_CHEF_X%20asc&start=0&rows=1000" } + let(:query_string) { "search/node?q=platform:rhel&sort=X_CHEF_id_CHEF_X%20asc&start=0" } let(:server_url) { "https://api.opscode.com/organizations/opscode/nodes" } let(:args) { { filter_key => filter_hash } } let(:filter_hash) { @@ -65,6 +65,14 @@ describe Chef::Search::Query do "total" => 4 } } + let(:response_rows) { + [ + { "env" => "elysium", "ruby_plat" => "nudibranch" }, + { "env" => "hades", "ruby_plat" => "i386-mingw32"}, + { "env" => "elysium", "ruby_plat" => "centos"}, + { "env" => "moon", "ruby_plat" => "solaris2"} + ] + } end before(:each) do @@ -132,59 +140,59 @@ describe Chef::Search::Query do "total" => 4 } } - it "should accept a type as the first argument" do + it "accepts a type as the first argument" do expect { query.search("node") }.not_to raise_error expect { query.search(:node) }.not_to raise_error expect { query.search(Hash.new) }.to raise_error(Chef::Exceptions::InvalidSearchQuery, /(Hash)/) end - it "should query for every object of a type by default" do - expect(rest).to receive(:get_rest).with("search/node?q=*:*&sort=X_CHEF_id_CHEF_X%20asc&start=0&rows=1000").and_return(response) + it "queries for every object of a type by default" do + expect(rest).to receive(:get_rest).with("search/node?q=*:*&sort=X_CHEF_id_CHEF_X%20asc&start=0").and_return(response) query.search(:node) end - it "should allow a custom query" do - expect(rest).to receive(:get_rest).with("search/node?q=platform:rhel&sort=X_CHEF_id_CHEF_X%20asc&start=0&rows=1000").and_return(response) + it "allows a custom query" do + expect(rest).to receive(:get_rest).with("search/node?q=platform:rhel&sort=X_CHEF_id_CHEF_X%20asc&start=0").and_return(response) query.search(:node, "platform:rhel") end - it "should let you set a sort order" do - expect(rest).to receive(:get_rest).with("search/node?q=platform:rhel&sort=id%20desc&start=0&rows=1000").and_return(response) - query.search(:node, "platform:rhel", "id desc") + it "lets you set a sort order" do + expect(rest).to receive(:get_rest).with("search/node?q=platform:rhel&sort=id%20desc&start=0").and_return(response) + query.search(:node, "platform:rhel", sort: "id desc") end - it "should let you set a starting object" do - expect(rest).to receive(:get_rest).with("search/node?q=platform:rhel&sort=id%20desc&start=2&rows=1000").and_return(response) - query.search(:node, "platform:rhel", "id desc", 2) + it "lets you set a starting object" do + expect(rest).to receive(:get_rest).with("search/node?q=platform:rhel&sort=X_CHEF_id_CHEF_X%20asc&start=2").and_return(response) + query.search(:node, "platform:rhel", start: 2) end - it "should let you set how many rows to return" do - expect(rest).to receive(:get_rest).with("search/node?q=platform:rhel&sort=id%20desc&start=2&rows=40").and_return(response) - query.search(:node, "platform:rhel", "id desc", 2, 40) + it "lets you set how many rows to return" do + expect(rest).to receive(:get_rest).with("search/node?q=platform:rhel&sort=X_CHEF_id_CHEF_X%20asc&start=0&rows=40").and_return(response) + query.search(:node, "platform:rhel", rows: 40) end - it "should throw an exception if you pass to many options" do - expect { query.search(:node, "platform:rhel", "id desc", 2, 40, "wrong") } - .to raise_error(Chef::Exceptions::InvalidSearchQuery, "Too many arguments! (4 for <= 3)") + it "throws an exception if you pass an incorrect option" do + expect { query.search(:node, "platform:rhel", total: 10) } + .to raise_error(ArgumentError, /unknown keyword: total/) end - it "should return the raw rows, start, and total if no block is passed" do + it "returns the raw rows, start, and total if no block is passed" do rows, start, total = query.search(:node) expect(rows).to equal(response["rows"]) expect(start).to equal(response["start"]) expect(total).to equal(response["total"]) end - it "should call a block for each object in the response" do + it "calls a block for each object in the response" do @call_me = double("blocky") response["rows"].each { |r| expect(@call_me).to receive(:do).with(r) } query.search(:node) { |r| @call_me.do(r) } end - it "should page through the responses" do + it "pages through the responses" do @call_me = double("blocky") response["rows"].each { |r| expect(@call_me).to receive(:do).with(r) } - query.search(:node, "*:*", nil, 0, 1) { |r| @call_me.do(r) } + query.search(:node, "*:*", sort: nil, start: 0, rows: 1) { |r| @call_me.do(r) } end context "when :filter_result is provided as a result" do @@ -195,31 +203,19 @@ describe Chef::Search::Query do expect(rest).to receive(:post_rest).with(query_string, args[filter_key]).and_return(response) end - it "should return start" do + it "returns start" do start = query.search(:node, "platform:rhel", args)[1] expect(start).to eq(response['start']) end - it "should return total" do + it "returns total" do total = query.search(:node, "platform:rhel", args)[2] expect(total).to eq(response['total']) end - it "should return rows with the filter applied" do - results = query.search(:node, "platform:rhel", args)[0] - - results.each_with_index do |result, idx| - expected = response["rows"][idx] - - expect(result).to have_key("url") - expect(result["url"]).to eq(expected["url"]) - - expect(result).to have_key("data") - filter_hash.keys.each do |filter_key| - expect(result["data"]).to have_key(filter_key) - expect(result["data"][filter_key]).to eq(expected["data"][filter_key]) - end - end + it "returns rows with the filter applied" do + filtered_rows = query.search(:node, "platform:rhel", args)[0] + expect(filtered_rows).to match_array(response_rows) end end @@ -230,25 +226,17 @@ describe Chef::Search::Query do include_context "filtered search" do let(:filter_key) { :keys } - it "should emit a deprecation warning" do + it "emits a deprecation warning" do # partial_search calls search, so we'll stub search to return empty allow(query).to receive(:search).and_return( [ [], 0, 0 ] ) - expect(Chef::Log).to receive(:warn).with("DEPRECATED: The 'partial_search' api is deprecated, please use the search api with 'filter_result'") + expect(Chef::Log).to receive(:warn).with(/DEPRECATED: The 'partial_search' API is deprecated/) query.partial_search(:node, "platform:rhel", args) end - it "should return an array of filtered hashes" do + it "returns an array of filtered hashes" do expect(rest).to receive(:post_rest).with(query_string, args[filter_key]).and_return(response) results = query.partial_search(:node, "platform:rhel", args) - - results.each_with_index do |result, idx| - expected = response["rows"][idx] - - filter_hash.keys.each do |filter_key| - expect(result).to have_key(filter_key) - expect(result[filter_key]).to eq(expected["data"][filter_key]) - end - end + expect(results[0]).to match_array(response_rows) end end end |