1bb2952e9cd17bf319e36488a715f51634a8e8c8
[kamaki] / kamaki / cli / commands / image.py
1 # Copyright 2012 GRNET S.A. All rights reserved.
2 #
3 # Redistribution and use in source and binary forms, with or
4 # without modification, are permitted provided that the following
5 # conditions are met:
6 #
7 #   1. Redistributions of source code must retain the above
8 #      copyright notice, this list of conditions and the following
9 #      disclaimer.
10 #
11 #   2. Redistributions in binary form must reproduce the above
12 #      copyright notice, this list of conditions and the following
13 #      disclaimer in the documentation and/or other materials
14 #      provided with the distribution.
15 #
16 # THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17 # OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19 # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20 # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23 # USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24 # AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27 # POSSIBILITY OF SUCH DAMAGE.
28 #
29 # The views and conclusions contained in the software and
30 # documentation are those of the authors and should not be
31 # interpreted as representing official policies, either expressed
32 # or implied, of GRNET S.A.command
33
34 from kamaki.cli import command
35 from kamaki.cli.command_tree import CommandTree
36 from kamaki.cli.utils import print_dict, print_items
37 from kamaki.clients.image import ImageClient
38 from kamaki.cli.argument import FlagArgument, ValueArgument, KeyValueArgument
39 from kamaki.cli.argument import IntArgument
40 from kamaki.cli.commands.cyclades_cli import _init_cyclades
41 from kamaki.cli.commands import _command_init, errors
42
43
44 image_cmds = CommandTree(
45     'image',
46     'Cyclades/Plankton API image commands\n'
47     'image compute:\tCyclades/Compute API image commands')
48 _commands = [image_cmds]
49
50
51 about_image_id = [
52     'To see a list of available image ids: /image list']
53
54
55 class _init_image(_command_init):
56     @errors.generic.all
57     def _run(self):
58         token = self.config.get('image', 'token')\
59             or self.config.get('compute', 'token')\
60             or self.config.get('global', 'token')
61         base_url = self.config.get('image', 'url')\
62             or self.config.get('compute', 'url')\
63             or self.config.get('global', 'url')
64         self.client = ImageClient(base_url=base_url, token=token)
65         self._set_log_params()
66         self._update_max_threads()
67
68     def main(self):
69         self._run()
70
71
72 # Plankton Image Commands
73
74
75 @command(image_cmds)
76 class image_list(_init_image):
77     """List images accessible by user"""
78
79     arguments = dict(
80         detail=FlagArgument('show detailed output', ('-l', '--details')),
81         container_format=ValueArgument(
82             'filter by container format',
83             '--container-format'),
84         disk_format=ValueArgument('filter by disk format', '--disk-format'),
85         name=ValueArgument('filter by name', '--name'),
86         name_pref=ValueArgument(
87             'filter by name prefix (case insensitive)',
88             '--name-prefix'),
89         name_suff=ValueArgument(
90             'filter by name suffix (case insensitive)',
91             '--name-suffix'),
92         name_like=ValueArgument(
93             'print only if name contains this (case insensitive)',
94             '--name-like'),
95         size_min=IntArgument('filter by minimum size', '--size-min'),
96         size_max=IntArgument('filter by maximum size', '--size-max'),
97         status=ValueArgument('filter by status', '--status'),
98         owner=ValueArgument('filter by owner', '--owner'),
99         order=ValueArgument(
100             'order by FIELD ( - to reverse order)',
101             '--order',
102             default=''),
103         limit=IntArgument('limit number of listed images', ('-n', '--number')),
104         more=FlagArgument(
105             'output results in pages (-n to set items per page, default 10)',
106             '--more')
107     )
108
109     def _filtered_by_owner(self, detail, *list_params):
110         images = []
111         MINKEYS = set([
112             'id', 'size', 'status', 'disk_format', 'container_format', 'name'])
113         for img in self.client.list_public(True, *list_params):
114             if img['owner'] == self['owner']:
115                 if not detail:
116                     for key in set(img.keys()).difference(MINKEYS):
117                         img.pop(key)
118                 images.append(img)
119         return images
120
121     def _filtered_by_name(self, images):
122         np, ns, nl = self['name_pref'], self['name_suff'], self['name_like']
123         return [img for img in images if (
124             (not np) or img['name'].lower().startswith(np.lower())) and (
125             (not ns) or img['name'].lower().endswith(ns.lower())) and (
126             (not nl) or nl.lower() in img['name'].lower())]
127
128     @errors.generic.all
129     @errors.cyclades.connection
130     def _run(self):
131         super(self.__class__, self)._run()
132         filters = {}
133         for arg in set([
134                 'container_format',
135                 'disk_format',
136                 'name',
137                 'size_min',
138                 'size_max',
139                 'status']).intersection(self.arguments):
140             filters[arg] = self[arg]
141
142         order = self['order']
143         detail = self['detail']
144         if self['owner']:
145             images = self._filtered_by_owner(detail, filters, order)
146         else:
147             images = self.client.list_public(detail, filters, order)
148         images = self._filtered_by_name(images)
149
150         if self['more']:
151             print_items(
152                 images,
153                 title=('name',),
154                 with_enumeration=True,
155                 page_size=self['limit'] or 10)
156         elif self['limit']:
157             print_items(
158                 images[:self['limit']],
159                 title=('name',),
160                 with_enumeration=True)
161         else:
162             print_items(images, title=('name',), with_enumeration=True)
163
164     def main(self):
165         super(self.__class__, self)._run()
166         self._run()
167
168
169 @command(image_cmds)
170 class image_meta(_init_image):
171     """Get image metadata
172     Image metadata include:
173     - image file information (location, size, etc.)
174     - image information (id, name, etc.)
175     - image os properties (os, fs, etc.)
176     """
177
178     @errors.generic.all
179     @errors.plankton.connection
180     @errors.plankton.id
181     def _run(self, image_id):
182         image = self.client.get_meta(image_id)
183         print_dict(image)
184
185     def main(self, image_id):
186         super(self.__class__, self)._run()
187         self._run(image_id=image_id)
188
189
190 @command(image_cmds)
191 class image_register(_init_image):
192     """(Re)Register an image"""
193
194     arguments = dict(
195         checksum=ValueArgument('set image checksum', '--checksum'),
196         container_format=ValueArgument(
197             'set container format',
198             '--container-format'),
199         disk_format=ValueArgument('set disk format', '--disk-format'),
200         #id=ValueArgument('set image ID', '--id'),
201         owner=ValueArgument('set image owner (admin only)', '--owner'),
202         properties=KeyValueArgument(
203             'add property in key=value form (can be repeated)',
204             ('-p, --property')),
205         is_public=FlagArgument('mark image as public', '--public'),
206         size=IntArgument('set image size', '--size'),
207         update=FlagArgument(
208             'update existing image properties',
209             ('-u', '--update'))
210     )
211
212     @errors.generic.all
213     @errors.plankton.connection
214     def _run(self, name, location):
215         if not location.startswith('pithos://'):
216             account = self.config.get('file', 'account') \
217                 or self.config.get('global', 'account')
218             assert account, 'No user account provided'
219             if account[-1] == '/':
220                 account = account[:-1]
221             container = self.config.get('file', 'container') \
222                 or self.config.get('global', 'container')
223             if not container:
224                 location = 'pithos://%s/%s' % (account, location)
225             else:
226                 location = 'pithos://%s/%s/%s' % (account, container, location)
227
228         params = {}
229         for key in set([
230                 'checksum',
231                 'container_format',
232                 'disk_format',
233                 'owner',
234                 'size',
235                 'is_public']).intersection(self.arguments):
236             params[key] = self[key]
237
238             properties = self['properties']
239         if self['update']:
240             self.client.reregister(location, name, params, properties)
241         else:
242             r = self.client.register(name, location, params, properties)
243             print_dict(r)
244
245     def main(self, name, location):
246         super(self.__class__, self)._run()
247         self._run(name, location)
248
249
250 @command(image_cmds)
251 class image_members(_init_image):
252     """Get image members"""
253
254     @errors.generic.all
255     @errors.plankton.connection
256     @errors.plankton.id
257     def _run(self, image_id):
258         members = self.client.list_members(image_id)
259         print_items(members)
260
261     def main(self, image_id):
262         super(self.__class__, self)._run()
263         self._run(image_id=image_id)
264
265
266 @command(image_cmds)
267 class image_shared(_init_image):
268     """List images shared by a member"""
269
270     @errors.generic.all
271     @errors.plankton.connection
272     def _run(self, member):
273         images = self.client.list_shared(member)
274         print_items(images)
275
276     def main(self, member):
277         super(self.__class__, self)._run()
278         self._run(member)
279
280
281 @command(image_cmds)
282 class image_addmember(_init_image):
283     """Add a member to an image"""
284
285     @errors.generic.all
286     @errors.plankton.connection
287     @errors.plankton.id
288     def _run(self, image_id=None, member=None):
289             self.client.add_member(image_id, member)
290
291     def main(self, image_id, member):
292         super(self.__class__, self)._run()
293         self._run(image_id=image_id, member=member)
294
295
296 @command(image_cmds)
297 class image_delmember(_init_image):
298     """Remove a member from an image"""
299
300     @errors.generic.all
301     @errors.plankton.connection
302     @errors.plankton.id
303     def _run(self, image_id=None, member=None):
304             self.client.remove_member(image_id, member)
305
306     def main(self, image_id, member):
307         super(self.__class__, self)._run()
308         self._run(image_id=image_id, member=member)
309
310
311 @command(image_cmds)
312 class image_setmembers(_init_image):
313     """Set the members of an image"""
314
315     @errors.generic.all
316     @errors.plankton.connection
317     @errors.plankton.id
318     def _run(self, image_id, members):
319             self.client.set_members(image_id, members)
320
321     def main(self, image_id, *members):
322         super(self.__class__, self)._run()
323         self._run(image_id=image_id, members=members)
324
325
326 # Compute Image Commands
327
328
329 @command(image_cmds)
330 class image_compute(_init_cyclades):
331     """Cyclades/Compute API image commands"""
332
333
334 @command(image_cmds)
335 class image_compute_list(_init_cyclades):
336     """List images"""
337
338     arguments = dict(
339         detail=FlagArgument('show detailed output', ('-l', '--details')),
340         limit=IntArgument('limit number listed images', ('-n', '--number')),
341         more=FlagArgument(
342             'output results in pages (-n to set items per page, default 10)',
343             '--more')
344     )
345
346     def _make_results_pretty(self, images):
347         for img in images:
348             if 'metadata' in img:
349                 img['metadata'] = img['metadata']['values']
350
351     @errors.generic.all
352     @errors.cyclades.connection
353     def _run(self):
354         images = self.client.list_images(self['detail'])
355         if self['detail']:
356             self._make_results_pretty(images)
357         if self['more']:
358             print_items(images, page_size=self['limit'] or 10)
359         else:
360             print_items(images[:self['limit']])
361
362     def main(self):
363         super(self.__class__, self)._run()
364         self._run()
365
366
367 @command(image_cmds)
368 class image_compute_info(_init_cyclades):
369     """Get detailed information on an image"""
370
371     @errors.generic.all
372     @errors.cyclades.connection
373     @errors.plankton.id
374     def _run(self, image_id):
375         image = self.client.get_image_details(image_id)
376         if 'metadata' in image:
377             image['metadata'] = image['metadata']['values']
378         print_dict(image)
379
380     def main(self, image_id):
381         super(self.__class__, self)._run()
382         self._run(image_id=image_id)
383
384
385 @command(image_cmds)
386 class image_compute_delete(_init_cyclades):
387     """Delete an image (WARNING: image file is also removed)"""
388
389     @errors.generic.all
390     @errors.cyclades.connection
391     @errors.plankton.id
392     def _run(self, image_id):
393         self.client.delete_image(image_id)
394
395     def main(self, image_id):
396         super(self.__class__, self)._run()
397         self._run(image_id=image_id)
398
399
400 @command(image_cmds)
401 class image_compute_properties(_init_cyclades):
402     """Get properties related to OS installation in an image"""
403
404     @errors.generic.all
405     @errors.cyclades.connection
406     @errors.plankton.id
407     @errors.plankton.metadata
408     def _run(self, image_id, key):
409         r = self.client.get_image_metadata(image_id, key)
410         print_dict(r)
411
412     def main(self, image_id, key=''):
413         super(self.__class__, self)._run()
414         self._run(image_id=image_id, key=key)
415
416
417 @command(image_cmds)
418 class image_addproperty(_init_cyclades):
419     """Add an OS-related property to an image"""
420
421     @errors.generic.all
422     @errors.cyclades.connection
423     @errors.plankton.id
424     @errors.plankton.metadata
425     def _run(self, image_id, key, val):
426         r = self.client.create_image_metadata(image_id, key, val)
427         print_dict(r)
428
429     def main(self, image_id, key, val):
430         super(self.__class__, self)._run()
431         self._run(image_id=image_id, key=key, val=val)
432
433
434 @command(image_cmds)
435 class image_compute_setproperty(_init_cyclades):
436     """Update an existing property in an image"""
437
438     @errors.generic.all
439     @errors.cyclades.connection
440     @errors.plankton.id
441     @errors.plankton.metadata
442     def _run(self, image_id, key, val):
443         metadata = {key: val}
444         r = self.client.update_image_metadata(image_id, **metadata)
445         print_dict(r)
446
447     def main(self, image_id, key, val):
448         super(self.__class__, self)._run()
449         self._run(image_id=image_id, key=key, val=val)
450
451
452 @command(image_cmds)
453 class image_compute_delproperty(_init_cyclades):
454     """Delete a property of an image"""
455
456     @errors.generic.all
457     @errors.cyclades.connection
458     @errors.plankton.id
459     @errors.plankton.metadata
460     def _run(self, image_id, key):
461         self.client.delete_image_metadata(image_id, key)
462
463     def main(self, image_id, key):
464         super(self.__class__, self)._run()
465         self._run(image_id=image_id, key=key)