1
|
|
|
'''# Miner Helper functions |
2
|
|
|
# Skylake Software, Inc |
3
|
|
|
#"bitmain-fan-ctrl" : true, |
4
|
|
|
#"bitmain-fan-pwm" : "80", |
5
|
|
|
''' |
6
|
|
|
import time |
7
|
|
|
import datetime |
8
|
|
|
import domain.minerstatistics |
9
|
|
|
from helpers.minerapi import MinerApi |
10
|
|
|
from helpers.ssh import Ssh |
11
|
|
|
from domain.mining import Miner, MinerInfo, MinerCurrentPool, MinerAccessLevel, MinerApiCall |
12
|
|
|
|
13
|
|
|
class MinerMonitorException(Exception): |
14
|
|
|
"""Happens during monitoring of miner""" |
15
|
|
|
def friendlymessage(self): |
16
|
|
|
msg = getattr(self, 'message', repr(self)) |
17
|
|
|
return msg |
18
|
|
|
|
19
|
|
|
def istimedout(self): |
20
|
|
|
return self.friendlymessage().find('timed out') >= 0 |
21
|
|
|
def isconnectionrefused(self): |
22
|
|
|
return self.friendlymessage().find('ConnectionRefusedError') >= 0 |
23
|
|
|
|
24
|
|
|
class MinerCommandException(Exception): |
25
|
|
|
"""Happens during executing miner command""" |
26
|
|
|
|
27
|
|
|
def stats(miner: Miner): |
28
|
|
|
'''returns MinerStatistics, MinerInfo, and MinerApiCall''' |
29
|
|
|
if not miner.can_monitor(): |
30
|
|
|
raise MinerMonitorException('miner {0} cannot be monitored. ip={1} port={2}'.format(miner.name, miner.ipaddress, miner.port)) |
31
|
|
|
|
32
|
|
|
try: |
33
|
|
|
thecall = MinerApiCall(miner) |
34
|
|
|
entity = domain.minerstatistics.MinerStatistics(miner, when=datetime.datetime.utcnow()) |
35
|
|
|
api = MinerApi(host=miner.ipaddress, port=int(miner.port)) |
36
|
|
|
|
37
|
|
|
thecall.start() |
38
|
|
|
#jstats = api.stats() |
39
|
|
|
stats_and_pools = api.command('stats+pools') |
40
|
|
|
thecall.stop() |
41
|
|
|
if 'stats' in stats_and_pools: |
42
|
|
|
jstats = stats_and_pools['stats'][0] |
43
|
|
|
else: |
44
|
|
|
#if call failed then only one result is returned, so parse it |
45
|
|
|
jstats = stats_and_pools |
46
|
|
|
entity.rawstats = jstats |
47
|
|
|
jstatus = jstats['STATUS'] |
48
|
|
|
if jstatus[0]['STATUS'] == 'error': |
49
|
|
|
if not miner.is_disabled(): |
50
|
|
|
raise MinerMonitorException(jstatus[0]['description']) |
51
|
|
|
else: |
52
|
|
|
status = jstats['STATS'][0] |
53
|
|
|
jsonstats = jstats['STATS'][1] |
54
|
|
|
#details = jstats['STATS'][1] |
55
|
|
|
|
56
|
|
|
minerinfo = parse_minerinfo(status) |
57
|
|
|
|
58
|
|
|
#build MinerStatistics from stats |
59
|
|
|
parse_statistics(entity, jsonstats, status) |
60
|
|
|
minerpool = parse_minerpool(miner, stats_and_pools['pools'][0]) |
61
|
|
|
|
62
|
|
|
return entity, minerinfo, thecall, minerpool |
63
|
|
|
except BaseException as ex: |
64
|
|
|
print('Failed to call miner stats api: ' + str(ex)) |
65
|
|
|
raise MinerMonitorException(ex) |
66
|
|
|
return None, None, None, None |
67
|
|
|
|
68
|
|
|
def parse_statistics(entity, jsonstats, status): |
69
|
|
|
entity.minercount = int(jsonstats['miner_count']) |
70
|
|
|
entity.elapsed = int(jsonstats['Elapsed']) |
71
|
|
|
entity.currenthash = int(float(jsonstats['GHS 5s'])) |
72
|
|
|
entity.frequency = jsonstats['frequency'] |
73
|
|
|
|
74
|
|
|
frequencies = {k:v for (k, v) in jsonstats.items() if k.startswith('freq_avg') and v != 0} |
75
|
|
|
entity.frequency = str(int(sum(frequencies.values()) / len(frequencies))) |
76
|
|
|
|
77
|
|
|
controllertemps = {k:v for (k, v) in jsonstats.items() if k in ['temp6', 'temp7', 'temp8']} |
78
|
|
|
entity.controllertemp = max(controllertemps.values()) |
79
|
|
|
parse_board_temps(entity, jsonstats) |
80
|
|
|
parse_fans(entity, jsonstats) |
81
|
|
|
parse_board_status(entity, jsonstats) |
82
|
|
|
|
83
|
|
|
def parse_board_status(entity, jsonstats): |
84
|
|
|
chains = {k:v for (k, v) in jsonstats.items() if k.startswith('chain_acs') and v != ''} |
85
|
|
|
chainkeys = list(chains.keys()) |
86
|
|
|
if len(chains) > 0: |
87
|
|
|
entity.boardstatus1 = chains[chainkeys[0]] |
88
|
|
|
if len(chains) > 1: |
89
|
|
|
entity.boardstatus2 = chains[chainkeys[1]] |
90
|
|
|
if len(chains) > 2: |
91
|
|
|
entity.boardstatus3 = chains[chainkeys[2]] |
92
|
|
|
|
93
|
|
|
def parse_fans(entity, jsonstats): |
94
|
|
|
fans = {k:v for (k, v) in jsonstats.items() if k.startswith('fan') and k != 'fan_num' and v != 0} |
95
|
|
|
fankeys = list(fans.keys()) |
96
|
|
|
if len(fans) > 0: |
97
|
|
|
entity.fan1 = fans[fankeys[0]] |
98
|
|
|
if len(fans) > 1: |
99
|
|
|
entity.fan2 = fans[fankeys[1]] |
100
|
|
|
if len(fans) > 2: |
101
|
|
|
entity.fan3 = fans[fankeys[2]] |
102
|
|
|
|
103
|
|
|
def parse_board_temps(entity, jsonstats): |
104
|
|
|
#should be 3 |
105
|
|
|
boardtemps = {k:v for (k, v) in jsonstats.items() if k.startswith('temp2_') and v != 0} |
106
|
|
|
boardtempkeys = list(boardtemps.keys()) |
107
|
|
|
if len(boardtemps) > 0: |
108
|
|
|
entity.tempboard1 = boardtemps[boardtempkeys[0]] |
109
|
|
|
if len(boardtemps) > 1: |
110
|
|
|
entity.tempboard2 = boardtemps[boardtempkeys[1]] |
111
|
|
|
if len(boardtemps) > 2: |
112
|
|
|
entity.tempboard3 = boardtemps[boardtempkeys[2]] |
113
|
|
|
|
114
|
|
|
def parse_minerinfo(status): |
115
|
|
|
#build MinerInfo from stats |
116
|
|
|
minerid = 'unknown' |
117
|
|
|
minertype = 'unknown' |
118
|
|
|
if 'Type' in status: |
119
|
|
|
minertype = status['Type'] |
120
|
|
|
else: |
121
|
|
|
if status['Description'].startswith('cgminer'): |
122
|
|
|
minertype = status['Description'] |
123
|
|
|
if 'miner_id' in status: |
124
|
|
|
minerid = status['miner_id'] |
125
|
|
|
minerinfo = MinerInfo(minertype, minerid) |
126
|
|
|
return minerinfo |
127
|
|
|
|
128
|
|
|
def pools(miner: Miner): |
129
|
|
|
'''Gets the current pool for the miner''' |
130
|
|
|
try: |
131
|
|
|
api = MinerApi(host=miner.ipaddress, port=int(miner.port)) |
132
|
|
|
jstatuspools = api.pools() |
133
|
|
|
if jstatuspools['STATUS'][0]['STATUS'] == 'error': |
134
|
|
|
if not miner.is_disabled(): |
135
|
|
|
raise MinerMonitorException(jstatuspools['STATUS'][0]['description']) |
136
|
|
|
else: |
137
|
|
|
return parse_minerpool(miner, jstatuspools) |
138
|
|
|
except BaseException as ex: |
139
|
|
|
print('Failed to call miner pools api: ' + str(ex)) |
140
|
|
|
return None |
141
|
|
|
|
142
|
|
|
def parse_minerpool(miner, jstatuspools): |
143
|
|
|
def sort_by_priority(j): |
144
|
|
|
return j['Priority'] |
145
|
|
|
|
146
|
|
|
jpools = jstatuspools["POOLS"] |
147
|
|
|
#sort by priority |
148
|
|
|
jpools.sort(key=sort_by_priority) |
149
|
|
|
#try to do elegant way, but not working |
150
|
|
|
#cPool = namedtuple('Pool', 'POOL, URL, Status,Priority,Quota,Getworks,Accepted,Rejected,Long Poll') |
151
|
|
|
#colpools = [cPool(**k) for k in jsonpools["POOLS"]] |
152
|
|
|
#for pool in colpools: |
153
|
|
|
# print(pool.POOL) |
154
|
|
|
currentpool = currentworker = '' |
155
|
|
|
for pool in jpools: |
156
|
|
|
if str(pool["Status"]) == "Alive": |
157
|
|
|
currentpool = pool["URL"] |
158
|
|
|
currentworker = pool["User"] |
159
|
|
|
#print("{0} {1} {2} {3} {4} {5}".format(pool["POOL"],pool["Priority"],pool["URL"],pool["User"],pool["Status"],pool["Stratum Active"])) |
160
|
|
|
break |
161
|
|
|
minerpool = MinerCurrentPool(miner, currentpool, currentworker, jstatuspools) |
162
|
|
|
return minerpool |
163
|
|
|
|
164
|
|
|
def getminerlcd(miner: Miner): |
165
|
|
|
'''gets a summary (quick display values) for the miner''' |
166
|
|
|
try: |
167
|
|
|
api = MinerApi(host=miner.ipaddress, port=int(miner.port)) |
168
|
|
|
jstatuspools = api.lcd() |
169
|
|
|
return jstatuspools |
170
|
|
|
except BaseException as ex: |
171
|
|
|
return str(ex) |
172
|
|
|
|
173
|
|
|
def setminertoprivileged(miner, login, allowsetting): |
174
|
|
|
try: |
175
|
|
|
mineraccess = privileged(miner) |
176
|
|
|
except MinerMonitorException as ex: |
177
|
|
|
if ex.istimedout(): |
178
|
|
|
mineraccess = MinerAccessLevel.Waiting |
179
|
|
|
if mineraccess == MinerAccessLevel.Restricted or mineraccess == MinerAccessLevel.Waiting: |
180
|
|
|
if mineraccess == MinerAccessLevel.Restricted: |
181
|
|
|
setprivileged(miner, login, allowsetting) |
182
|
|
|
#todo: not ideal to wait in a loop here, need a pattern that will wait in non blocking mode |
183
|
|
|
waitforonline(miner) |
184
|
|
|
mineraccess = privileged(miner) |
185
|
|
|
return mineraccess |
186
|
|
|
|
187
|
|
|
def privileged(miner: Miner): |
188
|
|
|
'''gets status: privileged or restricted''' |
189
|
|
|
api = MinerApi(host=miner.ipaddress, port=int(miner.port)) |
190
|
|
|
apiresponse = api.privileged() |
191
|
|
|
jstatus = apiresponse["STATUS"][0] |
192
|
|
|
if jstatus is not None and jstatus["STATUS"] == "S": |
193
|
|
|
return MinerAccessLevel.Privileged |
194
|
|
|
return MinerAccessLevel.Restricted |
195
|
|
|
|
196
|
|
|
#restart (*) none Status is a single "RESTART" reply before cgminer restarts |
197
|
|
|
def restart(miner: Miner): |
198
|
|
|
'''restart miner through api''' |
199
|
|
|
api = MinerApi(host=miner.ipaddress, port=int(miner.port)) |
200
|
|
|
apiresponse = api.restart() |
201
|
|
|
print(apiresponse) |
202
|
|
|
return apiresponse |
203
|
|
|
|
204
|
|
|
def print_connection_data(connection): |
205
|
|
|
if connection.strdata: |
206
|
|
|
print(connection.strdata) # print the last line of received data |
207
|
|
|
print('==========================') |
208
|
|
|
if connection.alldata: |
209
|
|
|
print(connection.alldata) # This contains the complete data received. |
210
|
|
|
print('==========================') |
211
|
|
|
|
212
|
|
|
def print_response(response): |
213
|
|
|
for line in response: |
214
|
|
|
print(line) |
215
|
|
|
|
216
|
|
|
def getportfromminer(miner: Miner): |
217
|
|
|
if isinstance(miner.ftpport, int): |
218
|
|
|
return miner.ftpport |
219
|
|
|
if isinstance(miner.ftpport, str) and miner.ftpport: |
220
|
|
|
tryport = int(miner.ftpport) |
221
|
|
|
if tryport > 0: |
222
|
|
|
return tryport |
223
|
|
|
return 22 |
224
|
|
|
|
225
|
|
|
def getminerconfig(miner: Miner, login): |
226
|
|
|
'''ger the miner config file''' |
227
|
|
|
connection = Ssh(miner.ipaddress, login.username, login.password, port=getportfromminer(miner)) |
228
|
|
|
config = connection.exec_command('cat /config/{0}.conf'.format(getminerfilename(miner))) |
229
|
|
|
connection.close_connection() |
230
|
|
|
return config |
231
|
|
|
|
232
|
|
|
def stopmining(miner: Miner, login): |
233
|
|
|
miner_shell_command(miner, login, 'restart', 15) |
234
|
|
|
|
235
|
|
|
def restartmining(miner: Miner, login): |
236
|
|
|
miner_shell_command(miner, login, 'restart', 15) |
237
|
|
|
|
238
|
|
|
def miner_shell_command(miner: Miner, login, command, timetorun): |
239
|
|
|
'''send the command stop/restart to miner shell command''' |
240
|
|
|
connection = Ssh(miner.ipaddress, login.username, login.password, port=getportfromminer(miner)) |
241
|
|
|
connection.open_shell() |
242
|
|
|
connection.send_shell('/etc/init.d/{0}.sh {1}'.format(getminerfilename(miner), command)) |
243
|
|
|
time.sleep(timetorun) |
244
|
|
|
print_connection_data(connection) |
245
|
|
|
connection.close_connection() |
246
|
|
|
|
247
|
|
|
def changesshpassword(miner: Miner, oldlogin, newlogin): |
248
|
|
|
""" change the factory ssh password """ |
249
|
|
|
if newlogin.username != oldlogin.username: |
250
|
|
|
print("changesshpassword: currently username change is not supported. only change password") |
251
|
|
|
return |
252
|
|
|
connection = Ssh(miner.ipaddress, oldlogin.username, oldlogin.password, port=getportfromminer(miner)) |
253
|
|
|
connection.open_shell() |
254
|
|
|
connection.send_shell('echo "{0}:{1}" | chpasswd'.format(newlogin.username, newlogin.password)) |
255
|
|
|
time.sleep(5) |
256
|
|
|
print_connection_data(connection) |
257
|
|
|
connection.close_connection() |
258
|
|
|
|
259
|
|
|
def reboot(miner: Miner, login): |
260
|
|
|
""" reboot the miner through ftp """ |
261
|
|
|
connection = Ssh(miner.ipaddress, login.username, login.password, port=getportfromminer(miner)) |
262
|
|
|
connection.open_shell() |
263
|
|
|
response = connection.exec_command('/sbin/reboot') |
264
|
|
|
print_connection_data(connection) |
265
|
|
|
connection.close_connection() |
266
|
|
|
return response |
267
|
|
|
|
268
|
|
|
def shutdown(miner: Miner, login): |
269
|
|
|
""" shutdown the miner through ftp |
270
|
|
|
Warning! this will not turn off the power to the machine! |
271
|
|
|
It will only shut down the operating system. Machine will still be consuming power if power supply |
272
|
|
|
does not have on/off switch. You will have to manually restart the machine. |
273
|
|
|
""" |
274
|
|
|
connection = Ssh(miner.ipaddress, login.username, login.password, port=getportfromminer(miner)) |
275
|
|
|
connection.open_shell() |
276
|
|
|
connection.send_shell('/sbin/poweroff') |
277
|
|
|
time.sleep(5) |
278
|
|
|
print_connection_data(connection) |
279
|
|
|
connection.close_connection() |
280
|
|
|
|
281
|
|
|
def waitforonline(miner: Miner): |
282
|
|
|
#poll miner until it comes up, returns MinerInfo or None for timeout |
283
|
|
|
cnt = 60 |
284
|
|
|
sleeptime = 5 |
285
|
|
|
minerinfo = None |
286
|
|
|
while cnt > 0: |
287
|
|
|
try: |
288
|
|
|
minerstats, minerinfo, apicall, minerpool = stats(miner) |
289
|
|
|
return minerinfo |
290
|
|
|
except MinerMonitorException as ex: |
291
|
|
|
if not ex.istimedout() and not ex.isconnectionrefused(): |
292
|
|
|
raise ex |
293
|
|
|
else: |
294
|
|
|
print(ex.friendlymessage()) |
295
|
|
|
|
296
|
|
|
if minerinfo is not None: |
297
|
|
|
if minerinfo.miner_type: |
298
|
|
|
print(' found {0} {1}'.format(minerinfo.miner_type, minerinfo.minerid)) |
299
|
|
|
cnt = 0 |
300
|
|
|
if cnt > 0: |
301
|
|
|
cnt -= sleeptime |
302
|
|
|
print('Waiting for {0}...'.format(miner.name)) |
303
|
|
|
time.sleep(sleeptime) |
304
|
|
|
return None |
305
|
|
|
|
306
|
|
|
#The 'poolpriority' command can be used to reset the priority order of multiple |
307
|
|
|
#pools with a single command - 'switchpool' only sets a single pool to first priority |
308
|
|
|
#Each pool should be listed by id number in order of preference (first = most preferred) |
309
|
|
|
#Any pools not listed will be prioritised after the ones that are listed, in the |
310
|
|
|
#priority order they were originally |
311
|
|
|
#If the priority change affects the miner's preference for mining, it may switch immediately |
312
|
|
|
def switch(miner: Miner, poolnumber): |
313
|
|
|
api = MinerApi(host=miner.ipaddress, port=int(miner.port)) |
314
|
|
|
jswitch = api.switchpool("{0}".format(poolnumber)) |
315
|
|
|
print(jswitch["STATUS"][0]["Msg"]) |
316
|
|
|
|
317
|
|
|
#addpool|URL,USR,PASS (*) |
318
|
|
|
# none There is no reply section just the STATUS section |
319
|
|
|
# stating the results of attempting to add pool N |
320
|
|
|
# The Msg includes the pool number and URL |
321
|
|
|
# Use '\\' to get a '\' and '\,' to include a comma |
322
|
|
|
# inside URL, USR or PASS |
323
|
|
|
def addpool(miner: Miner, pool): |
324
|
|
|
""" Add a pool to a miner. Allows user to select it from drop down and easily switch to it """ |
325
|
|
|
api = MinerApi(host=miner.ipaddress, port=int(miner.port)) |
326
|
|
|
jaddpool = api.addpool("{0},{1},{2}".format(pool.url, pool.user, "x")) |
327
|
|
|
return jaddpool["STATUS"][0]["Msg"] |
328
|
|
|
|
329
|
|
|
def getminerfilename(miner: Miner): |
330
|
|
|
'''cgminer for D3 and A3''' |
331
|
|
|
minerfilename = 'cgminer' |
332
|
|
|
if miner.miner_type.startswith('Antminer S9'): |
333
|
|
|
minerfilename = 'bmminer' |
334
|
|
|
return minerfilename |
335
|
|
|
|
336
|
|
|
def change_setting(settingkey, newvalue): |
337
|
|
|
'''todo:there is bug here if updating the last line of config file! command (,) not needed at end''' |
338
|
|
|
return "sed -i \'s_^\\(\"{0}\" : \\).*_\\1\"{1}\",_\'".format(settingkey, newvalue) |
339
|
|
|
|
340
|
|
|
def get_changeconfigcommands(configfilename, setting, newvalue): |
341
|
|
|
commands = [] |
342
|
|
|
commands.append('cd /config') |
343
|
|
|
commands.append('cp {0}.conf {1}_last.conf'.format(configfilename, configfilename)) |
344
|
|
|
commands.append('chmod u=rw {0}.conf'.format(configfilename)) |
345
|
|
|
commands.append("{0} {1}.conf".format(change_setting(setting, newvalue), configfilename)) |
346
|
|
|
commands.append('chmod u=r {0}.conf'.format(configfilename)) |
347
|
|
|
return commands |
348
|
|
|
|
349
|
|
|
def sendcommands_and_restart(miner: Miner, login, commands): |
350
|
|
|
stopmining(miner, login) |
351
|
|
|
try: |
352
|
|
|
connection = Ssh(miner.ipaddress, login.username, login.password, port=getportfromminer(miner)) |
353
|
|
|
connection.open_shell() |
354
|
|
|
for cmd in commands: |
355
|
|
|
connection.send_shell(cmd) |
356
|
|
|
time.sleep(5) |
357
|
|
|
print_connection_data(connection) |
358
|
|
|
connection.close_connection() |
359
|
|
|
finally: |
360
|
|
|
restartmining(miner, login) |
361
|
|
|
|
362
|
|
|
def setprivileged(miner: Miner, login, allowsetting): |
363
|
|
|
""" Set miner to privileged mode """ |
364
|
|
|
commands = get_changeconfigcommands(getminerfilename(miner), 'api-allow', allowsetting) |
365
|
|
|
sendcommands_and_restart(miner, login, commands) |
366
|
|
|
|
367
|
|
|
def setrestricted(miner: Miner, login, allowsetting): |
368
|
|
|
""" Set miner to restricted mode """ |
369
|
|
|
commands = get_changeconfigcommands(getminerfilename(miner), 'api-allow', allowsetting) |
370
|
|
|
sendcommands_and_restart(miner, login, commands) |
371
|
|
|
|
372
|
|
|
def set_frequency(miner: Miner, login, frequency): |
373
|
|
|
""" Set miner frequency |
374
|
|
|
Does not work for S9 with auto tune. |
375
|
|
|
Fixed frequency firmware (650m) has to be loaded first before frequency can be adjusted |
376
|
|
|
""" |
377
|
|
|
#default for S9 is 550 |
378
|
|
|
#"bitmain-freq" : "550", |
379
|
|
|
commands = get_changeconfigcommands(getminerfilename(miner), 'bitmain-freq', frequency) |
380
|
|
|
sendcommands_and_restart(miner, login, commands) |
381
|
|
|
|
382
|
|
|
def load_firmware(): |
383
|
|
|
""" |
384
|
|
|
TODO: load firmware file |
385
|
|
|
this will probably change the ip address of the miner |
386
|
|
|
""" |
387
|
|
|
pass |
388
|
|
|
|
389
|
|
|
def load_miner(miner, login): |
390
|
|
|
""" |
391
|
|
|
change the miner software |
392
|
|
|
""" |
393
|
|
|
#ftp the new miner |
394
|
|
|
commands = [] |
395
|
|
|
commands.append('cd /usr/bin') |
396
|
|
|
commands.append('cp bmminer bmminer.original') |
397
|
|
|
commands.append('cp bmminer880 bmminer') |
398
|
|
|
commands.append('chmod +x bmminer') |
399
|
|
|
sendcommands_and_restart(miner, login, commands) |
400
|
|
|
|
401
|
|
|
def file_upload(miner, login, localfile, remotefile): |
402
|
|
|
connection = Ssh(miner.ipaddress, login.username, login.password, port=getportfromminer(miner)) |
403
|
|
|
connection.put(localfile, remotefile) |
404
|
|
|
|
405
|
|
|
def file_download(miner, login, localfile, remotefile): |
406
|
|
|
connection = Ssh(miner.ipaddress, login.username, login.password, port=getportfromminer(miner)) |
407
|
|
|
connection.get(localfile, remotefile) |
408
|
|
|
|