Merge lp:~veebers/juju-ci-tools/initial-persistent-storage-skel into lp:juju-ci-tools

Proposed by Christopher Lee
Status: Merged
Merged at revision: 1935
Proposed branch: lp:~veebers/juju-ci-tools/initial-persistent-storage-skel
Merge into: lp:juju-ci-tools
Diff against target: 394 lines (+385/-0)
2 files modified
assess_persistent_storage.py (+157/-0)
tests/test_assess_persistent_storage.py (+228/-0)
To merge this branch: bzr merge lp:~veebers/juju-ci-tools/initial-persistent-storage-skel
Reviewer Review Type Date Requested Status
Curtis Hovey (community) code Approve
Review via email: mp+319899@code.launchpad.net

Commit message

Initial skeleton for persistent storage testing.

Description of the change

Initial skeleton for persistent storage testing.

First in an upcoming series of branches adding persistent storage testing.
This MP introduces some of the fundamental methods used in testing i.e. asserting token values stored on file.

As this feature hasn't fully landed the test isn't expected to pass.

This work will continue to be done by Burton with some hand-off work in the process (e.g. peer-ing initially then leading to fully handed off).

To post a comment you must log in.
Revision history for this message
Curtis Hovey (sinzui) wrote :

Thank you.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'assess_persistent_storage.py'
2--- assess_persistent_storage.py 1970-01-01 00:00:00 +0000
3+++ assess_persistent_storage.py 2017-03-15 08:41:57 +0000
4@@ -0,0 +1,157 @@
5+#!/usr/bin/env python
6+"""Testing persistent storage function of Juju."""
7+
8+from __future__ import print_function
9+
10+import argparse
11+import yaml
12+import logging
13+from subprocess import CalledProcessError
14+import sys
15+
16+from deploy_stack import (
17+ BootstrapManager,
18+ get_random_string,
19+ )
20+from utility import (
21+ JujuAssertionError,
22+ add_basic_testing_arguments,
23+ configure_logging,
24+ )
25+from jujucharm import local_charm_path
26+
27+__metaclass__ = type
28+
29+
30+log = logging.getLogger("assess_persistent_storage")
31+
32+
33+def assess_persistent_storage(client):
34+ ensure_storage_remains_after_application_removal()
35+
36+
37+def ensure_storage_remains_after_application_removal(client):
38+ """Storage created during a deploy must persist after application removal.
39+
40+ Steps taken to test:
41+ - Deploy the dummy storage charm
42+ - Set config and ensure data is stored on storage.
43+ - Remove application, taking note of the remaining storage name
44+ - Re-deploy new charm using persisted storage
45+ - Ensure data has remained.
46+
47+ :param client: ModelClient object to deploy the charm on.
48+ """
49+ random_token = get_random_string()
50+ expected_token_values = {'single-fs-token': random_token}
51+ charm_path = local_charm_path(
52+ charm='dummy-storage', juju_ver=client.version)
53+ client.deploy(charm_path, storage='single-fs=rootfs')
54+ client.wait_for_started()
55+ client.set_config('dummy-storage', {'single-fs-token': random_token})
56+ client.wait_for_workloads()
57+
58+ assert_storage_is_intact(client, expected_results=expected_token_values)
59+
60+ try:
61+ single_filesystem_name = get_storage_filesystems(
62+ client, 'single-fs')[0]
63+ except IndexError:
64+ raise JujuAssertionError('Storage was not found.')
65+
66+ client.remove_service('dummy-storage')
67+
68+ # Wait for application to be removed then re-deploy with existing storage.
69+
70+ storage_command = '--attach-storage single-fs={}'.format(
71+ single_filesystem_name)
72+ client.deploy(charm_path, alias=storage_command)
73+ client.wait_for_started()
74+ client.wait_for_workloads()
75+
76+ assert_storage_is_intact(client, expected_token_values)
77+
78+
79+def get_storage_filesystems(client, storage_name):
80+ """Return storage unit names for a named storage.
81+
82+ :param client: ModelClient object to query.
83+ :param storage_name: Name of storage unit to get filesystem names for.
84+ :return: List of filesystem names
85+ """
86+ all_storage = yaml.safe_load(client.list_storage())['filesystems']
87+ return [
88+ details['storage'] for unit, details in all_storage.items()
89+ if details['storage'].startswith(storage_name)]
90+
91+
92+def assert_storage_is_intact(client, expected_results):
93+ """Ensure stored tokens match the expected values in `expected_results`.
94+
95+ Checks the token values stored on the storage assigned to the deployed
96+ dummy-storage charm.
97+
98+ Only matches the provided expected token values and ignores any that have
99+ no values provided for them.
100+
101+ :param client: ModelClient object where dummy-storage application is
102+ deployed
103+ :param expected_results: Dict containing 'token name' -> 'expected value'
104+ look up values.
105+ :raises JujuAssertionError: If expected token values are not present in the
106+ stored token details.
107+ """
108+ stored_content = get_stored_token_content(client)
109+
110+ for expected_name, expected_value in expected_results.iteritems():
111+ try:
112+ stored_value = stored_content[expected_name]
113+ except KeyError:
114+ raise JujuAssertionError(
115+ 'Expected token "{}" not found in stored results.'.format(
116+ expected_name))
117+ if stored_value != expected_value:
118+ raise JujuAssertionError(
119+ 'Token values do not match. Expected: {} got: {}'.format(
120+ expected_value, stored_value))
121+
122+
123+def get_stored_token_content(client):
124+ """Retrieve token values from file stored on the unit.
125+
126+ Token values are retrieved from the storage units and logged into a file on
127+ the application unit.
128+
129+ :param client: ModelClient object to query.
130+ :return: Dict containing 'token name' -> 'token value'.
131+ """
132+ try:
133+ # TODO: Need to pass in application/unit
134+ contents = client.get_juju_output(
135+ 'ssh', ('dummy-storage/0', 'cat', '/tmp/status'))
136+ except CalledProcessError as e:
137+ raise JujuAssertionError('Failed to read token file: {}'.format(e))
138+
139+ return {token_name: token_value for token_name, token_value in (
140+ line.split(':', 1) for line in contents.splitlines() if line != '')}
141+
142+
143+def parse_args(argv):
144+ """Parse all arguments."""
145+ parser = argparse.ArgumentParser(
146+ description="Test for Persistent Storage feature.")
147+ add_basic_testing_arguments(parser)
148+ return parser.parse_args(argv)
149+
150+
151+def main(argv=None):
152+ args = parse_args(argv)
153+ configure_logging(args.verbose)
154+ bs_manager = BootstrapManager.from_args(args)
155+ with bs_manager.booted_context(args.upload_tools):
156+ assess_persistent_storage(bs_manager.client)
157+ return 0
158+
159+
160+if __name__ == '__main__':
161+ sys.exit(main())
162
163=== added file 'tests/test_assess_persistent_storage.py'
164--- tests/test_assess_persistent_storage.py 1970-01-01 00:00:00 +0000
165+++ tests/test_assess_persistent_storage.py 2017-03-15 08:41:57 +0000
166@@ -0,0 +1,228 @@
167+"""Tests for assess_persistent_storage module."""
168+
169+import StringIO
170+from subprocess import CalledProcessError
171+from textwrap import dedent
172+
173+from mock import (
174+ Mock,
175+ patch,
176+ )
177+
178+import assess_persistent_storage as aps
179+from tests import (
180+ parse_error,
181+ TestCase,
182+ )
183+from utility import JujuAssertionError
184+
185+
186+class TestParseArgs(TestCase):
187+
188+ def test_common_args(self):
189+ args = aps.parse_args(
190+ ["an-env", "/bin/juju", "/tmp/logs", "an-env-mod"])
191+ self.assertEqual("an-env", args.env)
192+ self.assertEqual("/bin/juju", args.juju_bin)
193+ self.assertEqual("/tmp/logs", args.logs)
194+ self.assertEqual("an-env-mod", args.temp_env_name)
195+ self.assertEqual(False, args.debug)
196+
197+ def test_help(self):
198+ fake_stdout = StringIO.StringIO()
199+ with parse_error(self) as fake_stderr:
200+ with patch("sys.stdout", fake_stdout):
201+ aps.parse_args(["--help"])
202+ self.assertEqual("", fake_stderr.getvalue())
203+ self.assertNotIn("TODO", fake_stdout.getvalue())
204+
205+
206+class TestGetStorageSystems(TestCase):
207+ def test_returns_single_known_filesystem(self):
208+ storage_json = dedent("""\
209+ filesystems:
210+ 0/0:
211+ provider-id: 0/1
212+ storage: single-fs/1
213+ attachments:
214+ machines:
215+ "0":
216+ mount-point: /srv/single-fs
217+ read-only: false
218+ life: alive
219+ units:
220+ dummy-storage/0:
221+ machine: "0"
222+ location: /srv/single-fs
223+ life: alive
224+ pool: rootfs
225+ size: 28775
226+ life: alive
227+ status:
228+ current: attached
229+ since: 14 Mar 2017 17:01:15+13:00
230+ """)
231+ client = Mock()
232+ client.list_storage.return_value = storage_json
233+
234+ self.assertEqual(
235+ aps.get_storage_filesystems(client, 'single-fs'),
236+ ['single-fs/1'])
237+
238+ def test_returns_empty_list_when_none_found(self):
239+ storage_json = dedent("""\
240+ filesystems:
241+ 0/0:
242+ provider-id: 0/1
243+ storage: single-fs/1
244+ attachments:
245+ machines:
246+ "0":
247+ mount-point: /srv/single-fs
248+ read-only: false
249+ life: alive
250+ units:
251+ dummy-storage/0:
252+ machine: "0"
253+ location: /srv/single-fs
254+ life: alive
255+ pool: rootfs
256+ size: 28775
257+ life: alive
258+ status:
259+ current: attached
260+ since: 14 Mar 2017 17:01:15+13:00
261+ """)
262+ client = Mock()
263+ client.list_storage.return_value = storage_json
264+
265+ self.assertEqual(
266+ aps.get_storage_filesystems(client, 'not-found'),
267+ [])
268+
269+ def test_returns_many_for_multiple_finds(self):
270+ storage_json = dedent("""\
271+ filesystems:
272+ "0/0":
273+ provider-id: 0/1
274+ storage: multi-fs/1
275+ attachments:
276+ machines:
277+ "0":
278+ mount-point: /srv/multi-fs
279+ read-only: false
280+ life: alive
281+ units:
282+ dummy-storage/0:
283+ machine: "0"
284+ location: /srv/multi-fs
285+ life: alive
286+ pool: rootfs
287+ size: 28775
288+ life: alive
289+ status:
290+ current: attached
291+ since: 14 Mar 2017 17:01:15+13:00
292+ 0/1:
293+ provider-id: 0/2
294+ storage: multi-fs/2
295+ attachments:
296+ machines:
297+ "0":
298+ mount-point: /srv/multi-fs
299+ read-only: false
300+ life: alive
301+ units:
302+ dummy-storage/0:
303+ machine: "0"
304+ location: /srv/multi-fs
305+ life: alive
306+ pool: rootfs
307+ size: 28775
308+ life: alive
309+ status:
310+ current: attached
311+ since: 14 Mar 2017 17:01:15+13:00
312+ """)
313+ client = Mock()
314+ client.list_storage.return_value = storage_json
315+
316+ self.assertEqual(
317+ aps.get_storage_filesystems(client, 'multi-fs'),
318+ ['multi-fs/1', 'multi-fs/2'])
319+
320+
321+class TestAssertStorageIsIntact(TestCase):
322+
323+ def test_passes_when_token_values_match(self):
324+ client = Mock()
325+ stored_values = {'single-fs-token': 'abc123'}
326+ expected_results = {'single-fs-token': 'abc123'}
327+ with patch.object(
328+ aps, 'get_stored_token_content',
329+ autospec=True,
330+ return_value=stored_values) as m_gstc:
331+ aps.assert_storage_is_intact(client, expected_results)
332+ m_gstc.assert_called_once_with(client)
333+
334+ def test_ignores_token_values_not_supplied(self):
335+ client = Mock()
336+ stored_values = {
337+ 'single-fs-token': 'abc123',
338+ 'multi-fs-token/1': '00000'
339+ }
340+ expected_results = {'single-fs-token': 'abc123'}
341+ with patch.object(
342+ aps, 'get_stored_token_content',
343+ autospec=True,
344+ return_value=stored_values) as m_gstc:
345+ aps.assert_storage_is_intact(client, expected_results)
346+ m_gstc.assert_called_once_with(client)
347+
348+ def test_raises_when_token_values_do_not_match(self):
349+ client = Mock()
350+ stored_values = {'single-fs-token': 'abc123x'}
351+ expected_results = {'single-fs-token': 'abc123'}
352+ with patch.object(
353+ aps, 'get_stored_token_content',
354+ autospec=True,
355+ return_value=stored_values):
356+ with self.assertRaises(JujuAssertionError):
357+ aps.assert_storage_is_intact(client, expected_results)
358+
359+
360+class TestGetStoredTokenContent(TestCase):
361+
362+ def test_raises_if_token_file_not_present(self):
363+ client = Mock()
364+ client.get_juju_output.side_effect = CalledProcessError(-1, None, '')
365+ with self.assertRaises(JujuAssertionError):
366+ aps.get_stored_token_content(client)
367+
368+ def test_returns_dict_containing_all_values_when_single_value(self):
369+ token_file_contents = dedent("""\
370+
371+ single-fs-token:Blocked: not set
372+ """)
373+ client = Mock()
374+ client.get_juju_output.return_value = token_file_contents
375+
376+ self.assertEqual(
377+ aps.get_stored_token_content(client),
378+ {'single-fs-token': 'Blocked: not set'})
379+
380+ def test_returns_dict_containing_all_values_when_many_values(self):
381+ token_file_contents = dedent("""\
382+
383+ single-fs-token:Blocked: not set
384+ multi-fs-token/2:abc123
385+ """)
386+ client = Mock()
387+ client.get_juju_output.return_value = token_file_contents
388+
389+ self.assertEqual(
390+ aps.get_stored_token_content(client),
391+ {
392+ 'single-fs-token': 'Blocked: not set',
393+ 'multi-fs-token/2': 'abc123'
394+ })

Subscribers

People subscribed via source and target branches