Merge lp:~gandelman-a/ubuntu/precise/python-cinderclient/trunk into lp:~ubuntu-cloud-archive/ubuntu/precise/python-cinderclient/trunk

Proposed by Adam Gandelman
Status: Approved
Approved by: Chuck Short
Approved revision: 7
Proposed branch: lp:~gandelman-a/ubuntu/precise/python-cinderclient/trunk
Merge into: lp:~ubuntu-cloud-archive/ubuntu/precise/python-cinderclient/trunk
Diff against target: 3386 lines (+1838/-424)
41 files modified
.gitignore (+0/-13)
.gitreview (+0/-4)
AUTHORS (+7/-0)
ChangeLog (+465/-0)
MANIFEST.in (+2/-7)
PKG-INFO (+2/-1)
cinderclient/__init__.py (+36/-0)
cinderclient/client.py (+111/-62)
cinderclient/exceptions.py (+11/-8)
cinderclient/openstack/common/setup.py (+84/-46)
cinderclient/openstack/common/version.py (+148/-0)
cinderclient/shell.py (+22/-8)
cinderclient/utils.py (+14/-6)
cinderclient/v1/client.py (+8/-5)
cinderclient/v1/contrib/list_extensions.py (+47/-0)
cinderclient/v1/shell.py (+131/-10)
cinderclient/v1/volume_snapshots.py (+19/-0)
cinderclient/v1/volume_types.py (+46/-1)
cinderclient/v1/volumes.py (+51/-46)
cinderclient/versioninfo (+1/-1)
debian/changelog (+22/-0)
debian/control (+6/-2)
debian/copyright (+0/-5)
openstack-common.conf (+7/-0)
python_cinderclient.egg-info/PKG-INFO (+2/-1)
python_cinderclient.egg-info/SOURCES.txt (+6/-2)
python_cinderclient.egg-info/requires.txt (+1/-1)
setup.py (+4/-1)
tests/test_client.py (+0/-3)
tests/test_http.py (+135/-17)
tests/test_shell.py (+29/-36)
tests/test_utils.py (+1/-0)
tests/utils.py (+33/-5)
tests/v1/contrib/test_list_extensions.py (+21/-0)
tests/v1/fakes.py (+157/-42)
tests/v1/test_auth.py (+83/-75)
tests/v1/test_shell.py (+74/-9)
tests/v1/test_types.py (+35/-0)
tests/v1/test_volumes.py (+11/-2)
tools/pip-requires (+1/-1)
tools/test-requires (+5/-4)
To merge this branch: bzr merge lp:~gandelman-a/ubuntu/precise/python-cinderclient/trunk
Reviewer Review Type Date Requested Status
Ubuntu Cloud Archive Team Pending
Review via email: mp+142798@code.launchpad.net
To post a comment you must log in.

Unmerged revisions

7. By Adam Gandelman

* New upstream release for the Ubuntu Cloud Archive.
* New upstream release.
* New upstream release.
* debian/control: Add python-requests as a build dep
* debian/control: Add python-testtools and python-fixtures as a build dep.
* debian/control: add python-setuptools-git dependency
* debian/copyright: Removed remaining dh boilerplate template.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== removed file '.gitignore'
2--- .gitignore 2012-08-16 12:03:50 +0000
3+++ .gitignore 1970-01-01 00:00:00 +0000
4@@ -1,13 +0,0 @@
5-.coverage
6-.venv
7-*,cover
8-cover
9-*.pyc
10-.idea
11-*.swp
12-*~
13-AUTHORS
14-build
15-dist
16-.*egg-info
17-cinderclient/versioninfo
18
19=== removed file '.gitreview'
20--- .gitreview 2012-08-16 12:03:50 +0000
21+++ .gitreview 1970-01-01 00:00:00 +0000
22@@ -1,4 +0,0 @@
23-[gerrit]
24-host=review.openstack.org
25-port=29418
26-project=openstack/python-cinderclient.git
27
28=== modified file 'AUTHORS'
29--- AUTHORS 2012-10-01 12:05:26 +0000
30+++ AUTHORS 2013-01-10 22:15:27 +0000
31@@ -1,16 +1,23 @@
32 Adam Gandelman <adam.gandelman@canonical.com>
33+Alessandro Pilotti <ap@pilotti.it>
34 Anthony Young <sleepsonthefloor@gmail.com>
35 Avishay Traeger <avishay@il.ibm.com>
36 Bhuvan Arumugam <bhuvan@apache.org>
37 Chmouel Boudjnah <chmouel.boudjnah@rackspace.co.uk>
38+Christian Berendt <berendt@b1-systems.de>
39 Chuck Short <chuck.short@canonical.com>
40 Clark Boylan <clark.boylan@gmail.com>
41 Clay Gerrard <clay.gerrard@gmail.com>
42+Cory Stone <corystone@gmail.com>
43 Dean Troyer <dtroyer@gmail.com>
44+Doug Hellmann <doug.hellmann@dreamhost.com>
45+Eric Harney <eharney@redhat.com>
46 jakedahn <jake@ansolabs.com>
47+john-griffith <john.griffith@solidfire.com>
48 John Griffith <john.griffith@solidfire.com>
49 Josh Durgin <josh.durgin@inktank.com>
50 lrqrun <lrqrun@gmail.com>
51+Mike Perez <thingee@gmail.com>
52 Monty Taylor <mordred@inaugust.com>
53 Rongze Zhu <zrzhit@gmail.com>
54 Vincent Hou <sbhou@cn.ibm.com>
55
56=== modified file 'ChangeLog'
57--- ChangeLog 2012-10-01 12:05:26 +0000
58+++ ChangeLog 2013-01-10 22:15:27 +0000
59@@ -1,3 +1,468 @@
60+commit 61e2a4237f0d7a4c5c9591ac364a01c844309757
61+Merge: 06acb0c 765cb3d
62+Author: Jenkins <jenkins@review.openstack.org>
63+Date: Wed Jan 9 07:57:09 2013 +0000
64+
65+ Merge "Add access to update volume metadata."
66+
67+commit 765cb3ddbea2f0e770a2ad911b1ba17760b50818
68+Author: John Griffith <john.griffith@solidfire.com>
69+Date: Sat Jan 5 01:12:59 2013 +0000
70+
71+ Add access to update volume metadata.
72+
73+ This implements the cinderclient side of
74+ blueprint: update-vol-metadata
75+
76+ Change-Id: I97c120d69b6d30843009f6a297bb51299799f4ec
77+
78+ cinderclient/v1/shell.py | 25 +++++++++++++++++++++++++
79+ cinderclient/v1/volumes.py | 30 ++++++++++++++++++++++++++++++
80+ tests/v1/fakes.py | 15 +++++++++++++++
81+ tests/v1/test_shell.py | 15 +++++++++++++++
82+ tests/v1/test_volumes.py | 13 +++++++++++--
83+ 5 files changed, 96 insertions(+), 2 deletions(-)
84+
85+commit 06acb0c009ac53777884cf5b0aa8942184d06622
86+Author: Monty Taylor <mordred@inaugust.com>
87+Date: Mon Dec 24 21:49:34 2012 -0600
88+
89+ Move from unittest2 to testtools
90+
91+ Part of blueprint grizzly-testtools
92+
93+ Change-Id: I13e068ca156f12114eaa3a65bdb557e4eb2c988d
94+
95+ tests/test_client.py | 3 ---
96+ tests/test_shell.py | 43 ++++++++++++++++++++++++-------------------
97+ tests/test_utils.py | 1 +
98+ tests/utils.py | 4 ++--
99+ tests/v1/test_shell.py | 24 +++++++++++++++---------
100+ tools/test-requires | 7 ++++---
101+ 6 files changed, 46 insertions(+), 36 deletions(-)
102+
103+commit bf7b86748a49455ae1545f01fe5cfc9b801223aa
104+Merge: 9a49690 2473311
105+Author: Jenkins <jenkins@review.openstack.org>
106+Date: Fri Dec 21 08:46:11 2012 +0000
107+
108+ Merge "Add clone volume support to cinderclient."
109+
110+commit 9a496900964c7037db994db800bab456d57c2164
111+Author: john-griffith <john.griffith@solidfire.com>
112+Date: Mon Dec 17 14:52:05 2012 -0700
113+
114+ Add list-extensions capability to cinderclient.
115+
116+ Implements the list extension functionality from novaclient
117+ written by harlowja (Change-Id: I5b72f5ea73c00f1c1a0f09f670d744c820e05837)
118+
119+ This provides a mechanism to view available extensions, and also
120+ starts actually using the cinderclient contrib dir.
121+
122+ Original Author: harlowja
123+
124+ Change-Id: I4f9d04cb8a2aa05c978a2a5a926c9175ee3614bb
125+
126+ cinderclient/v1/contrib/list_extensions.py | 47 ++++++++++++++++++++++++++++
127+ tests/v1/contrib/test_list_extensions.py | 21 +++++++++++++
128+ tests/v1/fakes.py | 30 ++++++++++++++++--
129+ 3 files changed, 96 insertions(+), 2 deletions(-)
130+
131+commit 82e47d0866397b702ae777972add66b33afae69e
132+Author: Dean Troyer <dtroyer@gmail.com>
133+Date: Thu Dec 13 12:01:07 2012 -0600
134+
135+ Use requests module for HTTP/HTTPS
136+
137+ * Implement correct certificate verification
138+ * Add --os-cacert
139+ * Rework tests for requests
140+
141+ Pinned requests module to < 1.0 as 1.0.2 is now current in pipi
142+ as of 17Dec2012.
143+
144+ Blueprint: tls-verify
145+
146+ Change-Id: I71066ff7297f3b70c08b7ae1c8ae8b6a1b82bbae
147+
148+ cinderclient/client.py | 95 +++++++++++++++++------------
149+ cinderclient/exceptions.py | 19 +++---
150+ cinderclient/shell.py | 17 ++++--
151+ cinderclient/v1/client.py | 6 +-
152+ tests/test_http.py | 87 +++++++++++++++------------
153+ tests/test_shell.py | 6 --
154+ tests/utils.py | 30 +++++++++-
155+ tests/v1/fakes.py | 109 ++++++++++++++++++----------------
156+ tests/v1/test_auth.py | 142 +++++++++++++++++++++++---------------------
157+ tools/pip-requires | 2 +-
158+ 10 files changed, 296 insertions(+), 217 deletions(-)
159+
160+commit d3603535d2f47580bf49520048af1f27adea3e7c
161+Author: Dean Troyer <dtroyer@gmail.com>
162+Date: Thu Dec 13 14:52:54 2012 -0600
163+
164+ Port some additional logging changes from novaclient
165+
166+ * Allows capture of timestamps prior to and after request for timing
167+ https://review.openstack.org/11519
168+ * Add -X to DELETE and PUT in debug mode
169+ https://review.openstack.org/12069
170+ * Show request body in curl command
171+ https://review.openstack.org/12203
172+
173+ Change-Id: I0d87ab6b3c2b35ff843323cb818915e03993a844
174+
175+ cinderclient/client.py | 18 +++++++++++-------
176+ 1 file changed, 11 insertions(+), 7 deletions(-)
177+
178+commit e30724df5b55f5b989f294f82ad7209e150b3559
179+Author: Dean Troyer <dtroyer@gmail.com>
180+Date: Thu Dec 13 15:25:01 2012 -0600
181+
182+ Bring back the output from client.http_log()
183+
184+ Ported from novaclient https://review.openstack.org/9241
185+ Support CINDERCLIENT_DEBUG as synonym for --debug
186+
187+ Change-Id: Ic03b9e7d84c8db14f6e193ca2b478fd0d70d1299
188+
189+ cinderclient/client.py | 34 ++++++++++++++++++----------------
190+ cinderclient/shell.py | 6 ++++--
191+ cinderclient/v1/client.py | 6 ++++--
192+ 3 files changed, 26 insertions(+), 20 deletions(-)
193+
194+commit 24733119fe5da11bccf16d345f6f27cec8bbd06f
195+Author: John Griffith <john.griffith@solidfire.com>
196+Date: Thu Dec 13 21:21:55 2012 +0000
197+
198+ Add clone volume support to cinderclient.
199+
200+ Adds the option to create a clone of a volume, using --source-volid xxx
201+
202+ relies on Change-Id: I72bf90baf22bec2d4806d00e2b827a594ed213f4
203+
204+ Change-Id: Iad2178deb66c328625fbff747e123044f9d86a88
205+
206+ cinderclient/v1/shell.py | 9 +++++++++
207+ cinderclient/v1/volumes.py | 4 +++-
208+ 2 files changed, 12 insertions(+), 1 deletion(-)
209+
210+commit 9201cee6ef79dd4955f6f762e6adfaee65885ca1
211+Author: Monty Taylor <mordred@inaugust.com>
212+Date: Fri Nov 30 14:22:54 2012 -0800
213+
214+ Update to swapped versioninfo logic.
215+
216+ Change-Id: I6a8dd1c84df0a92c21f468c7dcad1ce79f69c463
217+
218+ cinderclient/openstack/common/setup.py | 18 ++++++++++--------
219+ 1 file changed, 10 insertions(+), 8 deletions(-)
220+
221+commit 5adf79136042004ceb76d3d1e68e0ab08edf1917
222+Author: Monty Taylor <mordred@inaugust.com>
223+Date: Fri Nov 30 14:05:54 2012 -0800
224+
225+ Align cinderclient version code.
226+
227+ Change-Id: I81d6a279a52656720626357a1c4ca8bb382ef1f8
228+
229+ .gitignore | 2 +
230+ MANIFEST.in | 9 +-
231+ cinderclient/__init__.py | 36 ++++++++
232+ cinderclient/openstack/common/version.py | 148 ++++++++++++++++++++++++++++++
233+ openstack-common.conf | 2 +-
234+ setup.py | 4 +-
235+ 6 files changed, 192 insertions(+), 9 deletions(-)
236+
237+commit a74dee000e682b5b227cd9032df6719aa22ffb00
238+Merge: 3ca2c99 62eb92a
239+Author: Jenkins <jenkins@review.openstack.org>
240+Date: Thu Nov 22 07:55:01 2012 +0000
241+
242+ Merge "Pin pep8 to 1.3.3"
243+
244+commit 3ca2c999583a57ef785ee011ac8d805571b8624c
245+Merge: 79dc21d c01e782
246+Author: Jenkins <jenkins@review.openstack.org>
247+Date: Thu Nov 22 06:13:15 2012 +0000
248+
249+ Merge "Adding bootable column to volume list view"
250+
251+commit c01e7822f9de3b17b8cca8d0b10cbf03e6890f8f
252+Author: Mike Perez <thingee@gmail.com>
253+Date: Sun Nov 18 19:55:49 2012 -0800
254+
255+ Adding bootable column to volume list view
256+
257+ displays whether a volume is bootable based on cinder's api response for
258+ /volumes
259+
260+ Change-Id: I8f6cc6e02be8226914f65717dcb2e0367553e51f
261+
262+ cinderclient/v1/shell.py | 2 +-
263+ tests/v1/fakes.py | 1 +
264+ 2 files changed, 2 insertions(+), 1 deletion(-)
265+
266+commit 62eb92a1f2cffa5a81778018932b539f436b50f0
267+Author: Chuck Short <chuck.short@canonical.com>
268+Date: Tue Nov 20 09:41:28 2012 -0600
269+
270+ Pin pep8 to 1.3.3
271+
272+ Apart of making pep8 version standard across all openstack
273+ projects.
274+
275+ Change-Id: If5ef6be394e557b3554a80f1ad9b44d472c273b6
276+ Signed-off-by: Chuck Short <chuck.short@canonical.com>
277+
278+ tools/test-requires | 2 +-
279+ 1 file changed, 1 insertion(+), 1 deletion(-)
280+
281+commit 79dc21d17183fa78126515f9d8436c207fbed7b7
282+Author: Christian Berendt <berendt@b1-systems.de>
283+Date: Thu Nov 8 23:33:14 2012 +0100
284+
285+ show help when calling without arguments
286+
287+ When calling cinder without arguments you'll receive the following
288+ output:
289+
290+ error: too few arguments
291+ Try 'cinder help ' for more information.
292+
293+ With this change the help is also shown when calling cinder
294+ without arguments. I think that's the expected behavior.
295+
296+ Change-Id: I8f46f67b7fef472ac344bb74f80cf1b77c4c4745
297+
298+ cinderclient/shell.py | 2 +-
299+ tests/test_shell.py | 18 ++++++------------
300+ 2 files changed, 7 insertions(+), 13 deletions(-)
301+
302+commit 112bd60d4e17ad435c274d52585f70faaaf9f70d
303+Author: Cory Stone <corystone@gmail.com>
304+Date: Thu Nov 1 14:27:52 2012 -0500
305+
306+ Add retries to cinderclient.
307+
308+ HTTPClient now supports a retries argument. It will reissue requests
309+ for any 5xx or socket (400 with n/a) errors. This retry loop was
310+ "inspired" by swiftclient's loop. It reauths one extra time if
311+ necessary. It uses backoff times of 1, 2, 4... seconds.
312+
313+ The default is 0 retries. It is also exposed to the shell as well with
314+ a --retries arg.
315+
316+ Change-Id: I67bed02d65155f4a4d5d879bb233f56cc78849fa
317+
318+ cinderclient/client.py | 54 ++++++++++++++++------
319+ cinderclient/shell.py | 9 +++-
320+ cinderclient/v1/client.py | 5 +-
321+ tests/test_http.py | 113 +++++++++++++++++++++++++++++++++++++++++++--
322+ 4 files changed, 159 insertions(+), 22 deletions(-)
323+
324+commit 1abc0b4edfa70485cd32bac2060b1b223850752e
325+Author: Alessandro Pilotti <ap@pilotti.it>
326+Date: Tue Nov 6 19:38:28 2012 +0200
327+
328+ Fixes setup compatibility issue on Windows
329+
330+ Fixes Bug #1052161
331+
332+ "python setup.py build" fails on Windows due to a hardcoded shell path:
333+ /bin/sh
334+
335+ setup.py updated using openstack-common/update.py
336+
337+ Change-Id: Iafae444a43c76560020a84e3a1c5c8cb4b6860da
338+
339+ cinderclient/openstack/common/setup.py | 120 +++++++++++++++++++++-----------
340+ openstack-common.conf | 7 ++
341+ 2 files changed, 85 insertions(+), 42 deletions(-)
342+
343+commit dcbebd7b78235008e36fe6b8c8cc4a75f5031042
344+Author: John Griffith <john.griffith@solidfire.com>
345+Date: Fri Nov 2 22:17:03 2012 +0000
346+
347+ Revert "Add retries to cinderclient."
348+
349+ This reverts commit 3b1eda3b3bb6678e9c9ec3e783e3e12b0c42f0a4
350+
351+ cinderclient/client.py | 47 +++++++++++-----------------------
352+ cinderclient/shell.py | 8 +-----
353+ cinderclient/v1/client.py | 5 ++--
354+ tests/test_http.py | 62 +--------------------------------------------
355+ 4 files changed, 19 insertions(+), 103 deletions(-)
356+
357+commit 3b1eda3b3bb6678e9c9ec3e783e3e12b0c42f0a4
358+Author: Cory Stone <corystone@gmail.com>
359+Date: Thu Nov 1 14:27:52 2012 -0500
360+
361+ Add retries to cinderclient.
362+
363+ HTTPClient now supports a retries argument. It will reissue requests
364+ for any 5xx or socket (400 with n/a) errors. This retry loop was
365+ "inspired" by swiftclient's loop. It reauths one time if necessary.
366+ It uses backoff times of 1, 2, 4... seconds.
367+
368+ The default is 0 retries. It is also exposed to the shell as well with
369+ a --retries arg.
370+
371+ Change-Id: I75d9a13d6c4ba16a5da13d4bf5cad78a777d67d7
372+
373+ cinderclient/client.py | 47 +++++++++++++++++++++++-----------
374+ cinderclient/shell.py | 8 +++++-
375+ cinderclient/v1/client.py | 5 ++--
376+ tests/test_http.py | 62 ++++++++++++++++++++++++++++++++++++++++++++-
377+ 4 files changed, 103 insertions(+), 19 deletions(-)
378+
379+commit 7d3749e7c3c3f66900a4b08563a3b1e8d1dd0c35
380+Merge: 0288e7f a8be1b2
381+Author: Jenkins <jenkins@review.openstack.org>
382+Date: Thu Nov 1 19:04:42 2012 +0000
383+
384+ Merge "Remove extra-specs from types-list command output"
385+
386+commit 0288e7fd095037898fe451275a3ecfc183eac134
387+Merge: 92bd08b 28d4d6b
388+Author: Jenkins <jenkins@review.openstack.org>
389+Date: Thu Nov 1 18:59:45 2012 +0000
390+
391+ Merge "Remove attach/detach code from cinderclient"
392+
393+commit a8be1b2a205dfba0a23d92454be67adfe0a0aaf4
394+Author: John Griffith <john.griffith@solidfire.com>
395+Date: Wed Oct 31 18:10:54 2012 -0600
396+
397+ Remove extra-specs from types-list command output
398+
399+ type-list was including the extra-specs info in it's response,
400+ however extra-specs are admin accessible only. As a result only admin
401+ users could run type-list which is not what we want.
402+
403+ Add a seperate admin only call to provide volume-types with associated
404+ extra-specs info (extra-specs-list).
405+
406+ Change-Id: I2a716cd7076fb17fd5f2ceb94363eef0009e9bce
407+
408+ cinderclient/v1/shell.py | 12 +++++++++++-
409+ 1 file changed, 11 insertions(+), 1 deletion(-)
410+
411+commit 92bd08b9e1082620d41e827db2eeade5da6c8e19
412+Merge: 4649420 942ab5c
413+Author: Jenkins <jenkins@review.openstack.org>
414+Date: Wed Oct 31 20:24:16 2012 +0000
415+
416+ Merge "Add volume_type extra_specs support to client"
417+
418+commit 28d4d6bfd09063e8d814cf64ef504aa01a9a90e7
419+Author: John Griffith <john.griffith@solidfire.com>
420+Date: Tue Oct 30 10:24:47 2012 -0600
421+
422+ Remove attach/detach code from cinderclient
423+
424+ Attach and Detach actions are done in the novaclient, however
425+ during the creation of Cinder a number of these methods were copied into
426+ the Cinder client. This is dead code and just adds confusion, so let's
427+ rip it out.
428+
429+ Change-Id: Ib96fe0cac26f19e7d9f2aa01c71ba9762e4f8b8e
430+ Fixes: bug #1071003
431+
432+ cinderclient/v1/volumes.py | 46 --------------------------------------------
433+ 1 file changed, 46 deletions(-)
434+
435+commit 4649420ca2e8e24c8e651d141839d62fbbb53416
436+Merge: c20d7d9 01dad32
437+Author: Jenkins <jenkins@review.openstack.org>
438+Date: Tue Oct 30 16:15:09 2012 +0000
439+
440+ Merge "Fix support for Unicode volume names"
441+
442+commit c20d7d902ee6e2ef50f20fa850f47afa310b5998
443+Author: Dean Troyer <dtroyer@gmail.com>
444+Date: Tue Oct 23 14:48:38 2012 -0500
445+
446+ Add python_cinderclient.egg-info to .gitignore
447+
448+ Change-Id: I24a7a5f423e99a9b6839b5f3a1b1fbf05297dc53
449+
450+ .gitignore | 2 +-
451+ 1 file changed, 1 insertion(+), 1 deletion(-)
452+
453+commit 01dad32c07ad500f2e0000dcd24a50f5c47cd206
454+Author: Eric Harney <eharney@redhat.com>
455+Date: Tue Oct 23 15:40:17 2012 -0400
456+
457+ Fix support for Unicode volume names
458+
459+ It is possible to create a Unicode volume from the command line,
460+ but it cannot be manipulated by name for operations such as delete.
461+ This is because the find_resource function tries to match the
462+ Unicode string to a regular byte string, and a UnicodeWarning is
463+ issued, failing the match. Fix by decoding the Unicode name when
464+ trying to match.
465+
466+ Fixes bug 1065275.
467+
468+ Change-Id: I8e19a78bbc1ccb503ccd39dc3b904fc4f6f77858
469+
470+ cinderclient/utils.py | 20 ++++++++++++++------
471+ 1 file changed, 14 insertions(+), 6 deletions(-)
472+
473+commit 5a3a18d0cbe71ae7e150fe69efe5239601413f9b
474+Author: Doug Hellmann <doug.hellmann@dreamhost.com>
475+Date: Mon Oct 22 18:42:34 2012 -0400
476+
477+ Add OpenStack trove classifier for PyPI
478+
479+ Add trove classifier to have the client listed among the
480+ other OpenStack-related projets on PyPI.
481+
482+ Change-Id: I904372caf0a8eaa44dd048729b4d87e5333f04cc
483+ Signed-off-by: Doug Hellmann <doug.hellmann@dreamhost.com>
484+
485+ setup.py | 1 +
486+ 1 file changed, 1 insertion(+)
487+
488+commit 942ab5c7fe901dd925ee38328c91e1cd160bc437
489+Author: John Griffith <john.griffith@solidfire.com>
490+Date: Tue Oct 9 18:45:41 2012 -0600
491+
492+ Add volume_type extra_specs support to client
493+
494+ Adds ability to set/clear volume_type extra_specs
495+ via python-cinderclient.
496+
497+ * Adds extra_specs info to the list-types output
498+ * Adds missing tests for volume_types features
499+ * Fixes unset loop so it actually iterates through all of the supplied keys
500+
501+ Change-Id: I3552de722f76389cfef6d4f12320720e022ebfac
502+
503+ cinderclient/v1/client.py | 4 +--
504+ cinderclient/v1/shell.py | 63 ++++++++++++++++++++++++++++++++-------
505+ cinderclient/v1/volume_types.py | 47 ++++++++++++++++++++++++++++-
506+ tests/v1/fakes.py | 32 ++++++++++++++++++++
507+ tests/v1/test_types.py | 35 ++++++++++++++++++++++
508+ 5 files changed, 167 insertions(+), 14 deletions(-)
509+
510+commit 7cba8eb7863705fcf0ef5c677cef2d334bd8ad26
511+Author: Clay Gerrard <clay.gerrard@gmail.com>
512+Date: Tue Aug 28 20:20:35 2012 +0000
513+
514+ add rename and snapshot-rename commands
515+
516+ Change-Id: I06549f19b846d886fabd611d2167b894c4d02df8
517+
518+ cinderclient/v1/shell.py | 34 +++++++++++++++++++++++++++
519+ cinderclient/v1/volume_snapshots.py | 19 +++++++++++++++
520+ cinderclient/v1/volumes.py | 19 +++++++++++++++
521+ tests/v1/fakes.py | 36 +++++++++++++++++++++++++++-
522+ tests/v1/test_shell.py | 44 +++++++++++++++++++++++++++++++++++
523+ 5 files changed, 151 insertions(+), 1 deletion(-)
524+
525 commit 2e6be694c17d505bf52e339bccf6848776493ffe
526 Author: Vishvananda Ishaya <vishvananda@gmail.com>
527 Date: Thu Sep 20 14:29:21 2012 +0000
528
529=== modified file 'MANIFEST.in'
530--- MANIFEST.in 2012-08-16 12:03:50 +0000
531+++ MANIFEST.in 2013-01-10 22:15:27 +0000
532@@ -1,10 +1,5 @@
533 include AUTHORS
534 include ChangeLog
535-include HACKING
536-include LICENSE
537-include README.rst
538-include run_tests.sh tox.ini
539 include cinderclient/versioninfo
540-recursive-include doc *
541-recursive-include tests *
542-recursive-include tools *
543+exclude .gitignore
544+exclude .gitreview
545
546=== modified file 'PKG-INFO'
547--- PKG-INFO 2012-10-01 12:05:26 +0000
548+++ PKG-INFO 2013-01-10 22:15:27 +0000
549@@ -1,6 +1,6 @@
550 Metadata-Version: 1.1
551 Name: python-cinderclient
552-Version: 1.0.0
553+Version: 1.0.2.3.g61e2a42
554 Summary: Client library for OpenStack Cinder API.
555 Home-page: https://github.com/openstack/python-cinderclient
556 Author: Rackspace, based on work by Jacob Kaplan-Moss
557@@ -161,6 +161,7 @@
558 Platform: UNKNOWN
559 Classifier: Development Status :: 5 - Production/Stable
560 Classifier: Environment :: Console
561+Classifier: Environment :: OpenStack
562 Classifier: Intended Audience :: Developers
563 Classifier: Intended Audience :: Information Technology
564 Classifier: License :: OSI Approved :: Apache Software License
565
566=== modified file 'cinderclient/__init__.py'
567--- cinderclient/__init__.py 2012-06-27 13:32:14 +0000
568+++ cinderclient/__init__.py 2013-01-10 22:15:27 +0000
569@@ -0,0 +1,36 @@
570+# vim: tabstop=4 shiftwidth=4 softtabstop=4
571+
572+# Copyright 2012 OpenStack LLC
573+#
574+# Licensed under the Apache License, Version 2.0 (the "License"); you may
575+# not use this file except in compliance with the License. You may obtain
576+# a copy of the License at
577+#
578+# http://www.apache.org/licenses/LICENSE-2.0
579+#
580+# Unless required by applicable law or agreed to in writing, software
581+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
582+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
583+# License for the specific language governing permissions and limitations
584+# under the License.
585+
586+import os
587+import inspect
588+
589+
590+def _get_client_version():
591+ """Read version from versioninfo file."""
592+ mod_abspath = inspect.getabsfile(inspect.currentframe())
593+ client_path = os.path.dirname(mod_abspath)
594+ version_path = os.path.join(client_path, 'versioninfo')
595+
596+ if os.path.exists(version_path):
597+ version = open(version_path).read().strip()
598+ else:
599+ version = "Unknown, couldn't find versioninfo file at %s"\
600+ % version_path
601+
602+ return version
603+
604+
605+__version__ = _get_client_version()
606
607=== modified file 'cinderclient/client.py'
608--- cinderclient/client.py 2012-10-01 12:05:26 +0000
609+++ cinderclient/client.py 2013-01-10 22:15:27 +0000
610@@ -7,10 +7,14 @@
611 OpenStack Client interface. Handles the REST calls and responses.
612 """
613
614-import httplib2
615 import logging
616 import os
617+import sys
618 import urlparse
619+try:
620+ from eventlet import sleep
621+except ImportError:
622+ from time import sleep
623
624 try:
625 import json
626@@ -22,28 +26,27 @@
627 import cgi
628 urlparse.parse_qsl = cgi.parse_qsl
629
630+import requests
631+
632 from cinderclient import exceptions
633 from cinderclient import service_catalog
634 from cinderclient import utils
635
636
637-_logger = logging.getLogger(__name__)
638-if 'CINDERCLIENT_DEBUG' in os.environ and os.environ['CINDERCLIENT_DEBUG']:
639- ch = logging.StreamHandler()
640- _logger.setLevel(logging.DEBUG)
641- _logger.addHandler(ch)
642-
643-
644-class HTTPClient(httplib2.Http):
645+class HTTPClient(object):
646
647 USER_AGENT = 'python-cinderclient'
648
649+ requests_config = {
650+ 'danger_mode': False,
651+ }
652+
653 def __init__(self, user, password, projectid, auth_url, insecure=False,
654 timeout=None, tenant_id=None, proxy_tenant_id=None,
655 proxy_token=None, region_name=None,
656 endpoint_type='publicURL', service_type=None,
657- service_name=None, volume_service_name=None):
658- super(HTTPClient, self).__init__(timeout=timeout)
659+ service_name=None, volume_service_name=None, retries=None,
660+ http_log_debug=False, cacert=None):
661 self.user = user
662 self.password = password
663 self.projectid = projectid
664@@ -55,23 +58,36 @@
665 self.service_type = service_type
666 self.service_name = service_name
667 self.volume_service_name = volume_service_name
668+ self.retries = int(retries or 0)
669+ self.http_log_debug = http_log_debug
670
671 self.management_url = None
672 self.auth_token = None
673 self.proxy_token = proxy_token
674 self.proxy_tenant_id = proxy_tenant_id
675
676- # httplib2 overrides
677- self.force_exception_to_status_code = True
678- self.disable_ssl_certificate_validation = insecure
679-
680- def http_log(self, args, kwargs, resp, body):
681- if not _logger.isEnabledFor(logging.DEBUG):
682+ if insecure:
683+ self.verify_cert = False
684+ else:
685+ if cacert:
686+ self.verify_cert = cacert
687+ else:
688+ self.verify_cert = True
689+
690+ self._logger = logging.getLogger(__name__)
691+ if self.http_log_debug:
692+ ch = logging.StreamHandler()
693+ self._logger.setLevel(logging.DEBUG)
694+ self._logger.addHandler(ch)
695+ self.requests_config['verbose'] = sys.stderr
696+
697+ def http_log_req(self, args, kwargs):
698+ if not self.http_log_debug:
699 return
700
701 string_parts = ['curl -i']
702 for element in args:
703- if element in ('GET', 'POST'):
704+ if element in ('GET', 'POST', 'DELETE', 'PUT'):
705 string_parts.append(' -X %s' % element)
706 else:
707 string_parts.append(' %s' % element)
708@@ -80,59 +96,94 @@
709 header = ' -H "%s: %s"' % (element, kwargs['headers'][element])
710 string_parts.append(header)
711
712- _logger.debug("REQ: %s\n" % "".join(string_parts))
713 if 'body' in kwargs:
714- _logger.debug("REQ BODY: %s\n" % (kwargs['body']))
715- _logger.debug("RESP:%s %s\n", resp, body)
716-
717- def request(self, *args, **kwargs):
718+ string_parts.append(" -d '%s'" % (kwargs['body']))
719+ self._logger.debug("\nREQ: %s\n" % "".join(string_parts))
720+
721+ def http_log_resp(self, resp):
722+ if not self.http_log_debug:
723+ return
724+ self._logger.debug(
725+ "RESP: [%s] %s\nRESP BODY: %s\n",
726+ resp.status_code,
727+ resp.headers,
728+ resp.text)
729+
730+ def request(self, url, method, **kwargs):
731 kwargs.setdefault('headers', kwargs.get('headers', {}))
732 kwargs['headers']['User-Agent'] = self.USER_AGENT
733 kwargs['headers']['Accept'] = 'application/json'
734 if 'body' in kwargs:
735 kwargs['headers']['Content-Type'] = 'application/json'
736- kwargs['body'] = json.dumps(kwargs['body'])
737-
738- resp, body = super(HTTPClient, self).request(*args, **kwargs)
739-
740- self.http_log(args, kwargs, resp, body)
741-
742- if body:
743+ kwargs['data'] = json.dumps(kwargs['body'])
744+ del kwargs['body']
745+
746+ self.http_log_req((url, method,), kwargs)
747+ resp = requests.request(
748+ method,
749+ url,
750+ verify=self.verify_cert,
751+ config=self.requests_config,
752+ **kwargs)
753+ self.http_log_resp(resp)
754+
755+ if resp.text:
756 try:
757- body = json.loads(body)
758+ body = json.loads(resp.text)
759 except ValueError:
760 pass
761+ body = None
762 else:
763 body = None
764
765- if resp.status >= 400:
766+ if resp.status_code >= 400:
767 raise exceptions.from_response(resp, body)
768
769 return resp, body
770
771 def _cs_request(self, url, method, **kwargs):
772- if not self.management_url:
773- self.authenticate()
774-
775- # Perform the request once. If we get a 401 back then it
776- # might be because the auth token expired, so try to
777- # re-authenticate and try again. If it still fails, bail.
778- try:
779+ auth_attempts = 0
780+ attempts = 0
781+ backoff = 1
782+ while True:
783+ attempts += 1
784+ if not self.management_url or not self.auth_token:
785+ self.authenticate()
786 kwargs.setdefault('headers', {})['X-Auth-Token'] = self.auth_token
787 if self.projectid:
788 kwargs['headers']['X-Auth-Project-Id'] = self.projectid
789-
790- resp, body = self.request(self.management_url + url, method,
791- **kwargs)
792- return resp, body
793- except exceptions.Unauthorized, ex:
794 try:
795- self.authenticate()
796 resp, body = self.request(self.management_url + url, method,
797 **kwargs)
798 return resp, body
799+ except exceptions.BadRequest as e:
800+ if attempts > self.retries:
801+ raise
802 except exceptions.Unauthorized:
803- raise ex
804+ if auth_attempts > 0:
805+ raise
806+ self._logger.debug("Unauthorized, reauthenticating.")
807+ self.management_url = self.auth_token = None
808+ # First reauth. Discount this attempt.
809+ attempts -= 1
810+ auth_attempts += 1
811+ continue
812+ except exceptions.ClientException as e:
813+ if attempts > self.retries:
814+ raise
815+ if 500 <= e.code <= 599:
816+ pass
817+ else:
818+ raise
819+ except requests.exceptions.ConnectionError as e:
820+ # Catch a connection refused from requests.request
821+ self._logger.debug("Connection refused: %s" % e)
822+ raise
823+ self._logger.debug(
824+ "Failed attempt(%s of %s), retrying in %s seconds" %
825+ (attempts, self.retries, backoff))
826+ sleep(backoff)
827+ backoff *= 2
828
829 def get(self, url, **kwargs):
830 return self._cs_request(url, 'GET', **kwargs)
831@@ -151,7 +202,7 @@
832 We may get redirected to another site, fail or actually get
833 back a service catalog with a token and our endpoints."""
834
835- if resp.status == 200: # content must always present
836+ if resp.status_code == 200: # content must always present
837 try:
838 self.auth_url = url
839 self.service_catalog = \
840@@ -179,7 +230,7 @@
841 print "Could not find any suitable endpoint. Correct region?"
842 raise
843
844- elif resp.status == 305:
845+ elif resp.status_code == 305:
846 return resp['location']
847 else:
848 raise exceptions.from_response(resp, body)
849@@ -199,7 +250,7 @@
850 # GET ...:5001/v2.0/tokens/#####/endpoints
851 url = '/'.join([url, 'tokens', '%s?belongsTo=%s'
852 % (self.proxy_token, self.proxy_tenant_id)])
853- _logger.debug("Using Endpoint URL: %s" % url)
854+ self._logger.debug("Using Endpoint URL: %s" % url)
855 resp, body = self.request(url, "GET",
856 headers={'X-Auth_Token': self.auth_token})
857 return self._extract_service_catalog(url, resp, body,
858@@ -262,16 +313,16 @@
859 headers['X-Auth-Project-Id'] = self.projectid
860
861 resp, body = self.request(url, 'GET', headers=headers)
862- if resp.status in (200, 204): # in some cases we get No Content
863+ if resp.status_code in (200, 204): # in some cases we get No Content
864 try:
865 mgmt_header = 'x-server-management-url'
866- self.management_url = resp[mgmt_header].rstrip('/')
867- self.auth_token = resp['x-auth-token']
868+ self.management_url = resp.headers[mgmt_header].rstrip('/')
869+ self.auth_token = resp.headers['x-auth-token']
870 self.auth_url = url
871- except KeyError:
872+ except (KeyError, TypeError):
873 raise exceptions.AuthorizationFailure()
874- elif resp.status == 305:
875- return resp['location']
876+ elif resp.status_code == 305:
877+ return resp.headers['location']
878 else:
879 raise exceptions.from_response(resp, body)
880
881@@ -303,13 +354,11 @@
882 token_url = url + "/tokens"
883
884 # Make sure we follow redirects when trying to reach Keystone
885- tmp_follow_all_redirects = self.follow_all_redirects
886- self.follow_all_redirects = True
887-
888- try:
889- resp, body = self.request(token_url, "POST", body=body)
890- finally:
891- self.follow_all_redirects = tmp_follow_all_redirects
892+ resp, body = self.request(
893+ token_url,
894+ "POST",
895+ body=body,
896+ allow_redirects=True)
897
898 return self._extract_service_catalog(url, resp, body)
899
900
901=== modified file 'cinderclient/exceptions.py'
902--- cinderclient/exceptions.py 2012-06-27 13:32:14 +0000
903+++ cinderclient/exceptions.py 2013-01-10 22:15:27 +0000
904@@ -124,16 +124,19 @@
905 def from_response(response, body):
906 """
907 Return an instance of an ClientException or subclass
908- based on an httplib2 response.
909+ based on an requests response.
910
911 Usage::
912
913- resp, body = http.request(...)
914- if resp.status != 200:
915- raise exception_from_response(resp, body)
916+ resp, body = requests.request(...)
917+ if resp.status_code != 200:
918+ raise exception_from_response(resp, rest.text)
919 """
920- cls = _code_map.get(response.status, ClientException)
921- request_id = response.get('x-compute-request-id')
922+ cls = _code_map.get(response.status_code, ClientException)
923+ if response.headers:
924+ request_id = response.headers.get('x-compute-request-id')
925+ else:
926+ request_id = None
927 if body:
928 message = "n/a"
929 details = "n/a"
930@@ -141,7 +144,7 @@
931 error = body[body.keys()[0]]
932 message = error.get('message', None)
933 details = error.get('details', None)
934- return cls(code=response.status, message=message, details=details,
935+ return cls(code=response.status_code, message=message, details=details,
936 request_id=request_id)
937 else:
938- return cls(code=response.status, request_id=request_id)
939+ return cls(code=response.status_code, request_id=request_id)
940
941=== modified file 'cinderclient/openstack/common/setup.py'
942--- cinderclient/openstack/common/setup.py 2012-08-16 12:03:50 +0000
943+++ cinderclient/openstack/common/setup.py 2013-01-10 22:15:27 +0000
944@@ -31,13 +31,13 @@
945 def parse_mailmap(mailmap='.mailmap'):
946 mapping = {}
947 if os.path.exists(mailmap):
948- fp = open(mailmap, 'r')
949- for l in fp:
950- l = l.strip()
951- if not l.startswith('#') and ' ' in l:
952- canonical_email, alias = [x for x in l.split(' ')
953- if x.startswith('<')]
954- mapping[alias] = canonical_email
955+ with open(mailmap, 'r') as fp:
956+ for l in fp:
957+ l = l.strip()
958+ if not l.startswith('#') and ' ' in l:
959+ canonical_email, alias = [x for x in l.split(' ')
960+ if x.startswith('<')]
961+ mapping[alias] = canonical_email
962 return mapping
963
964
965@@ -52,10 +52,10 @@
966
967 # Get requirements from the first file that exists
968 def get_reqs_from_files(requirements_files):
969- reqs_in = []
970 for requirements_file in requirements_files:
971 if os.path.exists(requirements_file):
972- return open(requirements_file, 'r').read().split('\n')
973+ with open(requirements_file, 'r') as fil:
974+ return fil.read().split('\n')
975 return []
976
977
978@@ -117,8 +117,12 @@
979
980
981 def _run_shell_command(cmd):
982- output = subprocess.Popen(["/bin/sh", "-c", cmd],
983- stdout=subprocess.PIPE)
984+ if os.name == 'nt':
985+ output = subprocess.Popen(["cmd.exe", "/C", cmd],
986+ stdout=subprocess.PIPE)
987+ else:
988+ output = subprocess.Popen(["/bin/sh", "-c", cmd],
989+ stdout=subprocess.PIPE)
990 out = output.communicate()
991 if len(out) == 0:
992 return None
993@@ -136,11 +140,19 @@
994 _run_shell_command("git fetch origin +refs/meta/*:refs/remotes/meta/*")
995 milestone_cmd = "git show meta/openstack/release:%s" % branch_name
996 milestonever = _run_shell_command(milestone_cmd)
997- if not milestonever:
998- milestonever = ""
999+ if milestonever:
1000+ first_half = "%s~%s" % (milestonever, datestamp)
1001+ else:
1002+ first_half = datestamp
1003+
1004 post_version = _get_git_post_version()
1005- revno = post_version.split(".")[-1]
1006- return "%s~%s.%s%s" % (milestonever, datestamp, revno_prefix, revno)
1007+ # post version should look like:
1008+ # 0.1.1.4.gcc9e28a
1009+ # where the bit after the last . is the short sha, and the bit between
1010+ # the last and second to last is the revno count
1011+ (revno, sha) = post_version.split(".")[-2:]
1012+ second_half = "%s%s.%s" % (revno_prefix, revno, sha)
1013+ return ".".join((first_half, second_half))
1014
1015
1016 def _get_git_current_tag():
1017@@ -162,39 +174,48 @@
1018 cmd = "git --no-pager log --oneline"
1019 out = _run_shell_command(cmd)
1020 revno = len(out.split("\n"))
1021+ sha = _run_shell_command("git describe --always")
1022 else:
1023 tag_infos = tag_info.split("-")
1024 base_version = "-".join(tag_infos[:-2])
1025- revno = tag_infos[-2]
1026- return "%s.%s" % (base_version, revno)
1027+ (revno, sha) = tag_infos[-2:]
1028+ return "%s.%s.%s" % (base_version, revno, sha)
1029
1030
1031 def write_git_changelog():
1032 """Write a changelog based on the git changelog."""
1033- if os.path.isdir('.git'):
1034- git_log_cmd = 'git log --stat'
1035- changelog = _run_shell_command(git_log_cmd)
1036- mailmap = parse_mailmap()
1037- with open("ChangeLog", "w") as changelog_file:
1038- changelog_file.write(canonicalize_emails(changelog, mailmap))
1039+ new_changelog = 'ChangeLog'
1040+ if not os.getenv('SKIP_WRITE_GIT_CHANGELOG'):
1041+ if os.path.isdir('.git'):
1042+ git_log_cmd = 'git log --stat'
1043+ changelog = _run_shell_command(git_log_cmd)
1044+ mailmap = parse_mailmap()
1045+ with open(new_changelog, "w") as changelog_file:
1046+ changelog_file.write(canonicalize_emails(changelog, mailmap))
1047+ else:
1048+ open(new_changelog, 'w').close()
1049
1050
1051 def generate_authors():
1052 """Create AUTHORS file using git commits."""
1053- jenkins_email = 'jenkins@review.openstack.org'
1054+ jenkins_email = 'jenkins@review.(openstack|stackforge).org'
1055 old_authors = 'AUTHORS.in'
1056 new_authors = 'AUTHORS'
1057- if os.path.isdir('.git'):
1058- # don't include jenkins email address in AUTHORS file
1059- git_log_cmd = ("git log --format='%aN <%aE>' | sort -u | "
1060- "grep -v " + jenkins_email)
1061- changelog = _run_shell_command(git_log_cmd)
1062- mailmap = parse_mailmap()
1063- with open(new_authors, 'w') as new_authors_fh:
1064- new_authors_fh.write(canonicalize_emails(changelog, mailmap))
1065- if os.path.exists(old_authors):
1066- with open(old_authors, "r") as old_authors_fh:
1067- new_authors_fh.write('\n' + old_authors_fh.read())
1068+ if not os.getenv('SKIP_GENERATE_AUTHORS'):
1069+ if os.path.isdir('.git'):
1070+ # don't include jenkins email address in AUTHORS file
1071+ git_log_cmd = ("git log --format='%aN <%aE>' | sort -u | "
1072+ "egrep -v '" + jenkins_email + "'")
1073+ changelog = _run_shell_command(git_log_cmd)
1074+ mailmap = parse_mailmap()
1075+ with open(new_authors, 'w') as new_authors_fh:
1076+ new_authors_fh.write(canonicalize_emails(changelog, mailmap))
1077+ if os.path.exists(old_authors):
1078+ with open(old_authors, "r") as old_authors_fh:
1079+ new_authors_fh.write('\n' + old_authors_fh.read())
1080+ else:
1081+ open(new_authors, 'w').close()
1082+
1083
1084 _rst_template = """%(heading)s
1085 %(underline)s
1086@@ -206,9 +227,24 @@
1087 """
1088
1089
1090+def read_versioninfo(project):
1091+ """Read the versioninfo file. If it doesn't exist, we're in a github
1092+ zipball, and there's really no way to know what version we really
1093+ are, but that should be ok, because the utility of that should be
1094+ just about nil if this code path is in use in the first place."""
1095+ versioninfo_path = os.path.join(project, 'versioninfo')
1096+ if os.path.exists(versioninfo_path):
1097+ with open(versioninfo_path, 'r') as vinfo:
1098+ version = vinfo.read().strip()
1099+ else:
1100+ version = None
1101+ return version
1102+
1103+
1104 def write_versioninfo(project, version):
1105 """Write a simple file containing the version of the package."""
1106- open(os.path.join(project, 'versioninfo'), 'w').write("%s\n" % version)
1107+ with open(os.path.join(project, 'versioninfo'), 'w') as fil:
1108+ fil.write("%s\n" % version)
1109
1110
1111 def get_cmdclass():
1112@@ -299,8 +335,10 @@
1113
1114
1115 def get_pre_version(projectname, base_version):
1116- """Return a version which is based"""
1117- if os.path.isdir('.git'):
1118+ """Return a version which is leading up to a version that will
1119+ be released in the future."""
1120+ version = read_versioninfo(projectname)
1121+ if not version and os.path.isdir('.git'):
1122 current_tag = _get_git_current_tag()
1123 if current_tag is not None:
1124 version = current_tag
1125@@ -311,11 +349,9 @@
1126 version_suffix = _get_git_next_version_suffix(branch_name)
1127 version = "%s~%s" % (base_version, version_suffix)
1128 write_versioninfo(projectname, version)
1129- return version.split('~')[0]
1130- else:
1131- with open(os.path.join(projectname, 'versioninfo'), 'r') as vinfo:
1132- full_version = vinfo.read().strip()
1133- return full_version.split('~')[0]
1134+ if not version:
1135+ version = "0.0.0"
1136+ return version
1137
1138
1139 def get_post_version(projectname):
1140@@ -323,8 +359,10 @@
1141 revision if there is one, or tag plus number of additional revisions
1142 if the current revision has no tag."""
1143
1144- if os.path.isdir('.git'):
1145+ version = read_versioninfo(projectname)
1146+ if not version and os.path.isdir('.git'):
1147 version = _get_git_post_version()
1148 write_versioninfo(projectname, version)
1149- return version
1150- return open(os.path.join(projectname, 'versioninfo'), 'r').read().strip()
1151+ if not version:
1152+ version = "0.0.0"
1153+ return version
1154
1155=== added file 'cinderclient/openstack/common/version.py'
1156--- cinderclient/openstack/common/version.py 1970-01-01 00:00:00 +0000
1157+++ cinderclient/openstack/common/version.py 2013-01-10 22:15:27 +0000
1158@@ -0,0 +1,148 @@
1159+# vim: tabstop=4 shiftwidth=4 softtabstop=4
1160+
1161+# Copyright 2012 OpenStack LLC
1162+#
1163+# Licensed under the Apache License, Version 2.0 (the "License"); you may
1164+# not use this file except in compliance with the License. You may obtain
1165+# a copy of the License at
1166+#
1167+# http://www.apache.org/licenses/LICENSE-2.0
1168+#
1169+# Unless required by applicable law or agreed to in writing, software
1170+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
1171+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
1172+# License for the specific language governing permissions and limitations
1173+# under the License.
1174+
1175+"""
1176+Utilities for consuming the auto-generated versioninfo files.
1177+"""
1178+
1179+import datetime
1180+import pkg_resources
1181+
1182+import setup
1183+
1184+
1185+class _deferred_version_string(object):
1186+ """Internal helper class which provides delayed version calculation."""
1187+ def __init__(self, version_info, prefix):
1188+ self.version_info = version_info
1189+ self.prefix = prefix
1190+
1191+ def __str__(self):
1192+ return "%s%s" % (self.prefix, self.version_info.version_string())
1193+
1194+ def __repr__(self):
1195+ return "%s%s" % (self.prefix, self.version_info.version_string())
1196+
1197+
1198+class VersionInfo(object):
1199+
1200+ def __init__(self, package, python_package=None, pre_version=None):
1201+ """Object that understands versioning for a package
1202+ :param package: name of the top level python namespace. For glance,
1203+ this would be "glance" for python-glanceclient, it
1204+ would be "glanceclient"
1205+ :param python_package: optional name of the project name. For
1206+ glance this can be left unset. For
1207+ python-glanceclient, this would be
1208+ "python-glanceclient"
1209+ :param pre_version: optional version that the project is working to
1210+ """
1211+ self.package = package
1212+ if python_package is None:
1213+ self.python_package = package
1214+ else:
1215+ self.python_package = python_package
1216+ self.pre_version = pre_version
1217+ self.version = None
1218+
1219+ def _generate_version(self):
1220+ """Defer to the openstack.common.setup routines for making a
1221+ version from git."""
1222+ if self.pre_version is None:
1223+ return setup.get_post_version(self.python_package)
1224+ else:
1225+ return setup.get_pre_version(self.python_package, self.pre_version)
1226+
1227+ def _newer_version(self, pending_version):
1228+ """Check to see if we're working with a stale version or not.
1229+ We expect a version string that either looks like:
1230+ 2012.2~f3~20120708.10.4426392
1231+ which is an unreleased version of a pre-version, or:
1232+ 0.1.1.4.gcc9e28a
1233+ which is an unreleased version of a post-version, or:
1234+ 0.1.1
1235+ Which is a release and which should match tag.
1236+ For now, if we have a date-embedded version, check to see if it's
1237+ old, and if so re-generate. Otherwise, just deal with it.
1238+ """
1239+ try:
1240+ version_date = int(self.version.split("~")[-1].split('.')[0])
1241+ if version_date < int(datetime.date.today().strftime('%Y%m%d')):
1242+ return self._generate_version()
1243+ else:
1244+ return pending_version
1245+ except Exception:
1246+ return pending_version
1247+
1248+ def version_string_with_vcs(self, always=False):
1249+ """Return the full version of the package including suffixes indicating
1250+ VCS status.
1251+
1252+ For instance, if we are working towards the 2012.2 release,
1253+ canonical_version_string should return 2012.2 if this is a final
1254+ release, or else something like 2012.2~f1~20120705.20 if it's not.
1255+
1256+ :param always: if true, skip all version caching
1257+ """
1258+ if always:
1259+ self.version = self._generate_version()
1260+
1261+ if self.version is None:
1262+
1263+ requirement = pkg_resources.Requirement.parse(self.python_package)
1264+ versioninfo = "%s/versioninfo" % self.package
1265+ try:
1266+ raw_version = pkg_resources.resource_string(requirement,
1267+ versioninfo)
1268+ self.version = self._newer_version(raw_version.strip())
1269+ except (IOError, pkg_resources.DistributionNotFound):
1270+ self.version = self._generate_version()
1271+
1272+ return self.version
1273+
1274+ def canonical_version_string(self, always=False):
1275+ """Return the simple version of the package excluding any suffixes.
1276+
1277+ For instance, if we are working towards the 2012.2 release,
1278+ canonical_version_string should return 2012.2 in all cases.
1279+
1280+ :param always: if true, skip all version caching
1281+ """
1282+ return self.version_string_with_vcs(always).split('~')[0]
1283+
1284+ def version_string(self, always=False):
1285+ """Return the base version of the package.
1286+
1287+ For instance, if we are working towards the 2012.2 release,
1288+ version_string should return 2012.2 if this is a final release, or
1289+ 2012.2-dev if it is not.
1290+
1291+ :param always: if true, skip all version caching
1292+ """
1293+ version_parts = self.version_string_with_vcs(always).split('~')
1294+ if len(version_parts) == 1:
1295+ return version_parts[0]
1296+ else:
1297+ return '%s-dev' % (version_parts[0],)
1298+
1299+ def deferred_version_string(self, prefix=""):
1300+ """Generate an object which will expand in a string context to
1301+ the results of version_string(). We do this so that don't
1302+ call into pkg_resources every time we start up a program when
1303+ passing version information into the CONF constructor, but
1304+ rather only do the calculation when and if a version is requested
1305+ """
1306+ return _deferred_version_string(self, prefix)
1307
1308=== modified file 'cinderclient/shell.py'
1309--- cinderclient/shell.py 2012-09-07 11:35:32 +0000
1310+++ cinderclient/shell.py 2013-01-10 22:15:27 +0000
1311@@ -20,7 +20,6 @@
1312
1313 import argparse
1314 import glob
1315-import httplib2
1316 import imp
1317 import itertools
1318 import os
1319@@ -81,8 +80,9 @@
1320 help=argparse.SUPPRESS)
1321
1322 parser.add_argument('--debug',
1323- default=False,
1324 action='store_true',
1325+ default=utils.env('CINDERCLIENT_DEBUG',
1326+ default=False),
1327 help="Print debugging output")
1328
1329 parser.add_argument('--os-username',
1330@@ -163,12 +163,25 @@
1331 parser.add_argument('--os_volume_api_version',
1332 help=argparse.SUPPRESS)
1333
1334+ parser.add_argument('--os-cacert',
1335+ metavar='<ca-certificate>',
1336+ default=utils.env('OS_CACERT', default=None),
1337+ help='Specify a CA bundle file to use in '
1338+ 'verifying a TLS (https) server certificate. '
1339+ 'Defaults to env[OS_CACERT]')
1340+
1341 parser.add_argument('--insecure',
1342 default=utils.env('CINDERCLIENT_INSECURE',
1343 default=False),
1344 action='store_true',
1345 help=argparse.SUPPRESS)
1346
1347+ parser.add_argument('--retries',
1348+ metavar='<retries>',
1349+ type=int,
1350+ default=0,
1351+ help='Number of retries.')
1352+
1353 # FIXME(dtroyer): The args below are here for diablo compatibility,
1354 # remove them in folsum cycle
1355
1356@@ -301,8 +314,6 @@
1357 logger.setLevel(logging.DEBUG)
1358 logger.addHandler(streamhandler)
1359
1360- httplib2.debuglevel = 1
1361-
1362 def main(self, argv):
1363 # Parse args once to find version and debug settings
1364 parser = self.get_base_parser()
1365@@ -318,7 +329,7 @@
1366 options.os_volume_api_version)
1367 self.parser = subcommand_parser
1368
1369- if options.help and len(args) == 0:
1370+ if options.help or not argv:
1371 subcommand_parser.print_help()
1372 return 0
1373
1374@@ -336,14 +347,14 @@
1375 (os_username, os_password, os_tenant_name, os_auth_url,
1376 os_region_name, endpoint_type, insecure,
1377 service_type, service_name, volume_service_name,
1378- username, apikey, projectid, url, region_name) = (
1379+ username, apikey, projectid, url, region_name, cacert) = (
1380 args.os_username, args.os_password,
1381 args.os_tenant_name, args.os_auth_url,
1382 args.os_region_name, args.endpoint_type,
1383 args.insecure, args.service_type, args.service_name,
1384 args.volume_service_name, args.username,
1385 args.apikey, args.projectid,
1386- args.url, args.region_name)
1387+ args.url, args.region_name, args.os_cacert)
1388
1389 if not endpoint_type:
1390 endpoint_type = DEFAULT_CINDER_ENDPOINT_TYPE
1391@@ -408,7 +419,10 @@
1392 extensions=self.extensions,
1393 service_type=service_type,
1394 service_name=service_name,
1395- volume_service_name=volume_service_name)
1396+ volume_service_name=volume_service_name,
1397+ retries=options.retries,
1398+ http_log_debug=args.debug,
1399+ cacert=cacert)
1400
1401 try:
1402 if not utils.isunauthenticated(args.func):
1403
1404=== modified file 'cinderclient/utils.py'
1405--- cinderclient/utils.py 2012-06-27 13:32:14 +0000
1406+++ cinderclient/utils.py 2013-01-10 22:15:27 +0000
1407@@ -1,3 +1,4 @@
1408+import locale
1409 import os
1410 import re
1411 import sys
1412@@ -179,12 +180,19 @@
1413 return manager.find(name=name_or_id)
1414 except exceptions.NotFound:
1415 try:
1416- # Volumes does not have name, but display_name
1417- return manager.find(display_name=name_or_id)
1418- except exceptions.NotFound:
1419- msg = "No %s with a name or ID of '%s' exists." % \
1420- (manager.resource_class.__name__.lower(), name_or_id)
1421- raise exceptions.CommandError(msg)
1422+ # For command-line arguments that are in Unicode
1423+ encoding = (locale.getpreferredencoding() or
1424+ sys.stdin.encoding or
1425+ 'UTF-8')
1426+ return manager.find(display_name=(name_or_id.decode(encoding)))
1427+ except (UnicodeDecodeError, exceptions.NotFound):
1428+ try:
1429+ # Volumes does not have name, but display_name
1430+ return manager.find(display_name=name_or_id)
1431+ except exceptions.NotFound:
1432+ msg = "No %s with a name or ID of '%s' exists." % \
1433+ (manager.resource_class.__name__.lower(), name_or_id)
1434+ raise exceptions.CommandError(msg)
1435 except exceptions.NoUniqueMatch:
1436 msg = ("Multiple %s matches found for '%s', use an ID to be more"
1437 " specific." % (manager.resource_class.__name__.lower(),
1438
1439=== modified file 'cinderclient/v1/client.py'
1440--- cinderclient/v1/client.py 2012-10-01 12:05:26 +0000
1441+++ cinderclient/v1/client.py 2013-01-10 22:15:27 +0000
1442@@ -17,9 +17,7 @@
1443
1444 Then call methods on its managers::
1445
1446- >>> client.servers.list()
1447- ...
1448- >>> client.flavors.list()
1449+ >>> client.volumes.list()
1450 ...
1451
1452 """
1453@@ -29,7 +27,9 @@
1454 proxy_tenant_id=None, proxy_token=None, region_name=None,
1455 endpoint_type='publicURL', extensions=None,
1456 service_type='volume', service_name=None,
1457- volume_service_name=None):
1458+ volume_service_name=None, retries=None,
1459+ http_log_debug=False,
1460+ cacert=None):
1461 # FIXME(comstud): Rename the api_key argument above when we
1462 # know it's not being used as keyword argument
1463 password = api_key
1464@@ -63,7 +63,10 @@
1465 endpoint_type=endpoint_type,
1466 service_type=service_type,
1467 service_name=service_name,
1468- volume_service_name=volume_service_name)
1469+ volume_service_name=volume_service_name,
1470+ retries=retries,
1471+ http_log_debug=http_log_debug,
1472+ cacert=cacert)
1473
1474 def authenticate(self):
1475 """
1476
1477=== added file 'cinderclient/v1/contrib/list_extensions.py'
1478--- cinderclient/v1/contrib/list_extensions.py 1970-01-01 00:00:00 +0000
1479+++ cinderclient/v1/contrib/list_extensions.py 2013-01-10 22:15:27 +0000
1480@@ -0,0 +1,47 @@
1481+# Copyright 2011 OpenStack LLC.
1482+# All Rights Reserved.
1483+#
1484+# Licensed under the Apache License, Version 2.0 (the "License"); you may
1485+# not use this file except in compliance with the License. You may obtain
1486+# a copy of the License at
1487+#
1488+# http://www.apache.org/licenses/LICENSE-2.0
1489+#
1490+# Unless required by applicable law or agreed to in writing, software
1491+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
1492+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
1493+# License for the specific language governing permissions and limitations
1494+# under the License.
1495+
1496+from cinderclient import base
1497+from cinderclient import utils
1498+
1499+
1500+class ListExtResource(base.Resource):
1501+ @property
1502+ def summary(self):
1503+ descr = self.description.strip()
1504+ if not descr:
1505+ return '??'
1506+ lines = descr.split("\n")
1507+ if len(lines) == 1:
1508+ return lines[0]
1509+ else:
1510+ return lines[0] + "..."
1511+
1512+
1513+class ListExtManager(base.Manager):
1514+ resource_class = ListExtResource
1515+
1516+ def show_all(self):
1517+ return self._list("/extensions", 'extensions')
1518+
1519+
1520+@utils.service_type('volume')
1521+def do_list_extensions(client, _args):
1522+ """
1523+ List all the os-api extensions that are available.
1524+ """
1525+ extensions = client.list_extensions.show_all()
1526+ fields = ["Name", "Summary", "Alias", "Updated"]
1527+ utils.print_list(extensions, fields)
1528
1529=== modified file 'cinderclient/v1/shell.py'
1530--- cinderclient/v1/shell.py 2012-10-01 12:05:26 +0000
1531+++ cinderclient/v1/shell.py 2013-01-10 22:15:27 +0000
1532@@ -20,6 +20,7 @@
1533 import sys
1534 import time
1535
1536+from cinderclient import exceptions
1537 from cinderclient import utils
1538
1539
1540@@ -91,14 +92,17 @@
1541 setattr(item, to_key, item._info[from_key])
1542
1543
1544-def _extract_metadata(arg_list):
1545+def _extract_metadata(args):
1546 metadata = {}
1547- for metadatum in arg_list:
1548- assert(metadatum.find('=') > -1), "Improperly formatted metadata "\
1549- "input (%s)" % metadatum
1550- (key, value) = metadatum.split('=', 1)
1551+ for metadatum in args.metadata[0]:
1552+ # unset doesn't require a val, so we have the if/else
1553+ if '=' in metadatum:
1554+ (key, value) = metadatum.split('=', 1)
1555+ else:
1556+ key = metadatum
1557+ value = None
1558+
1559 metadata[key] = value
1560-
1561 return metadata
1562
1563
1564@@ -144,7 +148,7 @@
1565 servers = [s.get('server_id') for s in vol.attachments]
1566 setattr(vol, 'attached_to', ','.join(map(str, servers)))
1567 utils.print_list(volumes, ['ID', 'Status', 'Display Name',
1568- 'Size', 'Volume Type', 'Attached to'])
1569+ 'Size', 'Volume Type', 'Bootable', 'Attached to'])
1570
1571
1572 @utils.arg('volume', metavar='<volume>', help='ID of the volume.')
1573@@ -168,6 +172,14 @@
1574 '--snapshot_id',
1575 help=argparse.SUPPRESS)
1576 @utils.arg(
1577+ '--source-volid',
1578+ metavar='<source-volid>',
1579+ default=None,
1580+ help='Create volume from volume id (Optional, Default=None)')
1581+@utils.arg(
1582+ '--source_volid',
1583+ help=argparse.SUPPRESS)
1584+@utils.arg(
1585 '--image-id',
1586 metavar='<image-id>',
1587 default=None,
1588@@ -219,10 +231,11 @@
1589
1590 volume_metadata = None
1591 if args.metadata is not None:
1592- volume_metadata = _extract_metadata(args.metadata)
1593+ volume_metadata = _extract_metadata(args)
1594
1595 volume = cs.volumes.create(args.size,
1596 args.snapshot_id,
1597+ args.source_volid,
1598 args.display_name,
1599 args.display_description,
1600 args.volume_type,
1601@@ -240,6 +253,48 @@
1602 volume.delete()
1603
1604
1605+@utils.arg('volume', metavar='<volume>', help='ID of the volume to rename.')
1606+@utils.arg('display_name', nargs='?', metavar='<display-name>',
1607+ help='New display-name for the volume.')
1608+@utils.arg('--display-description', metavar='<display-description>',
1609+ help='Optional volume description. (Default=None)',
1610+ default=None)
1611+@utils.service_type('volume')
1612+def do_rename(cs, args):
1613+ """Rename a volume."""
1614+ kwargs = {}
1615+ if args.display_name is not None:
1616+ kwargs['display_name'] = args.display_name
1617+ if args.display_description is not None:
1618+ kwargs['display_description'] = args.display_description
1619+ _find_volume(cs, args.volume).update(**kwargs)
1620+
1621+
1622+@utils.arg('volume',
1623+ metavar='<volume>',
1624+ help='ID of the volume to update metadata on.')
1625+@utils.arg('action',
1626+ metavar='<action>',
1627+ choices=['set', 'unset'],
1628+ help="Actions: 'set' or 'unset'")
1629+@utils.arg('metadata',
1630+ metavar='<key=value>',
1631+ nargs='+',
1632+ action='append',
1633+ default=[],
1634+ help='Metadata to set/unset (only key is necessary on unset)')
1635+@utils.service_type('volume')
1636+def do_metadata(cs, args):
1637+ """Set or Delete metadata on a volume."""
1638+ volume = _find_volume(cs, args.volume)
1639+ metadata = _extract_metadata(args)
1640+
1641+ if args.action == 'set':
1642+ cs.volumes.set_metadata(volume, metadata)
1643+ elif args.action == 'unset':
1644+ cs.volumes.delete_metadata(volume, metadata.keys())
1645+
1646+
1647 @utils.arg(
1648 '--all-tenants',
1649 dest='all_tenants',
1650@@ -340,10 +395,32 @@
1651 snapshot.delete()
1652
1653
1654+@utils.arg('snapshot', metavar='<snapshot>', help='ID of the snapshot.')
1655+@utils.arg('display_name', nargs='?', metavar='<display-name>',
1656+ help='New display-name for the snapshot.')
1657+@utils.arg('--display-description', metavar='<display-description>',
1658+ help='Optional snapshot description. (Default=None)',
1659+ default=None)
1660+@utils.service_type('volume')
1661+def do_snapshot_rename(cs, args):
1662+ """Rename a snapshot."""
1663+ kwargs = {}
1664+ if args.display_name is not None:
1665+ kwargs['display_name'] = args.display_name
1666+ if args.display_description is not None:
1667+ kwargs['display_description'] = args.display_description
1668+ _find_volume_snapshot(cs, args.snapshot).update(**kwargs)
1669+
1670+
1671 def _print_volume_type_list(vtypes):
1672 utils.print_list(vtypes, ['ID', 'Name'])
1673
1674
1675+def _print_type_and_extra_specs_list(vtypes):
1676+ formatters = {'extra_specs': _print_type_extra_specs}
1677+ utils.print_list(vtypes, ['ID', 'Name', 'extra_specs'], formatters)
1678+
1679+
1680 @utils.service_type('volume')
1681 def do_type_list(cs, args):
1682 """Print a list of available 'volume types'."""
1683@@ -351,9 +428,16 @@
1684 _print_volume_type_list(vtypes)
1685
1686
1687+@utils.service_type('volume')
1688+def do_extra_specs_list(cs, args):
1689+ """Print a list of current 'volume types and extra specs' (Admin Only)."""
1690+ vtypes = cs.volume_types.list()
1691+ _print_type_and_extra_specs_list(vtypes)
1692+
1693+
1694 @utils.arg('name',
1695 metavar='<name>',
1696- help="Name of the new flavor")
1697+ help="Name of the new volume type")
1698 @utils.service_type('volume')
1699 def do_type_create(cs, args):
1700 """Create a new volume type."""
1701@@ -366,10 +450,35 @@
1702 help="Unique ID of the volume type to delete")
1703 @utils.service_type('volume')
1704 def do_type_delete(cs, args):
1705- """Delete a specific flavor"""
1706+ """Delete a specific volume type"""
1707 cs.volume_types.delete(args.id)
1708
1709
1710+@utils.arg('vtype',
1711+ metavar='<vtype>',
1712+ help="Name or ID of the volume type")
1713+@utils.arg('action',
1714+ metavar='<action>',
1715+ choices=['set', 'unset'],
1716+ help="Actions: 'set' or 'unset'")
1717+@utils.arg('metadata',
1718+ metavar='<key=value>',
1719+ nargs='+',
1720+ action='append',
1721+ default=[],
1722+ help='Extra_specs to set/unset (only key is necessary on unset)')
1723+@utils.service_type('volume')
1724+def do_type_key(cs, args):
1725+ "Set or unset extra_spec for a volume type."""
1726+ vtype = _find_volume_type(cs, args.vtype)
1727+ keypair = _extract_metadata(args)
1728+
1729+ if args.action == 'set':
1730+ vtype.set_keys(keypair)
1731+ elif args.action == 'unset':
1732+ vtype.unset_keys(keypair.keys())
1733+
1734+
1735 def do_endpoints(cs, args):
1736 """Discover endpoints that get returned from the authenticate services"""
1737 catalog = cs.client.service_catalog.catalog
1738@@ -479,3 +588,15 @@
1739 limits = cs.limits.get().rate
1740 columns = ['Verb', 'URI', 'Value', 'Remain', 'Unit', 'Next_Available']
1741 utils.print_list(limits, columns)
1742+
1743+
1744+def _print_type_extra_specs(vol_type):
1745+ try:
1746+ return vol_type.get_keys()
1747+ except exceptions.NotFound:
1748+ return "N/A"
1749+
1750+
1751+def _find_volume_type(cs, vtype):
1752+ """Get a volume type by name or ID."""
1753+ return utils.find_resource(cs.volume_types, vtype)
1754
1755=== modified file 'cinderclient/v1/volume_snapshots.py'
1756--- cinderclient/v1/volume_snapshots.py 2012-10-01 12:05:26 +0000
1757+++ cinderclient/v1/volume_snapshots.py 2013-01-10 22:15:27 +0000
1758@@ -34,6 +34,12 @@
1759 """
1760 self.manager.delete(self)
1761
1762+ def update(self, **kwargs):
1763+ """
1764+ Update the display_name or display_description for this snapshot.
1765+ """
1766+ self.manager.update(self, **kwargs)
1767+
1768 @property
1769 def progress(self):
1770 return self._info.get('os-extended-snapshot-attributes:progress')
1771@@ -109,3 +115,16 @@
1772 :param snapshot: The :class:`Snapshot` to delete.
1773 """
1774 self._delete("/snapshots/%s" % base.getid(snapshot))
1775+
1776+ def update(self, snapshot, **kwargs):
1777+ """
1778+ Update the display_name or display_description for a snapshot.
1779+
1780+ :param snapshot: The :class:`Snapshot` to delete.
1781+ """
1782+ if not kwargs:
1783+ return
1784+
1785+ body = {"snapshot": kwargs}
1786+
1787+ self._update("/snapshots/%s" % base.getid(snapshot), body)
1788
1789=== modified file 'cinderclient/v1/volume_types.py'
1790--- cinderclient/v1/volume_types.py 2012-06-27 13:32:14 +0000
1791+++ cinderclient/v1/volume_types.py 2013-01-10 22:15:27 +0000
1792@@ -26,7 +26,52 @@
1793 A Volume Type is the type of volume to be created
1794 """
1795 def __repr__(self):
1796- return "<Volume Type: %s>" % self.name
1797+ return "<VolumeType: %s>" % self.name
1798+
1799+ def get_keys(self):
1800+ """
1801+ Get extra specs from a volume type.
1802+
1803+ :param vol_type: The :class:`VolumeType` to get extra specs from
1804+ """
1805+ _resp, body = self.manager.api.client.get(
1806+ "/types/%s/extra_specs" %
1807+ base.getid(self))
1808+ return body["extra_specs"]
1809+
1810+ def set_keys(self, metadata):
1811+ """
1812+ Set extra specs on a volume type.
1813+
1814+ :param type : The :class:`VolumeType` to set extra spec on
1815+ :param metadata: A dict of key/value pairs to be set
1816+ """
1817+ body = {'extra_specs': metadata}
1818+ return self.manager._create(
1819+ "/types/%s/extra_specs" % base.getid(self),
1820+ body,
1821+ "extra_specs",
1822+ return_raw=True)
1823+
1824+ def unset_keys(self, keys):
1825+ """
1826+ Unset extra specs on a volue type.
1827+
1828+ :param type_id: The :class:`VolumeType` to unset extra spec on
1829+ :param keys: A list of keys to be unset
1830+ """
1831+
1832+ # NOTE(jdg): This wasn't actually doing all of the keys before
1833+ # the return in the loop resulted in ony ONE key being unset.
1834+ # since on success the return was NONE, we'll only interrupt the loop
1835+ # and return if there's an error
1836+ result = None
1837+ for k in keys:
1838+ resp = self.manager._delete(
1839+ "/types/%s/extra_specs/%s" % (
1840+ base.getid(self), k))
1841+ if resp is not None:
1842+ return resp
1843
1844
1845 class VolumeTypeManager(base.ManagerWithFind):
1846
1847=== modified file 'cinderclient/v1/volumes.py'
1848--- cinderclient/v1/volumes.py 2012-10-01 12:05:26 +0000
1849+++ cinderclient/v1/volumes.py 2013-01-10 22:15:27 +0000
1850@@ -34,6 +34,12 @@
1851 """
1852 self.manager.delete(self)
1853
1854+ def update(self, **kwargs):
1855+ """
1856+ Update the display_name or display_description for this volume.
1857+ """
1858+ self.manager.update(self, **kwargs)
1859+
1860 def attach(self, instance_uuid, mountpoint):
1861 """
1862 Set attachment metadata.
1863@@ -89,6 +95,15 @@
1864 """
1865 return self.manager.terminate_connection(self, connector)
1866
1867+ def set_metadata(self, volume, metadata):
1868+ """
1869+ Set or Append metadata to a volume.
1870+
1871+ :param type : The :class: `Volume` to set metadata on
1872+ :param metadata: A dict of key/value pairs to set
1873+ """
1874+ return self.manager.set_metadata(self, metadata)
1875+
1876
1877 class VolumeManager(base.ManagerWithFind):
1878 """
1879@@ -96,7 +111,7 @@
1880 """
1881 resource_class = Volume
1882
1883- def create(self, size, snapshot_id=None,
1884+ def create(self, size, snapshot_id=None, source_volid=None,
1885 display_name=None, display_description=None,
1886 volume_type=None, user_id=None,
1887 project_id=None, availability_zone=None,
1888@@ -115,6 +130,7 @@
1889 :param availability_zone: Availability Zone to use
1890 :param metadata: Optional metadata to set on volume creation
1891 :param imageRef: reference to an image stored in glance
1892+ :param source_volid: ID of source volume to clone from
1893 """
1894
1895 if metadata is None:
1896@@ -134,6 +150,7 @@
1897 'attach_status': "detached",
1898 'metadata': volume_metadata,
1899 'imageRef': imageRef,
1900+ 'source_volid': source_volid,
1901 }}
1902 return self._create('/volumes', body, 'volume')
1903
1904@@ -178,51 +195,18 @@
1905 """
1906 self._delete("/volumes/%s" % base.getid(volume))
1907
1908- def create_server_volume(self, server_id, volume_id, device):
1909- """
1910- Attach a volume identified by the volume ID to the given server ID
1911-
1912- :param server_id: The ID of the server
1913- :param volume_id: The ID of the volume to attach.
1914- :param device: The device name
1915- :rtype: :class:`Volume`
1916- """
1917- body = {'volumeAttachment': {'volumeId': volume_id,
1918- 'device': device}}
1919- return self._create("/servers/%s/os-volume_attachments" % server_id,
1920- body, "volumeAttachment")
1921-
1922- def get_server_volume(self, server_id, attachment_id):
1923- """
1924- Get the volume identified by the attachment ID, that is attached to
1925- the given server ID
1926-
1927- :param server_id: The ID of the server
1928- :param attachment_id: The ID of the attachment
1929- :rtype: :class:`Volume`
1930- """
1931- return self._get("/servers/%s/os-volume_attachments/%s" % (server_id,
1932- attachment_id,), "volumeAttachment")
1933-
1934- def get_server_volumes(self, server_id):
1935- """
1936- Get a list of all the attached volumes for the given server ID
1937-
1938- :param server_id: The ID of the server
1939- :rtype: list of :class:`Volume`
1940- """
1941- return self._list("/servers/%s/os-volume_attachments" % server_id,
1942- "volumeAttachments")
1943-
1944- def delete_server_volume(self, server_id, attachment_id):
1945- """
1946- Detach a volume identified by the attachment ID from the given server
1947-
1948- :param server_id: The ID of the server
1949- :param attachment_id: The ID of the attachment
1950- """
1951- self._delete("/servers/%s/os-volume_attachments/%s" %
1952- (server_id, attachment_id,))
1953+ def update(self, volume, **kwargs):
1954+ """
1955+ Update the display_name or display_description for a volume.
1956+
1957+ :param volume: The :class:`Volume` to delete.
1958+ """
1959+ if not kwargs:
1960+ return
1961+
1962+ body = {"volume": kwargs}
1963+
1964+ self._update("/volumes/%s" % base.getid(volume), body)
1965
1966 def _action(self, action, volume, info=None, **kwargs):
1967 """
1968@@ -311,3 +295,24 @@
1969 """
1970 self._action('os-terminate_connection', volume,
1971 {'connector': connector})
1972+
1973+ def set_metadata(self, volume, metadata):
1974+ """
1975+ Update/Set a volumes metadata.
1976+
1977+ :param volume: The :class:`Volume`.
1978+ :param metadata: A list of keys to be set.
1979+ """
1980+ body = {'metadata': metadata}
1981+ return self._create("/volumes/%s/metadata" % base.getid(volume),
1982+ body, "metadata")
1983+
1984+ def delete_metadata(self, volume, keys):
1985+ """
1986+ Delete specified keys from volumes metadata.
1987+
1988+ :param volume: The :class:`Volume`.
1989+ :param metadata: A list of keys to be removed.
1990+ """
1991+ for k in keys:
1992+ self._delete("/volumes/%s/metadata/%s" % (base.getid(volume), k))
1993
1994=== modified file 'cinderclient/versioninfo'
1995--- cinderclient/versioninfo 2012-10-01 12:05:26 +0000
1996+++ cinderclient/versioninfo 2013-01-10 22:15:27 +0000
1997@@ -1,1 +1,1 @@
1998-1.0.0
1999+1.0.2.3.g61e2a42
2000
2001=== modified file 'debian/changelog'
2002--- debian/changelog 2012-10-01 12:11:12 +0000
2003+++ debian/changelog 2013-01-10 22:15:27 +0000
2004@@ -1,3 +1,25 @@
2005+python-cinderclient (1:1.0.2.3.g61e2a42-0ubuntu1~cloud0) precise-grizzly; urgency=low
2006+
2007+ * New upstream release for the Ubuntu Cloud Archive.
2008+
2009+ -- Adam Gandelman <adamg@ubuntu.com> Thu, 10 Jan 2013 14:13:47 -0800
2010+
2011+python-cinderclient (1:1.0.2.3.g61e2a42-0ubuntu1) raring; urgency=low
2012+
2013+ [ Adam Gandelman ]
2014+ * New upstream release.
2015+
2016+ [ Chuck Short ]
2017+ * New upstream release.
2018+ * debian/control: Add python-requests as a build dep
2019+ * debian/control: Add python-testtools and python-fixtures as a build dep.
2020+
2021+ [ Yolanda Robla ]
2022+ * debian/control: add python-setuptools-git dependency
2023+ * debian/copyright: Removed remaining dh boilerplate template.
2024+
2025+ -- Adam Gandelman <adamg@ubuntu.com> Thu, 10 Jan 2013 11:35:58 -0800
2026+
2027 python-cinderclient (1:1.0.0-0ubuntu1~cloud0) precise-folsom; urgency=low
2028
2029 * New upstream release for the Ubuntu Cloud Archive.
2030
2031=== modified file 'debian/control'
2032--- debian/control 2012-10-01 12:05:26 +0000
2033+++ debian/control 2013-01-10 22:15:27 +0000
2034@@ -6,19 +6,23 @@
2035 python-setuptools,
2036 python-nose,
2037 python-httplib2,
2038+ python-requests,
2039 python-prettytable,
2040 python-simplejson,
2041 python-mock,
2042 pep8,
2043 python-sphinx,
2044- python-unittest2
2045+ python-unittest2,
2046+ python-setuptools-git (>= 0.4),
2047+ python-testtools,
2048+ python-fixtures
2049 Standards-Version: 3.9.3
2050 X-Python-Version: >= 2.5
2051
2052 Package: python-cinderclient
2053 Architecture: all
2054 Depends: ${python:Depends}, ${misc:Depends}, python-httplib2,
2055- python-prettytable, python-simplejson
2056+ python-prettytable, python-requests, python-simplejson
2057 Description: python bindings to the OpenStack Volume API
2058 This is a client for the OpenStack Volume API. There's a Python API (the
2059 ``cinderclient`` module), and a command-line script (``cinder``). Each
2060
2061=== modified file 'debian/copyright'
2062--- debian/copyright 2012-06-27 13:32:14 +0000
2063+++ debian/copyright 2013-01-10 22:15:27 +0000
2064@@ -41,8 +41,3 @@
2065 .
2066 On Debian systems, the complete text of the GNU General
2067 Public License version 2 can be found in "/usr/share/common-licenses/GPL-2".
2068-
2069-# Please also look if there are files or directories which have a
2070-# different copyright/license attached and list them here.
2071-# Please avoid to pick license terms that are more restrictive than the
2072-# packaged work, as it may make Debian's contributions unacceptable upstream.
2073
2074=== added file 'openstack-common.conf'
2075--- openstack-common.conf 1970-01-01 00:00:00 +0000
2076+++ openstack-common.conf 2013-01-10 22:15:27 +0000
2077@@ -0,0 +1,7 @@
2078+[DEFAULT]
2079+
2080+# The list of modules to copy from openstack-common
2081+modules=setup,version
2082+
2083+# The base module to hold the copy of openstack.common
2084+base=cinderclient
2085
2086=== modified file 'python_cinderclient.egg-info/PKG-INFO'
2087--- python_cinderclient.egg-info/PKG-INFO 2012-10-01 12:05:26 +0000
2088+++ python_cinderclient.egg-info/PKG-INFO 2013-01-10 22:15:27 +0000
2089@@ -1,6 +1,6 @@
2090 Metadata-Version: 1.1
2091 Name: python-cinderclient
2092-Version: 1.0.0
2093+Version: 1.0.2.3.g61e2a42
2094 Summary: Client library for OpenStack Cinder API.
2095 Home-page: https://github.com/openstack/python-cinderclient
2096 Author: Rackspace, based on work by Jacob Kaplan-Moss
2097@@ -161,6 +161,7 @@
2098 Platform: UNKNOWN
2099 Classifier: Development Status :: 5 - Production/Stable
2100 Classifier: Environment :: Console
2101+Classifier: Environment :: OpenStack
2102 Classifier: Intended Audience :: Developers
2103 Classifier: Intended Audience :: Information Technology
2104 Classifier: License :: OSI Approved :: Apache Software License
2105
2106=== modified file 'python_cinderclient.egg-info/SOURCES.txt'
2107--- python_cinderclient.egg-info/SOURCES.txt 2012-10-01 12:05:26 +0000
2108+++ python_cinderclient.egg-info/SOURCES.txt 2013-01-10 22:15:27 +0000
2109@@ -1,5 +1,3 @@
2110-.gitignore
2111-.gitreview
2112 .mailmap
2113 AUTHORS
2114 ChangeLog
2115@@ -7,6 +5,7 @@
2116 LICENSE
2117 MANIFEST.in
2118 README.rst
2119+openstack-common.conf
2120 run_tests.sh
2121 setup.cfg
2122 setup.py
2123@@ -23,6 +22,7 @@
2124 cinderclient/openstack/__init__.py
2125 cinderclient/openstack/common/__init__.py
2126 cinderclient/openstack/common/setup.py
2127+cinderclient/openstack/common/version.py
2128 cinderclient/v1/__init__.py
2129 cinderclient/v1/client.py
2130 cinderclient/v1/limits.py
2131@@ -33,6 +33,7 @@
2132 cinderclient/v1/volume_types.py
2133 cinderclient/v1/volumes.py
2134 cinderclient/v1/contrib/__init__.py
2135+cinderclient/v1/contrib/list_extensions.py
2136 doc/.gitignore
2137 doc/Makefile
2138 doc/source/api.rst
2139@@ -68,9 +69,12 @@
2140 tests/v1/test_quota_classes.py
2141 tests/v1/test_quotas.py
2142 tests/v1/test_shell.py
2143+tests/v1/test_types.py
2144 tests/v1/test_volumes.py
2145 tests/v1/testfile.txt
2146 tests/v1/utils.py
2147+tests/v1/contrib/__init__.py
2148+tests/v1/contrib/test_list_extensions.py
2149 tools/cinder.bash_completion
2150 tools/generate_authors.sh
2151 tools/install_venv.py
2152
2153=== modified file 'python_cinderclient.egg-info/requires.txt'
2154--- python_cinderclient.egg-info/requires.txt 2012-08-16 12:03:50 +0000
2155+++ python_cinderclient.egg-info/requires.txt 2013-01-10 22:15:27 +0000
2156@@ -1,3 +1,3 @@
2157-httplib2
2158 prettytable
2159+requests<1.0
2160 simplejson
2161\ No newline at end of file
2162
2163=== modified file 'setup.py'
2164--- setup.py 2012-09-07 11:35:32 +0000
2165+++ setup.py 2013-01-10 22:15:27 +0000
2166@@ -21,6 +21,7 @@
2167
2168 requires = setup.parse_requirements()
2169 depend_links = setup.parse_dependency_links()
2170+tests_require = setup.parse_requirements(['tools/test-requires'])
2171
2172
2173 def read_file(file_name):
2174@@ -39,12 +40,14 @@
2175 packages=setuptools.find_packages(exclude=['tests', 'tests.*']),
2176 cmdclass=setup.get_cmdclass(),
2177 install_requires=requires,
2178+ tests_require=tests_require,
2179+ setup_requires=['setuptools-git>=0.4'],
2180 dependency_links=depend_links,
2181- tests_require=["nose", "mock"],
2182 test_suite="nose.collector",
2183 classifiers=[
2184 "Development Status :: 5 - Production/Stable",
2185 "Environment :: Console",
2186+ "Environment :: OpenStack",
2187 "Intended Audience :: Developers",
2188 "Intended Audience :: Information Technology",
2189 "License :: OSI Approved :: Apache Software License",
2190
2191=== modified file 'tests/test_client.py'
2192--- tests/test_client.py 2012-06-27 13:32:14 +0000
2193+++ tests/test_client.py 2013-01-10 22:15:27 +0000
2194@@ -6,9 +6,6 @@
2195
2196 class ClientTest(utils.TestCase):
2197
2198- def setUp(self):
2199- pass
2200-
2201 def test_get_client_class_v1(self):
2202 output = cinderclient.client.get_client_class('1')
2203 self.assertEqual(output, cinderclient.v1.client.Client)
2204
2205=== modified file 'tests/test_http.py'
2206--- tests/test_http.py 2012-06-27 13:32:14 +0000
2207+++ tests/test_http.py 2013-01-10 22:15:27 +0000
2208@@ -1,24 +1,45 @@
2209-import httplib2
2210 import mock
2211
2212+import requests
2213+
2214 from cinderclient import client
2215 from cinderclient import exceptions
2216 from tests import utils
2217
2218
2219-fake_response = httplib2.Response({"status": 200})
2220-fake_body = '{"hi": "there"}'
2221-mock_request = mock.Mock(return_value=(fake_response, fake_body))
2222-
2223-
2224-def get_client():
2225+fake_response = utils.TestResponse({
2226+ "status_code": 200,
2227+ "text": '{"hi": "there"}',
2228+})
2229+mock_request = mock.Mock(return_value=(fake_response))
2230+
2231+bad_400_response = utils.TestResponse({
2232+ "status_code": 400,
2233+ "text": '{"error": {"message": "n/a", "details": "Terrible!"}}',
2234+})
2235+bad_400_request = mock.Mock(return_value=(bad_400_response))
2236+
2237+bad_401_response = utils.TestResponse({
2238+ "status_code": 401,
2239+ "text": '{"error": {"message": "FAILED!", "details": "DETAILS!"}}',
2240+})
2241+bad_401_request = mock.Mock(return_value=(bad_401_response))
2242+
2243+bad_500_response = utils.TestResponse({
2244+ "status_code": 500,
2245+ "text": '{"error": {"message": "FAILED!", "details": "DETAILS!"}}',
2246+})
2247+bad_500_request = mock.Mock(return_value=(bad_500_response))
2248+
2249+
2250+def get_client(retries=0):
2251 cl = client.HTTPClient("username", "password",
2252- "project_id", "auth_test")
2253+ "project_id", "auth_test", retries=retries)
2254 return cl
2255
2256
2257-def get_authed_client():
2258- cl = get_client()
2259+def get_authed_client(retries=0):
2260+ cl = get_client(retries=retries)
2261 cl.management_url = "http://example.com"
2262 cl.auth_token = "token"
2263 return cl
2264@@ -29,7 +50,7 @@
2265 def test_get(self):
2266 cl = get_authed_client()
2267
2268- @mock.patch.object(httplib2.Http, "request", mock_request)
2269+ @mock.patch.object(requests, "request", mock_request)
2270 @mock.patch('time.time', mock.Mock(return_value=1234))
2271 def test_get_call():
2272 resp, body = cl.get("/hi")
2273@@ -37,17 +58,110 @@
2274 "X-Auth-Project-Id": "project_id",
2275 "User-Agent": cl.USER_AGENT,
2276 'Accept': 'application/json', }
2277- mock_request.assert_called_with("http://example.com/hi",
2278- "GET", headers=headers)
2279+ mock_request.assert_called_with(
2280+ "GET",
2281+ "http://example.com/hi",
2282+ headers=headers,
2283+ **self.TEST_REQUEST_BASE)
2284 # Automatic JSON parsing
2285 self.assertEqual(body, {"hi": "there"})
2286
2287 test_get_call()
2288
2289+ def test_get_reauth_0_retries(self):
2290+ cl = get_authed_client(retries=0)
2291+
2292+ self.requests = [bad_401_request, mock_request]
2293+
2294+ def request(*args, **kwargs):
2295+ next_request = self.requests.pop(0)
2296+ return next_request(*args, **kwargs)
2297+
2298+ def reauth():
2299+ cl.management_url = "http://example.com"
2300+ cl.auth_token = "token"
2301+
2302+ @mock.patch.object(cl, 'authenticate', reauth)
2303+ @mock.patch.object(requests, "request", request)
2304+ @mock.patch('time.time', mock.Mock(return_value=1234))
2305+ def test_get_call():
2306+ resp, body = cl.get("/hi")
2307+
2308+ test_get_call()
2309+ self.assertEqual(self.requests, [])
2310+
2311+ def test_get_retry_500(self):
2312+ cl = get_authed_client(retries=1)
2313+
2314+ self.requests = [bad_500_request, mock_request]
2315+
2316+ def request(*args, **kwargs):
2317+ next_request = self.requests.pop(0)
2318+ return next_request(*args, **kwargs)
2319+
2320+ @mock.patch.object(requests, "request", request)
2321+ @mock.patch('time.time', mock.Mock(return_value=1234))
2322+ def test_get_call():
2323+ resp, body = cl.get("/hi")
2324+
2325+ test_get_call()
2326+ self.assertEqual(self.requests, [])
2327+
2328+ def test_retry_limit(self):
2329+ cl = get_authed_client(retries=1)
2330+
2331+ self.requests = [bad_500_request, bad_500_request, mock_request]
2332+
2333+ def request(*args, **kwargs):
2334+ next_request = self.requests.pop(0)
2335+ return next_request(*args, **kwargs)
2336+
2337+ @mock.patch.object(requests, "request", request)
2338+ @mock.patch('time.time', mock.Mock(return_value=1234))
2339+ def test_get_call():
2340+ resp, body = cl.get("/hi")
2341+
2342+ self.assertRaises(exceptions.ClientException, test_get_call)
2343+ self.assertEqual(self.requests, [mock_request])
2344+
2345+ def test_get_no_retry_400(self):
2346+ cl = get_authed_client(retries=0)
2347+
2348+ self.requests = [bad_400_request, mock_request]
2349+
2350+ def request(*args, **kwargs):
2351+ next_request = self.requests.pop(0)
2352+ return next_request(*args, **kwargs)
2353+
2354+ @mock.patch.object(requests, "request", request)
2355+ @mock.patch('time.time', mock.Mock(return_value=1234))
2356+ def test_get_call():
2357+ resp, body = cl.get("/hi")
2358+
2359+ self.assertRaises(exceptions.BadRequest, test_get_call)
2360+ self.assertEqual(self.requests, [mock_request])
2361+
2362+ def test_get_retry_400_socket(self):
2363+ cl = get_authed_client(retries=1)
2364+
2365+ self.requests = [bad_400_request, mock_request]
2366+
2367+ def request(*args, **kwargs):
2368+ next_request = self.requests.pop(0)
2369+ return next_request(*args, **kwargs)
2370+
2371+ @mock.patch.object(requests, "request", request)
2372+ @mock.patch('time.time', mock.Mock(return_value=1234))
2373+ def test_get_call():
2374+ resp, body = cl.get("/hi")
2375+
2376+ test_get_call()
2377+ self.assertEqual(self.requests, [])
2378+
2379 def test_post(self):
2380 cl = get_authed_client()
2381
2382- @mock.patch.object(httplib2.Http, "request", mock_request)
2383+ @mock.patch.object(requests, "request", mock_request)
2384 def test_post_call():
2385 cl.post("/hi", body=[1, 2, 3])
2386 headers = {
2387@@ -57,8 +171,12 @@
2388 'Accept': 'application/json',
2389 "User-Agent": cl.USER_AGENT
2390 }
2391- mock_request.assert_called_with("http://example.com/hi", "POST",
2392- headers=headers, body='[1, 2, 3]')
2393+ mock_request.assert_called_with(
2394+ "POST",
2395+ "http://example.com/hi",
2396+ headers=headers,
2397+ data='[1, 2, 3]',
2398+ **self.TEST_REQUEST_BASE)
2399
2400 test_post_call()
2401
2402@@ -66,7 +184,7 @@
2403 cl = get_client()
2404
2405 # response must not have x-server-management-url header
2406- @mock.patch.object(httplib2.Http, "request", mock_request)
2407+ @mock.patch.object(requests, "request", mock_request)
2408 def test_auth_call():
2409 self.assertRaises(exceptions.AuthorizationFailure, cl.authenticate)
2410
2411
2412=== modified file 'tests/test_shell.py'
2413--- tests/test_shell.py 2012-06-27 13:32:14 +0000
2414+++ tests/test_shell.py 2013-01-10 22:15:27 +0000
2415@@ -1,8 +1,11 @@
2416 import cStringIO
2417 import os
2418-import httplib2
2419+import re
2420 import sys
2421
2422+import fixtures
2423+from testtools import matchers
2424+
2425 from cinderclient import exceptions
2426 import cinderclient.shell
2427 from tests import utils
2428@@ -10,16 +13,19 @@
2429
2430 class ShellTest(utils.TestCase):
2431
2432+ FAKE_ENV = {
2433+ 'OS_USERNAME': 'username',
2434+ 'OS_PASSWORD': 'password',
2435+ 'OS_TENANT_NAME': 'tenant_name',
2436+ 'OS_AUTH_URL': 'http://no.where',
2437+ }
2438+
2439 # Patch os.environ to avoid required auth info.
2440 def setUp(self):
2441- global _old_env
2442- fake_env = {
2443- 'OS_USERNAME': 'username',
2444- 'OS_PASSWORD': 'password',
2445- 'OS_TENANT_NAME': 'tenant_name',
2446- 'OS_AUTH_URL': 'http://no.where',
2447- }
2448- _old_env, os.environ = os.environ, fake_env.copy()
2449+ super(ShellTest, self).setUp()
2450+ for var in self.FAKE_ENV:
2451+ self.useFixture(fixtures.EnvironmentVariable(var,
2452+ self.FAKE_ENV[var]))
2453
2454 def shell(self, argstr):
2455 orig = sys.stdout
2456@@ -37,39 +43,26 @@
2457
2458 return out
2459
2460- def tearDown(self):
2461- global _old_env
2462- os.environ = _old_env
2463-
2464 def test_help_unknown_command(self):
2465 self.assertRaises(exceptions.CommandError, self.shell, 'help foofoo')
2466
2467- def test_debug(self):
2468- httplib2.debuglevel = 0
2469- self.shell('--debug help')
2470- assert httplib2.debuglevel == 1
2471-
2472 def test_help(self):
2473 required = [
2474- '^usage: ',
2475- '(?m)^\s+create\s+Add a new volume.',
2476- '(?m)^See "cinder help COMMAND" for help on a specific command',
2477+ '.*?^usage: ',
2478+ '.*?(?m)^\s+create\s+Add a new volume.',
2479+ '.*?(?m)^See "cinder help COMMAND" for help on a specific command',
2480 ]
2481- for argstr in ['--help', 'help']:
2482- help_text = self.shell(argstr)
2483- for r in required:
2484- self.assertRegexpMatches(help_text, r)
2485+ help_text = self.shell('help')
2486+ for r in required:
2487+ self.assertThat(help_text,
2488+ matchers.MatchesRegex(r, re.DOTALL|re.MULTILINE))
2489
2490 def test_help_on_subcommand(self):
2491 required = [
2492- '^usage: cinder list',
2493- '(?m)^List all the volumes.',
2494- ]
2495- argstrings = [
2496- 'list --help',
2497- 'help list',
2498- ]
2499- for argstr in argstrings:
2500- help_text = self.shell(argstr)
2501- for r in required:
2502- self.assertRegexpMatches(help_text, r)
2503+ '.*?^usage: cinder list',
2504+ '.*?(?m)^List all the volumes.',
2505+ ]
2506+ help_text = self.shell('help list')
2507+ for r in required:
2508+ self.assertThat(help_text,
2509+ matchers.MatchesRegex(r, re.DOTALL|re.MULTILINE))
2510
2511=== modified file 'tests/test_utils.py'
2512--- tests/test_utils.py 2012-06-27 13:32:14 +0000
2513+++ tests/test_utils.py 2013-01-10 22:15:27 +0000
2514@@ -45,6 +45,7 @@
2515 class FindResourceTestCase(test_utils.TestCase):
2516
2517 def setUp(self):
2518+ super(FindResourceTestCase, self).setUp()
2519 self.manager = FakeManager(None)
2520
2521 def test_find_none(self):
2522
2523=== modified file 'tests/utils.py'
2524--- tests/utils.py 2012-06-27 13:32:14 +0000
2525+++ tests/utils.py 2013-01-10 22:15:27 +0000
2526@@ -1,5 +1,33 @@
2527-import unittest2
2528-
2529-
2530-class TestCase(unittest2.TestCase):
2531- pass
2532+import testtools
2533+
2534+import requests
2535+
2536+
2537+class TestCase(testtools.TestCase):
2538+ TEST_REQUEST_BASE = {
2539+ 'config': {'danger_mode': False},
2540+ 'verify': True,
2541+ }
2542+
2543+
2544+class TestResponse(requests.Response):
2545+ """ Class used to wrap requests.Response and provide some
2546+ convenience to initialize with a dict """
2547+
2548+ def __init__(self, data):
2549+ self._text = None
2550+ super(TestResponse, self)
2551+ if isinstance(data, dict):
2552+ self.status_code = data.get('status_code', None)
2553+ self.headers = data.get('headers', None)
2554+ # Fake the text attribute to streamline Response creation
2555+ self._text = data.get('text', None)
2556+ else:
2557+ self.status_code = data
2558+
2559+ def __eq__(self, other):
2560+ return self.__dict__ == other.__dict__
2561+
2562+ @property
2563+ def text(self):
2564+ return self._text
2565
2566=== added directory 'tests/v1/contrib'
2567=== added file 'tests/v1/contrib/__init__.py'
2568=== added file 'tests/v1/contrib/test_list_extensions.py'
2569--- tests/v1/contrib/test_list_extensions.py 1970-01-01 00:00:00 +0000
2570+++ tests/v1/contrib/test_list_extensions.py 2013-01-10 22:15:27 +0000
2571@@ -0,0 +1,21 @@
2572+from cinderclient import extension
2573+from cinderclient.v1.contrib import list_extensions
2574+
2575+from tests import utils
2576+from tests.v1 import fakes
2577+
2578+
2579+extensions = [
2580+ extension.Extension(list_extensions.__name__.split(".")[-1],
2581+ list_extensions),
2582+]
2583+cs = fakes.FakeClient(extensions=extensions)
2584+
2585+
2586+class ListExtensionsTests(utils.TestCase):
2587+ def test_list_extensions(self):
2588+ all_exts = cs.list_extensions.show_all()
2589+ cs.assert_called('GET', '/extensions')
2590+ self.assertTrue(len(all_exts) > 0)
2591+ for r in all_exts:
2592+ self.assertTrue(len(r.summary) > 0)
2593
2594=== modified file 'tests/v1/fakes.py'
2595--- tests/v1/fakes.py 2012-10-01 12:05:26 +0000
2596+++ tests/v1/fakes.py 2013-01-10 22:15:27 +0000
2597@@ -13,12 +13,34 @@
2598 # See the License for the specific language governing permissions and
2599 # limitations under the License.
2600
2601-import httplib2
2602 import urlparse
2603
2604 from cinderclient import client as base_client
2605 from cinderclient.v1 import client
2606 from tests import fakes
2607+import tests.utils as utils
2608+
2609+
2610+def _stub_volume(**kwargs):
2611+ volume = {
2612+ 'id': '1234',
2613+ 'display_name': None,
2614+ 'display_description': None,
2615+ "attachments": [],
2616+ "bootable": "false",
2617+ "availability_zone": "cinder",
2618+ "created_at": "2012-08-27T00:00:00.000000",
2619+ "display_description": None,
2620+ "display_name": None,
2621+ "id": '00000000-0000-0000-0000-000000000000',
2622+ "metadata": {},
2623+ "size": 1,
2624+ "snapshot_id": None,
2625+ "status": "available",
2626+ "volume_type": "None",
2627+ }
2628+ volume.update(kwargs)
2629+ return volume
2630
2631
2632 def _stub_snapshot(**kwargs):
2633@@ -39,7 +61,8 @@
2634
2635 def __init__(self, *args, **kwargs):
2636 client.Client.__init__(self, 'username', 'password',
2637- 'project_id', 'auth_url')
2638+ 'project_id', 'auth_url',
2639+ extensions=kwargs.get('extensions'))
2640 self.client = FakeHTTPClient(**kwargs)
2641
2642
2643@@ -74,28 +97,47 @@
2644
2645 # Note the call
2646 self.callstack.append((method, url, kwargs.get('body', None)))
2647+ status, headers, body = getattr(self, callback)(**kwargs)
2648+ r = utils.TestResponse({
2649+ "status_code": status,
2650+ "text": body,
2651+ "headers": headers,
2652+ })
2653+ return r, body
2654
2655- status, body = getattr(self, callback)(**kwargs)
2656 if hasattr(status, 'items'):
2657- return httplib2.Response(status), body
2658+ return utils.TestResponse(status), body
2659 else:
2660- return httplib2.Response({"status": status}), body
2661+ return utils.TestResponse({"status": status}), body
2662
2663 #
2664 # Snapshots
2665 #
2666
2667 def get_snapshots_detail(self, **kw):
2668- return (200, {'snapshots': [
2669+ return (200, {}, {'snapshots': [
2670 _stub_snapshot(),
2671 ]})
2672
2673- #
2674- # volumes
2675- #
2676+ def get_snapshots_1234(self, **kw):
2677+ return (200, {}, {'snapshot': _stub_snapshot(id='1234')})
2678+
2679+ def put_snapshots_1234(self, **kw):
2680+ snapshot = _stub_snapshot(id='1234')
2681+ snapshot.update(kw['body']['snapshot'])
2682+ return (200, {}, {'snapshot': snapshot})
2683+
2684+ #
2685+ # Volumes
2686+ #
2687+
2688+ def put_volumes_1234(self, **kw):
2689+ volume = _stub_volume(id='1234')
2690+ volume.update(kw['body']['volume'])
2691+ return (200, {}, {'volume': volume})
2692
2693 def get_volumes(self, **kw):
2694- return (200, {"volumes": [
2695+ return (200, {}, {"volumes": [
2696 {'id': 1234, 'name': 'sample-volume'},
2697 {'id': 5678, 'name': 'sample-volume2'}
2698 ]})
2699@@ -103,15 +145,15 @@
2700 # TODO(jdg): This will need to change
2701 # at the very least it's not complete
2702 def get_volumes_detail(self, **kw):
2703- return (200, {"volumes": [
2704+ return (200, {}, {"volumes": [
2705 {'id': 1234,
2706 'name': 'sample-volume',
2707 'attachments': [{'server_id': 1234}]},
2708 ]})
2709
2710 def get_volumes_1234(self, **kw):
2711- r = {'volume': self.get_volumes_detail()[1]['volumes'][0]}
2712- return (200, r)
2713+ r = {'volume': self.get_volumes_detail()[2]['volumes'][0]}
2714+ return (200, {}, r)
2715
2716 def post_volumes_1234_action(self, body, **kw):
2717 _body = None
2718@@ -128,7 +170,7 @@
2719 assert body[action] is None
2720 elif action == 'os-initialize_connection':
2721 assert body[action].keys() == ['connector']
2722- return (202, {'connection_info': 'foos'})
2723+ return (202, {}, {'connection_info': 'foos'})
2724 elif action == 'os-terminate_connection':
2725 assert body[action].keys() == ['connector']
2726 elif action == 'os-begin_detaching':
2727@@ -137,59 +179,132 @@
2728 assert body[action] is None
2729 else:
2730 raise AssertionError("Unexpected server action: %s" % action)
2731- return (resp, _body)
2732+ return (resp, {}, _body)
2733
2734 def post_volumes(self, **kw):
2735- return (202, {'volume': {}})
2736+ return (202, {}, {'volume': {}})
2737
2738 def delete_volumes_1234(self, **kw):
2739- return (202, None)
2740+ return (202, {}, None)
2741
2742 #
2743 # Quotas
2744 #
2745
2746 def get_os_quota_sets_test(self, **kw):
2747- return (200, {'quota_set': {
2748- 'tenant_id': 'test',
2749- 'metadata_items': [],
2750- 'volumes': 1,
2751- 'gigabytes': 1}})
2752+ return (200, {}, {'quota_set': {
2753+ 'tenant_id': 'test',
2754+ 'metadata_items': [],
2755+ 'volumes': 1,
2756+ 'gigabytes': 1}})
2757
2758 def get_os_quota_sets_test_defaults(self):
2759- return (200, {'quota_set': {
2760- 'tenant_id': 'test',
2761- 'metadata_items': [],
2762- 'volumes': 1,
2763- 'gigabytes': 1}})
2764+ return (200, {}, {'quota_set': {
2765+ 'tenant_id': 'test',
2766+ 'metadata_items': [],
2767+ 'volumes': 1,
2768+ 'gigabytes': 1}})
2769
2770 def put_os_quota_sets_test(self, body, **kw):
2771 assert body.keys() == ['quota_set']
2772 fakes.assert_has_keys(body['quota_set'],
2773 required=['tenant_id'])
2774- return (200, {'quota_set': {
2775- 'tenant_id': 'test',
2776- 'metadata_items': [],
2777- 'volumes': 2,
2778- 'gigabytes': 1}})
2779+ return (200, {}, {'quota_set': {
2780+ 'tenant_id': 'test',
2781+ 'metadata_items': [],
2782+ 'volumes': 2,
2783+ 'gigabytes': 1}})
2784
2785 #
2786 # Quota Classes
2787 #
2788
2789 def get_os_quota_class_sets_test(self, **kw):
2790- return (200, {'quota_class_set': {
2791- 'class_name': 'test',
2792- 'metadata_items': [],
2793- 'volumes': 1,
2794- 'gigabytes': 1}})
2795+ return (200, {}, {'quota_class_set': {
2796+ 'class_name': 'test',
2797+ 'metadata_items': [],
2798+ 'volumes': 1,
2799+ 'gigabytes': 1}})
2800
2801 def put_os_quota_class_sets_test(self, body, **kw):
2802 assert body.keys() == ['quota_class_set']
2803 fakes.assert_has_keys(body['quota_class_set'],
2804 required=['class_name'])
2805- return (200, {'quota_class_set': {
2806- 'class_name': 'test',
2807- 'metadata_items': [],
2808- 'volumes': 2,
2809- 'gigabytes': 1}})
2810+ return (200, {}, {'quota_class_set': {
2811+ 'class_name': 'test',
2812+ 'metadata_items': [],
2813+ 'volumes': 2,
2814+ 'gigabytes': 1}})
2815+
2816+ #
2817+ # VolumeTypes
2818+ #
2819+ def get_types(self, **kw):
2820+ return (200, {}, {
2821+ 'volume_types': [{'id': 1,
2822+ 'name': 'test-type-1',
2823+ 'extra_specs':{}},
2824+ {'id': 2,
2825+ 'name': 'test-type-2',
2826+ 'extra_specs':{}}]})
2827+
2828+ def get_types_1(self, **kw):
2829+ return (200, {}, {'volume_type': {'id': 1,
2830+ 'name': 'test-type-1',
2831+ 'extra_specs': {}}})
2832+
2833+ def post_types(self, body, **kw):
2834+ return (202, {}, {'volume_type': {'id': 3,
2835+ 'name': 'test-type-3',
2836+ 'extra_specs': {}}})
2837+
2838+ def post_types_1_extra_specs(self, body, **kw):
2839+ assert body.keys() == ['extra_specs']
2840+ return (200, {}, {'extra_specs': {'k': 'v'}})
2841+
2842+ def delete_types_1_extra_specs_k(self, **kw):
2843+ return(204, {}, None)
2844+
2845+ def delete_types_1(self, **kw):
2846+ return (202, {}, None)
2847+
2848+ #
2849+ # Set/Unset metadata
2850+ #
2851+ def delete_volumes_1234_metadata_test_key(self, **kw):
2852+ return (204, {}, None)
2853+
2854+ def delete_volumes_1234_metadata_key1(self, **kw):
2855+ return (204, {}, None)
2856+
2857+ def delete_volumes_1234_metadata_key2(self, **kw):
2858+ return (204, {}, None)
2859+
2860+ def post_volumes_1234_metadata(self, **kw):
2861+ return (204, {}, {'metadata': {'test_key': 'test_value'}})
2862+
2863+ #
2864+ # List all extensions
2865+ #
2866+ def get_extensions(self, **kw):
2867+ exts = [
2868+ {
2869+ "alias": "FAKE-1",
2870+ "description": "Fake extension number 1",
2871+ "links": [],
2872+ "name": "Fake1",
2873+ "namespace": ("http://docs.openstack.org/"
2874+ "/ext/fake1/api/v1.1"),
2875+ "updated": "2011-06-09T00:00:00+00:00"
2876+ },
2877+ {
2878+ "alias": "FAKE-2",
2879+ "description": "Fake extension number 2",
2880+ "links": [],
2881+ "name": "Fake2",
2882+ "namespace": ("http://docs.openstack.org/"
2883+ "/ext/fake1/api/v1.1"),
2884+ "updated": "2011-06-09T00:00:00+00:00"
2885+ },
2886+ ]
2887+ return (200, {}, {"extensions": exts, })
2888
2889=== modified file 'tests/v1/test_auth.py'
2890--- tests/v1/test_auth.py 2012-10-01 12:05:26 +0000
2891+++ tests/v1/test_auth.py 2013-01-10 22:15:27 +0000
2892@@ -1,20 +1,13 @@
2893-import httplib2
2894 import json
2895 import mock
2896
2897+import requests
2898+
2899 from cinderclient.v1 import client
2900 from cinderclient import exceptions
2901 from tests import utils
2902
2903
2904-def to_http_response(resp_dict):
2905- """Converts dict of response attributes to httplib response."""
2906- resp = httplib2.Response(resp_dict)
2907- for k, v in resp_dict['headers'].items():
2908- resp[k] = v
2909- return resp
2910-
2911-
2912 class AuthenticateAgainstKeystoneTests(utils.TestCase):
2913 def test_authenticate_success(self):
2914 cs = client.Client("username", "password", "project_id",
2915@@ -40,14 +33,14 @@
2916 ],
2917 },
2918 }
2919- auth_response = httplib2.Response({
2920- "status": 200,
2921- "body": json.dumps(resp), })
2922-
2923- mock_request = mock.Mock(return_value=(auth_response,
2924- json.dumps(resp)))
2925-
2926- @mock.patch.object(httplib2.Http, "request", mock_request)
2927+ auth_response = utils.TestResponse({
2928+ "status_code": 200,
2929+ "text": json.dumps(resp),
2930+ })
2931+
2932+ mock_request = mock.Mock(return_value=(auth_response))
2933+
2934+ @mock.patch.object(requests, "request", mock_request)
2935 def test_auth_call():
2936 cs.client.authenticate()
2937 headers = {
2938@@ -66,9 +59,13 @@
2939 }
2940
2941 token_url = cs.client.auth_url + "/tokens"
2942- mock_request.assert_called_with(token_url, "POST",
2943- headers=headers,
2944- body=json.dumps(body))
2945+ mock_request.assert_called_with(
2946+ "POST",
2947+ token_url,
2948+ headers=headers,
2949+ data=json.dumps(body),
2950+ allow_redirects=True,
2951+ **self.TEST_REQUEST_BASE)
2952
2953 endpoints = resp["access"]["serviceCatalog"][0]['endpoints']
2954 public_url = endpoints[0]["publicURL"].rstrip('/')
2955@@ -108,14 +105,14 @@
2956 ],
2957 },
2958 }
2959- auth_response = httplib2.Response({
2960- "status": 200,
2961- "body": json.dumps(resp), })
2962-
2963- mock_request = mock.Mock(return_value=(auth_response,
2964- json.dumps(resp)))
2965-
2966- @mock.patch.object(httplib2.Http, "request", mock_request)
2967+ auth_response = utils.TestResponse({
2968+ "status_code": 200,
2969+ "text": json.dumps(resp),
2970+ })
2971+
2972+ mock_request = mock.Mock(return_value=(auth_response))
2973+
2974+ @mock.patch.object(requests, "request", mock_request)
2975 def test_auth_call():
2976 cs.client.authenticate()
2977 headers = {
2978@@ -134,9 +131,13 @@
2979 }
2980
2981 token_url = cs.client.auth_url + "/tokens"
2982- mock_request.assert_called_with(token_url, "POST",
2983- headers=headers,
2984- body=json.dumps(body))
2985+ mock_request.assert_called_with(
2986+ "POST",
2987+ token_url,
2988+ headers=headers,
2989+ data=json.dumps(body),
2990+ allow_redirects=True,
2991+ **self.TEST_REQUEST_BASE)
2992
2993 endpoints = resp["access"]["serviceCatalog"][0]['endpoints']
2994 public_url = endpoints[0]["publicURL"].rstrip('/')
2995@@ -152,14 +153,14 @@
2996 cs = client.Client("username", "password", "project_id",
2997 "auth_url/v2.0")
2998 resp = {"unauthorized": {"message": "Unauthorized", "code": "401"}}
2999- auth_response = httplib2.Response({
3000- "status": 401,
3001- "body": json.dumps(resp), })
3002-
3003- mock_request = mock.Mock(return_value=(auth_response,
3004- json.dumps(resp)))
3005-
3006- @mock.patch.object(httplib2.Http, "request", mock_request)
3007+ auth_response = utils.TestResponse({
3008+ "status_code": 401,
3009+ "text": json.dumps(resp),
3010+ })
3011+
3012+ mock_request = mock.Mock(return_value=(auth_response))
3013+
3014+ @mock.patch.object(requests, "request", mock_request)
3015 def test_auth_call():
3016 self.assertRaises(exceptions.Unauthorized, cs.client.authenticate)
3017
3018@@ -192,29 +193,28 @@
3019 correct_response = json.dumps(dict_correct_response)
3020 dict_responses = [
3021 {"headers": {'location':'http://127.0.0.1:5001'},
3022- "status": 305,
3023- "body": "Use proxy"},
3024+ "status_code": 305,
3025+ "text": "Use proxy"},
3026 # Configured on admin port, cinder redirects to v2.0 port.
3027 # When trying to connect on it, keystone auth succeed by v1.0
3028 # protocol (through headers) but tokens are being returned in
3029 # body (looks like keystone bug). Leaved for compatibility.
3030 {"headers": {},
3031- "status": 200,
3032- "body": correct_response},
3033+ "status_code": 200,
3034+ "text": correct_response},
3035 {"headers": {},
3036- "status": 200,
3037- "body": correct_response}
3038+ "status_code": 200,
3039+ "text": correct_response}
3040 ]
3041
3042- responses = [(to_http_response(resp), resp['body'])
3043- for resp in dict_responses]
3044+ responses = [(utils.TestResponse(resp)) for resp in dict_responses]
3045
3046 def side_effect(*args, **kwargs):
3047 return responses.pop(0)
3048
3049 mock_request = mock.Mock(side_effect=side_effect)
3050
3051- @mock.patch.object(httplib2.Http, "request", mock_request)
3052+ @mock.patch.object(requests, "request", mock_request)
3053 def test_auth_call():
3054 cs.client.authenticate()
3055 headers = {
3056@@ -233,9 +233,13 @@
3057 }
3058
3059 token_url = cs.client.auth_url + "/tokens"
3060- mock_request.assert_called_with(token_url, "POST",
3061- headers=headers,
3062- body=json.dumps(body))
3063+ mock_request.assert_called_with(
3064+ "POST",
3065+ token_url,
3066+ headers=headers,
3067+ data=json.dumps(body),
3068+ allow_redirects=True,
3069+ **self.TEST_REQUEST_BASE)
3070
3071 resp = dict_correct_response
3072 endpoints = resp["access"]["serviceCatalog"][0]['endpoints']
3073@@ -282,16 +286,14 @@
3074 ],
3075 },
3076 }
3077- auth_response = httplib2.Response(
3078- {
3079- "status": 200,
3080- "body": json.dumps(resp),
3081- })
3082-
3083- mock_request = mock.Mock(return_value=(auth_response,
3084- json.dumps(resp)))
3085-
3086- @mock.patch.object(httplib2.Http, "request", mock_request)
3087+ auth_response = utils.TestResponse({
3088+ "status_code": 200,
3089+ "text": json.dumps(resp),
3090+ })
3091+
3092+ mock_request = mock.Mock(return_value=(auth_response))
3093+
3094+ @mock.patch.object(requests, "request", mock_request)
3095 def test_auth_call():
3096 self.assertRaises(exceptions.AmbiguousEndpoints,
3097 cs.client.authenticate)
3098@@ -302,15 +304,17 @@
3099 class AuthenticationTests(utils.TestCase):
3100 def test_authenticate_success(self):
3101 cs = client.Client("username", "password", "project_id", "auth_url")
3102- management_url = 'https://servers.api.rackspacecloud.com/v1.1/443470'
3103- auth_response = httplib2.Response({
3104- 'status': 204,
3105- 'x-server-management-url': management_url,
3106- 'x-auth-token': '1b751d74-de0c-46ae-84f0-915744b582d1',
3107+ management_url = 'https://localhost/v1.1/443470'
3108+ auth_response = utils.TestResponse({
3109+ 'status_code': 204,
3110+ 'headers': {
3111+ 'x-server-management-url': management_url,
3112+ 'x-auth-token': '1b751d74-de0c-46ae-84f0-915744b582d1',
3113+ },
3114 })
3115- mock_request = mock.Mock(return_value=(auth_response, None))
3116+ mock_request = mock.Mock(return_value=(auth_response))
3117
3118- @mock.patch.object(httplib2.Http, "request", mock_request)
3119+ @mock.patch.object(requests, "request", mock_request)
3120 def test_auth_call():
3121 cs.client.authenticate()
3122 headers = {
3123@@ -320,21 +324,25 @@
3124 'X-Auth-Project-Id': 'project_id',
3125 'User-Agent': cs.client.USER_AGENT
3126 }
3127- mock_request.assert_called_with(cs.client.auth_url, 'GET',
3128- headers=headers)
3129+ mock_request.assert_called_with(
3130+ "GET",
3131+ cs.client.auth_url,
3132+ headers=headers,
3133+ **self.TEST_REQUEST_BASE)
3134+
3135 self.assertEqual(cs.client.management_url,
3136- auth_response['x-server-management-url'])
3137+ auth_response.headers['x-server-management-url'])
3138 self.assertEqual(cs.client.auth_token,
3139- auth_response['x-auth-token'])
3140+ auth_response.headers['x-auth-token'])
3141
3142 test_auth_call()
3143
3144 def test_authenticate_failure(self):
3145 cs = client.Client("username", "password", "project_id", "auth_url")
3146- auth_response = httplib2.Response({'status': 401})
3147- mock_request = mock.Mock(return_value=(auth_response, None))
3148+ auth_response = utils.TestResponse({"status_code": 401})
3149+ mock_request = mock.Mock(return_value=(auth_response))
3150
3151- @mock.patch.object(httplib2.Http, "request", mock_request)
3152+ @mock.patch.object(requests, "request", mock_request)
3153 def test_auth_call():
3154 self.assertRaises(exceptions.Unauthorized, cs.client.authenticate)
3155
3156
3157=== modified file 'tests/v1/test_shell.py'
3158--- tests/v1/test_shell.py 2012-10-01 12:05:26 +0000
3159+++ tests/v1/test_shell.py 2013-01-10 22:15:27 +0000
3160@@ -17,6 +17,8 @@
3161
3162 import os
3163
3164+import fixtures
3165+
3166 from cinderclient import client
3167 from cinderclient import shell
3168 from tests.v1 import fakes
3169@@ -25,17 +27,21 @@
3170
3171 class ShellTest(utils.TestCase):
3172
3173+ FAKE_ENV = {
3174+ 'CINDER_USERNAME': 'username',
3175+ 'CINDER_PASSWORD': 'password',
3176+ 'CINDER_PROJECT_ID': 'project_id',
3177+ 'OS_COMPUTE_API_VERSION': '1.1',
3178+ 'CINDER_URL': 'http://no.where',
3179+ }
3180+
3181 # Patch os.environ to avoid required auth info.
3182 def setUp(self):
3183 """Run before each test."""
3184- self.old_environment = os.environ.copy()
3185- os.environ = {
3186- 'CINDER_USERNAME': 'username',
3187- 'CINDER_PASSWORD': 'password',
3188- 'CINDER_PROJECT_ID': 'project_id',
3189- 'OS_COMPUTE_API_VERSION': '1.1',
3190- 'CINDER_URL': 'http://no.where',
3191- }
3192+ super(ShellTest, self).setUp()
3193+ for var in self.FAKE_ENV:
3194+ self.useFixture(fixtures.EnvironmentVariable(var,
3195+ self.FAKE_ENV[var]))
3196
3197 self.shell = shell.OpenStackCinderShell()
3198
3199@@ -44,7 +50,6 @@
3200 client.get_client_class = lambda *_: fakes.FakeClient
3201
3202 def tearDown(self):
3203- os.environ = self.old_environment
3204 # For some method like test_image_meta_bad_action we are
3205 # testing a SystemExit to be thrown and object self.shell has
3206 # no time to get instantatiated which is OK in this case, so
3207@@ -54,6 +59,7 @@
3208
3209 #HACK(bcwaldon): replace this when we start using stubs
3210 client.get_client_class = self.old_get_client_class
3211+ super(ShellTest, self).tearDown()
3212
3213 def run_command(self, cmd):
3214 self.shell.main(cmd.split())
3215@@ -87,6 +93,7 @@
3216
3217 def test_delete(self):
3218 self.run_command('delete 1234')
3219+ self.assert_called('DELETE', '/volumes/1234')
3220
3221 def test_snapshot_list_filter_volume_id(self):
3222 self.run_command('snapshot-list --volume-id=1234')
3223@@ -96,3 +103,61 @@
3224 self.run_command('snapshot-list --status=available --volume-id=1234')
3225 self.assert_called('GET', '/snapshots/detail?'
3226 'status=available&volume_id=1234')
3227+
3228+ def test_rename(self):
3229+ # basic rename with positional agruments
3230+ self.run_command('rename 1234 new-name')
3231+ expected = {'volume': {'display_name': 'new-name'}}
3232+ self.assert_called('PUT', '/volumes/1234', body=expected)
3233+ # change description only
3234+ self.run_command('rename 1234 --display-description=new-description')
3235+ expected = {'volume': {'display_description': 'new-description'}}
3236+ self.assert_called('PUT', '/volumes/1234', body=expected)
3237+ # rename and change description
3238+ self.run_command('rename 1234 new-name '
3239+ '--display-description=new-description')
3240+ expected = {'volume': {
3241+ 'display_name': 'new-name',
3242+ 'display_description': 'new-description',
3243+ }}
3244+ self.assert_called('PUT', '/volumes/1234', body=expected)
3245+ # noop, the only all will be the lookup
3246+ self.run_command('rename 1234')
3247+ self.assert_called('GET', '/volumes/1234')
3248+
3249+ def test_rename_snapshot(self):
3250+ # basic rename with positional agruments
3251+ self.run_command('snapshot-rename 1234 new-name')
3252+ expected = {'snapshot': {'display_name': 'new-name'}}
3253+ self.assert_called('PUT', '/snapshots/1234', body=expected)
3254+ # change description only
3255+ self.run_command('snapshot-rename 1234 '
3256+ '--display-description=new-description')
3257+ expected = {'snapshot': {'display_description': 'new-description'}}
3258+ self.assert_called('PUT', '/snapshots/1234', body=expected)
3259+ # snapshot-rename and change description
3260+ self.run_command('snapshot-rename 1234 new-name '
3261+ '--display-description=new-description')
3262+ expected = {'snapshot': {
3263+ 'display_name': 'new-name',
3264+ 'display_description': 'new-description',
3265+ }}
3266+ self.assert_called('PUT', '/snapshots/1234', body=expected)
3267+ # noop, the only all will be the lookup
3268+ self.run_command('snapshot-rename 1234')
3269+ self.assert_called('GET', '/snapshots/1234')
3270+
3271+ def test_set_metadata_set(self):
3272+ self.run_command('metadata 1234 set key1=val1 key2=val2')
3273+ self.assert_called('POST', '/volumes/1234/metadata',
3274+ {'metadata': {'key1': 'val1', 'key2': 'val2'}})
3275+
3276+ def test_set_metadata_delete_dict(self):
3277+ self.run_command('metadata 1234 unset key1=val1 key2=val2')
3278+ self.assert_called('DELETE', '/volumes/1234/metadata/key1')
3279+ self.assert_called('DELETE', '/volumes/1234/metadata/key2', pos=-2)
3280+
3281+ def test_set_metadata_delete_keys(self):
3282+ self.run_command('metadata 1234 unset key1 key2')
3283+ self.assert_called('DELETE', '/volumes/1234/metadata/key1')
3284+ self.assert_called('DELETE', '/volumes/1234/metadata/key2', pos=-2)
3285
3286=== added file 'tests/v1/test_types.py'
3287--- tests/v1/test_types.py 1970-01-01 00:00:00 +0000
3288+++ tests/v1/test_types.py 2013-01-10 22:15:27 +0000
3289@@ -0,0 +1,35 @@
3290+from cinderclient import exceptions
3291+from cinderclient.v1 import volume_types
3292+from tests import utils
3293+from tests.v1 import fakes
3294+
3295+cs = fakes.FakeClient()
3296+
3297+
3298+class TypesTest(utils.TestCase):
3299+ def test_list_types(self):
3300+ tl = cs.volume_types.list()
3301+ cs.assert_called('GET', '/types')
3302+ for t in tl:
3303+ self.assertTrue(isinstance(t, volume_types.VolumeType))
3304+
3305+ def test_create(self):
3306+ t = cs.volume_types.create('test-type-3')
3307+ cs.assert_called('POST', '/types')
3308+ self.assertTrue(isinstance(t, volume_types.VolumeType))
3309+
3310+ def test_set_key(self):
3311+ t = cs.volume_types.get(1)
3312+ t.set_keys({'k': 'v'})
3313+ cs.assert_called('POST',
3314+ '/types/1/extra_specs',
3315+ {'extra_specs': {'k': 'v'}})
3316+
3317+ def test_unsset_keys(self):
3318+ t = cs.volume_types.get(1)
3319+ t.unset_keys(['k'])
3320+ cs.assert_called('DELETE', '/types/1/extra_specs/k')
3321+
3322+ def test_delete(self):
3323+ cs.volume_types.delete(1)
3324+ cs.assert_called('DELETE', '/types/1')
3325
3326=== modified file 'tests/v1/test_volumes.py'
3327--- tests/v1/test_volumes.py 2012-10-01 12:05:26 +0000
3328+++ tests/v1/test_volumes.py 2013-01-10 22:15:27 +0000
3329@@ -1,4 +1,3 @@
3330-from cinderclient.v1 import volumes
3331 from tests import utils
3332 from tests.v1 import fakes
3333
3334@@ -18,7 +17,7 @@
3335 cs.assert_called('DELETE', '/volumes/1234')
3336
3337 def test_create_keypair(self):
3338- kp = cs.volumes.create(1)
3339+ cs.volumes.create(1)
3340 cs.assert_called('POST', '/volumes')
3341
3342 def test_attach(self):
3343@@ -60,3 +59,13 @@
3344 v = cs.volumes.get('1234')
3345 cs.volumes.terminate_connection(v, {})
3346 cs.assert_called('POST', '/volumes/1234/action')
3347+
3348+ def test_set_metadata(self):
3349+ cs.volumes.set_metadata(1234, {'k1': 'v1'})
3350+ cs.assert_called('POST', '/volumes/1234/metadata',
3351+ {'metadata': {'k1': 'v1'}})
3352+
3353+ def test_delete_metadata(self):
3354+ keys = ['key1']
3355+ cs.volumes.delete_metadata(1234, keys)
3356+ cs.assert_called('DELETE', '/volumes/1234/metadata/key1')
3357
3358=== modified file 'tools/pip-requires'
3359--- tools/pip-requires 2012-06-27 13:32:14 +0000
3360+++ tools/pip-requires 2013-01-10 22:15:27 +0000
3361@@ -1,4 +1,4 @@
3362 argparse
3363-httplib2
3364 prettytable
3365+requests<1.0
3366 simplejson
3367
3368=== modified file 'tools/test-requires'
3369--- tools/test-requires 2012-09-07 11:35:32 +0000
3370+++ tools/test-requires 2013-01-10 22:15:27 +0000
3371@@ -1,10 +1,11 @@
3372+distribute>=0.6.24
3373
3374-distribute>=0.6.24
3375+fixtures
3376 mock
3377 nose
3378+nosehtmloutput
3379 nosexcover
3380 openstack.nose_plugin
3381-nosehtmloutput
3382-pep8==1.2
3383+pep8==1.3.3
3384 sphinx>=1.1.2
3385-unittest2
3386+testtools>=0.9.22

Subscribers

People subscribed via source and target branches