setup_oob 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,270 @@
1
+ # vim: syntax=ruby:expandtab:shiftwidth=2:softtabstop=2:tabstop=2
2
+
3
+ # Copyright 2021-present Vicarious
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ require 'ipaddress'
18
+
19
+ # Mixins for the stuff that's common to a given command regardless
20
+ # of the platform
21
+ module CommandMixins
22
+ module Hostname
23
+ def _converged?
24
+ hn = hostname
25
+ logger.debug("'#{hn}' vs '#{desired_hostname}'")
26
+ hn == desired_hostname
27
+ end
28
+
29
+ def _converge!
30
+ unless _converged?
31
+ logger.info(" - Setting hostname (#{desired_hostname})")
32
+ set_hostname
33
+ end
34
+ end
35
+
36
+ def desired_hostname
37
+ @data
38
+ end
39
+ end
40
+
41
+ module Ntp
42
+ def _converged?
43
+ servers_correct = true
44
+ servers.each_with_index do |server, idx|
45
+ res = get_server(idx) == server
46
+ logger.debug(" - NTP#{idx + 1} correct: #{res}")
47
+ servers_correct &&= res
48
+ end
49
+ enabled? && servers_correct
50
+ end
51
+
52
+ def _converge!
53
+ logger.debug(' - Checking if enabled')
54
+ unless enabled?
55
+ logger.info(' - Enabling NTP')
56
+ enable
57
+ end
58
+ servers.each_with_index do |server, idx|
59
+ logger.debug(" - Checking if NTP#{idx + 1} is correct")
60
+ unless get_server(idx) == server
61
+ logger.info(" - Setting NTP#{idx + 1} server")
62
+ set_server(idx)
63
+ end
64
+ end
65
+ end
66
+
67
+ def servers
68
+ @data
69
+ end
70
+ end
71
+
72
+ module Networkmode
73
+ def _converged?
74
+ mode_correct?
75
+ end
76
+
77
+ def desired_mode
78
+ @data
79
+ end
80
+
81
+ def _converge!
82
+ unless mode_correct?
83
+ logger.info(" - Setting network to #{desired_mode}")
84
+ set_mode
85
+ end
86
+ end
87
+ end
88
+
89
+ module Ddns
90
+ def _converged?
91
+ enabled?
92
+ end
93
+
94
+ def _converge!
95
+ unless enabled?
96
+ logger.info(' - Enabling DDNS')
97
+ enable
98
+ end
99
+ end
100
+ end
101
+
102
+ module Networksrc
103
+ def desired_mode
104
+ @data == 'dhcp' ? 'dhcp' : 'static'
105
+ end
106
+
107
+ def desired_address
108
+ if @data == 'dhcp'
109
+ fail 'Set to DHCP, but looking for address, what?'
110
+ end
111
+
112
+ IPAddress(@data).address
113
+ end
114
+
115
+ def desired_netmask
116
+ if @data == 'dhcp'
117
+ fail 'Set to DHCP, but looking for address, what?'
118
+ end
119
+
120
+ IPAddress(@data).netmask
121
+ end
122
+
123
+ def mode
124
+ x = current['IP Address Source']
125
+ case x
126
+ when /DHCP/
127
+ 'dhcp'
128
+ when /Static/
129
+ 'static'
130
+ else
131
+ 'other'
132
+ end
133
+ end
134
+
135
+ def address
136
+ current['IP Address']
137
+ end
138
+
139
+ def netmask
140
+ current['Subnet Mask']
141
+ end
142
+
143
+ def current
144
+ return @current if @current
145
+
146
+ @current = {}
147
+ cmd = ipmicmd + ['lan', 'print', '1']
148
+ s = run(cmd)
149
+ s.stdout.each_line do |line|
150
+ key, val = line.chomp.split(':')
151
+ @current[key.strip] = val.strip
152
+ end
153
+ @current
154
+ end
155
+
156
+ def _converged?
157
+ mode? && address?
158
+ end
159
+
160
+ def address?
161
+ if desired_mode == 'static'
162
+ logger.debug(' - Checking of address is set')
163
+ logger.debug("'#{@data}' vs '#{@data}'")
164
+ return address == desired_address || netmask == desired_netmask
165
+ end
166
+ true
167
+ end
168
+
169
+ def mode?
170
+ logger.debug(" - Checking if network src mode set to #{desired_mode}")
171
+ mode == desired_mode
172
+ end
173
+
174
+ def _converge!
175
+ unless mode?
176
+ logger.info(" - Setting network src to #{desired_mode}")
177
+ set_mode
178
+ end
179
+ if desired_mode == 'static'
180
+ logger.info(' - Checking address')
181
+ if address != desired_address
182
+ logger.info(" - Setting network address to #{desired_address}")
183
+ set_address
184
+ end
185
+ if netmask != desired_netmask
186
+ logger.info(" - Setting network mask to #{desired_netmask}")
187
+ set_netmask
188
+ end
189
+ end
190
+ end
191
+
192
+ def smc?
193
+ @smc ||= self.class.to_s.start_with?('SMC')
194
+ end
195
+
196
+ def set_mode
197
+ cmd = ipmicmd(true) + ['lan', 'set', '1', 'ipsrc', desired_mode]
198
+ run(cmd)
199
+ end
200
+
201
+ def set_address
202
+ cmd = ipmicmd(true) + ['lan', 'set', '1', 'ipaddr', desired_address]
203
+ run(cmd)
204
+ end
205
+
206
+ def set_netmask
207
+ cmd = ipmicmd(true) + ['lan', 'set', '1', 'netmask', desired_netmask]
208
+ run(cmd)
209
+ end
210
+
211
+ def ipmicmd(set = false)
212
+ smc? ? basecmd(set) : SMCCommandBase.basecmd('localhost')
213
+ end
214
+ end
215
+
216
+ module Password
217
+ def password
218
+ @data
219
+ end
220
+
221
+ def _converged?
222
+ password_set?
223
+ end
224
+
225
+ def _converge!
226
+ unless password_set?
227
+ logger.info(' - Setting password')
228
+ set_password
229
+ end
230
+ end
231
+
232
+ def smc?
233
+ @smc ||= self.class.to_s.start_with?('SMC')
234
+ end
235
+
236
+ # It turns out that 'ipmitool user test' and 'ipmitool user set'
237
+ # work fairly unversally. And there's no way to do 'user test'
238
+ # in racadm. So this should work pretty much everywhere.
239
+ #
240
+ # Though... 'ipmitool user list', for some reason, is not so
241
+ # universal. Womp Womp.
242
+ def password_set?
243
+ id = admin_id
244
+ cmd = ipmicmd + ['user', 'test', id, smc? ? '20' : '16', password]
245
+ s = run(cmd, false)
246
+ !s.error?
247
+ end
248
+
249
+ def set_password
250
+ id = admin_id
251
+ cmd = ipmicmd(true) + ['user', 'set', 'password', id, password]
252
+ s = run(cmd)
253
+ if s.error?
254
+ # if the password isn't what we expect *and* isn't ADMIN, it may be
255
+ # serial or an old password. We can reset it to ADMIN
256
+ # NOTE: this actually resets ALL it's settings. Which is OK because
257
+ # we set password first, so the rest will get set properly
258
+ run(ipmicmd(true) + ['raw', '0x30', '0x48', '0x1'])
259
+ # takes about seconds for it to come to its senses
260
+ sleep(5)
261
+ # now, try again
262
+ run(cmd)
263
+ end
264
+ end
265
+
266
+ def ipmicmd(set = false)
267
+ smc? ? basecmd(set) : SMCCommandBase.basecmd('localhost')
268
+ end
269
+ end
270
+ end
@@ -0,0 +1,341 @@
1
+ # vim: syntax=ruby:expandtab:shiftwidth=2:softtabstop=2:tabstop=2
2
+
3
+ # Copyright 2021-present Vicarious
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ require 'openssl'
18
+ require_relative 'base'
19
+ require_relative 'mixins'
20
+
21
+ # A slight extension of the Command class to add some SMC-specific
22
+ # utility functions. Will be the base class for all SMC commands
23
+ class SMCCommandBase < CommandBase
24
+ DEFAULT_PASSWORD = 'ADMIN'.freeze
25
+ USER = 'ADMIN'.freeze
26
+ COMMANDS = {
27
+ :hostname => [0x47],
28
+ :ntp => [0x68, 0x01],
29
+ :ddns => [0x68, 0x04],
30
+ :networkmode => [0x70, 0x0c],
31
+ :isactivated => [0x6A],
32
+ :setlicense => [0x69],
33
+ # some magic set of bytes for firmware version and mac ...
34
+ }.freeze
35
+
36
+ ACTIONS = {
37
+ :get => [0x00],
38
+ :set => [0x01],
39
+ }.freeze
40
+
41
+ def enabled?
42
+ # assumes a sub-classed cmdbytes
43
+ data = cmdbytes(:get, :enabled)
44
+ out = runraw(data)
45
+ # if the first byte is 1, "it" is enabled, whatever "it" is
46
+ en = out[0] == 1
47
+ logger.debug("#{pretty_self} enabled: #{en}")
48
+ en
49
+ end
50
+
51
+ # Get the IPMI bytes for the command we want to run
52
+ def cmdbytes(command, action = nil)
53
+ data = [0x30] + COMMANDS[command]
54
+ if action
55
+ data += ACTIONS[action]
56
+ end
57
+ data
58
+ end
59
+
60
+ # Wrapper to build the "raw" command, run it, and convert the answer into
61
+ # an array of actual integers.
62
+ def runraw(data)
63
+ s = run(rawcmd(data))
64
+ s.stdout.split.map { |x| x.to_i(16) }
65
+ end
66
+
67
+ # Builds the 'raw' command using 'data'
68
+ def rawcmd(data)
69
+ basecmd + ['raw'] + data.map(&:to_s)
70
+ end
71
+
72
+ def basecmd(defaultpass = false)
73
+ SMCCommandBase.basecmd(@host, @user, defaultpass ? 'ADMIN' : @password)
74
+ end
75
+
76
+ def self.basecmd(host, user = nil, pass = nil)
77
+ if host == 'localhost'
78
+ ['ipmitool']
79
+ else
80
+ unless user && pass
81
+ fail 'basecmd: No user and password sent with host'
82
+ end
83
+
84
+ [
85
+ 'ipmitool',
86
+ '-H', host,
87
+ '-U', user,
88
+ '-P', pass
89
+ ]
90
+ end
91
+ end
92
+ end
93
+
94
+ # The collection of all SMC command classes
95
+ class SMCCommands
96
+ class Hostname < SMCCommandBase
97
+ private
98
+
99
+ include CommandMixins::Hostname
100
+
101
+ def desired_hostname
102
+ @data
103
+ end
104
+
105
+ def hostname
106
+ # for hostname get is 0x02 and set is 0x01. no idea why
107
+ data = cmdbytes(:hostname) + [0x02]
108
+ out = runraw(data)
109
+ bytes_to_str(out)
110
+ end
111
+
112
+ def set_hostname
113
+ # no terminating null for this command...
114
+ data = cmdbytes(:hostname) + [0x01] + desired_hostname.bytes
115
+ runraw(data)
116
+ end
117
+ end
118
+
119
+ class Ntp < SMCCommandBase
120
+ SUB_COMMANDS = {
121
+ :enabled => [0x00],
122
+ :primary => [0x01],
123
+ :secondary => [0x02],
124
+ }.freeze
125
+
126
+ TYPES = [
127
+ :primary,
128
+ :secondary,
129
+ ].freeze
130
+
131
+ private
132
+
133
+ include CommandMixins::Ntp
134
+
135
+ def cmdbytes(action, subcmd)
136
+ super(:ntp, action) + SUB_COMMANDS[subcmd]
137
+ end
138
+
139
+ def enabled?
140
+ data = cmdbytes(:get, :enabled)
141
+ out = runraw(data)
142
+ en = out[0] == 1
143
+ # when you check if NTP is enabled, a bunch of extra bytes
144
+ # are passed back that MUST be passed in when enabling NTP
145
+ @_magic = out[1..-1]
146
+ logger.debug("NTP enabled: #{en}, magic bytes: #{@_magic}")
147
+ en
148
+ end
149
+
150
+ def enable
151
+ # 0x01 is "enable"
152
+ data = cmdbytes(:set, :enabled) + [0x01] + @_magic
153
+ runraw(data)
154
+ end
155
+
156
+ def get_server(idx)
157
+ data = cmdbytes(:get, type(idx))
158
+ bytes_to_str(runraw(data))
159
+ end
160
+
161
+ def type(idx)
162
+ TYPES[idx]
163
+ end
164
+
165
+ def set_server(idx)
166
+ name_bytes = servers[idx].bytes
167
+ data = cmdbytes(:set, type(idx)) + name_bytes + [0x00] # null termination
168
+ bytes_to_str(runraw(data))
169
+ end
170
+ end
171
+
172
+ class Networksrc < SMCCommandBase
173
+ include CommandMixins::Networksrc
174
+ end
175
+
176
+ class Networkmode < SMCCommandBase
177
+ private
178
+
179
+ MODES = {
180
+ :dedicated => 0x00,
181
+ :shared => 0x01,
182
+ :failover => 0x02,
183
+ }.freeze
184
+
185
+ include CommandMixins::Networkmode
186
+
187
+ def mode_val
188
+ val = MODES[desired_mode.to_sym]
189
+ unless val
190
+ fail "No such mode #{desired_mode}"
191
+ end
192
+
193
+ val
194
+ end
195
+
196
+ def cmdbytes(action, _subcmd = nil)
197
+ super(:networkmode, action)
198
+ end
199
+
200
+ def mode_correct?
201
+ data = cmdbytes(:get)
202
+ out = runraw(data)[0]
203
+ logger.debug("mode: #{out}")
204
+ out == mode_val
205
+ end
206
+
207
+ def set_mode
208
+ data = cmdbytes(:set) + [mode_val]
209
+ runraw(data)
210
+ end
211
+ end
212
+
213
+ class Ddns < SMCCommandBase
214
+ SUB_COMMANDS = {
215
+ :enabled => [0x00],
216
+ }.freeze
217
+
218
+ private
219
+
220
+ include CommandMixins::Ddns
221
+
222
+ def cmdbytes(action = nil, type = nil)
223
+ # this one is backwards... I dunno why
224
+ data = super(:ddns)
225
+ if action && type
226
+ data += SUB_COMMANDS[type] + ACTIONS[action]
227
+ end
228
+ data
229
+ end
230
+
231
+ def enable
232
+ # this seems to put it in some sort of setting mode...
233
+ data = cmdbytes(:set, :enabled)
234
+ runraw(data)
235
+ data = cmdbytes
236
+ # then you do this crazy magic...
237
+ data += [0x01, 0x01, 0x00, 0x7F, 0x00, 0x00, 0x01] +
238
+ '#host.#domain'.bytes + [0x00]
239
+ runraw(data)
240
+ end
241
+ end
242
+
243
+ # Manage the licenses
244
+ #
245
+ # READ CAREFULLY!!
246
+ # Generating licenses if you haven't purchased them is illegal!
247
+ # This is here for convenience since the license is derivable from
248
+ # the MAC address, if you have the keys. This allows activating
249
+ # the license in an automated fashion.
250
+ #
251
+ # I make no claims about the legality of having the key, it probably
252
+ # depends on what country you are in. The key is not distributed with
253
+ # this software. But you can find it if you want.
254
+ #
255
+ class License < SMCCommandBase
256
+ def key
257
+ @data
258
+ end
259
+
260
+ def _converged?
261
+ licensed?
262
+ end
263
+
264
+ def converge!
265
+ unless licensed?
266
+ logger.info(' - Setting license')
267
+ set_license
268
+ end
269
+ end
270
+
271
+ private
272
+
273
+ # Thanks Peter Kleissner for documenting this.
274
+ def generate_license
275
+ digest = OpenSSL::Digest.new('sha1')
276
+ data = mac.split(':').map { |x| x.to_i(16).chr }.join
277
+ raw = OpenSSL::HMAC.digest(digest, key, data)
278
+ raw_lic = raw.chars[0..11].map(&:ord)
279
+ fmt = "%02X%02X-%02X%02X-%02X%02X-%02X%02X-%02X%02X-%02X%02X\n"
280
+ logger.debug("Generated license: #{raw_lic}")
281
+ logger.debug("Human readable license: #{fmt % raw_lic}")
282
+ raw_lic
283
+ end
284
+
285
+ def licensed?
286
+ data = cmdbytes(:isactivated)
287
+ s = runraw(data)
288
+ # There's only one byte, and it's non-zero if we're activated
289
+ en = s[0].positive?
290
+ logger.debug("Activated: #{en}")
291
+ en
292
+ end
293
+
294
+ def mac
295
+ # does not use `cmdbytes` because this is the raw sequence of
296
+ # bytes and not inside of the "0x30' netfn that other commands
297
+ # are
298
+ data = [0x0C, 0x02, 0x01, 0x05, 0x00, 0x00]
299
+ # we want actual hex strings, so use run directly
300
+ s = run(rawcmd(data))
301
+ hex = s.stdout.split
302
+ # first byte is version, I think
303
+ hex.shift
304
+ mac = hex.join(':')
305
+ logger.debug("Mac is #{mac}")
306
+ mac
307
+ end
308
+
309
+ def set_license
310
+ lic = generate_license
311
+ data = cmdbytes(:setlicense) + lic
312
+ s = runraw(data)
313
+ unless s[0].zero?
314
+ fail 'Failed to set license key'
315
+ end
316
+ end
317
+ end
318
+
319
+ class Password < SMCCommandBase
320
+ private
321
+
322
+ include CommandMixins::Password
323
+
324
+ # Find the user id of the ADMIN user. Usually 2, but I
325
+ # didn't want to hard-code that.
326
+ def admin_id
327
+ return @admin_id if @admin_id
328
+
329
+ s = run(basecmd + ['user', 'list'])
330
+ s.stdout.each_line do |line|
331
+ next if line.start_with?('ID')
332
+
333
+ bits = line.split
334
+ if bits[1] == 'ADMIN'
335
+ @admin_id = bits[0]
336
+ return @admin_id
337
+ end
338
+ end
339
+ end
340
+ end
341
+ end