1#!/usr/bin/env python
2"""
3Binary memcached test client.
4
5Copyright (c) 2007  Dustin Sallings <dustin@spy.net>
6"""
7
8import hmac
9import socket
10import select
11import random
12import struct
13import exceptions
14import zlib
15
16from memcacheConstants import REQ_MAGIC_BYTE, RES_MAGIC_BYTE
17from memcacheConstants import REQ_PKT_FMT, RES_PKT_FMT, MIN_RECV_PACKET
18from memcacheConstants import SET_PKT_FMT, DEL_PKT_FMT, INCRDECR_RES_FMT
19from memcacheConstants import TOUCH_PKT_FMT, GAT_PKT_FMT, GETL_PKT_FMT
20import memcacheConstants
21
22class MemcachedError(exceptions.Exception):
23    """Error raised when a command fails."""
24
25    def __init__(self, status, msg):
26        error_msg = error_to_str(status)
27        supermsg = 'Memcached error #' + `status` + ' ' + `error_msg`
28        if msg: supermsg += ":  " + msg
29        exceptions.Exception.__init__(self, supermsg)
30
31        self.status = status
32        self.msg = msg
33
34    def __repr__(self):
35        return "<MemcachedError #%d ``%s''>" % (self.status, self.msg)
36
37class MemcachedClient(object):
38    """Simple memcached client."""
39
40    vbucketId = 0
41
42    def __init__(self, host='127.0.0.1', port=11211, timeout=30):
43        self.host = host
44        self.port = port
45        self.timeout = timeout
46        self._createConn()
47        self.r = random.Random()
48        self.vbucket_count = 1024
49
50    def _createConn(self):
51        self.s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
52        self.s.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
53        return self.s.connect_ex((self.host, self.port))
54
55    def reconnect(self):
56        self.s.close()
57        return self._createConn()
58
59    def close(self):
60        self.s.close()
61
62    def __del__(self):
63        self.close()
64
65    def _sendCmd(self, cmd, key, val, opaque, extraHeader='', cas=0):
66        self._sendMsg(cmd, key, val, opaque, extraHeader=extraHeader, cas=cas,
67                      vbucketId=self.vbucketId)
68
69    def _sendMsg(self, cmd, key, val, opaque, extraHeader='', cas=0,
70                 dtype=0, vbucketId=0,
71                 fmt=REQ_PKT_FMT, magic=REQ_MAGIC_BYTE):
72        msg = struct.pack(fmt, magic,
73            cmd, len(key), len(extraHeader), dtype, vbucketId,
74                len(key) + len(extraHeader) + len(val), opaque, cas)
75        _, w, _ = select.select([], [self.s], [], self.timeout)
76        if w:
77            self.s.send(msg + extraHeader + key + val)
78        else:
79            raise exceptions.EOFError("Timeout waiting for socket send. from {0}".format(self.host))
80
81    def _recvMsg(self):
82        response = ""
83        while len(response) < MIN_RECV_PACKET:
84            r, _, _ = select.select([self.s], [], [], self.timeout)
85            if r:
86                data = self.s.recv(MIN_RECV_PACKET - len(response))
87                if data == '':
88                    raise exceptions.EOFError("Got empty data (remote died?). from {0}".format(self.host))
89                response += data
90            else:
91                raise exceptions.EOFError("Timeout waiting for socket recv. from {0}".format(self.host))
92        assert len(response) == MIN_RECV_PACKET
93        magic, cmd, keylen, extralen, dtype, errcode, remaining, opaque, cas = \
94            struct.unpack(RES_PKT_FMT, response)
95
96        rv = ""
97        while remaining > 0:
98            r, _, _ = select.select([self.s], [], [], self.timeout)
99            if r:
100                data = self.s.recv(remaining)
101                if data == '':
102                    raise exceptions.EOFError("Got empty data (remote died?). from {0}".format(self.host))
103                rv += data
104                remaining -= len(data)
105            else:
106                raise exceptions.EOFError("Timeout waiting for socket recv. from {0}".format(self.host))
107
108        assert (magic in (RES_MAGIC_BYTE, REQ_MAGIC_BYTE)), "Got magic: %d" % magic
109        return cmd, errcode, opaque, cas, keylen, extralen, rv
110
111    def _handleKeyedResponse(self, myopaque):
112        cmd, errcode, opaque, cas, keylen, extralen, rv = self._recvMsg()
113        assert myopaque is None or opaque == myopaque, \
114            "expected opaque %x, got %x" % (myopaque, opaque)
115        if errcode:
116            rv += " for vbucket :{0} to mc {1}:{2}".format(self.vbucketId, self.host, self.port)
117            raise MemcachedError(errcode, rv)
118        return cmd, opaque, cas, keylen, extralen, rv
119
120    def _handleSingleResponse(self, myopaque):
121        cmd, opaque, cas, keylen, extralen, data = self._handleKeyedResponse(myopaque)
122        return opaque, cas, data
123
124    def _doCmd(self, cmd, key, val, extraHeader='', cas=0):
125        """Send a command and await its response."""
126        opaque = self.r.randint(0, 2 ** 32)
127        self._sendCmd(cmd, key, val, opaque, extraHeader, cas)
128        return self._handleSingleResponse(opaque)
129
130    def _mutate(self, cmd, key, exp, flags, cas, val):
131        return self._doCmd(cmd, key, val, struct.pack(SET_PKT_FMT, flags, exp),
132            cas)
133
134    def _cat(self, cmd, key, cas, val):
135        return self._doCmd(cmd, key, val, '', cas)
136
137    def append(self, key, value, cas=0, vbucket= -1):
138        self._set_vbucket(key, vbucket)
139        return self._cat(memcacheConstants.CMD_APPEND, key, cas, value)
140
141    def prepend(self, key, value, cas=0, vbucket= -1):
142        self._set_vbucket(key, vbucket)
143        return self._cat(memcacheConstants.CMD_PREPEND, key, cas, value)
144
145    def __incrdecr(self, cmd, key, amt, init, exp):
146        something, cas, val = self._doCmd(cmd, key, '',
147            struct.pack(memcacheConstants.INCRDECR_PKT_FMT, amt, init, exp))
148        return struct.unpack(INCRDECR_RES_FMT, val)[0], cas
149
150    def incr(self, key, amt=1, init=0, exp=0, vbucket= -1):
151        """Increment or create the named counter."""
152        self._set_vbucket(key, vbucket)
153        return self.__incrdecr(memcacheConstants.CMD_INCR, key, amt, init, exp)
154
155    def decr(self, key, amt=1, init=0, exp=0, vbucket= -1):
156        """Decrement or create the named counter."""
157        self._set_vbucket(key, vbucket)
158        return self.__incrdecr(memcacheConstants.CMD_DECR, key, amt, init, exp)
159
160    def set(self, key, exp, flags, val, vbucket= -1):
161        """Set a value in the memcached server."""
162        self._set_vbucket(key, vbucket)
163        return self._mutate(memcacheConstants.CMD_SET, key, exp, flags, 0, val)
164
165    def send_set(self, key, exp, flags, val, vbucket= -1):
166        """Set a value in the memcached server without handling the response"""
167        self._set_vbucket(key, vbucket)
168        opaque = self.r.randint(0, 2 ** 32)
169        self._sendCmd(memcacheConstants.CMD_SET, key, val, opaque, struct.pack(SET_PKT_FMT, flags, exp), 0)
170
171    def add(self, key, exp, flags, val, vbucket= -1):
172        """Add a value in the memcached server iff it doesn't already exist."""
173        self._set_vbucket(key, vbucket)
174        return self._mutate(memcacheConstants.CMD_ADD, key, exp, flags, 0, val)
175
176    def replace(self, key, exp, flags, val, vbucket= -1):
177        """Replace a value in the memcached server iff it already exists."""
178        self._set_vbucket(key, vbucket)
179        return self._mutate(memcacheConstants.CMD_REPLACE, key, exp, flags, 0,
180            val)
181    def observe(self, key, vbucket= -1):
182        """Observe a key for persistence and replication."""
183        self._set_vbucket(key, vbucket)
184        value = struct.pack('>HH', self.vbucketId, len(key)) + key
185        opaque, cas, data = self._doCmd(memcacheConstants.CMD_OBSERVE, '', value)
186        rep_time = (cas & 0xFFFFFFFF)
187        persist_time = (cas >> 32) & 0xFFFFFFFF
188        persisted = struct.unpack('>B', data[4 + len(key)])[0]
189        return opaque, rep_time, persist_time, persisted, cas
190
191    def __parseGet(self, data, klen=0):
192        flags = struct.unpack(memcacheConstants.GET_RES_FMT, data[-1][:4])[0]
193        return flags, data[1], data[-1][4 + klen:]
194
195    def get(self, key, vbucket= -1):
196        """Get the value for a given key within the memcached server."""
197        self._set_vbucket(key, vbucket)
198        parts = self._doCmd(memcacheConstants.CMD_GET, key, '')
199
200        return self.__parseGet(parts)
201
202    def send_get(self, key, vbucket= -1):
203        """ sends a get message without parsing the response """
204        self._set_vbucket(key, vbucket)
205        opaque = self.r.randint(0, 2 ** 32)
206        self._sendCmd(memcacheConstants.CMD_GET, key, '', opaque)
207
208    def getl(self, key, exp=15, vbucket= -1):
209        """Get the value for a given key within the memcached server."""
210        self._set_vbucket(key, vbucket)
211        parts = self._doCmd(memcacheConstants.CMD_GET_LOCKED, key, '',
212            struct.pack(memcacheConstants.GETL_PKT_FMT, exp))
213        return self.__parseGet(parts)
214
215    def getr(self, key, vbucket= -1):
216        """Get the value for a given key within the memcached server from a replica vbucket."""
217        self._set_vbucket(key, vbucket)
218        parts = self._doCmd(memcacheConstants.CMD_GET_REPLICA, key, '')
219        return self.__parseGet(parts, len(key))
220
221
222    def getMeta(self, key):
223        """Get the metadata for a given key within the memcached server."""
224        self._set_vbucket(key)
225        opaque, cas, data = self._doCmd(memcacheConstants.CMD_GET_META, key, '')
226        deleted = struct.unpack('>I', data[0:4])[0]
227        flags = struct.unpack('>I', data[4:8])[0]
228        exp = struct.unpack('>I', data[8:12])[0]
229        seqno = struct.unpack('>Q', data[12:20])[0]
230        return (deleted, flags, exp, seqno, cas)
231
232    def cas(self, key, exp, flags, oldVal, val, vbucket= -1):
233        """CAS in a new value for the given key and comparison value."""
234        self._set_vbucket(key, vbucket)
235        self._mutate(memcacheConstants.CMD_SET, key, exp, flags,
236            oldVal, val)
237
238    def touch(self, key, exp, vbucket= -1):
239        """Touch a key in the memcached server."""
240        self._set_vbucket(key, vbucket)
241        return self._doCmd(memcacheConstants.CMD_TOUCH, key, '',
242            struct.pack(memcacheConstants.TOUCH_PKT_FMT, exp))
243
244    def gat(self, key, exp, vbucket= -1):
245        """Get the value for a given key and touch it within the memcached server."""
246        self._set_vbucket(key, vbucket)
247        parts = self._doCmd(memcacheConstants.CMD_GAT, key, '',
248            struct.pack(memcacheConstants.GAT_PKT_FMT, exp))
249        return self.__parseGet(parts)
250
251    def version(self):
252        """Get the value for a given key within the memcached server."""
253        return self._doCmd(memcacheConstants.CMD_VERSION, '', '')
254
255    def sasl_mechanisms(self):
256        """Get the supported SASL methods."""
257        return set(self._doCmd(memcacheConstants.CMD_SASL_LIST_MECHS,
258                               '', '')[2].split(' '))
259
260    def sasl_auth_start(self, mech, data):
261        """Start a sasl auth session."""
262        return self._doCmd(memcacheConstants.CMD_SASL_AUTH, mech, data)
263
264    def sasl_auth_plain(self, user, password, foruser=''):
265        """Perform plain auth."""
266        return self.sasl_auth_start('PLAIN', '\0'.join([foruser, user, password]))
267
268    def sasl_auth_cram_md5(self, user, password):
269        """Start a plan auth session."""
270        challenge = None
271        try:
272            self.sasl_auth_start('CRAM-MD5', '')
273        except MemcachedError, e:
274            if e.status != memcacheConstants.ERR_AUTH_CONTINUE:
275                raise
276            challenge = e.msg.split(' ')[0]
277
278        dig = hmac.HMAC(password, challenge).hexdigest()
279        return self._doCmd(memcacheConstants.CMD_SASL_STEP, 'CRAM-MD5',
280                           user + ' ' + dig)
281
282    def stop_persistence(self):
283        return self._doCmd(memcacheConstants.CMD_STOP_PERSISTENCE, '', '')
284
285    def start_persistence(self):
286        return self._doCmd(memcacheConstants.CMD_START_PERSISTENCE, '', '')
287
288    def set_flush_param(self, key, val):
289        print "setting flush param:", key, val
290        return self._doCmd(memcacheConstants.CMD_SET_FLUSH_PARAM, key, val)
291
292    def set_param(self, key, val, type):
293        print "setting param:", key, val
294        type = struct.pack(memcacheConstants.GET_RES_FMT, type)
295        return self._doCmd(memcacheConstants.CMD_SET_FLUSH_PARAM, key, val, type)
296
297    def start_onlineupdate(self):
298        return self._doCmd(memcacheConstants.CMD_START_ONLINEUPDATE, '', '')
299
300    def complete_onlineupdate(self):
301        return self._doCmd(memcacheConstants.CMD_COMPLETE_ONLINEUPDATE, '', '')
302
303    def revert_onlineupdate(self):
304        return self._doCmd(memcacheConstants.CMD_REVERT_ONLINEUPDATE, '', '')
305
306    def set_tap_param(self, key, val):
307        print "setting tap param:", key, val
308        return self._doCmd(memcacheConstants.CMD_SET_TAP_PARAM, key, val)
309
310    def set_vbucket_state(self, vbucket, stateName):
311        assert isinstance(vbucket, int)
312        self.vbucketId = vbucket
313        state = struct.pack(memcacheConstants.VB_SET_PKT_FMT,
314                            memcacheConstants.VB_STATE_NAMES[stateName])
315        return self._doCmd(memcacheConstants.CMD_SET_VBUCKET_STATE, '', '', state)
316
317    def get_vbucket_state(self, vbucket):
318        assert isinstance(vbucket, int)
319        self.vbucketId = vbucket
320        return self._doCmd(memcacheConstants.CMD_GET_VBUCKET_STATE,
321                           str(vbucket), '')
322
323    def delete_vbucket(self, vbucket):
324        assert isinstance(vbucket, int)
325        self.vbucketId = vbucket
326        return self._doCmd(memcacheConstants.CMD_DELETE_VBUCKET, '', '')
327
328    def evict_key(self, key, vbucket= -1):
329        self._set_vbucket(key, vbucket)
330        return self._doCmd(memcacheConstants.CMD_EVICT_KEY, key, '')
331
332    def getMulti(self, keys, vbucket= -1):
333        """Get values for any available keys in the given iterable.
334
335        Returns a dict of matched keys to their values."""
336        opaqued = dict(enumerate(keys))
337        terminal = len(opaqued) + 10
338        # Send all of the keys in quiet
339        vbs = set()
340        for k, v in opaqued.iteritems():
341            self._set_vbucket(v, vbucket)
342            vbs.add(self.vbucketId)
343            self._sendCmd(memcacheConstants.CMD_GETQ, v, '', k)
344
345        for vb in vbs:
346            self.vbucketId = vb
347            self._sendCmd(memcacheConstants.CMD_NOOP, '', '', terminal)
348
349        # Handle the response
350        rv = {}
351        for vb in vbs:
352            self.vbucketId = vb
353            done = False
354            while not done:
355                opaque, cas, data = self._handleSingleResponse(None)
356                if opaque != terminal:
357                    rv[opaqued[opaque]] = self.__parseGet((opaque, cas, data))
358                else:
359                    done = True
360
361        return rv
362
363    def setMulti(self, exp, flags, items, vbucket= -1):
364        """Multi-set (using setq).
365
366        Give me (key, value) pairs."""
367
368        # If this is a dict, convert it to a pair generator
369        if hasattr(items, 'iteritems'):
370            items = items.iteritems()
371
372        opaqued = dict(enumerate(items))
373        terminal = len(opaqued) + 10
374        extra = struct.pack(SET_PKT_FMT, flags, exp)
375
376        # Send all of the keys in quiet
377        vbs = set()
378        for opaque, kv in opaqued.iteritems():
379            self._set_vbucket(kv[0], vbucket)
380            vbs.add(self.vbucketId)
381            self._sendCmd(memcacheConstants.CMD_SETQ, kv[0], kv[1], opaque, extra)
382
383        for vb in vbs:
384            self.vbucketId = vb
385            self._sendCmd(memcacheConstants.CMD_NOOP, '', '', terminal)
386
387        # Handle the response
388        failed = []
389        for vb in vbs:
390            self.vbucketId = vb
391            done = False
392            while not done:
393                try:
394                    opaque, cas, data = self._handleSingleResponse(None)
395                    done = opaque == terminal
396                except MemcachedError, e:
397                    failed.append(e)
398
399        return failed
400
401    def stats(self, sub=''):
402        """Get stats."""
403        opaque = self.r.randint(0, 2 ** 32)
404        self._sendCmd(memcacheConstants.CMD_STAT, sub, '', opaque)
405        done = False
406        rv = {}
407        while not done:
408            cmd, opaque, cas, klen, extralen, data = self._handleKeyedResponse(None)
409            if klen:
410                rv[data[0:klen]] = data[klen:]
411            else:
412                done = True
413        return rv
414
415    def noop(self):
416        """Send a noop command."""
417        return self._doCmd(memcacheConstants.CMD_NOOP, '', '')
418
419    def delete(self, key, cas=0, vbucket= -1):
420        """Delete the value for a given key within the memcached server."""
421        self._set_vbucket(key, vbucket)
422        return self._doCmd(memcacheConstants.CMD_DELETE, key, '', '', cas)
423
424    def flush(self, timebomb=0):
425        """Flush all storage in a memcached instance."""
426        return self._doCmd(memcacheConstants.CMD_FLUSH, '', '',
427            struct.pack(memcacheConstants.FLUSH_PKT_FMT, timebomb))
428
429    def bucket_select(self, name):
430        return self._doCmd(memcacheConstants.CMD_SELECT_BUCKET, name, '')
431
432    def sync_persistence(self, keyspecs):
433        payload = self._build_sync_payload(0x8, keyspecs)
434
435        print "sending sync for persistence command for the following keyspecs:", keyspecs
436        (opaque, cas, data) = self._doCmd(memcacheConstants.CMD_SYNC, "", payload)
437        return (opaque, cas, self._parse_sync_response(data))
438
439    def sync_mutation(self, keyspecs):
440        payload = self._build_sync_payload(0x4, keyspecs)
441
442        print "sending sync for mutation command for the following keyspecs:", keyspecs
443        (opaque, cas, data) = self._doCmd(memcacheConstants.CMD_SYNC, "", payload)
444        return (opaque, cas, self._parse_sync_response(data))
445
446    def sync_replication(self, keyspecs, numReplicas=1):
447        payload = self._build_sync_payload((numReplicas & 0x0f) << 4, keyspecs)
448
449        print "sending sync for replication command for the following keyspecs:", keyspecs
450        (opaque, cas, data) = self._doCmd(memcacheConstants.CMD_SYNC, "", payload)
451        return (opaque, cas, self._parse_sync_response(data))
452
453    def sync_replication_or_persistence(self, keyspecs, numReplicas=1):
454        payload = self._build_sync_payload(((numReplicas & 0x0f) << 4) | 0x8, keyspecs)
455
456        print "sending sync for replication or persistence command for the " \
457            "following keyspecs:", keyspecs
458        (opaque, cas, data) = self._doCmd(memcacheConstants.CMD_SYNC, "", payload)
459        return (opaque, cas, self._parse_sync_response(data))
460
461    def sync_replication_and_persistence(self, keyspecs, numReplicas=1):
462        payload = self._build_sync_payload(((numReplicas & 0x0f) << 4) | 0xA, keyspecs)
463
464        print "sending sync for replication and persistence command for the " \
465            "following keyspecs:", keyspecs
466        (opaque, cas, data) = self._doCmd(memcacheConstants.CMD_SYNC, "", payload)
467        return (opaque, cas, self._parse_sync_response(data))
468
469    def _build_sync_payload(self, flags, keyspecs):
470        payload = struct.pack(">I", flags)
471        payload += struct.pack(">H", len(keyspecs))
472
473        for spec in keyspecs:
474            if not isinstance(spec, dict):
475                raise TypeError("each keyspec must be a dict")
476            if not spec.has_key('vbucket'):
477                raise TypeError("missing vbucket property in keyspec")
478            if not spec.has_key('key'):
479                raise TypeError("missing key property in keyspec")
480
481            payload += struct.pack(">Q", spec.get('cas', 0))
482            payload += struct.pack(">H", spec['vbucket'])
483            payload += struct.pack(">H", len(spec['key']))
484            payload += spec['key']
485
486        return payload
487
488    def _parse_sync_response(self, data):
489        keyspecs = []
490        nkeys = struct.unpack(">H", data[0 : struct.calcsize("H")])[0]
491        offset = struct.calcsize("H")
492
493        for i in xrange(nkeys):
494            spec = {}
495            width = struct.calcsize("QHHB")
496            (spec['cas'], spec['vbucket'], keylen, eventid) = \
497                struct.unpack(">QHHB", data[offset : offset + width])
498            offset += width
499            spec['key'] = data[offset : offset + keylen]
500            offset += keylen
501
502            if eventid == memcacheConstants.CMD_SYNC_EVENT_PERSISTED:
503                spec['event'] = 'persisted'
504            elif eventid == memcacheConstants.CMD_SYNC_EVENT_MODIFED:
505                spec['event'] = 'modified'
506            elif eventid == memcacheConstants.CMD_SYNC_EVENT_DELETED:
507                spec['event'] = 'deleted'
508            elif eventid == memcacheConstants.CMD_SYNC_EVENT_REPLICATED:
509                spec['event'] = 'replicated'
510            elif eventid == memcacheConstants.CMD_SYNC_INVALID_KEY:
511                spec['event'] = 'invalid key'
512            elif spec['event'] == memcacheConstants.CMD_SYNC_INVALID_CAS:
513                spec['event'] = 'invalid cas'
514            else:
515                spec['event'] = eventid
516
517            keyspecs.append(spec)
518
519        return keyspecs
520
521    def restore_file(self, filename):
522        """Initiate restore of a given file."""
523        return self._doCmd(memcacheConstants.CMD_RESTORE_FILE, filename, '', '', 0)
524
525    def restore_complete(self):
526        """Notify the server that we're done restoring."""
527        return self._doCmd(memcacheConstants.CMD_RESTORE_COMPLETE, '', '', '', 0)
528
529    def deregister_tap_client(self, tap_name):
530        """Deregister the TAP client with a given name."""
531        return self._doCmd(memcacheConstants.CMD_DEREGISTER_TAP_CLIENT, tap_name, '', '', 0)
532
533    def reset_replication_chain(self):
534        """Reset the replication chain."""
535        return self._doCmd(memcacheConstants.CMD_RESET_REPLICATION_CHAIN, '', '', '', 0)
536
537    def _set_vbucket(self, key, vbucket= -1):
538        if vbucket < 0:
539            self.vbucketId = (((zlib.crc32(key)) >> 16) & 0x7fff) & (self.vbucket_count - 1)
540        else:
541            self.vbucketId = vbucket
542
543    def get_config(self):
544        """Get the config within the memcached server."""
545        return self._doCmd(memcacheConstants.CMD_GET_CLUSTER_CONFIG, '', '')
546
547    def set_config(self, blob_conf):
548        """Set the config within the memcached server."""
549        return self._doCmd(memcacheConstants.CMD_SET_CLUSTER_CONFIG, blob_conf, '')
550
551def error_to_str(errno):
552    if errno == 0x01:
553        return "Not found"
554    elif errno == 0x02:
555        return "Exists"
556    elif errno == 0x03:
557        return "Too big"
558    elif errno == 0x04:
559        return "Invalid"
560    elif errno == 0x05:
561        return "Not stored"
562    elif errno == 0x06:
563        return "Bad Delta"
564    elif errno == 0x07:
565        return "Not my vbucket"
566    elif errno == 0x20:
567        return "Auth error"
568    elif errno == 0x21:
569        return "Auth continue"
570    elif errno == 0x81:
571        return "Unknown Command"
572    elif errno == 0x82:
573        return "No Memory"
574    elif errno == 0x83:
575        return "Not Supported"
576    elif errno == 0x84:
577        return "Internal error"
578    elif errno == 0x85:
579        return "Busy"
580    elif errno == 0x86:
581        return "Temporary failure"
582