cyclades.py 33.4 KB
Newer Older
1
# Copyright 2011-2013 GRNET S.A. All rights reserved.
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that the following
# conditions are met:
#
#   1. Redistributions of source code must retain the above
#      copyright notice, this list of conditions and the following
#      disclaimer.
#
#   2. Redistributions in binary form must reproduce the above
#      copyright notice, this list of conditions and the following
#      disclaimer in the documentation and/or other materials
#      provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# The views and conclusions contained in the software and
# documentation are those of the authors and should not be
# interpreted as representing official policies, either expressed
# or implied, of GRNET S.A.

34
from base64 import b64encode
35
from os.path import exists, expanduser
36
37
38
from io import StringIO
from pydoc import pager

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
43
from kamaki.cli.errors import (
    raiseCLIError, CLISyntaxError, CLIBaseUrlError, CLIInvalidArgument)
44
from kamaki.clients.cyclades import CycladesClient
45
46
from kamaki.cli.argument import (
    FlagArgument, ValueArgument, KeyValueArgument, RepeatableArgument,
47
    ProgressBarArgument, DateArgument, IntArgument, StatusArgument)
48
from kamaki.cli.commands import _command_init, errors, addLogSettings
49
50
from kamaki.cli.commands import (
    _optional_output_cmd, _optional_json, _name_filter, _id_filter)
51

Stavros Sachtouris's avatar
Stavros Sachtouris committed
52

53
54
server_cmds = CommandTree('server', 'Cyclades/Compute API server commands')
flavor_cmds = CommandTree('flavor', 'Cyclades/Compute API flavor commands')
Stavros Sachtouris's avatar
Stavros Sachtouris committed
55
_commands = [server_cmds, flavor_cmds]
Stavros Sachtouris's avatar
Stavros Sachtouris committed
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
66
67
68
    '  [local-path=]PATH: local file to be injected (relative or absolute)',
    '  [server-path=]SERVER_PATH: destination location inside server Image',
    '  [owner=]OWNER: virtual servers user id for the remote file',
    '  [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
server_states = ('BUILD', 'ACTIVE', 'STOPPED', 'REBOOT')

74

75
class _service_wait(object):
76
77
78

    wait_arguments = dict(
        progress_bar=ProgressBarArgument(
79
            'do not show progress bar', ('-N', '--no-progress-bar'), False)
80
81
    )

82
83
    def _wait(
            self, service, service_id, status_method, current_status,
84
            countdown=True, timeout=60):
85
        (progress_bar, wait_cb) = self._safe_progress_bar(
86
87
            '%s %s: status is still %s' % (
                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
96
                    service, service_id, new_mode))
            else:
97
                self.error('%s %s: status is still %s' % (
98
                    service, service_id, current_status))
99
100
        except KeyboardInterrupt:
            self.error('\n- canceled')
101
102
103
104
        finally:
            self._safe_progress_bar_finish(progress_bar)


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
111
            countdown=(current_status not in ('BUILD', )),
            timeout=timeout if current_status not in ('BUILD', ) else 100)
112
113


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

138
139
    def main(self):
        self._run()
Stavros Sachtouris's avatar
Stavros Sachtouris committed
140

Stavros Sachtouris's avatar
Stavros Sachtouris committed
141

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

148
149
    PERMANENTS = ('id', 'name')

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

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

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

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

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

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

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

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

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

259
260
261
262
    def main(self):
        super(self.__class__, self)._run()
        self._run()

Stavros Sachtouris's avatar
Stavros Sachtouris committed
263

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

    arguments = dict(
269
        nics=FlagArgument(
270
271
            'Show only the network interfaces of this virtual server',
            '--nics'),
272
273
        network_id=ValueArgument(
            'Show the connection details to that network', '--network-id'),
Stavros Sachtouris's avatar
Stavros Sachtouris committed
274
275
        stats=FlagArgument('Get URLs for server statistics', '--stats'),
        diagnostics=FlagArgument('Diagnostic information', '--diagnostics')
276
    )
277

278
279
    @errors.generic.all
    @errors.cyclades.connection
Stavros Sachtouris's avatar
Stavros Sachtouris committed
280
    @errors.cyclades.server_id
281
    def _run(self, server_id):
282
        if self['nics']:
283
284
            self._print(
                self.client.get_server_nics(server_id), self.print_dict)
285
286
287
288
        elif self['network_id']:
            self._print(
                self.client.get_server_network_nics(
                    server_id, self['network_id']), self.print_dict)
289
290
291
292
        elif self['stats']:
            self._print(
                self.client.get_server_stats(server_id), self.print_dict)
        else:
293
            vm = self.client.get_server_details(server_id)
294
295
296
297
            uuids = self._uuids2usernames([vm['user_id'], vm['tenant_id']])
            vm['user_id'] += ' (%s)' % uuids[vm['user_id']]
            vm['tenant_id'] += ' (%s)' % uuids[vm['tenant_id']]
            self._print(vm, self.print_dict)
298

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

Stavros Sachtouris's avatar
Stavros Sachtouris committed
309

310
class PersonalityArgument(KeyValueArgument):
311
312
313
314
315
316
317
318

    terms = (
        ('local-path', 'contents'),
        ('server-path', 'path'),
        ('owner', 'owner'),
        ('group', 'group'),
        ('mode', 'mode'))

Stavros Sachtouris's avatar
Stavros Sachtouris committed
319
    @property
320
    def value(self):
321
        return getattr(self, '_value', [])
Stavros Sachtouris's avatar
Stavros Sachtouris committed
322
323

    @value.setter
324
325
326
    def value(self, newvalue):
        if newvalue == self.default:
            return self.value
327
        self._value, input_dict = [], {}
328
329
        for i, terms in enumerate(newvalue):
            termlist = terms.split(',')
330
331
            if len(termlist) > len(self.terms):
                msg = 'Wrong number of terms (1<=terms<=%s)' % len(self.terms)
332
                raiseCLIError(CLISyntaxError(msg), details=howto_personality)
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352

            for k, v in self.terms:
                prefix = '%s=' % k
                for item in termlist:
                    if item.lower().startswith(prefix):
                        input_dict[k] = item[len(k) + 1:]
                        break
                    item = None
                if item:
                    termlist.remove(item)

            try:
                path = input_dict['local-path']
            except KeyError:
                path = termlist.pop(0)
                if not path:
                    raise CLIInvalidArgument(
                        '--personality: No local path specified',
                        details=howto_personality)

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

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

Stavros Sachtouris's avatar
Stavros Sachtouris committed
377

378
379
class NetworkArgument(RepeatableArgument):
    """[id=]NETWORK_ID[,[ip=]IP]"""
380
381
382

    @property
    def value(self):
383
        return getattr(self, '_value', self.default)
384
385
386

    @value.setter
    def value(self, new_value):
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
        for v in new_value or []:
            part1, sep, part2 = v.partition(',')
            netid, ip = '', ''
            if part1.startswith('id='):
                netid = part1[len('id='):]
            elif part1.startswith('ip='):
                ip = part1[len('ip='):]
            else:
                netid = part1
            if part2:
                if (part2.startswith('id=') and netid) or (
                        part2.startswith('ip=') and ip):
                    raise CLIInvalidArgument(
                        'Invalid network argument %s' % v, details=[
                        'Valid format: [id=]NETWORK_ID[,[ip=]IP]'])
                if part2.startswith('id='):
                    netid = part2[len('id='):]
                elif part2.startswith('ip='):
                    ip = part2[len('ip='):]
                elif netid:
                    ip = part2
                else:
                    netid = part2
            if not netid:
411
                raise CLIInvalidArgument(
412
413
414
415
416
417
                    'Invalid network argument %s' % v, details=[
                    'Valid format: [id=]NETWORK_ID[,[ip=]IP]'])
            self._value = getattr(self, '_value', [])
            self._value.append(dict(uuid=netid))
            if ip:
                self._value[-1]['fixed_ip'] = ip
418
419


420
@command(server_cmds)
421
class server_create(_init_cyclades, _optional_json, _server_wait):
422
    """Create a server (aka Virtual Machine)"""
423

424
    arguments = dict(
425
        server_name=ValueArgument('The name of the new server', '--name'),
426
427
        flavor_id=IntArgument('The ID of the flavor', '--flavor-id'),
        image_id=ValueArgument('The ID of the image', '--image-id'),
428
        personality=PersonalityArgument(
429
            (80 * ' ').join(howto_personality), ('-p', '--personality')),
430
431
432
433
434
        wait=FlagArgument('Wait server to build', ('-w', '--wait')),
        cluster_size=IntArgument(
            'Create a cluster of servers of this size. In this case, the name'
            'parameter is the prefix of each server in the cluster (e.g.,'
            'srv1, srv2, etc.',
435
436
            '--cluster-size'),
        max_threads=IntArgument(
437
            'Max threads in cluster mode (default 1)', '--threads'),
438
439
440
441
442
443
444
445
446
447
        network_configuration=NetworkArgument(
            'Connect server to network: [id=]NETWORK_ID[,[ip=]IP]        . '
            'Use only NETWORK_ID for private networks.        . '
            'Use NETWORK_ID,[ip=]IP for networks with IP.        . '
            'Can be repeated, mutually exclussive with --no-network',
            '--network'),
        no_network=FlagArgument(
            'Do not create any network NICs on the server.        . '
            'Mutually exclusive to --network        . '
            'If neither --network or --no-network are used, the default '
448
449
            'network policy is applied. These policies are set on the cloud, '
            'so kamaki is oblivious to them',
450
            '--no-network')
451
    )
452
    required = ('server_name', 'flavor_id', 'image_id')
453

454
455
    @errors.cyclades.cluster_size
    def _create_cluster(self, prefix, flavor_id, image_id, size):
456
        networks = self['network_configuration'] or (
457
            [] if self['no_network'] else None)
458
        servers = [dict(
459
            name='%s%s' % (prefix, i if size > 1 else ''),
460
461
            flavor_id=flavor_id,
            image_id=image_id,
462
463
            personality=self['personality'],
            networks=networks) for i in range(1, 1 + size)]
464
465
        if size == 1:
            return [self.client.create_server(**servers[0])]
466
        self.client.MAX_THREADS = int(self['max_threads'] or 1)
467
        try:
468
469
            r = self.client.async_run(self.client.create_server, servers)
            return r
470
471
472
473
474
475
476
477
478
479
        except Exception as e:
            if size == 1:
                raise e
            try:
                requested_names = [s['name'] for s in servers]
                spawned_servers = [dict(
                    name=s['name'],
                    id=s['id']) for s in self.client.list_servers() if (
                        s['name'] in requested_names)]
                self.error('Failed to build %s servers' % size)
480
                self.error('Found %s matching servers:' % len(spawned_servers))
481
482
483
484
485
486
                self._print(spawned_servers, out=self._err)
                self.error('Check if any of these servers should be removed\n')
            except Exception as ne:
                self.error('Error (%s) while notifying about errors' % ne)
            finally:
                raise e
487

488
489
490
491
492
    @errors.generic.all
    @errors.cyclades.connection
    @errors.plankton.id
    @errors.cyclades.flavor_id
    def _run(self, name, flavor_id, image_id):
493
494
        for r in self._create_cluster(
                name, flavor_id, image_id, size=self['cluster_size'] or 1):
495
496
497
498
499
            if not r:
                self.error('Create %s: server response was %s' % (name, r))
                continue
            usernames = self._uuids2usernames(
                [r['user_id'], r['tenant_id']])
500
501
502
503
504
            r['user_id'] += ' (%s)' % usernames[r['user_id']]
            r['tenant_id'] += ' (%s)' % usernames[r['tenant_id']]
            self._print(r, self.print_dict)
            if self['wait']:
                self._wait(r['id'], r['status'])
505
            self.writeln(' ')
506

507
    def main(self):
508
        super(self.__class__, self)._run()
509
        if self['no_network'] and self['network_configuration']:
510
511
512
513
            raise CLIInvalidArgument(
                'Invalid argument compination', importance=2, details=[
                'Arguments %s and %s are mutually exclusive' % (
                    self.arguments['no_network'].lvalue,
514
                    self.arguments['network_configuration'].lvalue)])
515
516
517
518
        self._run(
            name=self['server_name'],
            flavor_id=self['flavor_id'],
            image_id=self['image_id'])
519

Stavros Sachtouris's avatar
Stavros Sachtouris committed
520

521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
class FirewallProfileArgument(ValueArgument):

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

    @property
    def value(self):
        return getattr(self, '_value', None)

    @value.setter
    def value(self, new_profile):
        if new_profile:
            new_profile = new_profile.upper()
            if new_profile in self.profiles:
                self._value = new_profile
            else:
                raise CLIInvalidArgument(
                    'Invalid firewall profile %s' % new_profile,
                    details=['Valid values: %s' % ', '.join(self.profiles)])
539

Stavros Sachtouris's avatar
Stavros Sachtouris committed
540

541
@command(server_cmds)
542
543
544
545
546
547
class server_modify(_init_cyclades, _optional_output_cmd):
    """Modify attributes of a virtual server"""

    arguments = dict(
        server_name=ValueArgument('The new name', '--name'),
        flavor_id=IntArgument('Set a different flavor', '--flavor-id'),
548
549
550
551
552
        firewall_profile=FirewallProfileArgument(
            'Valid values: %s' % (', '.join(FirewallProfileArgument.profiles)),
            '--firewall'),
        metadata_to_set=KeyValueArgument(
            'Set metadata in key=value form (can be repeated)',
553
            '--metadata-set'),
554
        metadata_to_delete=RepeatableArgument(
555
            'Delete metadata by key (can be repeated)', '--metadata-del')
556
    )
557
558
    required = [
        'server_name', 'flavor_id', 'firewall_profile', 'metadata_to_set',
559
        'metadata_to_delete']
560

Stavros Sachtouris's avatar
Stavros Sachtouris committed
561
562
563
    @errors.generic.all
    @errors.cyclades.connection
    @errors.cyclades.server_id
564
565
566
567
568
    def _run(self, server_id):
        if self['server_name']:
            self.client.update_server_name((server_id), self['server_name'])
        if self['flavor_id']:
            self.client.resize_server(server_id, self['flavor_id'])
569
570
571
572
573
574
        if self['firewall_profile']:
            self.client.set_firewall_profile(
                server_id=server_id, profile=self['firewall_profile'])
        if self['metadata_to_set']:
            self.client.update_server_metadata(
                server_id, **self['metadata_to_set'])
575
        for key in (self['metadata_to_delete'] or []):
576
577
            errors.cyclades.metadata(
                self.client.delete_server_metadata)(server_id, key=key)
578
579
        if self['with_output']:
            self._optional_output(self.client.get_server_details(server_id))
Stavros Sachtouris's avatar
Stavros Sachtouris committed
580

581
    def main(self, server_id):
Stavros Sachtouris's avatar
Stavros Sachtouris committed
582
        super(self.__class__, self)._run()
583
        self._run(server_id=server_id)
Stavros Sachtouris's avatar
Stavros Sachtouris committed
584

585

586
@command(server_cmds)
587
class server_delete(_init_cyclades, _optional_output_cmd, _server_wait):
588
    """Delete a virtual server"""
589

590
    arguments = dict(
591
592
593
594
595
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait')),
        cluster=FlagArgument(
            '(DANGEROUS) Delete all virtual servers prefixed with the cluster '
            'prefix. In that case, the prefix replaces the server id',
            '--cluster')
596
597
    )

598
599
600
601
602
603
604
605
606
607
608
    def _server_ids(self, server_var):
        if self['cluster']:
            return [s['id'] for s in self.client.list_servers() if (
                s['name'].startswith(server_var))]

        @errors.cyclades.server_id
        def _check_server_id(self, server_id):
            return server_id

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

Stavros Sachtouris's avatar
Stavros Sachtouris committed
609
610
    @errors.generic.all
    @errors.cyclades.connection
611
612
    def _run(self, server_var):
        for server_id in self._server_ids(server_var):
613
614
615
616
            if self['wait']:
                details = self.client.get_server_details(server_id)
                status = details['status']

617
            r = self.client.delete_server(server_id)
618
619
620
621
            self._optional_output(r)

            if self['wait']:
                self._wait(server_id, status)
Stavros Sachtouris's avatar
Stavros Sachtouris committed
622

623
    def main(self, server_id_or_cluster_prefix):
Stavros Sachtouris's avatar
Stavros Sachtouris committed
624
        super(self.__class__, self)._run()
625
        self._run(server_id_or_cluster_prefix)
Stavros Sachtouris's avatar
Stavros Sachtouris committed
626

627

628
@command(server_cmds)
629
class server_reboot(_init_cyclades, _optional_output_cmd, _server_wait):
630
    """Reboot a virtual server"""
631

632
    arguments = dict(
633
634
635
        hard=FlagArgument(
            'perform a hard reboot (deprecated)', ('-f', '--force')),
        type=ValueArgument('SOFT or HARD - default: SOFT', ('--type')),
636
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
637
    )
638

Stavros Sachtouris's avatar
Stavros Sachtouris committed
639
640
641
642
    @errors.generic.all
    @errors.cyclades.connection
    @errors.cyclades.server_id
    def _run(self, server_id):
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
        hard_reboot = self['hard']
        if hard_reboot:
            self.error(
                'WARNING: -f/--force will be deprecated in version 0.12\n'
                '\tIn the future, please use --type=hard instead')
        if self['type']:
            if self['type'].lower() in ('soft', ):
                hard_reboot = False
            elif self['type'].lower() in ('hard', ):
                hard_reboot = True
            else:
                raise CLISyntaxError(
                    'Invalid reboot type %s' % self['type'],
                    importance=2, details=[
                        '--type values are either SOFT (default) or HARD'])

        r = self.client.reboot_server(int(server_id), hard_reboot)
660
661
662
663
        self._optional_output(r)

        if self['wait']:
            self._wait(server_id, 'REBOOT')
Stavros Sachtouris's avatar
Stavros Sachtouris committed
664

665
    def main(self, server_id):
Stavros Sachtouris's avatar
Stavros Sachtouris committed
666
667
        super(self.__class__, self)._run()
        self._run(server_id=server_id)
Stavros Sachtouris's avatar
Stavros Sachtouris committed
668

669

670
@command(server_cmds)
671
class server_start(_init_cyclades, _optional_output_cmd, _server_wait):
672
    """Start an existing virtual server"""
673

674
675
676
677
    arguments = dict(
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
    )

Stavros Sachtouris's avatar
Stavros Sachtouris committed
678
679
680
681
    @errors.generic.all
    @errors.cyclades.connection
    @errors.cyclades.server_id
    def _run(self, server_id):
682
683
684
685
686
687
688
689
690
691
692
693
        status = 'ACTIVE'
        if self['wait']:
            details = self.client.get_server_details(server_id)
            status = details['status']
            if status in ('ACTIVE', ):
                return

        r = self.client.start_server(int(server_id))
        self._optional_output(r)

        if self['wait']:
            self._wait(server_id, status)
Stavros Sachtouris's avatar
Stavros Sachtouris committed
694

695
    def main(self, server_id):
Stavros Sachtouris's avatar
Stavros Sachtouris committed
696
697
        super(self.__class__, self)._run()
        self._run(server_id=server_id)
Stavros Sachtouris's avatar
Stavros Sachtouris committed
698

699

700
@command(server_cmds)
701
class server_shutdown(_init_cyclades, _optional_output_cmd, _server_wait):
702
    """Shutdown an active virtual server"""
703

704
705
706
707
    arguments = dict(
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
    )

Stavros Sachtouris's avatar
Stavros Sachtouris committed
708
709
710
711
    @errors.generic.all
    @errors.cyclades.connection
    @errors.cyclades.server_id
    def _run(self, server_id):
712
713
714
715
716
717
718
719
720
721
722
723
        status = 'STOPPED'
        if self['wait']:
            details = self.client.get_server_details(server_id)
            status = details['status']
            if status in ('STOPPED', ):
                return

        r = self.client.shutdown_server(int(server_id))
        self._optional_output(r)

        if self['wait']:
            self._wait(server_id, status)
Stavros Sachtouris's avatar
Stavros Sachtouris committed
724

725
    def main(self, server_id):
Stavros Sachtouris's avatar
Stavros Sachtouris committed
726
727
        super(self.__class__, self)._run()
        self._run(server_id=server_id)
Stavros Sachtouris's avatar
Stavros Sachtouris committed
728

729

730
@command(server_cmds)
731
class server_nics(_init_cyclades):
Stavros Sachtouris's avatar
Stavros Sachtouris committed
732
    """DEPRECATED, use: [kamaki] server info SERVER_ID --nics"""
733

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

Stavros Sachtouris's avatar
Stavros Sachtouris committed
739

740
@command(server_cmds)
741
class server_console(_init_cyclades, _optional_json):
Stavros Sachtouris's avatar
Stavros Sachtouris committed
742
    """Create a VMC console and show connection information"""
743

Stavros Sachtouris's avatar
Stavros Sachtouris committed
744
745
746
747
748
749
750
751
752
753
754
    @errors.generic.all
    @errors.cyclades.connection
    @errors.cyclades.server_id
    def _run(self, server_id):
        self.error('The following credentials will be invalidated shortly')
        self._print(
            self.client.get_server_console(server_id), self.print_dict)

    def main(self, server_id):
        super(self.__class__, self)._run()
        self._run(server_id=server_id)
Stavros Sachtouris's avatar
Stavros Sachtouris committed
755

Stavros Sachtouris's avatar
Stavros Sachtouris committed
756

Stavros Sachtouris's avatar
Stavros Sachtouris committed
757
758
759
760
761
762
763
@command(server_cmds)
class server_rename(_init_cyclades, _optional_json):
    """DEPRECATED, use: [kamaki] server modify SERVER_ID --name=NEW_NAME"""

    def main(self, *args):
        raiseCLIError('DEPRECATED since v0.12', importance=3, details=[
            'Replaced by',
764
            '  [kamaki] server modify <SERVER_ID> --name=NEW_NAME'])
Stavros Sachtouris's avatar
Stavros Sachtouris committed
765
766


767
@command(server_cmds)
768
class server_stats(_init_cyclades, _optional_json):
Stavros Sachtouris's avatar
Stavros Sachtouris committed
769
    """DEPRECATED, use: [kamaki] server info SERVER_ID --stats"""
Stavros Sachtouris's avatar
Stavros Sachtouris committed
770

771
    def main(self, *args):
772
        raiseCLIError('DEPRECATED since v0.12', importance=3, details=[
773
774
            'Replaced by',
            '  [kamaki] server info <SERVER_ID> --stats'])
775

Stavros Sachtouris's avatar
Stavros Sachtouris committed
776

777
@command(server_cmds)
778
class server_wait(_init_cyclades, _server_wait):
779
    """Wait for server to change its status (default: BUILD)"""
780

781
782
    arguments = dict(
        timeout=IntArgument(
783
784
785
786
787
788
            'Wait limit in seconds (default: 60)', '--timeout', default=60),
        server_status=StatusArgument(
            'Status to wait for (%s, default: %s)' % (
                ', '.join(server_states), server_states[0]),
            '--status',
            valid_states=server_states)
789
790
    )

Stavros Sachtouris's avatar
Stavros Sachtouris committed
791
792
793
    @errors.generic.all
    @errors.cyclades.connection
    @errors.cyclades.server_id
794
795
796
    def _run(self, server_id, current_status):
        r = self.client.get_server_details(server_id)
        if r['status'].lower() == current_status.lower():
797
            self._wait(server_id, current_status, timeout=self['timeout'])
798
799
800
801
802
        else:
            self.error(
                'Server %s: Cannot wait for status %s, '
                'status is already %s' % (
                    server_id, current_status, r['status']))
803

804
    def main(self, server_id):
Stavros Sachtouris's avatar
Stavros Sachtouris committed
805
        super(self.__class__, self)._run()
806
        self._run(server_id=server_id, current_status=self['server_status'])
Stavros Sachtouris's avatar
Stavros Sachtouris committed
807

808

809
@command(flavor_cmds)
810
class flavor_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
811
    """List available hardware flavors"""
812

813
814
    PERMANENTS = ('id', 'name')

815
    arguments = dict(
816
817
        detail=FlagArgument('show detailed output', ('-l', '--details')),
        limit=IntArgument('limit # of listed flavors', ('-n', '--number')),
818
        more=FlagArgument(
819
            'output results in pages (-n to set items per page, default 10)',
820
            '--more'),
821
822
823
824
825
826
        enum=FlagArgument('Enumerate results', '--enumerate'),
        ram=ValueArgument('filter by ram', ('--ram')),
        vcpus=ValueArgument('filter by number of VCPUs', ('--vcpus')),
        disk=ValueArgument('filter by disk size in GB', ('--disk')),
        disk_template=ValueArgument(
            'filter by disk_templace', ('--disk-template'))
827
    )
828

829
830
831
832
833
834
835
836
837
838
839
840
    def _apply_common_filters(self, flavors):
        common_filters = dict()
        if self['ram']:
            common_filters['ram'] = self['ram']
        if self['vcpus']:
            common_filters['vcpus'] = self['vcpus']
        if self['disk']:
            common_filters['disk'] = self['disk']
        if self['disk_template']:
            common_filters['SNF:disk_template'] = self['disk_template']
        return filter_dicts_by_dict(flavors, common_filters)

Stavros Sachtouris's avatar
Stavros Sachtouris committed
841
842
843
    @errors.generic.all
    @errors.cyclades.connection
    def _run(self):
844
845
846
847
        withcommons = self['ram'] or self['vcpus'] or (
            self['disk'] or self['disk_template'])
        detail = self['detail'] or withcommons
        flavors = self.client.list_flavors(detail)
848
849
        flavors = self._filter_by_name(flavors)
        flavors = self._filter_by_id(flavors)
850
851
        if withcommons:
            flavors = self._apply_common_filters(flavors)
852
853
        if not (self['detail'] or (
                self['json_output'] or self['output_format'])):
854
            remove_from_items(flavors, 'links')
855
856
857
858
        if detail and not self['detail']:
            for flv in flavors:
                for key in set(flv).difference(self.PERMANENTS):
                    flv.pop(key)
859
        kwargs = dict(out=StringIO(), title=()) if self['more'] else {}
860
        self._print(
861
            flavors,
862
863
864
865
            with_redundancy=self['detail'], with_enumeration=self['enum'],
            **kwargs)
        if self['more']:
            pager(kwargs['out'].getvalue())
Stavros Sachtouris's avatar
Stavros Sachtouris committed
866

867
    def main(self):
Stavros Sachtouris's avatar
Stavros Sachtouris committed
868
869
        super(self.__class__, self)._run()
        self._run()
870

Stavros Sachtouris's avatar
Stavros Sachtouris committed
871

872
@command(flavor_cmds)
873
class flavor_info(_init_cyclades, _optional_json):
874
    """Detailed information on a hardware flavor
875
876
    To get a list of available flavors and flavor ids, try /flavor list
    """
877

Stavros Sachtouris's avatar
Stavros Sachtouris committed
878
879
880
881
    @errors.generic.all
    @errors.cyclades.connection
    @errors.cyclades.flavor_id
    def _run(self, flavor_id):
882
        self._print(
883
            self.client.get_flavor_details(int(flavor_id)), self.print_dict)
884

Stavros Sachtouris's avatar
Stavros Sachtouris committed
885
886
887
888
    def main(self, flavor_id):
        super(self.__class__, self)._run()
        self._run(flavor_id=flavor_id)

Stavros Sachtouris's avatar
Stavros Sachtouris committed
889

890
891
892
893
894
895
896
897
898
899
900
901
def _add_name(self, net):
        user_id, tenant_id, uuids = net['user_id'], net['tenant_id'], []
        if user_id:
            uuids.append(user_id)
        if tenant_id:
            uuids.append(tenant_id)
        if uuids:
            usernames = self._uuids2usernames(uuids)
            if user_id:
                net['user_id'] += ' (%s)' % usernames[user_id]
            if tenant_id:
                net['tenant_id'] += ' (%s)' % usernames[tenant_id]