Add enumeration as optional for all list cmds
[kamaki] / kamaki / cli / commands / cyclades.py
1 # Copyright 2011-2013 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.
33
34 from kamaki.cli import command
35 from kamaki.cli.command_tree import CommandTree
36 from kamaki.cli.utils import print_dict, print_list, print_items
37 from kamaki.cli.errors import raiseCLIError, CLISyntaxError
38 from kamaki.clients.cyclades import CycladesClient, ClientError
39 from kamaki.cli.argument import FlagArgument, ValueArgument, KeyValueArgument
40 from kamaki.cli.argument import ProgressBarArgument, DateArgument, IntArgument
41 from kamaki.cli.commands import _command_init, errors
42
43 from base64 import b64encode
44 from os.path import exists
45
46
47 server_cmds = CommandTree('server', 'Cyclades/Compute API server commands')
48 flavor_cmds = CommandTree('flavor', 'Cyclades/Compute API flavor commands')
49 network_cmds = CommandTree('network', 'Cyclades/Compute API network commands')
50 _commands = [server_cmds, flavor_cmds, network_cmds]
51
52
53 about_authentication = '\nUser Authentication:\
54     \n* to check authentication: /user authenticate\
55     \n* to set authentication token: /config set token <token>'
56
57 howto_personality = [
58     'Defines a file to be injected to VMs personality.',
59     'Personality value syntax: PATH,[SERVER_PATH,[OWNER,[GROUP,[MODE]]]]',
60     '  PATH: of local file to be injected',
61     '  SERVER_PATH: destination location inside server Image',
62     '  OWNER: user id of destination file owner',
63     '  GROUP: group id or name to own destination file',
64     '  MODEL: permition in octal (e.g. 0777 or o+rwx)']
65
66
67 class _init_cyclades(_command_init):
68     @errors.generic.all
69     def _run(self, service='compute'):
70         token = self.config.get(service, 'token')\
71             or self.config.get('global', 'token')
72         base_url = self.config.get(service, 'url')\
73             or self.config.get('global', 'url')
74         self.client = CycladesClient(base_url=base_url, token=token)
75         self._set_log_params()
76         self._update_max_threads()
77
78     def main(self):
79         self._run()
80
81
82 @command(server_cmds)
83 class server_list(_init_cyclades):
84     """List Virtual Machines accessible by user"""
85
86     __doc__ += about_authentication
87
88     arguments = dict(
89         detail=FlagArgument('show detailed output', ('-l', '--details')),
90         since=DateArgument(
91             'show only items since date (\' d/m/Y H:M:S \')',
92             '--since'),
93         limit=IntArgument('limit number of listed VMs', ('-n', '--number')),
94         more=FlagArgument(
95             'output results in pages (-n to set items per page, default 10)',
96             '--more'),
97         enum=FlagArgument('Enumerate results', '--enumerate')
98     )
99
100     def _make_results_pretty(self, servers):
101         for server in servers:
102             addr_dict = {}
103             if 'attachments' in server:
104                 for addr in server['attachments']['values']:
105                     ips = addr.pop('values', [])
106                     for ip in ips:
107                         addr['IPv%s' % ip['version']] = ip['addr']
108                     if 'firewallProfile' in addr:
109                         addr['firewall'] = addr.pop('firewallProfile')
110                     addr_dict[addr.pop('id')] = addr
111                 server['attachments'] = addr_dict if addr_dict else None
112             if 'metadata' in server:
113                 server['metadata'] = server['metadata']['values']
114
115     @errors.generic.all
116     @errors.cyclades.connection
117     @errors.cyclades.date
118     def _run(self):
119         servers = self.client.list_servers(self['detail'], self['since'])
120         if self['detail']:
121             self._make_results_pretty(servers)
122
123         if self['more']:
124             print_items(
125                 servers,
126                 page_size=self['limit'] if self['limit'] else 10,
127                 with_enumeration=self['enum'])
128         else:
129             print_items(
130                 servers[:self['limit'] if self['limit'] else len(servers)],
131                 with_enumeration=self['enum'])
132
133     def main(self):
134         super(self.__class__, self)._run()
135         self._run()
136
137
138 @command(server_cmds)
139 class server_info(_init_cyclades):
140     """Detailed information on a Virtual Machine
141     Contains:
142     - name, id, status, create/update dates
143     - network interfaces
144     - metadata (e.g. os, superuser) and diagnostics
145     - hardware flavor and os image ids
146     """
147
148     def _print(self, server):
149         addr_dict = {}
150         if 'attachments' in server:
151             atts = server.pop('attachments')
152             for addr in atts['values']:
153                 ips = addr.pop('values', [])
154                 for ip in ips:
155                     addr['IPv%s' % ip['version']] = ip['addr']
156                 if 'firewallProfile' in addr:
157                     addr['firewall'] = addr.pop('firewallProfile')
158                 addr_dict[addr.pop('id')] = addr
159             server['attachments'] = addr_dict if addr_dict else None
160         if 'metadata' in server:
161             server['metadata'] = server['metadata']['values']
162         print_dict(server, ident=1)
163
164     @errors.generic.all
165     @errors.cyclades.connection
166     @errors.cyclades.server_id
167     def _run(self, server_id):
168         server = self.client.get_server_details(server_id)
169         self._print(server)
170
171     def main(self, server_id):
172         super(self.__class__, self)._run()
173         self._run(server_id=server_id)
174
175
176 class PersonalityArgument(KeyValueArgument):
177     @property
178     def value(self):
179         return self._value if hasattr(self, '_value') else []
180
181     @value.setter
182     def value(self, newvalue):
183         if newvalue == self.default:
184             return self.value
185         self._value = []
186         for i, terms in enumerate(newvalue):
187             termlist = terms.split(',')
188             if len(termlist) > 5:
189                 msg = 'Wrong number of terms (should be 1 to 5)'
190                 raiseCLIError(CLISyntaxError(msg), details=howto_personality)
191             path = termlist[0]
192             if not exists(path):
193                 raiseCLIError(
194                     None,
195                     '--personality: File %s does not exist' % path,
196                     importance=1,
197                     details=howto_personality)
198             self._value.append(dict(path=path))
199             with open(path) as f:
200                 self._value[i]['contents'] = b64encode(f.read())
201             try:
202                 self._value[i]['path'] = termlist[1]
203                 self._value[i]['owner'] = termlist[2]
204                 self._value[i]['group'] = termlist[3]
205                 self._value[i]['mode'] = termlist[4]
206             except IndexError:
207                 pass
208
209
210 @command(server_cmds)
211 class server_create(_init_cyclades):
212     """Create a server (aka Virtual Machine)
213     Parameters:
214     - name: (single quoted text)
215     - flavor id: Hardware flavor. Pick one from: /flavor list
216     - image id: OS images. Pick one from: /image list
217     """
218
219     arguments = dict(
220         personality=PersonalityArgument(
221             ' /// '.join(howto_personality),
222             ('-p', '--personality'))
223     )
224
225     @errors.generic.all
226     @errors.cyclades.connection
227     @errors.plankton.id
228     @errors.cyclades.flavor_id
229     def _run(self, name, flavor_id, image_id):
230         r = self.client.create_server(
231             name,
232             int(flavor_id),
233             image_id,
234             self['personality'])
235         print_dict(r)
236
237     def main(self, name, flavor_id, image_id):
238         super(self.__class__, self)._run()
239         self._run(name=name, flavor_id=flavor_id, image_id=image_id)
240
241
242 @command(server_cmds)
243 class server_rename(_init_cyclades):
244     """Set/update a server (VM) name
245     VM names are not unique, therefore multiple servers may share the same name
246     """
247
248     @errors.generic.all
249     @errors.cyclades.connection
250     @errors.cyclades.server_id
251     def _run(self, server_id, new_name):
252         self.client.update_server_name(int(server_id), new_name)
253
254     def main(self, server_id, new_name):
255         super(self.__class__, self)._run()
256         self._run(server_id=server_id, new_name=new_name)
257
258
259 @command(server_cmds)
260 class server_delete(_init_cyclades):
261     """Delete a server (VM)"""
262
263     @errors.generic.all
264     @errors.cyclades.connection
265     @errors.cyclades.server_id
266     def _run(self, server_id):
267             self.client.delete_server(int(server_id))
268
269     def main(self, server_id):
270         super(self.__class__, self)._run()
271         self._run(server_id=server_id)
272
273
274 @command(server_cmds)
275 class server_reboot(_init_cyclades):
276     """Reboot a server (VM)"""
277
278     arguments = dict(
279         hard=FlagArgument('perform a hard reboot', ('-f', '--force'))
280     )
281
282     @errors.generic.all
283     @errors.cyclades.connection
284     @errors.cyclades.server_id
285     def _run(self, server_id):
286         self.client.reboot_server(int(server_id), self['hard'])
287
288     def main(self, server_id):
289         super(self.__class__, self)._run()
290         self._run(server_id=server_id)
291
292
293 @command(server_cmds)
294 class server_start(_init_cyclades):
295     """Start an existing server (VM)"""
296
297     @errors.generic.all
298     @errors.cyclades.connection
299     @errors.cyclades.server_id
300     def _run(self, server_id):
301         self.client.start_server(int(server_id))
302
303     def main(self, server_id):
304         super(self.__class__, self)._run()
305         self._run(server_id=server_id)
306
307
308 @command(server_cmds)
309 class server_shutdown(_init_cyclades):
310     """Shutdown an active server (VM)"""
311
312     @errors.generic.all
313     @errors.cyclades.connection
314     @errors.cyclades.server_id
315     def _run(self, server_id):
316         self.client.shutdown_server(int(server_id))
317
318     def main(self, server_id):
319         super(self.__class__, self)._run()
320         self._run(server_id=server_id)
321
322
323 @command(server_cmds)
324 class server_console(_init_cyclades):
325     """Get a VNC console to access an existing server (VM)
326     Console connection information provided (at least):
327     - host: (url or address) a VNC host
328     - port: (int) the gateway to enter VM on host
329     - password: for VNC authorization
330     """
331
332     @errors.generic.all
333     @errors.cyclades.connection
334     @errors.cyclades.server_id
335     def _run(self, server_id):
336         r = self.client.get_server_console(int(server_id))
337         print_dict(r)
338
339     def main(self, server_id):
340         super(self.__class__, self)._run()
341         self._run(server_id=server_id)
342
343
344 @command(server_cmds)
345 class server_firewall(_init_cyclades):
346     """Set the server (VM) firewall profile on VMs public network
347     Values for profile:
348     - DISABLED: Shutdown firewall
349     - ENABLED: Firewall in normal mode
350     - PROTECTED: Firewall in secure mode
351     """
352
353     @errors.generic.all
354     @errors.cyclades.connection
355     @errors.cyclades.server_id
356     @errors.cyclades.firewall
357     def _run(self, server_id, profile):
358         self.client.set_firewall_profile(
359             server_id=int(server_id),
360             profile=('%s' % profile).upper())
361
362     def main(self, server_id, profile):
363         super(self.__class__, self)._run()
364         self._run(server_id=server_id, profile=profile)
365
366
367 @command(server_cmds)
368 class server_addr(_init_cyclades):
369     """List the addresses of all network interfaces on a server (VM)"""
370
371     @errors.generic.all
372     @errors.cyclades.connection
373     @errors.cyclades.server_id
374     def _run(self, server_id):
375         reply = self.client.list_server_nics(int(server_id))
376         print_list(reply, with_enumeration=len(reply) > 1)
377
378     def main(self, server_id):
379         super(self.__class__, self)._run()
380         self._run(server_id=server_id)
381
382
383 @command(server_cmds)
384 class server_meta(_init_cyclades):
385     """Get a server's metadatum
386     Metadata are formed as key:value pairs where key is used to retrieve them
387     """
388
389     @errors.generic.all
390     @errors.cyclades.connection
391     @errors.cyclades.server_id
392     @errors.cyclades.metadata
393     def _run(self, server_id, key=''):
394         r = self.client.get_server_metadata(int(server_id), key)
395         print_dict(r)
396
397     def main(self, server_id, key=''):
398         super(self.__class__, self)._run()
399         self._run(server_id=server_id, key=key)
400
401
402 @command(server_cmds)
403 class server_setmeta(_init_cyclades):
404     """set server (VM) metadata
405     Metadata are formed as key:value pairs, both needed to set one
406     """
407
408     @errors.generic.all
409     @errors.cyclades.connection
410     @errors.cyclades.server_id
411     def _run(self, server_id, key, val):
412         metadata = {key: val}
413         r = self.client.update_server_metadata(int(server_id), **metadata)
414         print_dict(r)
415
416     def main(self, server_id, key, val):
417         super(self.__class__, self)._run()
418         self._run(server_id=server_id, key=key, val=val)
419
420
421 @command(server_cmds)
422 class server_delmeta(_init_cyclades):
423     """Delete server (VM) metadata"""
424
425     @errors.generic.all
426     @errors.cyclades.connection
427     @errors.cyclades.server_id
428     @errors.cyclades.metadata
429     def _run(self, server_id, key):
430         self.client.delete_server_metadata(int(server_id), key)
431
432     def main(self, server_id, key):
433         super(self.__class__, self)._run()
434         self._run(server_id=server_id, key=key)
435
436
437 @command(server_cmds)
438 class server_stats(_init_cyclades):
439     """Get server (VM) statistics"""
440
441     @errors.generic.all
442     @errors.cyclades.connection
443     @errors.cyclades.server_id
444     def _run(self, server_id):
445         r = self.client.get_server_stats(int(server_id))
446         print_dict(r, exclude=('serverRef',))
447
448     def main(self, server_id):
449         super(self.__class__, self)._run()
450         self._run(server_id=server_id)
451
452
453 @command(server_cmds)
454 class server_wait(_init_cyclades):
455     """Wait for server to finish [BUILD, STOPPED, REBOOT, ACTIVE]"""
456
457     arguments = dict(
458         progress_bar=ProgressBarArgument(
459             'do not show progress bar',
460             ('-N', '--no-progress-bar'),
461             False
462         )
463     )
464
465     @errors.generic.all
466     @errors.cyclades.connection
467     @errors.cyclades.server_id
468     def _run(self, server_id, currect_status):
469         (progress_bar, wait_cb) = self._safe_progress_bar(
470             'Server %s still in %s mode' % (server_id, currect_status))
471
472         try:
473             new_mode = self.client.wait_server(
474                 server_id,
475                 currect_status,
476                 wait_cb=wait_cb)
477         except Exception:
478             self._safe_progress_bar_finish(progress_bar)
479             raise
480         finally:
481             self._safe_progress_bar_finish(progress_bar)
482         if new_mode:
483             print('Server %s is now in %s mode' % (server_id, new_mode))
484         else:
485             raiseCLIError(None, 'Time out')
486
487     def main(self, server_id, currect_status='BUILD'):
488         super(self.__class__, self)._run()
489         self._run(server_id=server_id, currect_status=currect_status)
490
491
492 @command(flavor_cmds)
493 class flavor_list(_init_cyclades):
494     """List available hardware flavors"""
495
496     arguments = dict(
497         detail=FlagArgument('show detailed output', ('-l', '--details')),
498         limit=IntArgument('limit # of listed flavors', ('-n', '--number')),
499         more=FlagArgument(
500             'output results in pages (-n to set items per page, default 10)',
501             '--more'),
502         enum=FlagArgument('Enumerate results', '--enumerate')
503     )
504
505     @errors.generic.all
506     @errors.cyclades.connection
507     def _run(self):
508         flavors = self.client.list_flavors(self['detail'])
509         pg_size = 10 if self['more'] and not self['limit'] else self['limit']
510         print_items(
511             flavors,
512             with_redundancy=self['detail'],
513             page_size=pg_size,
514             with_enumeration=self['enum'])
515
516     def main(self):
517         super(self.__class__, self)._run()
518         self._run()
519
520
521 @command(flavor_cmds)
522 class flavor_info(_init_cyclades):
523     """Detailed information on a hardware flavor
524     To get a list of available flavors and flavor ids, try /flavor list
525     """
526
527     @errors.generic.all
528     @errors.cyclades.connection
529     @errors.cyclades.flavor_id
530     def _run(self, flavor_id):
531         flavor = self.client.get_flavor_details(int(flavor_id))
532         print_dict(flavor)
533
534     def main(self, flavor_id):
535         super(self.__class__, self)._run()
536         self._run(flavor_id=flavor_id)
537
538
539 @command(network_cmds)
540 class network_info(_init_cyclades):
541     """Detailed information on a network
542     To get a list of available networks and network ids, try /network list
543     """
544
545     @classmethod
546     def _make_result_pretty(self, net):
547         if 'attachments' in net:
548             att = net['attachments']['values']
549             count = len(att)
550             net['attachments'] = att if count else None
551
552     @errors.generic.all
553     @errors.cyclades.connection
554     @errors.cyclades.network_id
555     def _run(self, network_id):
556         network = self.client.get_network_details(int(network_id))
557         self._make_result_pretty(network)
558         print_dict(network, exclude=('id'))
559
560     def main(self, network_id):
561         super(self.__class__, self)._run()
562         self._run(network_id=network_id)
563
564
565 @command(network_cmds)
566 class network_list(_init_cyclades):
567     """List networks"""
568
569     arguments = dict(
570         detail=FlagArgument('show detailed output', ('-l', '--details')),
571         limit=IntArgument('limit # of listed networks', ('-n', '--number')),
572         more=FlagArgument(
573             'output results in pages (-n to set items per page, default 10)',
574             '--more'),
575         enum=FlagArgument('Enumerate results', '--enumerate')
576     )
577
578     def _make_results_pretty(self, nets):
579         for net in nets:
580             network_info._make_result_pretty(net)
581
582     @errors.generic.all
583     @errors.cyclades.connection
584     def _run(self):
585         networks = self.client.list_networks(self['detail'])
586         if self['detail']:
587             self._make_results_pretty(networks)
588         if self['more']:
589             print_items(
590                 networks,
591                 page_size=self['limit'] or 10, with_enumeration=self['enum'])
592         elif self['limit']:
593             print_items(
594                 networks[:self['limit']],
595                 with_enumeration=self['enum'])
596         else:
597             print_items(networks, with_enumeration=self['enum'])
598
599     def main(self):
600         super(self.__class__, self)._run()
601         self._run()
602
603
604 @command(network_cmds)
605 class network_create(_init_cyclades):
606     """Create an (unconnected) network"""
607
608     arguments = dict(
609         cidr=ValueArgument('explicitly set cidr', '--with-cidr'),
610         gateway=ValueArgument('explicitly set gateway', '--with-gateway'),
611         dhcp=FlagArgument('Use dhcp (default: off)', '--with-dhcp'),
612         type=ValueArgument(
613             'Valid network types are '
614             'CUSTOM, IP_LESS_ROUTED, MAC_FILTERED (default), PHYSICAL_VLAN',
615             '--with-type',
616             default='MAC_FILTERED')
617     )
618
619     @errors.generic.all
620     @errors.cyclades.connection
621     @errors.cyclades.network_max
622     def _run(self, name):
623         r = self.client.create_network(
624             name,
625             cidr=self['cidr'],
626             gateway=self['gateway'],
627             dhcp=self['dhcp'],
628             type=self['type'])
629         print_items([r])
630
631     def main(self, name):
632         super(self.__class__, self)._run()
633         self._run(name)
634
635
636 @command(network_cmds)
637 class network_rename(_init_cyclades):
638     """Set the name of a network"""
639
640     @errors.generic.all
641     @errors.cyclades.connection
642     @errors.cyclades.network_id
643     def _run(self, network_id, new_name):
644         self.client.update_network_name(int(network_id), new_name)
645
646     def main(self, network_id, new_name):
647         super(self.__class__, self)._run()
648         self._run(network_id=network_id, new_name=new_name)
649
650
651 @command(network_cmds)
652 class network_delete(_init_cyclades):
653     """Delete a network"""
654
655     @errors.generic.all
656     @errors.cyclades.connection
657     @errors.cyclades.network_id
658     @errors.cyclades.network_in_use
659     def _run(self, network_id):
660         self.client.delete_network(int(network_id))
661
662     def main(self, network_id):
663         super(self.__class__, self)._run()
664         self._run(network_id=network_id)
665
666
667 @command(network_cmds)
668 class network_connect(_init_cyclades):
669     """Connect a server to a network"""
670
671     @errors.generic.all
672     @errors.cyclades.connection
673     @errors.cyclades.server_id
674     @errors.cyclades.network_id
675     def _run(self, server_id, network_id):
676         self.client.connect_server(int(server_id), int(network_id))
677
678     def main(self, server_id, network_id):
679         super(self.__class__, self)._run()
680         self._run(server_id=server_id, network_id=network_id)
681
682
683 @command(network_cmds)
684 class network_disconnect(_init_cyclades):
685     """Disconnect a nic that connects a server to a network
686     Nic ids are listed as "attachments" in detailed network information
687     To get detailed network information: /network info <network id>
688     """
689
690     @errors.cyclades.nic_format
691     def _server_id_from_nic(self, nic_id):
692         return nic_id.split('-')[1]
693
694     @errors.generic.all
695     @errors.cyclades.connection
696     @errors.cyclades.server_id
697     @errors.cyclades.nic_id
698     def _run(self, nic_id, server_id):
699         if not self.client.disconnect_server(server_id, nic_id):
700             raise ClientError(
701                 'Network Interface %s not found on server %s' % (
702                     nic_id,
703                     server_id),
704                 status=404)
705
706     def main(self, nic_id):
707         super(self.__class__, self)._run()
708         server_id = self._server_id_from_nic(nic_id=nic_id)
709         self._run(nic_id=nic_id, server_id=server_id)