Add osprofiler option to trace operations
Add --profile option, initialize osprofiler, and pass appropriate headers to the mistral server. Change-Id: Ib10a126902c707bd82c3fadff94e119fb18cf096 Implements: blueprint mistral-osprofiler
This commit is contained in:
@@ -20,7 +20,8 @@ from mistralclient.api.v2 import client as client_v2
|
|||||||
def client(mistral_url=None, username=None, api_key=None,
|
def client(mistral_url=None, username=None, api_key=None,
|
||||||
project_name=None, auth_url=None, project_id=None,
|
project_name=None, auth_url=None, project_id=None,
|
||||||
endpoint_type='publicURL', service_type='workflow',
|
endpoint_type='publicURL', service_type='workflow',
|
||||||
auth_token=None, user_id=None, cacert=None, insecure=False):
|
auth_token=None, user_id=None, cacert=None, insecure=False,
|
||||||
|
profile=None):
|
||||||
|
|
||||||
if mistral_url and not isinstance(mistral_url, six.string_types):
|
if mistral_url and not isinstance(mistral_url, six.string_types):
|
||||||
raise RuntimeError('Mistral url should be a string.')
|
raise RuntimeError('Mistral url should be a string.')
|
||||||
@@ -37,7 +38,8 @@ def client(mistral_url=None, username=None, api_key=None,
|
|||||||
auth_token=auth_token,
|
auth_token=auth_token,
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
cacert=cacert,
|
cacert=cacert,
|
||||||
insecure=insecure
|
insecure=insecure,
|
||||||
|
profile=profile
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@@ -20,6 +20,8 @@ import requests
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
import osprofiler.web
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -106,4 +108,7 @@ class HTTPClient(object):
|
|||||||
if user_id:
|
if user_id:
|
||||||
headers['X-User-Id'] = user_id
|
headers['X-User-Id'] = user_id
|
||||||
|
|
||||||
|
# Add headers for osprofiler.
|
||||||
|
headers.update(osprofiler.web.get_trace_id_headers())
|
||||||
|
|
||||||
return headers
|
return headers
|
||||||
|
@@ -15,6 +15,8 @@
|
|||||||
|
|
||||||
import six
|
import six
|
||||||
|
|
||||||
|
import osprofiler.profiler
|
||||||
|
|
||||||
from mistralclient.api import httpclient
|
from mistralclient.api import httpclient
|
||||||
from mistralclient.api.v2 import action_executions
|
from mistralclient.api.v2 import action_executions
|
||||||
from mistralclient.api.v2 import actions
|
from mistralclient.api.v2 import actions
|
||||||
@@ -32,7 +34,8 @@ class Client(object):
|
|||||||
def __init__(self, mistral_url=None, username=None, api_key=None,
|
def __init__(self, mistral_url=None, username=None, api_key=None,
|
||||||
project_name=None, auth_url=None, project_id=None,
|
project_name=None, auth_url=None, project_id=None,
|
||||||
endpoint_type='publicURL', service_type='workflowv2',
|
endpoint_type='publicURL', service_type='workflowv2',
|
||||||
auth_token=None, user_id=None, cacert=None, insecure=False):
|
auth_token=None, user_id=None, cacert=None, insecure=False,
|
||||||
|
profile=None):
|
||||||
|
|
||||||
if mistral_url and not isinstance(mistral_url, six.string_types):
|
if mistral_url and not isinstance(mistral_url, six.string_types):
|
||||||
raise RuntimeError('Mistral url should be string')
|
raise RuntimeError('Mistral url should be string')
|
||||||
@@ -58,6 +61,9 @@ class Client(object):
|
|||||||
if not mistral_url:
|
if not mistral_url:
|
||||||
mistral_url = "http://localhost:8989/v2"
|
mistral_url = "http://localhost:8989/v2"
|
||||||
|
|
||||||
|
if profile:
|
||||||
|
osprofiler.profiler.init(profile)
|
||||||
|
|
||||||
self.http_client = httpclient.HTTPClient(
|
self.http_client = httpclient.HTTPClient(
|
||||||
mistral_url,
|
mistral_url,
|
||||||
auth_token,
|
auth_token,
|
||||||
|
@@ -142,18 +142,21 @@ class MistralShell(app.App):
|
|||||||
:paramtype extra_kwargs: dict
|
:paramtype extra_kwargs: dict
|
||||||
"""
|
"""
|
||||||
argparse_kwargs = argparse_kwargs or {}
|
argparse_kwargs = argparse_kwargs or {}
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description=description,
|
description=description,
|
||||||
add_help=False,
|
add_help=False,
|
||||||
formatter_class=OpenStackHelpFormatter,
|
formatter_class=OpenStackHelpFormatter,
|
||||||
**argparse_kwargs
|
**argparse_kwargs
|
||||||
)
|
)
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--version',
|
'--version',
|
||||||
action='version',
|
action='version',
|
||||||
version='%(prog)s {0}'.format(version),
|
version='%(prog)s {0}'.format(version),
|
||||||
help='Show program\'s version number and exit.'
|
help='Show program\'s version number and exit.'
|
||||||
)
|
)
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'-v', '--verbose',
|
'-v', '--verbose',
|
||||||
action='count',
|
action='count',
|
||||||
@@ -161,12 +164,14 @@ class MistralShell(app.App):
|
|||||||
default=self.DEFAULT_VERBOSE_LEVEL,
|
default=self.DEFAULT_VERBOSE_LEVEL,
|
||||||
help='Increase verbosity of output. Can be repeated.',
|
help='Increase verbosity of output. Can be repeated.',
|
||||||
)
|
)
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--log-file',
|
'--log-file',
|
||||||
action='store',
|
action='store',
|
||||||
default=None,
|
default=None,
|
||||||
help='Specify a file to log output. Disabled by default.',
|
help='Specify a file to log output. Disabled by default.',
|
||||||
)
|
)
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'-q', '--quiet',
|
'-q', '--quiet',
|
||||||
action='store_const',
|
action='store_const',
|
||||||
@@ -174,6 +179,7 @@ class MistralShell(app.App):
|
|||||||
const=0,
|
const=0,
|
||||||
help='Suppress output except warnings and errors.',
|
help='Suppress output except warnings and errors.',
|
||||||
)
|
)
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'-h', '--help',
|
'-h', '--help',
|
||||||
action=HelpAction,
|
action=HelpAction,
|
||||||
@@ -181,12 +187,14 @@ class MistralShell(app.App):
|
|||||||
default=self, # tricky
|
default=self, # tricky
|
||||||
help="Show this help message and exit.",
|
help="Show this help message and exit.",
|
||||||
)
|
)
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--debug',
|
'--debug',
|
||||||
default=False,
|
default=False,
|
||||||
action='store_true',
|
action='store_true',
|
||||||
help='Show tracebacks on errors.',
|
help='Show tracebacks on errors.',
|
||||||
)
|
)
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--os-mistral-url',
|
'--os-mistral-url',
|
||||||
action='store',
|
action='store',
|
||||||
@@ -194,6 +202,7 @@ class MistralShell(app.App):
|
|||||||
default=c.env('OS_MISTRAL_URL'),
|
default=c.env('OS_MISTRAL_URL'),
|
||||||
help='Mistral API host (Env: OS_MISTRAL_URL)'
|
help='Mistral API host (Env: OS_MISTRAL_URL)'
|
||||||
)
|
)
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--os-mistral-version',
|
'--os-mistral-version',
|
||||||
action='store',
|
action='store',
|
||||||
@@ -202,6 +211,7 @@ class MistralShell(app.App):
|
|||||||
help='Mistral API version (default = v2) (Env: '
|
help='Mistral API version (default = v2) (Env: '
|
||||||
'OS_MISTRAL_VERSION)'
|
'OS_MISTRAL_VERSION)'
|
||||||
)
|
)
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--os-mistral-service-type',
|
'--os-mistral-service-type',
|
||||||
action='store',
|
action='store',
|
||||||
@@ -211,6 +221,7 @@ class MistralShell(app.App):
|
|||||||
'keystone-endpoint) (default = workflowv2) (Env: '
|
'keystone-endpoint) (default = workflowv2) (Env: '
|
||||||
'OS_MISTRAL_SERVICE_TYPE)'
|
'OS_MISTRAL_SERVICE_TYPE)'
|
||||||
)
|
)
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--os-mistral-endpoint-type',
|
'--os-mistral-endpoint-type',
|
||||||
action='store',
|
action='store',
|
||||||
@@ -220,6 +231,7 @@ class MistralShell(app.App):
|
|||||||
'keystone-endpoint) (default = publicURL) (Env: '
|
'keystone-endpoint) (default = publicURL) (Env: '
|
||||||
'OS_MISTRAL_ENDPOINT_TYPE)'
|
'OS_MISTRAL_ENDPOINT_TYPE)'
|
||||||
)
|
)
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--os-username',
|
'--os-username',
|
||||||
action='store',
|
action='store',
|
||||||
@@ -227,6 +239,7 @@ class MistralShell(app.App):
|
|||||||
default=c.env('OS_USERNAME', default='admin'),
|
default=c.env('OS_USERNAME', default='admin'),
|
||||||
help='Authentication username (Env: OS_USERNAME)'
|
help='Authentication username (Env: OS_USERNAME)'
|
||||||
)
|
)
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--os-password',
|
'--os-password',
|
||||||
action='store',
|
action='store',
|
||||||
@@ -234,6 +247,7 @@ class MistralShell(app.App):
|
|||||||
default=c.env('OS_PASSWORD'),
|
default=c.env('OS_PASSWORD'),
|
||||||
help='Authentication password (Env: OS_PASSWORD)'
|
help='Authentication password (Env: OS_PASSWORD)'
|
||||||
)
|
)
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--os-tenant-id',
|
'--os-tenant-id',
|
||||||
action='store',
|
action='store',
|
||||||
@@ -241,6 +255,7 @@ class MistralShell(app.App):
|
|||||||
default=c.env('OS_TENANT_ID'),
|
default=c.env('OS_TENANT_ID'),
|
||||||
help='Authentication tenant identifier (Env: OS_TENANT_ID)'
|
help='Authentication tenant identifier (Env: OS_TENANT_ID)'
|
||||||
)
|
)
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--os-tenant-name',
|
'--os-tenant-name',
|
||||||
action='store',
|
action='store',
|
||||||
@@ -248,6 +263,7 @@ class MistralShell(app.App):
|
|||||||
default=c.env('OS_TENANT_NAME', 'Default'),
|
default=c.env('OS_TENANT_NAME', 'Default'),
|
||||||
help='Authentication tenant name (Env: OS_TENANT_NAME)'
|
help='Authentication tenant name (Env: OS_TENANT_NAME)'
|
||||||
)
|
)
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--os-auth-token',
|
'--os-auth-token',
|
||||||
action='store',
|
action='store',
|
||||||
@@ -255,6 +271,7 @@ class MistralShell(app.App):
|
|||||||
default=c.env('OS_AUTH_TOKEN'),
|
default=c.env('OS_AUTH_TOKEN'),
|
||||||
help='Authentication token (Env: OS_AUTH_TOKEN)'
|
help='Authentication token (Env: OS_AUTH_TOKEN)'
|
||||||
)
|
)
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--os-auth-url',
|
'--os-auth-url',
|
||||||
action='store',
|
action='store',
|
||||||
@@ -262,6 +279,7 @@ class MistralShell(app.App):
|
|||||||
default=c.env('OS_AUTH_URL'),
|
default=c.env('OS_AUTH_URL'),
|
||||||
help='Authentication URL (Env: OS_AUTH_URL)'
|
help='Authentication URL (Env: OS_AUTH_URL)'
|
||||||
)
|
)
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--os-cacert',
|
'--os-cacert',
|
||||||
action='store',
|
action='store',
|
||||||
@@ -269,6 +287,7 @@ class MistralShell(app.App):
|
|||||||
default=c.env('OS_CACERT'),
|
default=c.env('OS_CACERT'),
|
||||||
help='Authentication CA Certificate (Env: OS_CACERT)'
|
help='Authentication CA Certificate (Env: OS_CACERT)'
|
||||||
)
|
)
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--insecure',
|
'--insecure',
|
||||||
action='store_true',
|
action='store_true',
|
||||||
@@ -277,6 +296,20 @@ class MistralShell(app.App):
|
|||||||
help='Disables SSL/TLS certificate verification '
|
help='Disables SSL/TLS certificate verification '
|
||||||
'(Env: MISTRALCLIENT_INSECURE)'
|
'(Env: MISTRALCLIENT_INSECURE)'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--profile',
|
||||||
|
dest='profile',
|
||||||
|
metavar='HMAC_KEY',
|
||||||
|
help='HMAC key to use for encrypting context data for performance '
|
||||||
|
'profiling of operation. This key should be one of the '
|
||||||
|
'values configured for the osprofiler middleware in mistral, '
|
||||||
|
'it is specified in the profiler section of the mistral '
|
||||||
|
'configuration (i.e. /etc/mistral/mistral.conf). Without the '
|
||||||
|
'key, profiling will not be triggered even if osprofiler is '
|
||||||
|
'enabled on the server side.'
|
||||||
|
)
|
||||||
|
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
def initialize_app(self, argv):
|
def initialize_app(self, argv):
|
||||||
@@ -310,7 +343,8 @@ class MistralShell(app.App):
|
|||||||
service_type=self.options.service_type,
|
service_type=self.options.service_type,
|
||||||
auth_token=self.options.token,
|
auth_token=self.options.token,
|
||||||
cacert=self.options.cacert,
|
cacert=self.options.cacert,
|
||||||
insecure=self.options.insecure
|
insecure=self.options.insecure,
|
||||||
|
profile=self.options.profile
|
||||||
)
|
)
|
||||||
|
|
||||||
# Adding client_manager variable to make mistral client work with
|
# Adding client_manager variable to make mistral client work with
|
||||||
|
@@ -20,12 +20,15 @@ import uuid
|
|||||||
import mock
|
import mock
|
||||||
import testtools
|
import testtools
|
||||||
|
|
||||||
|
import osprofiler.profiler
|
||||||
|
|
||||||
from mistralclient.api import client
|
from mistralclient.api import client
|
||||||
|
|
||||||
AUTH_HTTP_URL = 'http://localhost:35357/v3'
|
AUTH_HTTP_URL = 'http://localhost:35357/v3'
|
||||||
AUTH_HTTPS_URL = AUTH_HTTP_URL.replace('http', 'https')
|
AUTH_HTTPS_URL = AUTH_HTTP_URL.replace('http', 'https')
|
||||||
MISTRAL_HTTP_URL = 'http://localhost:8989/v2'
|
MISTRAL_HTTP_URL = 'http://localhost:8989/v2'
|
||||||
MISTRAL_HTTPS_URL = MISTRAL_HTTP_URL.replace('http', 'https')
|
MISTRAL_HTTPS_URL = MISTRAL_HTTP_URL.replace('http', 'https')
|
||||||
|
PROFILER_HMAC_KEY = 'SECRET_HMAC_KEY'
|
||||||
|
|
||||||
|
|
||||||
class BaseClientTests(testtools.TestCase):
|
class BaseClientTests(testtools.TestCase):
|
||||||
@@ -175,3 +178,38 @@ class BaseClientTests(testtools.TestCase):
|
|||||||
os.unlink(path)
|
os.unlink(path)
|
||||||
|
|
||||||
self.assertTrue(log_warning_mock.called)
|
self.assertTrue(log_warning_mock.called)
|
||||||
|
|
||||||
|
@mock.patch('keystoneclient.v3.client.Client')
|
||||||
|
@mock.patch('mistralclient.api.httpclient.HTTPClient')
|
||||||
|
def test_mistral_profile_enabled(self, mock, keystone_client_mock):
|
||||||
|
keystone_client_instance = keystone_client_mock.return_value
|
||||||
|
keystone_client_instance.auth_token = str(uuid.uuid4())
|
||||||
|
keystone_client_instance.project_id = str(uuid.uuid4())
|
||||||
|
keystone_client_instance.user_id = str(uuid.uuid4())
|
||||||
|
|
||||||
|
expected_args = (
|
||||||
|
MISTRAL_HTTP_URL,
|
||||||
|
keystone_client_instance.auth_token,
|
||||||
|
keystone_client_instance.project_id,
|
||||||
|
keystone_client_instance.user_id
|
||||||
|
)
|
||||||
|
|
||||||
|
expected_kwargs = {
|
||||||
|
'cacert': None,
|
||||||
|
'insecure': False
|
||||||
|
}
|
||||||
|
|
||||||
|
client.client(
|
||||||
|
username='mistral',
|
||||||
|
project_name='mistral',
|
||||||
|
auth_url=AUTH_HTTP_URL,
|
||||||
|
profile=PROFILER_HMAC_KEY
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertTrue(mock.called)
|
||||||
|
self.assertEqual(mock.call_args[0], expected_args)
|
||||||
|
self.assertDictEqual(mock.call_args[1], expected_kwargs)
|
||||||
|
|
||||||
|
profiler = osprofiler.profiler.get()
|
||||||
|
|
||||||
|
self.assertEqual(profiler.hmac_key, PROFILER_HMAC_KEY)
|
||||||
|
@@ -19,6 +19,9 @@ import mock
|
|||||||
import requests
|
import requests
|
||||||
import testtools
|
import testtools
|
||||||
|
|
||||||
|
from osprofiler import _utils as osprofiler_utils
|
||||||
|
import osprofiler.profiler
|
||||||
|
|
||||||
from mistralclient.api import httpclient
|
from mistralclient.api import httpclient
|
||||||
|
|
||||||
API_BASE_URL = 'http://localhost:8989/v2'
|
API_BASE_URL = 'http://localhost:8989/v2'
|
||||||
@@ -29,6 +32,8 @@ EXPECTED_URL = API_BASE_URL + API_URL
|
|||||||
AUTH_TOKEN = str(uuid.uuid4())
|
AUTH_TOKEN = str(uuid.uuid4())
|
||||||
PROJECT_ID = str(uuid.uuid4())
|
PROJECT_ID = str(uuid.uuid4())
|
||||||
USER_ID = str(uuid.uuid4())
|
USER_ID = str(uuid.uuid4())
|
||||||
|
PROFILER_HMAC_KEY = 'SECRET_HMAC_KEY'
|
||||||
|
PROFILER_TRACE_ID = str(uuid.uuid4())
|
||||||
|
|
||||||
EXPECTED_AUTH_HEADERS = {
|
EXPECTED_AUTH_HEADERS = {
|
||||||
'x-auth-token': AUTH_TOKEN,
|
'x-auth-token': AUTH_TOKEN,
|
||||||
@@ -65,6 +70,7 @@ class HTTPClientTest(testtools.TestCase):
|
|||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(HTTPClientTest, self).setUp()
|
super(HTTPClientTest, self).setUp()
|
||||||
|
osprofiler.profiler.init(None)
|
||||||
self.client = httpclient.HTTPClient(
|
self.client = httpclient.HTTPClient(
|
||||||
API_BASE_URL,
|
API_BASE_URL,
|
||||||
AUTH_TOKEN,
|
AUTH_TOKEN,
|
||||||
@@ -103,6 +109,42 @@ class HTTPClientTest(testtools.TestCase):
|
|||||||
**expected_options
|
**expected_options
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@mock.patch.object(
|
||||||
|
osprofiler.profiler._Profiler,
|
||||||
|
'get_base_id',
|
||||||
|
mock.MagicMock(return_value=PROFILER_TRACE_ID)
|
||||||
|
)
|
||||||
|
@mock.patch.object(
|
||||||
|
osprofiler.profiler._Profiler,
|
||||||
|
'get_id',
|
||||||
|
mock.MagicMock(return_value=PROFILER_TRACE_ID)
|
||||||
|
)
|
||||||
|
@mock.patch.object(
|
||||||
|
requests,
|
||||||
|
'get',
|
||||||
|
mock.MagicMock(return_value=FakeResponse('get', EXPECTED_URL, 200))
|
||||||
|
)
|
||||||
|
def test_get_request_options_with_profile_enabled(self):
|
||||||
|
osprofiler.profiler.init(PROFILER_HMAC_KEY)
|
||||||
|
|
||||||
|
data = {'base_id': PROFILER_TRACE_ID, 'parent_id': PROFILER_TRACE_ID}
|
||||||
|
signed_data = osprofiler_utils.signed_pack(data, PROFILER_HMAC_KEY)
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
'X-Trace-Info': signed_data[0],
|
||||||
|
'X-Trace-HMAC': signed_data[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
self.client.get(API_URL)
|
||||||
|
|
||||||
|
expected_options = copy.deepcopy(EXPECTED_REQ_OPTIONS)
|
||||||
|
expected_options['headers'].update(headers)
|
||||||
|
|
||||||
|
requests.get.assert_called_with(
|
||||||
|
EXPECTED_URL,
|
||||||
|
**expected_options
|
||||||
|
)
|
||||||
|
|
||||||
@mock.patch.object(
|
@mock.patch.object(
|
||||||
requests,
|
requests,
|
||||||
'post',
|
'post',
|
||||||
|
@@ -109,3 +109,10 @@ class TestShell(base.BaseShellTests):
|
|||||||
self.assertTrue(mock.called)
|
self.assertTrue(mock.called)
|
||||||
params = mock.call_args
|
params = mock.call_args
|
||||||
self.assertEqual('http://localhost:35357/v3', params[1]['auth_url'])
|
self.assertEqual('http://localhost:35357/v3', params[1]['auth_url'])
|
||||||
|
|
||||||
|
@mock.patch('mistralclient.api.client.client')
|
||||||
|
def test_profile(self, mock):
|
||||||
|
self.shell('--profile=SECRET_HMAC_KEY workbook-list')
|
||||||
|
self.assertTrue(mock.called)
|
||||||
|
params = mock.call_args
|
||||||
|
self.assertEqual('SECRET_HMAC_KEY', params[1]['profile'])
|
||||||
|
@@ -2,6 +2,7 @@
|
|||||||
# of appearance. Changing the order has an impact on the overall integration
|
# of appearance. Changing the order has an impact on the overall integration
|
||||||
# process, which may cause wedges in the gate later.
|
# process, which may cause wedges in the gate later.
|
||||||
cliff!=1.16.0,!=1.17.0,>=1.15.0 # Apache-2.0
|
cliff!=1.16.0,!=1.17.0,>=1.15.0 # Apache-2.0
|
||||||
|
osprofiler>=1.3.0 # Apache-2.0
|
||||||
pbr>=1.6 # Apache-2.0
|
pbr>=1.6 # Apache-2.0
|
||||||
python-keystoneclient!=1.8.0,!=2.1.0,>=1.7.0 # Apache-2.0
|
python-keystoneclient!=1.8.0,!=2.1.0,>=1.7.0 # Apache-2.0
|
||||||
python-openstackclient>=2.1.0 # Apache-2.0
|
python-openstackclient>=2.1.0 # Apache-2.0
|
||||||
|
Reference in New Issue
Block a user