diff options
author | Tim Smith <tsmith@chef.io> | 2019-04-05 10:52:30 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-04-05 10:52:30 -0700 |
commit | da8539b5a8aaa5adefdaa10adcdc41412c5d1693 (patch) | |
tree | b192ca1f69f754929a5409c856e8d3e82348d14b | |
parent | 3c264946e8dd051e56418751f093eb3afa3c1387 (diff) | |
parent | 9b19748867fcb05fab550262449a1bd825db15cf (diff) | |
download | chef-da8539b5a8aaa5adefdaa10adcdc41412c5d1693.tar.gz |
Merge pull request #8324 from MsysTechnologiesllc/Nimesh/MSYS-988_lang_add_lc_support
locale: Add support to set all LC ENV variables and deprecate LC_ALL
-rw-r--r-- | lib/chef/deprecated.rb | 5 | ||||
-rw-r--r-- | lib/chef/resource/locale.rb | 140 | ||||
-rw-r--r-- | spec/functional/resource/locale_spec.rb | 78 | ||||
-rw-r--r-- | spec/spec_helper.rb | 1 | ||||
-rw-r--r-- | spec/unit/resource/locale_spec.rb | 207 |
5 files changed, 364 insertions, 67 deletions
diff --git a/lib/chef/deprecated.rb b/lib/chef/deprecated.rb index 1e33fdde0a..6516366d49 100644 --- a/lib/chef/deprecated.rb +++ b/lib/chef/deprecated.rb @@ -226,6 +226,10 @@ class Chef target 26 end + class LocaleLcAll < Base + target 27 + end + class Generic < Base def url "https://docs.chef.io/chef_deprecations_client.html" @@ -235,6 +239,5 @@ class Chef "Deprecation from #{location}\n\n #{message}\n\n#{link}" end end - end end diff --git a/lib/chef/resource/locale.rb b/lib/chef/resource/locale.rb index ca6ea510c0..b16cfe7e3c 100644 --- a/lib/chef/resource/locale.rb +++ b/lib/chef/resource/locale.rb @@ -25,67 +25,119 @@ class Chef description "Use the locale resource to set the system's locale." introduced "14.5" + LC_VARIABLES = %w{LC_ADDRESS LC_COLLATE LC_CTYPE LC_IDENTIFICATION LC_MEASUREMENT LC_MESSAGES LC_MONETARY LC_NAME LC_NUMERIC LC_PAPER LC_TELEPHONE LC_TIME}.freeze + LOCALE_CONF = "/etc/locale.conf".freeze + LOCALE_REGEX = /\A\S+/.freeze + property :lang, String, - default: "en_US.utf8", - description: "Sets the default system language." + description: "Sets the default system language.", + regex: [LOCALE_REGEX], + validation_message: "The provided lang is not valid. It should be a non-empty string without any leading whitespaces." + + property :lc_env, Hash, + description: "A Hash of LC_* env variables in the form of ({ 'LC_ENV_VARIABLE' => 'VALUE' }).", + default: lazy { {} }, + coerce: proc { |h| + if h.respond_to?(:keys) + invalid_keys = h.keys - LC_VARIABLES + unless invalid_keys.empty? + error_msg = "Key of option lc_env must be equal to one of: \"#{LC_VARIABLES.join('", "')}\"! You passed \"#{invalid_keys.join(', ')}\"." + raise Chef::Exceptions::ValidationFailed, error_msg + end + end + unless h.values.all? { |x| x =~ LOCALE_REGEX } + error_msg = "Values of option lc_env should be non-empty string without any leading whitespaces." + raise Chef::Exceptions::ValidationFailed, error_msg + end + h + } - property :lc_all, String, - default: "en_US.utf8", - description: "Sets the fallback system language." + # @deprecated Use {#lc_env} instead of this property. + # {#lc_env} uses Hash with specific LC var as key. + # @raise [Chef::Deprecated] + # + def lc_all(arg = nil) + unless arg.nil? + Chef.deprecated(:locale_lc_all, "Changing LC_ALL can break Chef's parsing of command output in unexpected ways.\n Use one of the more specific LC_ properties as needed.") + end + end action :update do description "Update the system's locale." - - if node["init_package"] == "systemd" - # on systemd settings LC_ALL is (correctly) reserved only for testing and cannot be set globally - execute "localectl set-locale LANG=#{new_resource.lang}" do - # RHEL uses /etc/locale.conf - not_if { up_to_date?("/etc/locale.conf", new_resource.lang) } if ::File.exist?("/etc/locale.conf") - # Ubuntu 16.04 still uses /etc/default/locale - not_if { up_to_date?("/etc/default/locale", new_resource.lang) } if ::File.exist?("/etc/default/locale") + begin + unless up_to_date? + converge_by "Updating System Locale" do + generate_locales unless unavailable_locales.empty? + update_locale + end end - elsif ::File.exist?("/etc/sysconfig/i18n") - locale_file_path = "/etc/sysconfig/i18n" + rescue + # It might affect debugging + raise "#{node['platform']} platform is not supported by the chef locale resource. " + + "If you believe this is in error please file an issue at https://github.com/chef/chef/issues" + end + end - updated = up_to_date?(locale_file_path, new_resource.lang, new_resource.lc_all) + action_class do - file locale_file_path do - content lazy { - locale = IO.read(locale_file_path) - variables = Hash[locale.lines.map { |line| line.strip.split("=") }] - variables["LANG"] = new_resource.lang - variables["LC_ALL"] = - variables.map { |pairs| pairs.join("=") }.join("\n") + "\n" - } - not_if { updated } + # Generates the localisation files from templates using locale-gen. + # @see http://manpages.ubuntu.com/manpages/cosmic/man8/locale-gen.8.html + # @raise [Mixlib::ShellOut::ShellCommandFailed] not a supported language or locale + # + def generate_locales + bash "Generating locales: #{unavailable_locales.join(' ')}" do + code <<~CODE + if type locale-gen >/dev/null 2>&1 + then + locale-gen #{unavailable_locales.join(' ')} + fi + CODE end + end - execute "reload root's lang profile script" do - command "source /etc/sysconfig/i18n; source /etc/profile.d/lang.sh" - not_if { updated } + # Updates system locale by appropriately writing them in /etc/locale.conf + # @note This locale change won't affect the current run. At this time it is an exercise + # left to the user to restart or reboot if the locale change is required at + # later part of the client run. + # @see https://wiki.archlinux.org/index.php/locale#Setting_the_system_locale + # + def update_locale + file "Updating system locale" do + path LOCALE_CONF + content new_content end - elsif ::File.exist?("/usr/sbin/update-locale") - execute "Generate locales" do - command "locale-gen" - not_if { up_to_date?("/etc/default/locale", new_resource.lang, new_resource.lc_all) } + end + + # @return [Array<String>] Locales that user wants to set but are not available on + # the system. They are required to be generated. + # + def unavailable_locales + @unavailable_locales ||= begin + available = shell_out!("locale -a").stdout.split("\n") + required = [new_resource.lang, new_resource.lc_env.values].flatten.compact.uniq + required - available end + end - execute "Update locale" do - command "update-locale LANG=#{new_resource.lang} LC_ALL=#{new_resource.lc_all}" - not_if { up_to_date?("/etc/default/locale", new_resource.lang, new_resource.lc_all) } + # @return [String] Contents that are required to be + # updated in /etc/locale.conf + # + def new_content + @new_content ||= begin + content = {} + content = new_resource.lc_env.dup if new_resource.lc_env + content["LANG"] = new_resource.lang if new_resource.lang + content.sort.map { |t| t.join("=") }.join("\n") + "\n" end - else - raise "#{node["platform"]} platform not supported by the chef locale resource." end - end - action_class do - def up_to_date?(file_path, lang, lc_all = nil) - locale = IO.read(file_path) - locale.include?("LANG=#{lang}") && - (node["init_package"] == "systemd" || lc_all.nil? || locale.include?("LC_ALL=#{lc_all}")) + # @return [Boolean] Whether any modification is required in /etc/locale.conf + # + def up_to_date? + old_content = ::File.read(LOCALE_CONF) + new_content == old_content rescue - false + false # We need to create the file if it is not present end end end diff --git a/spec/functional/resource/locale_spec.rb b/spec/functional/resource/locale_spec.rb new file mode 100644 index 0000000000..61ef4630bc --- /dev/null +++ b/spec/functional/resource/locale_spec.rb @@ -0,0 +1,78 @@ +# +# Author:: Nimesh Patni (<nimesh.patni@msystechnologies.com>) +# Copyright:: Copyright 2008-2018, 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::Locale, :requires_root, :not_supported_on_windows do + + let(:node) { Chef::Node.new } + let(:events) { Chef::EventDispatch::Dispatcher.new } + let(:run_context) { Chef::RunContext.new(node, {}, events) } + let(:resource) { Chef::Resource::Locale.new("fakey_fakerton", run_context) } + + def sets_system_locale(*locales) + system_locales = File.readlines("/etc/locale.conf") + expect(system_locales.map(&:strip)).to eq(locales) + end + + def unsets_system_locale(*locales) + system_locales = File.readlines("/etc/locale.conf") + expect(system_locales.map(&:strip)).not_to eq(locales) + end + + describe "action: update" do + context "Sets system variable" do + it "when LC var is given" do + resource.lc_env({ "LC_MESSAGES" => "en_US" }) + resource.run_action(:update) + sets_system_locale("LC_MESSAGES=en_US") + end + it "when lang is given" do + resource.lang("en_US") + resource.run_action(:update) + sets_system_locale("LANG=en_US") + end + it "when both lang & LC vars are given" do + resource.lang("en_US") + resource.lc_env({ "LC_TIME" => "en_IN" }) + resource.run_action(:update) + sets_system_locale("LANG=en_US", "LC_TIME=en_IN") + end + end + + context "Unsets system variable" do + it "when LC var is not given" do + resource.lc_env() + resource.run_action(:update) + unsets_system_locale("LC_MESSAGES=en_US") + end + it "when lang is not given" do + resource.lang() + resource.run_action(:update) + unsets_system_locale("LANG=en_US") + end + it "when both lang & LC vars are not given" do + resource.lang() + resource.lc_env() + resource.run_action(:update) + unsets_system_locale("LANG=en_US", "LC_TIME=en_IN") + sets_system_locale("") + end + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index d13453b778..027be2e619 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -143,6 +143,7 @@ RSpec.configure do |config| config.filter_run_excluding skip_travis: true if ENV["TRAVIS"] config.filter_run_excluding windows_only: true unless windows? + config.filter_run_excluding not_supported_on_windows: true if windows? config.filter_run_excluding not_supported_on_macos: true if mac_osx? config.filter_run_excluding macos_only: true if !mac_osx? config.filter_run_excluding not_supported_on_aix: true if aix? diff --git a/spec/unit/resource/locale_spec.rb b/spec/unit/resource/locale_spec.rb index 7e1292d211..544f6342c5 100644 --- a/spec/unit/resource/locale_spec.rb +++ b/spec/unit/resource/locale_spec.rb @@ -1,5 +1,5 @@ # -# Author:: Vincent AUBERT (<vincentaubert88@gmail.com>) +# Author:: Nimesh Patni (<nimesh.patni@msystechnologies.com>) # Copyright:: Copyright 2008-2018, Chef Software Inc. # License:: Apache License, Version 2.0 # @@ -21,40 +21,203 @@ require "spec_helper" describe Chef::Resource::Locale do let(:resource) { Chef::Resource::Locale.new("fakey_fakerton") } + let(:provider) { resource.provider_for_action(:update) } - it "has a name of locale" do - expect(resource.resource_name).to eq(:locale) + describe "default:" do + it "name would be locale" do + expect(resource.resource_name).to eq(:locale) + end + it "lang would be nil" do + expect(resource.lang).to be_nil + end + it "lc_env would be an empty hash" do + expect(resource.lc_env).to be_a(Hash) + expect(resource.lc_env).to be_empty + end + it "action would be :update" do + expect(resource.action).to eql([:update]) + end end - it "the lang property is equal to en_US.utf8" do - expect(resource.lang).to eql("en_US.utf8") - end + describe "validations:" do + let(:validation) { Chef::Exceptions::ValidationFailed } + context "lang" do + it "is non empty" do + expect { resource.lang("") }.to raise_error(validation) + end + it "does not contain any leading whitespaces" do + expect { resource.lang(" XX") }.to raise_error(validation) + end + end - it "the lc_all property is equal to en_US.utf8" do - expect(resource.lc_all).to eql("en_US.utf8") + context "lc_env" do + it "is non empty" do + expect { resource.lc_env({ "LC_TIME" => "" }) }.to raise_error(validation) + end + it "does not contain any leading whitespaces" do + expect { resource.lc_env({ "LC_TIME" => " XX" }) }.to raise_error(validation) + end + it "keys are valid and case sensitive" do + expect { resource.lc_env({ "LC_TIMES" => " XX" }) }.to raise_error(validation) + expect { resource.lc_env({ "Lc_Time" => " XX" }) }.to raise_error(validation) + expect(resource.lc_env({ "LC_TIME" => "XX" })).to eql({ "LC_TIME" => "XX" }) + end + end end - it "sets the default action as :update" do - expect(resource.action).to eql([:update]) - end + describe "#unavailable_locales" do + let(:available_locales) do + <<~LOC + C + C.UTF-8 + en_AG + en_AG.utf8 + en_US + POSIX + LOC + end + before do + dummy = Mixlib::ShellOut.new + allow_any_instance_of(Chef::Mixin::ShellOut).to receive(:shell_out!).with("locale -a").and_return(dummy) + allow(dummy).to receive(:stdout).and_return(available_locales) + end + context "when all locales are available on system" do + context "with both properties" do + it "returns an empty array" do + resource.lang("en_US") + resource.lc_env({ "LC_TIME" => "en_AG.utf8", "LC_MESSAGES" => "en_AG.utf8" }) + expect(provider.unavailable_locales).to eq([]) + end + end + context "without lang" do + it "returns an empty array" do + resource.lang() + resource.lc_env({ "LC_TIME" => "en_AG.utf8", "LC_MESSAGES" => "en_AG.utf8" }) + expect(provider.unavailable_locales).to eq([]) + end + end + context "without lc_env" do + it "returns an empty array" do + resource.lang("en_US") + resource.lc_env() + expect(provider.unavailable_locales).to eq([]) + end + end + context "without both" do + it "returns an empty array" do + resource.lang() + resource.lc_env() + expect(provider.unavailable_locales).to eq([]) + end + end + end - it "supports :update action" do - expect { resource.action :update }.not_to raise_error + context "when some locales are not available" do + context "with both properties" do + it "returns list" do + resource.lang("de_DE") + resource.lc_env({ "LC_TIME" => "en_AG.utf8", "LC_MESSAGES" => "en_US.utf8" }) + expect(provider.unavailable_locales).to eq(["de_DE", "en_US.utf8"]) + end + end + context "without lang" do + it "returns list" do + resource.lang() + resource.lc_env({ "LC_TIME" => "en_AG.utf8", "LC_MESSAGES" => "en_US.utf8" }) + expect(provider.unavailable_locales).to eq(["en_US.utf8"]) + end + end + context "without lc_env" do + it "returns list" do + resource.lang("de_DE") + resource.lc_env() + expect(provider.unavailable_locales).to eq(["de_DE"]) + end + end + context "without both" do + it "returns an empty array" do + resource.lang() + resource.lc_env() + expect(provider.unavailable_locales).to eq([]) + end + end + end end - describe "when the language is not the default one" do - let(:resource) { Chef::Resource::Locale.new("fakey_fakerton") } - before do - resource.lang("fr_FR.utf8") - resource.lc_all("fr_FR.utf8") + describe "#new_content" do + context "with both properties" do + before do + resource.lang("en_US") + resource.lc_env({ "LC_TIME" => "en_AG.utf8", "LC_MESSAGES" => "en_AG.utf8" }) + end + it "returns string" do + expect(provider.new_content).to be_a(String) + expect(provider.new_content).not_to be_empty + end + it "keys will be sorted" do + expect(provider.new_content.split("\n").map { |x| x.split("=") }.collect(&:first)).to eq(%w{LANG LC_MESSAGES LC_TIME}) + end + it "ends with a new-line character" do + expect(provider.new_content[-1]).to eq("\n") + end + it "returns a valid string" do + expect(provider.new_content).to eq("LANG=en_US\nLC_MESSAGES=en_AG.utf8\nLC_TIME=en_AG.utf8\n") + end end + context "without lang" do + it "returns a valid string" do + resource.lang() + resource.lc_env({ "LC_TIME" => "en_AG.utf8", "LC_MESSAGES" => "en_AG.utf8" }) + expect(provider.new_content).to eq("LC_MESSAGES=en_AG.utf8\nLC_TIME=en_AG.utf8\n") + end + end + context "without lc_env" do + it "returns a valid string" do + resource.lang("en_US") + resource.lc_env() + expect(provider.new_content).to eq("LANG=en_US\n") + end + end + context "without both" do + it "returns string with only new-line character" do + resource.lang() + resource.lc_env() + expect(provider.new_content).to eq("\n") + end + end + end - it "the lang property is equal to fr_FR.utf8" do - expect(resource.lang).to eql("fr_FR.utf8") + describe "#up_to_date?" do + context "when file does not exists" do + it "returns false" do + allow(File).to receive(:read).and_raise(Errno::ENOENT, "No such file or directory") + expect(provider.up_to_date?).to be_falsy + end end - it "the lc_all property is equal to fr_FR.utf8" do - expect(resource.lc_all).to eql("fr_FR.utf8") + context "when file exists" do + let(:content) { "LANG=en_US\nLC_MESSAGES=en_AG.utf8\nLC_TIME=en_AG.utf8\n" } + before do + allow(provider).to receive(:new_content).and_return(content) + end + context "but is empty" do + it "returns false" do + allow(File).to receive(:read).and_return("") + expect(provider.up_to_date?).to be_falsy + end + end + context "and contains old key-vals" do + it "returns false" do + allow(File).to receive(:read).and_return("LC_MESSAGES=en_AG.utf8\n") + expect(provider.up_to_date?).to be_falsy + end + end + context "and contains new key-vals" do + it "returns true" do + allow(File).to receive(:read).and_return(content) + expect(provider.up_to_date?).to be_truthy + end + end end end end |