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
|
iso8601>=0.1.9
|
||||||
jsonschema>=2.0.0,<3.0.0
|
jsonschema>=2.0.0,<3.0.0
|
||||||
httplib2>=0.7.5
|
httplib2>=0.7.5
|
||||||
|
paramiko>=1.13.0
|
||||||
six>=1.9.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 copy
|
||||||
import datetime
|
import datetime
|
||||||
import re
|
import re
|
||||||
try:
|
|
||||||
import urllib.parse as urlparse
|
|
||||||
except ImportError:
|
|
||||||
import urlparse
|
|
||||||
|
|
||||||
from oslo_log import log as logging
|
from oslo_log import log as logging
|
||||||
import six
|
import six
|
||||||
|
from six.moves.urllib import parse as urlparse
|
||||||
|
|
||||||
from tempest_lib import exceptions
|
from tempest_lib import exceptions
|
||||||
from tempest_lib.services.identity.v2 import token_client as json_v2id
|
from tempest_lib.services.identity.v2 import token_client as json_v2id
|
||||||
|
@@ -190,7 +190,11 @@ def run_time():
|
|||||||
runtime = 0.0
|
runtime = 0.0
|
||||||
for k, v in RESULTS.items():
|
for k, v in RESULTS.items():
|
||||||
for test in v:
|
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
|
return runtime
|
||||||
|
|
||||||
|
|
||||||
|
@@ -270,8 +270,8 @@ class RestClient(object):
|
|||||||
"""Send a HTTP DELETE request using keystone service catalog and auth
|
"""Send a HTTP DELETE request using keystone service catalog and auth
|
||||||
|
|
||||||
:param str url: the relative url to send the post request to
|
: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 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()
|
:param dict extra_headers: If the headers returned by the get_headers()
|
||||||
method are to be used but additional headers
|
method are to be used but additional headers
|
||||||
are needed in the request pass them in as a
|
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
|
headers and the catalog to determine the endpoint to use for the
|
||||||
baseurl to send the request to. Additionally
|
baseurl to send the request to. Additionally
|
||||||
|
|
||||||
When a response is recieved it will check it to see if an error
|
When a response is received it will check it to see if an error
|
||||||
response was recieved. If it was an exception will be raised to enable
|
response was received. If it was an exception will be raised to enable
|
||||||
it to be handled quickly.
|
it to be handled quickly.
|
||||||
|
|
||||||
This method will also handle rate-limiting, if a 413 response code is
|
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.
|
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 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
|
:param dict extra_headers: If specified without the headers kwarg the
|
||||||
headers sent with the request will be the
|
headers sent with the request will be the
|
||||||
combination from the get_headers() method
|
combination from the get_headers() method
|
||||||
@@ -578,23 +578,23 @@ class RestClient(object):
|
|||||||
and the second the response body
|
and the second the response body
|
||||||
:raises InvalidContentType: If the content-type of the response isn't
|
:raises InvalidContentType: If the content-type of the response isn't
|
||||||
an expect type or a 415 response code is
|
an expect type or a 415 response code is
|
||||||
recieved
|
received
|
||||||
:raises Unauthorized: If a 401 response code is recieved
|
:raises Unauthorized: If a 401 response code is received
|
||||||
:raises Forbidden: If a 403 response code is recieved
|
:raises Forbidden: If a 403 response code is received
|
||||||
:raises NotFound: If a 404 response code is recieved
|
:raises NotFound: If a 404 response code is received
|
||||||
:raises BadRequest: If a 400 response code is recieved
|
:raises BadRequest: If a 400 response code is received
|
||||||
:raises Conflict: If a 409 response code is recieved
|
:raises Conflict: If a 409 response code is received
|
||||||
:raies Overlimit: If a 413 response code is recieved and over_limit is
|
:raises OverLimit: If a 413 response code is received and over_limit is
|
||||||
not in the response body
|
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
|
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
|
:raises InvalidHTTPResponseBody: The response body wasn't valid JSON
|
||||||
and couldn't be parsed
|
and couldn't be parsed
|
||||||
:raises NotImplemented: If a 501 response code is recieved
|
:raises NotImplemented: If a 501 response code is received
|
||||||
:raises ServerFault: If a 500 response code is recieved
|
:raises ServerFault: If a 500 response code is received
|
||||||
:raises UnexpectedResponseCode: If a response code above 400 is
|
: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
|
of the handled checks
|
||||||
"""
|
"""
|
||||||
# if extra_headers is True
|
# 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():
|
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())
|
return str(uuid.uuid4())
|
||||||
|
|
||||||
|
|
||||||
def rand_uuid_hex():
|
def rand_uuid_hex():
|
||||||
|
"""Generate a random UUID hex string
|
||||||
|
|
||||||
|
:return: a random UUID (e.g. '0b98cf96d90447bda4b46f31aeb1508c')
|
||||||
|
:rtype: string
|
||||||
|
"""
|
||||||
return uuid.uuid4().hex
|
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))
|
randbits = str(random.randint(1, 0x7fffffff))
|
||||||
|
rand_name = randbits
|
||||||
if name:
|
if name:
|
||||||
return name + '-' + randbits
|
rand_name = name + '-' + rand_name
|
||||||
else:
|
if prefix:
|
||||||
return randbits
|
rand_name = prefix + '-' + rand_name
|
||||||
|
return rand_name
|
||||||
|
|
||||||
|
|
||||||
def rand_url():
|
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))
|
randbits = str(random.randint(1, 0x7fffffff))
|
||||||
return 'https://url-' + randbits + '.com'
|
return 'https://url-' + randbits + '.com'
|
||||||
|
|
||||||
|
|
||||||
def rand_int_id(start=0, end=0x7fffffff):
|
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)
|
return random.randint(start, end)
|
||||||
|
|
||||||
|
|
||||||
def rand_mac_address():
|
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
|
# NOTE(vish): We would prefer to use 0xfe here to ensure that linux
|
||||||
# bridge mac addresses don't change, but it appears to
|
# bridge mac addresses don't change, but it appears to
|
||||||
# conflict with libvirt, so we use the next highest octet
|
# conflict with libvirt, so we use the next highest octet
|
||||||
@@ -60,14 +98,27 @@ def rand_mac_address():
|
|||||||
|
|
||||||
|
|
||||||
def parse_image_id(image_ref):
|
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]
|
return image_ref.rsplit('/')[-1]
|
||||||
|
|
||||||
|
|
||||||
def arbitrary_string(size=4, base_text=None):
|
def arbitrary_string(size=4, base_text=None):
|
||||||
"""Return size characters from base_text
|
"""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:
|
if not base_text:
|
||||||
base_text = 'test'
|
base_text = 'test'
|
||||||
@@ -75,12 +126,24 @@ def arbitrary_string(size=4, base_text=None):
|
|||||||
|
|
||||||
|
|
||||||
def random_bytes(size=1024):
|
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))
|
return ''.join([chr(random.randint(0, 255))
|
||||||
for i in range(size)])
|
for i in range(size)])
|
||||||
|
|
||||||
|
|
||||||
def get_ipv6_addr_by_EUI64(cidr, mac):
|
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
|
# Check if the prefix is IPv4 address
|
||||||
is_ipv4 = netaddr.valid_ipv4(cidr)
|
is_ipv4 = netaddr.valid_ipv4(cidr)
|
||||||
if is_ipv4:
|
if is_ipv4:
|
||||||
|
@@ -150,4 +150,19 @@ class IdentityError(TempestException):
|
|||||||
|
|
||||||
|
|
||||||
class EndpointNotFound(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
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
import netaddr
|
||||||
|
|
||||||
from tempest_lib.common.utils import data_utils
|
from tempest_lib.common.utils import data_utils
|
||||||
from tempest_lib.tests import base
|
from tempest_lib.tests import base
|
||||||
@@ -48,6 +49,20 @@ class TestDataUtils(base.TestCase):
|
|||||||
self.assertTrue(actual.startswith('foo'))
|
self.assertTrue(actual.startswith('foo'))
|
||||||
self.assertNotEqual(actual, actual2)
|
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):
|
def test_rand_int(self):
|
||||||
actual = data_utils.rand_int_id()
|
actual = data_utils.rand_int_id()
|
||||||
self.assertIsInstance(actual, int)
|
self.assertIsInstance(actual, int)
|
||||||
@@ -75,3 +90,50 @@ class TestDataUtils(base.TestCase):
|
|||||||
self.assertEqual(actual, "abc" * int(30 / len("abc")))
|
self.assertEqual(actual, "abc" * int(30 / len("abc")))
|
||||||
actual = data_utils.arbitrary_string(size=5, base_text="deadbeaf")
|
actual = data_utils.arbitrary_string(size=5, base_text="deadbeaf")
|
||||||
self.assertEqual(actual, "deadb")
|
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):
|
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):
|
def auth_request(self, method, url, headers=None, body=None, filters=None):
|
||||||
return url, headers, body
|
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):
|
def setUp(self):
|
||||||
super(BaseRestClientTestClass, self).setUp()
|
super(BaseRestClientTestClass, self).setUp()
|
||||||
|
self.fake_auth_provider = fake_auth_provider.FakeAuthProvider()
|
||||||
self.rest_client = rest_client.RestClient(
|
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.stubs.Set(httplib2.Http, 'request', self.fake_http.request)
|
||||||
self.useFixture(mockpatch.PatchObject(self.rest_client,
|
self.useFixture(mockpatch.PatchObject(self.rest_client,
|
||||||
'_log_request'))
|
'_log_request'))
|
||||||
@@ -437,6 +438,59 @@ class TestRestClientUtils(BaseRestClientTestClass):
|
|||||||
self.rest_client.wait_for_resource_deletion,
|
self.rest_client.wait_for_resource_deletion,
|
||||||
'1234')
|
'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):
|
class TestExpectedSuccess(BaseRestClientTestClass):
|
||||||
|
|
||||||
@@ -483,3 +537,33 @@ class TestExpectedSuccess(BaseRestClientTestClass):
|
|||||||
read_code = 202
|
read_code = 202
|
||||||
self.assertRaises(AssertionError, self.rest_client.expected_success,
|
self.assertRaises(AssertionError, self.rest_client.expected_success,
|
||||||
expected_code, read_code)
|
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):
|
class TestTempest_lib(base.TestCase):
|
||||||
|
|
||||||
def test_something(self):
|
def test_something(self):
|
||||||
pass
|
pass
|
||||||
|
@@ -1,15 +1,17 @@
|
|||||||
# The order of packages is significant, because pip processes them in the order
|
# 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
|
# 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.
|
||||||
hacking>=0.9.2,<0.10
|
hacking<0.11,>=0.10.0
|
||||||
|
|
||||||
|
|
||||||
coverage>=3.6
|
coverage>=3.6
|
||||||
discover
|
discover
|
||||||
python-subunit>=0.0.18
|
python-subunit>=0.0.18
|
||||||
sphinx>=1.1.2,!=1.2.0,!=1.3b1,<1.3
|
sphinx>=1.1.2,!=1.2.0,!=1.3b1,<1.3
|
||||||
oslosphinx>=2.2.0 # Apache-2.0
|
oslosphinx>=2.5.0 # Apache-2.0
|
||||||
oslotest>=1.2.0 # Apache-2.0
|
oslotest>=1.5.1 # Apache-2.0
|
||||||
testrepository>=0.0.18
|
testrepository>=0.0.18
|
||||||
testscenarios>=0.4
|
testscenarios>=0.4
|
||||||
testtools>=0.9.36,!=1.2.0
|
testtools>=0.9.36,!=1.2.0
|
||||||
mock>=1.0
|
mock>=1.0
|
||||||
|
ddt>=0.4.0
|
||||||
|
7
tox.ini
7
tox.ini
@@ -20,19 +20,16 @@ commands = flake8
|
|||||||
commands = {posargs}
|
commands = {posargs}
|
||||||
|
|
||||||
[testenv:cover]
|
[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]
|
[testenv:docs]
|
||||||
commands = python setup.py build_sphinx
|
commands = python setup.py build_sphinx
|
||||||
|
|
||||||
[flake8]
|
[flake8]
|
||||||
# H803 skipped on purpose per list discussion.
|
|
||||||
# E125 skipped as it is invalid PEP-8.
|
# 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
|
# E123 skipped because it is ignored by default in the default pep8
|
||||||
# E129 skipped because it is too limiting when combined with other rules
|
# E129 skipped because it is too limiting when combined with other rules
|
||||||
# H305 skipped because it is inconsistent between python versions
|
|
||||||
show-source = True
|
show-source = True
|
||||||
ignore = E125,H803,H402,E123,E129,H305
|
ignore = E125,E123,E129
|
||||||
builtins = _
|
builtins = _
|
||||||
exclude=.venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build
|
exclude=.venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build
|
||||||
|
Reference in New Issue
Block a user