Merge tag '0.5.0' into debian/kilo
Release 0.5.0 This tempest-lib release includes: * Support for specifying a prefix on the rand_name() function * Migration of the ssh module from tempest * A few cleanups and missing pieces for the auth.py module * subunit-trace bug fix * Improved unit tests and docstring coverage This also marks the last release which will include subunit-trace as part of tempest-lib. This will be moved to the os-testr project in future releases. (and os-testr will be added as a requirement)
This commit is contained in:
@@ -7,5 +7,6 @@ fixtures>=0.3.14
|
||||
iso8601>=0.1.9
|
||||
jsonschema>=2.0.0,<3.0.0
|
||||
httplib2>=0.7.5
|
||||
paramiko>=1.13.0
|
||||
six>=1.9.0
|
||||
oslo.log>=0.4.0 # Apache-2.0
|
||||
oslo.log>=1.0.0 # Apache-2.0
|
||||
|
@@ -17,13 +17,10 @@ import abc
|
||||
import copy
|
||||
import datetime
|
||||
import re
|
||||
try:
|
||||
import urllib.parse as urlparse
|
||||
except ImportError:
|
||||
import urlparse
|
||||
|
||||
from oslo_log import log as logging
|
||||
import six
|
||||
from six.moves.urllib import parse as urlparse
|
||||
|
||||
from tempest_lib import exceptions
|
||||
from tempest_lib.services.identity.v2 import token_client as json_v2id
|
||||
|
@@ -190,7 +190,11 @@ def run_time():
|
||||
runtime = 0.0
|
||||
for k, v in RESULTS.items():
|
||||
for test in v:
|
||||
runtime += float(get_duration(test['timestamps']).strip('s'))
|
||||
test_dur = get_duration(test['timestamps']).strip('s')
|
||||
# NOTE(toabctl): get_duration() can return an empty string
|
||||
# which leads to a ValueError when casting to float
|
||||
if test_dur:
|
||||
runtime += float(test_dur)
|
||||
return runtime
|
||||
|
||||
|
||||
|
@@ -270,8 +270,8 @@ class RestClient(object):
|
||||
"""Send a HTTP DELETE request using keystone service catalog and auth
|
||||
|
||||
:param str url: the relative url to send the post request to
|
||||
:param dict body: the request body
|
||||
:param dict headers: The headers to use for the request
|
||||
:param dict body: the request body
|
||||
:param dict extra_headers: If the headers returned by the get_headers()
|
||||
method are to be used but additional headers
|
||||
are needed in the request pass them in as a
|
||||
@@ -554,16 +554,16 @@ class RestClient(object):
|
||||
headers and the catalog to determine the endpoint to use for the
|
||||
baseurl to send the request to. Additionally
|
||||
|
||||
When a response is recieved it will check it to see if an error
|
||||
response was recieved. If it was an exception will be raised to enable
|
||||
When a response is received it will check it to see if an error
|
||||
response was received. If it was an exception will be raised to enable
|
||||
it to be handled quickly.
|
||||
|
||||
This method will also handle rate-limiting, if a 413 response code is
|
||||
recieved it will retry the request after waiting the 'retry-after'
|
||||
received it will retry the request after waiting the 'retry-after'
|
||||
duration from the header.
|
||||
|
||||
:param str url: Relative url to send the request to
|
||||
:param str method: The HTTP verb to use for the request
|
||||
:param str url: Relative url to send the request to
|
||||
:param dict extra_headers: If specified without the headers kwarg the
|
||||
headers sent with the request will be the
|
||||
combination from the get_headers() method
|
||||
@@ -578,23 +578,23 @@ class RestClient(object):
|
||||
and the second the response body
|
||||
:raises InvalidContentType: If the content-type of the response isn't
|
||||
an expect type or a 415 response code is
|
||||
recieved
|
||||
:raises Unauthorized: If a 401 response code is recieved
|
||||
:raises Forbidden: If a 403 response code is recieved
|
||||
:raises NotFound: If a 404 response code is recieved
|
||||
:raises BadRequest: If a 400 response code is recieved
|
||||
:raises Conflict: If a 409 response code is recieved
|
||||
:raies Overlimit: If a 413 response code is recieved and over_limit is
|
||||
received
|
||||
:raises Unauthorized: If a 401 response code is received
|
||||
:raises Forbidden: If a 403 response code is received
|
||||
:raises NotFound: If a 404 response code is received
|
||||
:raises BadRequest: If a 400 response code is received
|
||||
:raises Conflict: If a 409 response code is received
|
||||
:raises OverLimit: If a 413 response code is received and over_limit is
|
||||
not in the response body
|
||||
:raises RateLimitExceeded: If a 413 response code is recieved and
|
||||
:raises RateLimitExceeded: If a 413 response code is received and
|
||||
over_limit is in the response body
|
||||
:raises UnprocessableEntity: If a 422 response code is recieved
|
||||
:raises UnprocessableEntity: If a 422 response code is received
|
||||
:raises InvalidHTTPResponseBody: The response body wasn't valid JSON
|
||||
and couldn't be parsed
|
||||
:raises NotImplemented: If a 501 response code is recieved
|
||||
:raises ServerFault: If a 500 response code is recieved
|
||||
:raises NotImplemented: If a 501 response code is received
|
||||
:raises ServerFault: If a 500 response code is received
|
||||
:raises UnexpectedResponseCode: If a response code above 400 is
|
||||
recieved and it doesn't fall into any
|
||||
received and it doesn't fall into any
|
||||
of the handled checks
|
||||
"""
|
||||
# if extra_headers is True
|
||||
|
152
tempest_lib/common/ssh.py
Normal file
152
tempest_lib/common/ssh.py
Normal file
@@ -0,0 +1,152 @@
|
||||
# Copyright 2012 OpenStack Foundation
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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 select
|
||||
import socket
|
||||
import time
|
||||
import warnings
|
||||
|
||||
from oslo_log import log as logging
|
||||
import six
|
||||
|
||||
from tempest_lib import exceptions
|
||||
|
||||
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore")
|
||||
import paramiko
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Client(object):
|
||||
|
||||
def __init__(self, host, username, password=None, timeout=300, pkey=None,
|
||||
channel_timeout=10, look_for_keys=False, key_filename=None):
|
||||
self.host = host
|
||||
self.username = username
|
||||
self.password = password
|
||||
if isinstance(pkey, six.string_types):
|
||||
pkey = paramiko.RSAKey.from_private_key(
|
||||
six.StringIO(str(pkey)))
|
||||
self.pkey = pkey
|
||||
self.look_for_keys = look_for_keys
|
||||
self.key_filename = key_filename
|
||||
self.timeout = int(timeout)
|
||||
self.channel_timeout = float(channel_timeout)
|
||||
self.buf_size = 1024
|
||||
|
||||
def _get_ssh_connection(self, sleep=1.5, backoff=1):
|
||||
"""Returns an ssh connection to the specified host."""
|
||||
bsleep = sleep
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(
|
||||
paramiko.AutoAddPolicy())
|
||||
_start_time = time.time()
|
||||
if self.pkey is not None:
|
||||
LOG.info("Creating ssh connection to '%s' as '%s'"
|
||||
" with public key authentication",
|
||||
self.host, self.username)
|
||||
else:
|
||||
LOG.info("Creating ssh connection to '%s' as '%s'"
|
||||
" with password %s",
|
||||
self.host, self.username, str(self.password))
|
||||
attempts = 0
|
||||
while True:
|
||||
try:
|
||||
ssh.connect(self.host, username=self.username,
|
||||
password=self.password,
|
||||
look_for_keys=self.look_for_keys,
|
||||
key_filename=self.key_filename,
|
||||
timeout=self.channel_timeout, pkey=self.pkey)
|
||||
LOG.info("ssh connection to %s@%s successfuly created",
|
||||
self.username, self.host)
|
||||
return ssh
|
||||
except (socket.error,
|
||||
paramiko.SSHException) as e:
|
||||
if self._is_timed_out(_start_time):
|
||||
LOG.exception("Failed to establish authenticated ssh"
|
||||
" connection to %s@%s after %d attempts",
|
||||
self.username, self.host, attempts)
|
||||
raise exceptions.SSHTimeout(host=self.host,
|
||||
user=self.username,
|
||||
password=self.password)
|
||||
bsleep += backoff
|
||||
attempts += 1
|
||||
LOG.warning("Failed to establish authenticated ssh"
|
||||
" connection to %s@%s (%s). Number attempts: %s."
|
||||
" Retry after %d seconds.",
|
||||
self.username, self.host, e, attempts, bsleep)
|
||||
time.sleep(bsleep)
|
||||
|
||||
def _is_timed_out(self, start_time):
|
||||
return (time.time() - self.timeout) > start_time
|
||||
|
||||
def exec_command(self, cmd):
|
||||
"""Execute the specified command on the server
|
||||
|
||||
Note that this method is reading whole command outputs to memory, thus
|
||||
shouldn't be used for large outputs.
|
||||
|
||||
:param str cmd: Command to run at remote server.
|
||||
:returns: data read from standard output of the command.
|
||||
:raises: SSHExecCommandFailed if command returns nonzero
|
||||
status. The exception contains command status stderr content.
|
||||
:raises: TimeoutException if cmd doesn't end when timeout expires.
|
||||
"""
|
||||
ssh = self._get_ssh_connection()
|
||||
transport = ssh.get_transport()
|
||||
channel = transport.open_session()
|
||||
channel.fileno() # Register event pipe
|
||||
channel.exec_command(cmd)
|
||||
channel.shutdown_write()
|
||||
out_data = []
|
||||
err_data = []
|
||||
poll = select.poll()
|
||||
poll.register(channel, select.POLLIN)
|
||||
start_time = time.time()
|
||||
|
||||
while True:
|
||||
ready = poll.poll(self.channel_timeout)
|
||||
if not any(ready):
|
||||
if not self._is_timed_out(start_time):
|
||||
continue
|
||||
raise exceptions.TimeoutException(
|
||||
"Command: '{0}' executed on host '{1}'.".format(
|
||||
cmd, self.host))
|
||||
if not ready[0]: # If there is nothing to read.
|
||||
continue
|
||||
out_chunk = err_chunk = None
|
||||
if channel.recv_ready():
|
||||
out_chunk = channel.recv(self.buf_size)
|
||||
out_data += out_chunk,
|
||||
if channel.recv_stderr_ready():
|
||||
err_chunk = channel.recv_stderr(self.buf_size)
|
||||
err_data += err_chunk,
|
||||
if channel.closed and not err_chunk and not out_chunk:
|
||||
break
|
||||
exit_status = channel.recv_exit_status()
|
||||
if 0 != exit_status:
|
||||
raise exceptions.SSHExecCommandFailed(
|
||||
command=cmd, exit_status=exit_status,
|
||||
strerror=''.join(err_data))
|
||||
return ''.join(out_data)
|
||||
|
||||
def test_connection_auth(self):
|
||||
"""Raises an exception when we can not connect to server via ssh."""
|
||||
connection = self._get_ssh_connection()
|
||||
connection.close()
|
@@ -20,32 +20,70 @@ import uuid
|
||||
|
||||
|
||||
def rand_uuid():
|
||||
"""Generate a random UUID string
|
||||
|
||||
:return: a random UUID (e.g. '1dc12c7d-60eb-4b61-a7a2-17cf210155b6')
|
||||
:rtype: string
|
||||
"""
|
||||
return str(uuid.uuid4())
|
||||
|
||||
|
||||
def rand_uuid_hex():
|
||||
"""Generate a random UUID hex string
|
||||
|
||||
:return: a random UUID (e.g. '0b98cf96d90447bda4b46f31aeb1508c')
|
||||
:rtype: string
|
||||
"""
|
||||
return uuid.uuid4().hex
|
||||
|
||||
|
||||
def rand_name(name=''):
|
||||
def rand_name(name='', prefix=None):
|
||||
"""Generate a random name that inclues a random number
|
||||
|
||||
:param str name: The name that you want to include
|
||||
:param str prefix: The prefix that you want to include
|
||||
:return: a random name. The format is
|
||||
'<prefix>-<random number>-<name>-<random number>'.
|
||||
(e.g. 'prefixfoo-1308607012-namebar-154876201')
|
||||
:rtype: string
|
||||
"""
|
||||
randbits = str(random.randint(1, 0x7fffffff))
|
||||
rand_name = randbits
|
||||
if name:
|
||||
return name + '-' + randbits
|
||||
else:
|
||||
return randbits
|
||||
rand_name = name + '-' + rand_name
|
||||
if prefix:
|
||||
rand_name = prefix + '-' + rand_name
|
||||
return rand_name
|
||||
|
||||
|
||||
def rand_url():
|
||||
"""Generate a random url that inclues a random number
|
||||
|
||||
:return: a random url. The format is 'https://url-<random number>.com'.
|
||||
(e.g. 'https://url-154876201.com')
|
||||
:rtype: string
|
||||
"""
|
||||
randbits = str(random.randint(1, 0x7fffffff))
|
||||
return 'https://url-' + randbits + '.com'
|
||||
|
||||
|
||||
def rand_int_id(start=0, end=0x7fffffff):
|
||||
"""Generate a random integer value
|
||||
|
||||
:param int start: The value that you expect to start here
|
||||
:param int end: The value that you expect to end here
|
||||
:return: a random integer value
|
||||
:rtype: int
|
||||
"""
|
||||
return random.randint(start, end)
|
||||
|
||||
|
||||
def rand_mac_address():
|
||||
"""Generate an Ethernet MAC address."""
|
||||
"""Generate an Ethernet MAC address
|
||||
|
||||
:return: an random Ethernet MAC address
|
||||
:rtype: string
|
||||
"""
|
||||
# NOTE(vish): We would prefer to use 0xfe here to ensure that linux
|
||||
# bridge mac addresses don't change, but it appears to
|
||||
# conflict with libvirt, so we use the next highest octet
|
||||
@@ -60,14 +98,27 @@ def rand_mac_address():
|
||||
|
||||
|
||||
def parse_image_id(image_ref):
|
||||
"""Return the image id from a given image ref."""
|
||||
"""Return the image id from a given image ref
|
||||
|
||||
This function just returns the last word of the given image ref string
|
||||
splitting with '/'.
|
||||
:param str image_ref: a string that includes the image id
|
||||
:return: the image id string
|
||||
:rtype: string
|
||||
"""
|
||||
return image_ref.rsplit('/')[-1]
|
||||
|
||||
|
||||
def arbitrary_string(size=4, base_text=None):
|
||||
"""Return size characters from base_text
|
||||
|
||||
Repeating the base_text infinitely if needed.
|
||||
This generates a string with an arbitrary number of characters, generated
|
||||
by looping the base_text string. If the size is smaller than the size of
|
||||
base_text, returning string is shrinked to the size.
|
||||
:param int size: a returning charactors size
|
||||
:param str base_text: a string you want to repeat
|
||||
:return: size string
|
||||
:rtype: string
|
||||
"""
|
||||
if not base_text:
|
||||
base_text = 'test'
|
||||
@@ -75,12 +126,24 @@ def arbitrary_string(size=4, base_text=None):
|
||||
|
||||
|
||||
def random_bytes(size=1024):
|
||||
"""Return size randomly selected bytes as a string."""
|
||||
"""Return size randomly selected bytes as a string
|
||||
|
||||
:param int size: a returning bytes size
|
||||
:return: size randomly bytes
|
||||
:rtype: string
|
||||
"""
|
||||
return ''.join([chr(random.randint(0, 255))
|
||||
for i in range(size)])
|
||||
|
||||
|
||||
def get_ipv6_addr_by_EUI64(cidr, mac):
|
||||
"""Generate a IPv6 addr by EUI-64 with CIDR and MAC
|
||||
|
||||
:param str cidr: a IPv6 CIDR
|
||||
:param str mac: a MAC address
|
||||
:return: an IPv6 Address
|
||||
:rtype: netaddr.IPAddress
|
||||
"""
|
||||
# Check if the prefix is IPv4 address
|
||||
is_ipv4 = netaddr.valid_ipv4(cidr)
|
||||
if is_ipv4:
|
||||
|
@@ -150,4 +150,19 @@ class IdentityError(TempestException):
|
||||
|
||||
|
||||
class EndpointNotFound(TempestException):
|
||||
message = "Endpoint not found"
|
||||
message = "Endpoint not found"
|
||||
|
||||
|
||||
class InvalidCredentials(TempestException):
|
||||
message = "Invalid Credentials"
|
||||
|
||||
|
||||
class SSHTimeout(TempestException):
|
||||
message = ("Connection to the %(host)s via SSH timed out.\n"
|
||||
"User: %(user)s, Password: %(password)s")
|
||||
|
||||
|
||||
class SSHExecCommandFailed(TempestException):
|
||||
"""Raised when remotely executed command returns nonzero status."""
|
||||
message = ("Command '%(command)s', exit status: %(exit_status)d, "
|
||||
"Error:\n%(strerror)s")
|
||||
|
0
tempest_lib/tests/cmd/__init__.py
Normal file
0
tempest_lib/tests/cmd/__init__.py
Normal file
61
tempest_lib/tests/cmd/test_subunit_trace.py
Normal file
61
tempest_lib/tests/cmd/test_subunit_trace.py
Normal file
@@ -0,0 +1,61 @@
|
||||
# Copyright 2015 SUSE Linux GmbH
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
from datetime import datetime as dt
|
||||
|
||||
from ddt import data
|
||||
from ddt import ddt
|
||||
from ddt import unpack
|
||||
from mock import patch
|
||||
|
||||
from tempest_lib.cmd import subunit_trace
|
||||
from tempest_lib.tests import base
|
||||
|
||||
|
||||
@ddt
|
||||
class TestSubunitTrace(base.TestCase):
|
||||
|
||||
@data(([dt(2015, 4, 17, 22, 23, 14, 111111),
|
||||
dt(2015, 4, 17, 22, 23, 14, 111111)],
|
||||
"0.000000s"),
|
||||
([dt(2015, 4, 17, 22, 23, 14, 111111),
|
||||
dt(2015, 4, 17, 22, 23, 15, 111111)],
|
||||
"1.000000s"),
|
||||
([dt(2015, 4, 17, 22, 23, 14, 111111),
|
||||
None],
|
||||
""))
|
||||
@unpack
|
||||
def test_get_durating(self, timestamps, expected_result):
|
||||
self.assertEqual(subunit_trace.get_duration(timestamps),
|
||||
expected_result)
|
||||
|
||||
@data(([dt(2015, 4, 17, 22, 23, 14, 111111),
|
||||
dt(2015, 4, 17, 22, 23, 14, 111111)],
|
||||
0.0),
|
||||
([dt(2015, 4, 17, 22, 23, 14, 111111),
|
||||
dt(2015, 4, 17, 22, 23, 15, 111111)],
|
||||
1.0),
|
||||
([dt(2015, 4, 17, 22, 23, 14, 111111),
|
||||
None],
|
||||
0.0))
|
||||
@unpack
|
||||
def test_run_time(self, timestamps, expected_result):
|
||||
patched_res = {
|
||||
0: [
|
||||
{'timestamps': timestamps}
|
||||
]
|
||||
}
|
||||
with patch.dict(subunit_trace.RESULTS, patched_res, clear=True):
|
||||
self.assertEqual(subunit_trace.run_time(), expected_result)
|
@@ -13,6 +13,7 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import netaddr
|
||||
|
||||
from tempest_lib.common.utils import data_utils
|
||||
from tempest_lib.tests import base
|
||||
@@ -48,6 +49,20 @@ class TestDataUtils(base.TestCase):
|
||||
self.assertTrue(actual.startswith('foo'))
|
||||
self.assertNotEqual(actual, actual2)
|
||||
|
||||
def test_rand_name_with_prefix(self):
|
||||
actual = data_utils.rand_name(prefix='prefix-str')
|
||||
self.assertIsInstance(actual, str)
|
||||
self.assertRegexpMatches(actual, "^prefix-str-")
|
||||
actual2 = data_utils.rand_name(prefix='prefix-str')
|
||||
self.assertNotEqual(actual, actual2)
|
||||
|
||||
def test_rand_url(self):
|
||||
actual = data_utils.rand_url()
|
||||
self.assertIsInstance(actual, str)
|
||||
self.assertRegexpMatches(actual, "^https://url-[0-9]*\.com$")
|
||||
actual2 = data_utils.rand_url()
|
||||
self.assertNotEqual(actual, actual2)
|
||||
|
||||
def test_rand_int(self):
|
||||
actual = data_utils.rand_int_id()
|
||||
self.assertIsInstance(actual, int)
|
||||
@@ -75,3 +90,50 @@ class TestDataUtils(base.TestCase):
|
||||
self.assertEqual(actual, "abc" * int(30 / len("abc")))
|
||||
actual = data_utils.arbitrary_string(size=5, base_text="deadbeaf")
|
||||
self.assertEqual(actual, "deadb")
|
||||
|
||||
def test_random_bytes(self):
|
||||
actual = data_utils.random_bytes() # default size=1024
|
||||
self.assertIsInstance(actual, str)
|
||||
self.assertRegexpMatches(actual, "^[\x00-\xFF]{1024}")
|
||||
actual2 = data_utils.random_bytes()
|
||||
self.assertNotEqual(actual, actual2)
|
||||
|
||||
actual = data_utils.random_bytes(size=2048)
|
||||
self.assertRegexpMatches(actual, "^[\x00-\xFF]{2048}")
|
||||
|
||||
def test_get_ipv6_addr_by_EUI64(self):
|
||||
actual = data_utils.get_ipv6_addr_by_EUI64('2001:db8::',
|
||||
'00:16:3e:33:44:55')
|
||||
self.assertIsInstance(actual, netaddr.IPAddress)
|
||||
self.assertEqual(actual,
|
||||
netaddr.IPAddress('2001:db8::216:3eff:fe33:4455'))
|
||||
|
||||
def test_get_ipv6_addr_by_EUI64_with_IPv4_prefix(self):
|
||||
ipv4_prefix = '10.0.8'
|
||||
mac = '00:16:3e:33:44:55'
|
||||
self.assertRaises(TypeError, data_utils.get_ipv6_addr_by_EUI64,
|
||||
ipv4_prefix, mac)
|
||||
|
||||
def test_get_ipv6_addr_by_EUI64_bad_cidr_type(self):
|
||||
bad_cidr = 123
|
||||
mac = '00:16:3e:33:44:55'
|
||||
self.assertRaises(TypeError, data_utils.get_ipv6_addr_by_EUI64,
|
||||
bad_cidr, mac)
|
||||
|
||||
def test_get_ipv6_addr_by_EUI64_bad_cidr_value(self):
|
||||
bad_cidr = 'bb'
|
||||
mac = '00:16:3e:33:44:55'
|
||||
self.assertRaises(TypeError, data_utils.get_ipv6_addr_by_EUI64,
|
||||
bad_cidr, mac)
|
||||
|
||||
def test_get_ipv6_addr_by_EUI64_bad_mac_value(self):
|
||||
cidr = '2001:db8::'
|
||||
bad_mac = '00:16:3e:33:44:5Z'
|
||||
self.assertRaises(TypeError, data_utils.get_ipv6_addr_by_EUI64,
|
||||
cidr, bad_mac)
|
||||
|
||||
def test_get_ipv6_addr_by_EUI64_bad_mac_type(self):
|
||||
cidr = '2001:db8::'
|
||||
bad_mac = 99999999999999999999
|
||||
self.assertRaises(TypeError, data_utils.get_ipv6_addr_by_EUI64,
|
||||
cidr, bad_mac)
|
||||
|
@@ -16,5 +16,18 @@
|
||||
|
||||
class FakeAuthProvider(object):
|
||||
|
||||
def __init__(self, creds_dict={}):
|
||||
self.credentials = FakeCredentials(creds_dict)
|
||||
|
||||
def auth_request(self, method, url, headers=None, body=None, filters=None):
|
||||
return url, headers, body
|
||||
|
||||
def base_url(self, filters, auth_data=None):
|
||||
return "https://example.com"
|
||||
|
||||
|
||||
class FakeCredentials(object):
|
||||
|
||||
def __init__(self, creds_dict):
|
||||
for key in creds_dict.keys():
|
||||
setattr(self, key, creds_dict[key])
|
||||
|
180
tempest_lib/tests/test_credentials.py
Normal file
180
tempest_lib/tests/test_credentials.py
Normal file
@@ -0,0 +1,180 @@
|
||||
# Copyright 2014 Hewlett-Packard Development Company, L.P.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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 copy
|
||||
|
||||
from tempest_lib import auth
|
||||
from tempest_lib import exceptions
|
||||
from tempest_lib.services.identity.v2 import token_client as v2_client
|
||||
from tempest_lib.services.identity.v3 import token_client as v3_client
|
||||
from tempest_lib.tests import base
|
||||
from tempest_lib.tests import fake_identity
|
||||
|
||||
|
||||
class CredentialsTests(base.TestCase):
|
||||
attributes = {}
|
||||
credentials_class = auth.Credentials
|
||||
|
||||
def _get_credentials(self, attributes=None):
|
||||
if attributes is None:
|
||||
attributes = self.attributes
|
||||
return self.credentials_class(**attributes)
|
||||
|
||||
def _check(self, credentials, credentials_class, filled):
|
||||
# Check the right version of credentials has been returned
|
||||
self.assertIsInstance(credentials, credentials_class)
|
||||
# Check the id attributes are filled in
|
||||
attributes = [x for x in credentials.ATTRIBUTES if (
|
||||
'_id' in x and x != 'domain_id')]
|
||||
for attr in attributes:
|
||||
if filled:
|
||||
self.assertIsNotNone(getattr(credentials, attr))
|
||||
else:
|
||||
self.assertIsNone(getattr(credentials, attr))
|
||||
|
||||
def test_create(self):
|
||||
creds = self._get_credentials()
|
||||
self.assertEqual(self.attributes, creds._initial)
|
||||
|
||||
def test_create_invalid_attr(self):
|
||||
self.assertRaises(exceptions.InvalidCredentials,
|
||||
self._get_credentials,
|
||||
attributes=dict(invalid='fake'))
|
||||
|
||||
def test_is_valid(self):
|
||||
creds = self._get_credentials()
|
||||
self.assertRaises(NotImplementedError, creds.is_valid)
|
||||
|
||||
|
||||
class KeystoneV2CredentialsTests(CredentialsTests):
|
||||
attributes = {
|
||||
'username': 'fake_username',
|
||||
'password': 'fake_password',
|
||||
'tenant_name': 'fake_tenant_name'
|
||||
}
|
||||
|
||||
identity_response = fake_identity._fake_v2_response
|
||||
credentials_class = auth.KeystoneV2Credentials
|
||||
tokenclient_class = v2_client.TokenClientJSON
|
||||
identity_version = 'v2'
|
||||
|
||||
def setUp(self):
|
||||
super(KeystoneV2CredentialsTests, self).setUp()
|
||||
self.stubs.Set(self.tokenclient_class, 'raw_request',
|
||||
self.identity_response)
|
||||
|
||||
def _verify_credentials(self, credentials_class, creds_dict, filled=True):
|
||||
creds = auth.get_credentials(fake_identity.FAKE_AUTH_URL,
|
||||
fill_in=filled,
|
||||
identity_version=self.identity_version,
|
||||
**creds_dict)
|
||||
self._check(creds, credentials_class, filled)
|
||||
|
||||
def test_get_credentials(self):
|
||||
self._verify_credentials(credentials_class=self.credentials_class,
|
||||
creds_dict=self.attributes)
|
||||
|
||||
def test_get_credentials_not_filled(self):
|
||||
self._verify_credentials(credentials_class=self.credentials_class,
|
||||
creds_dict=self.attributes,
|
||||
filled=False)
|
||||
|
||||
def test_is_valid(self):
|
||||
creds = self._get_credentials()
|
||||
self.assertTrue(creds.is_valid())
|
||||
|
||||
def _test_is_not_valid(self, ignore_key):
|
||||
creds = self._get_credentials()
|
||||
for attr in self.attributes.keys():
|
||||
if attr == ignore_key:
|
||||
continue
|
||||
temp_attr = getattr(creds, attr)
|
||||
delattr(creds, attr)
|
||||
self.assertFalse(creds.is_valid(),
|
||||
"Credentials should be invalid without %s" % attr)
|
||||
setattr(creds, attr, temp_attr)
|
||||
|
||||
def test_is_not_valid(self):
|
||||
# NOTE(mtreinish): A KeystoneV2 credential object is valid without
|
||||
# a tenant_name. So skip that check. See tempest.auth for the valid
|
||||
# credential requirements
|
||||
self._test_is_not_valid('tenant_name')
|
||||
|
||||
def test_reset_all_attributes(self):
|
||||
creds = self._get_credentials()
|
||||
initial_creds = copy.deepcopy(creds)
|
||||
set_attr = creds.__dict__.keys()
|
||||
missing_attr = set(creds.ATTRIBUTES).difference(set_attr)
|
||||
# Set all unset attributes, then reset
|
||||
for attr in missing_attr:
|
||||
setattr(creds, attr, 'fake' + attr)
|
||||
creds.reset()
|
||||
# Check reset credentials are same as initial ones
|
||||
self.assertEqual(creds, initial_creds)
|
||||
|
||||
def test_reset_single_attribute(self):
|
||||
creds = self._get_credentials()
|
||||
initial_creds = copy.deepcopy(creds)
|
||||
set_attr = creds.__dict__.keys()
|
||||
missing_attr = set(creds.ATTRIBUTES).difference(set_attr)
|
||||
# Set one unset attributes, then reset
|
||||
for attr in missing_attr:
|
||||
setattr(creds, attr, 'fake' + attr)
|
||||
creds.reset()
|
||||
# Check reset credentials are same as initial ones
|
||||
self.assertEqual(creds, initial_creds)
|
||||
|
||||
|
||||
class KeystoneV3CredentialsTests(KeystoneV2CredentialsTests):
|
||||
attributes = {
|
||||
'username': 'fake_username',
|
||||
'password': 'fake_password',
|
||||
'project_name': 'fake_project_name',
|
||||
'user_domain_name': 'fake_domain_name'
|
||||
}
|
||||
|
||||
credentials_class = auth.KeystoneV3Credentials
|
||||
identity_response = fake_identity._fake_v3_response
|
||||
tokenclient_class = v3_client.V3TokenClientJSON
|
||||
identity_version = 'v3'
|
||||
|
||||
def test_is_not_valid(self):
|
||||
# NOTE(mtreinish) For a Keystone V3 credential object a project name
|
||||
# is not required to be valid, so we skip that check. See tempest.auth
|
||||
# for the valid credential requirements
|
||||
self._test_is_not_valid('project_name')
|
||||
|
||||
def test_synced_attributes(self):
|
||||
attributes = self.attributes
|
||||
# Create V3 credentials with tenant instead of project, and user_domain
|
||||
for attr in ['project_id', 'user_domain_id']:
|
||||
attributes[attr] = 'fake_' + attr
|
||||
creds = self._get_credentials(attributes)
|
||||
self.assertEqual(creds.project_name, creds.tenant_name)
|
||||
self.assertEqual(creds.project_id, creds.tenant_id)
|
||||
self.assertEqual(creds.user_domain_name, creds.project_domain_name)
|
||||
self.assertEqual(creds.user_domain_id, creds.project_domain_id)
|
||||
# Replace user_domain with project_domain
|
||||
del attributes['user_domain_name']
|
||||
del attributes['user_domain_id']
|
||||
del attributes['project_name']
|
||||
del attributes['project_id']
|
||||
for attr in ['project_domain_name', 'project_domain_id',
|
||||
'tenant_name', 'tenant_id']:
|
||||
attributes[attr] = 'fake_' + attr
|
||||
self.assertEqual(creds.tenant_name, creds.project_name)
|
||||
self.assertEqual(creds.tenant_id, creds.project_id)
|
||||
self.assertEqual(creds.project_domain_name, creds.user_domain_name)
|
||||
self.assertEqual(creds.project_domain_id, creds.user_domain_id)
|
@@ -31,8 +31,9 @@ class BaseRestClientTestClass(base.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(BaseRestClientTestClass, self).setUp()
|
||||
self.fake_auth_provider = fake_auth_provider.FakeAuthProvider()
|
||||
self.rest_client = rest_client.RestClient(
|
||||
fake_auth_provider.FakeAuthProvider(), None, None)
|
||||
self.fake_auth_provider, None, None)
|
||||
self.stubs.Set(httplib2.Http, 'request', self.fake_http.request)
|
||||
self.useFixture(mockpatch.PatchObject(self.rest_client,
|
||||
'_log_request'))
|
||||
@@ -437,6 +438,59 @@ class TestRestClientUtils(BaseRestClientTestClass):
|
||||
self.rest_client.wait_for_resource_deletion,
|
||||
'1234')
|
||||
|
||||
def test_get_versions(self):
|
||||
self.rest_client._parse_resp = lambda x: [{'id': 'v1'}, {'id': 'v2'}]
|
||||
actual_resp, actual_versions = self.rest_client.get_versions()
|
||||
self.assertEqual(['v1', 'v2'], list(actual_versions))
|
||||
|
||||
def test__str__(self):
|
||||
def get_token():
|
||||
return "deadbeef"
|
||||
|
||||
self.fake_auth_provider.get_token = get_token
|
||||
self.assertIsNotNone(str(self.rest_client))
|
||||
|
||||
|
||||
class TestProperties(BaseRestClientTestClass):
|
||||
|
||||
def setUp(self):
|
||||
self.fake_http = fake_http.fake_httplib2()
|
||||
super(TestProperties, self).setUp()
|
||||
creds_dict = {
|
||||
'username': 'test-user',
|
||||
'user_id': 'test-user_id',
|
||||
'tenant_name': 'test-tenant_name',
|
||||
'tenant_id': 'test-tenant_id',
|
||||
'password': 'test-password'
|
||||
}
|
||||
self.rest_client = rest_client.RestClient(
|
||||
fake_auth_provider.FakeAuthProvider(creds_dict=creds_dict),
|
||||
None, None)
|
||||
|
||||
def test_properties(self):
|
||||
self.assertEqual('test-user', self.rest_client.user)
|
||||
self.assertEqual('test-user_id', self.rest_client.user_id)
|
||||
self.assertEqual('test-tenant_name', self.rest_client.tenant_name)
|
||||
self.assertEqual('test-tenant_id', self.rest_client.tenant_id)
|
||||
self.assertEqual('test-password', self.rest_client.password)
|
||||
|
||||
self.rest_client.api_version = 'v1'
|
||||
expected = {'api_version': 'v1',
|
||||
'endpoint_type': 'publicURL',
|
||||
'region': None,
|
||||
'service': None,
|
||||
'skip_path': True}
|
||||
self.rest_client.skip_path()
|
||||
self.assertEqual(expected, self.rest_client.filters)
|
||||
|
||||
self.rest_client.reset_path()
|
||||
self.rest_client.api_version = 'v1'
|
||||
expected = {'api_version': 'v1',
|
||||
'endpoint_type': 'publicURL',
|
||||
'region': None,
|
||||
'service': None}
|
||||
self.assertEqual(expected, self.rest_client.filters)
|
||||
|
||||
|
||||
class TestExpectedSuccess(BaseRestClientTestClass):
|
||||
|
||||
@@ -483,3 +537,33 @@ class TestExpectedSuccess(BaseRestClientTestClass):
|
||||
read_code = 202
|
||||
self.assertRaises(AssertionError, self.rest_client.expected_success,
|
||||
expected_code, read_code)
|
||||
|
||||
|
||||
class TestResponseBody(base.TestCase):
|
||||
|
||||
def test_str(self):
|
||||
response = {'status': 200}
|
||||
body = {'key1': 'value1'}
|
||||
actual = rest_client.ResponseBody(response, body)
|
||||
self.assertEqual("response: %s\nBody: %s" % (response, body),
|
||||
str(actual))
|
||||
|
||||
|
||||
class TestResponseBodyData(base.TestCase):
|
||||
|
||||
def test_str(self):
|
||||
response = {'status': 200}
|
||||
data = 'data1'
|
||||
actual = rest_client.ResponseBodyData(response, data)
|
||||
self.assertEqual("response: %s\nBody: %s" % (response, data),
|
||||
str(actual))
|
||||
|
||||
|
||||
class TestResponseBodyList(base.TestCase):
|
||||
|
||||
def test_str(self):
|
||||
response = {'status': 200}
|
||||
body = ['value1', 'value2', 'value3']
|
||||
actual = rest_client.ResponseBodyList(response, body)
|
||||
self.assertEqual("response: %s\nBody: %s" % (response, body),
|
||||
str(actual))
|
||||
|
188
tempest_lib/tests/test_ssh.py
Normal file
188
tempest_lib/tests/test_ssh.py
Normal file
@@ -0,0 +1,188 @@
|
||||
# Copyright 2014 OpenStack Foundation
|
||||
#
|
||||
# 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 socket
|
||||
import time
|
||||
|
||||
import mock
|
||||
import testtools
|
||||
|
||||
from tempest_lib.common import ssh
|
||||
from tempest_lib import exceptions
|
||||
from tempest_lib.tests import base
|
||||
|
||||
|
||||
class TestSshClient(base.TestCase):
|
||||
|
||||
@mock.patch('paramiko.RSAKey.from_private_key')
|
||||
@mock.patch('six.StringIO')
|
||||
def test_pkey_calls_paramiko_RSAKey(self, cs_mock, rsa_mock):
|
||||
cs_mock.return_value = mock.sentinel.csio
|
||||
pkey = 'mykey'
|
||||
ssh.Client('localhost', 'root', pkey=pkey)
|
||||
rsa_mock.assert_called_once_with(mock.sentinel.csio)
|
||||
cs_mock.assert_called_once_with('mykey')
|
||||
rsa_mock.reset_mock()
|
||||
cs_mock.reset_mock()
|
||||
pkey = mock.sentinel.pkey
|
||||
# Shouldn't call out to load a file from RSAKey, since
|
||||
# a sentinel isn't a basestring...
|
||||
ssh.Client('localhost', 'root', pkey=pkey)
|
||||
self.assertEqual(0, rsa_mock.call_count)
|
||||
self.assertEqual(0, cs_mock.call_count)
|
||||
|
||||
def _set_ssh_connection_mocks(self):
|
||||
client_mock = mock.MagicMock()
|
||||
client_mock.connect.return_value = True
|
||||
return (self.patch('paramiko.SSHClient'),
|
||||
self.patch('paramiko.AutoAddPolicy'),
|
||||
client_mock)
|
||||
|
||||
def test_get_ssh_connection(self):
|
||||
c_mock, aa_mock, client_mock = self._set_ssh_connection_mocks()
|
||||
s_mock = self.patch('time.sleep')
|
||||
|
||||
c_mock.return_value = client_mock
|
||||
aa_mock.return_value = mock.sentinel.aa
|
||||
|
||||
# Test normal case for successful connection on first try
|
||||
client = ssh.Client('localhost', 'root', timeout=2)
|
||||
client._get_ssh_connection(sleep=1)
|
||||
|
||||
aa_mock.assert_called_once_with()
|
||||
client_mock.set_missing_host_key_policy.assert_called_once_with(
|
||||
mock.sentinel.aa)
|
||||
expected_connect = [mock.call(
|
||||
'localhost',
|
||||
username='root',
|
||||
pkey=None,
|
||||
key_filename=None,
|
||||
look_for_keys=False,
|
||||
timeout=10.0,
|
||||
password=None
|
||||
)]
|
||||
self.assertEqual(expected_connect, client_mock.connect.mock_calls)
|
||||
self.assertEqual(0, s_mock.call_count)
|
||||
|
||||
def test_get_ssh_connection_two_attemps(self):
|
||||
c_mock, aa_mock, client_mock = self._set_ssh_connection_mocks()
|
||||
|
||||
c_mock.return_value = client_mock
|
||||
client_mock.connect.side_effect = [
|
||||
socket.error,
|
||||
mock.MagicMock()
|
||||
]
|
||||
|
||||
client = ssh.Client('localhost', 'root', timeout=1)
|
||||
start_time = int(time.time())
|
||||
client._get_ssh_connection(sleep=1)
|
||||
end_time = int(time.time())
|
||||
self.assertLess((end_time - start_time), 4)
|
||||
self.assertGreater((end_time - start_time), 1)
|
||||
|
||||
def test_get_ssh_connection_timeout(self):
|
||||
c_mock, aa_mock, client_mock = self._set_ssh_connection_mocks()
|
||||
|
||||
c_mock.return_value = client_mock
|
||||
client_mock.connect.side_effect = [
|
||||
socket.error,
|
||||
socket.error,
|
||||
socket.error,
|
||||
]
|
||||
|
||||
client = ssh.Client('localhost', 'root', timeout=2)
|
||||
start_time = int(time.time())
|
||||
with testtools.ExpectedException(exceptions.SSHTimeout):
|
||||
client._get_ssh_connection()
|
||||
end_time = int(time.time())
|
||||
self.assertLess((end_time - start_time), 5)
|
||||
self.assertGreaterEqual((end_time - start_time), 2)
|
||||
|
||||
def test_exec_command(self):
|
||||
gsc_mock = self.patch('tempest_lib.common.ssh.Client.'
|
||||
'_get_ssh_connection')
|
||||
ito_mock = self.patch('tempest_lib.common.ssh.Client._is_timed_out')
|
||||
select_mock = self.patch('select.poll')
|
||||
|
||||
client_mock = mock.MagicMock()
|
||||
tran_mock = mock.MagicMock()
|
||||
chan_mock = mock.MagicMock()
|
||||
poll_mock = mock.MagicMock()
|
||||
|
||||
def reset_mocks():
|
||||
gsc_mock.reset_mock()
|
||||
ito_mock.reset_mock()
|
||||
select_mock.reset_mock()
|
||||
poll_mock.reset_mock()
|
||||
client_mock.reset_mock()
|
||||
tran_mock.reset_mock()
|
||||
chan_mock.reset_mock()
|
||||
|
||||
select_mock.return_value = poll_mock
|
||||
gsc_mock.return_value = client_mock
|
||||
ito_mock.return_value = True
|
||||
client_mock.get_transport.return_value = tran_mock
|
||||
tran_mock.open_session.return_value = chan_mock
|
||||
poll_mock.poll.side_effect = [
|
||||
[0, 0, 0]
|
||||
]
|
||||
|
||||
# Test for a timeout condition immediately raised
|
||||
client = ssh.Client('localhost', 'root', timeout=2)
|
||||
with testtools.ExpectedException(exceptions.TimeoutException):
|
||||
client.exec_command("test")
|
||||
|
||||
chan_mock.fileno.assert_called_once_with()
|
||||
chan_mock.exec_command.assert_called_once_with("test")
|
||||
chan_mock.shutdown_write.assert_called_once_with()
|
||||
|
||||
SELECT_POLLIN = 1
|
||||
poll_mock.register.assert_called_once_with(chan_mock, SELECT_POLLIN)
|
||||
poll_mock.poll.assert_called_once_with(10)
|
||||
|
||||
# Test for proper reading of STDOUT and STDERROR and closing
|
||||
# of all file descriptors.
|
||||
|
||||
reset_mocks()
|
||||
|
||||
select_mock.return_value = poll_mock
|
||||
gsc_mock.return_value = client_mock
|
||||
ito_mock.return_value = False
|
||||
client_mock.get_transport.return_value = tran_mock
|
||||
tran_mock.open_session.return_value = chan_mock
|
||||
poll_mock.poll.side_effect = [
|
||||
[1, 0, 0]
|
||||
]
|
||||
closed_prop = mock.PropertyMock(return_value=True)
|
||||
type(chan_mock).closed = closed_prop
|
||||
chan_mock.recv_exit_status.return_value = 0
|
||||
chan_mock.recv.return_value = ''
|
||||
chan_mock.recv_stderr.return_value = ''
|
||||
|
||||
client = ssh.Client('localhost', 'root', timeout=2)
|
||||
client.exec_command("test")
|
||||
|
||||
chan_mock.fileno.assert_called_once_with()
|
||||
chan_mock.exec_command.assert_called_once_with("test")
|
||||
chan_mock.shutdown_write.assert_called_once_with()
|
||||
|
||||
SELECT_POLLIN = 1
|
||||
poll_mock.register.assert_called_once_with(chan_mock, SELECT_POLLIN)
|
||||
poll_mock.poll.assert_called_once_with(10)
|
||||
chan_mock.recv_ready.assert_called_once_with()
|
||||
chan_mock.recv.assert_called_once_with(1024)
|
||||
chan_mock.recv_stderr_ready.assert_called_once_with()
|
||||
chan_mock.recv_stderr.assert_called_once_with(1024)
|
||||
chan_mock.recv_exit_status.assert_called_once_with()
|
||||
closed_prop.assert_called_once_with()
|
@@ -25,4 +25,4 @@ from tempest_lib.tests import base
|
||||
class TestTempest_lib(base.TestCase):
|
||||
|
||||
def test_something(self):
|
||||
pass
|
||||
pass
|
||||
|
@@ -1,15 +1,17 @@
|
||||
# The order of packages is significant, because pip processes them in the order
|
||||
# of appearance. Changing the order has an impact on the overall integration
|
||||
# process, which may cause wedges in the gate later.
|
||||
hacking>=0.9.2,<0.10
|
||||
hacking<0.11,>=0.10.0
|
||||
|
||||
|
||||
coverage>=3.6
|
||||
discover
|
||||
python-subunit>=0.0.18
|
||||
sphinx>=1.1.2,!=1.2.0,!=1.3b1,<1.3
|
||||
oslosphinx>=2.2.0 # Apache-2.0
|
||||
oslotest>=1.2.0 # Apache-2.0
|
||||
oslosphinx>=2.5.0 # Apache-2.0
|
||||
oslotest>=1.5.1 # Apache-2.0
|
||||
testrepository>=0.0.18
|
||||
testscenarios>=0.4
|
||||
testtools>=0.9.36,!=1.2.0
|
||||
mock>=1.0
|
||||
ddt>=0.4.0
|
||||
|
7
tox.ini
7
tox.ini
@@ -20,19 +20,16 @@ commands = flake8
|
||||
commands = {posargs}
|
||||
|
||||
[testenv:cover]
|
||||
commands = python setup.py testr --coverage --coverage-package-name='tempest_lib' --testr-args='{posargs}'
|
||||
commands = python setup.py test --coverage --coverage-package-name='tempest_lib' --testr-args='{posargs}'
|
||||
|
||||
[testenv:docs]
|
||||
commands = python setup.py build_sphinx
|
||||
|
||||
[flake8]
|
||||
# H803 skipped on purpose per list discussion.
|
||||
# E125 skipped as it is invalid PEP-8.
|
||||
# H402 skipped because some docstrings aren't sentences
|
||||
# E123 skipped because it is ignored by default in the default pep8
|
||||
# E129 skipped because it is too limiting when combined with other rules
|
||||
# H305 skipped because it is inconsistent between python versions
|
||||
show-source = True
|
||||
ignore = E125,H803,H402,E123,E129,H305
|
||||
ignore = E125,E123,E129
|
||||
builtins = _
|
||||
exclude=.venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build
|
||||
|
Reference in New Issue
Block a user