From df8398534bd868c77d785e9f94a3a01bdeb0211f Mon Sep 17 00:00:00 2001 From: Tim Smith Date: Sat, 7 Oct 2017 16:09:17 -0400 Subject: Add new resources cpan_module dmg_package homebrew_cask homebrew_tap openssl_dhparam openssl_rsa_public_key openssl_rsa_private_key ohai_hint swap_file trusted_certificate windows_font windows_pagefile windows_printer windows_printer_port windows_share windows_shortcut Signed-off-by: Tim Smith --- lib/chef/mixin/openssl.rb | 40 ++++ lib/chef/resource/cpan_module.rb | 94 +++++++++ lib/chef/resource/dmg_package.rb | 104 ++++++++++ lib/chef/resource/homebrew_cask.rb | 60 ++++++ lib/chef/resource/homebrew_tap.rb | 63 ++++++ lib/chef/resource/ohai_hint.rb | 83 ++++++++ lib/chef/resource/openssl_dhparam.rb | 72 +++++++ lib/chef/resource/openssl_rsa_private_key.rb | 81 ++++++++ lib/chef/resource/openssl_rsa_public_key.rb | 54 +++++ lib/chef/resource/openssl_x509.rb | 146 +++++++++++++ lib/chef/resource/swap_file.rb | 200 ++++++++++++++++++ lib/chef/resource/trusted_certificate.rb | 60 ++++++ lib/chef/resource/windows_font.rb | 113 ++++++++++ lib/chef/resource/windows_pagefile.rb | 164 +++++++++++++++ lib/chef/resource/windows_path.rb | 9 +- lib/chef/resource/windows_printer.rb | 113 ++++++++++ lib/chef/resource/windows_printer_port.rb | 110 ++++++++++ lib/chef/resource/windows_share.rb | 299 +++++++++++++++++++++++++++ lib/chef/resource/windows_shortcut.rb | 61 ++++++ 19 files changed, 1918 insertions(+), 8 deletions(-) create mode 100644 lib/chef/mixin/openssl.rb create mode 100644 lib/chef/resource/cpan_module.rb create mode 100644 lib/chef/resource/dmg_package.rb create mode 100644 lib/chef/resource/homebrew_cask.rb create mode 100644 lib/chef/resource/homebrew_tap.rb create mode 100644 lib/chef/resource/ohai_hint.rb create mode 100644 lib/chef/resource/openssl_dhparam.rb create mode 100644 lib/chef/resource/openssl_rsa_private_key.rb create mode 100644 lib/chef/resource/openssl_rsa_public_key.rb create mode 100644 lib/chef/resource/openssl_x509.rb create mode 100644 lib/chef/resource/swap_file.rb create mode 100644 lib/chef/resource/trusted_certificate.rb create mode 100644 lib/chef/resource/windows_font.rb create mode 100644 lib/chef/resource/windows_pagefile.rb create mode 100644 lib/chef/resource/windows_printer.rb create mode 100644 lib/chef/resource/windows_printer_port.rb create mode 100644 lib/chef/resource/windows_share.rb create mode 100644 lib/chef/resource/windows_shortcut.rb diff --git a/lib/chef/mixin/openssl.rb b/lib/chef/mixin/openssl.rb new file mode 100644 index 0000000000..a13e2c22af --- /dev/null +++ b/lib/chef/mixin/openssl.rb @@ -0,0 +1,40 @@ +# +# Copyright 2008-2017, Chef Software, Inc +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Ported from the homebrew cookbook's Homebrew::Mixin owner helpers +# +# This lives here in Chef::Mixin because Chef's namespacing makes it +# awkward to use modules elsewhere (e.g., chef/provider/package/homebrew/owner) + +require "openssl" + +class Chef + module Mixin + module Openssl + # Validation helpers + def key_length_valid?(number) + number >= 1024 && number & (number - 1) == 0 + end + + def key_file_valid?(key_file_path, key_password = nil) + # Check if the key file exists + # Verify the key file contains a private key + return false unless ::File.exist?(key_file_path) + key = OpenSSL::PKey::RSA.new File.read(key_file_path), key_password + key.private? + end + end + end +end diff --git a/lib/chef/resource/cpan_module.rb b/lib/chef/resource/cpan_module.rb new file mode 100644 index 0000000000..da63dc9a74 --- /dev/null +++ b/lib/chef/resource/cpan_module.rb @@ -0,0 +1,94 @@ +require "chef/resource" + +class Chef + class Resource + # @author Joshua Timberman + # @author Steve Nolen + # resource to install modules from CPAN + class CpanModule < Chef::Resource + resource_name :cpan_module + + property :module_name, String, name_property: true + property :cpam_bin_path, String, default: "/usr/local/bin/cpanm" + property :force, [true, false], default: false + property :test, [true, false], default: false + property :version, String + property :cwd, String + + action :install do + declare_resource(:execute, "CPAN :install #{new_resource.module_name}") do + cwd current_working_dir + command cpanm_install_cmd + environment "HOME" => current_working_dir, "PATH" => "/usr/local/bin:/usr/bin:/bin" + not_if { module_exists_new_enough } + end + end + + action :uninstall do + declare_resource(:execute, "CPAN :uninstall #{new_resource.module_name}") do + cwd current_working_dir + command cpanm_uninstall_cmd + only_if module_exists + end + end + + action_class do + def module_exists_new_enough + existing_version = parse_cpan_version + return false if existing_version.empty? # mod doesn't exist + return true if new_resource.version.nil? # mod exists and version is unimportant + @comparator, @pending_version = new_resource.version.split(" ", 2) + @current_vers = Gem::Version.new(existing_version) + @pending_vers = Gem::Version.new(@pending_version) + @current_vers.method(@comparator).call(@pending_vers) + end + + def parse_cpan_version + mod_ver_cmd = shell_out("perl -M#{new_resource.module_name} -e 'print $#{new_resource.module_name}::VERSION;' 2> /dev/null") + mod_ver = mod_ver_cmd.stdout + return mod_ver if mod_ver.empty? + # remove leading v and convert underscores to dots since gems parses them wrong + mod_ver.gsub!(/v_/, "v" => 3, "_" => ".") + # in the event that this command outputs whatever it feels like, only keep the first vers number! + version_match = /(^[0-9.]*)/.match(mod_ver) + version_match[0] + end + + # builds a string of the perl command to see if the module exists + # @return [String] + def module_exists + "perl -m#{new_resource.module_name} -e ';' 2> /dev/null" + end + + def cpanm_install_cmd + @cmd = "#{new_resource.cpam_bin_path} --quiet " + @cmd += "--force " if new_resource.force + @cmd += "--notest " unless new_resource.test + @cmd += new_resource.module_name + @cmd += parsed_version + @cmd + end + + def cpanm_uninstall_cmd + @cmd = "#{new_resource.cpam_bin_path} " + @cmd += "--force " if new_resource.force + @cmd += "--uninstall " + @cmd += new_resource.module_name + @cmd + end + + # a bit of a stub, could use a version parser for really consistent expeirence + def parsed_version + return "~\"#{new_resource.version}\"" if new_resource.version + "" + end + + def current_working_dir + return new_resource.cwd if new_resource.cwd + return "/var/root" if node["platform"] == "mac_os_x" + "/root" + end + end + end + end +end diff --git a/lib/chef/resource/dmg_package.rb b/lib/chef/resource/dmg_package.rb new file mode 100644 index 0000000000..344b117f77 --- /dev/null +++ b/lib/chef/resource/dmg_package.rb @@ -0,0 +1,104 @@ +# +# Copyright:: 2011-2017, Chef Software, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require "chef/resource" + +class Chef + class Resource + # @author Joshua Timberman + # resource to install OS X applications (.app) from dmg files. + class DmgPackage < Chef::Resource + resource_name :dmg_package + provides :dmg_package + + property :app, String, name_property: true + property :source, String + property :file, String + property :owner, String + property :destination, String, default: "/Applications" + property :checksum, String + property :volumes_dir, String + property :dmg_name, String + property :type, String, default: "app" + property :installed, [true, false], default: false, desired_state: false + property :package_id, String + property :dmg_passphrase, String + property :accept_eula, [true, false], default: false + property :headers, [Hash, nil], default: nil + + load_current_value do |new_resource| + if ::File.directory?("#{new_resource.destination}/#{new_resource.app}.app") + Chef::Log.info "Already installed; to upgrade, remove \"#{new_resource.destination}/#{new_resource.app}.app\"" + installed true + elsif shell_out("pkgutil --pkgs='#{new_resource.package_id}'").exitstatus == 0 + Chef::Log.info "Already installed; to upgrade, try \"sudo pkgutil --forget '#{new_resource.package_id}'\"" + installed true + else + installed false + end + end + + action :install do + unless current_resource.installed + + volumes_dir = new_resource.volumes_dir ? new_resource.volumes_dir : new_resource.app + dmg_name = new_resource.dmg_name ? new_resource.dmg_name : new_resource.app + dmg_file = new_resource.file ? new_resource.file : "#{Chef::Config[:file_cache_path]}/#{dmg_name}.dmg" + + if new_resource.source + declare_resource(:remote_file, "#{dmg_file} - #{new_resource.name}") do + path dmg_file + source new_resource.source + headers new_resource.headers if new_resource.headers + checksum new_resource.checksum if new_resource.checksum + end + end + + passphrase_cmd = new_resource.dmg_passphrase ? "-passphrase #{new_resource.dmg_passphrase}" : "" + declare_resource(:ruby_block, "attach #{dmg_file}") do + block do + cmd = shell_out("hdiutil imageinfo #{passphrase_cmd} '#{dmg_file}' | grep -q 'Software License Agreement: true'") + software_license_agreement = cmd.exitstatus == 0 + raise "Requires EULA Acceptance; add 'accept_eula true' to package resource" if software_license_agreement && !new_resource.accept_eula + accept_eula_cmd = new_resource.accept_eula ? "echo Y | PAGER=true" : "" + shell_out!("#{accept_eula_cmd} hdiutil attach #{passphrase_cmd} '#{dmg_file}' -mountpoint '/Volumes/#{volumes_dir}' -quiet") + end + not_if "hdiutil info #{passphrase_cmd} | grep -q 'image-path.*#{dmg_file}'" + end + + case new_resource.type + when "app" + declare_resource(:execute, "rsync --force --recursive --links --perms --executability --owner --group --times '/Volumes/#{volumes_dir}/#{new_resource.app}.app' '#{new_resource.destination}'") do + user new_resource.owner if new_resource.owner + end + + declare_resource(:file, "#{new_resource.destination}/#{new_resource.app}.app/Contents/MacOS/#{new_resource.app}") do + mode "755" + ignore_failure true + end + when "mpkg", "pkg" + declare_resource(:execute, "installation_file=$(ls '/Volumes/#{volumes_dir}' | grep '.#{new_resource.type}$') && sudo installer -pkg \"/Volumes/#{volumes_dir}/$installation_file\" -target /") do + # Prevent cfprefsd from holding up hdiutil detach for certain disk images + environment("__CFPREFERENCES_AVOID_DAEMON" => "1") + end + end + + declare_resource(:execute, "hdiutil detach '/Volumes/#{volumes_dir}' || hdiutil detach '/Volumes/#{volumes_dir}' -force") + end + end + end + end +end diff --git a/lib/chef/resource/homebrew_cask.rb b/lib/chef/resource/homebrew_cask.rb new file mode 100644 index 0000000000..e84330bb41 --- /dev/null +++ b/lib/chef/resource/homebrew_cask.rb @@ -0,0 +1,60 @@ +# +# Copyright:: 2011-2017, Chef Software, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require "chef/resource" + +class Chef + class Resource + # @author Graeme Mathieson + # @author Joshua Timberman + # Resource for brew cask, a Homebrew-style CLI workflow for the administration of Mac applications + # distributed as binaries. It's implemented as a homebrew "external command" called cask. + class HomebrewCask < Chef::Resource + resource_name :homebrew_cask + + property :cask_name, String, regex: /^[\w-]+$/, name_property: true + property :options, String + + action :install do + declare_resource(:execute, "installing cask #{new_resource.name}") do + command "/usr/local/bin/brew cask install #{new_resource.name} #{new_resource.options}" + user Homebrew.owner + environment lazy { { "HOME" => ::Dir.home(Homebrew.owner), "USER" => Homebrew.owner } } + not_if { casked? } + end + end + + action :uninstall do + declare_resource(:execute, "uninstalling cask #{new_resource.name}") do + command "/usr/local/bin/brew cask uninstall #{new_resource.name}" + user Homebrew.owner + environment lazy { { "HOME" => ::Dir.home(Homebrew.owner), "USER" => Homebrew.owner } } + only_if { casked? } + end + end + + action_class do + alias_method :action_cask, :action_install + alias_method :action_uncask, :action_uninstall + + def casked? + shell_out("/usr/local/bin/brew cask list 2>/dev/null").stdout.split.include?(name) + shell_out("/usr/local/bin/brew cask list 2>/dev/null", user: Homebrew.owner).stdout.split.include?(name) + end + end + end + end +end diff --git a/lib/chef/resource/homebrew_tap.rb b/lib/chef/resource/homebrew_tap.rb new file mode 100644 index 0000000000..4232a0e157 --- /dev/null +++ b/lib/chef/resource/homebrew_tap.rb @@ -0,0 +1,63 @@ +# +# Copyright:: 2011-2017, Chef Software, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +require "chef/resource" + +class Chef + class Resource + # @author Graeme Mathieson + # @author Joshua Timberman + # Resouce for brew tap, a Homebrew command used to add additional formula repositories + class HomebrewTap < Chef::Resource + resource_name :homebrew_tap + + property :tap_name, + String, + name_property: true, + regex: %r{^[\w-]+(?:\/[\w-]+)+$} + property :url, String + property :full, [TrueClass, FalseClass], default: false + + action :tap do + unless tapped?(new_resource.name) + declare_resource(:execute, "tapping #{new_resource.name}") do + command "/usr/local/bin/brew tap #{full ? '--full' : ''} #{new_resource.name} #{url || ''}" + environment lazy { { "HOME" => ::Dir.home(Homebrew.owner), "USER" => Homebrew.owner } } + not_if "/usr/local/bin/brew tap | grep #{new_resource.name}" + user Homebrew.owner + end + end + end + + action :untap do + if tapped?(new_resource.name) + declare_resource(:execute, "untapping #{new_resource.name}") do + command "/usr/local/bin/brew untap #{new_resource.name}" + environment lazy { { "HOME" => ::Dir.home(Homebrew.owner), "USER" => Homebrew.owner } } + only_if "/usr/local/bin/brew tap | grep #{new_resource.name}" + user Homebrew.owner + end + end + end + + action_class do + def tapped?(name) + tap_dir = name.gsub("/", "/homebrew-") + ::File.directory?("/usr/local/Homebrew/Library/Taps/#{tap_dir}") + end + end + end + end +end diff --git a/lib/chef/resource/ohai_hint.rb b/lib/chef/resource/ohai_hint.rb new file mode 100644 index 0000000000..cf414dd736 --- /dev/null +++ b/lib/chef/resource/ohai_hint.rb @@ -0,0 +1,83 @@ +# +# Copyright:: Copyright 2011-2016, Chef Software, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require "chef/resource" + +class Chef + class Resource + # @author Yukihiko SAWANOBORI + # creates Ohai hint files, which are consumed by Ohai plugins in order to determine if they should run or not. + class OhaiHint < Chef::Resource + resource_name :ohai_hint + + property :hint_name, String, name_property: true + property :content, Hash + property :compile_time, [true, false], default: true + + action :create do + declare_resource(:directory, ::Ohai::Config.ohai.hints_path.first) do + action :create + recursive true + end + + declare_resource(:file ohai_hint_file_path(new_resource.hint_name)) do + action :create + content format_content(new_resource.content) + end + end + + action :delete do + declare_resource(:file, ohai_hint_file_path(new_resource.hint_name)) do + action :delete + notifies :reload, ohai[reload ohai post hint removal] + end + + declare_resource(:ohai, "reload ohai post hint removal") do + action :nothing + end + end + + action_class do + def ohai_hint_file_path(filename) + path = ::File.join(::Ohai::Config.ohai.hints_path.first, filename) + path << ".json" unless path.end_with?(".json") + path + end + + def format_content(content) + return "" if content.nil? || content.empty? + JSON.pretty_generate(content) + end + + def file_content(path) + return JSON.parse(::File.read(path)) + rescue JSON::ParserError + Chef::Log.debug("Could not parse JSON in ohai hint at #{ohai_hint_path}. It's probably an empty hint file") + return nil + end + end + + # this resource forces itself to run at compile_time + def after_created + return unless compile_time + Array(action).each do |action| + run_action(action) + end + end + end + end +end diff --git a/lib/chef/resource/openssl_dhparam.rb b/lib/chef/resource/openssl_dhparam.rb new file mode 100644 index 0000000000..6260f83f82 --- /dev/null +++ b/lib/chef/resource/openssl_dhparam.rb @@ -0,0 +1,72 @@ +# +# Copyright:: Copyright 2008-2016, Chef Software Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require "chef/resource" + +class Chef + class Resource + # @author Charles Johnson + # generates dhparam.pem files. If a valid dhparam.pem file is found at the specified location, + # no new file will be created. If a file is found at the specified location but it is not a valid + # dhparam file, it will be overwritten. + class OpensslDhparam < Chef::Resource + require "openssl" + + resource_name :openssl_dhparam + + property :path, String, name_property: true + property :key_length, equal_to: [1024, 2048, 4096, 8192], default: 2048 + property :generator, equal_to: [2, 5], default: 2 + property :owner, String, default: "root" + property :group, String, default: node["root_group"] + property :mode, [Integer, String], default: "0640" + + action :create do + return if dhparam_pem_valid?(new_resource.path) + + converge_by("Create a dhparam file #{new_resource.path}") do + dhparam_content = gen_dhparam(new_resource.key_length, new_resource.generator).to_pem + + declare_resource(:file, new_resource.name) do + action :create + owner new_resource.owner + group new_resource.group + mode new_resource.mode + sensitive true + content dhparam_content + end + end + end + + action_class do + # Check if the dhparam.pem file exists + # Verify the dhparam.pem file contains a key + def dhparam_pem_valid?(dhparam_pem_path) + return false unless ::File.exist?(dhparam_pem_path) + dhparam = OpenSSL::PKey::DH.new File.read(dhparam_pem_path) + dhparam.params_ok? + end + + # generate a dhparam file + def gen_dhparam(key_length, generator) + raise ArgumentError, "Key length must be a power of 2 greater than or equal to 1024" unless key_length_valid?(key_length) + OpenSSL::PKey::DH.new(key_length, generator) + end + end + end + end +end diff --git a/lib/chef/resource/openssl_rsa_private_key.rb b/lib/chef/resource/openssl_rsa_private_key.rb new file mode 100644 index 0000000000..049a52e1a1 --- /dev/null +++ b/lib/chef/resource/openssl_rsa_private_key.rb @@ -0,0 +1,81 @@ +# +# Copyright:: Copyright 2008-2016, Chef Software Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require "chef/resource" + +class Chef + class Resource + # @author Charles Johnson + # generates rsa private key files. If a valid rsa key file can be opened at the specified location, + # no new file will be created. If the RSA key file cannot be opened, either because it does not exist + # or because the password to the RSA key file does not match the password in the recipe, it will be overwritten. + class OpensslRsaPrivateKey < Chef::Resource + require "openssl" + + resource_name :openssl_rsa_private_key + + property :path, String, name_property: true + property :key_length, equal_to: [1024, 2048, 4096, 8192], default: 2048 + property :key_pass, String + property :key_cipher, String, default: "des3", equal_to: valid_ciphers + property :owner, String, default: "root" + property :group, String, default: node["root_group"] + property :mode, [Integer, String], default: "0640" + property :force, [true, false], default: false + + action :create do + return if new_resource.force || priv_key_file_valid?(new_resource.path, new_resource.key_pass) + + converge_by("create #{new_resource.key_length} bit RSA key #{new_resource.path}") do + if new_resource.key_pass + unencrypted_rsa_key = gen_rsa_priv_key(new_resource.key_length) + rsa_key_content = encrypt_rsa_key(unencrypted_rsa_key, new_resource.key_pass, new_resource.cipher) + else + rsa_key_content = gen_rsa_priv_key(new_resource.key_length).to_pem + end + + declare_resource(:file, new_resource.path) do + action :create + owner new_resource.owner + group new_resource.group + mode new_resource.mode + sensitive true + content rsa_key_content + end + end + end + + action_class do + def gen_rsa_key(key_length) + raise ArgumentError, "Key length must be a power of 2 greater than or equal to 1024" unless key_length_valid?(key_length) + + OpenSSL::PKey::RSA.new(key_length) + end + + # Key manipulation helpers + # Returns a pem string + def encrypt_rsa_key(rsa_key, key_password) + raise TypeError, "rsa_key must be a Ruby OpenSSL::PKey::RSA object" unless rsa_key.is_a?(OpenSSL::PKey::RSA) + raise TypeError, "RSA key password must be a string" unless key_password.is_a?(String) + + cipher = OpenSSL::Cipher::Cipher.new("des3") + rsa_key.to_pem(cipher, key_password) + end + end + end + end +end diff --git a/lib/chef/resource/openssl_rsa_public_key.rb b/lib/chef/resource/openssl_rsa_public_key.rb new file mode 100644 index 0000000000..31899cbd4a --- /dev/null +++ b/lib/chef/resource/openssl_rsa_public_key.rb @@ -0,0 +1,54 @@ +# +# Copyright:: Copyright 2008-2016, Chef Software Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require "chef/resource" + +class Chef + class Resource + # @author Tim Smith + # generates rsa public key files given a private key. + class OpensslRsaPublicKey < Chef::Resource + require "openssl" + + resource_name :openssl_rsa_public_key + + property :path, String, name_property: true + property :private_key_path, String + property :private_key_content, String + property :private_key_pass, String + property :owner, String, default: 'root' + property :group, String, default: node['root_group'] + property :mode, [Integer, String], default: '0640' + + action :create do + raise ArgumentError, "You cannot specify both 'private_key_path' and 'private_key_content' properties at the same time." if new_resource.private_key_path && new_resource.private_key_content + raise ArgumentError, "You must specify the private key with either 'private_key_path' or 'private_key_content' properties." unless new_resource.private_key_path || new_resource.private_key_content + raise "#{new_resource.private_key_path} not a valid private RSA key or password is invalid" unless priv_key_file_valid?((new_resource.private_key_path || new_resource.private_key_content), new_resource.private_key_pass) + + rsa_key_content = gen_rsa_pub_key((new_resource.private_key_path || new_resource.private_key_content), new_resource.private_key_pass) + + file new_resource.path do + action :create + owner new_resource.owner + group new_resource.group + mode new_resource.mode + content rsa_key_content + end + end + end + end +end diff --git a/lib/chef/resource/openssl_x509.rb b/lib/chef/resource/openssl_x509.rb new file mode 100644 index 0000000000..1e74e4306f --- /dev/null +++ b/lib/chef/resource/openssl_x509.rb @@ -0,0 +1,146 @@ +# +# Copyright:: Copyright 2008-2016, Chef Software Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require "chef/resource" + +class Chef + class Resource + # @author Jesse Nelson + # generates self-signed, PEM-formatted x509 certificates + class OpensslX509 < Chef::Resource + require "openssl" + + resource_name :openssl_x509 + + property :owner, String + property :group, String + property :expire, Integer + property :mode, [Integer, String] + property :org, String, required: true + property :org_unit, String, required: true + property :country, String, required: true + property :common_name, String, required: true + property :subject_alt_name, Array, default: [] + property :key_file, String + property :key_pass, String + property :key_length, equal_to: [1024, 2048, 4096, 8192], default: 2048 + + action :create do + return if ::File.exist? new_resource.name + + converge_by("create #{new_resource.key_length} bit #{new_resource.name} x509 cert") do + create_keys + cert_content = cert.to_pem + key_content = key.to_pem + + declare_resource(:file, new_resource.name) do + action :create_if_missing + mode new_resource.mode + owner new_resource.owner + group new_resource.group + sensitive true + content cert_content + end + + declare_resource(:file, new_resource.key_file) do + action :create_if_missing + mode new_resource.mode + owner new_resource.owner + group new_resource.group + sensitive true + content key_content + end + end + end + + action_class do + def generate_key_file + unless new_resource.key_file + path, file = ::File.split(new_resource.name) + filename = ::File.basename(file, ::File.extname(file)) + new_resource.key_file path + "/" + filename + ".key" + end + new_resource.key_file + end + + def key + @key ||= if key_file_valid?(generate_key_file, new_resource.key_pass) + OpenSSL::PKey::RSA.new ::File.read(generate_key_file), new_resource.key_pass + else + OpenSSL::PKey::RSA.new(new_resource.key_length) + end + @key + end + + def cert + @cert ||= OpenSSL::X509::Certificate.new + end + + def gen_cert + cert + cert.subject = cert.issuer = OpenSSL::X509::Name.parse(subject) + cert.not_before = Time.now + cert.not_after = Time.now + (new_resource.expire.to_i * 24 * 60 * 60) + cert.public_key = key.public_key + cert.serial = 0x0 + cert.version = 2 + end + + def subject + @subject ||= "/C=" + new_resource.country + + "/O=" + new_resource.org + + "/OU=" + new_resource.org_unit + + "/CN=" + new_resource.common_name + end + + def extensions + exts = [] + exts << @ef.create_extension("basicConstraints", "CA:TRUE", true) + exts << @ef.create_extension("subjectKeyIdentifier", "hash") + + unless new_resource.subject_alt_name.empty? + san = {} + counters = {} + new_resource.subject_alt_name.each do |an| + kind, value = an.split(":", 2) + counters[kind] ||= 0 + counters[kind] += 1 + san["#{kind}.#{counters[kind]}"] = value + end + @ef.config["alt_names"] = san + exts << @ef.create_extension("subjectAltName", "@alt_names") + end + + exts + end + + def create_keys + gen_cert + @ef ||= OpenSSL::X509::ExtensionFactory.new + @ef.subject_certificate = cert + @ef.issuer_certificate = cert + @ef.config = OpenSSL::Config.load(OpenSSL::Config::DEFAULT_CONFIG_FILE) + + cert.extensions = extensions + cert.add_extension @ef.create_extension("authorityKeyIdentifier", + "keyid:always,issuer:always") + cert.sign key, OpenSSL::Digest::SHA256.new + end + end + end + end +end diff --git a/lib/chef/resource/swap_file.rb b/lib/chef/resource/swap_file.rb new file mode 100644 index 0000000000..991c6cdfb7 --- /dev/null +++ b/lib/chef/resource/swap_file.rb @@ -0,0 +1,200 @@ +# +# Copyright 2012-2014, Seth Vargo +# Copyright 2016-2017, Chef Software, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require "chef/resource" + +class Chef + class Resource + # @author Seth Vargo + # @author Tim Smith + # create and manage swap files + class SwapFile < Chef::Resource + resource_name :swap_file + + property :path, String, name_attribute: true + property :size, Integer + property :persist, [TrueClass, FalseClass], default: false + property :timeout, Integer, default: 600 + property :swappiness, Integer + + action :create do + if swap_enabled? + Chef::Log.debug("#{new_resource} already created - nothing to do") + else + begin + Chef::Log.info "starting first create: #{node['virtualization']['system']}" + do_create(swap_creation_command) + rescue Mixlib::ShellOut::ShellCommandFailed => e + Chef::Log.warn("#{new_resource} Rescuing failed swapfile creation for #{new_resource.path}") + Chef::Log.debug("#{new_resource} Exception when creating swapfile #{new_resource.path}: #{e}") + do_create(dd_command) + end + end + if new_resource.swappiness + include_recipe "sysctl::default" + + sysctl_param "vm.swappiness" do + value new_resource.swappiness + end + end + end + + action :remove do + swapoff if swap_enabled? + remove_swapfile if ::File.exist?(new_resource.path) + end + + action_class do + def do_create(command) + create_swapfile(command) + set_permissions + mkswap + swapon + persist if persist? + end + + def create_swapfile(command) + converge_by "create empty swapfile at #{new_resource.path}" do # ~FC054 + shell_out!(command, timeout: new_resource.timeout) + end + end + + def set_permissions + permissions = "600" + converge_by "set permissions on #{new_resource.path} to #{permissions}" do + shell_out!("chmod #{permissions} #{new_resource.path}") + end + end + + def mkswap + converge_by "make #{new_resource.path} swappable" do + shell_out!("mkswap -f #{new_resource.path}") + end + end + + def swapon + converge_by "enable swap for #{new_resource.path}" do + shell_out!("swapon #{new_resource.path}") + end + end + + def swapoff + converge_by "turn off swap for #{new_resource.path}" do + shell_out!("swapoff #{new_resource.path}") + end + end + + def remove_swapfile + converge_by "remove swap file #{new_resource.path}" do + ::FileUtils.rm(new_resource.path) + end + end + + def swap_enabled? + enabled_swapfiles = shell_out("swapon --summary").stdout + # Regex for our resource path and only our resource path + # It will terminate on whitespace after the path it match + # /testswapfile would match + # /testswapfiledir/someotherfile will not + swapfile_regex = Regexp.new("^#{new_resource.path}[\\s\\t\\n\\f]+") + !swapfile_regex.match(enabled_swapfiles).nil? + end + + def swap_creation_command + command = if compatible_filesystem? && compatible_kernel && !docker? + fallocate_command + else + dd_command + end + Chef::Log.debug("#{new_resource} swap creation command is '#{command}'") + command + end + + def fallback_swap_creation_command + command = dd_command + Chef::Log.debug("#{new_resource} fallback swap creation command is '#{command}'") + command + end + + # The block size (1MB) + def block_size + 1_048_576 + end + + def fallocate_size + size = block_size * new_resource.size + Chef::Log.debug("#{new_resource} fallocate size is #{size}") + size + end + + def fallocate_command + size = fallocate_size + command = "fallocate -l #{size} #{new_resource.path}" + Chef::Log.debug("#{new_resource} fallocate command is '#{command}'") + command + end + + def dd_command + command = "dd if=/dev/zero of=#{new_resource.path} bs=#{block_size} count=#{new_resource.size}" + Chef::Log.debug("#{new_resource} dd command is '#{command}'") + command + end + + def compatible_kernel + fallocate_location = shell_out("which fallocate").stdout + Chef::Log.debug("#{new_resource} fallocate location is '#{fallocate_location}'") + ::File.exist?(fallocate_location.chomp) + end + + def compatible_filesystem? + compatible_filesystems = %w{xfs ext4} + parent_directory = ::File.dirname(new_resource.path) + # Get FS info, get second line as first is column headings + command = "df -PT #{parent_directory} | awk 'NR==2 {print $2}'" + result = shell_out(command).stdout + Chef::Log.debug("#{new_resource} filesystem listing is '#{result}'") + compatible_filesystems.any? { |fs| result.include? fs } + end + + # we can remove this when we only support Chef 13 + def docker?(node = run_context.nil? ? nil : run_context.node) + !!(node && node["virtualization"] && node["virtualization"]["systems"] && + node["virtualization"]["systems"]["docker"] && node["virtualization"]["systems"]["docker"] == "guest") + end + + def persist? + !!new_resource.persist + end + + def persist + fstab = "/etc/fstab" + contents = ::File.readlines(fstab) + addition = "#{new_resource.path} swap swap defaults 0 0" + + if contents.any? { |line| line.strip == addition } + Chef::Log.debug("#{new_resource} already added to /etc/fstab - skipping") + else + Chef::Log.info("#{new_resource} adding entry to #{fstab} for #{new_resource.path}") + + contents << "#{addition}\n" + ::File.open(fstab, "w") { |f| f.write(contents.join("")) } + end + end + end + end + end +end diff --git a/lib/chef/resource/trusted_certificate.rb b/lib/chef/resource/trusted_certificate.rb new file mode 100644 index 0000000000..9f3908f899 --- /dev/null +++ b/lib/chef/resource/trusted_certificate.rb @@ -0,0 +1,60 @@ +# +# Copyright:: 2015-2017, Chef Software, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require "chef/resource" + +class Chef + class Resource + # @author Tom Duffield + # resource to manage adding SSL/TLS certificates to the operating system's trust store + class TrustedCertificate < Chef::Resource + resource_name :trusted_certificate + + property :certificate_name, String, name_property: true + property :content, String, required: true + + provides :trusted_certificate + + action :create do + declare_resource(:execute, "update trusted certificates") do + command platform_family?("debian", "suse") ? "update-ca-certificates" : "update-ca-trust extract" + action :nothing + end + + declare_resource(:file, "#{certificate_path}/#{new_resource.certificate_name}.crt") do + content new_resource.content + owner "root" + group "staff" if platform_family?("debian") + action :create + notifies :run, "execute[update trusted certificates]" + end + end + + action_class do + def certificate_path + case node["platform_family"] + when "debian" + "/usr/local/share/ca-certificates" + when "suse" + "/etc/pki/trust/anchors/" + else # probably RHEL + "/etc/pki/ca-trust/source/anchors" + end + end + end + end + end +end diff --git a/lib/chef/resource/windows_font.rb b/lib/chef/resource/windows_font.rb new file mode 100644 index 0000000000..fbcbbecfbb --- /dev/null +++ b/lib/chef/resource/windows_font.rb @@ -0,0 +1,113 @@ +# +# Copyright:: 2014-2017, Schuberg Philis BV. +# Copyright:: 2017, Chef Software, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require "chef/resource" + +class Chef + class Resource + # @author Sander Botman + # Installs font files. Sources the font by default from the cookbook, but a URI source can be specified as well. + class WindowsFont < Chef::Resource + resource_name :windows_font + provides :windows_font, os: "windows" + + property :font_name, String, name_property: true + property :source, String, coerce: proc { |x| x.tr('\\', "/").gsub("//", "/") } + + include Windows::Helper + + action :install do + if font_exists? + Chef::Log.debug("Not installing font: #{new_resource.font_name} as font already installed.") + else + retrieve_cookbook_font + install_font + del_cookbook_font + end + end + + action_class do + # if a source is specified fetch using remote_file. It not use cookbook_file + def retrieve_cookbook_font + font_file = new_resource.font_name + if new_resource.source + declare_resource(:remote_file, font_file) do + action :nothing + source source_uri + path win_friendly_path(::File.join(ENV["TEMP"], font_file)) + end.run_action(:create) + else + declare_resource(:cookbook_file, font_file) do + action :nothing + cookbook cookbook_name.to_s unless cookbook_name.nil? + path win_friendly_path(::File.join(ENV["TEMP"], font_file)) + end.run_action(:create) + end + end + + # delete the temp cookbook file + def del_cookbook_font + declare_resource(:file, ::File.join(ENV["TEMP"], new_resource.font_name)) do + action :delete + end + end + + # install the font into the appropriate fonts directory + def install_font + require "win32ole" if RUBY_PLATFORM =~ /mswin|mingw32|windows/ + fonts_dir = WIN32OLE.new("WScript.Shell").SpecialFolders("Fonts") + folder = WIN32OLE.new("Shell.Application").Namespace(fonts_dir) + converge_by("install font #{new_resource.font_name} to #{fonts_dir}") do + folder.CopyHere(win_friendly_path(::File.join(ENV["TEMP"], new_resource.font_name))) + end + end + + # Check to see if the font is installed in the fonts dir + # + # @return [Boolean] Is the font is installed? + def font_exists? + require "win32ole" if RUBY_PLATFORM =~ /mswin|mingw32|windows/ + fonts_dir = WIN32OLE.new("WScript.Shell").SpecialFolders("Fonts") + Chef::Log.debug("Seeing if the font at #{win_friendly_path(::File.join(fonts_dir, new_resource.font_name))} exists") + ::File.exist?(win_friendly_path(::File.join(fonts_dir, new_resource.font_name))) + end + + # Parse out the schema provided to us to see if it's one we support via remote_file. + # We do this because URI will parse C:/foo as schema 'c', which won't work with remote_file + def remote_file_schema?(schema) + return true if %w{http https ftp}.include?(schema) + end + + # return new_resource.source if we have a proper URI specified + # if it's a local file listed as a source return it in file:// format + def source_uri + begin + require "uri" + if remote_file_schema?(URI.parse(new_resource.source).scheme) + Chef::Log.debug("source property starts with ftp/http. Using source property unmodified") + return new_resource.source + end + rescue URI::InvalidURIError + Chef::Log.warn("source property of #{new_resource.source} could not be processed as a URI. Check the format you provided.") + end + Chef::Log.debug("source property does not start with ftp/http. Prepending with file:// as it appears to be a local file.") + "file://#{new_resource.source}" + end + end + end + end +end diff --git a/lib/chef/resource/windows_pagefile.rb b/lib/chef/resource/windows_pagefile.rb new file mode 100644 index 0000000000..f657384234 --- /dev/null +++ b/lib/chef/resource/windows_pagefile.rb @@ -0,0 +1,164 @@ +# +# +# +# Copyright:: 2012-2017, Nordstrom, Inc. +# Copyright:: 2017, Chef Software, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +class Chef + class Resource + # @author Kevin Moser + # Configures the file that provides virtual memory for applications requiring + # more memory than available RAM or that are paged out to free up memory in use. + class WindowsPagefile < Chef::Resource + + property :name, String, name_property: true + property :system_managed, [true, false] + property :automatic_managed, [true, false], default: false + property :initial_size, Integer + property :maximum_size, Integer + + include Chef::Mixin::ShellOut + include Windows::Helper + + action :set do + pagefile = new_resource.name + initial_size = new_resource.initial_size + maximum_size = new_resource.maximum_size + system_managed = new_resource.system_managed + automatic_managed = new_resource.automatic_managed + + if automatic_managed + set_automatic_managed unless automatic_managed? + else + unset_automatic_managed if automatic_managed? + + # Check that the resource is not just trying to unset automatic managed, if it is do nothing more + if (initial_size && maximum_size) || system_managed + validate_name + create(pagefile) unless exists?(pagefile) + + if system_managed + set_system_managed(pagefile) unless max_and_min_set?(pagefile, 0, 0) + else + unless max_and_min_set?(pagefile, initial_size, maximum_size) + set_custom_size(pagefile, initial_size, maximum_size) + end + end + end + end + end + + action :delete do + validate_name + pagefile = new_resource.name + delete(pagefile) if exists?(pagefile) + end + + action_class do + def validate_name + return if /^.:.*.sys/ =~ new_resource.name + raise "#{new_resource.name} does not match the format DRIVE:\\path\\file.sys for pagefiles. Example: C:\\pagefile.sys" + end + + def exists?(pagefile) + @exists ||= begin + Chef::Log.debug("Checking if #{pagefile} exists by runing: #{wmic} pagefileset where SettingID=\"#{get_setting_id(pagefile)}\" list /format:list") + cmd = shell_out("#{wmic} pagefileset where SettingID=\"#{get_setting_id(pagefile)}\" list /format:list", returns: [0]) + cmd.stderr.empty? && (cmd.stdout =~ /SettingID=#{get_setting_id(pagefile)}/i) + end + end + + def max_and_min_set?(pagefile, min, max) + @max_and_min_set ||= begin + Chef::Log.debug("Checking if #{pagefile} min: #{min} and max #{max} are set") + cmd = shell_out("#{wmic} pagefileset where SettingID=\"#{get_setting_id(pagefile)}\" list /format:list", returns: [0]) + cmd.stderr.empty? && (cmd.stdout =~ /InitialSize=#{min}/i) && (cmd.stdout =~ /MaximumSize=#{max}/i) + end + end + + def create(pagefile) + converge_by("create pagefile #{pagefile}") do + Chef::Log.debug("Running #{wmic} pagefileset create name=\"#{win_friendly_path(pagefile)}\"") + cmd = shell_out("#{wmic} pagefileset create name=\"#{win_friendly_path(pagefile)}\"") + check_for_errors(cmd.stderr) + end + end + + def delete(pagefile) + converge_by("remove pagefile #{pagefile}") do + Chef::Log.debug("Running #{wmic} pagefileset where SettingID=\"#{get_setting_id(pagefile)}\" delete") + cmd = shell_out("#{wmic} pagefileset where SettingID=\"#{get_setting_id(pagefile)}\" delete") + check_for_errors(cmd.stderr) + end + end + + def automatic_managed? + @automatic_managed ||= begin + Chef::Log.debug("Checking if pagefiles are automatically managed") + cmd = shell_out("#{wmic} computersystem where name=\"%computername%\" get AutomaticManagedPagefile /format:list") + cmd.stderr.empty? && (cmd.stdout =~ /AutomaticManagedPagefile=TRUE/i) + end + end + + def set_automatic_managed + converge_by("set pagefile to Automatic Managed") do + Chef::Log.debug("Running #{wmic} computersystem where name=\"%computername%\" set AutomaticManagedPagefile=True") + cmd = shell_out("#{wmic} computersystem where name=\"%computername%\" set AutomaticManagedPagefile=True") + check_for_errors(cmd.stderr) + end + end + + def unset_automatic_managed + converge_by("set pagefile to User Managed") do + Chef::Log.debug("Running #{wmic} computersystem where name=\"%computername%\" set AutomaticManagedPagefile=False") + cmd = shell_out("#{wmic} computersystem where name=\"%computername%\" set AutomaticManagedPagefile=False") + check_for_errors(cmd.stderr) + end + end + + def set_custom_size(pagefile, min, max) + converge_by("set #{pagefile} to InitialSize=#{min} & MaximumSize=#{max}") do + Chef::Log.debug("Running #{wmic} pagefileset where SettingID=\"#{get_setting_id(pagefile)}\" set InitialSize=#{min},MaximumSize=#{max}") + cmd = shell_out("#{wmic} pagefileset where SettingID=\"#{get_setting_id(pagefile)}\" set InitialSize=#{min},MaximumSize=#{max}", returns: [0]) + check_for_errors(cmd.stderr) + end + end + + def set_system_managed(pagefile) # rubocop: disable Style/AccessorMethodName + converge_by("set #{pagefile} to System Managed") do + Chef::Log.debug("Running #{wmic} pagefileset where SettingID=\"#{get_setting_id(pagefile)}\" set InitialSize=0,MaximumSize=0") + cmd = shell_out("#{wmic} pagefileset where SettingID=\"#{get_setting_id(pagefile)}\" set InitialSize=0,MaximumSize=0", returns: [0]) + check_for_errors(cmd.stderr) + end + end + + def get_setting_id(pagefile) + pagefile = win_friendly_path(pagefile) + pagefile = pagefile.split('\\') + "#{pagefile[1]} @ #{pagefile[0]}" + end + + def check_for_errors(stderr) + raise stderr.chomp unless stderr.empty? + end + + def wmic + @wmic ||= locate_sysnative_cmd("wmic.exe") + end + end + end + end +end diff --git a/lib/chef/resource/windows_path.rb b/lib/chef/resource/windows_path.rb index 5472a7e4fd..c09b34a0c9 100644 --- a/lib/chef/resource/windows_path.rb +++ b/lib/chef/resource/windows_path.rb @@ -22,19 +22,12 @@ class Chef class Resource class WindowsPath < Chef::Resource + resource_name :windows_path provides :windows_path, os: "windows" allowed_actions :add, :remove default_action :add - def initialize(name, run_context = nil) - super - @resource_name = :windows_path - @path = name - @provider = Chef::Provider::WindowsPath - @action = :add - end - property :path, String, name_property: true end end diff --git a/lib/chef/resource/windows_printer.rb b/lib/chef/resource/windows_printer.rb new file mode 100644 index 0000000000..669ffe2e18 --- /dev/null +++ b/lib/chef/resource/windows_printer.rb @@ -0,0 +1,113 @@ +# +# Copyright:: 2012-2017, Nordstrom, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# See here for more info: +# http://msdn.microsoft.com/en-us/library/windows/desktop/aa394492(v=vs.85).aspx + +require "chef/resource" + +class Chef + class Resource + # @author Doug Ireton + # Create Windows printer. Note that this doesn't currently install a printer driver. + # You must already have the driver installed on the system. + class WindowsPrinter < Chef::Resource + resource_name :windows_printer + provides :windows_printer, os: "windows" + + require "resolv" + + property :device_id, String, name_property: true, required: true + property :comment, String + property :default, [true, false], default: false + property :driver_name, String, required: true + property :location, String + property :shared, [true, false], default: false + property :share_name, String + property :ipv4_address, String, regex: Resolv::IPv4::Regex + property :exists, [true, false], desired_state: true + + PRINTERS_REG_KEY = 'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Print\Printers\\'.freeze unless defined?(PRINTERS_REG_KEY) + + def printer_exists?(name) + printer_reg_key = PRINTERS_REG_KEY + name + Chef::Log.debug "Checking to see if this reg key exists: '#{printer_reg_key}'" + Registry.key_exists?(printer_reg_key) + end + + load_current_value do |desired| + name desired.name + exists printer_exists?(desired.name) + # TODO: Set @current_resource printer properties from registry + end + + action :create do + if @current_resource.exists + Chef::Log.info "#{@new_resource} already exists - nothing to do." + else + converge_by("Create #{@new_resource}") do + create_printer + end + end + end + + action :delete do + if @current_resource.exists + converge_by("Delete #{@new_resource}") do + delete_printer + end + else + Chef::Log.info "#{@current_resource} doesn't exist - can't delete." + end + end + + action_class do + def create_printer + # Create the printer port first + windows_printer_port new_resource.ipv4_address do + end + + port_name = "IP_#{new_resource.ipv4_address}" + + declare_resource(:powershell_script, "Creating printer: #{new_resource.name}") do + code <<-EOH + + Set-WmiInstance -class Win32_Printer ` + -EnableAllPrivileges ` + -Argument @{ DeviceID = "#{new_resource.device_id}"; + Comment = "#{new_resource.comment}"; + Default = "$#{new_resource.default}"; + DriverName = "#{new_resource.driver_name}"; + Location = "#{new_resource.location}"; + PortName = "#{port_name}"; + Shared = "$#{new_resource.shared}"; + ShareName = "#{new_resource.share_name}"; + } + EOH + end + end + + def delete_printer + declare_resource(:powershell_script, "Deleting printer: #{new_resource.name}") do + code <<-EOH + $printer = Get-WMIObject -class Win32_Printer -EnableAllPrivileges -Filter "name = '#{new_resource.name}'" + $printer.Delete() + EOH + end + end + end + end + end +end diff --git a/lib/chef/resource/windows_printer_port.rb b/lib/chef/resource/windows_printer_port.rb new file mode 100644 index 0000000000..70aea8c30b --- /dev/null +++ b/lib/chef/resource/windows_printer_port.rb @@ -0,0 +1,110 @@ +# +# Copyright:: 2012-2017, Nordstrom, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# See here for more info: +# http://msdn.microsoft.com/en-us/library/windows/desktop/aa394492(v=vs.85).aspx + +require "chef/resource" + +class Chef + class Resource + # @author Doug Ireton + # Create and delete TCP/IPv4 printer ports. + class WindowsPrinterPort < Chef::Resource + resource_name :windows_printer_port + provides :windows_printer_port, os: "windows" + + require "resolv" + + property :ipv4_address, String, name_property: true, required: true, regex: Resolv::IPv4::Regex + property :port_name, String + property :port_number, Integer, default: 9100 + property :port_description, String + property :snmp_enabled, [true, false], default: false + property :port_protocol, Integer, default: 1, equal_to: [1, 2] + property :exists, [true, false], desired_state: true + + PORTS_REG_KEY = 'HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Print\Monitors\Standard TCP/IP Port\Ports\\'.freeze unless defined?(PORTS_REG_KEY) + + def port_exists?(name) + port_reg_key = PORTS_REG_KEY + name + + Chef::Log.debug "Checking to see if this reg key exists: '#{port_reg_key}'" + Registry.key_exists?(port_reg_key) + end + + load_current_value do |desired| + name desired.name + ipv4_address desired.ipv4_address + port_name desired.port_name || "IP_#{@new_resource.ipv4_address}" + exists port_exists?(desired.port_name) + # TODO: Set @current_resource port properties from registry + end + + action :create do + if current_resource.exists + Chef::Log.info "#{@new_resource} already exists - nothing to do." + else + converge_by("Create #{@new_resource}") do + create_printer_port + end + end + end + + action :delete do + if current_resource.exists + converge_by("Delete #{@new_resource}") do + delete_printer_port + end + else + Chef::Log.info "#{@current_resource} doesn't exist - can't delete." + end + end + + action_class do + def create_printer_port + port_name = new_resource.port_name || "IP_#{new_resource.ipv4_address}" + + # create the printer port using PowerShell + declare_resource(:powershell_script, "Creating printer port #{new_resource.port_name}") do + code <<-EOH + + Set-WmiInstance -class Win32_TCPIPPrinterPort ` + -EnableAllPrivileges ` + -Argument @{ HostAddress = "#{new_resource.ipv4_address}"; + Name = "#{port_name}"; + Description = "#{new_resource.port_description}"; + PortNumber = "#{new_resource.port_number}"; + Protocol = "#{new_resource.port_protocol}"; + SNMPEnabled = "$#{new_resource.snmp_enabled}"; + } + EOH + end + end + + def delete_printer_port + port_name = new_resource.port_name || "IP_#{new_resource.ipv4_address}" + + declare_resource(:powershell_script, "Deleting printer port: #{new_resource.port_name}") do + code <<-EOH + $port = Get-WMIObject -class Win32_TCPIPPrinterPort -EnableAllPrivileges -Filter "name = '#{port_name}'" + $port.Delete() + EOH + end + end + end + end + end +end diff --git a/lib/chef/resource/windows_share.rb b/lib/chef/resource/windows_share.rb new file mode 100644 index 0000000000..ee75041012 --- /dev/null +++ b/lib/chef/resource/windows_share.rb @@ -0,0 +1,299 @@ +# +# Copyright:: 2014-2017, Sölvi Páll Ásgeirsson. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require "chef/resource" + +class Chef + class Resource + # @author Sölvi Páll Ásgeirsson + # @author Richard Lavey + class WindowsShare < Chef::Resource + resource_name :windows_share + provides :windows_share, os: "windows" + + property :share_name, String, name_property: true + property :path, String + property :description, String, default: "" + property :full_users, Array, default: [] + property :change_users, Array, default: [] + property :read_users, Array, default: [] + + include Windows::Helper + include Chef::Mixin::PowershellOut + + require "win32ole" if RUBY_PLATFORM =~ /mswin|mingw32|windows/ + + ACCESS_FULL = 2_032_127 + ACCESS_CHANGE = 1_245_631 + ACCESS_READ = 1_179_817 + + action :create do + raise "No path property set" unless new_resource.path + + if different_path? + unless current_resource.path.nil? || current_resource.path.empty? + converge_by("Removing previous share") do + delete_share + end + end + converge_by("Creating share #{current_resource.share_name}") do + create_share + end + end + + if different_members?(:full_users) || + different_members?(:change_users) || + different_members?(:read_users) || + different_description? + converge_by("Setting permissions and description for #{new_resource.share_name}") do + set_share_permissions + end + end + end + + action :delete do + if !current_resource.path.nil? && !current_resource.path.empty? + converge_by("Deleting #{current_resource.share_name}") do + delete_share + end + else + Chef::Log.debug("#{current_resource.share_name} does not exist - nothing to do") + end + end + + load_current_value do |desired| + wmi = WIN32OLE.connect("winmgmts://") + shares = wmi.ExecQuery("SELECT * FROM Win32_Share WHERE name = '#{desired.share_name}'") + existing_share = shares.Count == 0 ? nil : shares.ItemIndex(0) + + description "" + unless existing_share.nil? + path existing_share.Path + description existing_share.Description + end + + perms = share_permissions name + unless perms.nil? + full_users perms[:full_users] + change_users perms[:change_users] + read_users perms[:read_users] + end + end + + def share_permissions(name) + wmi = WIN32OLE.connect("winmgmts://") + shares = wmi.ExecQuery("SELECT * FROM Win32_LogicalShareSecuritySetting WHERE name = '#{name}'") + + # The security descriptor is an output parameter + sd = nil + begin + shares.ItemIndex(0).GetSecurityDescriptor(sd) + sd = WIN32OLE::ARGV[0] + rescue WIN32OLERuntimeError + Chef::Log.warn("Failed to retrieve any security information about the share.") + end + + read = [] + change = [] + full = [] + + unless sd.nil? + sd.DACL.each do |dacl| + trustee = "#{dacl.Trustee.Domain}\\#{dacl.Trustee.Name}".downcase + case dacl.AccessMask + when ACCESS_FULL + full.push(trustee) + when ACCESS_CHANGE + change.push(trustee) + when ACCESS_READ + read.push(trustee) + else + Chef::Log.warn "Unknown access mask #{dacl.AccessMask} for user #{trustee}. This will be lost if permissions are updated" + end + end + end + + { + full_users: full, + change_users: change, + read_users: read, + } + end + + action_class do + def description_exists?(resource) + !resource.description.nil? + end + + def different_description? + if description_exists?(new_resource) && description_exists?(current_resource) + new_resource.description.casecmp(current_resource.description) != 0 + else + description_exists?(new_resource) || description_exists?(current_resource) + end + end + + def different_path? + return true if current_resource.path.nil? + win_friendly_path(new_resource.path).casecmp(win_friendly_path(current_resource.path)) != 0 + end + + def different_members?(permission_type) + !(current_resource.send(permission_type.to_sym) - new_resource.send(permission_type.to_sym).map(&:downcase)).empty? || + !(new_resource.send(permission_type.to_sym).map(&:downcase) - current_resource.send(permission_type.to_sym)).empty? + end + + def find_share_by_name(name) + wmi = WIN32OLE.connect("winmgmts://") + shares = wmi.ExecQuery("SELECT * FROM Win32_Share WHERE name = '#{name}'") + shares.Count == 0 ? nil : shares.ItemIndex(0) + end + + def delete_share + find_share_by_name(new_resource.share_name).delete + end + + def create_share + raise "#{new_resource.path} is missing or not a directory" unless ::File.directory? new_resource.path + new_share_script = <<-EOH + $share = [wmiclass]"\\\\#{ENV['COMPUTERNAME']}\\root\\CimV2:Win32_Share" + $result=$share.Create('#{new_resource.path}', + '#{new_resource.share_name}', + 0, + 16777216, + '#{new_resource.description}', + $null, + $null) + exit $result.returnValue + EOH + r = powershell_out new_share_script + message = case r.exitstatus + when 2 + "2 : Access Denied" + when 8 + "8 : Unknown Failure" + when 9 + "9 : Invalid Name" + when 10 + "10 : Invalid Level" + when 21 + "21 : Invalid Parameter" + when 22 + "22 : Duplicate Share" + when 23 + "23 : Redirected Path" + when 24 + "24 : Unknown Device or Directory" + when 25 + "25 : Net Name Not Found" + else + r.exitstatus.to_s + end + + raise "Could not create share. Win32_Share.create returned #{message}" if r.error? + end + + # set_share_permissions - Enforce the share permissions as dictated by the resource attributes + def set_share_permissions + share_permissions_script = <<-EOH + Function New-SecurityDescriptor + { + param ( + [array]$ACEs + ) + #Create SeCDesc object + $SecDesc = ([WMIClass] "\\\\$env:ComputerName\\root\\cimv2:Win32_SecurityDescriptor").CreateInstance() + + foreach ($ACE in $ACEs ) + { + $SecDesc.DACL += $ACE.psobject.baseobject + } + + #Return the security Descriptor + return $SecDesc + } + + Function New-ACE + { + param ( + [string] $Name, + [string] $Domain, + [string] $Permission = "Read" + ) + #Create the Trusteee Object + $Trustee = ([WMIClass] "\\\\$env:computername\\root\\cimv2:Win32_Trustee").CreateInstance() + $account = get-wmiobject Win32_Account -filter "Name = '$Name' and Domain = '$Domain'" + $accountSID = [WMI] "\\\\$env:ComputerName\\root\\cimv2:Win32_SID.SID='$($account.sid)'" + + $Trustee.Domain = $Domain + $Trustee.Name = $Name + $Trustee.SID = $accountSID.BinaryRepresentation + + #Create ACE (Access Control List) object. + $ACE = ([WMIClass] "\\\\$env:ComputerName\\root\\cimv2:Win32_ACE").CreateInstance() + switch ($Permission) + { + "Read" { $ACE.AccessMask = 1179817 } + "Change" { $ACE.AccessMask = 1245631 } + "Full" { $ACE.AccessMask = 2032127 } + default { throw "$Permission is not a supported permission value. Possible values are 'Read','Change','Full'" } + } + + $ACE.AceFlags = 3 + $ACE.AceType = 0 + $ACE.Trustee = $Trustee + + $ACE + } + + $dacl_array = @() + + EOH + new_resource.full_users.each do |user| + share_permissions_script += user_to_ace(user, "Full") + end + + new_resource.change_users.each do |user| + share_permissions_script += user_to_ace(user, "Change") + end + + new_resource.read_users.each do |user| + share_permissions_script += user_to_ace(user, "Read") + end + + share_permissions_script += <<-EOH + + $dacl = New-SecurityDescriptor -Aces $dacl_array + + $share = get-wmiobject win32_share -filter 'Name like "#{new_resource.share_name}"' + $return = $share.SetShareInfo($null, '#{new_resource.description}', $dacl) + exit $return.returnValue + EOH + r = powershell_out(share_permissions_script) + raise "Could not set share permissions. Win32_Share.SedtShareInfo returned #{r.exitstatus}" if r.error? + end + + def user_to_ace(fully_qualified_user_name, access) + domain, user = fully_qualified_user_name.split('\\') + unless domain && user + raise "Invalid user entry #{fully_qualified_user_name}. The user names must be specified as 'DOMAIN\\user'" + end + "\n$dacl_array += new-ace -Name '#{user}' -domain '#{domain}' -permission '#{access}'" + end + end + end + end +end diff --git a/lib/chef/resource/windows_shortcut.rb b/lib/chef/resource/windows_shortcut.rb new file mode 100644 index 0000000000..a547f6558c --- /dev/null +++ b/lib/chef/resource/windows_shortcut.rb @@ -0,0 +1,61 @@ +# +# Copyright:: 2010-2017, VMware, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require "chef/resource" + +class Chef + class Resource + # @author Doug MacEachern + # Creates a Windows shortcut file + class WindowsShortcut < Chef::Resource + resource_name :windows_shortcut + provides :windows_shortcut, os: "windows" + + property :target, String + property :arguments, String + property :description, String + property :cwd, String + property :iconlocation, String + + load_current_value do |desired| + require "win32ole" if RUBY_PLATFORM =~ /mswin|mingw32|windows/ + + link = WIN32OLE.new("WScript.Shell").CreateShortcut(desired.name) + name desired.name + target(link.TargetPath) + arguments(link.Arguments) + description(link.Description) + cwd(link.WorkingDirectory) + iconlocation(link.IconLocation) + end + + action :create do + converge_if_changed do + converge_by "creating shortcut #{new_resource.name}" do + link = WIN32OLE.new("WScript.Shell").CreateShortcut(new_resource.name) + link.TargetPath = new_resource.target unless new_resource.target.nil? + link.Arguments = new_resource.arguments unless new_resource.arguments.nil? + link.Description = new_resource.description unless new_resource.description.nil? + link.WorkingDirectory = new_resource.cwd unless new_resource.cwd.nil? + link.IconLocation = new_resource.iconlocation unless new_resource.iconlocation.nil? + # ignoring: WindowStyle, Hotkey + link.Save + end + end + end + end + end +end -- cgit v1.2.1