Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / cyclades.py @ 00b1248e

History | View | Annotate | Download (42.8 kB)

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
            '--metadata-set'),
483
        metadata_to_delete=RepeatableArgument(
484
            'Delete metadata by key (can be repeated)', '--metadata-del')
485
    )
486
    required = [
487
        'server_name', 'flavor_id', 'firewall_profile', 'metadata_to_set',
488
        'metadata_to_delete']
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_stats(_init_cyclades, _optional_json):
702
    """Get virtual server statistics"""
703

    
704
    @errors.generic.all
705
    @errors.cyclades.connection
706
    @errors.cyclades.server_id
707
    def _run(self, server_id):
708
        self._print(
709
            self.client.get_server_stats(int(server_id)), self.print_dict)
710

    
711
    def main(self, server_id):
712
        super(self.__class__, self)._run()
713
        self._run(server_id=server_id)
714

    
715

    
716
@command(server_cmds)
717
class server_wait(_init_cyclades, _server_wait):
718
    """Wait for server to finish [BUILD, STOPPED, REBOOT, ACTIVE]"""
719

    
720
    arguments = dict(
721
        timeout=IntArgument(
722
            'Wait limit in seconds (default: 60)', '--timeout', default=60)
723
    )
724

    
725
    @errors.generic.all
726
    @errors.cyclades.connection
727
    @errors.cyclades.server_id
728
    def _run(self, server_id, current_status):
729
        r = self.client.get_server_details(server_id)
730
        if r['status'].lower() == current_status.lower():
731
            self._wait(server_id, current_status, timeout=self['timeout'])
732
        else:
733
            self.error(
734
                'Server %s: Cannot wait for status %s, '
735
                'status is already %s' % (
736
                    server_id, current_status, r['status']))
737

    
738
    def main(self, server_id, current_status='BUILD'):
739
        super(self.__class__, self)._run()
740
        self._run(server_id=server_id, current_status=current_status)
741

    
742

    
743
@command(flavor_cmds)
744
class flavor_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
745
    """List available hardware flavors"""
746

    
747
    PERMANENTS = ('id', 'name')
748

    
749
    arguments = dict(
750
        detail=FlagArgument('show detailed output', ('-l', '--details')),
751
        limit=IntArgument('limit # of listed flavors', ('-n', '--number')),
752
        more=FlagArgument(
753
            'output results in pages (-n to set items per page, default 10)',
754
            '--more'),
755
        enum=FlagArgument('Enumerate results', '--enumerate'),
756
        ram=ValueArgument('filter by ram', ('--ram')),
757
        vcpus=ValueArgument('filter by number of VCPUs', ('--vcpus')),
758
        disk=ValueArgument('filter by disk size in GB', ('--disk')),
759
        disk_template=ValueArgument(
760
            'filter by disk_templace', ('--disk-template'))
761
    )
762

    
763
    def _apply_common_filters(self, flavors):
764
        common_filters = dict()
765
        if self['ram']:
766
            common_filters['ram'] = self['ram']
767
        if self['vcpus']:
768
            common_filters['vcpus'] = self['vcpus']
769
        if self['disk']:
770
            common_filters['disk'] = self['disk']
771
        if self['disk_template']:
772
            common_filters['SNF:disk_template'] = self['disk_template']
773
        return filter_dicts_by_dict(flavors, common_filters)
774

    
775
    @errors.generic.all
776
    @errors.cyclades.connection
777
    def _run(self):
778
        withcommons = self['ram'] or self['vcpus'] or (
779
            self['disk'] or self['disk_template'])
780
        detail = self['detail'] or withcommons
781
        flavors = self.client.list_flavors(detail)
782
        flavors = self._filter_by_name(flavors)
783
        flavors = self._filter_by_id(flavors)
784
        if withcommons:
785
            flavors = self._apply_common_filters(flavors)
786
        if not (self['detail'] or (
787
                self['json_output'] or self['output_format'])):
788
            remove_from_items(flavors, 'links')
789
        if detail and not self['detail']:
790
            for flv in flavors:
791
                for key in set(flv).difference(self.PERMANENTS):
792
                    flv.pop(key)
793
        kwargs = dict(out=StringIO(), title=()) if self['more'] else {}
794
        self._print(
795
            flavors,
796
            with_redundancy=self['detail'], with_enumeration=self['enum'],
797
            **kwargs)
798
        if self['more']:
799
            pager(kwargs['out'].getvalue())
800

    
801
    def main(self):
802
        super(self.__class__, self)._run()
803
        self._run()
804

    
805

    
806
@command(flavor_cmds)
807
class flavor_info(_init_cyclades, _optional_json):
808
    """Detailed information on a hardware flavor
809
    To get a list of available flavors and flavor ids, try /flavor list
810
    """
811

    
812
    @errors.generic.all
813
    @errors.cyclades.connection
814
    @errors.cyclades.flavor_id
815
    def _run(self, flavor_id):
816
        self._print(
817
            self.client.get_flavor_details(int(flavor_id)), self.print_dict)
818

    
819
    def main(self, flavor_id):
820
        super(self.__class__, self)._run()
821
        self._run(flavor_id=flavor_id)
822

    
823

    
824
def _add_name(self, net):
825
        user_id, tenant_id, uuids = net['user_id'], net['tenant_id'], []
826
        if user_id:
827
            uuids.append(user_id)
828
        if tenant_id:
829
            uuids.append(tenant_id)
830
        if uuids:
831
            usernames = self._uuids2usernames(uuids)
832
            if user_id:
833
                net['user_id'] += ' (%s)' % usernames[user_id]
834
            if tenant_id:
835
                net['tenant_id'] += ' (%s)' % usernames[tenant_id]
836

    
837

    
838
@command(network_cmds)
839
class network_info(_init_cyclades, _optional_json):
840
    """Detailed information on a network
841
    To get a list of available networks and network ids, try /network list
842
    """
843

    
844
    @errors.generic.all
845
    @errors.cyclades.connection
846
    @errors.cyclades.network_id
847
    def _run(self, network_id):
848
        network = self.client.get_network_details(int(network_id))
849
        _add_name(self, network)
850
        self._print(network, self.print_dict, exclude=('id'))
851

    
852
    def main(self, network_id):
853
        super(self.__class__, self)._run()
854
        self._run(network_id=network_id)
855

    
856

    
857
@command(network_cmds)
858
class network_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
859
    """List networks"""
860

    
861
    PERMANENTS = ('id', 'name')
862

    
863
    arguments = dict(
864
        detail=FlagArgument('show detailed output', ('-l', '--details')),
865
        limit=IntArgument('limit # of listed networks', ('-n', '--number')),
866
        more=FlagArgument(
867
            'output results in pages (-n to set items per page, default 10)',
868
            '--more'),
869
        enum=FlagArgument('Enumerate results', '--enumerate'),
870
        status=ValueArgument('filter by status', ('--status')),
871
        public=FlagArgument('only public networks', ('--public')),
872
        private=FlagArgument('only private networks', ('--private')),
873
        dhcp=FlagArgument('show networks with dhcp', ('--with-dhcp')),
874
        no_dhcp=FlagArgument('show networks without dhcp', ('--without-dhcp')),
875
        user_id=ValueArgument('filter by user id', ('--user-id')),
876
        user_name=ValueArgument('filter by user name', ('--user-name')),
877
        gateway=ValueArgument('filter by gateway (IPv4)', ('--gateway')),
878
        gateway6=ValueArgument('filter by gateway (IPv6)', ('--gateway6')),
879
        cidr=ValueArgument('filter by cidr (IPv4)', ('--cidr')),
880
        cidr6=ValueArgument('filter by cidr (IPv6)', ('--cidr6')),
881
        type=ValueArgument('filter by type', ('--type')),
882
    )
883

    
884
    def _apply_common_filters(self, networks):
885
        common_filter = dict()
886
        if self['public']:
887
            if self['private']:
888
                return []
889
            common_filter['public'] = self['public']
890
        elif self['private']:
891
            common_filter['public'] = False
892
        if self['dhcp']:
893
            if self['no_dhcp']:
894
                return []
895
            common_filter['dhcp'] = True
896
        elif self['no_dhcp']:
897
            common_filter['dhcp'] = False
898
        if self['user_id'] or self['user_name']:
899
            uuid = self['user_id'] or self._username2uuid(self['user_name'])
900
            common_filter['user_id'] = uuid
901
        for term in ('status', 'gateway', 'gateway6', 'cidr', 'cidr6', 'type'):
902
            if self[term]:
903
                common_filter[term] = self[term]
904
        return filter_dicts_by_dict(networks, common_filter)
905

    
906
    def _add_name(self, networks, key='user_id'):
907
        uuids = self._uuids2usernames(
908
            list(set([net[key] for net in networks])))
909
        for net in networks:
910
            v = net.get(key, None)
911
            if v:
912
                net[key] += ' (%s)' % uuids[v]
913
        return networks
914

    
915
    @errors.generic.all
916
    @errors.cyclades.connection
917
    def _run(self):
918
        withcommons = False
919
        for term in (
920
                'status', 'public', 'private', 'user_id', 'user_name', 'type',
921
                'gateway', 'gateway6', 'cidr', 'cidr6', 'dhcp', 'no_dhcp'):
922
            if self[term]:
923
                withcommons = True
924
                break
925
        detail = self['detail'] or withcommons
926
        networks = self.client.list_networks(detail)
927
        networks = self._filter_by_name(networks)
928
        networks = self._filter_by_id(networks)
929
        if withcommons:
930
            networks = self._apply_common_filters(networks)
931
        if not (self['detail'] or (
932
                self['json_output'] or self['output_format'])):
933
            remove_from_items(networks, 'links')
934
        if detail and not self['detail']:
935
            for net in networks:
936
                for key in set(net).difference(self.PERMANENTS):
937
                    net.pop(key)
938
        if self['detail'] and not (
939
                self['json_output'] or self['output_format']):
940
            self._add_name(networks)
941
            self._add_name(networks, 'tenant_id')
942
        kwargs = dict(with_enumeration=self['enum'])
943
        if self['more']:
944
            kwargs['out'] = StringIO()
945
            kwargs['title'] = ()
946
        if self['limit']:
947
            networks = networks[:self['limit']]
948
        self._print(networks, **kwargs)
949
        if self['more']:
950
            pager(kwargs['out'].getvalue())
951

    
952
    def main(self):
953
        super(self.__class__, self)._run()
954
        self._run()
955

    
956

    
957
@command(network_cmds)
958
class network_create(_init_cyclades, _optional_json, _network_wait):
959
    """Create an (unconnected) network"""
960

    
961
    arguments = dict(
962
        cidr=ValueArgument('explicitly set cidr', '--with-cidr'),
963
        gateway=ValueArgument('explicitly set gateway', '--with-gateway'),
964
        dhcp=FlagArgument('Use dhcp (default: off)', '--with-dhcp'),
965
        type=ValueArgument(
966
            'Valid network types are '
967
            'CUSTOM, IP_LESS_ROUTED, MAC_FILTERED (default), PHYSICAL_VLAN',
968
            '--with-type',
969
            default='MAC_FILTERED'),
970
        wait=FlagArgument('Wait network to build', ('-w', '--wait'))
971
    )
972

    
973
    @errors.generic.all
974
    @errors.cyclades.connection
975
    @errors.cyclades.network_max
976
    def _run(self, name):
977
        r = self.client.create_network(
978
            name,
979
            cidr=self['cidr'],
980
            gateway=self['gateway'],
981
            dhcp=self['dhcp'],
982
            type=self['type'])
983
        _add_name(self, r)
984
        self._print(r, self.print_dict)
985
        if self['wait'] and r['status'] in ('PENDING', ):
986
            self._wait(r['id'], 'PENDING')
987

    
988
    def main(self, name):
989
        super(self.__class__, self)._run()
990
        self._run(name)
991

    
992

    
993
@command(network_cmds)
994
class network_rename(_init_cyclades, _optional_output_cmd):
995
    """Set the name of a network"""
996

    
997
    @errors.generic.all
998
    @errors.cyclades.connection
999
    @errors.cyclades.network_id
1000
    def _run(self, network_id, new_name):
1001
        self._optional_output(
1002
                self.client.update_network_name(int(network_id), new_name))
1003

    
1004
    def main(self, network_id, new_name):
1005
        super(self.__class__, self)._run()
1006
        self._run(network_id=network_id, new_name=new_name)
1007

    
1008

    
1009
@command(network_cmds)
1010
class network_delete(_init_cyclades, _optional_output_cmd, _network_wait):
1011
    """Delete a network"""
1012

    
1013
    arguments = dict(
1014
        wait=FlagArgument('Wait network to build', ('-w', '--wait'))
1015
    )
1016

    
1017
    @errors.generic.all
1018
    @errors.cyclades.connection
1019
    @errors.cyclades.network_in_use
1020
    @errors.cyclades.network_id
1021
    def _run(self, network_id):
1022
        status = 'DELETED'
1023
        if self['wait']:
1024
            r = self.client.get_network_details(network_id)
1025
            status = r['status']
1026
            if status in ('DELETED', ):
1027
                return
1028

    
1029
        r = self.client.delete_network(int(network_id))
1030
        self._optional_output(r)
1031

    
1032
        if self['wait']:
1033
            self._wait(network_id, status)
1034

    
1035
    def main(self, network_id):
1036
        super(self.__class__, self)._run()
1037
        self._run(network_id=network_id)
1038

    
1039

    
1040
@command(network_cmds)
1041
class network_connect(_init_cyclades, _optional_output_cmd):
1042
    """Connect a server to a network"""
1043

    
1044
    @errors.generic.all
1045
    @errors.cyclades.connection
1046
    @errors.cyclades.server_id
1047
    @errors.cyclades.network_id
1048
    def _run(self, server_id, network_id):
1049
        self._optional_output(
1050
                self.client.connect_server(int(server_id), int(network_id)))
1051

    
1052
    def main(self, server_id, network_id):
1053
        super(self.__class__, self)._run()
1054
        self._run(server_id=server_id, network_id=network_id)
1055

    
1056

    
1057
@command(network_cmds)
1058
class network_disconnect(_init_cyclades):
1059
    """Disconnect a nic that connects a server to a network
1060
    Nic ids are listed as "attachments" in detailed network information
1061
    To get detailed network information: /network info <network id>
1062
    """
1063

    
1064
    @errors.cyclades.nic_format
1065
    def _server_id_from_nic(self, nic_id):
1066
        return nic_id.split('-')[1]
1067

    
1068
    @errors.generic.all
1069
    @errors.cyclades.connection
1070
    @errors.cyclades.server_id
1071
    @errors.cyclades.nic_id
1072
    def _run(self, nic_id, server_id):
1073
        num_of_disconnected = self.client.disconnect_server(server_id, nic_id)
1074
        if not num_of_disconnected:
1075
            raise ClientError(
1076
                'Network Interface %s not found on server %s' % (
1077
                    nic_id, server_id),
1078
                status=404)
1079
        print('Disconnected %s connections' % num_of_disconnected)
1080

    
1081
    def main(self, nic_id):
1082
        super(self.__class__, self)._run()
1083
        server_id = self._server_id_from_nic(nic_id=nic_id)
1084
        self._run(nic_id=nic_id, server_id=server_id)
1085

    
1086

    
1087
@command(network_cmds)
1088
class network_wait(_init_cyclades, _network_wait):
1089
    """Wait for server to finish [PENDING, ACTIVE, DELETED]"""
1090

    
1091
    arguments = dict(
1092
        timeout=IntArgument(
1093
            'Wait limit in seconds (default: 60)', '--timeout', default=60)
1094
    )
1095

    
1096
    @errors.generic.all
1097
    @errors.cyclades.connection
1098
    @errors.cyclades.network_id
1099
    def _run(self, network_id, current_status):
1100
        net = self.client.get_network_details(network_id)
1101
        if net['status'].lower() == current_status.lower():
1102
            self._wait(network_id, current_status, timeout=self['timeout'])
1103
        else:
1104
            self.error(
1105
                'Network %s: Cannot wait for status %s, '
1106
                'status is already %s' % (
1107
                    network_id, current_status, net['status']))
1108

    
1109
    def main(self, network_id, current_status='PENDING'):
1110
        super(self.__class__, self)._run()
1111
        self._run(network_id=network_id, current_status=current_status)
1112

    
1113

    
1114
@command(ip_cmds)
1115
class ip_pools(_init_cyclades, _optional_json):
1116
    """List pools of floating IPs"""
1117

    
1118
    @errors.generic.all
1119
    @errors.cyclades.connection
1120
    def _run(self):
1121
        r = self.client.get_floating_ip_pools()
1122
        self._print(r if self['json_output'] or self['output_format'] else r[
1123
            'floating_ip_pools'])
1124

    
1125
    def main(self):
1126
        super(self.__class__, self)._run()
1127
        self._run()
1128

    
1129

    
1130
@command(ip_cmds)
1131
class ip_list(_init_cyclades, _optional_json):
1132
    """List reserved floating IPs"""
1133

    
1134
    @errors.generic.all
1135
    @errors.cyclades.connection
1136
    def _run(self):
1137
        r = self.client.get_floating_ips()
1138
        self._print(r if self['json_output'] or self['output_format'] else r[
1139
            'floating_ips'])
1140

    
1141
    def main(self):
1142
        super(self.__class__, self)._run()
1143
        self._run()
1144

    
1145

    
1146
@command(ip_cmds)
1147
class ip_info(_init_cyclades, _optional_json):
1148
    """Details for an IP"""
1149

    
1150
    @errors.generic.all
1151
    @errors.cyclades.connection
1152
    def _run(self, ip):
1153
        self._print(self.client.get_floating_ip(ip), self.print_dict)
1154

    
1155
    def main(self, IP):
1156
        super(self.__class__, self)._run()
1157
        self._run(ip=IP)
1158

    
1159

    
1160
@command(ip_cmds)
1161
class ip_reserve(_init_cyclades, _optional_json):
1162
    """Reserve a floating IP
1163
    An IP is reserved from an IP pool. The default IP pool is chosen
1164
    automatically, but there is the option if specifying an explicit IP pool.
1165
    """
1166

    
1167
    arguments = dict(pool=ValueArgument('Source IP pool', ('--pool'), None))
1168

    
1169
    @errors.generic.all
1170
    @errors.cyclades.connection
1171
    def _run(self, ip=None):
1172
        self._print([self.client.alloc_floating_ip(self['pool'], ip)])
1173

    
1174
    def main(self, requested_IP=None):
1175
        super(self.__class__, self)._run()
1176
        self._run(ip=requested_IP)
1177

    
1178

    
1179
@command(ip_cmds)
1180
class ip_release(_init_cyclades, _optional_output_cmd):
1181
    """Release a floating IP
1182
    The release IP is "returned" to the IP pool it came from.
1183
    """
1184

    
1185
    @errors.generic.all
1186
    @errors.cyclades.connection
1187
    def _run(self, ip):
1188
        self._optional_output(self.client.delete_floating_ip(ip))
1189

    
1190
    def main(self, IP):
1191
        super(self.__class__, self)._run()
1192
        self._run(ip=IP)
1193

    
1194

    
1195
@command(ip_cmds)
1196
class ip_attach(_init_cyclades, _optional_output_cmd):
1197
    """Attach a floating IP to a server
1198
    """
1199

    
1200
    @errors.generic.all
1201
    @errors.cyclades.connection
1202
    @errors.cyclades.server_id
1203
    def _run(self, server_id, ip):
1204
        self._optional_output(self.client.attach_floating_ip(server_id, ip))
1205

    
1206
    def main(self, server_id, IP):
1207
        super(self.__class__, self)._run()
1208
        self._run(server_id=server_id, ip=IP)
1209

    
1210

    
1211
@command(ip_cmds)
1212
class ip_detach(_init_cyclades, _optional_output_cmd):
1213
    """Detach a floating IP from a server
1214
    """
1215

    
1216
    @errors.generic.all
1217
    @errors.cyclades.connection
1218
    @errors.cyclades.server_id
1219
    def _run(self, server_id, ip):
1220
        self._optional_output(self.client.detach_floating_ip(server_id, ip))
1221

    
1222
    def main(self, server_id, IP):
1223
        super(self.__class__, self)._run()
1224
        self._run(server_id=server_id, ip=IP)