Close files on server create, add tests
The files opened for the --files and --user-data options were never closed, potentially leaking memory in a long-running client. Close them if they are file objects. Add a couple of basic tests for server create. Change-Id: I1658b0caa2d6af17308149cb52196ee28266ddf2
This commit is contained in:
		| @@ -17,6 +17,7 @@ | |||||||
|  |  | ||||||
| import argparse | import argparse | ||||||
| import getpass | import getpass | ||||||
|  | import io | ||||||
| import logging | import logging | ||||||
| import os | import os | ||||||
| import six | import six | ||||||
| @@ -296,7 +297,7 @@ class CreateServer(show.ShowOne): | |||||||
|         for f in parsed_args.file: |         for f in parsed_args.file: | ||||||
|             dst, src = f.split('=', 1) |             dst, src = f.split('=', 1) | ||||||
|             try: |             try: | ||||||
|                 files[dst] = open(src) |                 files[dst] = io.open(src, 'rb') | ||||||
|             except IOError as e: |             except IOError as e: | ||||||
|                 raise exceptions.CommandError("Can't open '%s': %s" % (src, e)) |                 raise exceptions.CommandError("Can't open '%s': %s" % (src, e)) | ||||||
|  |  | ||||||
| @@ -313,7 +314,7 @@ class CreateServer(show.ShowOne): | |||||||
|         userdata = None |         userdata = None | ||||||
|         if parsed_args.user_data: |         if parsed_args.user_data: | ||||||
|             try: |             try: | ||||||
|                 userdata = open(parsed_args.user_data) |                 userdata = io.open(parsed_args.user_data) | ||||||
|             except IOError as e: |             except IOError as e: | ||||||
|                 msg = "Can't open '%s': %s" |                 msg = "Can't open '%s': %s" | ||||||
|                 raise exceptions.CommandError(msg % (parsed_args.user_data, e)) |                 raise exceptions.CommandError(msg % (parsed_args.user_data, e)) | ||||||
| @@ -368,7 +369,17 @@ class CreateServer(show.ShowOne): | |||||||
|  |  | ||||||
|         self.log.debug('boot_args: %s', boot_args) |         self.log.debug('boot_args: %s', boot_args) | ||||||
|         self.log.debug('boot_kwargs: %s', boot_kwargs) |         self.log.debug('boot_kwargs: %s', boot_kwargs) | ||||||
|  |  | ||||||
|  |         # Wrap the call to catch exceptions in order to close files | ||||||
|  |         try: | ||||||
|             server = compute_client.servers.create(*boot_args, **boot_kwargs) |             server = compute_client.servers.create(*boot_args, **boot_kwargs) | ||||||
|  |         finally: | ||||||
|  |             # Clean up open files - make sure they are not strings | ||||||
|  |             for f in files: | ||||||
|  |                 if hasattr(f, 'close'): | ||||||
|  |                     f.close() | ||||||
|  |             if hasattr(userdata, 'close'): | ||||||
|  |                 userdata.close() | ||||||
|  |  | ||||||
|         if parsed_args.wait: |         if parsed_args.wait: | ||||||
|             if utils.wait_for_status( |             if utils.wait_for_status( | ||||||
|   | |||||||
| @@ -26,6 +26,7 @@ server_name = 'waiter' | |||||||
| SERVER = { | SERVER = { | ||||||
|     'id': server_id, |     'id': server_id, | ||||||
|     'name': server_name, |     'name': server_name, | ||||||
|  |     'metadata': {}, | ||||||
| } | } | ||||||
|  |  | ||||||
| extension_name = 'Multinic' | extension_name = 'Multinic' | ||||||
|   | |||||||
| @@ -14,11 +14,13 @@ | |||||||
| # | # | ||||||
|  |  | ||||||
| import copy | import copy | ||||||
|  | import mock | ||||||
|  |  | ||||||
| from openstackclient.compute.v2 import server | from openstackclient.compute.v2 import server | ||||||
| from openstackclient.tests.compute.v2 import fakes as compute_fakes | from openstackclient.tests.compute.v2 import fakes as compute_fakes | ||||||
| from openstackclient.tests import fakes | from openstackclient.tests import fakes | ||||||
| from openstackclient.tests.image.v2 import fakes as image_fakes | from openstackclient.tests.image.v2 import fakes as image_fakes | ||||||
|  | from openstackclient.tests import utils | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestServer(compute_fakes.TestComputev2): | class TestServer(compute_fakes.TestComputev2): | ||||||
| @@ -30,6 +32,10 @@ class TestServer(compute_fakes.TestComputev2): | |||||||
|         self.servers_mock = self.app.client_manager.compute.servers |         self.servers_mock = self.app.client_manager.compute.servers | ||||||
|         self.servers_mock.reset_mock() |         self.servers_mock.reset_mock() | ||||||
|  |  | ||||||
|  |         # Get a shortcut to the ImageManager Mock | ||||||
|  |         self.cimages_mock = self.app.client_manager.compute.images | ||||||
|  |         self.cimages_mock.reset_mock() | ||||||
|  |  | ||||||
|         # Get a shortcut to the FlavorManager Mock |         # Get a shortcut to the FlavorManager Mock | ||||||
|         self.flavors_mock = self.app.client_manager.compute.flavors |         self.flavors_mock = self.app.client_manager.compute.flavors | ||||||
|         self.flavors_mock.reset_mock() |         self.flavors_mock.reset_mock() | ||||||
| @@ -39,6 +45,174 @@ class TestServer(compute_fakes.TestComputev2): | |||||||
|         self.images_mock.reset_mock() |         self.images_mock.reset_mock() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class TestServerCreate(TestServer): | ||||||
|  |  | ||||||
|  |     def setUp(self): | ||||||
|  |         super(TestServerCreate, self).setUp() | ||||||
|  |  | ||||||
|  |         self.servers_mock.create.return_value = fakes.FakeResource( | ||||||
|  |             None, | ||||||
|  |             copy.deepcopy(compute_fakes.SERVER), | ||||||
|  |             loaded=True, | ||||||
|  |         ) | ||||||
|  |         new_server = fakes.FakeResource( | ||||||
|  |             None, | ||||||
|  |             copy.deepcopy(compute_fakes.SERVER), | ||||||
|  |             loaded=True, | ||||||
|  |         ) | ||||||
|  |         new_server.__dict__['networks'] = {} | ||||||
|  |         self.servers_mock.get.return_value = new_server | ||||||
|  |  | ||||||
|  |         self.image = fakes.FakeResource( | ||||||
|  |             None, | ||||||
|  |             copy.deepcopy(image_fakes.IMAGE), | ||||||
|  |             loaded=True, | ||||||
|  |         ) | ||||||
|  |         self.cimages_mock.get.return_value = self.image | ||||||
|  |  | ||||||
|  |         self.flavor = fakes.FakeResource( | ||||||
|  |             None, | ||||||
|  |             copy.deepcopy(compute_fakes.FLAVOR), | ||||||
|  |             loaded=True, | ||||||
|  |         ) | ||||||
|  |         self.flavors_mock.get.return_value = self.flavor | ||||||
|  |  | ||||||
|  |         # Get the command object to test | ||||||
|  |         self.cmd = server.CreateServer(self.app, None) | ||||||
|  |  | ||||||
|  |     def test_server_create_no_options(self): | ||||||
|  |         arglist = [ | ||||||
|  |             compute_fakes.server_id, | ||||||
|  |         ] | ||||||
|  |         verifylist = [ | ||||||
|  |             ('server_name', compute_fakes.server_id), | ||||||
|  |         ] | ||||||
|  |         try: | ||||||
|  |             # Missing required args should bail here | ||||||
|  |             self.check_parser(self.cmd, arglist, verifylist) | ||||||
|  |         except utils.ParserException: | ||||||
|  |             pass | ||||||
|  |  | ||||||
|  |     def test_server_create_minimal(self): | ||||||
|  |         arglist = [ | ||||||
|  |             '--image', 'image1', | ||||||
|  |             '--flavor', 'flavor1', | ||||||
|  |             compute_fakes.server_id, | ||||||
|  |         ] | ||||||
|  |         verifylist = [ | ||||||
|  |             ('image', 'image1'), | ||||||
|  |             ('flavor', 'flavor1'), | ||||||
|  |             ('config_drive', False), | ||||||
|  |             ('server_name', compute_fakes.server_id), | ||||||
|  |         ] | ||||||
|  |         parsed_args = self.check_parser(self.cmd, arglist, verifylist) | ||||||
|  |  | ||||||
|  |         # DisplayCommandBase.take_action() returns two tuples | ||||||
|  |         columns, data = self.cmd.take_action(parsed_args) | ||||||
|  |  | ||||||
|  |         # Set expected values | ||||||
|  |         kwargs = dict( | ||||||
|  |             meta=None, | ||||||
|  |             files={}, | ||||||
|  |             reservation_id=None, | ||||||
|  |             min_count=1, | ||||||
|  |             max_count=1, | ||||||
|  |             security_groups=[], | ||||||
|  |             userdata=None, | ||||||
|  |             key_name=None, | ||||||
|  |             availability_zone=None, | ||||||
|  |             block_device_mapping={}, | ||||||
|  |             nics=[], | ||||||
|  |             scheduler_hints={}, | ||||||
|  |             config_drive=None, | ||||||
|  |         ) | ||||||
|  |         # ServerManager.create(name, image, flavor, **kwargs) | ||||||
|  |         self.servers_mock.create.assert_called_with( | ||||||
|  |             compute_fakes.server_id, | ||||||
|  |             self.image, | ||||||
|  |             self.flavor, | ||||||
|  |             **kwargs | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         collist = ('addresses', 'flavor', 'id', 'image', 'name', 'properties') | ||||||
|  |         self.assertEqual(columns, collist) | ||||||
|  |         datalist = ( | ||||||
|  |             '', | ||||||
|  |             'Large ()', | ||||||
|  |             compute_fakes.server_id, | ||||||
|  |             'graven ()', | ||||||
|  |             compute_fakes.server_name, | ||||||
|  |             '', | ||||||
|  |         ) | ||||||
|  |         self.assertEqual(data, datalist) | ||||||
|  |  | ||||||
|  |     @mock.patch('openstackclient.compute.v2.server.io.open') | ||||||
|  |     def test_server_create_userdata(self, mock_open): | ||||||
|  |         mock_file = mock.MagicMock(name='File') | ||||||
|  |         mock_open.return_value = mock_file | ||||||
|  |         mock_open.read.return_value = '#!/bin/sh' | ||||||
|  |  | ||||||
|  |         arglist = [ | ||||||
|  |             '--image', 'image1', | ||||||
|  |             '--flavor', 'flavor1', | ||||||
|  |             '--user-data', 'userdata.sh', | ||||||
|  |             compute_fakes.server_id, | ||||||
|  |         ] | ||||||
|  |         verifylist = [ | ||||||
|  |             ('image', 'image1'), | ||||||
|  |             ('flavor', 'flavor1'), | ||||||
|  |             ('user_data', 'userdata.sh'), | ||||||
|  |             ('config_drive', False), | ||||||
|  |             ('server_name', compute_fakes.server_id), | ||||||
|  |         ] | ||||||
|  |         parsed_args = self.check_parser(self.cmd, arglist, verifylist) | ||||||
|  |  | ||||||
|  |         # DisplayCommandBase.take_action() returns two tuples | ||||||
|  |         columns, data = self.cmd.take_action(parsed_args) | ||||||
|  |  | ||||||
|  |         # Ensure the userdata file is opened | ||||||
|  |         mock_open.assert_called_with('userdata.sh') | ||||||
|  |  | ||||||
|  |         # Ensure the userdata file is closed | ||||||
|  |         mock_file.close.assert_called() | ||||||
|  |  | ||||||
|  |         # Set expected values | ||||||
|  |         kwargs = dict( | ||||||
|  |             meta=None, | ||||||
|  |             files={}, | ||||||
|  |             reservation_id=None, | ||||||
|  |             min_count=1, | ||||||
|  |             max_count=1, | ||||||
|  |             security_groups=[], | ||||||
|  |             userdata=mock_file, | ||||||
|  |             key_name=None, | ||||||
|  |             availability_zone=None, | ||||||
|  |             block_device_mapping={}, | ||||||
|  |             nics=[], | ||||||
|  |             scheduler_hints={}, | ||||||
|  |             config_drive=None, | ||||||
|  |         ) | ||||||
|  |         # ServerManager.create(name, image, flavor, **kwargs) | ||||||
|  |         self.servers_mock.create.assert_called_with( | ||||||
|  |             compute_fakes.server_id, | ||||||
|  |             self.image, | ||||||
|  |             self.flavor, | ||||||
|  |             **kwargs | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         collist = ('addresses', 'flavor', 'id', 'image', 'name', 'properties') | ||||||
|  |         self.assertEqual(columns, collist) | ||||||
|  |         datalist = ( | ||||||
|  |             '', | ||||||
|  |             'Large ()', | ||||||
|  |             compute_fakes.server_id, | ||||||
|  |             'graven ()', | ||||||
|  |             compute_fakes.server_name, | ||||||
|  |             '', | ||||||
|  |         ) | ||||||
|  |         self.assertEqual(data, datalist) | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestServerDelete(TestServer): | class TestServerDelete(TestServer): | ||||||
|  |  | ||||||
|     def setUp(self): |     def setUp(self): | ||||||
|   | |||||||
| @@ -23,6 +23,10 @@ import testtools | |||||||
| from openstackclient.tests import fakes | from openstackclient.tests import fakes | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class ParserException(Exception): | ||||||
|  |     pass | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestCase(testtools.TestCase): | class TestCase(testtools.TestCase): | ||||||
|     def setUp(self): |     def setUp(self): | ||||||
|         testtools.TestCase.setUp(self) |         testtools.TestCase.setUp(self) | ||||||
| @@ -84,7 +88,7 @@ class TestCommand(TestCase): | |||||||
|         try: |         try: | ||||||
|             parsed_args = cmd_parser.parse_args(args) |             parsed_args = cmd_parser.parse_args(args) | ||||||
|         except SystemExit: |         except SystemExit: | ||||||
|             raise Exception("Argument parse failed") |             raise ParserException("Argument parse failed") | ||||||
|         for av in verify_args: |         for av in verify_args: | ||||||
|             attr, value = av |             attr, value = av | ||||||
|             if attr: |             if attr: | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Dean Troyer
					Dean Troyer