Passed
Push — master ( 93c134...fbe99c )
by Dave
01:10
created

domain.mining.Miner.currentpoolname()   A

Complexity

Conditions 2

Size

Total Lines 5
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nop 1
dl 0
loc 5
rs 10
c 0
b 0
f 0
1
'''#Full Cycle Mining Domain'''
2
import time
3
from datetime import datetime, timezone
4
5
class MinerStatus:
6
    '''Status of Miner'''
7
    Online = 'online'
8
    Offline = 'offline'
9
    Disabled = 'disabled'
10
11
class MinerAccessLevel:
12
    Restricted = 'restricted'
13
    Privileged = 'priviledged'
14
    #not really a level, waiting for access upgrade
15
    Waiting = 'waiting'
16
17
class Login(object):
18
    """Login name and password for access to miners"""
19
    def __init__(self, username, password):
20
        self.username = username
21
        self.password = password
22
23
class MinerInfo(object):
24
    '''Meta information about a miner
25
    type and algo
26
    '''
27
    def __init__(self, miner_type, minerid):
28
        self.miner_type = miner_type
29
        self.minerid = minerid
30
31
class MinerCommand(object):
32
    """Command that could be sent to a miner"""
33
    def __init__(self, command='', parameter=''):
34
        self.command = command
35
        self.parameter = parameter
36
37
class Miner(object):
38
    """Miner"""
39
40
    def __init__(self, name, status=MinerStatus.Online, miner_type='', ipaddress='',
41
                 port='', ftpport='', username='', password='', clientid='', networkid='',
42
                 minerid='', lastmonitor=None, offlinecount=0, defaultpool='', minerinfo=None,
43
                 minerpool=None, minerstats=None, laststatuschanged=None,
44
                 in_service_date=None, location=None):
45
        #friendly name for your miner
46
        self.name = name
47
        self._status = status
48
        #saved or derived from monitoring? type of miner. Antminer S9, Antminer D3, etc.
49
        self.miner_type = miner_type
50
        #ip address, usuall will be local ip address. example: 192.168.x.y
51
        self.ipaddress = ipaddress
52
        #ip port, usually will be 4028
53
        self.port = port
54
        self.ftpport = ftpport
55
        self.username = username
56
        self.password = password
57
        #the mydevices clientid for device
58
        self.clientid = clientid
59
        #network identifier for miner. usually the macaddress
60
        self.networkid = networkid
61
        #so far have only seen Antminer S9 have the minerid from STATS command
62
        self.minerid = minerid
63
        #last time the miner was monitored
64
        self.lastmonitor = lastmonitor
65
        self.monitorcount = 0
66
        self.monitortime = 0
67
        #number of times miner is offline during this session
68
        self.offlinecount = offlinecount
69
        #name of the pool that the miner should default to when it is provisioned
70
        self.defaultpool = defaultpool
71
        #meta info on the miner. should be assigned during discovery and monitor
72
        self.minerinfo = minerinfo
73
        #MinerCurrentPool
74
        self.minerpool = minerpool
75
        #MinerStatistics
76
        self.minerstats = minerstats
77
        #status of the miner. online, offline,disabled etc
78
        self.laststatuschanged = laststatuschanged
79
        #store is where the object was stored. mem is for memcache.
80
        self.store = ''
81
        # the date that the miner was put into service or first discovered
82
        self.in_service_date = in_service_date
83
        # location of miner. could be name of facility or rack
84
        self.location = location
85
86
    @classmethod
87
    def create(cls, values):
88
        '''create entity from values dict'''
89
        miner = Miner('', '', '', '', '', '', '')
90
        #todo: find pythonic way to do this
91
        for pair in values:
92
            if 'minerid' in pair:
93
                miner.minerid = pair['minerid']
94
            if 'name' in pair:
95
                miner.name = pair['name']
96
            if 'ipaddress' in pair:
97
                miner.ipaddress = pair['ipaddress']
98
            if 'port' in pair:
99
                miner.port = pair['port']
100
            if 'location' in pair:
101
                miner.location = pair['location']
102
            if 'in_service_date' in pair:
103
                miner.in_service_date = pair['in_service_date']
104
        return miner
105
106
    @property
107
    def status(self):
108
        return self._status
109
110
    @status.setter
111
    def status(self, value):
112
        if value != '' and value != MinerStatus.Online and value != MinerStatus.Offline and value != MinerStatus.Disabled:
113
            raise ValueError('Invalid miner status {0}'.format(value))
114
        if self._status != value:
115
            self.laststatuschanged = datetime.utcnow()
116
        self._status = value
117
118
    @property
119
    def pools_available(self):
120
        if self.minerpool is None:
121
            return None
122
        available = []
123
        if 'POOLS' in self.minerpool.allpools:
124
            jpools = self.minerpool.allpools['POOLS']
125
            for jpool in jpools:
126
                available.append(AvailablePool(pool_type=self.miner_type, named_pool=None, url=jpool['URL'], user=jpool['User'], priority=jpool['Priority']))
127
        return available
128
129
    #@property
130
    def key(self):
131
        '''cache key for this entity'''
132
        thekey = self.name
133
        if self.isvalid_minerid():
134
            thekey = self.minerid
135
        elif self.isvalid_networkid():
136
            thekey = str(self.networkid)
137
        elif self.isvalid_ipaddress():
138
            thekey = '{0}:{1}'.format(self.ipaddress, self.port)
139
        return thekey
140
141
    @property
142
    def is_unknown(self):
143
        return self.minerid == 'unknown'
144
145
    def isvalid_minerid(self):
146
        return self.minerid is not None and self.minerid and not self.is_unknown
147
148
    def isvalid_networkid(self):
149
        return self.networkid is not None and self.networkid and str(self.networkid) != '{}'
150
151
    def isvalid_ipaddress(self):
152
        return self.ipaddress is not None and self.ipaddress
153
154
    def set_ftp_port(self, port):
155
        if self.ftpport is not None and self.ftpport: return
156
        self.ftpport = port
157
158
    #todo: move ui code out of entity
159
    def summary(self):
160
        #datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%S%f%z')
161
        return '{0} {1} {2} {3}'.format(self.name, self.hash_or_offline(), self.formattime(self.lastmonitor), self.currentpoolname())
162
163
    def currentpoolname(self):
164
        if self.minerpool is None:
165
            return '?'
166
        #todo:look up pools here?
167
        return self.minerpool.poolname
168
169
    def hash_or_offline(self):
170
        '''hash or offline status of miner'''
171
        if self.status != MinerStatus.Online:
172
            return self.status
173
        if self.minerstats is None: return self.status
174
        return self.minerstats.stats_summary()
175
176
    #todo: move to appservice
177
    def utc_to_local(self, utc_dt):
178
        return utc_dt.replace(tzinfo=timezone.utc).astimezone(tz=None)
179
180
    def formattime(self, ptime):
181
        '''format time'''
182
        if ptime is None:
183
            return ''
184
        if isinstance(ptime, datetime):
185
            return self.utc_to_local(ptime).strftime('%m-%d %H:%M:%S')
186
        stime = ptime
187
        if '.' in stime:
188
            stime = stime[0:stime.index('.') - 1]
189
        try:
190
            parsedtime = datetime.strptime(stime, '%Y-%m-%dT%H:%M:%S')
191
            return self.utc_to_local(parsedtime).strftime('%m-%d %H:%M:%S')
192
        except ValueError:
193
            return stime
194
195
    def uptime(self, seconds):
196
        minutes, _ = divmod(seconds, 60)
197
        hours, minutes = divmod(minutes, 60)
198
        days, hours = divmod(hours, 24)
199
        return "%dd%dh%02dm" % (days, hours, minutes)
200
201
    def is_disabled(self):
202
        if self.is_manually_disabled() or self.status == MinerStatus.Disabled:
203
            return True
204
        return False
205
206
    def is_manually_disabled(self):
207
        if self.name.startswith("#"):
208
            return True
209
        return False
210
211
    def can_monitor(self):
212
        if not self.ipaddress:
213
            return False
214
        if not self.port:
215
            return False
216
        return True
217
218
    def should_monitor(self):
219
        #always monitor at least once when fcm app starts up
220
        if self.lastmonitor is None:
221
            return True
222
        #no need to monitor if manually disabled
223
        if self.is_manually_disabled():
224
            return False
225
        if self.is_disabled():
226
            #keep monitoring if it was us that disabled the miner
227
            #need to keep monitoring (at longer interval) so we can detect when comes online
228
            #if its a planned outage then user should manually disable to stop monitoring
229
            #since = (datetime.utcnow() - self.lastmonitor).total_seconds()
230
            #if since > 10 * 60:
231
            return True
232
            #return False
233
        return True
234
235
    def offline_now(self):
236
        self.status = MinerStatus.Offline
237
        self.offlinecount += 1
238
239
    def online_now(self):
240
        self.status = MinerStatus.Online
241
        self.offlinecount = 0
242
243
    def is_send_offline_alert(self):
244
        #todo: make configurable
245
        if self.offlinecount <= 3:
246
            return True
247
        return False
248
249
    def monitored(self, stats, pool=None, info=None, sec=None):
250
        if stats:
251
            self.lastmonitor = datetime.utcnow()
252
            self.status = MinerStatus.Online
253
        if sec is not None:
254
            self.monitorcount += 1
255
            self.monitortime += sec
256
        #todo: process stats and pool
257
        self.setminerinfo(info)
258
        if pool is not None:
259
            self.minerpool = pool
260
        self.minerstats = stats
261
262
    def monitorresponsetime(self):
263
        if self.monitorcount == 0: return 0
264
        return self.monitortime/self.monitorcount
265
266
    def setminerinfo(self, info):
267
        if info is not None:
268
            self.minerinfo = info
269
            if not self.miner_type:
270
                self.miner_type = info.miner_type
271
            if not self.minerid:
272
                self.minerid = info.minerid
273
274
    def updatefrom(self, updatedminer):
275
        if self.minerid != updatedminer.minerid and self.name != updatedminer.name:
276
            return
277
        if self.minerid == updatedminer.minerid and self.name != updatedminer.name:
278
            self.name = updatedminer.name
279
        self.setminerinfo(updatedminer.minerinfo)
280
281
        #self.minerid = updatedminer.minerid
282
        fields = ['lastmonitor', 'status', 'ipaddress', 'port', 'username', 'password', 'clientid']
283
        fields.append('offlinecount')
284
        fields.append('defaultpool')
285
        fields.append('minerpool')
286
        fields.append('minerstats')
287
        fields.append('networkid')
288
        fields.append('location')
289
        fields.append('in_service_date')
290
        for fld in fields:
291
            val = getattr(updatedminer, fld)
292
            if val:
293
                setattr(self, fld, val)
294
295
class AvailablePool(object):
296
    """A pool available on a miner
297
    pool_type is the miner type (e.g. Antminer S9)
298
    """
299
300
    def __init__(self, pool_type, named_pool=None, url='', user='', password='x', priority=None):
301
        self.pool_type = pool_type
302
        self.named_pool = named_pool
303
        self.url = url
304
        self.user = user
305
        self.password = password
306
        self.priority = priority
307
308
    @property
309
    def key(self):
310
        return '{0}|{1}'.format(self.url, self.user)
311
312
class MinerApiCall(object):
313
    '''info about one call to miner'''
314
    def __init__(self, miner: Miner):
315
        self.miner = miner
316
        self.when = datetime.now()
317
        self.start_time = None
318
        self.stop_time = None
319
320
    def start(self):
321
        self.start_time = time.perf_counter()
322
    def stop(self):
323
        self.stop_time = time.perf_counter()
324
    def elapsed(self):
325
        return self.stop_time - self.start_time
326
327
class Pool(object):
328
    """A configured (Named) Pool.
329
    Does not have to be attached to miner yet
330
    """
331
332
    def __init__(self, pool_type, name, url, user, priority, password='x'):
333
        self.pool_type = pool_type
334
        self.name = name
335
        self.url = url
336
        self.user = user
337
        self.priority = priority
338
        self.password = password
339
340
    @classmethod
341
    def create(cls, values):
342
        '''create entity from values dict'''
343
        entity = Pool('', '', '', '', '')
344
        #todo: find pythonic way to do this
345
        for pair in values:
346
            if 'pool_type' in pair:
347
                entity.pool_type = pair['pool_type']
348
            if 'name' in pair:
349
                entity.name = pair['name']
350
            if 'url' in pair:
351
                entity.url = pair['url']
352
            if 'user' in pair:
353
                entity.user = pair['user']
354
            if 'priority' in pair:
355
                entity.priority = pair['priority']
356
            if 'password' in pair:
357
                entity.password = pair['password']
358
        return entity
359
360
361
    def is_same_as(self, available_pool: AvailablePool):
362
        return available_pool.url == self.url and available_pool.user.startswith(self.user)
363
364
class MinerCurrentPool(object):
365
    '''The current pool where a miner is mining'''
366
    def __init__(self, miner, currentpool=None, currentworker=None, allpools=None):
367
        self.miner = miner
368
        self.poolname = '?'
369
        self.currentpool = currentpool
370
        self.currentworker = currentworker
371
        #allpools is a json object
372
        self.allpools = allpools
373
374
    def findpoolnumberforpool(self, url, worker):
375
        jpools = self.allpools["POOLS"]
376
        for pool in jpools:
377
            thisurl = pool["URL"]
378
            thisworker = pool["User"]
379
            if thisurl == url and thisworker.startswith(worker):
380
                return pool["POOL"]
381
        return None
382