foreman_wds 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.eslintrc.json +20 -0
- data/.gitignore +17 -0
- data/.rubocop.yml +66 -0
- data/.travis.yml +5 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +33 -0
- data/Rakefile +10 -0
- data/app/assets/javascripts/foreman_wds/wds_servers.js +19 -0
- data/app/assets/javascripts/host_edit_extensions.js +50 -0
- data/app/controllers/concerns/foreman/controller/parameters/wds_server.rb +19 -0
- data/app/controllers/concerns/foreman_wds/hosts_controller_extensions.rb +18 -0
- data/app/controllers/concerns/foreman_wds/unattended_controller_extensions.rb +21 -0
- data/app/controllers/wds_servers_controller.rb +85 -0
- data/app/lib/foreman_wds/wds_boot_image.rb +11 -0
- data/app/lib/foreman_wds/wds_image.rb +89 -0
- data/app/lib/foreman_wds/wds_install_image.rb +14 -0
- data/app/models/concerns/foreman_wds/compute_resource_extensions.rb +7 -0
- data/app/models/concerns/foreman_wds/host_extensions.rb +109 -0
- data/app/models/concerns/foreman_wds/nic_extensions.rb +40 -0
- data/app/models/foreman_wds/wds_facet.rb +48 -0
- data/app/models/wds_server.rb +239 -0
- data/app/services/wds_image_cache.rb +66 -0
- data/app/views/foreman_wds/unattend_2016.xml.erb +220 -0
- data/app/views/foreman_wds/windows_ptable.xml.erb +43 -0
- data/app/views/hosts/provision_method/wds/_form.html.erb +12 -0
- data/app/views/wds_servers/_form.html.erb +28 -0
- data/app/views/wds_servers/_image_select.html.erb +27 -0
- data/app/views/wds_servers/_server_select.html.erb +12 -0
- data/app/views/wds_servers/clients/_list.html.erb +43 -0
- data/app/views/wds_servers/edit.html.erb +3 -0
- data/app/views/wds_servers/images/_list.html.erb +36 -0
- data/app/views/wds_servers/index.html.erb +25 -0
- data/app/views/wds_servers/new.html.erb +3 -0
- data/app/views/wds_servers/show.html.erb +46 -0
- data/config/routes.rb +26 -0
- data/db/migrate/20180426133700_add_wds_servers.rb +23 -0
- data/db/seeds.d/50_ptable_templates.rb +16 -0
- data/db/seeds.d/50_unattend_templates.rb +19 -0
- data/foreman_wds.gemspec +23 -0
- data/lib/foreman_wds.rb +4 -0
- data/lib/foreman_wds/engine.rb +70 -0
- data/lib/foreman_wds/version.rb +3 -0
- data/test/foreman_wds_test.rb +11 -0
- data/test/test_helper.rb +4 -0
- metadata +147 -0
@@ -0,0 +1,14 @@
|
|
1
|
+
class ForemanWds::WdsInstallImage < ForemanWds::WdsImage
|
2
|
+
attr_accessor :compression, :dependent_files, :format, :image_group,
|
3
|
+
:partition_style, :security, :staged, :unattend_file_present
|
4
|
+
|
5
|
+
def initialize(json = {})
|
6
|
+
super json
|
7
|
+
end
|
8
|
+
|
9
|
+
def reload
|
10
|
+
return false if wds_server.nil?
|
11
|
+
@json = wds_server.install_image(name)
|
12
|
+
load!
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
module ForemanWds
|
2
|
+
module HostExtensions
|
3
|
+
def self.prepended(base)
|
4
|
+
base.class_eval do
|
5
|
+
after_build :ensure_wds_client
|
6
|
+
before_provision :remove_wds_client
|
7
|
+
|
8
|
+
has_one :wds_facet,
|
9
|
+
class_name: '::ForemanWds::WdsFacet',
|
10
|
+
foreign_key: :host_id,
|
11
|
+
inverse_of: :host,
|
12
|
+
dependent: :destroy
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
delegate :wds_server, to: :wds_facet
|
17
|
+
|
18
|
+
def wds_boot_image
|
19
|
+
ensure_wds_facet.boot_image
|
20
|
+
end
|
21
|
+
|
22
|
+
def wds_boot_image_name
|
23
|
+
ensure_wds_facet.boot_image_name
|
24
|
+
end
|
25
|
+
|
26
|
+
def wds_install_image
|
27
|
+
ensure_wds_facet.install_image
|
28
|
+
end
|
29
|
+
|
30
|
+
def wds_install_image_file
|
31
|
+
ensure_wds_facet.install_image_file
|
32
|
+
end
|
33
|
+
|
34
|
+
def wds_install_image_group
|
35
|
+
ensure_wds_facet.install_image_group
|
36
|
+
end
|
37
|
+
|
38
|
+
def wds_install_image_name
|
39
|
+
ensure_wds_facet.install_image_name
|
40
|
+
end
|
41
|
+
|
42
|
+
def capabilities
|
43
|
+
return super + [:wds] if compute_resource && (os.nil? || os.family == 'Windows')
|
44
|
+
super
|
45
|
+
end
|
46
|
+
|
47
|
+
def bare_metal_capabilities
|
48
|
+
return super + [:wds] if os.nil? || os.family == 'Windows'
|
49
|
+
super
|
50
|
+
end
|
51
|
+
|
52
|
+
def can_be_built?
|
53
|
+
super || (wds? && !build?)
|
54
|
+
end
|
55
|
+
|
56
|
+
def wds_build?
|
57
|
+
provision_method == 'wds'
|
58
|
+
end
|
59
|
+
|
60
|
+
def wds?
|
61
|
+
managed? && wds_build? && SETTINGS[:unattended]
|
62
|
+
end
|
63
|
+
|
64
|
+
def ensure_wds_facet
|
65
|
+
wds_facet || build_wds_facet
|
66
|
+
end
|
67
|
+
|
68
|
+
def unattend_pass(password, suffix = nil)
|
69
|
+
if suffix.nil?
|
70
|
+
suffix = password
|
71
|
+
password = Base64.decode64(root_pass)
|
72
|
+
end
|
73
|
+
Base64.encode64(Encoding::Converter.new('UTF-8', 'UTF-16LE', undef: nil).convert(password + suffix)).delete!("\n")
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def ensure_wds_client
|
79
|
+
raise NotImplementedError, 'Not implemented yet'
|
80
|
+
return unless wds?
|
81
|
+
|
82
|
+
client = wds_server.client(self) || wds_server.create_client(self)
|
83
|
+
|
84
|
+
Rails.logger.info client
|
85
|
+
true
|
86
|
+
rescue ScriptError, StandardError => ex
|
87
|
+
Rails.logger.error "Failed to ensure WDS client, #{ex}"
|
88
|
+
# false
|
89
|
+
end
|
90
|
+
|
91
|
+
def remove_wds_client
|
92
|
+
raise NotImplementedError, 'Not implemented yet'
|
93
|
+
return unless wds?
|
94
|
+
|
95
|
+
client = wds_server.client(self)
|
96
|
+
return unless client
|
97
|
+
|
98
|
+
wds_server.delete_client(self)
|
99
|
+
true
|
100
|
+
rescue ScriptError, StandardError => ex
|
101
|
+
Rails.logger.error "Failed to remove WDS client, #{ex}"
|
102
|
+
# false
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
class ::Host::Managed::Jail < Safemode::Jail
|
108
|
+
allow :unattend_pass, :wds_facet, :wds_server, :wds_install_image_file, :wds_install_image_group, :wds_install_image_name
|
109
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module ForemanWds
|
2
|
+
module NicExtensions
|
3
|
+
def dhcp_update_required?
|
4
|
+
return super if host.nil? || !host.wds? || host.wds_facet.nil?
|
5
|
+
|
6
|
+
# DHCP entry for WDS depends on build mode
|
7
|
+
return true if host.build_changed?
|
8
|
+
end
|
9
|
+
|
10
|
+
def boot_server
|
11
|
+
return super if host.nil? || !host.wds? || host.wds_facet.nil?
|
12
|
+
|
13
|
+
host.wds_server.next_server_ip if host.build? # TODO: Support choosing local boot method
|
14
|
+
end
|
15
|
+
|
16
|
+
def dhcp_records
|
17
|
+
# Always recalculate dhcp records for WDS hosts, to allow different filename for the deleting and setting of a DHCP rebuild
|
18
|
+
@dhcp_records = nil if !host.nil? && host.wds?
|
19
|
+
super
|
20
|
+
end
|
21
|
+
|
22
|
+
def dhcp_attrs(record_mac)
|
23
|
+
data = super(record_mac)
|
24
|
+
return data if host.nil? || !host.wds?
|
25
|
+
|
26
|
+
arch = WdsServer.wdsify_architecture(host.architecture)
|
27
|
+
build_stage = host.build? ? :pxe : :local
|
28
|
+
build_type = host.pxe_loader =~ /UEFI/i ? :uefi : :bios
|
29
|
+
|
30
|
+
if build_stage == :pxe # TODO: Support choosing local boot method
|
31
|
+
data[:filename] = WdsServer.bootfile_path(arch, build_type, build_stage)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Don't compare filenames if trying to check for collisions, WDS entries differ on file depending on build mode
|
35
|
+
data.delete :filename if caller_locations.map(&:label).include?('dhcp_conflict_detected?')
|
36
|
+
|
37
|
+
data
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module ForemanWds
|
2
|
+
class WdsFacet < ApplicationRecord
|
3
|
+
class Jail < Safemode::Jail
|
4
|
+
allow :boot_image_file, :boot_image_name, :install_image_file, :install_image_group, :install_image_name
|
5
|
+
end
|
6
|
+
|
7
|
+
include Facets::Base
|
8
|
+
|
9
|
+
belongs_to :wds_server,
|
10
|
+
class_name: '::WdsServer',
|
11
|
+
inverse_of: :wds_facets
|
12
|
+
|
13
|
+
validates_lengths_from_database
|
14
|
+
|
15
|
+
validates :host, presence: true, allow_blank: false
|
16
|
+
|
17
|
+
validates :install_image_name, presence: true, allow_blank: false
|
18
|
+
|
19
|
+
def boot_image
|
20
|
+
return nil unless wds_server
|
21
|
+
@boot_image ||= if boot_image_name
|
22
|
+
wds_server.boot_image(boot_image_name)
|
23
|
+
else
|
24
|
+
wds_server.boot_images.first
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def boot_image_file
|
29
|
+
return nil unless boot_image
|
30
|
+
@boot_image_file ||= boot_image.file_name
|
31
|
+
end
|
32
|
+
|
33
|
+
def install_image
|
34
|
+
return nil unless wds_server
|
35
|
+
@install_image ||= wds_server.install_image(install_image_name)
|
36
|
+
end
|
37
|
+
|
38
|
+
def install_image_file
|
39
|
+
return nil unless install_image
|
40
|
+
@install_image_file ||= install_image.file_name
|
41
|
+
end
|
42
|
+
|
43
|
+
def install_image_group
|
44
|
+
return nil unless install_image
|
45
|
+
@install_image_group ||= install_image.image_group
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,239 @@
|
|
1
|
+
class WdsServer < ApplicationRecord
|
2
|
+
class Jail < Safemode::Jail
|
3
|
+
allow :name, :shortname
|
4
|
+
end
|
5
|
+
|
6
|
+
# include Taxonomix
|
7
|
+
include Encryptable
|
8
|
+
|
9
|
+
extend FriendlyId
|
10
|
+
friendly_id :name
|
11
|
+
include Parameterizable::ByIdName
|
12
|
+
encrypts :password
|
13
|
+
|
14
|
+
validates_lengths_from_database
|
15
|
+
|
16
|
+
audited except: [:password]
|
17
|
+
has_associated_audits
|
18
|
+
|
19
|
+
before_destroy EnsureNotUsedBy.new(:hosts)
|
20
|
+
|
21
|
+
has_many :wds_facets,
|
22
|
+
class_name: '::ForemanWds::WdsFacet',
|
23
|
+
dependent: :nullify,
|
24
|
+
inverse_of: :wds_server
|
25
|
+
|
26
|
+
has_many :hosts,
|
27
|
+
class_name: '::Host::Managed',
|
28
|
+
dependent: :nullify,
|
29
|
+
inverse_of: :wds_server,
|
30
|
+
through: :wds_facets
|
31
|
+
|
32
|
+
validates :name, presence: true, uniqueness: true
|
33
|
+
validates :url, presence: true
|
34
|
+
validates :user, presence: true
|
35
|
+
validates :password, presence: true
|
36
|
+
|
37
|
+
scoped_search on: :name, complete_value: true
|
38
|
+
default_scope -> { order('wds_servers.name') }
|
39
|
+
|
40
|
+
def boot_image(name)
|
41
|
+
images(:boot, name).first
|
42
|
+
end
|
43
|
+
|
44
|
+
def install_image(name)
|
45
|
+
images(:install, name).first
|
46
|
+
end
|
47
|
+
|
48
|
+
def clients
|
49
|
+
objects = connection.run_wql('SELECT * FROM MSFT_WdsClient')[:msft_wdsclient] rescue nil
|
50
|
+
objects = nil if objects.empty?
|
51
|
+
objects ||= begin
|
52
|
+
data = connection.shell(:powershell) do |s|
|
53
|
+
s.run('Get-WdsClient | ConvertTo-Json -Compress')
|
54
|
+
end.stdout
|
55
|
+
data = '[]' if data.empty?
|
56
|
+
|
57
|
+
underscore_result([JSON.parse(data)].flatten)
|
58
|
+
end
|
59
|
+
|
60
|
+
objects
|
61
|
+
end
|
62
|
+
|
63
|
+
def client(host)
|
64
|
+
clients.find do |c|
|
65
|
+
[host.mac.upcase.tr(':', '-'), host.name].include?(c[:device_id]) || [host.name, host.shortname].include?(c[:device_name])
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def boot_images
|
70
|
+
cache.cache(:boot_images) do
|
71
|
+
images(:boot)
|
72
|
+
end.each { |i| i.wds_server = self }
|
73
|
+
end
|
74
|
+
|
75
|
+
def install_images
|
76
|
+
cache.cache(:install_images) do
|
77
|
+
images(:install)
|
78
|
+
end.each { |i| i.wds_server = self }
|
79
|
+
end
|
80
|
+
|
81
|
+
def create_client(host)
|
82
|
+
raise NotImplementedError, 'Not finished yet'
|
83
|
+
ensure_unattend(host)
|
84
|
+
|
85
|
+
connection.shell(:powershell) do |sh|
|
86
|
+
# New-WdsClient -DeviceID '#{host.mac.upcase.delete ':'}' -DeviceName '#{host.name}' -WdsClientUnattend '#{unattend_file(host)}' -BootImagePath 'boot\\#{wdsify_architecture(host.architecture)}\\images\\#{(host.wds_boot_image || boot_images.first).file_name}' -PxePromptPolicy 'NoPrompt'
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def delete_client(host)
|
91
|
+
raise NotImplementedError, 'Not finished yet'
|
92
|
+
delete_unattend(host)
|
93
|
+
|
94
|
+
connection.shell(:powershell) do |sh|
|
95
|
+
sh.run("Remove-WdsClient -DeviceID '#{host.mac.upcase.delete ':'}'")
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def all_images
|
100
|
+
boot_images + install_images
|
101
|
+
end
|
102
|
+
|
103
|
+
def timezone
|
104
|
+
cache.cache(:timezone) do
|
105
|
+
connection.run_wql('SELECT Bias FROM Win32_TimeZone')[:xml_fragment].first[:bias].to_i * 60
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def shortname
|
110
|
+
cache.cache(:shortname) do
|
111
|
+
connection.run_wql('SELECT Name FROM Win32_ComputerSystem')[:xml_fragment].first[:name]
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def next_server_ip
|
116
|
+
IPSocket.getaddress URI(url).host
|
117
|
+
rescue SocketError
|
118
|
+
::Rails.logger.info "Failed to look up IP of WDS server #{name}"
|
119
|
+
nil
|
120
|
+
end
|
121
|
+
|
122
|
+
def self.bootfile_path(architecture_name, loader = :bios, boot_type = :pxe)
|
123
|
+
file_name = nil
|
124
|
+
if boot_type == :local
|
125
|
+
file_name = 'bootmgfw.efi' if loader == :uefi
|
126
|
+
file_name = 'abortpxe.com' if loader == :bios
|
127
|
+
elsif boot_type == :pxe
|
128
|
+
file_name = 'wdsmgfw.efi' if loader == :uefi
|
129
|
+
file_name = 'wdsnbp.com' if loader == :bios
|
130
|
+
end
|
131
|
+
raise ArgumentError, 'Invalid loader or boot type provided' if file_name.nil?
|
132
|
+
|
133
|
+
"boot\\\\#{architecture_name}\\\\#{file_name}"
|
134
|
+
end
|
135
|
+
|
136
|
+
def self.wdsify_architecture(architecture)
|
137
|
+
wds_arch = ForemanWds::WdsImage::WDS_IMAGE_ARCHES.find_index { |arch| arch =~ architecture.name }
|
138
|
+
ForemanWds::WdsImage::WDS_ARCH_NAMES[wds_arch]
|
139
|
+
end
|
140
|
+
|
141
|
+
def test_connection
|
142
|
+
connection.run_wql('SELECT * FROM Win32_UTCTime').key? :win32_utc_time
|
143
|
+
rescue StandardError
|
144
|
+
false
|
145
|
+
end
|
146
|
+
|
147
|
+
def refresh_cache
|
148
|
+
cache.refresh
|
149
|
+
end
|
150
|
+
|
151
|
+
private
|
152
|
+
|
153
|
+
def unattend_path
|
154
|
+
cache.cache(:unattend_path) do
|
155
|
+
JSON.parse(connection.shell(:powershell) do |sh|
|
156
|
+
sh.run('Get-ItemProperty -Path HKLM:\SYSTEM\CurrentControlSet\Services\WDSServer\Providers\WDSTFTP -Name RootFolder | select RootFolder | ConvertTo-Json -Compress')
|
157
|
+
end, symbolize_names: true)[:RootFolder]
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
def unattend_file(host)
|
162
|
+
"#{unattend_path}\\#{host.mac.tr ':', '_'}.xml"
|
163
|
+
end
|
164
|
+
|
165
|
+
def target_image_for(host)
|
166
|
+
source_image = host.wds_install_image
|
167
|
+
ForemanWds::WdsInstallImage.new(
|
168
|
+
wds_server: self,
|
169
|
+
file_name: "install-#{host.mac.tr ':', '_'}.wim",
|
170
|
+
image_group: SETTINGS[:wds_unattend_group] || source_image.image_group,
|
171
|
+
image_name: "#{source_image.image_name} (specialized for #{host.name}/#{host.mac})"
|
172
|
+
)
|
173
|
+
end
|
174
|
+
|
175
|
+
def ensure_unattend(host)
|
176
|
+
raise NotImplementedException, 'TODO: Not implemented yet'
|
177
|
+
connection.shell(:powershell) do |sh|
|
178
|
+
target_image = target_image_for(host)
|
179
|
+
# TODO: render template, send as heredoc
|
180
|
+
# sh.run("$unattend_render = @'\n#{unattend_template}\n'@")
|
181
|
+
# sh.run("New-Item -Path '#{unattend_file(host)}' -ItemType 'file' -Value $unattend_render")
|
182
|
+
if SETTINGS[:wds_unattend_group]
|
183
|
+
# New-WdsInstallImageGroup -Name #{SETTINGS[:wds_unattend_group]}
|
184
|
+
# Export-WdsInstallImage -ImageGroup <Group> ...
|
185
|
+
# Import-WdsInstallImage -ImageGroup #{SETTINGS[:wds_unattend_group]} -UnattendFile '#{unattend_file(host)}' -OverwriteUnattend ...
|
186
|
+
else
|
187
|
+
source_image = host.wds_facet.install_image
|
188
|
+
|
189
|
+
sh.run("Copy-WdsInstallImage -ImageGroup '#{source_image.image_group}' -FileName '#{source_image.file_name}' -ImageName '#{source_image.image_name}' -NewFileName '#{target_image.file_name}' -NewImageName '#{target_image.image_name}'")
|
190
|
+
sh.run("Set-WdsInstallImage -ImageGroup '#{target_image.image_group}' -FileName '#{target_image.file_name}' -ImageName '#{target_image.image_name}' -DisplayOrder 99999 -UnattendFile '#{unattend_file(host)}' -OverwriteUnattend")
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
def delete_unattend(host)
|
196
|
+
image = target_image_for(host)
|
197
|
+
|
198
|
+
connection.shell(:powershell) do |sh|
|
199
|
+
sh.run("Remove-WdsInstallImage -ImageGroup '#{image.image_group}' -ImageName '#{image.image_name}' -FileName '#{image.file_name}'")
|
200
|
+
sh.run("Remove-Item -Path '#{unattend_file(host)}'")
|
201
|
+
end.errcode.zero?
|
202
|
+
end
|
203
|
+
|
204
|
+
|
205
|
+
def images(type, name = nil)
|
206
|
+
raise ArgumentError, 'Type must be :boot or :install' unless %i[boot install].include? type
|
207
|
+
|
208
|
+
objects = connection.run_wql("SELECT * FROM MSFT_Wds#{type.to_s.capitalize}Image#{" WHERE Name=\"#{name}\"" if name}")["msft_wds#{type}image".to_sym] rescue nil
|
209
|
+
objects = nil if objects.empty?
|
210
|
+
objects ||= underscore_result([JSON.parse(connection.shell(:powershell) do |s|
|
211
|
+
s.run("Get-WDS#{type.to_s.capitalize}Image #{"-ImageName '#{name.sub("'", "`'")}'" if name} | ConvertTo-Json -Compress")
|
212
|
+
end.stdout)].flatten)
|
213
|
+
|
214
|
+
objects.map do |obj|
|
215
|
+
ForemanWds.const_get("Wds#{type.to_s.capitalize}Image").new obj.merge(wds_server: self)
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
def underscore_result(result)
|
220
|
+
case result
|
221
|
+
when Array
|
222
|
+
result.map { |v| underscore_result(v) }
|
223
|
+
when Hash
|
224
|
+
Hash[result.map { |k, v| [k.to_s.underscore.to_sym, underscore_result(v)] }]
|
225
|
+
else
|
226
|
+
result
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
def cache
|
231
|
+
@cache ||= WdsImageCache.new(self)
|
232
|
+
end
|
233
|
+
|
234
|
+
def connection
|
235
|
+
require 'winrm'
|
236
|
+
|
237
|
+
@connection ||= WinRM::Connection.new endpoint: url, transport: :negotiate, user: user, password: password
|
238
|
+
end
|
239
|
+
end
|