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