setup_oob 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/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
|