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