Merge lp:~benji/charmworld/remove-charm into lp:charmworld
- remove-charm
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Benji York |
Approved revision: | 468 |
Merged at revision: | 465 |
Proposed branch: | lp:~benji/charmworld/remove-charm |
Merge into: | lp:charmworld |
Diff against target: |
284 lines (+216/-1) 6 files modified
charmworld/models.py (+13/-0) charmworld/scripts/remove_charm.py (+71/-0) charmworld/scripts/tests/test_remove_charm.py (+119/-0) charmworld/search.py (+1/-1) charmworld/testing/factory.py (+11/-0) setup.py (+1/-0) |
To merge this branch: | bzr merge lp:~benji/charmworld/remove-charm |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Juju Gui Bot | continuous-integration | Approve | |
Benji York (community) | Approve | ||
Review via email: mp+195839@code.launchpad.net |
Commit message
Description of the change
Add a script to remove a charm
Benji York (benji) wrote : | # |
Brad Crittenden (bac) wrote : | # |
LGTM. Cannot do QA at this time.
https:/
File charmworld/
https:/
charmworld/
status is set to indicate the error condition.
positive, non-zero
https:/
charmworld/
for sucessfully executing programs.
I love the editorial content.
https:/
File charmworld/
https:/
charmworld/
It is very odd that this caused the test failure you saw. The whole
point is a search with one of these characters would cause an exception.
I wonder if you have a different version of ES installed that handles
it differently.
I'm confused why this doesn't cause the test you had trouble with
earlier to *always* fail.
Richard Harding (rharding) wrote : | # |
Search QA is ok. Search change just made it so that ~charmers works vs
produces an unparseable error. The ~ is dropped from the actual search
and it goes on with just 'charmers'.
Benji York (benji) wrote : | # |
Thanks for the review and QA guys!
https:/
File charmworld/
https:/
charmworld/
status is set to indicate the error condition.
On 2013/11/19 19:05:48, bac wrote:
> positive, non-zero
Since POSIX requires it to be an "unsigned decimal integer", all
non-zero values are positive. ;)
- 468. By Benji York
-
remove silly, ineffective argument
Juju Gui Bot (juju-gui-bot) : | # |
Preview Diff
1 | === modified file 'charmworld/models.py' |
2 | --- charmworld/models.py 2013-11-13 19:54:55 +0000 |
3 | +++ charmworld/models.py 2013-11-19 20:26:17 +0000 |
4 | @@ -228,6 +228,7 @@ |
5 | |
6 | |
7 | def construct_charm_id(charm_data, use_revision=True): |
8 | + """Build the ID of a charm from a mapping of that charm's attributes.""" |
9 | elements = [charm_data['owner'], charm_data['series'], charm_data['name']] |
10 | if use_revision: |
11 | elements.append('%d' % charm_data['store_data']['revision']) |
12 | @@ -1288,6 +1289,18 @@ |
13 | def create_collection(self): |
14 | self.collection.database.create_collection(self.collection.name) |
15 | |
16 | + @staticmethod |
17 | + def make_all_revisions_query(charm_data): |
18 | + return { |
19 | + 'owner': charm_data['owner'], |
20 | + 'series': charm_data['series'], |
21 | + 'name': charm_data['name'], |
22 | + } |
23 | + |
24 | + def find_all_revisions(self, charm_data): |
25 | + query = self.make_all_revisions_query(charm_data) |
26 | + return list(self.collection.find(query)) |
27 | + |
28 | |
29 | def get_basket_info(revno, **data): |
30 | owner_segment = '~%s' % data['owner'] |
31 | |
32 | === added file 'charmworld/scripts/remove_charm.py' |
33 | --- charmworld/scripts/remove_charm.py 1970-01-01 00:00:00 +0000 |
34 | +++ charmworld/scripts/remove_charm.py 2013-11-19 20:26:17 +0000 |
35 | @@ -0,0 +1,71 @@ |
36 | +# Copyright 2012, 2013 Canonical Ltd. This software is licensed under the |
37 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
38 | + |
39 | +from __future__ import print_function |
40 | + |
41 | +__metaclass__ = type |
42 | + |
43 | +import sys |
44 | +from pyelasticsearch.exceptions import ElasticHttpNotFoundError |
45 | +from charmworld.utils import get_ini |
46 | +from charmworld.models import ( |
47 | + CharmSource, |
48 | + FeaturedSource, |
49 | + getconnection, |
50 | + getdb, |
51 | +) |
52 | +from charmworld.search import ElasticSearchClient |
53 | + |
54 | + |
55 | +CHARM_ID_MESSAGE = ( |
56 | + 'Please provide a charm ID, including revision. E.g., ' |
57 | + '~charmers/precise/apache2/14') |
58 | + |
59 | + |
60 | +def main(argv, stderr=sys.stderr, db=None, index_client=None): |
61 | + """The main function for the server start timestamp removal script.""" |
62 | + if len(argv) != 2: |
63 | + print( |
64 | + 'Please provide the ID of the charm you wish to remove.', |
65 | + file=stderr) |
66 | + return 1 |
67 | + |
68 | + charm_id = argv[1] |
69 | + |
70 | + if db is None: |
71 | + settings = get_ini() |
72 | + connection = getconnection(settings) |
73 | + db = getdb(connection, settings.get('mongo.database')) |
74 | + |
75 | + # If the specified charm does not exist, tell the user. |
76 | + charm_query = {'_id': charm_id} |
77 | + charm_data = db.charms.find_one(charm_query) |
78 | + if charm_data is None: |
79 | + print('Unknown charm ID: %r' % charm_id, file=stderr) |
80 | + print(CHARM_ID_MESSAGE, file=stderr) |
81 | + return 1 |
82 | + |
83 | + if index_client is None: |
84 | + index_client = ElasticSearchClient.from_settings(settings) |
85 | + |
86 | + # The charm needs to be removed from the search engine. |
87 | + try: |
88 | + index_client.delete_charm(charm_data) |
89 | + except ElasticHttpNotFoundError: |
90 | + # It is fine if the charm was not indexed. |
91 | + pass |
92 | + |
93 | + # If the charm is featured, we want to unfeature it. |
94 | + source = FeaturedSource.from_db(db) |
95 | + source.clear_featured(charm_data, 'charm') |
96 | + |
97 | + # Finally, remove all the revisions of the charm document from the |
98 | + # database. |
99 | + charm_query = CharmSource.make_all_revisions_query(charm_data) |
100 | + db.charms.remove(charm_query) |
101 | + return 0 |
102 | + |
103 | + |
104 | +def entry_point(): |
105 | + """Script entry point for removing a server's start up timestamp.""" |
106 | + sys.exit(main(sys.argv)) |
107 | |
108 | === added file 'charmworld/scripts/tests/test_remove_charm.py' |
109 | --- charmworld/scripts/tests/test_remove_charm.py 1970-01-01 00:00:00 +0000 |
110 | +++ charmworld/scripts/tests/test_remove_charm.py 2013-11-19 20:26:17 +0000 |
111 | @@ -0,0 +1,119 @@ |
112 | +# Copyright 2012, 2013, Canonical Ltd. This software is licensed under the |
113 | +# GNU Affero General Public License version 3 (see the file LICENSE.txt). |
114 | + |
115 | +from cStringIO import StringIO |
116 | +import os |
117 | + |
118 | +from charmworld.models import ( |
119 | + Charm, |
120 | + CharmSource, |
121 | + construct_charm_id, |
122 | + FeaturedSource, |
123 | +) |
124 | +from charmworld.scripts import remove_charm |
125 | +from charmworld.testing import MongoTestBase |
126 | +from charmworld.testing.factory import make_charm |
127 | +from charmworld.utils import project_root |
128 | + |
129 | + |
130 | +class RemoveCharmScriptTests(MongoTestBase): |
131 | + |
132 | + def setUp(self): |
133 | + super(RemoveCharmScriptTests, self).setUp() |
134 | + # This is a lot of work just to create a new revision of a charm. |
135 | + older_charm = make_charm(self.db) |
136 | + newer_charm_data = older_charm._representation |
137 | + newer_charm_data['store_data']['revision'] += 1 |
138 | + newer_charm_data['_id'] = construct_charm_id(newer_charm_data) |
139 | + self.db.charms.insert(newer_charm_data) |
140 | + self.charm = Charm(newer_charm_data) |
141 | + # As a convienience we will create a stderr stand-in here. |
142 | + self.stderr = StringIO() |
143 | + # Be paranoid about the charms actually existing in the database. |
144 | + assert self.db.charms.find_one({'_id': older_charm._id}) |
145 | + assert self.db.charms.find_one({'_id': self.charm._id}) |
146 | + |
147 | + def is_indexed(self, id_): |
148 | + return self.index_client.get(id_) is not None |
149 | + |
150 | + def test_entry_point_exists(self): |
151 | + # The entry point is wrapped in a script by setuptools. We want to be |
152 | + # sure the entrypoint actually exists. |
153 | + self.assertIn( |
154 | + 'remove-charm = charmworld.scripts.remove_charm:entry_point\n', |
155 | + open(os.path.join(project_root, 'setup.py')).read()) |
156 | + |
157 | + def test_script_chides_you_for_not_providing_an_id(self): |
158 | + # If no charm ID is provided on the command line the script prints a |
159 | + # helpful message. |
160 | + exit_status = remove_charm.main([], self.stderr) |
161 | + self.assertEqual( |
162 | + self.stderr.getvalue(), |
163 | + 'Please provide the ID of the charm you wish to remove.\n') |
164 | + # A non-zero exit status is set to indicate the error condition. |
165 | + self.assertGreater(exit_status, 0) |
166 | + |
167 | + def test_script_reports_unkown_charm_ids(self): |
168 | + # The script will generate an error message if the given charm ID does |
169 | + # not exist in the DB. |
170 | + exit_status = remove_charm.main(['', 'not a charm ID'], self.stderr) |
171 | + self.assertIn( |
172 | + "Unknown charm ID: 'not a charm ID'\n", |
173 | + self.stderr.getvalue()) |
174 | + # The user is also given a hint as to how the ID is structured. |
175 | + self.assertIn(remove_charm.CHARM_ID_MESSAGE, self.stderr.getvalue()) |
176 | + # A non-zero exit status is set to indicate the error condition. |
177 | + self.assertGreater(exit_status, 0) |
178 | + |
179 | + def test_script_removes_charm_from_mongodb(self): |
180 | + # The script removes the charm with the provided ID from the database. |
181 | + # ...we can invoke the script to delete it. |
182 | + exit_status = remove_charm.main(['', self.charm._id], self.stderr) |
183 | + # Lo and behold, the charm is actually gone. |
184 | + self.assertFalse(self.db.charms.find_one({'_id': self.charm._id})) |
185 | + # Silence is a virtue for sucessfully executing programs. |
186 | + self.assertEqual(self.stderr.getvalue(), '') |
187 | + # The exit status signals success. |
188 | + self.assertEqual(exit_status, 0) |
189 | + |
190 | + def test_script_removes_charm_from_featuerd(self): |
191 | + # If a charm is featured, its featured status is revoked by the script. |
192 | + source = FeaturedSource.from_db(self.db) |
193 | + charm_data = self.charm._representation |
194 | + source.set_featured(charm_data, 'charm') |
195 | + exit_status = remove_charm.main(['', self.charm._id], self.stderr) |
196 | + self.assertFalse(source.is_featured(charm_data, 'charm')) |
197 | + # The exit status signals success. |
198 | + self.assertEqual(exit_status, 0) |
199 | + |
200 | + def test_script_removes_charm_from_elastic_search(self): |
201 | + # The charm needs to be indexed so the script can remove it. |
202 | + self.use_index_client() |
203 | + charm_data = self.charm._representation |
204 | + self.index_client.index_charm(charm_data) |
205 | + # Ensure the charm actually got indexed. |
206 | + assert self.is_indexed(charm_data['_id']) |
207 | + # Removing a charm should also remove it from the search engine. |
208 | + exit_status = remove_charm.main( |
209 | + ['', self.charm._id], self.stderr, index_client=self.index_client) |
210 | + # The script removed the charm from the index. |
211 | + self.assertFalse(self.is_indexed(charm_data['_id'])) |
212 | + # The exit status signals success. |
213 | + self.assertEqual(exit_status, 0) |
214 | + |
215 | + def test_old_revisions_of_the_charm_are_removed_too(self): |
216 | + # Requesting the removal of a charm results in all revisions of that |
217 | + # charm being removed. |
218 | + charm_data = self.charm._representation |
219 | + # This can't be the best way to find all the revisions of a particular |
220 | + # charm. |
221 | + source = CharmSource(self.db, index_client='fake') |
222 | + # There should exist more than one revision of the charm in the |
223 | + # database. |
224 | + assert len(source.find_all_revisions(charm_data)) > 1 |
225 | + # Run the script. |
226 | + exit_status = remove_charm.main(['', self.charm._id], self.stderr) |
227 | + # The script removed all the charm revisions from the database. |
228 | + self.assertEqual(len(source.find_all_revisions(charm_data)), 0) |
229 | + # The exit status signals success. |
230 | + self.assertEqual(exit_status, 0) |
231 | |
232 | === modified file 'charmworld/search.py' |
233 | --- charmworld/search.py 2013-11-15 20:00:35 +0000 |
234 | +++ charmworld/search.py 2013-11-19 20:26:17 +0000 |
235 | @@ -99,7 +99,7 @@ |
236 | e) |
237 | |
238 | |
239 | -_BAD_CHARS = u'()/[]^{}' |
240 | +_BAD_CHARS = u'~()/[]^{}' |
241 | _BAD_CHAR_TABLE = [ |
242 | unichr(c) if unichr(c) not in _BAD_CHARS else u' ' for c in range(128)] |
243 | _BAD_CHAR_TABLE = u''.join(_BAD_CHAR_TABLE) |
244 | |
245 | === modified file 'charmworld/testing/factory.py' |
246 | --- charmworld/testing/factory.py 2013-11-08 14:59:51 +0000 |
247 | +++ charmworld/testing/factory.py 2013-11-19 20:26:17 +0000 |
248 | @@ -30,6 +30,7 @@ |
249 | ) |
250 | from charmworld.models import ( |
251 | Bundle, |
252 | + Charm, |
253 | CharmFileSet, |
254 | construct_charm_id, |
255 | getfs, |
256 | @@ -317,6 +318,16 @@ |
257 | return record_id, charm |
258 | |
259 | |
260 | +def make_charm(db, **kwargs): |
261 | + """Create a new charm and add it to the database. |
262 | + |
263 | + This function returns a charm model. |
264 | + """ |
265 | + charm_data = get_charm_json(**kwargs) |
266 | + db.charms.insert(charm_data) |
267 | + return Charm(charm_data) |
268 | + |
269 | + |
270 | def get_bundle_data(name=None, owner=None, basket_with_rev=None, |
271 | series='precise', title='', description='', |
272 | services=dict(), relations=dict(), promulgated=False, |
273 | |
274 | === modified file 'setup.py' |
275 | --- setup.py 2013-11-15 16:58:32 +0000 |
276 | +++ setup.py 2013-11-19 20:26:17 +0000 |
277 | @@ -41,6 +41,7 @@ |
278 | sync-index = charmworld.models:sync_index |
279 | remove-start-time-entry = \ |
280 | charmworld.scripts.remove_server_start_time:entry_point |
281 | + remove-charm = charmworld.scripts.remove_charm:entry_point |
282 | [beaker.backends] |
283 | mongodb = mongodb_beaker:MongoDBNamespaceManager |
284 | """, |
Reviewers: mp+195839_ code.launchpad. net,
Message:
Please take a look.
Description:
Add a script to remove a charm
https:/ /code.launchpad .net/~benji/ charmworld/ remove- charm/+ merge/195839
(do not edit description out of merge proposal)
Please review this at https:/ /codereview. appspot. com/29210043/
Affected files (+218, -1 lines): models. py scripts/ remove_ charm.py scripts/ tests/test_ remove_ charm.py search. py testing/ factory. py
A [revision details]
M charmworld/
A charmworld/
A charmworld/
M charmworld/
M charmworld/
M setup.py