Passed
Push — master ( 9f74b6...e21377 )
by Dave
01:04
created

helpers.antminerhelper.set_frequency()   A

Complexity

Conditions 1

Size

Total Lines 9
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nop 3
dl 0
loc 9
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
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