summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authortyler-ball <tyleraball@gmail.com>2014-12-29 15:00:52 -0800
committertyler-ball <tyleraball@gmail.com>2014-12-29 15:00:52 -0800
commit43eba1778fab8bc440f97824af7227476457a65c (patch)
tree5e967abff042f8a7279c3ad049b54cf7a009f6cb
parent4a91e5e40972f0be7e71aba4615b1f76affeafc6 (diff)
parentb7b7dad4e476b3fde67f0d9881e15efe7e5b60ac (diff)
downloadchef-43eba1778fab8bc440f97824af7227476457a65c.tar.gz
Merging master to audit-mode
-rw-r--r--CHANGELOG.md1
-rw-r--r--DOC_CHANGES.md6
-rw-r--r--RELEASE_NOTES.md4
-rw-r--r--lib/chef/application/windows_service_manager.rb10
-rw-r--r--lib/chef/http.rb2
-rw-r--r--lib/chef/knife/search.rb8
-rw-r--r--lib/chef/provider/service.rb2
-rw-r--r--lib/chef/provider/service/windows.rb100
-rw-r--r--lib/chef/resource/windows_service.rb18
-rw-r--r--lib/chef/search/query.rb153
-rw-r--r--spec/functional/knife/ssh_spec.rb2
-rw-r--r--spec/functional/resource/windows_service_spec.rb98
-rw-r--r--spec/functional/win32/service_manager_spec.rb60
-rw-r--r--spec/support/chef_helpers.rb6
-rw-r--r--spec/support/shared/functional/win32_service.rb60
-rw-r--r--spec/unit/http_spec.rb7
-rw-r--r--spec/unit/provider/service/windows_spec.rb94
-rw-r--r--spec/unit/search/query_spec.rb90
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