Add resources for floating_ip, keypair, volume. Add floating_ip test.

Provide mechanism to pre-allocate vms, floating_ips, keypairs and volumes.
Abstract time-related functions to PendingAction and move server-specific
  stuff to PendingServerAction subclass.
Rename State to ClusterState.
Add test that associates/disassociates floating_ips and servers.

Change-Id: I1651c38cc75d755bde370fb6a49ff4231e96255e
diff --git a/stress/driver.py b/stress/driver.py
index 9f263f6..71d02a9 100644
--- a/stress/driver.py
+++ b/stress/driver.py
@@ -23,9 +23,11 @@
 
 # local imports
 from test_case import *
-from state import State
 import utils.util
 from config import StressConfig
+from state import ClusterState, KeyPairState, FloatingIpState, VolumeState
+from tempest.common.utils.data_utils import rand_name
+
 
 # setup logging to file
 logging.basicConfig(
@@ -96,6 +98,53 @@
     return False
 
 
+def create_initial_vms(manager, state, count):
+    image = manager.config.compute.image_ref
+    flavor = manager.config.compute.flavor_ref
+    servers = []
+    logging.info('Creating %d vms' % count)
+    for _ in xrange(count):
+        name = rand_name('initial_vm-')
+        _, server = manager.servers_client.create_server(name, image, flavor)
+        servers.append(server)
+    for server in servers:
+        manager.servers_client.wait_for_server_status(server['id'], 'ACTIVE')
+        logging.info('Server Name: %s Id: %s' % (name, server['id']))
+        state.set_instance_state(server['id'], (server, 'ACTIVE'))
+
+
+def create_initial_floating_ips(manager, state, count):
+    logging.info('Creating %d floating ips' % count)
+    for _ in xrange(count):
+        _, ip = manager.floating_ips_client.create_floating_ip()
+        logging.info('Ip: %s' % ip['ip'])
+        state.add_floating_ip(FloatingIpState(ip))
+
+
+def create_initial_keypairs(manager, state, count):
+    logging.info('Creating %d keypairs' % count)
+    for _ in xrange(count):
+        name = rand_name('keypair-')
+        _, keypair = manager.keypairs_client.create_keypair(name)
+        logging.info('Keypair: %s' % name)
+        state.add_keypair(KeyPairState(keypair))
+
+
+def create_initial_volumes(manager, state, count):
+    volumes = []
+    logging.info('Creating %d volumes' % count)
+    for _ in xrange(count):
+        name = rand_name('volume-')
+        _, volume = manager.volumes_client.create_volume(size=1,
+                                                         display_name=name)
+        volumes.append(volume)
+    for volume in volumes:
+        manager.volumes_client.wait_for_volume_status(volume['id'],
+                                                      'available')
+        logging.info('Volume Name: %s Id: %s' % (name, volume['id']))
+        state.add_volume(VolumeState(volume))
+
+
 def bash_openstack(manager,
                    choice_spec,
                    **kwargs):
@@ -130,8 +179,16 @@
                               "rm -f %s/*.log" % logdir)
     random.seed(seed)
     cases = _create_cases(choice_spec)
+    state = ClusterState(max_vms=max_vms)
+    create_initial_keypairs(manager, state,
+                             int(kwargs.get('initial_keypairs', 0)))
+    create_initial_vms(manager, state,
+                       int(kwargs.get('initial_vms', 0)))
+    create_initial_floating_ips(manager, state,
+                                int(kwargs.get('initial_floating_ips', 0)))
+    create_initial_volumes(manager, state,
+                                int(kwargs.get('initial_volumes', 0)))
     test_end_time = time.time() + duration.seconds
-    state = State(max_vms=max_vms)
 
     retry_list = []
     last_retry = time.time()
@@ -163,6 +220,7 @@
             logging.debug('retry verifications for %d tasks', len(retry_list))
             new_retry_list = []
             for v in retry_list:
+                v.check_timeout()
                 if not v.retry():
                     new_retry_list.append(v)
             retry_list = new_retry_list
@@ -180,7 +238,8 @@
     # Cleanup
     logging.info('Cleaning up: terminating virtual machines...')
     vms = state.get_instances()
-    active_vms = [v for _k, v in vms.iteritems() if v and v[1] == 'ACTIVE']
+    active_vms = [v for _k, v in vms.iteritems()
+                  if v and v[1] != 'TERMINATING']
     for target in active_vms:
         manager.servers_client.delete_server(target[0]['id'])
         # check to see that the server was actually killed
@@ -199,6 +258,13 @@
             time.sleep(1)
         logging.info('killed %s' % kill_id)
         state.delete_instance_state(kill_id)
+    for floating_ip_state in state.get_floating_ips():
+        manager.floating_ips_client.delete_floating_ip(
+                                            floating_ip_state.resource_id)
+    for keypair_state in state.get_keypairs():
+        manager.keypairs_client.delete_keypair(keypair_state.name)
+    for volume_state in state.get_volumes():
+        manager.volumes_client.delete_volume(volume_state.resource_id)
 
     if test_succeeded:
         logging.info('*** Test succeeded ***')
diff --git a/stress/pending_action.py b/stress/pending_action.py
index 913cc42..67eba13 100644
--- a/stress/pending_action.py
+++ b/stress/pending_action.py
@@ -17,6 +17,7 @@
 
 import logging
 import time
+from tempest.exceptions import TimeoutException
 
 
 class PendingAction(object):
@@ -25,25 +26,55 @@
     is successful.
     """
 
-    def __init__(self, nova_manager, state, target_server, timeout=600):
+    def __init__(self, nova_manager, timeout=None):
         """
         `nova_manager` : Manager object.
+        `timeout`   : time before we declare a TimeoutException
+        """
+        if timeout == None:
+            timeout = nova_manager.config.compute.build_timeout
+        self._manager = nova_manager
+        self._logger = logging.getLogger(self.__class__.__name__)
+        self._start_time = time.time()
+        self._timeout = timeout
+
+    def retry(self):
+        """
+        Invoked by user of this class to verify completion of
+        previous TestCase actions
+        """
+        return False
+
+    def check_timeout(self):
+        """Check for timeouts of TestCase actions"""
+        time_diff = time.time() - self._start_time
+        if time_diff > self._timeout:
+            self._logger.error('%s exceeded timeout of %d' %
+                               (self.__class__.__name__, self._timeout))
+            raise TimeoutException
+
+    def elapsed(self):
+        return time.time() - self._start_time
+
+
+class PendingServerAction(PendingAction):
+    """
+    Initialize and describe actions to verify that a Nova API call that
+    changes server state is successful.
+    """
+
+    def __init__(self, nova_manager, state, target_server, timeout=None):
+        """
         `state`           : externally maintained data structure about
                             state of VMs or other persistent objects in
                             the nova cluster
         `target_server`   : server that actions were performed on
-        `target_server`   : time before we declare a TimeoutException
-        `pargs`           : positional arguments
-        `kargs`           : keyword arguments
         """
-        self._manager = nova_manager
+        super(PendingServerAction, self).__init__(nova_manager,
+                                                  timeout=timeout)
         self._state = state
         self._target = target_server
 
-        self._logger = logging.getLogger(self.__class__.__name__)
-        self._start_time = time.time()
-        self._timeout = timeout
-
     def _check_for_status(self, state_string):
         """Check to see if the machine has transitioned states"""
         t = time.time()  # for debugging
@@ -58,8 +89,3 @@
             return temp_obj[1]
         self._logger.debug('%s, time: %d' % (state_string, time.time() - t))
         return state_string
-
-    def retry(self):
-        """Invoked by user of this class to verify completion of"""
-        """previous TestCase actions"""
-        return False
diff --git a/stress/state.py b/stress/state.py
index 60b1acc..3a9f12e 100644
--- a/stress/state.py
+++ b/stress/state.py
@@ -11,19 +11,21 @@
 #    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.
-"""A class to store the state of various persistent objects in the Nova
-cluster, e.g. instances, volumes.  Use methods to query to state which than
-can be compared to the current state of the objects in Nova"""
 
 
-class State(object):
+class ClusterState(object):
+    """A class to store the state of various persistent objects in the Nova
+    cluster, e.g. instances, volumes.  Use methods to query to state which than
+    can be compared to the current state of the objects in Nova"""
 
     def __init__(self, **kwargs):
         self._max_vms = kwargs.get('max_vms', 32)
         self._instances = {}
-        self._volumes = {}
+        self._floating_ips = []
+        self._keypairs = []
+        self._volumes = []
 
-    # machine state methods
+    # instance state methods
     def get_instances(self):
         """return the instances dictionary that we believe are in cluster."""
         return self._instances
@@ -39,3 +41,75 @@
     def delete_instance_state(self, key):
         """Delete state indexed at `key`."""
         del self._instances[key]
+
+    #floating_ip state methods
+    def get_floating_ips(self):
+        """return the floating ips list for the cluster."""
+        return self._floating_ips
+
+    def add_floating_ip(self, floating_ip_state):
+        """Add floating ip."""
+        self._floating_ips.append(floating_ip_state)
+
+    def remove_floating_ip(self, floating_ip_state):
+        """Remove floating ip."""
+        self._floating_ips.remove(floating_ip_state)
+
+    # keypair methods
+    def get_keypairs(self):
+        """return the keypairs list for the cluster."""
+        return self._keypairs
+
+    def add_keypair(self, keypair_state):
+        """Add keypair."""
+        self._keypairs.append(keypair_state)
+
+    def remove_keypair(self, keypair_state):
+        """Remove keypair."""
+        self._keypairs.remove(keypair_state)
+
+    # volume methods
+    def get_volumes(self):
+        """return the volumes list for the cluster."""
+        return self._volumes
+
+    def add_volume(self, volume_state):
+        """Add volume."""
+        self._volumes.append(volume_state)
+
+    def remove_volume(self, volume_state):
+        """Remove volume."""
+        self._volumes.remove(volume_state)
+
+
+class ServerAssociatedState(object):
+    """Class that tracks resources that are associated with a particular server
+    such as a volume or floating ip"""
+
+    def __init__(self, resource_id):
+        # The id of the server.
+        self.server_id = None
+        # The id of the resource that is attached to the server.
+        self.resource_id = resource_id
+        # True if in the process of attaching/detaching the resource.
+        self.change_pending = False
+
+
+class FloatingIpState(ServerAssociatedState):
+
+    def __init__(self, ip_desc):
+        super(FloatingIpState, self).__init__(ip_desc['id'])
+        self.address = ip_desc['ip']
+
+
+class VolumeState(ServerAssociatedState):
+
+    def __init__(self, volume_desc):
+        super(VolumeState, self).__init__(volume_desc['id'])
+
+
+class KeyPairState(object):
+
+    def __init__(self, keypair_spec):
+        self.name = keypair_spec['name']
+        self.private_key = keypair_spec['private_key']
diff --git a/stress/test_floating_ips.py b/stress/test_floating_ips.py
new file mode 100755
index 0000000..a2a20db
--- /dev/null
+++ b/stress/test_floating_ips.py
@@ -0,0 +1,95 @@
+# Copyright 2011 Quanta Research Cambridge, Inc.
+#
+#    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.
+
+
+# system imports
+import random
+import time
+import telnetlib
+import logging
+
+# local imports
+import test_case
+import pending_action
+
+
+class TestChangeFloatingIp(test_case.StressTestCase):
+    """Add or remove a floating ip from a vm."""
+
+    def __init__(self):
+        super(TestChangeFloatingIp, self).__init__()
+        self.server_ids = None
+
+    def run(self, manager, state, *pargs, **kwargs):
+        if self.server_ids == None:
+            vms = state.get_instances()
+            self.server_ids = [k for k, v in vms.iteritems()]
+        floating_ip = random.choice(state.get_floating_ips())
+        if floating_ip.change_pending:
+            return None
+        floating_ip.change_pending = True
+        timeout = int(kwargs.get('timeout', 60))
+        if floating_ip.server_id == None:
+            server = random.choice(self.server_ids)
+            address = floating_ip.address
+            self._logger.info('Adding %s to server %s' % (address, server))
+            resp, body =\
+            manager.floating_ips_client.associate_floating_ip_to_server(
+                                                        address,
+                                                        server)
+            if resp.status != 202:
+                raise Exception("response: %s body: %s" % (resp, body))
+            floating_ip.server_id = server
+            return VerifyChangeFloatingIp(manager, floating_ip,
+                                          timeout, add=True)
+        else:
+            server = floating_ip.server_id
+            address = floating_ip.address
+            self._logger.info('Removing %s from server %s' % (address, server))
+            resp, body =\
+            manager.floating_ips_client.disassociate_floating_ip_from_server(
+                                                           address, server)
+            if resp.status != 202:
+                raise Exception("response: %s body: %s" % (resp, body))
+            return VerifyChangeFloatingIp(manager, floating_ip,
+                                          timeout, add=False)
+
+
+class VerifyChangeFloatingIp(pending_action.PendingAction):
+    """Verify that floating ip was changed"""
+    def __init__(self, manager, floating_ip, timeout, add=None):
+        super(VerifyChangeFloatingIp, self).__init__(manager, timeout=timeout)
+        self.floating_ip = floating_ip
+        self.add = add
+
+    def retry(self):
+        """
+        Check to see that we can contact the server at its new address.
+        """
+        try:
+            conn = telnetlib.Telnet(self.floating_ip.address, 22, timeout=0.5)
+            conn.close()
+            if self.add:
+                self._logger.info('%s added [%.1f secs elapsed]' %
+                          (self.floating_ip.address, self.elapsed()))
+                self.floating_ip.change_pending = False
+                return True
+        except:
+            if not self.add:
+                self._logger.info('%s removed [%.1f secs elapsed]' %
+                          (self.floating_ip.address, self.elapsed()))
+                self.floating_ip.change_pending = False
+                self.floating_ip.server_id = None
+                return True
+        return False
diff --git a/stress/test_server_actions.py b/stress/test_server_actions.py
index 7080630..6b4f462 100644
--- a/stress/test_server_actions.py
+++ b/stress/test_server_actions.py
@@ -12,9 +12,9 @@
 #    See the License for the specific language governing permissions and
 #    limitations under the License.
 """Defines various sub-classes of the `StressTestCase` and
-`PendingAction` class. The sub-classes of StressTestCase implement various
+`PendingServerAction` class. Sub-classes of StressTestCase implement various
 API calls on the Nova cluster having to do with Server Actions. Each
-sub-class will have a corresponding PendingAction. These pending
+sub-class will have a corresponding PendingServerAction. These pending
 actions veriy that the API call was successful or not."""
 
 
@@ -25,7 +25,7 @@
 # local imports
 import test_case
 import pending_action
-from tempest.exceptions import TimeoutException, Duplicate
+from tempest.exceptions import Duplicate
 from utils.util import *
 
 
@@ -83,7 +83,7 @@
                               reboot_state=reboot_state)
 
 
-class VerifyRebootVM(pending_action.PendingAction):
+class VerifyRebootVM(pending_action.PendingServerAction):
     """Class to verify that the reboot completed."""
     States = enum('REBOOT_CHECK', 'ACTIVE_CHECK')
 
@@ -110,8 +110,6 @@
                                self._target['id'])
             return True
 
-        if time.time() - self._start_time > self._timeout:
-            raise TimeoutException
         reboot_state = self._reboot_state
         if self._retry_state == self.States.REBOOT_CHECK:
             server_state = self._check_for_status(reboot_state)
@@ -131,8 +129,7 @@
                 return False
         target = self._target
         self._logger.info('machine %s %s -> ACTIVE [%.1f secs elapsed]' %
-                              (target['id'], reboot_state,
-                                time.time() - self._start_time))
+                              (target['id'], reboot_state, self.elapsed()))
         self._state.set_instance_state(target['id'],
                                       (target, 'ACTIVE'))
 
@@ -200,7 +197,7 @@
 #                              state_name=state_name,
 #                              timeout=_timeout)
 #
-#class VerifyResizeVM(pending_action.PendingAction):
+#class VerifyResizeVM(pending_action.PendingServerAction):
 #    """Verify that resizing of a VM was successful"""
 #    States = enum('VERIFY_RESIZE_CHECK', 'ACTIVE_CHECK')
 #
@@ -228,9 +225,6 @@
 #                               self._target['id'])
 #            return True
 #
-#        if time.time() - self._start_time > self._timeout:
-#            raise TimeoutException
-#
 #        if self._retry_state == self.States.VERIFY_RESIZE_CHECK:
 #            if self._check_for_status('VERIFY_RESIZE') == 'VERIFY_RESIZE':
 #                # now issue command to CONFIRM RESIZE
@@ -245,7 +239,7 @@
 #
 #                self._logger.info(
 #                    'CONFIRMING RESIZE of machine %s [%.1f secs elapsed]' %
-#                    (self._target['id'], time.time() - self._start_time)
+#                    (self._target['id'], self.elapsed())
 #                    )
 #                state.set_instance_state(self._target['id'],
 #                                        (self._target, 'CONFIRM_RESIZE'))
@@ -274,7 +268,7 @@
 #
 #                self._logger.info(
 #                    'machine %s: VERIFY_RESIZE -> ACTIVE [%.1f sec elapsed]' %
-#                    (self._target['id'], time.time() - self._start_time)
+#                    (self._target['id'], self.elapsed())
 #                    )
 #                self._state.set_instance_state(self._target['id'],
 #                                              (self._target, 'ACTIVE'))
diff --git a/stress/test_servers.py b/stress/test_servers.py
index a71bea2..57c923a 100644
--- a/stress/test_servers.py
+++ b/stress/test_servers.py
@@ -12,9 +12,9 @@
 #    See the License for the specific language governing permissions and
 #    limitations under the License.
 """Defines various sub-classes of the `StressTestCase` and
-`PendingAction` class. The sub-classes of StressTestCase implement various
+`PendingServerAction` class. Sub-classes of StressTestCase implement various
 API calls on the Nova cluster having to do with creating and deleting VMs.
-Each sub-class will have a corresponding PendingAction. These pending
+Each sub-class will have a corresponding PendingServerAction. These pending
 actions veriy that the API call was successful or not."""
 
 
@@ -26,7 +26,6 @@
 # local imports
 import test_case
 import pending_action
-from tempest.exceptions import TimeoutException
 
 
 class TestCreateVM(test_case.StressTestCase):
@@ -101,7 +100,7 @@
                               expected_server)
 
 
-class VerifyCreateVM(pending_action.PendingAction):
+class VerifyCreateVM(pending_action.PendingServerAction):
     """Verify that VM was built and is running"""
     def __init__(self, manager,
                  state,
@@ -127,12 +126,6 @@
                                self._target['id'])
             return True
 
-        time_diff = time.time() - self._start_time
-        if time_diff > self._timeout:
-            self._logger.error('%d exceeded launch server timeout of %d' %
-                               (time_diff, self._timeout))
-            raise TimeoutException
-
         admin_pass = self._target['adminPass']
         # Could check more things here.
         if (self._expected['adminPass'] != admin_pass):
@@ -146,7 +139,7 @@
             return False
 
         self._logger.info('machine %s: BUILD -> ACTIVE [%.1f secs elapsed]' %
-                          (self._target['id'], time.time() - self._start_time))
+                          (self._target['id'], self.elapsed()))
         self._state.set_instance_state(self._target['id'],
                                       (self._target, 'ACTIVE'))
         return True
@@ -186,7 +179,7 @@
                                   killtarget, timeout=_timeout)
 
 
-class VerifyKillActiveVM(pending_action.PendingAction):
+class VerifyKillActiveVM(pending_action.PendingServerAction):
     """Verify that server was destroyed"""
 
     def retry(self):
@@ -201,19 +194,13 @@
         if (not tid in self._state.get_instances().keys()):
             return False
 
-        time_diff = time.time() - self._start_time
-        if time_diff > self._timeout:
-            self._logger.error('server %s: %d exceeds terminate timeout %d' %
-                               (tid, time_diff, self._timeout))
-            raise TimeoutException
-
         try:
             self._manager.servers_client.get_server(tid)
         except Exception:
             # if we get a 404 response, is the machine really gone?
             target = self._target
             self._logger.info('machine %s: DELETED [%.1f secs elapsed]' %
-                              (target['id'], time.time() - self._start_time))
+                              (target['id'], self.elapsed()))
             self._state.delete_instance_state(target['id'])
             return True
 
@@ -305,7 +292,7 @@
                                   timeout=_timeout)
 
 
-class VerifyUpdateVMName(pending_action.PendingAction):
+class VerifyUpdateVMName(pending_action.PendingServerAction):
     """Check that VM has new name"""
     def retry(self):
         """
@@ -318,9 +305,6 @@
             'TERMINATING'):
             return False
 
-        if time.time() - self._start_time > self._timeout:
-            raise TimeoutException
-
         response, body = \
             self._manager.serverse_client.get_server(self._target['id'])
         if (response.status != 200):
diff --git a/stress/tests/floating_ips.py b/stress/tests/floating_ips.py
new file mode 100755
index 0000000..03bd509
--- /dev/null
+++ b/stress/tests/floating_ips.py
@@ -0,0 +1,33 @@
+# Copyright 2011 Quanta Research Cambridge, Inc.
+#
+#    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.
+"""Stress test that associates/disasssociates floating ips"""
+
+# local imports
+from stress.test_floating_ips import TestChangeFloatingIp
+from stress.basher import BasherAction
+from stress.driver import *
+from tempest import openstack
+
+choice_spec = [
+    BasherAction(TestChangeFloatingIp(), 100)
+]
+
+nova = openstack.Manager()
+
+bash_openstack(nova,
+               choice_spec,
+               duration=datetime.timedelta(seconds=300),
+               test_name="floating_ips",
+               initial_floating_ips=8,
+               initial_vms=8)
diff --git a/stress/tools/nova_destroy_all.py b/stress/tools/nova_destroy_all.py
index e9010cd..21cac11 100755
--- a/stress/tools/nova_destroy_all.py
+++ b/stress/tools/nova_destroy_all.py
@@ -31,6 +31,7 @@
 images_list = nt.images.list()
 keypairs_list = nt.keypairs.list()
 floating_ips_list = nt.floating_ips.list()
+volumes_list = nt.volumes.list()
 
 print "total servers: %3d, total flavors: %3d, total images: %3d," % \
     (len(server_list),
@@ -52,3 +53,7 @@
 print "deleting all floating_ips"
 for s in floating_ips_list:
     s.delete()
+
+print "deleting all volumes"
+for s in volumes_list:
+    s.delete()