domain.mining.Miner.is_key_updated()   A
last analyzed

Complexity

Conditions 1

Size

Total Lines 3
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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