setup_oob 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.
@@ -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