Passed
Push — master ( b92f2d...c77aa9 )
by Dave
01:03
created

domain.mining.Miner.to_date()   A

Complexity

Conditions 3

Size

Total Lines 9
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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