Move cluster handling in server 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         cluster_size=IntArgument(
380             'Create a cluster of servers of this size. In this case, the name'
381             'parameter is the prefix of each server in the cluster (e.g.,'
382             'srv1, srv2, etc.',
383             '--cluster-size')
384     )
385
386     @errors.cyclades.cluster_size
387     def _create_cluster(self, prefix, flavor_id, image_id, size):
388         servers = [dict(
389             name='%s%s' % (prefix, i),
390             flavor_id=flavor_id,
391             image_id=image_id,
392             personality=self['personality']) for i in range(size)]
393         if size == 1:
394             return [self.client.create_server(**servers[0])]
395         return self.client.async_run(self.client.create_server, servers)
396
397     @errors.generic.all
398     @errors.cyclades.connection
399     @errors.plankton.id
400     @errors.cyclades.flavor_id
401     def _run(self, name, flavor_id, image_id):
402         for r in self._create_cluster(
403                 name, flavor_id, image_id, size=self['cluster_size'] or 1):
404             print 'HEY I GOT A', r
405             print 'MKEY?????????????????'
406             usernames = self._uuids2usernames([r['user_id'], r['tenant_id']])
407             r['user_id'] += ' (%s)' % usernames[r['user_id']]
408             r['tenant_id'] += ' (%s)' % usernames[r['tenant_id']]
409             self._print(r, self.print_dict)
410             if self['wait']:
411                 self._wait(r['id'], r['status'])
412             self.error('')
413
414     def main(self, name, flavor_id, image_id):
415         super(self.__class__, self)._run()
416         self._run(name=name, flavor_id=flavor_id, image_id=image_id)
417
418
419 @command(server_cmds)
420 class server_rename(_init_cyclades, _optional_output_cmd):
421     """Set/update a virtual server name
422     virtual server names are not unique, therefore multiple servers may share
423     the same name
424     """
425
426     @errors.generic.all
427     @errors.cyclades.connection
428     @errors.cyclades.server_id
429     def _run(self, server_id, new_name):
430         self._optional_output(
431             self.client.update_server_name(int(server_id), new_name))
432
433     def main(self, server_id, new_name):
434         super(self.__class__, self)._run()
435         self._run(server_id=server_id, new_name=new_name)
436
437
438 @command(server_cmds)
439 class server_delete(_init_cyclades, _optional_output_cmd, _server_wait):
440     """Delete a virtual server"""
441
442     arguments = dict(
443         wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait')),
444         cluster=FlagArgument(
445             '(DANGEROUS) Delete all virtual servers prefixed with the cluster '
446             'prefix. In that case, the prefix replaces the server id',
447             '--cluster')
448     )
449
450     def _server_ids(self, server_var):
451         if self['cluster']:
452             return [s['id'] for s in self.client.list_servers() if (
453                 s['name'].startswith(server_var))]
454
455         @errors.cyclades.server_id
456         def _check_server_id(self, server_id):
457             return server_id
458
459         return [_check_server_id(self, server_id=server_var), ]
460
461     @errors.generic.all
462     @errors.cyclades.connection
463     def _run(self, server_var):
464         for server_id in self._server_ids(server_var):
465             if self['wait']:
466                 details = self.client.get_server_details(server_id)
467                 status = details['status']
468
469             r = self.client.delete_server(server_id)
470             self._optional_output(r)
471
472             if self['wait']:
473                 self._wait(server_id, status)
474
475     def main(self, server_id_or_cluster_prefix):
476         super(self.__class__, self)._run()
477         self._run(server_id_or_cluster_prefix)
478
479
480 @command(server_cmds)
481 class server_reboot(_init_cyclades, _optional_output_cmd, _server_wait):
482     """Reboot a virtual server"""
483
484     arguments = dict(
485         hard=FlagArgument(
486             'perform a hard reboot (deprecated)', ('-f', '--force')),
487         type=ValueArgument('SOFT or HARD - default: SOFT', ('--type')),
488         wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
489     )
490
491     @errors.generic.all
492     @errors.cyclades.connection
493     @errors.cyclades.server_id
494     def _run(self, server_id):
495         hard_reboot = self['hard']
496         if hard_reboot:
497             self.error(
498                 'WARNING: -f/--force will be deprecated in version 0.12\n'
499                 '\tIn the future, please use --type=hard instead')
500         if self['type']:
501             if self['type'].lower() in ('soft', ):
502                 hard_reboot = False
503             elif self['type'].lower() in ('hard', ):
504                 hard_reboot = True
505             else:
506                 raise CLISyntaxError(
507                     'Invalid reboot type %s' % self['type'],
508                     importance=2, details=[
509                         '--type values are either SOFT (default) or HARD'])
510
511         r = self.client.reboot_server(int(server_id), hard_reboot)
512         self._optional_output(r)
513
514         if self['wait']:
515             self._wait(server_id, 'REBOOT')
516
517     def main(self, server_id):
518         super(self.__class__, self)._run()
519         self._run(server_id=server_id)
520
521
522 @command(server_cmds)
523 class server_start(_init_cyclades, _optional_output_cmd, _server_wait):
524     """Start an existing virtual server"""
525
526     arguments = dict(
527         wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
528     )
529
530     @errors.generic.all
531     @errors.cyclades.connection
532     @errors.cyclades.server_id
533     def _run(self, server_id):
534         status = 'ACTIVE'
535         if self['wait']:
536             details = self.client.get_server_details(server_id)
537             status = details['status']
538             if status in ('ACTIVE', ):
539                 return
540
541         r = self.client.start_server(int(server_id))
542         self._optional_output(r)
543
544         if self['wait']:
545             self._wait(server_id, status)
546
547     def main(self, server_id):
548         super(self.__class__, self)._run()
549         self._run(server_id=server_id)
550
551
552 @command(server_cmds)
553 class server_shutdown(_init_cyclades, _optional_output_cmd, _server_wait):
554     """Shutdown an active virtual server"""
555
556     arguments = dict(
557         wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
558     )
559
560     @errors.generic.all
561     @errors.cyclades.connection
562     @errors.cyclades.server_id
563     def _run(self, server_id):
564         status = 'STOPPED'
565         if self['wait']:
566             details = self.client.get_server_details(server_id)
567             status = details['status']
568             if status in ('STOPPED', ):
569                 return
570
571         r = self.client.shutdown_server(int(server_id))
572         self._optional_output(r)
573
574         if self['wait']:
575             self._wait(server_id, status)
576
577     def main(self, server_id):
578         super(self.__class__, self)._run()
579         self._run(server_id=server_id)
580
581
582 @command(server_cmds)
583 class server_console(_init_cyclades, _optional_json):
584     """Get a VNC console to access an existing virtual server
585     Console connection information provided (at least):
586     - host: (url or address) a VNC host
587     - port: (int) the gateway to enter virtual server on host
588     - password: for VNC authorization
589     """
590
591     @errors.generic.all
592     @errors.cyclades.connection
593     @errors.cyclades.server_id
594     def _run(self, server_id):
595         self._print(
596             self.client.get_server_console(int(server_id)), self.print_dict)
597
598     def main(self, server_id):
599         super(self.__class__, self)._run()
600         self._run(server_id=server_id)
601
602
603 @command(server_cmds)
604 class server_resize(_init_cyclades, _optional_output_cmd):
605     """Set a different flavor for an existing server
606     To get server ids and flavor ids:
607     /server list
608     /flavor list
609     """
610
611     @errors.generic.all
612     @errors.cyclades.connection
613     @errors.cyclades.server_id
614     @errors.cyclades.flavor_id
615     def _run(self, server_id, flavor_id):
616         self._optional_output(self.client.resize_server(server_id, flavor_id))
617
618     def main(self, server_id, flavor_id):
619         super(self.__class__, self)._run()
620         self._run(server_id=server_id, flavor_id=flavor_id)
621
622
623 @command(server_cmds)
624 class server_firewall(_init_cyclades):
625     """Manage virtual server firewall profiles for public networks"""
626
627
628 @command(server_cmds)
629 class server_firewall_set(
630         _init_cyclades, _optional_output_cmd, _firewall_wait):
631     """Set the firewall profile on virtual server public network
632     Values for profile:
633     - DISABLED: Shutdown firewall
634     - ENABLED: Firewall in normal mode
635     - PROTECTED: Firewall in secure mode
636     """
637
638     arguments = dict(
639         wait=FlagArgument('Wait server firewall to build', ('-w', '--wait')),
640         timeout=IntArgument(
641             'Set wait timeout in seconds (default: 60)', '--timeout',
642             default=60)
643     )
644
645     @errors.generic.all
646     @errors.cyclades.connection
647     @errors.cyclades.server_id
648     @errors.cyclades.firewall
649     def _run(self, server_id, profile):
650         if self['timeout'] and not self['wait']:
651             raise CLIInvalidArgument('Invalid use of --timeout', details=[
652                 'Timeout is used only along with -w/--wait'])
653         old_profile = self.client.get_firewall_profile(server_id)
654         if old_profile.lower() == profile.lower():
655             self.error('Firewall of server %s: allready in status %s' % (
656                 server_id, old_profile))
657         else:
658             self._optional_output(self.client.set_firewall_profile(
659                 server_id=int(server_id), profile=('%s' % profile).upper()))
660             if self['wait']:
661                 self._wait(server_id, old_profile, timeout=self['timeout'])
662
663     def main(self, server_id, profile):
664         super(self.__class__, self)._run()
665         self._run(server_id=server_id, profile=profile)
666
667
668 @command(server_cmds)
669 class server_firewall_get(_init_cyclades):
670     """Get the firewall profile for a virtual servers' public network"""
671
672     @errors.generic.all
673     @errors.cyclades.connection
674     @errors.cyclades.server_id
675     def _run(self, server_id):
676         self.writeln(self.client.get_firewall_profile(server_id))
677
678     def main(self, server_id):
679         super(self.__class__, self)._run()
680         self._run(server_id=server_id)
681
682
683 @command(server_cmds)
684 class server_addr(_init_cyclades, _optional_json):
685     """List the addresses of all network interfaces on a virtual server"""
686
687     arguments = dict(
688         enum=FlagArgument('Enumerate results', '--enumerate')
689     )
690
691     @errors.generic.all
692     @errors.cyclades.connection
693     @errors.cyclades.server_id
694     def _run(self, server_id):
695         reply = self.client.list_server_nics(int(server_id))
696         self._print(reply, with_enumeration=self['enum'] and (reply) > 1)
697
698     def main(self, server_id):
699         super(self.__class__, self)._run()
700         self._run(server_id=server_id)
701
702
703 @command(server_cmds)
704 class server_metadata(_init_cyclades):
705     """Manage Server metadata (key:value pairs of server attributes)"""
706
707
708 @command(server_cmds)
709 class server_metadata_list(_init_cyclades, _optional_json):
710     """Get server metadata"""
711
712     @errors.generic.all
713     @errors.cyclades.connection
714     @errors.cyclades.server_id
715     @errors.cyclades.metadata
716     def _run(self, server_id, key=''):
717         self._print(
718             self.client.get_server_metadata(int(server_id), key),
719             self.print_dict)
720
721     def main(self, server_id, key=''):
722         super(self.__class__, self)._run()
723         self._run(server_id=server_id, key=key)
724
725
726 @command(server_cmds)
727 class server_metadata_set(_init_cyclades, _optional_json):
728     """Set / update virtual server metadata
729     Metadata should be given in key/value pairs in key=value format
730     For example: /server metadata set <server id> key1=value1 key2=value2
731     Old, unreferenced metadata will remain intact
732     """
733
734     @errors.generic.all
735     @errors.cyclades.connection
736     @errors.cyclades.server_id
737     def _run(self, server_id, keyvals):
738         assert keyvals, 'Please, add some metadata ( key=value)'
739         metadata = dict()
740         for keyval in keyvals:
741             k, sep, v = keyval.partition('=')
742             if sep and k:
743                 metadata[k] = v
744             else:
745                 raiseCLIError(
746                     'Invalid piece of metadata %s' % keyval,
747                     importance=2, details=[
748                         'Correct metadata format: key=val',
749                         'For example:',
750                         '/server metadata set <server id>'
751                         'key1=value1 key2=value2'])
752         self._print(
753             self.client.update_server_metadata(int(server_id), **metadata),
754             self.print_dict)
755
756     def main(self, server_id, *key_equals_val):
757         super(self.__class__, self)._run()
758         self._run(server_id=server_id, keyvals=key_equals_val)
759
760
761 @command(server_cmds)
762 class server_metadata_delete(_init_cyclades, _optional_output_cmd):
763     """Delete virtual server metadata"""
764
765     @errors.generic.all
766     @errors.cyclades.connection
767     @errors.cyclades.server_id
768     @errors.cyclades.metadata
769     def _run(self, server_id, key):
770         self._optional_output(
771             self.client.delete_server_metadata(int(server_id), key))
772
773     def main(self, server_id, key):
774         super(self.__class__, self)._run()
775         self._run(server_id=server_id, key=key)
776
777
778 @command(server_cmds)
779 class server_stats(_init_cyclades, _optional_json):
780     """Get virtual server statistics"""
781
782     @errors.generic.all
783     @errors.cyclades.connection
784     @errors.cyclades.server_id
785     def _run(self, server_id):
786         self._print(
787             self.client.get_server_stats(int(server_id)), self.print_dict)
788
789     def main(self, server_id):
790         super(self.__class__, self)._run()
791         self._run(server_id=server_id)
792
793
794 @command(server_cmds)
795 class server_wait(_init_cyclades, _server_wait):
796     """Wait for server to finish [BUILD, STOPPED, REBOOT, ACTIVE]"""
797
798     arguments = dict(
799         timeout=IntArgument(
800             'Wait limit in seconds (default: 60)', '--timeout', default=60)
801     )
802
803     @errors.generic.all
804     @errors.cyclades.connection
805     @errors.cyclades.server_id
806     def _run(self, server_id, current_status):
807         r = self.client.get_server_details(server_id)
808         if r['status'].lower() == current_status.lower():
809             self._wait(server_id, current_status, timeout=self['timeout'])
810         else:
811             self.error(
812                 'Server %s: Cannot wait for status %s, '
813                 'status is already %s' % (
814                     server_id, current_status, r['status']))
815
816     def main(self, server_id, current_status='BUILD'):
817         super(self.__class__, self)._run()
818         self._run(server_id=server_id, current_status=current_status)
819
820
821 @command(flavor_cmds)
822 class flavor_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
823     """List available hardware flavors"""
824
825     PERMANENTS = ('id', 'name')
826
827     arguments = dict(
828         detail=FlagArgument('show detailed output', ('-l', '--details')),
829         limit=IntArgument('limit # of listed flavors', ('-n', '--number')),
830         more=FlagArgument(
831             'output results in pages (-n to set items per page, default 10)',
832             '--more'),
833         enum=FlagArgument('Enumerate results', '--enumerate'),
834         ram=ValueArgument('filter by ram', ('--ram')),
835         vcpus=ValueArgument('filter by number of VCPUs', ('--vcpus')),
836         disk=ValueArgument('filter by disk size in GB', ('--disk')),
837         disk_template=ValueArgument(
838             'filter by disk_templace', ('--disk-template'))
839     )
840
841     def _apply_common_filters(self, flavors):
842         common_filters = dict()
843         if self['ram']:
844             common_filters['ram'] = self['ram']
845         if self['vcpus']:
846             common_filters['vcpus'] = self['vcpus']
847         if self['disk']:
848             common_filters['disk'] = self['disk']
849         if self['disk_template']:
850             common_filters['SNF:disk_template'] = self['disk_template']
851         return filter_dicts_by_dict(flavors, common_filters)
852
853     @errors.generic.all
854     @errors.cyclades.connection
855     def _run(self):
856         withcommons = self['ram'] or self['vcpus'] or (
857             self['disk'] or self['disk_template'])
858         detail = self['detail'] or withcommons
859         flavors = self.client.list_flavors(detail)
860         flavors = self._filter_by_name(flavors)
861         flavors = self._filter_by_id(flavors)
862         if withcommons:
863             flavors = self._apply_common_filters(flavors)
864         if not (self['detail'] or (
865                 self['json_output'] or self['output_format'])):
866             remove_from_items(flavors, 'links')
867         if detail and not self['detail']:
868             for flv in flavors:
869                 for key in set(flv).difference(self.PERMANENTS):
870                     flv.pop(key)
871         kwargs = dict(out=StringIO(), title=()) if self['more'] else {}
872         self._print(
873             flavors,
874             with_redundancy=self['detail'], with_enumeration=self['enum'],
875             **kwargs)
876         if self['more']:
877             pager(kwargs['out'].getvalue())
878
879     def main(self):
880         super(self.__class__, self)._run()
881         self._run()
882
883
884 @command(flavor_cmds)
885 class flavor_info(_init_cyclades, _optional_json):
886     """Detailed information on a hardware flavor
887     To get a list of available flavors and flavor ids, try /flavor list
888     """
889
890     @errors.generic.all
891     @errors.cyclades.connection
892     @errors.cyclades.flavor_id
893     def _run(self, flavor_id):
894         self._print(
895             self.client.get_flavor_details(int(flavor_id)), self.print_dict)
896
897     def main(self, flavor_id):
898         super(self.__class__, self)._run()
899         self._run(flavor_id=flavor_id)
900
901
902 def _add_name(self, net):
903         user_id, tenant_id, uuids = net['user_id'], net['tenant_id'], []
904         if user_id:
905             uuids.append(user_id)
906         if tenant_id:
907             uuids.append(tenant_id)
908         if uuids:
909             usernames = self._uuids2usernames(uuids)
910             if user_id:
911                 net['user_id'] += ' (%s)' % usernames[user_id]
912             if tenant_id:
913                 net['tenant_id'] += ' (%s)' % usernames[tenant_id]
914
915
916 @command(network_cmds)
917 class network_info(_init_cyclades, _optional_json):
918     """Detailed information on a network
919     To get a list of available networks and network ids, try /network list
920     """
921
922     @errors.generic.all
923     @errors.cyclades.connection
924     @errors.cyclades.network_id
925     def _run(self, network_id):
926         network = self.client.get_network_details(int(network_id))
927         _add_name(self, network)
928         self._print(network, self.print_dict, exclude=('id'))
929
930     def main(self, network_id):
931         super(self.__class__, self)._run()
932         self._run(network_id=network_id)
933
934
935 @command(network_cmds)
936 class network_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
937     """List networks"""
938
939     PERMANENTS = ('id', 'name')
940
941     arguments = dict(
942         detail=FlagArgument('show detailed output', ('-l', '--details')),
943         limit=IntArgument('limit # of listed networks', ('-n', '--number')),
944         more=FlagArgument(
945             'output results in pages (-n to set items per page, default 10)',
946             '--more'),
947         enum=FlagArgument('Enumerate results', '--enumerate'),
948         status=ValueArgument('filter by status', ('--status')),
949         public=FlagArgument('only public networks', ('--public')),
950         private=FlagArgument('only private networks', ('--private')),
951         dhcp=FlagArgument('show networks with dhcp', ('--with-dhcp')),
952         no_dhcp=FlagArgument('show networks without dhcp', ('--without-dhcp')),
953         user_id=ValueArgument('filter by user id', ('--user-id')),
954         user_name=ValueArgument('filter by user name', ('--user-name')),
955         gateway=ValueArgument('filter by gateway (IPv4)', ('--gateway')),
956         gateway6=ValueArgument('filter by gateway (IPv6)', ('--gateway6')),
957         cidr=ValueArgument('filter by cidr (IPv4)', ('--cidr')),
958         cidr6=ValueArgument('filter by cidr (IPv6)', ('--cidr6')),
959         type=ValueArgument('filter by type', ('--type')),
960     )
961
962     def _apply_common_filters(self, networks):
963         common_filter = dict()
964         if self['public']:
965             if self['private']:
966                 return []
967             common_filter['public'] = self['public']
968         elif self['private']:
969             common_filter['public'] = False
970         if self['dhcp']:
971             if self['no_dhcp']:
972                 return []
973             common_filter['dhcp'] = True
974         elif self['no_dhcp']:
975             common_filter['dhcp'] = False
976         if self['user_id'] or self['user_name']:
977             uuid = self['user_id'] or self._username2uuid(self['user_name'])
978             common_filter['user_id'] = uuid
979         for term in ('status', 'gateway', 'gateway6', 'cidr', 'cidr6', 'type'):
980             if self[term]:
981                 common_filter[term] = self[term]
982         return filter_dicts_by_dict(networks, common_filter)
983
984     def _add_name(self, networks, key='user_id'):
985         uuids = self._uuids2usernames(
986             list(set([net[key] for net in networks])))
987         for net in networks:
988             v = net.get(key, None)
989             if v:
990                 net[key] += ' (%s)' % uuids[v]
991         return networks
992
993     @errors.generic.all
994     @errors.cyclades.connection
995     def _run(self):
996         withcommons = False
997         for term in (
998                 'status', 'public', 'private', 'user_id', 'user_name', 'type',
999                 'gateway', 'gateway6', 'cidr', 'cidr6', 'dhcp', 'no_dhcp'):
1000             if self[term]:
1001                 withcommons = True
1002                 break
1003         detail = self['detail'] or withcommons
1004         networks = self.client.list_networks(detail)
1005         networks = self._filter_by_name(networks)
1006         networks = self._filter_by_id(networks)
1007         if withcommons:
1008             networks = self._apply_common_filters(networks)
1009         if not (self['detail'] or (
1010                 self['json_output'] or self['output_format'])):
1011             remove_from_items(networks, 'links')
1012         if detail and not self['detail']:
1013             for net in networks:
1014                 for key in set(net).difference(self.PERMANENTS):
1015                     net.pop(key)
1016         if self['detail'] and not (
1017                 self['json_output'] or self['output_format']):
1018             self._add_name(networks)
1019             self._add_name(networks, 'tenant_id')
1020         kwargs = dict(with_enumeration=self['enum'])
1021         if self['more']:
1022             kwargs['out'] = StringIO()
1023             kwargs['title'] = ()
1024         if self['limit']:
1025             networks = networks[:self['limit']]
1026         self._print(networks, **kwargs)
1027         if self['more']:
1028             pager(kwargs['out'].getvalue())
1029
1030     def main(self):
1031         super(self.__class__, self)._run()
1032         self._run()
1033
1034
1035 @command(network_cmds)
1036 class network_create(_init_cyclades, _optional_json, _network_wait):
1037     """Create an (unconnected) network"""
1038
1039     arguments = dict(
1040         cidr=ValueArgument('explicitly set cidr', '--with-cidr'),
1041         gateway=ValueArgument('explicitly set gateway', '--with-gateway'),
1042         dhcp=FlagArgument('Use dhcp (default: off)', '--with-dhcp'),
1043         type=ValueArgument(
1044             'Valid network types are '
1045             'CUSTOM, IP_LESS_ROUTED, MAC_FILTERED (default), PHYSICAL_VLAN',
1046             '--with-type',
1047             default='MAC_FILTERED'),
1048         wait=FlagArgument('Wait network to build', ('-w', '--wait'))
1049     )
1050
1051     @errors.generic.all
1052     @errors.cyclades.connection
1053     @errors.cyclades.network_max
1054     def _run(self, name):
1055         r = self.client.create_network(
1056             name,
1057             cidr=self['cidr'],
1058             gateway=self['gateway'],
1059             dhcp=self['dhcp'],
1060             type=self['type'])
1061         _add_name(self, r)
1062         self._print(r, self.print_dict)
1063         if self['wait'] and r['status'] in ('PENDING', ):
1064             self._wait(r['id'], 'PENDING')
1065
1066     def main(self, name):
1067         super(self.__class__, self)._run()
1068         self._run(name)
1069
1070
1071 @command(network_cmds)
1072 class network_rename(_init_cyclades, _optional_output_cmd):
1073     """Set the name of a network"""
1074
1075     @errors.generic.all
1076     @errors.cyclades.connection
1077     @errors.cyclades.network_id
1078     def _run(self, network_id, new_name):
1079         self._optional_output(
1080                 self.client.update_network_name(int(network_id), new_name))
1081
1082     def main(self, network_id, new_name):
1083         super(self.__class__, self)._run()
1084         self._run(network_id=network_id, new_name=new_name)
1085
1086
1087 @command(network_cmds)
1088 class network_delete(_init_cyclades, _optional_output_cmd, _network_wait):
1089     """Delete a network"""
1090
1091     arguments = dict(
1092         wait=FlagArgument('Wait network to build', ('-w', '--wait'))
1093     )
1094
1095     @errors.generic.all
1096     @errors.cyclades.connection
1097     @errors.cyclades.network_in_use
1098     @errors.cyclades.network_id
1099     def _run(self, network_id):
1100         status = 'DELETED'
1101         if self['wait']:
1102             r = self.client.get_network_details(network_id)
1103             status = r['status']
1104             if status in ('DELETED', ):
1105                 return
1106
1107         r = self.client.delete_network(int(network_id))
1108         self._optional_output(r)
1109
1110         if self['wait']:
1111             self._wait(network_id, status)
1112
1113     def main(self, network_id):
1114         super(self.__class__, self)._run()
1115         self._run(network_id=network_id)
1116
1117
1118 @command(network_cmds)
1119 class network_connect(_init_cyclades, _optional_output_cmd):
1120     """Connect a server to a network"""
1121
1122     @errors.generic.all
1123     @errors.cyclades.connection
1124     @errors.cyclades.server_id
1125     @errors.cyclades.network_id
1126     def _run(self, server_id, network_id):
1127         self._optional_output(
1128                 self.client.connect_server(int(server_id), int(network_id)))
1129
1130     def main(self, server_id, network_id):
1131         super(self.__class__, self)._run()
1132         self._run(server_id=server_id, network_id=network_id)
1133
1134
1135 @command(network_cmds)
1136 class network_disconnect(_init_cyclades):
1137     """Disconnect a nic that connects a server to a network
1138     Nic ids are listed as "attachments" in detailed network information
1139     To get detailed network information: /network info <network id>
1140     """
1141
1142     @errors.cyclades.nic_format
1143     def _server_id_from_nic(self, nic_id):
1144         return nic_id.split('-')[1]
1145
1146     @errors.generic.all
1147     @errors.cyclades.connection
1148     @errors.cyclades.server_id
1149     @errors.cyclades.nic_id
1150     def _run(self, nic_id, server_id):
1151         num_of_disconnected = self.client.disconnect_server(server_id, nic_id)
1152         if not num_of_disconnected:
1153             raise ClientError(
1154                 'Network Interface %s not found on server %s' % (
1155                     nic_id, server_id),
1156                 status=404)
1157         print('Disconnected %s connections' % num_of_disconnected)
1158
1159     def main(self, nic_id):
1160         super(self.__class__, self)._run()
1161         server_id = self._server_id_from_nic(nic_id=nic_id)
1162         self._run(nic_id=nic_id, server_id=server_id)
1163
1164
1165 @command(network_cmds)
1166 class network_wait(_init_cyclades, _network_wait):
1167     """Wait for server to finish [PENDING, ACTIVE, DELETED]"""
1168
1169     arguments = dict(
1170         timeout=IntArgument(
1171             'Wait limit in seconds (default: 60)', '--timeout', default=60)
1172     )
1173
1174     @errors.generic.all
1175     @errors.cyclades.connection
1176     @errors.cyclades.network_id
1177     def _run(self, network_id, current_status):
1178         net = self.client.get_network_details(network_id)
1179         if net['status'].lower() == current_status.lower():
1180             self._wait(network_id, current_status, timeout=self['timeout'])
1181         else:
1182             self.error(
1183                 'Network %s: Cannot wait for status %s, '
1184                 'status is already %s' % (
1185                     network_id, current_status, net['status']))
1186
1187     def main(self, network_id, current_status='PENDING'):
1188         super(self.__class__, self)._run()
1189         self._run(network_id=network_id, current_status=current_status)
1190
1191
1192 @command(ip_cmds)
1193 class ip_pools(_init_cyclades, _optional_json):
1194     """List pools of floating IPs"""
1195
1196     @errors.generic.all
1197     @errors.cyclades.connection
1198     def _run(self):
1199         r = self.client.get_floating_ip_pools()
1200         self._print(r if self['json_output'] or self['output_format'] else r[
1201             'floating_ip_pools'])
1202
1203     def main(self):
1204         super(self.__class__, self)._run()
1205         self._run()
1206
1207
1208 @command(ip_cmds)
1209 class ip_list(_init_cyclades, _optional_json):
1210     """List reserved floating IPs"""
1211
1212     @errors.generic.all
1213     @errors.cyclades.connection
1214     def _run(self):
1215         r = self.client.get_floating_ips()
1216         self._print(r if self['json_output'] or self['output_format'] else r[
1217             'floating_ips'])
1218
1219     def main(self):
1220         super(self.__class__, self)._run()
1221         self._run()
1222
1223
1224 @command(ip_cmds)
1225 class ip_info(_init_cyclades, _optional_json):
1226     """Details for an IP"""
1227
1228     @errors.generic.all
1229     @errors.cyclades.connection
1230     def _run(self, ip):
1231         self._print(self.client.get_floating_ip(ip), self.print_dict)
1232
1233     def main(self, IP):
1234         super(self.__class__, self)._run()
1235         self._run(ip=IP)
1236
1237
1238 @command(ip_cmds)
1239 class ip_reserve(_init_cyclades, _optional_json):
1240     """Reserve a floating IP
1241     An IP is reserved from an IP pool. The default IP pool is chosen
1242     automatically, but there is the option if specifying an explicit IP pool.
1243     """
1244
1245     arguments = dict(pool=ValueArgument('Source IP pool', ('--pool'), None))
1246
1247     @errors.generic.all
1248     @errors.cyclades.connection
1249     def _run(self, ip=None):
1250         self._print([self.client.alloc_floating_ip(self['pool'], ip)])
1251
1252     def main(self, requested_IP=None):
1253         super(self.__class__, self)._run()
1254         self._run(ip=requested_IP)
1255
1256
1257 @command(ip_cmds)
1258 class ip_release(_init_cyclades, _optional_output_cmd):
1259     """Release a floating IP
1260     The release IP is "returned" to the IP pool it came from.
1261     """
1262
1263     @errors.generic.all
1264     @errors.cyclades.connection
1265     def _run(self, ip):
1266         self._optional_output(self.client.delete_floating_ip(ip))
1267
1268     def main(self, IP):
1269         super(self.__class__, self)._run()
1270         self._run(ip=IP)
1271
1272
1273 @command(ip_cmds)
1274 class ip_attach(_init_cyclades, _optional_output_cmd):
1275     """Attach a floating IP to a server
1276     """
1277
1278     @errors.generic.all
1279     @errors.cyclades.connection
1280     @errors.cyclades.server_id
1281     def _run(self, server_id, ip):
1282         self._optional_output(self.client.attach_floating_ip(server_id, ip))
1283
1284     def main(self, server_id, IP):
1285         super(self.__class__, self)._run()
1286         self._run(server_id=server_id, ip=IP)
1287
1288
1289 @command(ip_cmds)
1290 class ip_detach(_init_cyclades, _optional_output_cmd):
1291     """Detach a floating IP from a server
1292     """
1293
1294     @errors.generic.all
1295     @errors.cyclades.connection
1296     @errors.cyclades.server_id
1297     def _run(self, server_id, ip):
1298         self._optional_output(self.client.detach_floating_ip(server_id, ip))
1299
1300     def main(self, server_id, IP):
1301         super(self.__class__, self)._run()
1302         self._run(server_id=server_id, ip=IP)