Use keystone service catalog for getting auth urls
Co-Authored-By: Andrey Pavlov <apavlov@mirantis.com> Change-Id: I6c64f4b975d12ccaca564d248f9ccbb02615e19f Closes-bug: #1371856
This commit is contained in:
parent
bfa73fcc5f
commit
4eade5480e
@ -42,7 +42,6 @@ class Context(context.RequestContext):
|
|||||||
roles=None,
|
roles=None,
|
||||||
is_admin=None,
|
is_admin=None,
|
||||||
remote_semaphore=None,
|
remote_semaphore=None,
|
||||||
auth_uri=None,
|
|
||||||
resource_uuid=None,
|
resource_uuid=None,
|
||||||
current_instance_info=None,
|
current_instance_info=None,
|
||||||
request_id=None,
|
request_id=None,
|
||||||
@ -64,10 +63,6 @@ class Context(context.RequestContext):
|
|||||||
self.remote_semaphore = remote_semaphore or semaphore.Semaphore(
|
self.remote_semaphore = remote_semaphore or semaphore.Semaphore(
|
||||||
CONF.cluster_remote_threshold)
|
CONF.cluster_remote_threshold)
|
||||||
self.roles = roles
|
self.roles = roles
|
||||||
if auth_uri:
|
|
||||||
self.auth_uri = auth_uri
|
|
||||||
else:
|
|
||||||
self.auth_uri = _get_auth_uri()
|
|
||||||
if overwrite or not hasattr(context._request_store, 'context'):
|
if overwrite or not hasattr(context._request_store, 'context'):
|
||||||
self.update_store()
|
self.update_store()
|
||||||
|
|
||||||
@ -87,7 +82,6 @@ class Context(context.RequestContext):
|
|||||||
self.roles,
|
self.roles,
|
||||||
self.is_admin,
|
self.is_admin,
|
||||||
self.remote_semaphore,
|
self.remote_semaphore,
|
||||||
self.auth_uri,
|
|
||||||
self.resource_uuid,
|
self.resource_uuid,
|
||||||
self.current_instance_info,
|
self.current_instance_info,
|
||||||
self.request_id,
|
self.request_id,
|
||||||
@ -105,7 +99,6 @@ class Context(context.RequestContext):
|
|||||||
'project_name': self.tenant_name,
|
'project_name': self.tenant_name,
|
||||||
'is_admin': self.is_admin,
|
'is_admin': self.is_admin,
|
||||||
'roles': self.roles,
|
'roles': self.roles,
|
||||||
'auth_uri': self.auth_uri,
|
|
||||||
'resource_uuid': self.resource_uuid,
|
'resource_uuid': self.resource_uuid,
|
||||||
'request_id': self.request_id,
|
'request_id': self.request_id,
|
||||||
}
|
}
|
||||||
@ -167,28 +160,6 @@ def set_ctx(new_ctx):
|
|||||||
setattr(context._request_store, 'context', new_ctx)
|
setattr(context._request_store, 'context', new_ctx)
|
||||||
|
|
||||||
|
|
||||||
def _get_auth_uri():
|
|
||||||
if CONF.keystone_authtoken.auth_uri is not None:
|
|
||||||
auth_uri = CONF.keystone_authtoken.auth_uri
|
|
||||||
else:
|
|
||||||
if CONF.keystone_authtoken.identity_uri is not None:
|
|
||||||
identity_uri = CONF.keystone_authtoken.identity_uri
|
|
||||||
else:
|
|
||||||
host = CONF.keystone_authtoken.auth_host
|
|
||||||
port = CONF.keystone_authtoken.auth_port
|
|
||||||
protocol = CONF.keystone_authtoken.auth_protocol
|
|
||||||
identity_uri = '%s://%s:%s' % (protocol, host, port)
|
|
||||||
|
|
||||||
if CONF.use_identity_api_v3 is False:
|
|
||||||
auth_version = 'v2.0'
|
|
||||||
else:
|
|
||||||
auth_version = 'v3'
|
|
||||||
|
|
||||||
auth_uri = '%s/%s' % (identity_uri, auth_version)
|
|
||||||
|
|
||||||
return auth_uri
|
|
||||||
|
|
||||||
|
|
||||||
def _wrapper(ctx, thread_description, thread_group, func, *args, **kwargs):
|
def _wrapper(ctx, thread_description, thread_group, func, *args, **kwargs):
|
||||||
try:
|
try:
|
||||||
set_ctx(ctx)
|
set_ctx(ctx)
|
||||||
|
@ -156,5 +156,4 @@ def use_os_admin_auth_token(cluster):
|
|||||||
ctx.tenant_id = cluster.tenant_id
|
ctx.tenant_id = cluster.tenant_id
|
||||||
client = keystone.client_for_admin_from_trust(cluster.trust_id)
|
client = keystone.client_for_admin_from_trust(cluster.trust_id)
|
||||||
ctx.auth_token = client.auth_token
|
ctx.auth_token = client.auth_token
|
||||||
ctx.service_catalog = json.dumps(
|
ctx.service_catalog = json.dumps(client.service_catalog.get_data())
|
||||||
client.service_catalog.catalog['catalog'])
|
|
||||||
|
@ -18,9 +18,7 @@ from oslo_config import cfg
|
|||||||
import six
|
import six
|
||||||
from six.moves.urllib import parse as urlparse
|
from six.moves.urllib import parse as urlparse
|
||||||
|
|
||||||
from sahara import context
|
|
||||||
from sahara.utils.openstack import base as clients_base
|
from sahara.utils.openstack import base as clients_base
|
||||||
from sahara.utils.openstack import keystone as k
|
|
||||||
|
|
||||||
|
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
@ -35,8 +33,7 @@ def retrieve_auth_url():
|
|||||||
|
|
||||||
Hadoop Swift library doesn't support keystone v3 api.
|
Hadoop Swift library doesn't support keystone v3 api.
|
||||||
"""
|
"""
|
||||||
auth_url = clients_base.url_for(context.current().service_catalog,
|
auth_url = clients_base.retrieve_auth_url()
|
||||||
'identity')
|
|
||||||
info = urlparse.urlparse(auth_url)
|
info = urlparse.urlparse(auth_url)
|
||||||
|
|
||||||
if CONF.use_domain_for_proxy_users:
|
if CONF.use_domain_for_proxy_users:
|
||||||
@ -56,22 +53,6 @@ def retrieve_auth_url():
|
|||||||
url=url)
|
url=url)
|
||||||
|
|
||||||
|
|
||||||
def retrieve_preauth_url():
|
|
||||||
'''This function returns the storage URL for Swift in the current project.
|
|
||||||
|
|
||||||
:returns: The storage URL for the current project's Swift store, or None
|
|
||||||
if it can't be found.
|
|
||||||
|
|
||||||
'''
|
|
||||||
client = k.client()
|
|
||||||
catalog = clients_base.execute_with_retries(
|
|
||||||
client.service_catalog.get_endpoints, 'object-store')
|
|
||||||
for ep in catalog.get('object-store'):
|
|
||||||
if ep.get('interface') == 'public':
|
|
||||||
return ep.get('url')
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def inject_swift_url_suffix(url):
|
def inject_swift_url_suffix(url):
|
||||||
if isinstance(url, six.string_types) and url.startswith("swift://"):
|
if isinstance(url, six.string_types) and url.startswith("swift://"):
|
||||||
u = urlparse.urlparse(url)
|
u = urlparse.urlparse(url)
|
||||||
|
@ -33,7 +33,7 @@ SERVICE_SPECIFIC = ["auth.url", "tenant",
|
|||||||
|
|
||||||
|
|
||||||
class SwiftIntegrationTestCase(base.SaharaTestCase):
|
class SwiftIntegrationTestCase(base.SaharaTestCase):
|
||||||
@mock.patch('sahara.utils.openstack.base.url_for')
|
@mock.patch('sahara.utils.openstack.base.retrieve_auth_url')
|
||||||
def test_get_swift_configs(self, url_for_mock):
|
def test_get_swift_configs(self, url_for_mock):
|
||||||
url_for_mock.return_value = 'http://localhost:5000/v2.0'
|
url_for_mock.return_value = 'http://localhost:5000/v2.0'
|
||||||
self.setup_context(tenant_name='test_tenant')
|
self.setup_context(tenant_name='test_tenant')
|
||||||
|
@ -25,7 +25,7 @@ class SwiftUtilsTest(testbase.SaharaTestCase):
|
|||||||
super(SwiftUtilsTest, self).setUp()
|
super(SwiftUtilsTest, self).setUp()
|
||||||
self.override_config('use_identity_api_v3', True)
|
self.override_config('use_identity_api_v3', True)
|
||||||
|
|
||||||
@mock.patch('sahara.utils.openstack.base.url_for')
|
@mock.patch('sahara.utils.openstack.base.retrieve_auth_url')
|
||||||
def test_retrieve_auth_url(self, url_for_mock):
|
def test_retrieve_auth_url(self, url_for_mock):
|
||||||
correct = "https://127.0.0.1:8080/v2.0/"
|
correct = "https://127.0.0.1:8080/v2.0/"
|
||||||
|
|
||||||
@ -41,7 +41,7 @@ class SwiftUtilsTest(testbase.SaharaTestCase):
|
|||||||
_assert("https://127.0.0.1:8080/v42/")
|
_assert("https://127.0.0.1:8080/v42/")
|
||||||
_assert("https://127.0.0.1:8080/foo")
|
_assert("https://127.0.0.1:8080/foo")
|
||||||
|
|
||||||
@mock.patch('sahara.utils.openstack.base.url_for')
|
@mock.patch('sahara.utils.openstack.base.retrieve_auth_url')
|
||||||
def test_retrieve_auth_url_without_port(self, url_for_mock):
|
def test_retrieve_auth_url_without_port(self, url_for_mock):
|
||||||
correct = "https://127.0.0.1/v2.0/"
|
correct = "https://127.0.0.1/v2.0/"
|
||||||
|
|
||||||
|
@ -13,7 +13,6 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
import functools
|
|
||||||
import random
|
import random
|
||||||
|
|
||||||
import fixtures
|
import fixtures
|
||||||
@ -23,7 +22,6 @@ import testtools
|
|||||||
|
|
||||||
from sahara import context
|
from sahara import context
|
||||||
from sahara import exceptions as ex
|
from sahara import exceptions as ex
|
||||||
from sahara.tests.unit import base as test_base
|
|
||||||
|
|
||||||
|
|
||||||
rnd = random.Random()
|
rnd = random.Random()
|
||||||
@ -137,31 +135,3 @@ class ContextTest(testtools.TestCase):
|
|||||||
|
|
||||||
class TestException(Exception):
|
class TestException(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class GetAuthURITest(test_base.SaharaTestCase):
|
|
||||||
def setUp(self):
|
|
||||||
super(GetAuthURITest, self).setUp()
|
|
||||||
|
|
||||||
self.override_auth_config = functools.partial(
|
|
||||||
self.override_config, group='keystone_authtoken')
|
|
||||||
|
|
||||||
def test_get_auth_url_from_auth_uri_param(self):
|
|
||||||
self.override_auth_config('auth_uri', 'http://pony:5000/v2.0')
|
|
||||||
self.assertEqual('http://pony:5000/v2.0', context._get_auth_uri())
|
|
||||||
|
|
||||||
def test_get_auth_uri_from_identity_uri(self):
|
|
||||||
self.override_auth_config('identity_uri', 'http://spam:35357')
|
|
||||||
self.assertEqual('http://spam:35357/v3', context._get_auth_uri())
|
|
||||||
|
|
||||||
self.override_config('use_identity_api_v3', False)
|
|
||||||
self.assertEqual('http://spam:35357/v2.0', context._get_auth_uri())
|
|
||||||
|
|
||||||
def test_get_auth_uri_from_auth_params(self):
|
|
||||||
self.override_auth_config('auth_host', 'eggs')
|
|
||||||
self.override_auth_config('auth_port', 12345)
|
|
||||||
self.override_auth_config('auth_protocol', 'http')
|
|
||||||
self.assertEqual('http://eggs:12345/v3', context._get_auth_uri())
|
|
||||||
|
|
||||||
self.override_config('use_identity_api_v3', False)
|
|
||||||
self.assertEqual('http://eggs:12345/v2.0', context._get_auth_uri())
|
|
||||||
|
@ -58,10 +58,10 @@ class AuthUrlTest(testbase.SaharaTestCase):
|
|||||||
|
|
||||||
def test_retrieve_auth_url_api_v3(self):
|
def test_retrieve_auth_url_api_v3(self):
|
||||||
self.override_config('use_identity_api_v3', True)
|
self.override_config('use_identity_api_v3', True)
|
||||||
correct = "https://127.0.0.1:8080/v3/"
|
correct = "https://127.0.0.1:8080/v3"
|
||||||
|
|
||||||
def _assert(uri):
|
def _assert(uri):
|
||||||
self.setup_context(auth_uri=uri)
|
self.override_config('auth_uri', uri, 'keystone_authtoken')
|
||||||
self.assertEqual(correct, base.retrieve_auth_url())
|
self.assertEqual(correct, base.retrieve_auth_url())
|
||||||
|
|
||||||
_assert("%s/" % correct)
|
_assert("%s/" % correct)
|
||||||
@ -74,12 +74,14 @@ class AuthUrlTest(testbase.SaharaTestCase):
|
|||||||
_assert("https://127.0.0.1:8080/v42")
|
_assert("https://127.0.0.1:8080/v42")
|
||||||
_assert("https://127.0.0.1:8080/v42/")
|
_assert("https://127.0.0.1:8080/v42/")
|
||||||
|
|
||||||
def test_retrieve_auth_url_api_v3_without_port(self):
|
@mock.patch("sahara.utils.openstack.base.url_for")
|
||||||
|
def test_retrieve_auth_url_api_v3_without_port(self, mock_url_for):
|
||||||
self.override_config('use_identity_api_v3', True)
|
self.override_config('use_identity_api_v3', True)
|
||||||
correct = "https://127.0.0.1/v3/"
|
self.setup_context(service_catalog=True)
|
||||||
|
correct = "https://127.0.0.1/v3"
|
||||||
|
|
||||||
def _assert(uri):
|
def _assert(uri):
|
||||||
self.setup_context(auth_uri=uri)
|
mock_url_for.return_value = uri
|
||||||
self.assertEqual(correct, base.retrieve_auth_url())
|
self.assertEqual(correct, base.retrieve_auth_url())
|
||||||
|
|
||||||
_assert("%s/" % correct)
|
_assert("%s/" % correct)
|
||||||
@ -94,10 +96,10 @@ class AuthUrlTest(testbase.SaharaTestCase):
|
|||||||
|
|
||||||
def test_retrieve_auth_url_api_v20(self):
|
def test_retrieve_auth_url_api_v20(self):
|
||||||
self.override_config('use_identity_api_v3', False)
|
self.override_config('use_identity_api_v3', False)
|
||||||
correct = "https://127.0.0.1:8080/v2.0/"
|
correct = "https://127.0.0.1:8080/v2.0"
|
||||||
|
|
||||||
def _assert(uri):
|
def _assert(uri):
|
||||||
self.setup_context(auth_uri=uri)
|
self.override_config('auth_uri', uri, 'keystone_authtoken')
|
||||||
self.assertEqual(correct, base.retrieve_auth_url())
|
self.assertEqual(correct, base.retrieve_auth_url())
|
||||||
|
|
||||||
_assert("%s/" % correct)
|
_assert("%s/" % correct)
|
||||||
@ -110,12 +112,14 @@ class AuthUrlTest(testbase.SaharaTestCase):
|
|||||||
_assert("https://127.0.0.1:8080/v42")
|
_assert("https://127.0.0.1:8080/v42")
|
||||||
_assert("https://127.0.0.1:8080/v42/")
|
_assert("https://127.0.0.1:8080/v42/")
|
||||||
|
|
||||||
def test_retrieve_auth_url_api_v20_without_port(self):
|
@mock.patch("sahara.utils.openstack.base.url_for")
|
||||||
|
def test_retrieve_auth_url_api_v20_without_port(self, mock_url_for):
|
||||||
self.override_config('use_identity_api_v3', False)
|
self.override_config('use_identity_api_v3', False)
|
||||||
correct = "https://127.0.0.1/v2.0/"
|
self.setup_context(service_catalog=True)
|
||||||
|
correct = "https://127.0.0.1/v2.0"
|
||||||
|
|
||||||
def _assert(uri):
|
def _assert(uri):
|
||||||
self.setup_context(auth_uri=uri)
|
mock_url_for.return_value = uri
|
||||||
self.assertEqual(correct, base.retrieve_auth_url())
|
self.assertEqual(correct, base.retrieve_auth_url())
|
||||||
|
|
||||||
_assert("%s/" % correct)
|
_assert("%s/" % correct)
|
||||||
|
@ -29,6 +29,8 @@ class FakeImage(object):
|
|||||||
class TestImages(base.SaharaTestCase):
|
class TestImages(base.SaharaTestCase):
|
||||||
@mock.patch('sahara.utils.openstack.base.url_for', return_value='')
|
@mock.patch('sahara.utils.openstack.base.url_for', return_value='')
|
||||||
def test_list_registered_images(self, url_for_mock):
|
def test_list_registered_images(self, url_for_mock):
|
||||||
|
self.override_config('auth_uri', 'https://127.0.0.1:8080/v3/',
|
||||||
|
'keystone_authtoken')
|
||||||
some_images = [
|
some_images = [
|
||||||
FakeImage('foo', ['bar', 'baz'], 'test'),
|
FakeImage('foo', ['bar', 'baz'], 'test'),
|
||||||
FakeImage('baz', [], 'test'),
|
FakeImage('baz', [], 'test'),
|
||||||
|
@ -13,6 +13,8 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
from keystoneclient import exceptions as keystone_ex
|
||||||
|
from keystoneclient import service_catalog as keystone_service_catalog
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
from oslo_log import log as logging
|
from oslo_log import log as logging
|
||||||
from oslo_serialization import jsonutils as json
|
from oslo_serialization import jsonutils as json
|
||||||
@ -20,7 +22,6 @@ from six.moves.urllib import parse as urlparse
|
|||||||
|
|
||||||
from sahara import context
|
from sahara import context
|
||||||
from sahara import exceptions as ex
|
from sahara import exceptions as ex
|
||||||
from sahara.i18n import _
|
|
||||||
from sahara.i18n import _LE
|
from sahara.i18n import _LE
|
||||||
from sahara.i18n import _LW
|
from sahara.i18n import _LW
|
||||||
|
|
||||||
@ -47,74 +48,32 @@ CONF.register_group(retries)
|
|||||||
CONF.register_opts(opts, group=retries)
|
CONF.register_opts(opts, group=retries)
|
||||||
|
|
||||||
|
|
||||||
def url_for(service_catalog, service_type, admin=False, endpoint_type=None):
|
def url_for(service_catalog=None, service_type='identity',
|
||||||
if not endpoint_type:
|
endpoint_type='publicURL'):
|
||||||
endpoint_type = 'publicURL'
|
if not service_catalog:
|
||||||
if admin:
|
service_catalog = context.current().service_catalog
|
||||||
endpoint_type = 'adminURL'
|
try:
|
||||||
|
return keystone_service_catalog.ServiceCatalogV2(
|
||||||
service = _get_service_from_catalog(service_catalog, service_type)
|
{'serviceCatalog': json.loads(service_catalog)}).url_for(
|
||||||
|
service_type=service_type, endpoint_type=endpoint_type,
|
||||||
if service:
|
region_name=CONF.os_region_name)
|
||||||
endpoints = service['endpoints']
|
except keystone_ex.EndpointNotFound:
|
||||||
if CONF.os_region_name:
|
ctx = context.current()
|
||||||
endpoints = [e for e in endpoints
|
return keystone_service_catalog.ServiceCatalogV3(
|
||||||
if e['region'] == CONF.os_region_name]
|
ctx.auth_token,
|
||||||
try:
|
{'catalog': json.loads(service_catalog)}).url_for(
|
||||||
return _get_endpoint_url(endpoints, endpoint_type)
|
service_type=service_type, endpoint_type=endpoint_type,
|
||||||
except Exception:
|
region_name=CONF.os_region_name)
|
||||||
raise ex.SystemError(
|
|
||||||
_("Endpoint with type %(type)s is not found for service "
|
|
||||||
"%(service)s")
|
|
||||||
% {'type': endpoint_type,
|
|
||||||
'service': service_type})
|
|
||||||
|
|
||||||
else:
|
|
||||||
raise ex.SystemError(
|
|
||||||
_('Service "%s" not found in service catalog') % service_type)
|
|
||||||
|
|
||||||
|
|
||||||
def _get_service_from_catalog(catalog, service_type):
|
|
||||||
if catalog:
|
|
||||||
catalog = json.loads(catalog)
|
|
||||||
for service in catalog:
|
|
||||||
if service['type'] == service_type:
|
|
||||||
return service
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def _get_endpoint_url(endpoints, endpoint_type):
|
|
||||||
if 'interface' in endpoints[0]:
|
|
||||||
endpoint_type = endpoint_type[0:-3]
|
|
||||||
for endpoint in endpoints:
|
|
||||||
if endpoint['interface'] == endpoint_type:
|
|
||||||
return endpoint['url']
|
|
||||||
return _get_case_insensitive(endpoints[0], endpoint_type)
|
|
||||||
|
|
||||||
|
|
||||||
def _get_case_insensitive(dictionary, key):
|
|
||||||
for k, v in dictionary.items():
|
|
||||||
if str(k).lower() == str(key).lower():
|
|
||||||
return v
|
|
||||||
|
|
||||||
# this will raise an exception as usual if key was not found
|
|
||||||
return dictionary[key]
|
|
||||||
|
|
||||||
|
|
||||||
def retrieve_auth_url():
|
def retrieve_auth_url():
|
||||||
info = urlparse.urlparse(context.current().auth_uri)
|
|
||||||
version = 'v3' if CONF.use_identity_api_v3 else 'v2.0'
|
version = 'v3' if CONF.use_identity_api_v3 else 'v2.0'
|
||||||
|
ctx = context.current()
|
||||||
if info.port:
|
if ctx.service_catalog:
|
||||||
return "%s://%s:%s/%s/" % (info.scheme,
|
info = urlparse.urlparse(url_for(ctx.service_catalog, 'identity'))
|
||||||
info.hostname,
|
|
||||||
info.port,
|
|
||||||
version)
|
|
||||||
else:
|
else:
|
||||||
return "%s://%s/%s/" % (info.scheme,
|
info = urlparse.urlparse(CONF.keystone_authtoken.auth_uri)
|
||||||
info.hostname,
|
return "%s://%s/%s" % (info[:2] + (version,))
|
||||||
version)
|
|
||||||
|
|
||||||
|
|
||||||
def execute_with_retries(method, *args, **kwargs):
|
def execute_with_retries(method, *args, **kwargs):
|
||||||
|
@ -18,6 +18,7 @@ import swiftclient
|
|||||||
|
|
||||||
from sahara.swift import swift_helper as sh
|
from sahara.swift import swift_helper as sh
|
||||||
from sahara.swift import utils as su
|
from sahara.swift import utils as su
|
||||||
|
from sahara.utils.openstack import base
|
||||||
from sahara.utils.openstack import keystone as k
|
from sahara.utils.openstack import keystone as k
|
||||||
|
|
||||||
opts = [
|
opts = [
|
||||||
@ -76,7 +77,8 @@ def client_from_token(token):
|
|||||||
return swiftclient.Connection(auth_version='2.0',
|
return swiftclient.Connection(auth_version='2.0',
|
||||||
cacert=CONF.swift.ca_file,
|
cacert=CONF.swift.ca_file,
|
||||||
insecure=CONF.swift.api_insecure,
|
insecure=CONF.swift.api_insecure,
|
||||||
preauthurl=su.retrieve_preauth_url(),
|
preauthurl=base.url_for(
|
||||||
|
service_type="object-store"),
|
||||||
preauthtoken=token,
|
preauthtoken=token,
|
||||||
retries=CONF.retries.retries_number,
|
retries=CONF.retries.retries_number,
|
||||||
retry_on_ratelimit=True,
|
retry_on_ratelimit=True,
|
||||||
|
Loading…
Reference in New Issue
Block a user