90af91e499faf275bde46e4eb421eb8a9b919aec
[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, expanduser
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
45 from kamaki.cli.argument import (
46     FlagArgument, ValueArgument, KeyValueArgument, RepeatableArgument,
47     ProgressBarArgument, DateArgument, IntArgument)
48 from kamaki.cli.commands import _command_init, errors, addLogSettings
49 from kamaki.cli.commands import (
50     _optional_output_cmd, _optional_json, _name_filter, _id_filter)
51
52
53 server_cmds = CommandTree('server', 'Cyclades/Compute API server commands')
54 flavor_cmds = CommandTree('flavor', 'Cyclades/Compute API flavor commands')
55 _commands = [server_cmds, flavor_cmds]
56
57
58 about_authentication = '\nUser Authentication:\
59     \n* to check authentication: /user authenticate\
60     \n* to set authentication token: /config set cloud.<cloud>.token <token>'
61
62 howto_personality = [
63     'Defines a file to be injected to virtual servers file system.',
64     'syntax:  PATH,[SERVER_PATH,[OWNER,[GROUP,[MODE]]]]',
65     '  [local-path=]PATH: local file to be injected (relative or absolute)',
66     '  [server-path=]SERVER_PATH: destination location inside server Image',
67     '  [owner=]OWNER: virtual servers user id for the remote file',
68     '  [group=]GROUP: virtual servers group id or name for the remote file',
69     '  [mode=]MODE: permission in octal (e.g., 0777)',
70     'e.g., -p /tmp/my.file,owner=root,mode=0777']
71
72
73 class _service_wait(object):
74
75     wait_arguments = dict(
76         progress_bar=ProgressBarArgument(
77             'do not show progress bar', ('-N', '--no-progress-bar'), False)
78     )
79
80     def _wait(
81             self, service, service_id, status_method, current_status,
82             countdown=True, timeout=60):
83         (progress_bar, wait_cb) = self._safe_progress_bar(
84             '%s %s: status is still %s' % (
85                 service, service_id, current_status),
86             countdown=countdown, timeout=timeout)
87
88         try:
89             new_mode = status_method(
90                 service_id, current_status, max_wait=timeout, wait_cb=wait_cb)
91             if new_mode:
92                 self.error('%s %s: status is now %s' % (
93                     service, service_id, new_mode))
94             else:
95                 self.error('%s %s: status is still %s' % (
96                     service, service_id, current_status))
97         except KeyboardInterrupt:
98             self.error('\n- canceled')
99         finally:
100             self._safe_progress_bar_finish(progress_bar)
101
102
103 class _server_wait(_service_wait):
104
105     def _wait(self, server_id, current_status, timeout=60):
106         super(_server_wait, self)._wait(
107             'Server', server_id, self.client.wait_server, current_status,
108             countdown=(current_status not in ('BUILD', )),
109             timeout=timeout if current_status not in ('BUILD', ) else 100)
110
111
112 class _init_cyclades(_command_init):
113     @errors.generic.all
114     @addLogSettings
115     def _run(self, service='compute'):
116         if getattr(self, 'cloud', None):
117             base_url = self._custom_url(service) or self._custom_url(
118                 'cyclades')
119             if base_url:
120                 token = self._custom_token(service) or self._custom_token(
121                     'cyclades') or self.config.get_cloud('token')
122                 self.client = CycladesClient(base_url=base_url, token=token)
123                 return
124         else:
125             self.cloud = 'default'
126         if getattr(self, 'auth_base', False):
127             cyclades_endpoints = self.auth_base.get_service_endpoints(
128                 self._custom_type('cyclades') or 'compute',
129                 self._custom_version('cyclades') or '')
130             base_url = cyclades_endpoints['publicURL']
131             token = self.auth_base.token
132             self.client = CycladesClient(base_url=base_url, token=token)
133         else:
134             raise CLIBaseUrlError(service='cyclades')
135
136     def main(self):
137         self._run()
138
139
140 @command(server_cmds)
141 class server_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
142     """List virtual servers accessible by user
143     Use filtering arguments (e.g., --name-like) to manage long server lists
144     """
145
146     PERMANENTS = ('id', 'name')
147
148     arguments = dict(
149         detail=FlagArgument('show detailed output', ('-l', '--details')),
150         since=DateArgument(
151             'show only items since date (\' d/m/Y H:M:S \')',
152             '--since'),
153         limit=IntArgument(
154             'limit number of listed virtual servers', ('-n', '--number')),
155         more=FlagArgument(
156             'output results in pages (-n to set items per page, default 10)',
157             '--more'),
158         enum=FlagArgument('Enumerate results', '--enumerate'),
159         flavor_id=ValueArgument('filter by flavor id', ('--flavor-id')),
160         image_id=ValueArgument('filter by image id', ('--image-id')),
161         user_id=ValueArgument('filter by user id', ('--user-id')),
162         user_name=ValueArgument('filter by user name', ('--user-name')),
163         status=ValueArgument(
164             'filter by status (ACTIVE, STOPPED, REBOOT, ERROR, etc.)',
165             ('--status')),
166         meta=KeyValueArgument('filter by metadata key=values', ('--metadata')),
167         meta_like=KeyValueArgument(
168             'print only if in key=value, the value is part of actual value',
169             ('--metadata-like')),
170     )
171
172     def _add_user_name(self, servers):
173         uuids = self._uuids2usernames(list(set(
174                 [srv['user_id'] for srv in servers] +
175                 [srv['tenant_id'] for srv in servers])))
176         for srv in servers:
177             srv['user_id'] += ' (%s)' % uuids[srv['user_id']]
178             srv['tenant_id'] += ' (%s)' % uuids[srv['tenant_id']]
179         return servers
180
181     def _apply_common_filters(self, servers):
182         common_filters = dict()
183         if self['status']:
184             common_filters['status'] = self['status']
185         if self['user_id'] or self['user_name']:
186             uuid = self['user_id'] or self._username2uuid(self['user_name'])
187             common_filters['user_id'] = uuid
188         return filter_dicts_by_dict(servers, common_filters)
189
190     def _filter_by_image(self, servers):
191         iid = self['image_id']
192         return [srv for srv in servers if srv['image']['id'] == iid]
193
194     def _filter_by_flavor(self, servers):
195         fid = self['flavor_id']
196         return [srv for srv in servers if (
197             '%s' % srv['image']['id'] == '%s' % fid)]
198
199     def _filter_by_metadata(self, servers):
200         new_servers = []
201         for srv in servers:
202             if not 'metadata' in srv:
203                 continue
204             meta = [dict(srv['metadata'])]
205             if self['meta']:
206                 meta = filter_dicts_by_dict(meta, self['meta'])
207             if meta and self['meta_like']:
208                 meta = filter_dicts_by_dict(
209                     meta, self['meta_like'], exact_match=False)
210             if meta:
211                 new_servers.append(srv)
212         return new_servers
213
214     @errors.generic.all
215     @errors.cyclades.connection
216     @errors.cyclades.date
217     def _run(self):
218         withimage = bool(self['image_id'])
219         withflavor = bool(self['flavor_id'])
220         withmeta = bool(self['meta'] or self['meta_like'])
221         withcommons = bool(
222             self['status'] or self['user_id'] or self['user_name'])
223         detail = self['detail'] or (
224             withimage or withflavor or withmeta or withcommons)
225         servers = self.client.list_servers(detail, self['since'])
226
227         servers = self._filter_by_name(servers)
228         servers = self._filter_by_id(servers)
229         servers = self._apply_common_filters(servers)
230         if withimage:
231             servers = self._filter_by_image(servers)
232         if withflavor:
233             servers = self._filter_by_flavor(servers)
234         if withmeta:
235             servers = self._filter_by_metadata(servers)
236
237         if self['detail'] and not (
238                 self['json_output'] or self['output_format']):
239             servers = self._add_user_name(servers)
240         elif not (self['detail'] or (
241                 self['json_output'] or self['output_format'])):
242             remove_from_items(servers, 'links')
243         if detail and not self['detail']:
244             for srv in servers:
245                 for key in set(srv).difference(self.PERMANENTS):
246                     srv.pop(key)
247         kwargs = dict(with_enumeration=self['enum'])
248         if self['more']:
249             kwargs['out'] = StringIO()
250             kwargs['title'] = ()
251         if self['limit']:
252             servers = servers[:self['limit']]
253         self._print(servers, **kwargs)
254         if self['more']:
255             pager(kwargs['out'].getvalue())
256
257     def main(self):
258         super(self.__class__, self)._run()
259         self._run()
260
261
262 @command(server_cmds)
263 class server_info(_init_cyclades, _optional_json):
264     """Detailed information on a Virtual Machine"""
265
266     arguments = dict(
267         nics=FlagArgument(
268             'Show only the network interfaces of this virtual server',
269             '--nics'),
270         network_id=ValueArgument(
271             'Show the connection details to that network', '--network-id'),
272         stats=FlagArgument('Get URLs for server statistics', '--stats'),
273         diagnostics=FlagArgument('Diagnostic information', '--diagnostics')
274     )
275
276     @errors.generic.all
277     @errors.cyclades.connection
278     @errors.cyclades.server_id
279     def _run(self, server_id):
280         vm = self.client.get_server_nics(server_id)
281         if self['nics']:
282             self._print(vm.get('attachments', []))
283         elif self['network_id']:
284             self._print(
285                 self.client.get_server_network_nics(
286                     server_id, self['network_id']), self.print_dict)
287         elif self['stats']:
288             self._print(
289                 self.client.get_server_stats(server_id), self.print_dict)
290         else:
291             uuids = self._uuids2usernames([vm['user_id'], vm['tenant_id']])
292             vm['user_id'] += ' (%s)' % uuids[vm['user_id']]
293             vm['tenant_id'] += ' (%s)' % uuids[vm['tenant_id']]
294             self._print(vm, self.print_dict)
295
296     def main(self, server_id):
297         super(self.__class__, self)._run()
298         choose_one = ('nics', 'vnc', 'stats')
299         count = len([a for a in choose_one if self[a]])
300         if count > 1:
301             raise CLIInvalidArgument('Invalid argument compination', details=[
302                 'Arguments %s cannot be used simultaneously' % ', '.join(
303                     [self.arguments[a].lvalue for a in choose_one])])
304         self._run(server_id=server_id)
305
306
307 class PersonalityArgument(KeyValueArgument):
308
309     terms = (
310         ('local-path', 'contents'),
311         ('server-path', 'path'),
312         ('owner', 'owner'),
313         ('group', 'group'),
314         ('mode', 'mode'))
315
316     @property
317     def value(self):
318         return getattr(self, '_value', [])
319
320     @value.setter
321     def value(self, newvalue):
322         if newvalue == self.default:
323             return self.value
324         self._value, input_dict = [], {}
325         for i, terms in enumerate(newvalue):
326             termlist = terms.split(',')
327             if len(termlist) > len(self.terms):
328                 msg = 'Wrong number of terms (1<=terms<=%s)' % len(self.terms)
329                 raiseCLIError(CLISyntaxError(msg), details=howto_personality)
330
331             for k, v in self.terms:
332                 prefix = '%s=' % k
333                 for item in termlist:
334                     if item.lower().startswith(prefix):
335                         input_dict[k] = item[len(k) + 1:]
336                         break
337                     item = None
338                 if item:
339                     termlist.remove(item)
340
341             try:
342                 path = input_dict['local-path']
343             except KeyError:
344                 path = termlist.pop(0)
345                 if not path:
346                     raise CLIInvalidArgument(
347                         '--personality: No local path specified',
348                         details=howto_personality)
349
350             if not exists(path):
351                 raise CLIInvalidArgument(
352                     '--personality: File %s does not exist' % path,
353                     details=howto_personality)
354
355             self._value.append(dict(path=path))
356             with open(expanduser(path)) as f:
357                 self._value[i]['contents'] = b64encode(f.read())
358             for k, v in self.terms[1:]:
359                 try:
360                     self._value[i][v] = input_dict[k]
361                 except KeyError:
362                     try:
363                         self._value[i][v] = termlist.pop(0)
364                     except IndexError:
365                         continue
366                 if k in ('mode', ) and self._value[i][v]:
367                     try:
368                         self._value[i][v] = int(self._value[i][v], 8)
369                     except ValueError as ve:
370                         raise CLIInvalidArgument(
371                             'Personality mode must be in octal', details=[
372                                 '%s' % ve])
373
374
375 class NetworkArgument(RepeatableArgument):
376     """[id=]NETWORK_ID[,[ip=]IP]"""
377
378     @property
379     def value(self):
380         return getattr(self, '_value', self.default)
381
382     @value.setter
383     def value(self, new_value):
384         for v in new_value or []:
385             part1, sep, part2 = v.partition(',')
386             netid, ip = '', ''
387             if part1.startswith('id='):
388                 netid = part1[len('id='):]
389             elif part1.startswith('ip='):
390                 ip = part1[len('ip='):]
391             else:
392                 netid = part1
393             if part2:
394                 if (part2.startswith('id=') and netid) or (
395                         part2.startswith('ip=') and ip):
396                     raise CLIInvalidArgument(
397                         'Invalid network argument %s' % v, details=[
398                         'Valid format: [id=]NETWORK_ID[,[ip=]IP]'])
399                 if part2.startswith('id='):
400                     netid = part2[len('id='):]
401                 elif part2.startswith('ip='):
402                     ip = part2[len('ip='):]
403                 elif netid:
404                     ip = part2
405                 else:
406                     netid = part2
407             if not netid:
408                 raise CLIInvalidArgument(
409                     'Invalid network argument %s' % v, details=[
410                     'Valid format: [id=]NETWORK_ID[,[ip=]IP]'])
411             self._value = getattr(self, '_value', [])
412             self._value.append(dict(uuid=netid))
413             if ip:
414                 self._value[-1]['fixed_ip'] = ip
415
416
417 @command(server_cmds)
418 class server_create(_init_cyclades, _optional_json, _server_wait):
419     """Create a server (aka Virtual Machine)"""
420
421     arguments = dict(
422         server_name=ValueArgument('The name of the new server', '--name'),
423         flavor_id=IntArgument('The ID of the flavor', '--flavor-id'),
424         image_id=ValueArgument('The ID of the image', '--image-id'),
425         personality=PersonalityArgument(
426             (80 * ' ').join(howto_personality), ('-p', '--personality')),
427         wait=FlagArgument('Wait server to build', ('-w', '--wait')),
428         cluster_size=IntArgument(
429             'Create a cluster of servers of this size. In this case, the name'
430             'parameter is the prefix of each server in the cluster (e.g.,'
431             'srv1, srv2, etc.',
432             '--cluster-size'),
433         max_threads=IntArgument(
434             'Max threads in cluster mode (default 1)', '--threads'),
435         network_configuration=NetworkArgument(
436             'Connect server to network: [id=]NETWORK_ID[,[ip=]IP]        . '
437             'Use only NETWORK_ID for private networks.        . '
438             'Use NETWORK_ID,[ip=]IP for networks with IP.        . '
439             'Can be repeated, mutually exclussive with --no-network',
440             '--network'),
441         no_network=FlagArgument(
442             'Do not create any network NICs on the server.        . '
443             'Mutually exclusive to --network        . '
444             'If neither --network or --no-network are used, the default '
445             'network policy is applied. This policy is configured on the '
446             'cloud and kamaki is oblivious to it',
447             '--no-network')
448     )
449     required = ('server_name', 'flavor_id', 'image_id')
450
451     @errors.cyclades.cluster_size
452     def _create_cluster(self, prefix, flavor_id, image_id, size):
453         networks = self['network_configuration'] or (
454             None if self['no_network'] else [])
455         servers = [dict(
456             name='%s%s' % (prefix, i if size > 1 else ''),
457             flavor_id=flavor_id,
458             image_id=image_id,
459             personality=self['personality'],
460             networks=networks) for i in range(1, 1 + size)]
461         if size == 1:
462             return [self.client.create_server(**servers[0])]
463         self.client.MAX_THREADS = int(self['max_threads'] or 1)
464         try:
465             r = self.client.async_run(self.client.create_server, servers)
466             return r
467         except Exception as e:
468             if size == 1:
469                 raise e
470             try:
471                 requested_names = [s['name'] for s in servers]
472                 spawned_servers = [dict(
473                     name=s['name'],
474                     id=s['id']) for s in self.client.list_servers() if (
475                         s['name'] in requested_names)]
476                 self.error('Failed to build %s servers' % size)
477                 self.error('Found %s matching servers:' % len(spawned_servers))
478                 self._print(spawned_servers, out=self._err)
479                 self.error('Check if any of these servers should be removed\n')
480             except Exception as ne:
481                 self.error('Error (%s) while notifying about errors' % ne)
482             finally:
483                 raise e
484
485     @errors.generic.all
486     @errors.cyclades.connection
487     @errors.plankton.id
488     @errors.cyclades.flavor_id
489     def _run(self, name, flavor_id, image_id):
490         for r in self._create_cluster(
491                 name, flavor_id, image_id, size=self['cluster_size'] or 1):
492             if not r:
493                 self.error('Create %s: server response was %s' % (name, r))
494                 continue
495             usernames = self._uuids2usernames(
496                 [r['user_id'], r['tenant_id']])
497             r['user_id'] += ' (%s)' % usernames[r['user_id']]
498             r['tenant_id'] += ' (%s)' % usernames[r['tenant_id']]
499             self._print(r, self.print_dict)
500             if self['wait']:
501                 self._wait(r['id'], r['status'])
502             self.writeln(' ')
503
504     def main(self):
505         super(self.__class__, self)._run()
506         if self['no_network'] and self['network_configuration']:
507             raise CLIInvalidArgument(
508                 'Invalid argument compination', importance=2, details=[
509                 'Arguments %s and %s are mutually exclusive' % (
510                     self.arguments['no_network'].lvalue,
511                     self.arguments['network_configuration'].lvalue)])
512         self._run(
513             name=self['server_name'],
514             flavor_id=self['flavor_id'],
515             image_id=self['image_id'])
516
517
518 class FirewallProfileArgument(ValueArgument):
519
520     profiles = ('DISABLED', 'ENABLED', 'PROTECTED')
521
522     @property
523     def value(self):
524         return getattr(self, '_value', None)
525
526     @value.setter
527     def value(self, new_profile):
528         if new_profile:
529             new_profile = new_profile.upper()
530             if new_profile in self.profiles:
531                 self._value = new_profile
532             else:
533                 raise CLIInvalidArgument(
534                     'Invalid firewall profile %s' % new_profile,
535                     details=['Valid values: %s' % ', '.join(self.profiles)])
536
537
538 @command(server_cmds)
539 class server_modify(_init_cyclades, _optional_output_cmd):
540     """Modify attributes of a virtual server"""
541
542     arguments = dict(
543         server_name=ValueArgument('The new name', '--name'),
544         flavor_id=IntArgument('Set a different flavor', '--flavor-id'),
545         firewall_profile=FirewallProfileArgument(
546             'Valid values: %s' % (', '.join(FirewallProfileArgument.profiles)),
547             '--firewall'),
548         metadata_to_set=KeyValueArgument(
549             'Set metadata in key=value form (can be repeated)',
550             '--metadata-set'),
551         metadata_to_delete=RepeatableArgument(
552             'Delete metadata by key (can be repeated)', '--metadata-del')
553     )
554     required = [
555         'server_name', 'flavor_id', 'firewall_profile', 'metadata_to_set',
556         'metadata_to_delete']
557
558     @errors.generic.all
559     @errors.cyclades.connection
560     @errors.cyclades.server_id
561     def _run(self, server_id):
562         if self['server_name']:
563             self.client.update_server_name((server_id), self['server_name'])
564         if self['flavor_id']:
565             self.client.resize_server(server_id, self['flavor_id'])
566         if self['firewall_profile']:
567             self.client.set_firewall_profile(
568                 server_id=server_id, profile=self['firewall_profile'])
569         if self['metadata_to_set']:
570             self.client.update_server_metadata(
571                 server_id, **self['metadata_to_set'])
572         for key in (self['metadata_to_delete'] or []):
573             errors.cyclades.metadata(
574                 self.client.delete_server_metadata)(server_id, key=key)
575         if self['with_output']:
576             self._optional_output(self.client.get_server_details(server_id))
577
578     def main(self, server_id):
579         super(self.__class__, self)._run()
580         self._run(server_id=server_id)
581
582
583 @command(server_cmds)
584 class server_delete(_init_cyclades, _optional_output_cmd, _server_wait):
585     """Delete a virtual server"""
586
587     arguments = dict(
588         wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait')),
589         cluster=FlagArgument(
590             '(DANGEROUS) Delete all virtual servers prefixed with the cluster '
591             'prefix. In that case, the prefix replaces the server id',
592             '--cluster')
593     )
594
595     def _server_ids(self, server_var):
596         if self['cluster']:
597             return [s['id'] for s in self.client.list_servers() if (
598                 s['name'].startswith(server_var))]
599
600         @errors.cyclades.server_id
601         def _check_server_id(self, server_id):
602             return server_id
603
604         return [_check_server_id(self, server_id=server_var), ]
605
606     @errors.generic.all
607     @errors.cyclades.connection
608     def _run(self, server_var):
609         for server_id in self._server_ids(server_var):
610             if self['wait']:
611                 details = self.client.get_server_details(server_id)
612                 status = details['status']
613
614             r = self.client.delete_server(server_id)
615             self._optional_output(r)
616
617             if self['wait']:
618                 self._wait(server_id, status)
619
620     def main(self, server_id_or_cluster_prefix):
621         super(self.__class__, self)._run()
622         self._run(server_id_or_cluster_prefix)
623
624
625 @command(server_cmds)
626 class server_reboot(_init_cyclades, _optional_output_cmd, _server_wait):
627     """Reboot a virtual server"""
628
629     arguments = dict(
630         hard=FlagArgument(
631             'perform a hard reboot (deprecated)', ('-f', '--force')),
632         type=ValueArgument('SOFT or HARD - default: SOFT', ('--type')),
633         wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
634     )
635
636     @errors.generic.all
637     @errors.cyclades.connection
638     @errors.cyclades.server_id
639     def _run(self, server_id):
640         hard_reboot = self['hard']
641         if hard_reboot:
642             self.error(
643                 'WARNING: -f/--force will be deprecated in version 0.12\n'
644                 '\tIn the future, please use --type=hard instead')
645         if self['type']:
646             if self['type'].lower() in ('soft', ):
647                 hard_reboot = False
648             elif self['type'].lower() in ('hard', ):
649                 hard_reboot = True
650             else:
651                 raise CLISyntaxError(
652                     'Invalid reboot type %s' % self['type'],
653                     importance=2, details=[
654                         '--type values are either SOFT (default) or HARD'])
655
656         r = self.client.reboot_server(int(server_id), hard_reboot)
657         self._optional_output(r)
658
659         if self['wait']:
660             self._wait(server_id, 'REBOOT')
661
662     def main(self, server_id):
663         super(self.__class__, self)._run()
664         self._run(server_id=server_id)
665
666
667 @command(server_cmds)
668 class server_start(_init_cyclades, _optional_output_cmd, _server_wait):
669     """Start an existing virtual server"""
670
671     arguments = dict(
672         wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
673     )
674
675     @errors.generic.all
676     @errors.cyclades.connection
677     @errors.cyclades.server_id
678     def _run(self, server_id):
679         status = 'ACTIVE'
680         if self['wait']:
681             details = self.client.get_server_details(server_id)
682             status = details['status']
683             if status in ('ACTIVE', ):
684                 return
685
686         r = self.client.start_server(int(server_id))
687         self._optional_output(r)
688
689         if self['wait']:
690             self._wait(server_id, status)
691
692     def main(self, server_id):
693         super(self.__class__, self)._run()
694         self._run(server_id=server_id)
695
696
697 @command(server_cmds)
698 class server_shutdown(_init_cyclades, _optional_output_cmd, _server_wait):
699     """Shutdown an active virtual server"""
700
701     arguments = dict(
702         wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
703     )
704
705     @errors.generic.all
706     @errors.cyclades.connection
707     @errors.cyclades.server_id
708     def _run(self, server_id):
709         status = 'STOPPED'
710         if self['wait']:
711             details = self.client.get_server_details(server_id)
712             status = details['status']
713             if status in ('STOPPED', ):
714                 return
715
716         r = self.client.shutdown_server(int(server_id))
717         self._optional_output(r)
718
719         if self['wait']:
720             self._wait(server_id, status)
721
722     def main(self, server_id):
723         super(self.__class__, self)._run()
724         self._run(server_id=server_id)
725
726
727 @command(server_cmds)
728 class server_nics(_init_cyclades):
729     """DEPRECATED, use: [kamaki] server info SERVER_ID --nics"""
730
731     def main(self, *args):
732         raiseCLIError('DEPRECATED since v0.12', importance=3, details=[
733             'Replaced by',
734             '  [kamaki] server info <SERVER_ID> --nics'])
735
736
737 @command(server_cmds)
738 class server_console(_init_cyclades, _optional_json):
739     """Create a VMC console and show connection information"""
740
741     @errors.generic.all
742     @errors.cyclades.connection
743     @errors.cyclades.server_id
744     def _run(self, server_id):
745         self.error('The following credentials will be invalidated shortly')
746         self._print(
747             self.client.get_server_console(server_id), self.print_dict)
748
749     def main(self, server_id):
750         super(self.__class__, self)._run()
751         self._run(server_id=server_id)
752
753
754 @command(server_cmds)
755 class server_rename(_init_cyclades, _optional_json):
756     """DEPRECATED, use: [kamaki] server modify SERVER_ID --name=NEW_NAME"""
757
758     def main(self, *args):
759         raiseCLIError('DEPRECATED since v0.12', importance=3, details=[
760             'Replaced by',
761             '  [kamaki] server modify <SERVER_ID> --name=NEW_NAME'])
762
763
764 @command(server_cmds)
765 class server_stats(_init_cyclades, _optional_json):
766     """DEPRECATED, use: [kamaki] server info SERVER_ID --stats"""
767
768     def main(self, *args):
769         raiseCLIError('DEPRECATED since v0.12', importance=3, details=[
770             'Replaced by',
771             '  [kamaki] server info <SERVER_ID> --stats'])
772
773
774 @command(server_cmds)
775 class server_wait(_init_cyclades, _server_wait):
776     """Wait for server to finish (BUILD, STOPPED, REBOOT, ACTIVE)"""
777
778     arguments = dict(
779         timeout=IntArgument(
780             'Wait limit in seconds (default: 60)', '--timeout', default=60)
781     )
782
783     @errors.generic.all
784     @errors.cyclades.connection
785     @errors.cyclades.server_id
786     def _run(self, server_id, current_status):
787         r = self.client.get_server_details(server_id)
788         if r['status'].lower() == current_status.lower():
789             self._wait(server_id, current_status, timeout=self['timeout'])
790         else:
791             self.error(
792                 'Server %s: Cannot wait for status %s, '
793                 'status is already %s' % (
794                     server_id, current_status, r['status']))
795
796     def main(self, server_id, current_status='BUILD'):
797         super(self.__class__, self)._run()
798         self._run(server_id=server_id, current_status=current_status)
799
800
801 @command(flavor_cmds)
802 class flavor_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
803     """List available hardware flavors"""
804
805     PERMANENTS = ('id', 'name')
806
807     arguments = dict(
808         detail=FlagArgument('show detailed output', ('-l', '--details')),
809         limit=IntArgument('limit # of listed flavors', ('-n', '--number')),
810         more=FlagArgument(
811             'output results in pages (-n to set items per page, default 10)',
812             '--more'),
813         enum=FlagArgument('Enumerate results', '--enumerate'),
814         ram=ValueArgument('filter by ram', ('--ram')),
815         vcpus=ValueArgument('filter by number of VCPUs', ('--vcpus')),
816         disk=ValueArgument('filter by disk size in GB', ('--disk')),
817         disk_template=ValueArgument(
818             'filter by disk_templace', ('--disk-template'))
819     )
820
821     def _apply_common_filters(self, flavors):
822         common_filters = dict()
823         if self['ram']:
824             common_filters['ram'] = self['ram']
825         if self['vcpus']:
826             common_filters['vcpus'] = self['vcpus']
827         if self['disk']:
828             common_filters['disk'] = self['disk']
829         if self['disk_template']:
830             common_filters['SNF:disk_template'] = self['disk_template']
831         return filter_dicts_by_dict(flavors, common_filters)
832
833     @errors.generic.all
834     @errors.cyclades.connection
835     def _run(self):
836         withcommons = self['ram'] or self['vcpus'] or (
837             self['disk'] or self['disk_template'])
838         detail = self['detail'] or withcommons
839         flavors = self.client.list_flavors(detail)
840         flavors = self._filter_by_name(flavors)
841         flavors = self._filter_by_id(flavors)
842         if withcommons:
843             flavors = self._apply_common_filters(flavors)
844         if not (self['detail'] or (
845                 self['json_output'] or self['output_format'])):
846             remove_from_items(flavors, 'links')
847         if detail and not self['detail']:
848             for flv in flavors:
849                 for key in set(flv).difference(self.PERMANENTS):
850                     flv.pop(key)
851         kwargs = dict(out=StringIO(), title=()) if self['more'] else {}
852         self._print(
853             flavors,
854             with_redundancy=self['detail'], with_enumeration=self['enum'],
855             **kwargs)
856         if self['more']:
857             pager(kwargs['out'].getvalue())
858
859     def main(self):
860         super(self.__class__, self)._run()
861         self._run()
862
863
864 @command(flavor_cmds)
865 class flavor_info(_init_cyclades, _optional_json):
866     """Detailed information on a hardware flavor
867     To get a list of available flavors and flavor ids, try /flavor list
868     """
869
870     @errors.generic.all
871     @errors.cyclades.connection
872     @errors.cyclades.flavor_id
873     def _run(self, flavor_id):
874         self._print(
875             self.client.get_flavor_details(int(flavor_id)), self.print_dict)
876
877     def main(self, flavor_id):
878         super(self.__class__, self)._run()
879         self._run(flavor_id=flavor_id)
880
881
882 def _add_name(self, net):
883         user_id, tenant_id, uuids = net['user_id'], net['tenant_id'], []
884         if user_id:
885             uuids.append(user_id)
886         if tenant_id:
887             uuids.append(tenant_id)
888         if uuids:
889             usernames = self._uuids2usernames(uuids)
890             if user_id:
891                 net['user_id'] += ' (%s)' % usernames[user_id]
892             if tenant_id:
893                 net['tenant_id'] += ' (%s)' % usernames[tenant_id]