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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE +201 -0
- data/README.md +73 -0
- data/bin/setup-oob +137 -0
- data/lib/setup_oob/command/base.rb +91 -0
- data/lib/setup_oob/command/drac.rb +164 -0
- data/lib/setup_oob/command/mixins.rb +270 -0
- data/lib/setup_oob/command/smc.rb +341 -0
- data/lib/setup_oob/oob.rb +130 -0
- data/lib/setup_oob/version.rb +3 -0
- metadata +58 -0
@@ -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
|