Skip to content

Commit 89217a6

Browse files
author
Dean Troyer
committed
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
1 parent 36212c4 commit 89217a6

4 files changed

Lines changed: 194 additions & 4 deletions

File tree

‎openstackclient/compute/v2/server.py‎

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import argparse
1919
import getpass
20+
import io
2021
import logging
2122
import os
2223
import six
@@ -296,7 +297,7 @@ def take_action(self, parsed_args):
296297
for f in parsed_args.file:
297298
dst, src = f.split('=', 1)
298299
try:
299-
files[dst] = open(src)
300+
files[dst] = io.open(src, 'rb')
300301
except IOError as e:
301302
raise exceptions.CommandError("Can't open '%s': %s" % (src, e))
302303

@@ -313,7 +314,7 @@ def take_action(self, parsed_args):
313314
userdata = None
314315
if parsed_args.user_data:
315316
try:
316-
userdata = open(parsed_args.user_data)
317+
userdata = io.open(parsed_args.user_data)
317318
except IOError as e:
318319
msg = "Can't open '%s': %s"
319320
raise exceptions.CommandError(msg % (parsed_args.user_data, e))
@@ -368,7 +369,17 @@ def take_action(self, parsed_args):
368369

369370
self.log.debug('boot_args: %s', boot_args)
370371
self.log.debug('boot_kwargs: %s', boot_kwargs)
371-
server = compute_client.servers.create(*boot_args, **boot_kwargs)
372+
373+
# Wrap the call to catch exceptions in order to close files
374+
try:
375+
server = compute_client.servers.create(*boot_args, **boot_kwargs)
376+
finally:
377+
# Clean up open files - make sure they are not strings
378+
for f in files:
379+
if hasattr(f, 'close'):
380+
f.close()
381+
if hasattr(userdata, 'close'):
382+
userdata.close()
372383

373384
if parsed_args.wait:
374385
if utils.wait_for_status(

‎openstackclient/tests/compute/v2/fakes.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
SERVER = {
2727
'id': server_id,
2828
'name': server_name,
29+
'metadata': {},
2930
}
3031

3132
extension_name = 'Multinic'

‎openstackclient/tests/compute/v2/test_server.py‎

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,13 @@
1414
#
1515

1616
import copy
17+
import mock
1718

1819
from openstackclient.compute.v2 import server
1920
from openstackclient.tests.compute.v2 import fakes as compute_fakes
2021
from openstackclient.tests import fakes
2122
from openstackclient.tests.image.v2 import fakes as image_fakes
23+
from openstackclient.tests import utils
2224

2325

2426
class TestServer(compute_fakes.TestComputev2):
@@ -30,6 +32,10 @@ def setUp(self):
3032
self.servers_mock = self.app.client_manager.compute.servers
3133
self.servers_mock.reset_mock()
3234

35+
# Get a shortcut to the ImageManager Mock
36+
self.cimages_mock = self.app.client_manager.compute.images
37+
self.cimages_mock.reset_mock()
38+
3339
# Get a shortcut to the FlavorManager Mock
3440
self.flavors_mock = self.app.client_manager.compute.flavors
3541
self.flavors_mock.reset_mock()
@@ -39,6 +45,174 @@ def setUp(self):
3945
self.images_mock.reset_mock()
4046

4147

48+
class TestServerCreate(TestServer):
49+
50+
def setUp(self):
51+
super(TestServerCreate, self).setUp()
52+
53+
self.servers_mock.create.return_value = fakes.FakeResource(
54+
None,
55+
copy.deepcopy(compute_fakes.SERVER),
56+
loaded=True,
57+
)
58+
new_server = fakes.FakeResource(
59+
None,
60+
copy.deepcopy(compute_fakes.SERVER),
61+
loaded=True,
62+
)
63+
new_server.__dict__['networks'] = {}
64+
self.servers_mock.get.return_value = new_server
65+
66+
self.image = fakes.FakeResource(
67+
None,
68+
copy.deepcopy(image_fakes.IMAGE),
69+
loaded=True,
70+
)
71+
self.cimages_mock.get.return_value = self.image
72+
73+
self.flavor = fakes.FakeResource(
74+
None,
75+
copy.deepcopy(compute_fakes.FLAVOR),
76+
loaded=True,
77+
)
78+
self.flavors_mock.get.return_value = self.flavor
79+
80+
# Get the command object to test
81+
self.cmd = server.CreateServer(self.app, None)
82+
83+
def test_server_create_no_options(self):
84+
arglist = [
85+
compute_fakes.server_id,
86+
]
87+
verifylist = [
88+
('server_name', compute_fakes.server_id),
89+
]
90+
try:
91+
# Missing required args should bail here
92+
self.check_parser(self.cmd, arglist, verifylist)
93+
except utils.ParserException:
94+
pass
95+
96+
def test_server_create_minimal(self):
97+
arglist = [
98+
'--image', 'image1',
99+
'--flavor', 'flavor1',
100+
compute_fakes.server_id,
101+
]
102+
verifylist = [
103+
('image', 'image1'),
104+
('flavor', 'flavor1'),
105+
('config_drive', False),
106+
('server_name', compute_fakes.server_id),
107+
]
108+
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
109+
110+
# DisplayCommandBase.take_action() returns two tuples
111+
columns, data = self.cmd.take_action(parsed_args)
112+
113+
# Set expected values
114+
kwargs = dict(
115+
meta=None,
116+
files={},
117+
reservation_id=None,
118+
min_count=1,
119+
max_count=1,
120+
security_groups=[],
121+
userdata=None,
122+
key_name=None,
123+
availability_zone=None,
124+
block_device_mapping={},
125+
nics=[],
126+
scheduler_hints={},
127+
config_drive=None,
128+
)
129+
# ServerManager.create(name, image, flavor, **kwargs)
130+
self.servers_mock.create.assert_called_with(
131+
compute_fakes.server_id,
132+
self.image,
133+
self.flavor,
134+
**kwargs
135+
)
136+
137+
collist = ('addresses', 'flavor', 'id', 'image', 'name', 'properties')
138+
self.assertEqual(columns, collist)
139+
datalist = (
140+
'',
141+
'Large ()',
142+
compute_fakes.server_id,
143+
'graven ()',
144+
compute_fakes.server_name,
145+
'',
146+
)
147+
self.assertEqual(data, datalist)
148+
149+
@mock.patch('openstackclient.compute.v2.server.io.open')
150+
def test_server_create_userdata(self, mock_open):
151+
mock_file = mock.MagicMock(name='File')
152+
mock_open.return_value = mock_file
153+
mock_open.read.return_value = '#!/bin/sh'
154+
155+
arglist = [
156+
'--image', 'image1',
157+
'--flavor', 'flavor1',
158+
'--user-data', 'userdata.sh',
159+
compute_fakes.server_id,
160+
]
161+
verifylist = [
162+
('image', 'image1'),
163+
('flavor', 'flavor1'),
164+
('user_data', 'userdata.sh'),
165+
('config_drive', False),
166+
('server_name', compute_fakes.server_id),
167+
]
168+
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
169+
170+
# DisplayCommandBase.take_action() returns two tuples
171+
columns, data = self.cmd.take_action(parsed_args)
172+
173+
# Ensure the userdata file is opened
174+
mock_open.assert_called_with('userdata.sh')
175+
176+
# Ensure the userdata file is closed
177+
mock_file.close.assert_called()
178+
179+
# Set expected values
180+
kwargs = dict(
181+
meta=None,
182+
files={},
183+
reservation_id=None,
184+
min_count=1,
185+
max_count=1,
186+
security_groups=[],
187+
userdata=mock_file,
188+
key_name=None,
189+
availability_zone=None,
190+
block_device_mapping={},
191+
nics=[],
192+
scheduler_hints={},
193+
config_drive=None,
194+
)
195+
# ServerManager.create(name, image, flavor, **kwargs)
196+
self.servers_mock.create.assert_called_with(
197+
compute_fakes.server_id,
198+
self.image,
199+
self.flavor,
200+
**kwargs
201+
)
202+
203+
collist = ('addresses', 'flavor', 'id', 'image', 'name', 'properties')
204+
self.assertEqual(columns, collist)
205+
datalist = (
206+
'',
207+
'Large ()',
208+
compute_fakes.server_id,
209+
'graven ()',
210+
compute_fakes.server_name,
211+
'',
212+
)
213+
self.assertEqual(data, datalist)
214+
215+
42216
class TestServerDelete(TestServer):
43217

44218
def setUp(self):

‎openstackclient/tests/utils.py‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@
2323
from openstackclient.tests import fakes
2424

2525

26+
class ParserException(Exception):
27+
pass
28+
29+
2630
class TestCase(testtools.TestCase):
2731
def setUp(self):
2832
testtools.TestCase.setUp(self)
@@ -84,7 +88,7 @@ def check_parser(self, cmd, args, verify_args):
8488
try:
8589
parsed_args = cmd_parser.parse_args(args)
8690
except SystemExit:
87-
raise Exception("Argument parse failed")
91+
raise ParserException("Argument parse failed")
8892
for av in verify_args:
8993
attr, value = av
9094
if attr:

0 commit comments

Comments
 (0)