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