Merge lp:~frankban/juju-quickstart/envs-backup into lp:juju-quickstart

Proposed by Francesco Banconi
Status: Merged
Merged at revision: 45
Proposed branch: lp:~frankban/juju-quickstart/envs-backup
Merge into: lp:juju-quickstart
Diff against target: 244 lines (+114/-9)
6 files modified
quickstart/manage.py (+4/-1)
quickstart/models/envs.py (+18/-4)
quickstart/tests/models/test_envs.py (+45/-0)
quickstart/tests/test_manage.py (+13/-4)
quickstart/tests/test_utils.py (+19/-0)
quickstart/utils.py (+15/-0)
To merge this branch: bzr merge lp:~frankban/juju-quickstart/envs-backup
Reviewer Review Type Date Requested Status
Juju GUI Hackers Pending
Review via email: mp+201449@code.launchpad.net

Description of the change

Create a backup copy of the envs file.

In a pre-imp call with Gary, we decided the
following strategy: a backup copy of the
environments.yaml file is created once per
quickstart session the first time environments
are changed by the user. This way we prevent the
original backup to be overwritten if the user
makes two subsequent changes to the envs.

Tests: `make check`.

QA:

- Create a backup copy of your environments file:
  `cp ~/.juju/environments.yaml ~/environments.yaml.bak`.

- Run `.venv/bin/python juju-quickstart -i`, select one
  of your environments, exit with ^X without changing anything.
  Check your original file has not been changed.

- Run `.venv/bin/python juju-quickstart -i`, change something
  (e.g. create, remove or change an environment, or just change
  the default one). Change at least two environments.
  Exit with ^X. The envs.yaml file now should be the one
  generated by quickstart and should reflect your changes.
  It also includes the path to the backup copy, which
  should be your original file.

- Restore your original environments file:
  `mv ~/environments.yaml.bak ~/.juju/environments.yaml`.

Done, thank you!

https://codereview.appspot.com/51520043/

To post a comment you must log in.
Revision history for this message
Francesco Banconi (frankban) wrote :

Reviewers: mp+201449_code.launchpad.net,

Message:
Please take a look.

Description:
Create a backup copy of the envs file.

In a pre-imp call with Gary, we decided the
following strategy: a backup copy of the
environments.yaml file is created once per
quickstart session the first time environments
are changed by the user. This way we prevent the
original backup to be overwritten if the user
makes two subsequent changes to the envs.

Tests: `make check`.

QA:

- Create a backup copy of your environments file:
   `cp ~/.juju/environments.yaml ~/environments.yaml.bak`.

- Run `.venv/bin/python juju-quickstart -i`, select one
   of your environments, exit with ^X without changing anything.
   Check your original file has not been changed.

- Run `.venv/bin/python juju-quickstart -i`, change something
   (e.g. create, remove or change an environment, or just change
   the default one). Change at least two environments.
   Exit with ^X. The envs.yaml file now should be the one
   generated by quickstart and should reflect your changes.
   It also includes the path to the backup copy, which
   should be your original file.

- Restore your original environments file:
   `mv ~/environments.yaml.bak ~/.juju/environments.yaml`.

Done, thank you!

https://code.launchpad.net/~frankban/juju-quickstart/envs-backup/+merge/201449

(do not edit description out of merge proposal)

Please review this at https://codereview.appspot.com/51520043/

Affected files (+116, -9 lines):
   A [revision details]
   M quickstart/manage.py
   M quickstart/models/envs.py
   M quickstart/tests/models/test_envs.py
   M quickstart/tests/test_manage.py
   M quickstart/tests/test_utils.py
   M quickstart/utils.py

61. By Francesco Banconi

Fix typo.

Revision history for this message
Francesco Banconi (frankban) wrote :

Matthew approved this MP, but he's not currently able to log in to
Rietveld.
Thanks for the review, submitting now.

https://codereview.appspot.com/51520043/

Revision history for this message
Francesco Banconi (frankban) wrote :

*** Submitted:

Create a backup copy of the envs file.

In a pre-imp call with Gary, we decided the
following strategy: a backup copy of the
environments.yaml file is created once per
quickstart session the first time environments
are changed by the user. This way we prevent the
original backup to be overwritten if the user
makes two subsequent changes to the envs.

Tests: `make check`.

QA:

- Create a backup copy of your environments file:
   `cp ~/.juju/environments.yaml ~/environments.yaml.bak`.

- Run `.venv/bin/python juju-quickstart -i`, select one
   of your environments, exit with ^X without changing anything.
   Check your original file has not been changed.

- Run `.venv/bin/python juju-quickstart -i`, change something
   (e.g. create, remove or change an environment, or just change
   the default one). Change at least two environments.
   Exit with ^X. The envs.yaml file now should be the one
   generated by quickstart and should reflect your changes.
   It also includes the path to the backup copy, which
   should be your original file.

- Restore your original environments file:
   `mv ~/environments.yaml.bak ~/.juju/environments.yaml`.

Done, thank you!

R=
CC=
https://codereview.appspot.com/51520043

https://codereview.appspot.com/51520043/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'quickstart/manage.py'
2--- quickstart/manage.py 2014-01-13 15:30:44 +0000
3+++ quickstart/manage.py 2014-01-13 16:51:49 +0000
4@@ -25,6 +25,7 @@
5 import codecs
6 import logging
7 import os
8+import shutil
9 import sys
10 import webbrowser
11
12@@ -155,9 +156,11 @@
13 application with an error if an OSError exception is raised while saving
14 the environments database.
15 """
16+ backup_function = utils.run_once(shutil.copyfile)
17+
18 def save_callable(env_db):
19 try:
20- envs.save(env_file, env_db)
21+ envs.save(env_file, env_db, backup_function=backup_function)
22 except OSError as err:
23 return parser.error(bytes(err))
24
25
26=== modified file 'quickstart/models/envs.py'
27--- quickstart/models/envs.py 2014-01-10 09:08:06 +0000
28+++ quickstart/models/envs.py 2014-01-13 16:51:49 +0000
29@@ -190,24 +190,38 @@
30 return env_db
31
32
33-def save(env_file, env_db):
34+def save(env_file, env_db, backup_function=None):
35 """Save the given env_db to the provided environments.yaml file.
36
37 The new environments are saved to disk in the most atomic way possible.
38+ If backup_function is not None, it is called passing the original
39+ environments file path and the backup destination path. The backup is
40+ obviously performed only if the file exists.
41
42 Raise an OSError if any errors occur in the process.
43 """
44+ dirname = os.path.dirname(env_file)
45+ backup_file = None
46+ if os.path.exists(env_file):
47+ if backup_function is not None:
48+ # Create a backup of the original file.
49+ backup_file = env_file + '.quickstart.bak'
50+ backup_function(env_file, backup_file)
51+ else:
52+ utils.mkdir(dirname)
53+ banner = utils.get_quickstart_banner()
54 # Save the contents in a temporary file, then rename to the real file.
55 # Since the renaming operation may fail on some Unix flavors if the source
56 # and destination files are on different file systems, use for the
57 # temporary file the same directory where the env_file is stored.
58- dirname = os.path.dirname(env_file)
59- utils.mkdir(dirname)
60- banner = utils.get_quickstart_banner()
61 try:
62 temp_file = tempfile.NamedTemporaryFile(
63 prefix='quickstart-temp-envs-', dir=dirname, delete=False)
64 temp_file.write(banner.encode('utf-8'))
65+ if backup_file is not None:
66+ temp_file.write(
67+ '# A backup copy of this file can be found in\n'
68+ '# {}\n\n'.format(backup_file))
69 serializers.yaml_dump(env_db, temp_file)
70 except Exception as err:
71 raise OSError(err)
72
73=== modified file 'quickstart/tests/models/test_envs.py'
74--- quickstart/tests/models/test_envs.py 2014-01-09 12:48:20 +0000
75+++ quickstart/tests/models/test_envs.py 2014-01-13 16:51:49 +0000
76@@ -22,6 +22,8 @@
77 import copy
78 import functools
79 import os
80+import shutil
81+import tempfile
82 import unittest
83
84 import mock
85@@ -199,6 +201,18 @@
86 self.env_file = self.make_env_file()
87 self.original_contents = open(self.env_file).read()
88
89+ def make_juju_home(self):
90+ """Create a temporary Juju home directory.
91+
92+ Return the Juju home path and the path of the corresponding
93+ environments file.
94+ """
95+ playground = tempfile.mkdtemp()
96+ self.addCleanup(shutil.rmtree, playground)
97+ juju_home = os.path.join(playground, '.juju')
98+ os.mkdir(juju_home)
99+ return juju_home, os.path.join(juju_home, 'environments.yaml')
100+
101 def test_dump_error(self):
102 # An OSError is raised if errors occur in the serialization process.
103 with mock.patch('quickstart.serializers.yaml_dump') as mock_yaml_dump:
104@@ -238,6 +252,37 @@
105 # Also the new contents have been saved.
106 self.assertIn('new: contents', contents)
107
108+ def test_backup(self):
109+ # A backup function, if provided, is used to create a backup copy
110+ # of the original environments file.
111+ mock_backup = mock.Mock()
112+ envs.save(
113+ self.env_file, {'new': 'contents'}, backup_function=mock_backup)
114+ contents = open(self.env_file).read()
115+ expected_backup_path = self.env_file + '.quickstart.bak'
116+ # The backup function has been called.
117+ mock_backup.assert_called_once_with(
118+ self.env_file, expected_backup_path)
119+ # The new file indicates where to find the backup copy.
120+ self.assertIn(
121+ '# A backup copy of this file can be found in\n'
122+ '# {}\n'.format(expected_backup_path),
123+ contents)
124+
125+ def test_missing_juju_home(self):
126+ # The environments file directory is created if not existing.
127+ juju_home, env_file = self.make_juju_home()
128+ shutil.rmtree(juju_home)
129+ envs.save(env_file, {'new': 'contents'})
130+ self.assertTrue(os.path.isdir(juju_home))
131+
132+ def test_backup_missing_juju_home(self):
133+ # The backup function is not executed if there is nothing to backup.
134+ juju_home, env_file = self.make_juju_home()
135+ mock_backup = mock.Mock()
136+ envs.save(env_file, {'new': 'contents'}, backup_function=mock_backup)
137+ self.assertFalse(mock_backup.called)
138+
139
140 class EnvDataTestsMixin(object):
141 """Set up an initial environments dictionary."""
142
143=== modified file 'quickstart/tests/test_manage.py'
144--- quickstart/tests/test_manage.py 2014-01-13 15:30:44 +0000
145+++ quickstart/tests/test_manage.py 2014-01-13 16:51:49 +0000
146@@ -278,13 +278,16 @@
147 self.parser = mock.Mock()
148 self.env_file = '/tmp/envfile.yaml'
149 self.env_db = helpers.make_env_db()
150- self.save_callable = manage._create_save_callable(
151- self.parser, self.env_file)
152+ with mock.patch('quickstart.manage.utils.run_once') as mock_run_once:
153+ self.save_callable = manage._create_save_callable(
154+ self.parser, self.env_file)
155+ self.mock_run_once = mock_run_once
156
157 def test_saved(self, mock_save):
158 # The returned function correctly saves the new environments database.
159 self.save_callable(self.env_db)
160- mock_save.assert_called_once_with(self.env_file, self.env_db)
161+ mock_save.assert_called_once_with(
162+ self.env_file, self.env_db, backup_function=self.mock_run_once())
163 self.assertFalse(self.parser.error.called)
164
165 def test_error(self, mock_save):
166@@ -292,9 +295,15 @@
167 # occurs while saving the new environments database.
168 mock_save.side_effect = OSError(b'bad wolf')
169 self.save_callable(self.env_db)
170- mock_save.assert_called_once_with(self.env_file, self.env_db)
171+ mock_save.assert_called_once_with(
172+ self.env_file, self.env_db, backup_function=self.mock_run_once())
173 self.parser.error.assert_called_once_with('bad wolf')
174
175+ def test_backup_function(self, mock_save):
176+ # The backup function is correctly created.
177+ self.save_callable(self.env_db)
178+ self.mock_run_once.assert_called_once_with(shutil.copyfile)
179+
180
181 class TestStartInteractiveSession(
182 helpers.EnvFileTestsMixin, unittest.TestCase):
183
184=== modified file 'quickstart/tests/test_utils.py'
185--- quickstart/tests/test_utils.py 2014-01-09 12:31:30 +0000
186+++ quickstart/tests/test_utils.py 2014-01-13 16:51:49 +0000
187@@ -601,6 +601,25 @@
188 self.assertEqual('zydeco', bsn_series)
189
190
191+class TestRunOnce(unittest.TestCase):
192+
193+ def setUp(self):
194+ self.results = []
195+ self.func = utils.run_once(self.results.append)
196+
197+ def test_runs_once(self):
198+ # The wrapped function runs only the first time it is invoked.
199+ self.func(1)
200+ self.assertEqual([1], self.results)
201+ self.func(2)
202+ self.assertEqual([1], self.results)
203+
204+ def test_wrapped(self):
205+ # The wrapped function looks like the original one.
206+ self.assertEqual('append', self.func.__name__)
207+ self.assertEqual(list.append.__doc__, self.func.__doc__)
208+
209+
210 class TestUnitChanges(unittest.TestCase):
211
212 unit = 'django/42'
213
214=== modified file 'quickstart/utils.py'
215--- quickstart/utils.py 2014-01-09 11:22:54 +0000
216+++ quickstart/utils.py 2014-01-13 16:51:49 +0000
217@@ -24,6 +24,7 @@
218 import collections
219 import datetime
220 import errno
221+import functools
222 import httplib
223 import json
224 import logging
225@@ -326,6 +327,20 @@
226 return parse_status_output(output, ['machines', '0', 'series'])
227
228
229+def run_once(function):
230+ """Return a decorated version of the given function which runs only once.
231+
232+ Subsequent runs are just ignored and return None.
233+ """
234+ @functools.wraps(function)
235+ def decorated(*args, **kwargs):
236+ if not decorated.called:
237+ decorated.called = True
238+ return function(*args, **kwargs)
239+ decorated.called = False
240+ return decorated
241+
242+
243 def unit_changes(unit_name, changeset):
244 """Parse the changeset and return the change related to the given unit.
245

Subscribers

People subscribed via source and target branches