Remove VMTP key pair and use user's key pair instead

make --controler-node callable
refactor SSH management code
standardize on ssh access argument format

Change-Id: Ie0b422f2381a735621bb732686a167dc1a4ca3b5
This commit is contained in:
ahothan 2015-04-30 16:11:59 -07:00
parent 0db3502c03
commit f8c3d9d711
11 changed files with 363 additions and 270 deletions

View File

@ -99,17 +99,17 @@ internal_cidr: ['192.168.1.0/24' , '192.168.2.0/24']
# Default CIDRs to use for data network for ipv6
internal_cidr_v6: ['2001:45::/64','2001:46::/64']
# The public key to use to ssh to all targets (VMs, containers, hosts)
# If starting with './' is relative to the location of the VMTP script
# else can be an absolute path
public_key_file: './ssh/id_rsa.pub'
# The public and private keys to use to ssh to all targets (VMs, containers, hosts)
# By default the SSH library will try several methods to authenticate:
# - password if provided on the command line
# - user's own key pair (under the home directory $HOME) if already setup
# - the below key pair if not empty
# If you want to use a specific key pair, specify the key pair files here.
# This can be a pathname that is absolute or relative to the current directory
public_key_file:
private_key_file:
# File containing the private key to use along with the publick key
# If starting with './' is relative to the location of the script
# else can be an absolute path
private_key_file: './ssh/id_rsa'
# Name of the P&S public key in OpenStack
# Name of the P&S public key in OpenStack to create for all test VMs
public_key_name: 'pns_public_key'
# name of the server VM
@ -122,6 +122,7 @@ vm_name_client: 'TestClient'
security_group_name: 'pns-security'
# Location to the performance test tools.
# If relative, is relative to the vmtp directory
perf_tool_path: './tools'
# ping variables

View File

@ -114,7 +114,8 @@ class Compute(object):
def create_keypair(self, name, private_key_pair_file):
self.remove_public_key(name)
keypair = self.novaclient.keypairs.create(name)
# Now write the keypair to the file
# Now write the keypair to the file if requested
if private_key_pair_file:
kpf = os.open(private_key_pair_file,
os.O_WRONLY | os.O_CREAT, 0o600)
with os.fdopen(kpf, 'w') as kpf:
@ -133,10 +134,21 @@ class Compute(object):
print 'ERROR: Cannot open public key file %s: %s' % \
(public_key_file, exc)
return None
print 'Adding public key %s' % (name)
keypair = self.novaclient.keypairs.create(name, public_key)
return keypair
def init_key_pair(self, kp_name, ssh_access):
'''Initialize the key pair for all test VMs
if a key pair is specified in access, use that key pair else
create a temporary key pair
'''
if ssh_access.public_key_file:
return self.add_public_key(kp_name, ssh_access.public_key_file)
else:
keypair = self.create_keypair(kp_name, None)
ssh_access.private_key = keypair.private_key
return keypair
def find_network(self, label):
net = self.novaclient.networks.find(label=label)
return net

View File

@ -88,7 +88,7 @@ class Credentials(object):
# just check that they are present
for varname in ['OS_USERNAME', 'OS_AUTH_URL', 'OS_TENANT_NAME']:
if varname not in os.environ:
print 'Warning: %s is missing' % (varname)
# print 'Warning: %s is missing' % (varname)
success = False
if success:
self.rc_username = os.environ['OS_USERNAME']

View File

@ -2,18 +2,32 @@
Setup
=====
Public Cloud
------------
Public clouds are special because they may not expose all OpenStack APIs and may not allow all types of operations. Some public clouds have limitations in the way virtual networks can be used or require the use of a specific external router. Running VMTP against a public cloud will require a specific configuration file that takes into account those specificities.
SSH Authentication
------------------
Refer to the provided public cloud sample configuration files for more information.
VMTP can optionally SSH to the following hosts:
- OpenStack controller node (if the --controller-node option is used)
- External host for cloud upload/download performance test (if the --external-host option is used)
- Native host throughput (if the --host option is used)
To connect to these hosts, the SSH library used by VMTP will try a number of authentication methods:
- if provided at the command line, try the provided password (e.g. --controller-node localadmin@10.1.1.78:secret)
- user's personal private key (~/.ssh/id_rsa)
- if provided in the configuration file, a specific private key file (private_key_file variable)
SSH to the test VMs is always based on key pairs with the following precedence:
- if provided in the passed configuration file, use the configured key pair (private_key_file and public_key_file variables),
- otherwise use the user's personal key pair (~/.ssh/id_rsa and ~/.ssh/id_rsa.pub)
- otherwise if there is no personal key pair configured, create a temporary key pair to access all test VMs
To summarize:
- if you have a personal key pair configured in your home directory, VMTP will use that key pair for all SSH connections (including to the test VMs)
- if you want to use your personal key pair, there is nothing to do other than making sure that the targeted hosts have been configured with the associated public key
In any case make sure you specify the correct username.
If there is a problem, you should see an error message and stack trace after the SSH library times out.
SSH Password-less Access
------------------------
For host throughput (*--host*), VMTP expects the target hosts to be pre-provisioned with a public key in order to allow password-less SSH.
Test VMs are created through OpenStack by VMTP with the appropriate public key to allow password-less ssh. By default, VMTP uses a default VMTP public key located in ssh/id_rsa.pub, simply append the content of that file into the .ssh/authorized_keys file under the host login home directory).
**Note:** This default VMTP public key should only be used for transient test VMs and **MUST NOT** be used to provision native hosts since the corresponding private key is open to anybody! To use alternate key pairs, the 'private_key_file' variable in the configuration file must be overridden to point to the file containing the private key to use to connect with SSH.

View File

@ -9,19 +9,19 @@ VMTP Usage
usage: vmtp.py [-h] [-c <config_file>] [-r <openrc_file>]
[-m <gmond_ip>[:<port>]] [-p <password>] [-t <time>]
[--host <user>@<host_ssh_ip>[:<server-listen-if-name>]]
[--host <user>@<host_ssh_ip>[:<password>:<server-listen-if-name>]]
[--external-host <user>@<host_ssh_ip>[:password>]]
[--controller-node <user>@<host_ssh_ip>[:<password>]]
[--mongod_server <server ip>] [--json <file>]
[--mongod-server <server ip>] [--json <file>]
[--tp-tool <nuttcp|iperf>] [--hypervisor [<az>:] <hostname>]
[--inter-node-only] [--protocols <T|U|I>]
[--bandwidth <bandwidth>] [--tcpbuf <tcp_pkt_size1,...>]
[--udpbuf <udp_pkt_size1,...>] [--no-env]
[--vnic-type <direct|macvtap|normal>] [-d] [-v]
[--stop-on-error] [--vm_image_url <url_to_image>]
[--test_description <test_description>]
[--stop-on-error] [--vm-image-url <url_to_image>]
[--test-description <test_description>]
OpenStack VM Throughput V2.0.4
OpenStack VM Throughput V2.1.0
optional arguments:
-h, --help show this help message and exit
@ -35,15 +35,15 @@ VMTP Usage
OpenStack password
-t <time>, --time <time>
throughput test duration in seconds (default 10 sec)
--host <user>@<host_ssh_ip>[:<server-listen-if-name>]
native host throughput (targets requires ssh key)
--host <user>@<host_ssh_ip>[:<password>:<server-listen-if-name>]
native host throughput (password or public key
required)
--external-host <user>@<host_ssh_ip>[:password>]
external-VM throughput (host requires public key if no
password)
external-VM throughput (password or public key
required)
--controller-node <user>@<host_ssh_ip>[:<password>]
controller node ssh (host requires public key if no
password)
--mongod_server <server ip>
controller node ssh (password or public key required)
--mongod-server <server ip>
provide mongoDB server IP to store results
--json <file> store results in json format file
--tp-tool <nuttcp|iperf>
@ -68,10 +68,10 @@ VMTP Usage
-v, --version print version of this script and exit
--stop-on-error Stop and keep everything as-is on error (must cleanup
manually)
--vm_image_url <url_to_image>
--vm-image-url <url_to_image>
URL to a Linux image in qcow2 format that can be
downloaded from
--test_description <test_description>
--test-description <test_description>
The test description to be stored in JSON or MongoDB
Configuration File
@ -107,8 +107,12 @@ This file should then be passed to VMTP using the *-r* option or should be sourc
Access Info for Controller Node
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
By default, VMTP is not able to get the Linux distro nor the OpenStack version of the cloud deployment under test. However, by providing the credentials of the controller node under test, VMTP will try to fetch these information, and output them along in the JSON file or to the MongoDB server.
By default, VMTP is not able to get the Linux distro nor the OpenStack version of the cloud deployment under test.
However, by providing the credentials of the controller node under test, VMTP will try to fetch these information, and output them along in the JSON file or to the MongoDB server.
For example to retrieve the OpenStack distribution information on a given controller node:
.. code:
python vmtp.py --json tb172.json --test-description 'Testbed 172' --controller-node root@172.22.191.172
Bandwidth Limit for TCP/UDP Flow Measurements
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@ -179,9 +183,10 @@ Create one configuration file for your specific cloud and use the *-c* option to
Upload the Linux image to the OpenStack controller node, so that OpenStack is able to spawning VMs. You will be prompted an error if the image defined in the config file is not available to use when running the tool. The image can be uploaded using either Horizon dashboard, or the command below::
.. code::
python vmtp.py -r admin-openrc.sh -p admin --vm_image_url http://<url_to_the_image>
**Note:** Currently, VMTP only supports the Linux image in qcow2 format.
**Note:** Currently, VMTP only supports the qcow2 format.
Examples of running VMTP on an OpenStack Cloud
@ -192,6 +197,7 @@ Example 1: Typical Run
Run VMTP on an OpenStack cloud with the default configuration file, use "admin-openrc.sh" as the rc file, and "admin" as the password::
.. code::
python vmtp.py -r admin-openrc.sh -p admin
This will generate 6 standard sets of performance data:
@ -214,22 +220,26 @@ Example 2: Cloud upload/download performance measurement
Run VMTP on an OpenStack cloud with a specified configuration file (mycfg.yaml), and saved the result to a JSON file::
.. code::
python vmtp.py -c mycfg.yaml -r admin-openrc.sh -p admin --external_host localadmin@172.29.87.29 --json res.json
This run will generate 8 sets of performance data, the standard 6 sets mentioned above, plus two sets of upload/download performance data for both TCP and UDP.
If you do not have ssh password-less access to the external host (public key) you must specify a password:
**Note:** In order to perform the upload/download performance test, an external server must be specified and configured with SSH password-less access. See below for more info.
.. code::
python vmtp.py -c mycfg.yaml -r admin-openrc.sh -p admin --external_host localadmin@172.29.87.29:secret --json res.json
Example 3: Store the OpenStack deployment details
"""""""""""""""""""""""""""""""""""""""""""""""""
Run VMTP on an OpenStack cloud, fetch the defails of the deployment and store it to JSON file. Assume the controlloer node is on 192.168.12.34 with admin/admin::
.. code::
python vmtp.py -r admin-openrc.sh -p admin --json res.json --controller-node root@192.168.12.34:admin
In addition, VMTP also supports to store the results to a MongoDB server::
.. code::
python vmtp.py -r admin-openrc.sh -p admin --json res.json --mongod_server 172.29.87.29 --controller-node root@192.168.12.34:admin
Before storing info into MongoDB, some configurations are needed to change to fit in your environment. By default, VMTP will store to database "client_db" with collection name "pns_web_entry", and of course these can be changed in the configuration file. Below are the fields which are related to accessing MongoDB::
@ -244,6 +254,7 @@ Example 4: Specify which compute nodes to spawn VMs
Run VMTP on an OpenStack cloud, spawn the test server VM on tme212, and the test client VM on tme210. Save the result, and perform the inter-node measurement only::
.. code::
python vmtp.py -r admin-openrc.sh -p admin --inter-node-only --json res.json --hypervisor tme212 --hypervisor tme210
@ -252,23 +263,25 @@ Example 5: Collect native host performance data
Run VMTP to get native host throughput between 172.29.87.29 and 172.29.87.30 using the localadmin ssh username and run each tcp/udp test session for 120 seconds (instead of the default 10 seconds)::
.. code::
python vmtp.py --host localadmin@172.29.87.29 --host localadmin@172.29.87.30 --time 120
**Note:** This command requires each host to have the VMTP public key (ssh/id_rsa.pub) inserted into the ssh/authorized_keys file in the username home directory, i.e. SSH password-less access. See below for more info.
The first IP passed (*--host*) is always the one running the server side.
If you do not have public keys setup on these targets, you must provide a password:
.. code::
python vmtp.py --host localadmin@172.29.87.29:secret --host localadmin@172.29.87.30:secret --time 120
It is also possible to run VMTP between pre-existing VMs that are accessible through SSH (using floating IP) if you have the corresponding private key to access them.
In the case of servers that have multiple NIC and IP addresses, it is possible to specify the server side listening interface name to use (if you want the client side to connect using the associated IP address)
For example, to measure throughput between 2 hosts using the network attached to the server interface "eth5"::
.. code::
python vmtp.py --host localadmin@172.29.87.29::eth5 --host localadmin@172.29.87.30
Example 6: Measurement on pre-existing VMs
""""""""""""""""""""""""""""""""""""""""""
It is possible to run VMTP between pre-existing VMs that are accessible through SSH (using floating IP).
The first IP passed (*--host*) is always the one running the server side. Optionally a server side listening interface name can be passed if clients should connect using a particular server IP. For example, to measure throughput between 2 hosts using the network attached to the server interface "eth5"::
python vmtp.py --host localadmin@172.29.87.29:eth5 --host localadmin@172.29.87.30
**Note:** Prior to running, the VMTP public key must be installed on each VM.
Example 7: IPV6 throughput measurement
Example 6: IPV6 throughput measurement
""""""""""""""""""""""""""""""""""""""
It is possible to use VMTP to measure throughput for IPv6

View File

@ -44,9 +44,8 @@ class Instance(object):
self.config = config
# internal network IP
self.internal_ip = None
self.ssh_ip = None
self.ssh_access = sshutils.SSHAccess()
self.ssh_ip_id = None
self.ssh_user = config.ssh_vm_username
self.instance = None
self.ssh = None
self.port = None
@ -58,17 +57,18 @@ class Instance(object):
self.gmond_port = int(config.gmond_svr_port)
else:
self.gmond_port = 0
self.config_drive = None
# Setup the ssh connectivity
# this function is only used for native hosts
# Returns True if success
def setup_ssh(self, ssh_ip, ssh_user):
def setup_ssh(self, host_access):
# used for displaying the source IP in json results
if not self.internal_ip:
self.internal_ip = ssh_ip
self.ssh_ip = ssh_ip
self.ssh_user = ssh_user
self.ssh = sshutils.SSH(self.ssh_user, self.ssh_ip,
key_filename=self.config.private_key_file,
self.internal_ip = host_access.host
self.ssh_access = host_access
self.buginf('Setup SSH for %s@%s' % (host_access.username, host_access.host))
self.ssh = sshutils.SSH(self.ssh_access,
connect_retry_count=self.config.ssh_retry_count)
return True
@ -76,7 +76,7 @@ class Instance(object):
# and extract internal network IP
# Retruns True if success, False otherwise
def create(self, image, flavor_type,
keypair, int_net,
ssh_access, int_net,
az,
internal_network_name,
sec_group,
@ -108,7 +108,7 @@ class Instance(object):
self.instance = self.comp.create_server(self.name,
image,
flavor_type,
keypair,
self.config.public_key_name,
nics,
sec_group,
az,
@ -121,11 +121,14 @@ class Instance(object):
self.display('Server creation failed')
return False
# clone the provided ssh access to pick up user name and key pair
self.ssh_access.copy_from(ssh_access)
# If reusing existing management network skip the floating ip creation and association to VM
# Assume management network has direct access
if self.config.reuse_network_name:
self.ssh_ip = self.instance.networks[internal_network_name][0]
self.internal_ip = self.ssh_ip
self.ssh_access.host = self.instance.networks[internal_network_name][0]
self.internal_ip = self.ssh_access.host
else:
# Set the internal ip to the correct ip for v4 and v6
for ip_address in self.instance.networks[internal_network_name]:
@ -143,18 +146,18 @@ class Instance(object):
if not fip:
self.display('Floating ip creation failed')
return False
self.ssh_ip = fip['floatingip']['floating_ip_address']
self.ssh_access.host = fip['floatingip']['floating_ip_address']
self.ssh_ip_id = fip['floatingip']['id']
self.buginf('Floating IP %s created', self.ssh_ip)
self.buginf('Started - associating floating IP %s', self.ssh_ip)
self.instance.add_floating_ip(self.ssh_ip, ipv4_fixed_address)
self.buginf('Floating IP %s created', self.ssh_access.host)
self.buginf('Started - associating floating IP %s', self.ssh_access.host)
self.instance.add_floating_ip(self.ssh_access.host, ipv4_fixed_address)
# extract the IP for the data network
self.buginf('Internal network IP: %s', self.internal_ip)
self.buginf('SSH IP: %s', self.ssh_ip)
self.buginf('SSH IP: %s', self.ssh_access.host)
# create ssh session
if not self.setup_ssh(self.ssh_ip, self.config.ssh_vm_username):
if not self.setup_ssh(self.ssh_access):
return False
return True
@ -251,8 +254,8 @@ class Instance(object):
scp_cmd = 'scp -i %s %s %s %s@%s:%s' % (self.config.private_key_file,
scp_opts,
source,
self.ssh_user,
self.ssh_ip,
self.ssh_access.username,
self.ssh_access.host,
dest)
self.buginf('Copying %s to target...', tool_name)
self.buginf(scp_cmd)
@ -304,7 +307,7 @@ class Instance(object):
def dispose(self):
if self.ssh_ip_id:
self.net.delete_floating_ip(self.ssh_ip_id)
self.buginf('Floating IP %s deleted', self.ssh_ip)
self.buginf('Floating IP %s deleted', self.ssh_access.host)
self.ssh_ip_id = None
if self.instance:
self.comp.delete_server(self.instance)

View File

@ -38,14 +38,14 @@ class PerfInstance(Instance):
# No args is reserved for native host server
def create(self, image=None, flavor_type=None,
keypair=None, nics=None, az=None,
ssh_access=None, nics=None, az=None,
management_network_name=None,
sec_group=None,
init_file_name=None):
'''Create an instance
:return: True on success, False on error
'''
rc = Instance.create(self, image, flavor_type, keypair,
rc = Instance.create(self, image, flavor_type, ssh_access,
nics, az,
management_network_name,
sec_group,

View File

@ -1,27 +0,0 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAu1wjIM/GgPbbLPoyKfN+I1uSeqrF4PYcbsTcHaQ6mEF/Ufqe
6uZVJFR1mT1ECfxCckUMM6aaf5ESNAfxEjE9Hrs/Yd3Qw5hwSlG3HJ4uZg79m1yg
ifXfnkp4rGNI6sqZxgyMbeQW3KoDaQ6zOe7e/KIkxX4XzR8I4d5lRx4ofIb806AB
QJkDrq48dHLGZnOBPvpNImpg5u6EWHGa4HI4Dl2pdyEQXTXOErupalOC1Cr8oxwu
fimSxftnl9Nh94wQtTQADnCE2GBaMMxS/ClHtJLDfmtnVC51Y4F7Ux9F3MSTSRBP
gNxcd9OikMKSj6RNq/PHw5+9R0h5v2lJXvalCQIDAQABAoIBAArCu/HCfTAi/WuT
4xWtumzlcYBCFqNY/0ENZWb+a68a8+kNb9sl53Xys95dOm8oYdiWRqEgzHbPKjB6
1EmrMkt1japdRwQ02R4rm0y1eQy7h61IoJ/L01AQDuY3vZReln5dciNNmlKKITAD
fB+zrHLuDRaaq1tIkQYH8+ElxkWAkd/vRQC4FP1OMIGnX4TdQ8lcG2DxwMs5jqJ6
ufTeR6QMDEymNYQwcdFhe5wNi57IEbN9B+N95yaktWsYV34HuYV2ndZtrhMLFhcq
Psw3vgrXBrreVPZ/iX1zeWgrjJb1AVOCtsOZ+O4YfZIIBWnhjj9sJnDCpMWmioH5
a0UmF0ECgYEA+NyIS5MmbrVJKVqWUJubSbaZXQEW3Jv4njRFAyG7NVapSbllF5t2
lq5usUI+l1XaZ3v6IpYPG+K+U1Ggo3+E6RFEDwVrZF0NYLOPXBydhkFFB4nHpTSX
uBo65/SiMDSassrqs/PFCDdsiUQ87sMFp+gouDePcBDC1OyHRDxR220CgYEAwLv6
zvqi5AvzO+tZQHRHcQdvCNC436KsUZlV6jQ5j3tUlqXlLRl8bWfih7Hu2uBGNjy2
Fao517Nd/kBdjVaechn/fvmLwgflQso0q1j63u2nZ32uYTd+zLnW1yJM4UCs/Hqb
hebRYDeZuRfobp62lEl6rdGij5LLRzQClOArso0CgYAaHClApKO3odWXPSXgNzNH
vJzCoUagxsyC7MEA3x0hL4J7dbQhkfITRSHf/y9J+Xv8t4k677uOFXAalcng3ZQ4
T9NwMAVgdlLc/nngFDCC0X5ImDAWKTpx2m6rv4L0w9AnShrt3nmhrw74J+yssFF7
mGQNT+cAvwFyDY7zndCI0QKBgEkZw0on5ApsweezHxoEQGiNcj68s7IWyBb2+pAn
GMHj/DRbXa4aYYg5g8EF6ttXfynpIwLamq/GV1ss3I7UEKqkU7S8P5brWbhYa1um
FxjguMLW94HmA5Dw15ynZNN2rWXhtwU1g6pjzElY2Q7D4eoiaIZu4aJlAfbSsjv3
PnutAoGBAMBRX8BbFODtQr68c6LWWda5zQ+kNgeCv+2ejG6rsEQ+Lxwi86Oc6udG
kTP4xuZo80MEW/t+kibFgU6gm1WTVltpbjo0XTaHE1OV4JeNC8edYFTi1DVO5r1M
ch+pkN20FQmZ+cLLn6nOeTJ6/9KXWKAZMPZ4SH4BnmF7iEa7yc8f
-----END RSA PRIVATE KEY-----

View File

@ -1 +0,0 @@
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC7XCMgz8aA9tss+jIp834jW5J6qsXg9hxuxNwdpDqYQX9R+p7q5lUkVHWZPUQJ/EJyRQwzppp/kRI0B/ESMT0euz9h3dDDmHBKUbccni5mDv2bXKCJ9d+eSnisY0jqypnGDIxt5BbcqgNpDrM57t78oiTFfhfNHwjh3mVHHih8hvzToAFAmQOurjx0csZmc4E++k0iamDm7oRYcZrgcjgOXal3IRBdNc4Su6lqU4LUKvyjHC5+KZLF+2eX02H3jBC1NAAOcITYYFowzFL8KUe0ksN+a2dULnVjgXtTH0XcxJNJEE+A3Fx306KQwpKPpE2r88fDn71HSHm/aUle9qUJ openstack-pns

View File

@ -61,6 +61,7 @@ import re
import select
import socket
import StringIO
import sys
import time
import paramiko
@ -76,12 +77,70 @@ class SSHError(Exception):
class SSHTimeout(SSHError):
pass
# Check IPv4 address syntax - not completely fool proof but will catch
# some invalid formats
def is_ipv4(address):
try:
socket.inet_aton(address)
except socket.error:
return False
return True
class SSHAccess(object):
'''
A class to contain all the information needed to access a host
(native or virtual) using SSH
'''
def __init__(self, arg_value=None):
'''
decode user@host[:pwd]
'hugo@1.1.1.1:secret' -> ('hugo', '1.1.1.1', 'secret', None)
'huggy@2.2.2.2' -> ('huggy', '2.2.2.2', None, None)
None ->(None, None, None, None)
Examples of fatal errors (will call exit):
'hutch@q.1.1.1' (invalid IP)
'@3.3.3.3' (missing username)
'hiro@' or 'buggy' (missing host IP)
The error field will be None in case of success or will
contain a string describing the error
'''
self.username = None
self.host = None
self.password = None
# name of the file that contains the private key
self.private_key_file = None
# this is the private key itself (a long string starting with
# -----BEGIN RSA PRIVATE KEY-----
# used when the private key is not saved in any file
self.private_key = None
self.public_key_file = None
self.port = 22
self.error = None
if not arg_value:
return
match = re.search(r'^([^@]+)@([0-9\.]+):?(.*)$', arg_value)
if not match:
self.error = 'Invalid argument: ' + arg_value
return
if not is_ipv4(match.group(2)):
self.error = 'Invalid IPv4 address ' + match.group(2)
return
(self.username, self.host, self.password) = match.groups()
def copy_from(self, ssh_access):
self.username = ssh_access.username
self.host = ssh_access.host
self.port = ssh_access.port
self.password = ssh_access.password
self.private_key = ssh_access.private_key
self.public_key_file = ssh_access.public_key_file
self.private_key_file = ssh_access.private_key_file
class SSH(object):
"""Represent ssh connection."""
def __init__(self, user, host, port=22, pkey=None,
key_filename=None, password=None,
def __init__(self, ssh_access,
connect_timeout=60,
connect_retry_count=30,
connect_retry_wait_sec=2):
@ -98,12 +157,11 @@ class SSH(object):
:param connect_retry_wait_sec: seconds to wait between retries
"""
self.user = user
self.host = host
self.port = port
self.pkey = self._get_pkey(pkey) if pkey else None
self.password = password
self.key_filename = key_filename
self.ssh_access = ssh_access
if ssh_access.private_key:
self.pkey = self._get_pkey(ssh_access.private_key)
else:
self.pkey = None
self._client = False
self.connect_timeout = connect_timeout
self.connect_retry_count = connect_retry_count
@ -114,6 +172,9 @@ class SSH(object):
self.__get_distro()
def _get_pkey(self, key):
'''Get the binary form of the private key
from the text form
'''
if isinstance(key, basestring):
key = StringIO.StringIO(key)
errors = []
@ -131,10 +192,12 @@ class SSH(object):
self._client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
for _ in range(self.connect_retry_count):
try:
self._client.connect(self.host, username=self.user,
port=self.port, pkey=self.pkey,
key_filename=self.key_filename,
password=self.password,
self._client.connect(self.ssh_access.host,
username=self.ssh_access.username,
port=self.ssh_access.port,
pkey=self.pkey,
key_filename=self.ssh_access.private_key_file,
password=self.ssh_access.password,
timeout=self.connect_timeout)
return self._client
except (paramiko.AuthenticationException,
@ -144,7 +207,7 @@ class SSH(object):
time.sleep(self.connect_retry_wait_sec)
self._client = None
msg = '[%s] SSH Connection failed after %s attempts' % (self.host,
msg = '[%s] SSH Connection failed after %s attempts' % (self.ssh_access.host,
self.connect_retry_count)
raise SSHError(msg)
@ -225,7 +288,7 @@ class SSH(object):
break
if timeout and (time.time() - timeout) > start_time:
args = {'cmd': cmd, 'host': self.host}
args = {'cmd': cmd, 'host': self.ssh_access.host}
raise SSHTimeout(('Timeout executing command '
'"%(cmd)s" on host %(host)s') % args)
# if e:
@ -269,7 +332,7 @@ class SSH(object):
except (socket.error, SSHError):
time.sleep(interval)
if time.time() > (start_time + timeout):
raise SSHTimeout(('Timeout waiting for "%s"') % self.host)
raise SSHTimeout(('Timeout waiting for "%s"') % self.ssh_access.host)
def __extract_property(self, name, input_str):
expr = name + r'="?([\w\.]*)"?'
@ -587,19 +650,19 @@ class SSH(object):
# invoked from pns script.
##################################################
def main():
# ssh = SSH('localadmin', '172.29.87.29', key_filename='./ssh/id_rsa')
ssh = SSH('localadmin', '172.22.191.173', key_filename='./ssh/id_rsa')
# As argument pass the SSH access string, e.g. "localadmin@1.1.1.1:secret"
test_ssh = SSH(SSHAccess(sys.argv[1]))
print 'ID=' + ssh.distro_id
print 'ID_LIKE=' + ssh.distro_id_like
print 'VERSION_ID=' + ssh.distro_version
print 'ID=' + test_ssh.distro_id
print 'ID_LIKE=' + test_ssh.distro_id_like
print 'VERSION_ID=' + test_ssh.distro_version
# ssh.wait()
# print ssh.pidof('bash')
# print ssh.stat('/tmp')
print ssh.check_openstack_version()
print ssh.get_cpu_info()
print ssh.get_l2agent_version("Open vSwitch agent")
print test_ssh.check_openstack_version()
print test_ssh.get_cpu_info()
print test_ssh.get_l2agent_version("Open vSwitch agent")
if __name__ == "__main__":
main()

293
vmtp.py
View File

@ -20,8 +20,6 @@ import json
import os
import pprint
import re
import socket
import stat
import sys
import traceback
@ -40,23 +38,14 @@ from neutronclient.v2_0 import client as neutronclient
from novaclient.client import Client
from novaclient.exceptions import ClientException
__version__ = '2.0.4'
__version__ = '2.1.0'
from perf_instance import PerfInstance as PerfInstance
# Check IPv4 address syntax - not completely fool proof but will catch
# some invalid formats
def is_ipv4(address):
try:
socket.inet_aton(address)
except socket.error:
return False
return True
def get_absolute_path_for_file(file_name):
def get_vmtp_absolute_path_for_file(file_name):
'''
Return the filename in absolute path for any file
passed as relateive path.
passed as relative path to the vmtp directory
'''
if os.path.isabs(__file__):
abs_file_path = os.path.join(__file__.split("vmtp.py")[0],
@ -73,10 +62,16 @@ def normalize_paths(cfg):
'''
Normalize the various paths to config files, tools, ssh priv and pub key
files.
If a relative path is entered:
- the key pair file names are relative to the current directory
- the perftool path is relative to vmtp itself
'''
cfg.public_key_file = get_absolute_path_for_file(cfg.public_key_file)
cfg.private_key_file = get_absolute_path_for_file(cfg.private_key_file)
cfg.perf_tool_path = get_absolute_path_for_file(cfg.perf_tool_path)
if cfg.public_key_file:
cfg.public_key_file = os.path.abspath(os.path.expanduser(cfg.public_key_file))
if cfg.private_key_file:
cfg.private_key_file = os.path.expanduser(os.path.expanduser(cfg.private_key_file))
if cfg.perf_tool_path:
cfg.perf_tool_path = get_vmtp_absolute_path_for_file(cfg.perf_tool_path)
class FlowPrinter(object):
@ -100,6 +95,9 @@ class ResultsCollector(object):
def add_property(self, name, value):
self.results[name] = value
def add_properties(self, properties):
self.results.update(properties)
def add_flow_result(self, flow_res):
self.results['flows'].append(flow_res)
self.ppr.pprint(flow_res)
@ -110,45 +108,23 @@ class ResultsCollector(object):
def pprint(self, res):
self.ppr.pprint(res)
def get_controller_info(self, cfg, net):
if cfg.ctrl_username and cfg.ctrl_host:
print 'Fetching OpenStack deployment details...'
if cfg.ctrl_password:
sshcon = sshutils.SSH(cfg.ctrl_username,
cfg.ctrl_host,
password=cfg.ctrl_password)
else:
sshcon = sshutils.SSH(cfg.ctrl_username,
cfg.ctrl_host,
key_filename=cfg.private_key_file,
connect_retry_count=cfg.ssh_retry_count)
if sshcon is not None:
self.results['distro'] = sshcon.get_host_os_version()
self.results['openstack_version'] = sshcon.check_openstack_version()
self.results['cpu_info'] = sshcon.get_cpu_info()
if 'l2agent_type' in self.results and 'encapsulation' in self.results:
self.results['nic_name'] = sshcon.get_nic_name(
self.results['l2agent_type'], self.results['encapsulation'],
net.internal_iface_dict)
self.results['l2agent_version'] = sshcon.get_l2agent_version(
self.results['l2agent_type'])
else:
self.results['nic_name'] = "Unknown"
else:
print 'ERROR: Cannot connect to the controller node.'
def get_result(self, key):
if keystoneclient in self.results:
return self.results[key]
return None
def mask_credentials(self):
args = self.results['args']
if not args:
arguments = self.results['args']
if not arguments:
return
list = ['-p', '--host', '--external-host', '--controller-node']
for keyword in list:
arg_list = ['-p', '--host', '--external-host', '--controller-node']
for keyword in arg_list:
pattern = keyword + r'\s+[^\s]+'
string = keyword + ' <MASKED>'
args = re.sub(pattern, string, args)
arguments = re.sub(pattern, string, arguments)
self.results['args'] = args
self.results['args'] = arguments
def generate_runid(self):
key = self.results['args'] + self.results['date'] + self.results['version']
@ -203,7 +179,7 @@ class VmtpTest(object):
def create_instance(self, inst, az, int_net):
self.assert_true(inst.create(self.image_instance,
self.flavor_type,
config.public_key_name,
instance_access,
int_net,
az,
int_net['name'],
@ -223,9 +199,9 @@ class VmtpTest(object):
neutron = neutronclient.Client(**creds)
self.comp = compute.Compute(nova_client, config)
# Add the script public key to openstack
self.comp.add_public_key(config.public_key_name,
config.public_key_file)
# Add the appropriate public key to openstack
self.comp.init_key_pair(config.public_key_name, instance_access)
self.image_instance = self.comp.find_image(config.image_name)
if self.image_instance is None:
@ -269,13 +245,13 @@ class VmtpTest(object):
self.server.internal_ip = config.vm_server_internal_ip
self.client.internal_ip = config.vm_client_internal_ip
if config.vm_server_external_ip:
self.server.ssh_ip = config.vm_server_external_ip
self.server.ssh_access.host = config.vm_server_external_ip
else:
self.server.ssh_ip = config.vm_server_internal_ip
self.server.ssh_access.host = config.vm_server_internal_ip
if config.vm_client_external_ip:
self.client.ssh_ip = config.vm_client_external_ip
self.client.ssh_access.host = config.vm_client_external_ip
else:
self.client.ssh_ip = config.vm_client_internal_ip
self.client.ssh_access.host = config.vm_client_internal_ip
return
# this is the standard way of running the test
@ -318,15 +294,15 @@ class VmtpTest(object):
# Test throughput for the case of the external host
def ext_host_tp_test(self):
client = PerfInstance('Host-' + ext_host_list[1] + '-Client', config)
if not client.setup_ssh(ext_host_list[1], ext_host_list[0]):
client.display('SSH failed, check IP or make sure public key is configured')
client = PerfInstance('Host-' + config.ext_host.host + '-Client', config)
if not client.setup_ssh(config.ext_host):
client.display('SSH to ext host failed, check IP or make sure public key is configured')
else:
client.buginf('SSH connected')
client.create()
fpr.print_desc('External-VM (upload/download)')
res = client.run_client('External-VM',
self.server.ssh_ip,
self.server.ssh_access.host,
self.server,
bandwidth=config.vm_bandwidth,
bidirectional=True)
@ -392,13 +368,13 @@ class VmtpTest(object):
self.server.internal_ip)
if not config.ipv6_mode:
self.measure_flow("VM to VM different network floating IP",
self.server.ssh_ip)
self.server.ssh_access.host)
self.client.dispose()
self.client = None
# If external network is specified run that case
if ext_host_list[0]:
if config.ext_host:
self.ext_host_tp_test()
def teardown(self):
@ -442,12 +418,12 @@ class VmtpTest(object):
else:
self.teardown()
def test_native_tp(nhosts):
def test_native_tp(nhosts, ifname):
fpr.print_desc('Native Host to Host throughput')
server_host = nhosts[0]
server = PerfInstance('Host-' + server_host[1] + '-Server', config, server=True)
server = PerfInstance('Host-' + server_host.host + '-Server', config, server=True)
if not server.setup_ssh(server_host[1], server_host[0]):
if not server.setup_ssh(server_host):
server.display('SSH failed, check IP or make sure public key is configured')
else:
server.display('SSH connected')
@ -460,23 +436,23 @@ def test_native_tp(nhosts):
nhosts.pop(0)
# IP address clients should connect to, check if the user
# has passed a server listen interface name
if len(server_host) == 3:
if ifname:
# use the IP address configured on given interface
server_ip = server.get_interface_ip(server_host[2])
server_ip = server.get_interface_ip(ifname)
if not server_ip:
print('Error: cannot get IP address for interface ' + server_host[2])
print('Error: cannot get IP address for interface ' + ifname)
else:
server.display('Clients will use server IP address %s (%s)' %
(server_ip, server_host[2]))
(server_ip, ifname))
else:
# use same as ssh IP
server_ip = server_host[1]
server_ip = server_host.host
if server_ip:
# start client side, 1 per host provided
for client_host in nhosts:
client = PerfInstance('Host-' + client_host[1] + '-Client', config)
if not client.setup_ssh(client_host[1], client_host[0]):
client = PerfInstance('Host-' + client_host.host + '-Client', config)
if not client.setup_ssh(client_host):
client.display('SSH failed, check IP or make sure public key is configured')
else:
client.buginf('SSH connected')
@ -489,35 +465,30 @@ def test_native_tp(nhosts):
client.dispose()
server.dispose()
def extract_user_host_pwd(user_host_pwd):
def _get_ssh_access(opt_name, opt_value):
'''Allocate a HostSshAccess instance to the option value
Check that a password is provided or the key pair in the config file
is valid.
If invalid exit with proper error message
'''
splits user@host[:pwd] into a 3 element tuple
'hugo@1.1.1.1:secret' -> ('hugo', '1.1.1.1', 'secret')
'huggy@2.2.2.2' -> ('huggy', '2.2.2.2', None)
None ->(None, None, None)
Examples of fatal errors (will call exit):
'hutch@q.1.1.1' (invalid IP)
'@3.3.3.3' (missing username)
'hiro@' or 'buggy' (missing host IP)
'''
if not user_host_pwd:
return (None, None, None)
match = re.search(r'^([^@]+)@([0-9\.]+):?(.*)$', user_host_pwd)
if not match:
print('Invalid argument: ' + user_host_pwd)
sys.exit(3)
if not is_ipv4(match.group(2)):
print 'Invalid IPv4 address ' + match.group(2)
sys.exit(4)
return match.groups()
if not opt_value:
return None
def _merge_config(file, source_config, required=False):
host_access = sshutils.SSHAccess(opt_value)
host_access.private_key_file = config.private_key_file
host_access.public_key_file = config.public_key_file
if host_access.error:
print'Error for --' + (opt_name + ':' + host_access.error)
sys.exit(2)
return host_access
def _merge_config(cfg_file, source_config, required=False):
'''
returns the merged config or exits if the file does not exist and is required
'''
dest_config = source_config
fullname = os.path.expanduser(file)
fullname = os.path.expanduser(cfg_file)
if os.path.isfile(fullname):
print('Loading ' + fullname + '...')
try:
@ -535,6 +506,32 @@ def _merge_config(file, source_config, required=False):
sys.exit(1)
return dest_config
def get_controller_info(ssh_access, net, res_col):
if not ssh_access:
return
print 'Fetching OpenStack deployment details...'
sshcon = sshutils.SSH(ssh_access,
connect_retry_count=config.ssh_retry_count)
if sshcon is None:
print 'ERROR: Cannot connect to the controller node'
return
res = {}
res['distro'] = sshcon.get_host_os_version()
res['openstack_version'] = sshcon.check_openstack_version()
res['cpu_info'] = sshcon.get_cpu_info()
if net:
l2type = res_col.get_result('l2agent_type')
encap = res_col.get_result('encapsulation')
if l2type:
if encap:
res['nic_name'] = sshcon.get_nic_name(l2type, encap,
net.internal_iface_dict)
res['l2agent_version'] = sshcon.get_l2agent_version(l2type)
# print results
res_col.pprint(res)
res_col.add_properties(res)
if __name__ == '__main__':
fpr = FlowPrinter()
@ -569,20 +566,20 @@ if __name__ == '__main__':
parser.add_argument('--host', dest='hosts',
action='append',
help='native host throughput (targets requires ssh key)',
metavar='<user>@<host_ssh_ip>[:<server-listen-if-name>]')
help='native host throughput (password or public key required)',
metavar='<user>@<host_ssh_ip>[:<password>:<server-listen-if-name>]')
parser.add_argument('--external-host', dest='ext_host',
action='store',
help='external-VM throughput (host requires public key if no password)',
help='external-VM throughput (password or public key required)',
metavar='<user>@<host_ssh_ip>[:password>]')
parser.add_argument('--controller-node', dest='controller_node',
action='store',
help='controller node ssh (host requires public key if no password)',
help='controller node ssh (password or public key required)',
metavar='<user>@<host_ssh_ip>[:<password>]')
parser.add_argument('--mongod_server', dest='mongod_server',
parser.add_argument('--mongod-server', dest='mongod_server',
action='store',
help='provide mongoDB server IP to store results',
metavar='<server ip>')
@ -663,12 +660,12 @@ if __name__ == '__main__':
action='store_true',
help='Stop and keep everything as-is on error (must cleanup manually)')
parser.add_argument('--vm_image_url', dest='vm_image_url',
parser.add_argument('--vm-image-url', dest='vm_image_url',
action='store',
help='URL to a Linux image in qcow2 format that can be downloaded from',
metavar='<url_to_image>')
parser.add_argument('--test_description', dest='test_description',
parser.add_argument('--test-description', dest='test_description',
action='store',
help='The test description to be stored in JSON or MongoDB',
metavar='<test_description>')
@ -676,7 +673,7 @@ if __name__ == '__main__':
(opts, args) = parser.parse_known_args()
default_cfg_file = get_absolute_path_for_file("cfg.default.yaml")
default_cfg_file = get_vmtp_absolute_path_for_file("cfg.default.yaml")
# read the default configuration file and possibly an override config file
# the precedence order is as follows:
@ -697,6 +694,13 @@ if __name__ == '__main__':
config.debug = opts.debug
config.inter_node_only = opts.inter_node_only
if config.public_key_file and not os.path.isfile(config.public_key_file):
print('Warning: invalid public_key_file:' + config.public_key_file)
config.public_key_file = None
if config.private_key_file and not os.path.isfile(config.private_key_file):
print('Warning: invalid private_key_file:' + config.private_key_file)
config.private_key_file = None
# direct: use SR-IOV ports for all the test VMs
if opts.vnic_type not in [None, 'direct', 'macvtap', 'normal']:
print('Invalid vnic-type: ' + opts.vnic_type)
@ -716,14 +720,31 @@ if __name__ == '__main__':
else:
config.json_file = None
###################################################
# controller node ssh access to collect metadata for
# the run.
###################################################
(config.ctrl_username, config.ctrl_host, config.ctrl_password) = \
extract_user_host_pwd(opts.controller_node)
# Add the external host info to a list
ext_host_list = list(extract_user_host_pwd(opts.ext_host))
# Initialize the external host access
config.ext_host = _get_ssh_access('external-host', opts.ext_host)
# This is a template host access that will be used for all instances
# (the only specific field specific to each instance is the host IP)
# For test VM access, we never use password and always need a key pair
instance_access = sshutils.SSHAccess()
instance_access.username = config.ssh_vm_username
# if the configuration does not have a
# key pair specified, we check if the user has a personal key pair
# if no key pair is configured or usable, a temporary key pair will be created
if config.public_key_file and config.private_key_file:
instance_access.public_key_file = config.public_key_file
instance_access.private_key_file = config.private_key_file
else:
pub_key = os.path.expanduser('~/.ssh/id_rsa.pub')
priv_key = os.path.expanduser('~/.ssh/id_rsa')
if os.path.isfile(pub_key) and os.path.isfile(priv_key):
instance_access.public_key_file = pub_key
instance_access.private_key_file = priv_key
if opts.debug and instance_access.public_key_file:
print('VM public key: ' + instance_access.public_key_file)
print('VM private key: ' + instance_access.private_key_file)
###################################################
# VM Image URL
@ -812,10 +833,6 @@ if __name__ == '__main__':
###################################################
normalize_paths(config)
# first chmod the local private key since git does not keep the permission
# as this is required by ssh/scp
os.chmod(config.private_key_file, stat.S_IRUSR | stat.S_IWUSR)
# Check the tp-tool name
config.protocols = opts.protocols.upper()
if 'T' in config.protocols or 'U' in config.protocols:
@ -829,31 +846,24 @@ if __name__ == '__main__':
else:
config.tp_tool = None
# 3 forms are accepted:
# --host 1.1.1.1
# --host root@1.1.1.1
# --host root@1.1.1.1:eth0
# A list of 0 to 2 lists where each nested list is
# a list of 1 to 3 elements. e.g.:
# [['ubuntu','1.1.1.1'],['root', 2.2.2.2]]
# [['ubuntu','1.1.1.1', 'eth0'],['root', 2.2.2.2]]
# when not provided the default user is 'root'
# 3 forms
# A list of 0 to 2 HostSshAccess elements
if opts.hosts:
native_hosts = []
if_name = None
for host in opts.hosts:
# split on '@' first
elem_list = host.split("@")
if len(elem_list) == 1:
elem_list.insert(0, 'root')
# split out the if name if present
# ['root':'1.1.1.1:eth0'] becomes ['root':'1.1.1.1', 'eth0']
if ':' in elem_list[1]:
elem_list.extend(elem_list.pop().split(':'))
if not is_ipv4(elem_list[1]):
print 'Invalid IPv4 address ' + elem_list[1]
sys.exit(1)
native_hosts.append(elem_list)
test_native_tp(native_hosts)
# decode and extract the trailing if name first
# there is an if name if there are at least 2 ':' in the argument
# e.g. "root@1.1.1.1:secret:eth0"
if host.count(':') >= 2:
last_column_index = host.rfind(':')
# can be empty
last_arg = host[last_column_index + 1:]
if not if_name and last_arg:
if_name = last_arg
host = host[:last_column_index]
native_hosts.append(_get_ssh_access('host', host))
test_native_tp(native_hosts, if_name)
cred = credentials.Credentials(opts.rc, opts.pwd, opts.no_env)
@ -861,17 +871,22 @@ if __name__ == '__main__':
# those args that have not been parsed by this parser so that the
# unit test parser is not bothered by local arguments
sys.argv[1:] = args
vmtp_net = None
if cred.rc_auth_url:
if opts.debug:
print 'Using ' + cred.rc_auth_url
rescol.add_property('auth_url', cred.rc_auth_url)
vmtp = VmtpTest()
vmtp.run()
vmtp_net = vmtp.net
# Retrieve controller information if requested
# controller node ssh access to collect metadata for the run.
ctrl_host_access = _get_ssh_access('controller-node', opts.controller_node)
get_controller_info(ctrl_host_access, vmtp_net, rescol)
# If saving the results to JSON or MongoDB, get additional details:
if config.json_file or config.vmtp_mongod_ip:
rescol.get_controller_info(config, vmtp.net)
rescol.mask_credentials()
rescol.generate_runid()