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