Passed
Push — master ( f401c8...fbf2d0 )
by Dave
53s
created

domain.mining.Pool.create()   B

Complexity

Conditions 8

Size

Total Lines 19
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

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