1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
|
#
# 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_relative "../dist"
class Chef
class Resource
class WindowsAdJoin < Chef::Resource
provides :windows_ad_join
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 #{Chef::Dist::PRODUCT} run completes), and :never (Don't reboot)",
description: "Controls the system reboot behavior post domain joining. Reboot immediately, after the #{Chef::Dist::PRODUCT} run completes, or never. Note that a reboot is necessary for changes to take effect.",
default: :immediate
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_out(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.stderr}"
end
end
unless new_resource.reboot == :never
reboot "Reboot to join domain #{new_resource.domain_name}" do
action clarify_reboot(new_resource.reboot)
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_out(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.stderr}"
end
end
unless new_resource.reboot == :never
reboot "Reboot to leave domain #{new_resource.domain_name}" do
action clarify_reboot(new_resource.reboot)
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_out!("(Get-WmiObject Win32_ComputerSystem).Domain")
raise "Failed to check if the system is joined to the domain #{new_resource.domain_name}: #{node_domain.stderr}}" if node_domain.error?
node_domain.stdout.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_out!("(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.stdout.downcase.strip
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 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
|