Create/expose methods for mass VM create/delete
[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 base64 import b64encode
35 from os.path import exists
36 from io import StringIO
37 from pydoc import pager
38
39 from kamaki.cli import command
40 from kamaki.cli.command_tree import CommandTree
41 from kamaki.cli.utils import remove_from_items, filter_dicts_by_dict
42 from kamaki.cli.errors import (
43     raiseCLIError, CLISyntaxError, CLIBaseUrlError, CLIInvalidArgument)
44 from kamaki.clients.cyclades import CycladesClient, ClientError
45 from kamaki.cli.argument import FlagArgument, ValueArgument, KeyValueArgument
46 from kamaki.cli.argument import ProgressBarArgument, DateArgument, IntArgument
47 from kamaki.cli.commands import _command_init, errors, addLogSettings
48 from kamaki.cli.commands import (
49     _optional_output_cmd, _optional_json, _name_filter, _id_filter)
50
51
52 server_cmds = CommandTree('server', 'Cyclades/Compute API server commands')
53 flavor_cmds = CommandTree('flavor', 'Cyclades/Compute API flavor commands')
54 network_cmds = CommandTree('network', 'Cyclades/Compute API network commands')
55 ip_cmds = CommandTree('ip', 'Cyclades/Compute API floating ip commands')
56 _commands = [server_cmds, flavor_cmds, network_cmds, ip_cmds]
57
58
59 about_authentication = '\nUser Authentication:\
60     \n* to check authentication: /user authenticate\
61     \n* to set authentication token: /config set cloud.<cloud>.token <token>'
62
63 howto_personality = [
64     'Defines a file to be injected to virtual servers file system.',
65     'syntax:  PATH,[SERVER_PATH,[OWNER,[GROUP,[MODE]]]]',
66     '  [local-path=]PATH: local file to be injected (relative or absolute)',
67     '  [server-path=]SERVER_PATH: destination location inside server Image',
68     '  [owner=]OWNER: virtual servers user id for the remote file',
69     '  [group=]GROUP: virtual servers group id or name for the remote file',
70     '  [mode=]MODE: permission in octal (e.g., 0777 or o+rwx)',
71     'e.g., -p /tmp/my.file,owner=root,mode=0777']
72
73
74 class _service_wait(object):
75
76     wait_arguments = dict(
77         progress_bar=ProgressBarArgument(
78             'do not show progress bar', ('-N', '--no-progress-bar'), False)
79     )
80
81     def _wait(
82             self, service, service_id, status_method, current_status,
83             countdown=True, timeout=60):
84         (progress_bar, wait_cb) = self._safe_progress_bar(
85             '%s %s: status is still %s' % (
86                 service, service_id, current_status),
87             countdown=countdown, timeout=timeout)
88
89         try:
90             new_mode = status_method(
91                 service_id, current_status, max_wait=timeout, wait_cb=wait_cb)
92             if new_mode:
93                 self.error('%s %s: status is now %s' % (
94                     service, service_id, new_mode))
95             else:
96                 self.error('%s %s: status is still %s' % (
97                     service, service_id, current_status))
98         except KeyboardInterrupt:
99             self.error('\n- canceled')
100         finally:
101             self._safe_progress_bar_finish(progress_bar)
102
103
104 class _server_wait(_service_wait):
105
106     def _wait(self, server_id, current_status, timeout=60):
107         super(_server_wait, self)._wait(
108             'Server', server_id, self.client.wait_server, current_status,
109             countdown=(current_status not in ('BUILD', )),
110             timeout=timeout if current_status not in ('BUILD', ) else 100)
111
112
113 class _network_wait(_service_wait):
114
115     def _wait(self, net_id, current_status, timeout=60):
116         super(_network_wait, self)._wait(
117             'Network', net_id, self.client.wait_network, current_status,
118             timeout=timeout)
119
120
121 class _firewall_wait(_service_wait):
122
123     def _wait(self, server_id, current_status, timeout=60):
124         super(_firewall_wait, self)._wait(
125             'Firewall of server',
126             server_id, self.client.wait_firewall, current_status,
127             timeout=timeout)
128
129
130 class _init_cyclades(_command_init):
131     @errors.generic.all
132     @addLogSettings
133     def _run(self, service='compute'):
134         if getattr(self, 'cloud', None):
135             base_url = self._custom_url(service) or self._custom_url(
136                 'cyclades')
137             if base_url:
138                 token = self._custom_token(service) or self._custom_token(
139                     'cyclades') or self.config.get_cloud('token')
140                 self.client = CycladesClient(base_url=base_url, token=token)
141                 return
142         else:
143             self.cloud = 'default'
144         if getattr(self, 'auth_base', False):
145             cyclades_endpoints = self.auth_base.get_service_endpoints(
146                 self._custom_type('cyclades') or 'compute',
147                 self._custom_version('cyclades') or '')
148             base_url = cyclades_endpoints['publicURL']
149             token = self.auth_base.token
150             self.client = CycladesClient(base_url=base_url, token=token)
151         else:
152             raise CLIBaseUrlError(service='cyclades')
153
154     def main(self):
155         self._run()
156
157
158 @command(server_cmds)
159 class server_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
160     """List virtual servers accessible by user
161     Use filtering arguments (e.g., --name-like) to manage long server lists
162     """
163
164     PERMANENTS = ('id', 'name')
165
166     arguments = dict(
167         detail=FlagArgument('show detailed output', ('-l', '--details')),
168         since=DateArgument(
169             'show only items since date (\' d/m/Y H:M:S \')',
170             '--since'),
171         limit=IntArgument(
172             'limit number of listed virtual servers', ('-n', '--number')),
173         more=FlagArgument(
174             'output results in pages (-n to set items per page, default 10)',
175             '--more'),
176         enum=FlagArgument('Enumerate results', '--enumerate'),
177         flavor_id=ValueArgument('filter by flavor id', ('--flavor-id')),
178         image_id=ValueArgument('filter by image id', ('--image-id')),
179         user_id=ValueArgument('filter by user id', ('--user-id')),
180         user_name=ValueArgument('filter by user name', ('--user-name')),
181         status=ValueArgument(
182             'filter by status (ACTIVE, STOPPED, REBOOT, ERROR, etc.)',
183             ('--status')),
184         meta=KeyValueArgument('filter by metadata key=values', ('--metadata')),
185         meta_like=KeyValueArgument(
186             'print only if in key=value, the value is part of actual value',
187             ('--metadata-like')),
188     )
189
190     def _add_user_name(self, servers):
191         uuids = self._uuids2usernames(list(set(
192                 [srv['user_id'] for srv in servers] +
193                 [srv['tenant_id'] for srv in servers])))
194         for srv in servers:
195             srv['user_id'] += ' (%s)' % uuids[srv['user_id']]
196             srv['tenant_id'] += ' (%s)' % uuids[srv['tenant_id']]
197         return servers
198
199     def _apply_common_filters(self, servers):
200         common_filters = dict()
201         if self['status']:
202             common_filters['status'] = self['status']
203         if self['user_id'] or self['user_name']:
204             uuid = self['user_id'] or self._username2uuid(self['user_name'])
205             common_filters['user_id'] = uuid
206         return filter_dicts_by_dict(servers, common_filters)
207
208     def _filter_by_image(self, servers):
209         iid = self['image_id']
210         return [srv for srv in servers if srv['image']['id'] == iid]
211
212     def _filter_by_flavor(self, servers):
213         fid = self['flavor_id']
214         return [srv for srv in servers if (
215             '%s' % srv['image']['id'] == '%s' % fid)]
216
217     def _filter_by_metadata(self, servers):
218         new_servers = []
219         for srv in servers:
220             if not 'metadata' in srv:
221                 continue
222             meta = [dict(srv['metadata'])]
223             if self['meta']:
224                 meta = filter_dicts_by_dict(meta, self['meta'])
225             if meta and self['meta_like']:
226                 meta = filter_dicts_by_dict(
227                     meta, self['meta_like'], exact_match=False)
228             if meta:
229                 new_servers.append(srv)
230         return new_servers
231
232     @errors.generic.all
233     @errors.cyclades.connection
234     @errors.cyclades.date
235     def _run(self):
236         withimage = bool(self['image_id'])
237         withflavor = bool(self['flavor_id'])
238         withmeta = bool(self['meta'] or self['meta_like'])
239         withcommons = bool(
240             self['status'] or self['user_id'] or self['user_name'])
241         detail = self['detail'] or (
242             withimage or withflavor or withmeta or withcommons)
243         servers = self.client.list_servers(detail, self['since'])
244
245         servers = self._filter_by_name(servers)
246         servers = self._filter_by_id(servers)
247         servers = self._apply_common_filters(servers)
248         if withimage:
249             servers = self._filter_by_image(servers)
250         if withflavor:
251             servers = self._filter_by_flavor(servers)
252         if withmeta:
253             servers = self._filter_by_metadata(servers)
254
255         if self['detail'] and not (
256                 self['json_output'] or self['output_format']):
257             servers = self._add_user_name(servers)
258         elif not (self['detail'] or (
259                 self['json_output'] or self['output_format'])):
260             remove_from_items(servers, 'links')
261         if detail and not self['detail']:
262             for srv in servers:
263                 for key in set(srv).difference(self.PERMANENTS):
264                     srv.pop(key)
265         kwargs = dict(with_enumeration=self['enum'])
266         if self['more']:
267             kwargs['out'] = StringIO()
268             kwargs['title'] = ()
269         if self['limit']:
270             servers = servers[:self['limit']]
271         self._print(servers, **kwargs)
272         if self['more']:
273             pager(kwargs['out'].getvalue())
274
275     def main(self):
276         super(self.__class__, self)._run()
277         self._run()
278
279
280 @command(server_cmds)
281 class server_info(_init_cyclades, _optional_json):
282     """Detailed information on a Virtual Machine
283     Contains:
284     - name, id, status, create/update dates
285     - network interfaces
286     - metadata (e.g., os, superuser) and diagnostics
287     - hardware flavor and os image ids
288     """
289
290     @errors.generic.all
291     @errors.cyclades.connection
292     @errors.cyclades.server_id
293     def _run(self, server_id):
294         vm = self.client.get_server_details(server_id)
295         uuids = self._uuids2usernames([vm['user_id'], vm['tenant_id']])
296         vm['user_id'] += ' (%s)' % uuids[vm['user_id']]
297         vm['tenant_id'] += ' (%s)' % uuids[vm['tenant_id']]
298         self._print(vm, self.print_dict)
299
300     def main(self, server_id):
301         super(self.__class__, self)._run()
302         self._run(server_id=server_id)
303
304
305 class PersonalityArgument(KeyValueArgument):
306
307     terms = (
308         ('local-path', 'contents'),
309         ('server-path', 'path'),
310         ('owner', 'owner'),
311         ('group', 'group'),
312         ('mode', 'mode'))
313
314     @property
315     def value(self):
316         return self._value if hasattr(self, '_value') else []
317
318     @value.setter
319     def value(self, newvalue):
320         if newvalue == self.default:
321             return self.value
322         self._value, input_dict = [], {}
323         for i, terms in enumerate(newvalue):
324             termlist = terms.split(',')
325             if len(termlist) > len(self.terms):
326                 msg = 'Wrong number of terms (1<=terms<=%s)' % len(self.terms)
327                 raiseCLIError(CLISyntaxError(msg), details=howto_personality)
328
329             for k, v in self.terms:
330                 prefix = '%s=' % k
331                 for item in termlist:
332                     if item.lower().startswith(prefix):
333                         input_dict[k] = item[len(k) + 1:]
334                         break
335                     item = None
336                 if item:
337                     termlist.remove(item)
338
339             try:
340                 path = input_dict['local-path']
341             except KeyError:
342                 path = termlist.pop(0)
343                 if not path:
344                     raise CLIInvalidArgument(
345                         '--personality: No local path specified',
346                         details=howto_personality)
347
348             if not exists(path):
349                 raise CLIInvalidArgument(
350                     '--personality: File %s does not exist' % path,
351                     details=howto_personality)
352
353             self._value.append(dict(path=path))
354             with open(path) as f:
355                 self._value[i]['contents'] = b64encode(f.read())
356             for k, v in self.terms[1:]:
357                 try:
358                     self._value[i][v] = input_dict[k]
359                 except KeyError:
360                     try:
361                         self._value[i][v] = termlist.pop(0)
362                     except IndexError:
363                         continue
364
365
366 @command(server_cmds)
367 class server_create(_init_cyclades, _optional_json, _server_wait):
368     """Create a server (aka Virtual Machine)
369     Parameters:
370     - name: (single quoted text)
371     - flavor id: Hardware flavor. Pick one from: /flavor list
372     - image id: OS images. Pick one from: /image list
373     """
374
375     arguments = dict(
376         personality=PersonalityArgument(
377             (80 * ' ').join(howto_personality), ('-p', '--personality')),
378         wait=FlagArgument('Wait server to build', ('-w', '--wait'))
379     )
380
381     @errors.generic.all
382     @errors.cyclades.connection
383     @errors.plankton.id
384     @errors.cyclades.flavor_id
385     def _run(self, name, flavor_id, image_id):
386         r = self.client.create_server(
387             name, int(flavor_id), image_id, personality=self['personality'])
388         usernames = self._uuids2usernames([r['user_id'], r['tenant_id']])
389         r['user_id'] += ' (%s)' % usernames[r['user_id']]
390         r['tenant_id'] += ' (%s)' % usernames[r['tenant_id']]
391         self._print(r, self.print_dict)
392         if self['wait']:
393             self._wait(r['id'], r['status'])
394
395     def main(self, name, flavor_id, image_id):
396         super(self.__class__, self)._run()
397         self._run(name=name, flavor_id=flavor_id, image_id=image_id)
398
399
400 @command(server_cmds)
401 class server_rename(_init_cyclades, _optional_output_cmd):
402     """Set/update a virtual server name
403     virtual server names are not unique, therefore multiple servers may share
404     the same name
405     """
406
407     @errors.generic.all
408     @errors.cyclades.connection
409     @errors.cyclades.server_id
410     def _run(self, server_id, new_name):
411         self._optional_output(
412             self.client.update_server_name(int(server_id), new_name))
413
414     def main(self, server_id, new_name):
415         super(self.__class__, self)._run()
416         self._run(server_id=server_id, new_name=new_name)
417
418
419 @command(server_cmds)
420 class server_delete(_init_cyclades, _optional_output_cmd, _server_wait):
421     """Delete a virtual server"""
422
423     arguments = dict(
424         wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
425     )
426
427     @errors.generic.all
428     @errors.cyclades.connection
429     @errors.cyclades.server_id
430     def _run(self, server_id):
431             status = 'DELETED'
432             if self['wait']:
433                 details = self.client.get_server_details(server_id)
434                 status = details['status']
435
436             r = self.client.delete_server(int(server_id))
437             self._optional_output(r)
438
439             if self['wait']:
440                 self._wait(server_id, status)
441
442     def main(self, server_id):
443         super(self.__class__, self)._run()
444         self._run(server_id=server_id)
445
446
447 @command(server_cmds)
448 class server_reboot(_init_cyclades, _optional_output_cmd, _server_wait):
449     """Reboot a virtual server"""
450
451     arguments = dict(
452         hard=FlagArgument(
453             'perform a hard reboot (deprecated)', ('-f', '--force')),
454         type=ValueArgument('SOFT or HARD - default: SOFT', ('--type')),
455         wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
456     )
457
458     @errors.generic.all
459     @errors.cyclades.connection
460     @errors.cyclades.server_id
461     def _run(self, server_id):
462         hard_reboot = self['hard']
463         if hard_reboot:
464             self.error(
465                 'WARNING: -f/--force will be deprecated in version 0.12\n'
466                 '\tIn the future, please use --type=hard instead')
467         if self['type']:
468             if self['type'].lower() in ('soft', ):
469                 hard_reboot = False
470             elif self['type'].lower() in ('hard', ):
471                 hard_reboot = True
472             else:
473                 raise CLISyntaxError(
474                     'Invalid reboot type %s' % self['type'],
475                     importance=2, details=[
476                         '--type values are either SOFT (default) or HARD'])
477
478         r = self.client.reboot_server(int(server_id), hard_reboot)
479         self._optional_output(r)
480
481         if self['wait']:
482             self._wait(server_id, 'REBOOT')
483
484     def main(self, server_id):
485         super(self.__class__, self)._run()
486         self._run(server_id=server_id)
487
488
489 @command(server_cmds)
490 class server_start(_init_cyclades, _optional_output_cmd, _server_wait):
491     """Start an existing virtual server"""
492
493     arguments = dict(
494         wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
495     )
496
497     @errors.generic.all
498     @errors.cyclades.connection
499     @errors.cyclades.server_id
500     def _run(self, server_id):
501         status = 'ACTIVE'
502         if self['wait']:
503             details = self.client.get_server_details(server_id)
504             status = details['status']
505             if status in ('ACTIVE', ):
506                 return
507
508         r = self.client.start_server(int(server_id))
509         self._optional_output(r)
510
511         if self['wait']:
512             self._wait(server_id, status)
513
514     def main(self, server_id):
515         super(self.__class__, self)._run()
516         self._run(server_id=server_id)
517
518
519 @command(server_cmds)
520 class server_shutdown(_init_cyclades, _optional_output_cmd, _server_wait):
521     """Shutdown an active virtual server"""
522
523     arguments = dict(
524         wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
525     )
526
527     @errors.generic.all
528     @errors.cyclades.connection
529     @errors.cyclades.server_id
530     def _run(self, server_id):
531         status = 'STOPPED'
532         if self['wait']:
533             details = self.client.get_server_details(server_id)
534             status = details['status']
535             if status in ('STOPPED', ):
536                 return
537
538         r = self.client.shutdown_server(int(server_id))
539         self._optional_output(r)
540
541         if self['wait']:
542             self._wait(server_id, status)
543
544     def main(self, server_id):
545         super(self.__class__, self)._run()
546         self._run(server_id=server_id)
547
548
549 @command(server_cmds)
550 class server_console(_init_cyclades, _optional_json):
551     """Get a VNC console to access an existing virtual server
552     Console connection information provided (at least):
553     - host: (url or address) a VNC host
554     - port: (int) the gateway to enter virtual server on host
555     - password: for VNC authorization
556     """
557
558     @errors.generic.all
559     @errors.cyclades.connection
560     @errors.cyclades.server_id
561     def _run(self, server_id):
562         self._print(
563             self.client.get_server_console(int(server_id)), self.print_dict)
564
565     def main(self, server_id):
566         super(self.__class__, self)._run()
567         self._run(server_id=server_id)
568
569
570 @command(server_cmds)
571 class server_resize(_init_cyclades, _optional_output_cmd):
572     """Set a different flavor for an existing server
573     To get server ids and flavor ids:
574     /server list
575     /flavor list
576     """
577
578     @errors.generic.all
579     @errors.cyclades.connection
580     @errors.cyclades.server_id
581     @errors.cyclades.flavor_id
582     def _run(self, server_id, flavor_id):
583         self._optional_output(self.client.resize_server(server_id, flavor_id))
584
585     def main(self, server_id, flavor_id):
586         super(self.__class__, self)._run()
587         self._run(server_id=server_id, flavor_id=flavor_id)
588
589
590 @command(server_cmds)
591 class server_firewall(_init_cyclades):
592     """Manage virtual server firewall profiles for public networks"""
593
594
595 @command(server_cmds)
596 class server_firewall_set(
597         _init_cyclades, _optional_output_cmd, _firewall_wait):
598     """Set the firewall profile on virtual server public network
599     Values for profile:
600     - DISABLED: Shutdown firewall
601     - ENABLED: Firewall in normal mode
602     - PROTECTED: Firewall in secure mode
603     """
604
605     arguments = dict(
606         wait=FlagArgument('Wait server firewall to build', ('-w', '--wait')),
607         timeout=IntArgument(
608             'Set wait timeout in seconds (default: 60)', '--timeout',
609             default=60)
610     )
611
612     @errors.generic.all
613     @errors.cyclades.connection
614     @errors.cyclades.server_id
615     @errors.cyclades.firewall
616     def _run(self, server_id, profile):
617         if self['timeout'] and not self['wait']:
618             raise CLIInvalidArgument('Invalid use of --timeout', details=[
619                 'Timeout is used only along with -w/--wait'])
620         old_profile = self.client.get_firewall_profile(server_id)
621         if old_profile.lower() == profile.lower():
622             self.error('Firewall of server %s: allready in status %s' % (
623                 server_id, old_profile))
624         else:
625             self._optional_output(self.client.set_firewall_profile(
626                 server_id=int(server_id), profile=('%s' % profile).upper()))
627             if self['wait']:
628                 self._wait(server_id, old_profile, timeout=self['timeout'])
629
630     def main(self, server_id, profile):
631         super(self.__class__, self)._run()
632         self._run(server_id=server_id, profile=profile)
633
634
635 @command(server_cmds)
636 class server_firewall_get(_init_cyclades):
637     """Get the firewall profile for a virtual servers' public network"""
638
639     @errors.generic.all
640     @errors.cyclades.connection
641     @errors.cyclades.server_id
642     def _run(self, server_id):
643         self.writeln(self.client.get_firewall_profile(server_id))
644
645     def main(self, server_id):
646         super(self.__class__, self)._run()
647         self._run(server_id=server_id)
648
649
650 @command(server_cmds)
651 class server_addr(_init_cyclades, _optional_json):
652     """List the addresses of all network interfaces on a virtual server"""
653
654     arguments = dict(
655         enum=FlagArgument('Enumerate results', '--enumerate')
656     )
657
658     @errors.generic.all
659     @errors.cyclades.connection
660     @errors.cyclades.server_id
661     def _run(self, server_id):
662         reply = self.client.list_server_nics(int(server_id))
663         self._print(reply, with_enumeration=self['enum'] and (reply) > 1)
664
665     def main(self, server_id):
666         super(self.__class__, self)._run()
667         self._run(server_id=server_id)
668
669
670 @command(server_cmds)
671 class server_metadata(_init_cyclades):
672     """Manage Server metadata (key:value pairs of server attributes)"""
673
674
675 @command(server_cmds)
676 class server_metadata_list(_init_cyclades, _optional_json):
677     """Get server metadata"""
678
679     @errors.generic.all
680     @errors.cyclades.connection
681     @errors.cyclades.server_id
682     @errors.cyclades.metadata
683     def _run(self, server_id, key=''):
684         self._print(
685             self.client.get_server_metadata(int(server_id), key),
686             self.print_dict)
687
688     def main(self, server_id, key=''):
689         super(self.__class__, self)._run()
690         self._run(server_id=server_id, key=key)
691
692
693 @command(server_cmds)
694 class server_metadata_set(_init_cyclades, _optional_json):
695     """Set / update virtual server metadata
696     Metadata should be given in key/value pairs in key=value format
697     For example: /server metadata set <server id> key1=value1 key2=value2
698     Old, unreferenced metadata will remain intact
699     """
700
701     @errors.generic.all
702     @errors.cyclades.connection
703     @errors.cyclades.server_id
704     def _run(self, server_id, keyvals):
705         assert keyvals, 'Please, add some metadata ( key=value)'
706         metadata = dict()
707         for keyval in keyvals:
708             k, sep, v = keyval.partition('=')
709             if sep and k:
710                 metadata[k] = v
711             else:
712                 raiseCLIError(
713                     'Invalid piece of metadata %s' % keyval,
714                     importance=2, details=[
715                         'Correct metadata format: key=val',
716                         'For example:',
717                         '/server metadata set <server id>'
718                         'key1=value1 key2=value2'])
719         self._print(
720             self.client.update_server_metadata(int(server_id), **metadata),
721             self.print_dict)
722
723     def main(self, server_id, *key_equals_val):
724         super(self.__class__, self)._run()
725         self._run(server_id=server_id, keyvals=key_equals_val)
726
727
728 @command(server_cmds)
729 class server_metadata_delete(_init_cyclades, _optional_output_cmd):
730     """Delete virtual server metadata"""
731
732     @errors.generic.all
733     @errors.cyclades.connection
734     @errors.cyclades.server_id
735     @errors.cyclades.metadata
736     def _run(self, server_id, key):
737         self._optional_output(
738             self.client.delete_server_metadata(int(server_id), key))
739
740     def main(self, server_id, key):
741         super(self.__class__, self)._run()
742         self._run(server_id=server_id, key=key)
743
744
745 @command(server_cmds)
746 class server_stats(_init_cyclades, _optional_json):
747     """Get virtual server statistics"""
748
749     @errors.generic.all
750     @errors.cyclades.connection
751     @errors.cyclades.server_id
752     def _run(self, server_id):
753         self._print(
754             self.client.get_server_stats(int(server_id)), self.print_dict)
755
756     def main(self, server_id):
757         super(self.__class__, self)._run()
758         self._run(server_id=server_id)
759
760
761 @command(server_cmds)
762 class server_wait(_init_cyclades, _server_wait):
763     """Wait for server to finish [BUILD, STOPPED, REBOOT, ACTIVE]"""
764
765     arguments = dict(
766         timeout=IntArgument(
767             'Wait limit in seconds (default: 60)', '--timeout', default=60)
768     )
769
770     @errors.generic.all
771     @errors.cyclades.connection
772     @errors.cyclades.server_id
773     def _run(self, server_id, current_status):
774         r = self.client.get_server_details(server_id)
775         if r['status'].lower() == current_status.lower():
776             self._wait(server_id, current_status, timeout=self['timeout'])
777         else:
778             self.error(
779                 'Server %s: Cannot wait for status %s, '
780                 'status is already %s' % (
781                     server_id, current_status, r['status']))
782
783     def main(self, server_id, current_status='BUILD'):
784         super(self.__class__, self)._run()
785         self._run(server_id=server_id, current_status=current_status)
786
787
788 @command(server_cmds)
789 class server_cluster_create(_init_cyclades):
790     """Create a cluster of virtual servers
791     All new servers will be named as <prefix><increment> e.g.,
792     mycluster1, mycluster2, etc.
793     All servers in the cluster will run the same image on the same hardware
794     flavor.
795     """
796
797     @errors.generic.all
798     @errors.cyclades.connection
799     @errors.plankton.id
800     @errors.cyclades.flavor_id
801     @errors.cyclades.cluster_size
802     def _run(self, prefix, image_id, flavor_id, size):
803         servers = [dict(
804             name='%s%s' % (prefix, i),
805             flavor_id=flavor_id,
806             image_id=image_id) for i in range(int(size))]
807         self.client.create_cluster(servers)
808
809     def main(self, prefix, image_id, flavor_id, size):
810         super(self.__class__, self)._run()
811         self._run(prefix, image_id=image_id, flavor_id=flavor_id, size=size)
812
813
814 @command(server_cmds)
815 class server_cluster_delete(_init_cyclades):
816     """Remove all servers that belong to a virtual cluster
817     A virtual cluster consists of the virtual servers with the same name prefix
818     ATTENTION: make sure you want to delete all servers of that prefix
819     To get a list of your servers:  /server list
820     """
821
822     @errors.generic.all
823     @errors.cyclades.connection
824     def _run(self, prefix):
825         servers = [s['id'] for s in self.client.list_servers() if (
826             s['name'].startswith(prefix))]
827         self.client.delete_cluster(servers)
828
829     def main(self, prefix):
830         super(self.__class__, self)._run()
831         self._run(prefix)
832
833
834 @command(flavor_cmds)
835 class flavor_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
836     """List available hardware flavors"""
837
838     PERMANENTS = ('id', 'name')
839
840     arguments = dict(
841         detail=FlagArgument('show detailed output', ('-l', '--details')),
842         limit=IntArgument('limit # of listed flavors', ('-n', '--number')),
843         more=FlagArgument(
844             'output results in pages (-n to set items per page, default 10)',
845             '--more'),
846         enum=FlagArgument('Enumerate results', '--enumerate'),
847         ram=ValueArgument('filter by ram', ('--ram')),
848         vcpus=ValueArgument('filter by number of VCPUs', ('--vcpus')),
849         disk=ValueArgument('filter by disk size in GB', ('--disk')),
850         disk_template=ValueArgument(
851             'filter by disk_templace', ('--disk-template'))
852     )
853
854     def _apply_common_filters(self, flavors):
855         common_filters = dict()
856         if self['ram']:
857             common_filters['ram'] = self['ram']
858         if self['vcpus']:
859             common_filters['vcpus'] = self['vcpus']
860         if self['disk']:
861             common_filters['disk'] = self['disk']
862         if self['disk_template']:
863             common_filters['SNF:disk_template'] = self['disk_template']
864         return filter_dicts_by_dict(flavors, common_filters)
865
866     @errors.generic.all
867     @errors.cyclades.connection
868     def _run(self):
869         withcommons = self['ram'] or self['vcpus'] or (
870             self['disk'] or self['disk_template'])
871         detail = self['detail'] or withcommons
872         flavors = self.client.list_flavors(detail)
873         flavors = self._filter_by_name(flavors)
874         flavors = self._filter_by_id(flavors)
875         if withcommons:
876             flavors = self._apply_common_filters(flavors)
877         if not (self['detail'] or (
878                 self['json_output'] or self['output_format'])):
879             remove_from_items(flavors, 'links')
880         if detail and not self['detail']:
881             for flv in flavors:
882                 for key in set(flv).difference(self.PERMANENTS):
883                     flv.pop(key)
884         kwargs = dict(out=StringIO(), title=()) if self['more'] else {}
885         self._print(
886             flavors,
887             with_redundancy=self['detail'], with_enumeration=self['enum'],
888             **kwargs)
889         if self['more']:
890             pager(kwargs['out'].getvalue())
891
892     def main(self):
893         super(self.__class__, self)._run()
894         self._run()
895
896
897 @command(flavor_cmds)
898 class flavor_info(_init_cyclades, _optional_json):
899     """Detailed information on a hardware flavor
900     To get a list of available flavors and flavor ids, try /flavor list
901     """
902
903     @errors.generic.all
904     @errors.cyclades.connection
905     @errors.cyclades.flavor_id
906     def _run(self, flavor_id):
907         self._print(
908             self.client.get_flavor_details(int(flavor_id)), self.print_dict)
909
910     def main(self, flavor_id):
911         super(self.__class__, self)._run()
912         self._run(flavor_id=flavor_id)
913
914
915 def _add_name(self, net):
916         user_id, tenant_id, uuids = net['user_id'], net['tenant_id'], []
917         if user_id:
918             uuids.append(user_id)
919         if tenant_id:
920             uuids.append(tenant_id)
921         if uuids:
922             usernames = self._uuids2usernames(uuids)
923             if user_id:
924                 net['user_id'] += ' (%s)' % usernames[user_id]
925             if tenant_id:
926                 net['tenant_id'] += ' (%s)' % usernames[tenant_id]
927
928
929 @command(network_cmds)
930 class network_info(_init_cyclades, _optional_json):
931     """Detailed information on a network
932     To get a list of available networks and network ids, try /network list
933     """
934
935     @errors.generic.all
936     @errors.cyclades.connection
937     @errors.cyclades.network_id
938     def _run(self, network_id):
939         network = self.client.get_network_details(int(network_id))
940         _add_name(self, network)
941         self._print(network, self.print_dict, exclude=('id'))
942
943     def main(self, network_id):
944         super(self.__class__, self)._run()
945         self._run(network_id=network_id)
946
947
948 @command(network_cmds)
949 class network_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
950     """List networks"""
951
952     PERMANENTS = ('id', 'name')
953
954     arguments = dict(
955         detail=FlagArgument('show detailed output', ('-l', '--details')),
956         limit=IntArgument('limit # of listed networks', ('-n', '--number')),
957         more=FlagArgument(
958             'output results in pages (-n to set items per page, default 10)',
959             '--more'),
960         enum=FlagArgument('Enumerate results', '--enumerate'),
961         status=ValueArgument('filter by status', ('--status')),
962         public=FlagArgument('only public networks', ('--public')),
963         private=FlagArgument('only private networks', ('--private')),
964         dhcp=FlagArgument('show networks with dhcp', ('--with-dhcp')),
965         no_dhcp=FlagArgument('show networks without dhcp', ('--without-dhcp')),
966         user_id=ValueArgument('filter by user id', ('--user-id')),
967         user_name=ValueArgument('filter by user name', ('--user-name')),
968         gateway=ValueArgument('filter by gateway (IPv4)', ('--gateway')),
969         gateway6=ValueArgument('filter by gateway (IPv6)', ('--gateway6')),
970         cidr=ValueArgument('filter by cidr (IPv4)', ('--cidr')),
971         cidr6=ValueArgument('filter by cidr (IPv6)', ('--cidr6')),
972         type=ValueArgument('filter by type', ('--type')),
973     )
974
975     def _apply_common_filters(self, networks):
976         common_filter = dict()
977         if self['public']:
978             if self['private']:
979                 return []
980             common_filter['public'] = self['public']
981         elif self['private']:
982             common_filter['public'] = False
983         if self['dhcp']:
984             if self['no_dhcp']:
985                 return []
986             common_filter['dhcp'] = True
987         elif self['no_dhcp']:
988             common_filter['dhcp'] = False
989         if self['user_id'] or self['user_name']:
990             uuid = self['user_id'] or self._username2uuid(self['user_name'])
991             common_filter['user_id'] = uuid
992         for term in ('status', 'gateway', 'gateway6', 'cidr', 'cidr6', 'type'):
993             if self[term]:
994                 common_filter[term] = self[term]
995         return filter_dicts_by_dict(networks, common_filter)
996
997     def _add_name(self, networks, key='user_id'):
998         uuids = self._uuids2usernames(
999             list(set([net[key] for net in networks])))
1000         for net in networks:
1001             v = net.get(key, None)
1002             if v:
1003                 net[key] += ' (%s)' % uuids[v]
1004         return networks
1005
1006     @errors.generic.all
1007     @errors.cyclades.connection
1008     def _run(self):
1009         withcommons = False
1010         for term in (
1011                 'status', 'public', 'private', 'user_id', 'user_name', 'type',
1012                 'gateway', 'gateway6', 'cidr', 'cidr6', 'dhcp', 'no_dhcp'):
1013             if self[term]:
1014                 withcommons = True
1015                 break
1016         detail = self['detail'] or withcommons
1017         networks = self.client.list_networks(detail)
1018         networks = self._filter_by_name(networks)
1019         networks = self._filter_by_id(networks)
1020         if withcommons:
1021             networks = self._apply_common_filters(networks)
1022         if not (self['detail'] or (
1023                 self['json_output'] or self['output_format'])):
1024             remove_from_items(networks, 'links')
1025         if detail and not self['detail']:
1026             for net in networks:
1027                 for key in set(net).difference(self.PERMANENTS):
1028                     net.pop(key)
1029         if self['detail'] and not (
1030                 self['json_output'] or self['output_format']):
1031             self._add_name(networks)
1032             self._add_name(networks, 'tenant_id')
1033         kwargs = dict(with_enumeration=self['enum'])
1034         if self['more']:
1035             kwargs['out'] = StringIO()
1036             kwargs['title'] = ()
1037         if self['limit']:
1038             networks = networks[:self['limit']]
1039         self._print(networks, **kwargs)
1040         if self['more']:
1041             pager(kwargs['out'].getvalue())
1042
1043     def main(self):
1044         super(self.__class__, self)._run()
1045         self._run()
1046
1047
1048 @command(network_cmds)
1049 class network_create(_init_cyclades, _optional_json, _network_wait):
1050     """Create an (unconnected) network"""
1051
1052     arguments = dict(
1053         cidr=ValueArgument('explicitly set cidr', '--with-cidr'),
1054         gateway=ValueArgument('explicitly set gateway', '--with-gateway'),
1055         dhcp=FlagArgument('Use dhcp (default: off)', '--with-dhcp'),
1056         type=ValueArgument(
1057             'Valid network types are '
1058             'CUSTOM, IP_LESS_ROUTED, MAC_FILTERED (default), PHYSICAL_VLAN',
1059             '--with-type',
1060             default='MAC_FILTERED'),
1061         wait=FlagArgument('Wait network to build', ('-w', '--wait'))
1062     )
1063
1064     @errors.generic.all
1065     @errors.cyclades.connection
1066     @errors.cyclades.network_max
1067     def _run(self, name):
1068         r = self.client.create_network(
1069             name,
1070             cidr=self['cidr'],
1071             gateway=self['gateway'],
1072             dhcp=self['dhcp'],
1073             type=self['type'])
1074         _add_name(self, r)
1075         self._print(r, self.print_dict)
1076         if self['wait'] and r['status'] in ('PENDING', ):
1077             self._wait(r['id'], 'PENDING')
1078
1079     def main(self, name):
1080         super(self.__class__, self)._run()
1081         self._run(name)
1082
1083
1084 @command(network_cmds)
1085 class network_rename(_init_cyclades, _optional_output_cmd):
1086     """Set the name of a network"""
1087
1088     @errors.generic.all
1089     @errors.cyclades.connection
1090     @errors.cyclades.network_id
1091     def _run(self, network_id, new_name):
1092         self._optional_output(
1093                 self.client.update_network_name(int(network_id), new_name))
1094
1095     def main(self, network_id, new_name):
1096         super(self.__class__, self)._run()
1097         self._run(network_id=network_id, new_name=new_name)
1098
1099
1100 @command(network_cmds)
1101 class network_delete(_init_cyclades, _optional_output_cmd, _network_wait):
1102     """Delete a network"""
1103
1104     arguments = dict(
1105         wait=FlagArgument('Wait network to build', ('-w', '--wait'))
1106     )
1107
1108     @errors.generic.all
1109     @errors.cyclades.connection
1110     @errors.cyclades.network_in_use
1111     @errors.cyclades.network_id
1112     def _run(self, network_id):
1113         status = 'DELETED'
1114         if self['wait']:
1115             r = self.client.get_network_details(network_id)
1116             status = r['status']
1117             if status in ('DELETED', ):
1118                 return
1119
1120         r = self.client.delete_network(int(network_id))
1121         self._optional_output(r)
1122
1123         if self['wait']:
1124             self._wait(network_id, status)
1125
1126     def main(self, network_id):
1127         super(self.__class__, self)._run()
1128         self._run(network_id=network_id)
1129
1130
1131 @command(network_cmds)
1132 class network_connect(_init_cyclades, _optional_output_cmd):
1133     """Connect a server to a network"""
1134
1135     @errors.generic.all
1136     @errors.cyclades.connection
1137     @errors.cyclades.server_id
1138     @errors.cyclades.network_id
1139     def _run(self, server_id, network_id):
1140         self._optional_output(
1141                 self.client.connect_server(int(server_id), int(network_id)))
1142
1143     def main(self, server_id, network_id):
1144         super(self.__class__, self)._run()
1145         self._run(server_id=server_id, network_id=network_id)
1146
1147
1148 @command(network_cmds)
1149 class network_disconnect(_init_cyclades):
1150     """Disconnect a nic that connects a server to a network
1151     Nic ids are listed as "attachments" in detailed network information
1152     To get detailed network information: /network info <network id>
1153     """
1154
1155     @errors.cyclades.nic_format
1156     def _server_id_from_nic(self, nic_id):
1157         return nic_id.split('-')[1]
1158
1159     @errors.generic.all
1160     @errors.cyclades.connection
1161     @errors.cyclades.server_id
1162     @errors.cyclades.nic_id
1163     def _run(self, nic_id, server_id):
1164         num_of_disconnected = self.client.disconnect_server(server_id, nic_id)
1165         if not num_of_disconnected:
1166             raise ClientError(
1167                 'Network Interface %s not found on server %s' % (
1168                     nic_id, server_id),
1169                 status=404)
1170         print('Disconnected %s connections' % num_of_disconnected)
1171
1172     def main(self, nic_id):
1173         super(self.__class__, self)._run()
1174         server_id = self._server_id_from_nic(nic_id=nic_id)
1175         self._run(nic_id=nic_id, server_id=server_id)
1176
1177
1178 @command(network_cmds)
1179 class network_wait(_init_cyclades, _network_wait):
1180     """Wait for server to finish [PENDING, ACTIVE, DELETED]"""
1181
1182     arguments = dict(
1183         timeout=IntArgument(
1184             'Wait limit in seconds (default: 60)', '--timeout', default=60)
1185     )
1186
1187     @errors.generic.all
1188     @errors.cyclades.connection
1189     @errors.cyclades.network_id
1190     def _run(self, network_id, current_status):
1191         net = self.client.get_network_details(network_id)
1192         if net['status'].lower() == current_status.lower():
1193             self._wait(network_id, current_status, timeout=self['timeout'])
1194         else:
1195             self.error(
1196                 'Network %s: Cannot wait for status %s, '
1197                 'status is already %s' % (
1198                     network_id, current_status, net['status']))
1199
1200     def main(self, network_id, current_status='PENDING'):
1201         super(self.__class__, self)._run()
1202         self._run(network_id=network_id, current_status=current_status)
1203
1204
1205 @command(ip_cmds)
1206 class ip_pools(_init_cyclades, _optional_json):
1207     """List pools of floating IPs"""
1208
1209     @errors.generic.all
1210     @errors.cyclades.connection
1211     def _run(self):
1212         r = self.client.get_floating_ip_pools()
1213         self._print(r if self['json_output'] or self['output_format'] else r[
1214             'floating_ip_pools'])
1215
1216     def main(self):
1217         super(self.__class__, self)._run()
1218         self._run()
1219
1220
1221 @command(ip_cmds)
1222 class ip_list(_init_cyclades, _optional_json):
1223     """List reserved floating IPs"""
1224
1225     @errors.generic.all
1226     @errors.cyclades.connection
1227     def _run(self):
1228         r = self.client.get_floating_ips()
1229         self._print(r if self['json_output'] or self['output_format'] else r[
1230             'floating_ips'])
1231
1232     def main(self):
1233         super(self.__class__, self)._run()
1234         self._run()
1235
1236
1237 @command(ip_cmds)
1238 class ip_info(_init_cyclades, _optional_json):
1239     """Details for an IP"""
1240
1241     @errors.generic.all
1242     @errors.cyclades.connection
1243     def _run(self, ip):
1244         self._print(self.client.get_floating_ip(ip), self.print_dict)
1245
1246     def main(self, IP):
1247         super(self.__class__, self)._run()
1248         self._run(ip=IP)
1249
1250
1251 @command(ip_cmds)
1252 class ip_reserve(_init_cyclades, _optional_json):
1253     """Reserve a floating IP
1254     An IP is reserved from an IP pool. The default IP pool is chosen
1255     automatically, but there is the option if specifying an explicit IP pool.
1256     """
1257
1258     arguments = dict(pool=ValueArgument('Source IP pool', ('--pool'), None))
1259
1260     @errors.generic.all
1261     @errors.cyclades.connection
1262     def _run(self, ip=None):
1263         self._print([self.client.alloc_floating_ip(self['pool'], ip)])
1264
1265     def main(self, requested_IP=None):
1266         super(self.__class__, self)._run()
1267         self._run(ip=requested_IP)
1268
1269
1270 @command(ip_cmds)
1271 class ip_release(_init_cyclades, _optional_output_cmd):
1272     """Release a floating IP
1273     The release IP is "returned" to the IP pool it came from.
1274     """
1275
1276     @errors.generic.all
1277     @errors.cyclades.connection
1278     def _run(self, ip):
1279         self._optional_output(self.client.delete_floating_ip(ip))
1280
1281     def main(self, IP):
1282         super(self.__class__, self)._run()
1283         self._run(ip=IP)
1284
1285
1286 @command(ip_cmds)
1287 class ip_attach(_init_cyclades, _optional_output_cmd):
1288     """Attach a floating IP to a server
1289     """
1290
1291     @errors.generic.all
1292     @errors.cyclades.connection
1293     @errors.cyclades.server_id
1294     def _run(self, server_id, ip):
1295         self._optional_output(self.client.attach_floating_ip(server_id, ip))
1296
1297     def main(self, server_id, IP):
1298         super(self.__class__, self)._run()
1299         self._run(server_id=server_id, ip=IP)
1300
1301
1302 @command(ip_cmds)
1303 class ip_detach(_init_cyclades, _optional_output_cmd):
1304     """Detach a floating IP from a server
1305     """
1306
1307     @errors.generic.all
1308     @errors.cyclades.connection
1309     @errors.cyclades.server_id
1310     def _run(self, server_id, ip):
1311         self._optional_output(self.client.detach_floating_ip(server_id, ip))
1312
1313     def main(self, server_id, IP):
1314         super(self.__class__, self)._run()
1315         self._run(server_id=server_id, ip=IP)