Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / cyclades.py @ dbf65aaf

History | View | Annotate | Download (32.1 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
45
from kamaki.cli.argument import (
46
    FlagArgument, ValueArgument, KeyValueArgument, RepeatableArgument,
47
    ProgressBarArgument, DateArgument, IntArgument)
48
from kamaki.cli.commands import _command_init, errors, addLogSettings
49
from kamaki.cli.commands import (
50
    _optional_output_cmd, _optional_json, _name_filter, _id_filter)
51

    
52

    
53
server_cmds = CommandTree('server', 'Cyclades/Compute API server commands')
54
flavor_cmds = CommandTree('flavor', 'Cyclades/Compute API flavor commands')
55
_commands = [server_cmds, flavor_cmds]
56

    
57

    
58
about_authentication = '\nUser Authentication:\
59
    \n* to check authentication: /user authenticate\
60
    \n* to set authentication token: /config set cloud.<cloud>.token <token>'
61

    
62
howto_personality = [
63
    'Defines a file to be injected to virtual servers file system.',
64
    'syntax:  PATH,[SERVER_PATH,[OWNER,[GROUP,[MODE]]]]',
65
    '  [local-path=]PATH: local file to be injected (relative or absolute)',
66
    '  [server-path=]SERVER_PATH: destination location inside server Image',
67
    '  [owner=]OWNER: virtual servers user id for the remote file',
68
    '  [group=]GROUP: virtual servers group id or name for the remote file',
69
    '  [mode=]MODE: permission in octal (e.g., 0777)',
70
    'e.g., -p /tmp/my.file,owner=root,mode=0777']
71

    
72

    
73
class _service_wait(object):
74

    
75
    wait_arguments = dict(
76
        progress_bar=ProgressBarArgument(
77
            'do not show progress bar', ('-N', '--no-progress-bar'), False)
78
    )
79

    
80
    def _wait(
81
            self, service, service_id, status_method, current_status,
82
            countdown=True, timeout=60):
83
        (progress_bar, wait_cb) = self._safe_progress_bar(
84
            '%s %s: status is still %s' % (
85
                service, service_id, current_status),
86
            countdown=countdown, timeout=timeout)
87

    
88
        try:
89
            new_mode = status_method(
90
                service_id, current_status, max_wait=timeout, wait_cb=wait_cb)
91
            if new_mode:
92
                self.error('%s %s: status is now %s' % (
93
                    service, service_id, new_mode))
94
            else:
95
                self.error('%s %s: status is still %s' % (
96
                    service, service_id, current_status))
97
        except KeyboardInterrupt:
98
            self.error('\n- canceled')
99
        finally:
100
            self._safe_progress_bar_finish(progress_bar)
101

    
102

    
103
class _server_wait(_service_wait):
104

    
105
    def _wait(self, server_id, current_status, timeout=60):
106
        super(_server_wait, self)._wait(
107
            'Server', server_id, self.client.wait_server, current_status,
108
            countdown=(current_status not in ('BUILD', )),
109
            timeout=timeout if current_status not in ('BUILD', ) else 100)
110

    
111

    
112
class _init_cyclades(_command_init):
113
    @errors.generic.all
114
    @addLogSettings
115
    def _run(self, service='compute'):
116
        if getattr(self, 'cloud', None):
117
            base_url = self._custom_url(service) or self._custom_url(
118
                'cyclades')
119
            if base_url:
120
                token = self._custom_token(service) or self._custom_token(
121
                    'cyclades') or self.config.get_cloud('token')
122
                self.client = CycladesClient(base_url=base_url, token=token)
123
                return
124
        else:
125
            self.cloud = 'default'
126
        if getattr(self, 'auth_base', False):
127
            cyclades_endpoints = self.auth_base.get_service_endpoints(
128
                self._custom_type('cyclades') or 'compute',
129
                self._custom_version('cyclades') or '')
130
            base_url = cyclades_endpoints['publicURL']
131
            token = self.auth_base.token
132
            self.client = CycladesClient(base_url=base_url, token=token)
133
        else:
134
            raise CLIBaseUrlError(service='cyclades')
135

    
136
    def main(self):
137
        self._run()
138

    
139

    
140
@command(server_cmds)
141
class server_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
142
    """List virtual servers accessible by user
143
    Use filtering arguments (e.g., --name-like) to manage long server lists
144
    """
145

    
146
    PERMANENTS = ('id', 'name')
147

    
148
    arguments = dict(
149
        detail=FlagArgument('show detailed output', ('-l', '--details')),
150
        since=DateArgument(
151
            'show only items since date (\' d/m/Y H:M:S \')',
152
            '--since'),
153
        limit=IntArgument(
154
            'limit number of listed virtual servers', ('-n', '--number')),
155
        more=FlagArgument(
156
            'output results in pages (-n to set items per page, default 10)',
157
            '--more'),
158
        enum=FlagArgument('Enumerate results', '--enumerate'),
159
        flavor_id=ValueArgument('filter by flavor id', ('--flavor-id')),
160
        image_id=ValueArgument('filter by image id', ('--image-id')),
161
        user_id=ValueArgument('filter by user id', ('--user-id')),
162
        user_name=ValueArgument('filter by user name', ('--user-name')),
163
        status=ValueArgument(
164
            'filter by status (ACTIVE, STOPPED, REBOOT, ERROR, etc.)',
165
            ('--status')),
166
        meta=KeyValueArgument('filter by metadata key=values', ('--metadata')),
167
        meta_like=KeyValueArgument(
168
            'print only if in key=value, the value is part of actual value',
169
            ('--metadata-like')),
170
    )
171

    
172
    def _add_user_name(self, servers):
173
        uuids = self._uuids2usernames(list(set(
174
                [srv['user_id'] for srv in servers] +
175
                [srv['tenant_id'] for srv in servers])))
176
        for srv in servers:
177
            srv['user_id'] += ' (%s)' % uuids[srv['user_id']]
178
            srv['tenant_id'] += ' (%s)' % uuids[srv['tenant_id']]
179
        return servers
180

    
181
    def _apply_common_filters(self, servers):
182
        common_filters = dict()
183
        if self['status']:
184
            common_filters['status'] = self['status']
185
        if self['user_id'] or self['user_name']:
186
            uuid = self['user_id'] or self._username2uuid(self['user_name'])
187
            common_filters['user_id'] = uuid
188
        return filter_dicts_by_dict(servers, common_filters)
189

    
190
    def _filter_by_image(self, servers):
191
        iid = self['image_id']
192
        return [srv for srv in servers if srv['image']['id'] == iid]
193

    
194
    def _filter_by_flavor(self, servers):
195
        fid = self['flavor_id']
196
        return [srv for srv in servers if (
197
            '%s' % srv['image']['id'] == '%s' % fid)]
198

    
199
    def _filter_by_metadata(self, servers):
200
        new_servers = []
201
        for srv in servers:
202
            if not 'metadata' in srv:
203
                continue
204
            meta = [dict(srv['metadata'])]
205
            if self['meta']:
206
                meta = filter_dicts_by_dict(meta, self['meta'])
207
            if meta and self['meta_like']:
208
                meta = filter_dicts_by_dict(
209
                    meta, self['meta_like'], exact_match=False)
210
            if meta:
211
                new_servers.append(srv)
212
        return new_servers
213

    
214
    @errors.generic.all
215
    @errors.cyclades.connection
216
    @errors.cyclades.date
217
    def _run(self):
218
        withimage = bool(self['image_id'])
219
        withflavor = bool(self['flavor_id'])
220
        withmeta = bool(self['meta'] or self['meta_like'])
221
        withcommons = bool(
222
            self['status'] or self['user_id'] or self['user_name'])
223
        detail = self['detail'] or (
224
            withimage or withflavor or withmeta or withcommons)
225
        servers = self.client.list_servers(detail, self['since'])
226

    
227
        servers = self._filter_by_name(servers)
228
        servers = self._filter_by_id(servers)
229
        servers = self._apply_common_filters(servers)
230
        if withimage:
231
            servers = self._filter_by_image(servers)
232
        if withflavor:
233
            servers = self._filter_by_flavor(servers)
234
        if withmeta:
235
            servers = self._filter_by_metadata(servers)
236

    
237
        if self['detail'] and not (
238
                self['json_output'] or self['output_format']):
239
            servers = self._add_user_name(servers)
240
        elif not (self['detail'] or (
241
                self['json_output'] or self['output_format'])):
242
            remove_from_items(servers, 'links')
243
        if detail and not self['detail']:
244
            for srv in servers:
245
                for key in set(srv).difference(self.PERMANENTS):
246
                    srv.pop(key)
247
        kwargs = dict(with_enumeration=self['enum'])
248
        if self['more']:
249
            kwargs['out'] = StringIO()
250
            kwargs['title'] = ()
251
        if self['limit']:
252
            servers = servers[:self['limit']]
253
        self._print(servers, **kwargs)
254
        if self['more']:
255
            pager(kwargs['out'].getvalue())
256

    
257
    def main(self):
258
        super(self.__class__, self)._run()
259
        self._run()
260

    
261

    
262
@command(server_cmds)
263
class server_info(_init_cyclades, _optional_json):
264
    """Detailed information on a Virtual Machine"""
265

    
266
    arguments = dict(
267
        addr=FlagArgument(
268
            'Show only the network interfaces of this virtual server',
269
            '--nics'),
270
        vnc=FlagArgument(
271
            'Show VNC connection information (valid for a short period)',
272
            '--vnc-credentials'),
273
        stats=FlagArgument('Get URLs for server statistics', '--stats')
274
    )
275

    
276
    @errors.generic.all
277
    @errors.cyclades.connection
278
    @errors.cyclades.server_id
279
    def _run(self, server_id):
280
        vm = self.client.get_server_details(server_id)
281
        if self['addr']:
282
            self._print(vm.get('attachments', []))
283
        elif self['vnc']:
284
            self.error(
285
                '(!) For security reasons, the following credentials are '
286
                'invalidated\nafter a short time period, depending on the '
287
                'server settings\n')
288
            self._print(
289
                self.client.get_server_console(server_id), self.print_dict)
290
        elif self['stats']:
291
            self._print(
292
                self.client.get_server_stats(server_id), self.print_dict)
293
        else:
294
            uuids = self._uuids2usernames([vm['user_id'], vm['tenant_id']])
295
            vm['user_id'] += ' (%s)' % uuids[vm['user_id']]
296
            vm['tenant_id'] += ' (%s)' % uuids[vm['tenant_id']]
297
            self._print(vm, self.print_dict)
298

    
299
    def main(self, server_id):
300
        super(self.__class__, self)._run()
301
        choose_one = ('addr', 'vnc', 'stats')
302
        count = len([a for a in choose_one if self[a]])
303
        if count > 1:
304
            raise CLIInvalidArgument('Invalid argument compination', details=[
305
                'Arguments %s cannot be used simultaneously' % ', '.join(
306
                    [self.arguments[a].lvalue for a in choose_one])])
307
        self._run(server_id=server_id)
308

    
309

    
310
class PersonalityArgument(KeyValueArgument):
311

    
312
    terms = (
313
        ('local-path', 'contents'),
314
        ('server-path', 'path'),
315
        ('owner', 'owner'),
316
        ('group', 'group'),
317
        ('mode', 'mode'))
318

    
319
    @property
320
    def value(self):
321
        return getattr(self, '_value', [])
322

    
323
    @value.setter
324
    def value(self, newvalue):
325
        if newvalue == self.default:
326
            return self.value
327
        self._value, input_dict = [], {}
328
        for i, terms in enumerate(newvalue):
329
            termlist = terms.split(',')
330
            if len(termlist) > len(self.terms):
331
                msg = 'Wrong number of terms (1<=terms<=%s)' % len(self.terms)
332
                raiseCLIError(CLISyntaxError(msg), details=howto_personality)
333

    
334
            for k, v in self.terms:
335
                prefix = '%s=' % k
336
                for item in termlist:
337
                    if item.lower().startswith(prefix):
338
                        input_dict[k] = item[len(k) + 1:]
339
                        break
340
                    item = None
341
                if item:
342
                    termlist.remove(item)
343

    
344
            try:
345
                path = input_dict['local-path']
346
            except KeyError:
347
                path = termlist.pop(0)
348
                if not path:
349
                    raise CLIInvalidArgument(
350
                        '--personality: No local path specified',
351
                        details=howto_personality)
352

    
353
            if not exists(path):
354
                raise CLIInvalidArgument(
355
                    '--personality: File %s does not exist' % path,
356
                    details=howto_personality)
357

    
358
            self._value.append(dict(path=path))
359
            with open(expanduser(path)) as f:
360
                self._value[i]['contents'] = b64encode(f.read())
361
            for k, v in self.terms[1:]:
362
                try:
363
                    self._value[i][v] = input_dict[k]
364
                except KeyError:
365
                    try:
366
                        self._value[i][v] = termlist.pop(0)
367
                    except IndexError:
368
                        continue
369
                if k in ('mode', ) and self._value[i][v]:
370
                    try:
371
                        self._value[i][v] = int(self._value[i][v], 8)
372
                    except ValueError as ve:
373
                        raise CLIInvalidArgument(
374
                            'Personality mode must be in octal', details=[
375
                                '%s' % ve])
376

    
377

    
378
class NetworkIpArgument(RepeatableArgument):
379

    
380
    @property
381
    def value(self):
382
        return getattr(self, '_value', [])
383

    
384
    @value.setter
385
    def value(self, new_value):
386
        for v in (new_value or []):
387
            net_and_ip = v.split(',')
388
            if len(net_and_ip) < 2:
389
                raise CLIInvalidArgument(
390
                    'Value "%s" is missing parts' % v,
391
                    details=['Correct format: %s NETWORK_ID,IP' % (
392
                        self.parsed_name[0])])
393
            self._value = getattr(self, '_value', list())
394
            self._value.append(
395
                dict(uuid=net_and_ip[0], fixed_ip=net_and_ip[1]))
396

    
397

    
398
@command(server_cmds)
399
class server_create(_init_cyclades, _optional_json, _server_wait):
400
    """Create a server (aka Virtual Machine)"""
401

    
402
    arguments = dict(
403
        server_name=ValueArgument('The name of the new server', '--name'),
404
        flavor_id=IntArgument('The ID of the hardware flavor', '--flavor-id'),
405
        image_id=ValueArgument('The ID of the hardware image', '--image-id'),
406
        personality=PersonalityArgument(
407
            (80 * ' ').join(howto_personality), ('-p', '--personality')),
408
        wait=FlagArgument('Wait server to build', ('-w', '--wait')),
409
        cluster_size=IntArgument(
410
            'Create a cluster of servers of this size. In this case, the name'
411
            'parameter is the prefix of each server in the cluster (e.g.,'
412
            'srv1, srv2, etc.',
413
            '--cluster-size'),
414
        max_threads=IntArgument(
415
            'Max threads in cluster mode (default 1)', '--threads'),
416
        network_id=RepeatableArgument(
417
            'Connect server to network (can be repeated)', '--network'),
418
        network_id_and_ip=NetworkIpArgument(
419
            'Connect server to network w. floating ip ( NETWORK_ID,IP )'
420
            '(can be repeated)',
421
            '--network-with-ip'),
422
        automatic_ip=FlagArgument(
423
            'Automatically assign an IP to the server', '--automatic-ip')
424
    )
425
    required = ('server_name', 'flavor_id', 'image_id')
426

    
427
    @errors.cyclades.cluster_size
428
    def _create_cluster(self, prefix, flavor_id, image_id, size):
429
        if self['automatic_ip']:
430
            networks = []
431
        else:
432
            networks = [dict(uuid=netid) for netid in (
433
                self['network_id'] or [])] + (self['network_id_and_ip'] or [])
434
            networks = networks or None
435
        servers = [dict(
436
            name='%s%s' % (prefix, i if size > 1 else ''),
437
            flavor_id=flavor_id,
438
            image_id=image_id,
439
            personality=self['personality'],
440
            networks=networks) for i in range(1, 1 + size)]
441
        if size == 1:
442
            return [self.client.create_server(**servers[0])]
443
        self.client.MAX_THREADS = int(self['max_threads'] or 1)
444
        try:
445
            r = self.client.async_run(self.client.create_server, servers)
446
            return r
447
        except Exception as e:
448
            if size == 1:
449
                raise e
450
            try:
451
                requested_names = [s['name'] for s in servers]
452
                spawned_servers = [dict(
453
                    name=s['name'],
454
                    id=s['id']) for s in self.client.list_servers() if (
455
                        s['name'] in requested_names)]
456
                self.error('Failed to build %s servers' % size)
457
                self.error('Found %s matching servers:' % len(spawned_servers))
458
                self._print(spawned_servers, out=self._err)
459
                self.error('Check if any of these servers should be removed\n')
460
            except Exception as ne:
461
                self.error('Error (%s) while notifying about errors' % ne)
462
            finally:
463
                raise e
464

    
465
    @errors.generic.all
466
    @errors.cyclades.connection
467
    @errors.plankton.id
468
    @errors.cyclades.flavor_id
469
    def _run(self, name, flavor_id, image_id):
470
        for r in self._create_cluster(
471
                name, flavor_id, image_id, size=self['cluster_size'] or 1):
472
            if not r:
473
                self.error('Create %s: server response was %s' % (name, r))
474
                continue
475
            usernames = self._uuids2usernames(
476
                [r['user_id'], r['tenant_id']])
477
            r['user_id'] += ' (%s)' % usernames[r['user_id']]
478
            r['tenant_id'] += ' (%s)' % usernames[r['tenant_id']]
479
            self._print(r, self.print_dict)
480
            if self['wait']:
481
                self._wait(r['id'], r['status'])
482
            self.writeln(' ')
483

    
484
    def main(self):
485
        super(self.__class__, self)._run()
486
        if self['automatic_ip'] and (
487
                self['network_id'] or self['network_id_and_ip']):
488
            raise CLIInvalidArgument('Invalid argument combination', details=[
489
                'Argument %s should not be combined with other' % (
490
                    self.arguments['automatic_ip'].lvalue),
491
                'network-related arguments i.e., %s or %s' % (
492
                    self.arguments['network_id'].lvalue,
493
                    self.arguments['network_id_and_ip'].lvalue)])
494
        self._run(
495
            name=self['server_name'],
496
            flavor_id=self['flavor_id'],
497
            image_id=self['image_id'])
498

    
499

    
500
class FirewallProfileArgument(ValueArgument):
501

    
502
    profiles = ('DISABLED', 'ENABLED', 'PROTECTED')
503

    
504
    @property
505
    def value(self):
506
        return getattr(self, '_value', None)
507

    
508
    @value.setter
509
    def value(self, new_profile):
510
        if new_profile:
511
            new_profile = new_profile.upper()
512
            if new_profile in self.profiles:
513
                self._value = new_profile
514
            else:
515
                raise CLIInvalidArgument(
516
                    'Invalid firewall profile %s' % new_profile,
517
                    details=['Valid values: %s' % ', '.join(self.profiles)])
518

    
519

    
520
@command(server_cmds)
521
class server_modify(_init_cyclades, _optional_output_cmd):
522
    """Modify attributes of a virtual server"""
523

    
524
    arguments = dict(
525
        server_name=ValueArgument('The new name', '--name'),
526
        flavor_id=IntArgument('Set a different flavor', '--flavor-id'),
527
        firewall_profile=FirewallProfileArgument(
528
            'Valid values: %s' % (', '.join(FirewallProfileArgument.profiles)),
529
            '--firewall'),
530
        metadata_to_set=KeyValueArgument(
531
            'Set metadata in key=value form (can be repeated)',
532
            '--metadata-set'),
533
        metadata_to_delete=RepeatableArgument(
534
            'Delete metadata by key (can be repeated)', '--metadata-del')
535
    )
536
    required = [
537
        'server_name', 'flavor_id', 'firewall_profile', 'metadata_to_set',
538
        'metadata_to_delete']
539

    
540
    @errors.generic.all
541
    @errors.cyclades.connection
542
    @errors.cyclades.server_id
543
    def _run(self, server_id):
544
        if self['server_name']:
545
            self.client.update_server_name((server_id), self['server_name'])
546
        if self['flavor_id']:
547
            self.client.resize_server(server_id, self['flavor_id'])
548
        if self['firewall_profile']:
549
            self.client.set_firewall_profile(
550
                server_id=server_id, profile=self['firewall_profile'])
551
        if self['metadata_to_set']:
552
            self.client.update_server_metadata(
553
                server_id, **self['metadata_to_set'])
554
        for key in (self['metadata_to_delete'] or []):
555
            errors.cyclades.metadata(
556
                self.client.delete_server_metadata)(server_id, key=key)
557
        if self['with_output']:
558
            self._optional_output(self.client.get_server_details(server_id))
559

    
560
    def main(self, server_id):
561
        super(self.__class__, self)._run()
562
        self._run(server_id=server_id)
563

    
564

    
565
@command(server_cmds)
566
class server_delete(_init_cyclades, _optional_output_cmd, _server_wait):
567
    """Delete a virtual server"""
568

    
569
    arguments = dict(
570
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait')),
571
        cluster=FlagArgument(
572
            '(DANGEROUS) Delete all virtual servers prefixed with the cluster '
573
            'prefix. In that case, the prefix replaces the server id',
574
            '--cluster')
575
    )
576

    
577
    def _server_ids(self, server_var):
578
        if self['cluster']:
579
            return [s['id'] for s in self.client.list_servers() if (
580
                s['name'].startswith(server_var))]
581

    
582
        @errors.cyclades.server_id
583
        def _check_server_id(self, server_id):
584
            return server_id
585

    
586
        return [_check_server_id(self, server_id=server_var), ]
587

    
588
    @errors.generic.all
589
    @errors.cyclades.connection
590
    def _run(self, server_var):
591
        for server_id in self._server_ids(server_var):
592
            if self['wait']:
593
                details = self.client.get_server_details(server_id)
594
                status = details['status']
595

    
596
            r = self.client.delete_server(server_id)
597
            self._optional_output(r)
598

    
599
            if self['wait']:
600
                self._wait(server_id, status)
601

    
602
    def main(self, server_id_or_cluster_prefix):
603
        super(self.__class__, self)._run()
604
        self._run(server_id_or_cluster_prefix)
605

    
606

    
607
@command(server_cmds)
608
class server_reboot(_init_cyclades, _optional_output_cmd, _server_wait):
609
    """Reboot a virtual server"""
610

    
611
    arguments = dict(
612
        hard=FlagArgument(
613
            'perform a hard reboot (deprecated)', ('-f', '--force')),
614
        type=ValueArgument('SOFT or HARD - default: SOFT', ('--type')),
615
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
616
    )
617

    
618
    @errors.generic.all
619
    @errors.cyclades.connection
620
    @errors.cyclades.server_id
621
    def _run(self, server_id):
622
        hard_reboot = self['hard']
623
        if hard_reboot:
624
            self.error(
625
                'WARNING: -f/--force will be deprecated in version 0.12\n'
626
                '\tIn the future, please use --type=hard instead')
627
        if self['type']:
628
            if self['type'].lower() in ('soft', ):
629
                hard_reboot = False
630
            elif self['type'].lower() in ('hard', ):
631
                hard_reboot = True
632
            else:
633
                raise CLISyntaxError(
634
                    'Invalid reboot type %s' % self['type'],
635
                    importance=2, details=[
636
                        '--type values are either SOFT (default) or HARD'])
637

    
638
        r = self.client.reboot_server(int(server_id), hard_reboot)
639
        self._optional_output(r)
640

    
641
        if self['wait']:
642
            self._wait(server_id, 'REBOOT')
643

    
644
    def main(self, server_id):
645
        super(self.__class__, self)._run()
646
        self._run(server_id=server_id)
647

    
648

    
649
@command(server_cmds)
650
class server_start(_init_cyclades, _optional_output_cmd, _server_wait):
651
    """Start an existing virtual server"""
652

    
653
    arguments = dict(
654
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
655
    )
656

    
657
    @errors.generic.all
658
    @errors.cyclades.connection
659
    @errors.cyclades.server_id
660
    def _run(self, server_id):
661
        status = 'ACTIVE'
662
        if self['wait']:
663
            details = self.client.get_server_details(server_id)
664
            status = details['status']
665
            if status in ('ACTIVE', ):
666
                return
667

    
668
        r = self.client.start_server(int(server_id))
669
        self._optional_output(r)
670

    
671
        if self['wait']:
672
            self._wait(server_id, status)
673

    
674
    def main(self, server_id):
675
        super(self.__class__, self)._run()
676
        self._run(server_id=server_id)
677

    
678

    
679
@command(server_cmds)
680
class server_shutdown(_init_cyclades, _optional_output_cmd, _server_wait):
681
    """Shutdown an active virtual server"""
682

    
683
    arguments = dict(
684
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
685
    )
686

    
687
    @errors.generic.all
688
    @errors.cyclades.connection
689
    @errors.cyclades.server_id
690
    def _run(self, server_id):
691
        status = 'STOPPED'
692
        if self['wait']:
693
            details = self.client.get_server_details(server_id)
694
            status = details['status']
695
            if status in ('STOPPED', ):
696
                return
697

    
698
        r = self.client.shutdown_server(int(server_id))
699
        self._optional_output(r)
700

    
701
        if self['wait']:
702
            self._wait(server_id, status)
703

    
704
    def main(self, server_id):
705
        super(self.__class__, self)._run()
706
        self._run(server_id=server_id)
707

    
708

    
709
@command(server_cmds)
710
class server_addr(_init_cyclades):
711
    """DEPRECATED, use: [kamaki] server info SERVER_ID --nics"""
712

    
713
    def main(self, *args):
714
        raiseCLIError('DEPRECATED since v0.12', importance=3, details=[
715
            'Replaced by',
716
            '  [kamaki] server info <SERVER_ID> --nics'])
717

    
718

    
719
@command(server_cmds)
720
class server_console(_init_cyclades, _optional_json):
721
    """DEPRECATED, use: [kamaki] server info SERVER_ID --vnc-credentials"""
722

    
723
    def main(self, *args):
724
        raiseCLIError('DEPRECATED since v0.12', importance=3, details=[
725
            'Replaced by',
726
            '  [kamaki] server info <SERVER_ID> --vnc-credentials'])
727

    
728

    
729
@command(server_cmds)
730
class server_rename(_init_cyclades, _optional_json):
731
    """DEPRECATED, use: [kamaki] server modify SERVER_ID --name=NEW_NAME"""
732

    
733
    def main(self, *args):
734
        raiseCLIError('DEPRECATED since v0.12', importance=3, details=[
735
            'Replaced by',
736
            '  [kamaki] server modify <SERVER_ID> --name=NEW_NAME'])
737

    
738

    
739
@command(server_cmds)
740
class server_stats(_init_cyclades, _optional_json):
741
    """DEPRECATED, use: [kamaki] server info SERVER_ID --stats"""
742

    
743
    def main(self, *args):
744
        raiseCLIError('DEPRECATED since v0.12', importance=3, details=[
745
            'Replaced by',
746
            '  [kamaki] server info <SERVER_ID> --stats'])
747

    
748

    
749
@command(server_cmds)
750
class server_wait(_init_cyclades, _server_wait):
751
    """Wait for server to finish (BUILD, STOPPED, REBOOT, ACTIVE)"""
752

    
753
    arguments = dict(
754
        timeout=IntArgument(
755
            'Wait limit in seconds (default: 60)', '--timeout', default=60)
756
    )
757

    
758
    @errors.generic.all
759
    @errors.cyclades.connection
760
    @errors.cyclades.server_id
761
    def _run(self, server_id, current_status):
762
        r = self.client.get_server_details(server_id)
763
        if r['status'].lower() == current_status.lower():
764
            self._wait(server_id, current_status, timeout=self['timeout'])
765
        else:
766
            self.error(
767
                'Server %s: Cannot wait for status %s, '
768
                'status is already %s' % (
769
                    server_id, current_status, r['status']))
770

    
771
    def main(self, server_id, current_status='BUILD'):
772
        super(self.__class__, self)._run()
773
        self._run(server_id=server_id, current_status=current_status)
774

    
775

    
776
@command(flavor_cmds)
777
class flavor_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
778
    """List available hardware flavors"""
779

    
780
    PERMANENTS = ('id', 'name')
781

    
782
    arguments = dict(
783
        detail=FlagArgument('show detailed output', ('-l', '--details')),
784
        limit=IntArgument('limit # of listed flavors', ('-n', '--number')),
785
        more=FlagArgument(
786
            'output results in pages (-n to set items per page, default 10)',
787
            '--more'),
788
        enum=FlagArgument('Enumerate results', '--enumerate'),
789
        ram=ValueArgument('filter by ram', ('--ram')),
790
        vcpus=ValueArgument('filter by number of VCPUs', ('--vcpus')),
791
        disk=ValueArgument('filter by disk size in GB', ('--disk')),
792
        disk_template=ValueArgument(
793
            'filter by disk_templace', ('--disk-template'))
794
    )
795

    
796
    def _apply_common_filters(self, flavors):
797
        common_filters = dict()
798
        if self['ram']:
799
            common_filters['ram'] = self['ram']
800
        if self['vcpus']:
801
            common_filters['vcpus'] = self['vcpus']
802
        if self['disk']:
803
            common_filters['disk'] = self['disk']
804
        if self['disk_template']:
805
            common_filters['SNF:disk_template'] = self['disk_template']
806
        return filter_dicts_by_dict(flavors, common_filters)
807

    
808
    @errors.generic.all
809
    @errors.cyclades.connection
810
    def _run(self):
811
        withcommons = self['ram'] or self['vcpus'] or (
812
            self['disk'] or self['disk_template'])
813
        detail = self['detail'] or withcommons
814
        flavors = self.client.list_flavors(detail)
815
        flavors = self._filter_by_name(flavors)
816
        flavors = self._filter_by_id(flavors)
817
        if withcommons:
818
            flavors = self._apply_common_filters(flavors)
819
        if not (self['detail'] or (
820
                self['json_output'] or self['output_format'])):
821
            remove_from_items(flavors, 'links')
822
        if detail and not self['detail']:
823
            for flv in flavors:
824
                for key in set(flv).difference(self.PERMANENTS):
825
                    flv.pop(key)
826
        kwargs = dict(out=StringIO(), title=()) if self['more'] else {}
827
        self._print(
828
            flavors,
829
            with_redundancy=self['detail'], with_enumeration=self['enum'],
830
            **kwargs)
831
        if self['more']:
832
            pager(kwargs['out'].getvalue())
833

    
834
    def main(self):
835
        super(self.__class__, self)._run()
836
        self._run()
837

    
838

    
839
@command(flavor_cmds)
840
class flavor_info(_init_cyclades, _optional_json):
841
    """Detailed information on a hardware flavor
842
    To get a list of available flavors and flavor ids, try /flavor list
843
    """
844

    
845
    @errors.generic.all
846
    @errors.cyclades.connection
847
    @errors.cyclades.flavor_id
848
    def _run(self, flavor_id):
849
        self._print(
850
            self.client.get_flavor_details(int(flavor_id)), self.print_dict)
851

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

    
856

    
857
def _add_name(self, net):
858
        user_id, tenant_id, uuids = net['user_id'], net['tenant_id'], []
859
        if user_id:
860
            uuids.append(user_id)
861
        if tenant_id:
862
            uuids.append(tenant_id)
863
        if uuids:
864
            usernames = self._uuids2usernames(uuids)
865
            if user_id:
866
                net['user_id'] += ' (%s)' % usernames[user_id]
867
            if tenant_id:
868
                net['tenant_id'] += ' (%s)' % usernames[tenant_id]