diff options
Diffstat (limited to 'lib/chef/resource/windows_ad_join.rb')
-rw-r--r-- | lib/chef/resource/windows_ad_join.rb | 242 |
1 files changed, 242 insertions, 0 deletions
diff --git a/lib/chef/resource/windows_ad_join.rb b/lib/chef/resource/windows_ad_join.rb new file mode 100644 index 0000000000..731ce9333e --- /dev/null +++ b/lib/chef/resource/windows_ad_join.rb @@ -0,0 +1,242 @@ +# +# Author:: John Snow (<jsnow@chef.io>) +# Copyright:: 2016-2018, John Snow +# +# 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_relative "../resource" +require "chef-utils/dist" unless defined?(ChefUtils::Dist) + +class Chef + class Resource + class WindowsAdJoin < Chef::Resource + provides :windows_ad_join + + unified_mode true + + description "Use the **windows_ad_join** resource to join a Windows Active Directory domain." + introduced "14.0" + examples <<~DOC + **Join a domain** + + ```ruby + windows_ad_join 'ad.example.org' do + domain_user 'nick' + domain_password 'p@ssw0rd1' + end + ``` + + **Join a domain, as `win-workstation`** + + ```ruby + windows_ad_join 'ad.example.org' do + domain_user 'nick' + domain_password 'p@ssw0rd1' + new_hostname 'win-workstation' + end + ``` + + **Leave the current domain and re-join the `local` workgroup** + + ```ruby + windows_ad_join 'Leave domain' do + action :leave + workgroup 'local' + end + ``` + DOC + + property :domain_name, String, + description: "An optional property to set the FQDN of the Active Directory domain to join if it differs from the resource block's name.", + validation_message: "The 'domain_name' property must be a FQDN.", + regex: /.\../, # anything.anything + name_property: true + + property :domain_user, String, + description: "The domain user that will be used to join the domain.", + required: true + + property :domain_password, String, + description: "The password for the domain user. Note that this resource is set to hide sensitive information by default. ", + required: true + + property :ou_path, String, + description: "The path to the Organizational Unit where the host will be placed." + + property :reboot, Symbol, + equal_to: %i{immediate delayed never request_reboot reboot_now}, + validation_message: "The reboot property accepts :immediate (reboot as soon as the resource completes), :delayed (reboot once the #{ChefUtils::Dist::Infra::PRODUCT} run completes), and :never (Don't reboot)", + description: "Controls the system reboot behavior post domain joining. Reboot immediately, after the #{ChefUtils::Dist::Infra::PRODUCT} run completes, or never. Note that a reboot is necessary for changes to take effect.", + default: :immediate + + property :reboot_delay, Integer, + description: "The amount of time (in minutes) to delay a reboot request.", + default: 0, + introduced: "16.5" + + property :new_hostname, String, + description: "Specifies a new hostname for the computer in the new domain.", + introduced: "14.5" + + property :workgroup_name, String, + description: "Specifies the name of a workgroup to which the computer is added to when it is removed from the domain. The default value is WORKGROUP. This property is only applicable to the :leave action.", + introduced: "15.4" + + # define this again so we can default it to true. Otherwise failures print the password + property :sensitive, [TrueClass, FalseClass], + default: true, desired_state: false + + action :join do + description "Join the Active Directory domain." + + unless on_desired_domain? + cmd = "$pswd = ConvertTo-SecureString \'#{new_resource.domain_password}\' -AsPlainText -Force;" + cmd << "$credential = New-Object System.Management.Automation.PSCredential (\"#{sanitize_usename}\",$pswd);" + cmd << "Add-Computer -DomainName #{new_resource.domain_name} -Credential $credential" + cmd << " -OUPath \"#{new_resource.ou_path}\"" if new_resource.ou_path + cmd << " -NewName \"#{new_resource.new_hostname}\"" if new_resource.new_hostname + cmd << " -Force" + + converge_by("join Active Directory domain #{new_resource.domain_name}") do + ps_run = powershell_exec(cmd) + if ps_run.error? + if sensitive? + raise "Failed to join the domain #{new_resource.domain_name}: *suppressed sensitive resource output*" + else + raise "Failed to join the domain #{new_resource.domain_name}: #{ps_run.errors}" + end + end + + unless new_resource.reboot == :never + reboot "Reboot to join domain #{new_resource.domain_name}" do + action clarify_reboot(new_resource.reboot) + delay_mins new_resource.reboot_delay + reason "Reboot to join domain #{new_resource.domain_name}" + end + end + end + end + end + + action :leave do + description "Leave the Active Directory domain." + + if joined_to_domain? + cmd = "" + cmd << "$pswd = ConvertTo-SecureString \'#{new_resource.domain_password}\' -AsPlainText -Force;" + cmd << "$credential = New-Object System.Management.Automation.PSCredential (\"#{sanitize_usename}\",$pswd);" + cmd << "Remove-Computer" + cmd << " -UnjoinDomainCredential $credential" + cmd << " -NewName \"#{new_resource.new_hostname}\"" if new_resource.new_hostname + cmd << " -WorkgroupName \"#{new_resource.workgroup_name}\"" if new_resource.workgroup_name + cmd << " -Force" + + converge_by("leave Active Directory domain #{node_domain}") do + ps_run = powershell_exec(cmd) + if ps_run.error? + if sensitive? + raise "Failed to leave the domain #{node_domain}: *suppressed sensitive resource output*" + else + raise "Failed to leave the domain #{node_domain}: #{ps_run.errors}" + end + end + + unless new_resource.reboot == :never + reboot "Reboot to leave domain #{new_resource.domain_name}" do + action clarify_reboot(new_resource.reboot) + delay_mins new_resource.reboot_delay + reason "Reboot to leave domain #{new_resource.domain_name}" + end + end + end + end + end + + action_class do + # + # @return [String] The domain name the node is joined to. When the node + # is not joined to a domain this will return the name of the + # workgroup the node is a member of. + # + def node_domain + node_domain = powershell_exec!("(Get-WmiObject Win32_ComputerSystem).Domain") + raise "Failed to check if the system is joined to the domain #{new_resource.domain_name}: #{node_domain.errors}}" if node_domain.error? + + node_domain.result.downcase.strip + end + + # + # @return [String] The workgroup the node is a member of. This will + # return an empty string if the system is not a member of a + # workgroup. + # + def node_workgroup + node_workgroup = powershell_exec!("(Get-WmiObject Win32_ComputerSystem).Workgroup") + raise "Failed to check if the system is currently a member of a workgroup" if node_workgroup.error? + + node_workgroup.result + end + + # + # @return [true, false] Whether or not the node is joined to ANY domain + # + def joined_to_domain? + node_workgroup.empty? && !node_domain.empty? + end + + # + # @return [true, false] Whether or not the node is joined to the domain + # defined by the resource :domain_name property. + # + def on_desired_domain? + node_domain == new_resource.domain_name.downcase + end + + # + # @return [String] the correct user and domain to use. + # if the domain_user property contains an @ symbol followed by any number of non white space characters + # then we assume it is a user from another domain than the one specified in the resource domain_name property. + # if this is the case we do not append the domain_name property to the domain_user property + # the domain_user and domain_name form the UPN (userPrincipalName) + # The specification for the UPN format is RFC 822 + # links: https://docs.microsoft.com/en-us/windows/win32/ad/naming-properties#userprincipalname https://tools.ietf.org/html/rfc822 + # regex: https://rubular.com/r/isAWojpTMKzlnp + def sanitize_usename + if /@/.match?(new_resource.domain_user) + new_resource.domain_user + else + "#{new_resource.domain_user}@#{new_resource.domain_name}" + end + end + + # This resource historically took `:immediate` and `:delayed` as arguments to the reboot property but then + # tried to shove that straight to the `reboot` resource which objected strenuously + def clarify_reboot(reboot_action) + case reboot_action + when :immediate + :reboot_now + when :delayed + :request_reboot + else + reboot_action + end + end + + def sensitive? + !!new_resource.sensitive + end + end + end + end +end |