foreman_wds 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|