helpers.antminerhelper.print_connection_data()   A
last analyzed

Complexity

Conditions 3

Size

Total Lines 7
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 7
nop 1
dl 0
loc 7
rs 10
c 0
b 0
f 0
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
The variable helpers does not seem to be defined.
Loading history...
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
introduced by
The variable minerpool does not seem to be defined for all execution paths.
Loading history...
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