
This patch also contains fixes to bugs * 811501 * 812498 * 821310 Also, removed the default set of users in the proxy-server.conf file. Change-Id: Ief83905d10ff7bf7c43685ada4d7f05959cee9d1 BUG: 821310 Signed-off-by: Mohammed Junaid <junaid@redhat.com> Reviewed-on: http://review.gluster.com/3390 Tested-by: Gluster Build System <jenkins@build.gluster.com> Reviewed-by: Vijay Bellur <vijay@gluster.com>
681 lines
20 KiB
Python
681 lines
20 KiB
Python
# Copyright (c) 2011 Red Hat, Inc.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
# implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
import logging
|
|
import os
|
|
import errno
|
|
from hashlib import md5
|
|
from swift.common.utils import normalize_timestamp
|
|
from xattr import setxattr, removexattr, getxattr, removexattr
|
|
import cPickle as pickle
|
|
|
|
X_CONTENT_TYPE = 'Content-Type'
|
|
X_CONTENT_LENGTH = 'Content-Length'
|
|
X_TIMESTAMP = 'X-Timestamp'
|
|
X_PUT_TIMESTAMP = 'X-PUT-Timestamp'
|
|
X_TYPE = 'X-Type'
|
|
X_ETAG = 'ETag'
|
|
X_OBJECTS_COUNT = 'X-Object-Count'
|
|
X_BYTES_USED = 'X-Bytes-Used'
|
|
X_CONTAINER_COUNT = 'X-Container-Count'
|
|
X_OBJECT_TYPE = 'X-Object-Type'
|
|
DIR_TYPE = 'application/directory'
|
|
ACCOUNT = 'Account'
|
|
MOUNT_PATH = '/mnt/gluster-object'
|
|
METADATA_KEY = 'user.swift.metadata'
|
|
CONTAINER = 'container'
|
|
DIR = 'dir'
|
|
MARKER_DIR = 'marker_dir'
|
|
FILE = 'file'
|
|
DIR_TYPE = 'application/directory'
|
|
FILE_TYPE = 'application/octet-stream'
|
|
OBJECT = 'Object'
|
|
OBJECT_TYPE = 'application/octet-stream'
|
|
DEFAULT_UID = -1
|
|
DEFAULT_GID = -1
|
|
PICKLE_PROTOCOL = 2
|
|
CHUNK_SIZE = 65536
|
|
|
|
|
|
def mkdirs(path):
|
|
"""
|
|
Ensures the path is a directory or makes it if not. Errors if the path
|
|
exists but is a file or on permissions failure.
|
|
|
|
:param path: path to create
|
|
"""
|
|
if not os.path.isdir(path):
|
|
try:
|
|
do_makedirs(path)
|
|
except OSError, err:
|
|
#TODO: check, isdir will fail if mounted and volume stopped.
|
|
#if err.errno != errno.EEXIST or not os.path.isdir(path)
|
|
if err.errno != errno.EEXIST:
|
|
raise
|
|
|
|
def rmdirs(path):
|
|
if os.path.isdir(path) and dir_empty(path):
|
|
do_rmdir(path)
|
|
else:
|
|
logging.error("rmdirs failed dir may not be empty or not valid dir")
|
|
return False
|
|
|
|
def strip_obj_storage_path(path, string='/mnt/gluster-object'):
|
|
"""
|
|
strip /mnt/gluster-object
|
|
"""
|
|
return path.replace(string, '').strip('/')
|
|
|
|
def do_mkdir(path):
|
|
try:
|
|
os.mkdir(path)
|
|
except Exception, err:
|
|
logging.exception("Mkdir failed on %s err: %s", path, str(err))
|
|
if err.errno != errno.EEXIST:
|
|
raise
|
|
return True
|
|
|
|
def do_makedirs(path):
|
|
try:
|
|
os.makedirs(path)
|
|
except Exception, err:
|
|
logging.exception("Makedirs failed on %s err: %s", path, str(err))
|
|
if err.errno != errno.EEXIST:
|
|
raise
|
|
return True
|
|
|
|
|
|
def do_listdir(path):
|
|
try:
|
|
buf = os.listdir(path)
|
|
except Exception, err:
|
|
logging.exception("Listdir failed on %s err: %s", path, str(err))
|
|
raise
|
|
return buf
|
|
|
|
def do_chown(path, uid, gid):
|
|
try:
|
|
os.chown(path, uid, gid)
|
|
except Exception, err:
|
|
logging.exception("Chown failed on %s err: %s", path, str(err))
|
|
raise
|
|
return True
|
|
|
|
def do_stat(path):
|
|
try:
|
|
#Check for fd.
|
|
if isinstance(path, int):
|
|
buf = os.fstat(path)
|
|
else:
|
|
buf = os.stat(path)
|
|
except Exception, err:
|
|
logging.exception("Stat failed on %s err: %s", path, str(err))
|
|
raise
|
|
|
|
return buf
|
|
|
|
def do_open(path, mode):
|
|
try:
|
|
fd = open(path, mode)
|
|
except Exception, err:
|
|
logging.exception("Open failed on %s err: %s", path, str(err))
|
|
raise
|
|
return fd
|
|
|
|
def do_close(fd):
|
|
#fd could be file or int type.
|
|
try:
|
|
if isinstance(fd, int):
|
|
os.close(fd)
|
|
else:
|
|
fd.close()
|
|
except Exception, err:
|
|
logging.exception("Close failed on %s err: %s", fd, str(err))
|
|
raise
|
|
return True
|
|
|
|
def do_unlink(path, log = True):
|
|
try:
|
|
os.unlink(path)
|
|
except Exception, err:
|
|
if log:
|
|
logging.exception("Unlink failed on %s err: %s", path, str(err))
|
|
if err.errno != errno.ENOENT:
|
|
raise
|
|
return True
|
|
|
|
def do_rmdir(path):
|
|
try:
|
|
os.rmdir(path)
|
|
except Exception, err:
|
|
logging.exception("Rmdir failed on %s err: %s", path, str(err))
|
|
if err.errno != errno.ENOENT:
|
|
raise
|
|
return True
|
|
|
|
def do_rename(old_path, new_path):
|
|
try:
|
|
os.rename(old_path, new_path)
|
|
except Exception, err:
|
|
logging.exception("Rename failed on %s to %s err: %s", old_path, new_path, \
|
|
str(err))
|
|
raise
|
|
return True
|
|
|
|
def do_setxattr(path, key, value):
|
|
fd = None
|
|
if not os.path.isdir(path):
|
|
fd = do_open(path, 'rb')
|
|
else:
|
|
fd = path
|
|
if fd or os.path.isdir(path):
|
|
try:
|
|
setxattr(fd, key, value)
|
|
except Exception, err:
|
|
logging.exception("setxattr failed on %s key %s err: %s", path, key, str(err))
|
|
raise
|
|
finally:
|
|
if fd and not os.path.isdir(path):
|
|
do_close(fd)
|
|
else:
|
|
logging.error("Open failed path %s", path)
|
|
return False
|
|
return True
|
|
|
|
|
|
|
|
def do_getxattr(path, key, log = True):
|
|
fd = None
|
|
if not os.path.isdir(path):
|
|
fd = do_open(path, 'rb')
|
|
else:
|
|
fd = path
|
|
if fd or os.path.isdir(path):
|
|
try:
|
|
value = getxattr(fd, key)
|
|
except Exception, err:
|
|
if log:
|
|
logging.exception("getxattr failed on %s key %s err: %s", path, key, str(err))
|
|
raise
|
|
finally:
|
|
if fd and not os.path.isdir(path):
|
|
do_close(fd)
|
|
else:
|
|
logging.error("Open failed path %s", path)
|
|
return False
|
|
return value
|
|
|
|
def do_removexattr(path, key):
|
|
fd = None
|
|
if not os.path.isdir(path):
|
|
fd = do_open(path, 'rb')
|
|
else:
|
|
fd = path
|
|
if fd or os.path.isdir(path):
|
|
try:
|
|
removexattr(fd, key)
|
|
except Exception, err:
|
|
logging.exception("removexattr failed on %s key %s err: %s", path, key, str(err))
|
|
raise
|
|
finally:
|
|
if fd and not os.path.isdir(path):
|
|
do_close(fd)
|
|
else:
|
|
logging.error("Open failed path %s", path)
|
|
return False
|
|
return True
|
|
|
|
def read_metadata(path):
|
|
"""
|
|
Helper function to read the pickled metadata from a File/Directory .
|
|
|
|
:param path: File/Directory to read metadata from.
|
|
|
|
:returns: dictionary of metadata
|
|
"""
|
|
metadata = ''
|
|
key = 0
|
|
while True:
|
|
try:
|
|
metadata += do_getxattr(path, '%s%s' % (METADATA_KEY, (key or '')),
|
|
log = False)
|
|
except Exception:
|
|
break
|
|
key += 1
|
|
if metadata:
|
|
return pickle.loads(metadata)
|
|
else:
|
|
return {}
|
|
|
|
|
|
def write_metadata(path, metadata):
|
|
"""
|
|
Helper function to write pickled metadata for a File/Directory.
|
|
|
|
:param path: File/Directory path to write the metadata
|
|
:param metadata: metadata to write
|
|
"""
|
|
metastr = pickle.dumps(metadata, PICKLE_PROTOCOL)
|
|
key = 0
|
|
while metastr:
|
|
do_setxattr(path, '%s%s' % (METADATA_KEY, key or ''), metastr[:254])
|
|
metastr = metastr[254:]
|
|
key += 1
|
|
|
|
def clean_metadata(path):
|
|
key = 0
|
|
while True:
|
|
value = do_getxattr(path, '%s%s' % (METADATA_KEY, (key or '')))
|
|
do_removexattr(path, '%s%s' % (METADATA_KEY, (key or '')))
|
|
key += 1
|
|
|
|
|
|
def dir_empty(path):
|
|
"""
|
|
Return true if directory/container is empty.
|
|
:param path: Directory path.
|
|
:returns: True/False.
|
|
"""
|
|
if os.path.isdir(path):
|
|
try:
|
|
files = do_listdir(path)
|
|
except Exception, err:
|
|
logging.exception("listdir failed on %s err: %s", path, str(err))
|
|
raise
|
|
if not files:
|
|
return True
|
|
else:
|
|
return False
|
|
else:
|
|
if not os.path.exists(path):
|
|
return True
|
|
|
|
|
|
def get_device_from_account(account):
|
|
if account.startswith(RESELLER_PREFIX):
|
|
device = account.replace(RESELLER_PREFIX, '', 1)
|
|
return device
|
|
|
|
def check_user_xattr(path):
|
|
if not os.path.exists(path):
|
|
return False
|
|
do_setxattr(path, 'user.test.key1', 'value1')
|
|
try:
|
|
removexattr(path, 'user.test.key1')
|
|
except Exception, err:
|
|
logging.exception("removexattr failed on %s err: %s", path, str(err))
|
|
#Remove xattr may fail in case of concurrent remove.
|
|
return True
|
|
|
|
|
|
def _check_valid_account(account, fs_object):
|
|
mount_path = getattr(fs_object, 'mount_path', MOUNT_PATH)
|
|
|
|
if os.path.ismount(os.path.join(mount_path, account)):
|
|
return True
|
|
|
|
if not check_account_exists(fs_object.get_export_from_account_id(account), fs_object):
|
|
logging.error('Account not present %s', account)
|
|
return False
|
|
|
|
if not os.path.isdir(os.path.join(mount_path, account)):
|
|
mkdirs(os.path.join(mount_path, account))
|
|
fs_object.unmount(os.path.join(mount_path, account))
|
|
|
|
if fs_object:
|
|
if not fs_object.mount(account):
|
|
return False
|
|
|
|
return True
|
|
|
|
def check_valid_account(account, fs_object):
|
|
return _check_valid_account(account, fs_object)
|
|
|
|
def validate_container(metadata):
|
|
if not metadata:
|
|
logging.error('No metadata')
|
|
return False
|
|
|
|
if X_TYPE not in metadata.keys() or \
|
|
X_TIMESTAMP not in metadata.keys() or \
|
|
X_PUT_TIMESTAMP not in metadata.keys() or \
|
|
X_OBJECTS_COUNT not in metadata.keys() or \
|
|
X_BYTES_USED not in metadata.keys():
|
|
#logging.error('Container error %s' % metadata)
|
|
return False
|
|
|
|
if metadata[X_TYPE] == CONTAINER:
|
|
return True
|
|
|
|
logging.error('Container error %s' % metadata)
|
|
return False
|
|
|
|
def validate_account(metadata):
|
|
if not metadata:
|
|
logging.error('No metadata')
|
|
return False
|
|
|
|
if X_TYPE not in metadata.keys() or \
|
|
X_TIMESTAMP not in metadata.keys() or \
|
|
X_PUT_TIMESTAMP not in metadata.keys() or \
|
|
X_OBJECTS_COUNT not in metadata.keys() or \
|
|
X_BYTES_USED not in metadata.keys() or \
|
|
X_CONTAINER_COUNT not in metadata.keys():
|
|
#logging.error('Account error %s' % metadata)
|
|
return False
|
|
|
|
if metadata[X_TYPE] == ACCOUNT:
|
|
return True
|
|
|
|
logging.error('Account error %s' % metadata)
|
|
return False
|
|
|
|
def validate_object(metadata):
|
|
if not metadata:
|
|
logging.error('No metadata')
|
|
return False
|
|
|
|
if X_TIMESTAMP not in metadata.keys() or \
|
|
X_CONTENT_TYPE not in metadata.keys() or \
|
|
X_ETAG not in metadata.keys() or \
|
|
X_CONTENT_LENGTH not in metadata.keys() or \
|
|
X_TYPE not in metadata.keys() or \
|
|
X_OBJECT_TYPE not in metadata.keys():
|
|
#logging.error('Object error %s' % metadata)
|
|
return False
|
|
|
|
if metadata[X_TYPE] == OBJECT:
|
|
return True
|
|
|
|
logging.error('Object error %s' % metadata)
|
|
return False
|
|
|
|
def is_marker(metadata):
|
|
if not metadata:
|
|
logging.error('No metadata')
|
|
return False
|
|
|
|
if X_OBJECT_TYPE not in metadata.keys():
|
|
logging.error('X_OBJECT_TYPE missing %s' % metadata)
|
|
return False
|
|
|
|
if metadata[X_OBJECT_TYPE] == MARKER_DIR:
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
def _update_list(path, const_path, src_list, reg_file=True, object_count=0,
|
|
bytes_used=0, obj_list=[]):
|
|
obj_path = strip_obj_storage_path(path, const_path)
|
|
|
|
for i in src_list:
|
|
if obj_path:
|
|
obj_list.append(os.path.join(obj_path, i))
|
|
else:
|
|
obj_list.append(i)
|
|
|
|
object_count += 1
|
|
|
|
if reg_file:
|
|
bytes_used += os.path.getsize(path + '/' + i)
|
|
|
|
return object_count, bytes_used
|
|
|
|
def update_list(path, const_path, dirs=[], files=[], object_count=0,
|
|
bytes_used=0, obj_list=[]):
|
|
object_count, bytes_used = _update_list (path, const_path, files, True,
|
|
object_count, bytes_used,
|
|
obj_list)
|
|
object_count, bytes_used = _update_list (path, const_path, dirs, False,
|
|
object_count, bytes_used,
|
|
obj_list)
|
|
return object_count, bytes_used
|
|
|
|
def get_container_details_from_fs(cont_path, const_path,
|
|
memcache=None):
|
|
"""
|
|
get container details by traversing the filesystem
|
|
"""
|
|
bytes_used = 0
|
|
object_count = 0
|
|
obj_list=[]
|
|
dir_list = []
|
|
|
|
if os.path.isdir(cont_path):
|
|
for (path, dirs, files) in os.walk(cont_path):
|
|
object_count, bytes_used = update_list(path, const_path, dirs, files,
|
|
object_count, bytes_used,
|
|
obj_list)
|
|
|
|
dir_list.append(path + ':' + str(do_stat(path).st_mtime))
|
|
|
|
if memcache:
|
|
memcache.set(strip_obj_storage_path(cont_path), obj_list)
|
|
memcache.set(strip_obj_storage_path(cont_path) + '-dir_list',
|
|
','.join(dir_list))
|
|
memcache.set(strip_obj_storage_path(cont_path) + '-cont_meta',
|
|
[object_count, bytes_used])
|
|
|
|
return obj_list, object_count, bytes_used
|
|
|
|
def get_container_details_from_memcache(cont_path, const_path,
|
|
memcache):
|
|
"""
|
|
get container details stored in memcache
|
|
"""
|
|
|
|
bytes_used = 0
|
|
object_count = 0
|
|
obj_list=[]
|
|
|
|
dir_contents = memcache.get(strip_obj_storage_path(cont_path) + '-dir_list')
|
|
if not dir_contents:
|
|
return get_container_details_from_fs(cont_path, const_path,
|
|
memcache=memcache)
|
|
|
|
for i in dir_contents.split(','):
|
|
path, mtime = i.split(':')
|
|
if mtime != str(do_stat(path).st_mtime):
|
|
return get_container_details_from_fs(cont_path, const_path,
|
|
memcache=memcache)
|
|
|
|
obj_list = memcache.get(strip_obj_storage_path(cont_path))
|
|
|
|
object_count, bytes_used = memcache.get(strip_obj_storage_path(cont_path) + '-cont_meta')
|
|
|
|
return obj_list, object_count, bytes_used
|
|
|
|
def get_container_details(cont_path, memcache=None):
|
|
"""
|
|
Return object_list, object_count and bytes_used.
|
|
"""
|
|
if memcache:
|
|
object_list, object_count, bytes_used = get_container_details_from_memcache(cont_path, cont_path,
|
|
memcache=memcache)
|
|
else:
|
|
object_list, object_count, bytes_used = get_container_details_from_fs(cont_path, cont_path)
|
|
|
|
return object_list, object_count, bytes_used
|
|
|
|
def get_account_details_from_fs(acc_path, memcache=None):
|
|
container_list = []
|
|
container_count = 0
|
|
|
|
if os.path.isdir(acc_path):
|
|
for name in do_listdir(acc_path):
|
|
if not os.path.isdir(acc_path + '/' + name) or \
|
|
name.lower() == 'tmp':
|
|
continue
|
|
container_count += 1
|
|
container_list.append(name)
|
|
|
|
if memcache:
|
|
memcache.set(strip_obj_storage_path(acc_path) + '_container_list', container_list)
|
|
memcache.set(strip_obj_storage_path(acc_path)+'_mtime', str(do_stat(acc_path).st_mtime))
|
|
memcache.set(strip_obj_storage_path(acc_path)+'_container_count', container_count)
|
|
|
|
return container_list, container_count
|
|
|
|
def get_account_details_from_memcache(acc_path, memcache=None):
|
|
if memcache:
|
|
mtime = memcache.get(strip_obj_storage_path(acc_path)+'_mtime')
|
|
if not mtime or mtime != str(do_stat(acc_path).st_mtime):
|
|
return get_account_details_from_fs(acc_path, memcache)
|
|
container_list = memcache.get(strip_obj_storage_path(acc_path) + '_container_list')
|
|
container_count = memcache.get(strip_obj_storage_path(acc_path)+'_container_count')
|
|
return container_list, container_count
|
|
|
|
|
|
def get_account_details(acc_path, memcache=None):
|
|
"""
|
|
Return container_list and container_count.
|
|
"""
|
|
if memcache:
|
|
return get_account_details_from_memcache(acc_path, memcache)
|
|
else:
|
|
return get_account_details_from_fs(acc_path, memcache)
|
|
|
|
|
|
|
|
def get_etag(path):
|
|
etag = None
|
|
if os.path.exists(path):
|
|
etag = md5()
|
|
if not os.path.isdir(path):
|
|
fp = open(path, 'rb')
|
|
if fp:
|
|
while True:
|
|
chunk = fp.read(CHUNK_SIZE)
|
|
if chunk:
|
|
etag.update(chunk)
|
|
else:
|
|
break
|
|
fp.close()
|
|
|
|
etag = etag.hexdigest()
|
|
|
|
return etag
|
|
|
|
|
|
def get_object_metadata(obj_path):
|
|
"""
|
|
Return metadata of object.
|
|
"""
|
|
metadata = {}
|
|
if os.path.exists(obj_path):
|
|
if not os.path.isdir(obj_path):
|
|
metadata = {
|
|
X_TIMESTAMP: normalize_timestamp(os.path.getctime(obj_path)),
|
|
X_CONTENT_TYPE: FILE_TYPE,
|
|
X_ETAG: get_etag(obj_path),
|
|
X_CONTENT_LENGTH: os.path.getsize(obj_path),
|
|
X_TYPE: OBJECT,
|
|
X_OBJECT_TYPE: FILE,
|
|
}
|
|
else:
|
|
metadata = {
|
|
X_TIMESTAMP: normalize_timestamp(os.path.getctime(obj_path)),
|
|
X_CONTENT_TYPE: DIR_TYPE,
|
|
X_ETAG: get_etag(obj_path),
|
|
X_CONTENT_LENGTH: 0,
|
|
X_TYPE: OBJECT,
|
|
X_OBJECT_TYPE: DIR,
|
|
}
|
|
|
|
return metadata
|
|
|
|
def get_container_metadata(cont_path, memcache=None):
|
|
objects = []
|
|
object_count = 0
|
|
bytes_used = 0
|
|
objects, object_count, bytes_used = get_container_details(cont_path,
|
|
memcache=memcache)
|
|
metadata = {X_TYPE: CONTAINER,
|
|
X_TIMESTAMP: normalize_timestamp(os.path.getctime(cont_path)),
|
|
X_PUT_TIMESTAMP: normalize_timestamp(os.path.getmtime(cont_path)),
|
|
X_OBJECTS_COUNT: object_count,
|
|
X_BYTES_USED: bytes_used}
|
|
return metadata
|
|
|
|
def get_account_metadata(acc_path, memcache=None):
|
|
containers = []
|
|
container_count = 0
|
|
containers, container_count = get_account_details(acc_path, memcache)
|
|
metadata = {X_TYPE: ACCOUNT,
|
|
X_TIMESTAMP: normalize_timestamp(os.path.getctime(acc_path)),
|
|
X_PUT_TIMESTAMP: normalize_timestamp(os.path.getmtime(acc_path)),
|
|
X_OBJECTS_COUNT: 0,
|
|
X_BYTES_USED: 0,
|
|
X_CONTAINER_COUNT: container_count}
|
|
return metadata
|
|
|
|
def restore_object(obj_path, metadata):
|
|
meta = read_metadata(obj_path)
|
|
if meta:
|
|
meta.update(metadata)
|
|
write_metadata(obj_path, meta)
|
|
else:
|
|
write_metadata(obj_path, metadata)
|
|
|
|
def restore_container(cont_path, metadata):
|
|
meta = read_metadata(cont_path)
|
|
if meta:
|
|
meta.update(metadata)
|
|
write_metadata(cont_path, meta)
|
|
else:
|
|
write_metadata(cont_path, metadata)
|
|
|
|
def restore_account(acc_path, metadata):
|
|
meta = read_metadata(acc_path)
|
|
if meta:
|
|
meta.update(metadata)
|
|
write_metadata(acc_path, meta)
|
|
else:
|
|
write_metadata(acc_path, metadata)
|
|
|
|
def create_object_metadata(obj_path):
|
|
meta = get_object_metadata(obj_path)
|
|
restore_object(obj_path, meta)
|
|
return meta
|
|
|
|
def create_container_metadata(cont_path, memcache=None):
|
|
meta = get_container_metadata(cont_path, memcache)
|
|
restore_container(cont_path, meta)
|
|
return meta
|
|
|
|
def create_account_metadata(acc_path, memcache=None):
|
|
meta = get_account_metadata(acc_path, memcache)
|
|
restore_account(acc_path, meta)
|
|
return meta
|
|
|
|
|
|
def check_account_exists(account, fs_object):
|
|
if account not in get_account_list(fs_object):
|
|
logging.error('Account not exists %s' % account)
|
|
return False
|
|
else:
|
|
return True
|
|
|
|
def get_account_list(fs_object):
|
|
account_list = []
|
|
if fs_object:
|
|
account_list = fs_object.get_export_list()
|
|
return account_list
|
|
|
|
|
|
def get_account_id(account):
|
|
return RESELLER_PREFIX + md5(account + HASH_PATH_SUFFIX).hexdigest()
|
|
|