Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / cyclades.py @ 89a1c636

History | View | Annotate | Download (34.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, StatusArgument)
48
from kamaki.cli.commands import _command_init, errors, addLogSettings
49
from kamaki.cli.commands import (
50
    _optional_output_cmd, _optional_json, _name_filter, _id_filter)
51

    
52

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

    
57

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

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

    
72
server_states = ('BUILD', 'ACTIVE', 'STOPPED', 'REBOOT')
73

    
74

    
75
class _service_wait(object):
76

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

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

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

    
104

    
105
class _server_wait(_service_wait):
106

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

    
113

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

    
138
    def main(self):
139
        self._run()
140

    
141

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

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

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

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

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

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

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

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

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

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

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

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

    
263

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

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

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

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

    
311

    
312
class PersonalityArgument(KeyValueArgument):
313

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

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

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

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

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

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

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

    
379

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

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

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

    
421

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

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

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

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

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

    
525

    
526
class FirewallProfileArgument(ValueArgument):
527

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

    
530
    @property
531
    def value(self):
532
        return getattr(self, '_value', None)
533

    
534
    @value.setter
535
    def value(self, new_profile):
536
        if new_profile:
537
            new_profile = new_profile.upper()
538
            if new_profile in self.profiles:
539
                self._value = new_profile
540
            else:
541
                raise CLIInvalidArgument(
542
                    'Invalid firewall profile %s' % new_profile,
543
                    details=['Valid values: %s' % ', '.join(self.profiles)])
544

    
545

    
546
@command(server_cmds)
547
class server_modify(_init_cyclades, _optional_output_cmd):
548
    """Modify attributes of a virtual server"""
549

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

    
566
    @errors.generic.all
567
    @errors.cyclades.connection
568
    @errors.cyclades.server_id
569
    def _run(self, server_id):
570
        if self['server_name'] is not None:
571
            self.client.update_server_name((server_id), self['server_name'])
572
        if self['flavor_id']:
573
            self.client.resize_server(server_id, self['flavor_id'])
574
        if self['firewall_profile']:
575
            self.client.set_firewall_profile(
576
                server_id=server_id, profile=self['firewall_profile'])
577
        if self['metadata_to_set']:
578
            self.client.update_server_metadata(
579
                server_id, **self['metadata_to_set'])
580
        for key in (self['metadata_to_delete'] or []):
581
            errors.cyclades.metadata(
582
                self.client.delete_server_metadata)(server_id, key=key)
583
        if self['with_output']:
584
            self._optional_output(self.client.get_server_details(server_id))
585

    
586
    def main(self, server_id):
587
        super(self.__class__, self)._run()
588
        self._run(server_id=server_id)
589

    
590

    
591
@command(server_cmds)
592
class server_reassign(_init_cyclades, _optional_json):
593
    """Assign a VM to a different project
594
    """
595

    
596
    @errors.generic.all
597
    @errors.cyclades.connection
598
    @errors.cyclades.server_id
599
    def _run(self, server_id, project):
600
        self.client.reassign_server(server_id, project)
601

    
602
    def main(self, server_id, project):
603
        super(self.__class__, self)._run()
604
        self._run(server_id=server_id, project=project)
605

    
606

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

    
611
    arguments = dict(
612
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait')),
613
        cluster=FlagArgument(
614
            '(DANGEROUS) Delete all virtual servers prefixed with the cluster '
615
            'prefix. In that case, the prefix replaces the server id',
616
            '--cluster')
617
    )
618

    
619
    def _server_ids(self, server_var):
620
        if self['cluster']:
621
            return [s['id'] for s in self.client.list_servers() if (
622
                s['name'].startswith(server_var))]
623

    
624
        @errors.cyclades.server_id
625
        def _check_server_id(self, server_id):
626
            return server_id
627

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

    
630
    @errors.generic.all
631
    @errors.cyclades.connection
632
    def _run(self, server_var):
633
        for server_id in self._server_ids(server_var):
634
            if self['wait']:
635
                details = self.client.get_server_details(server_id)
636
                status = details['status']
637

    
638
            r = self.client.delete_server(server_id)
639
            self._optional_output(r)
640

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

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

    
648

    
649
@command(server_cmds)
650
class server_reboot(_init_cyclades, _optional_output_cmd, _server_wait):
651
    """Reboot a virtual server"""
652

    
653
    arguments = dict(
654
        hard=FlagArgument(
655
            'perform a hard reboot (deprecated)', ('-f', '--force')),
656
        type=ValueArgument('SOFT or HARD - default: SOFT', ('--type')),
657
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
658
    )
659

    
660
    @errors.generic.all
661
    @errors.cyclades.connection
662
    @errors.cyclades.server_id
663
    def _run(self, server_id):
664
        hard_reboot = self['hard']
665
        if hard_reboot:
666
            self.error(
667
                'WARNING: -f/--force will be deprecated in version 0.12\n'
668
                '\tIn the future, please use --type=hard instead')
669
        if self['type']:
670
            if self['type'].lower() in ('soft', ):
671
                hard_reboot = False
672
            elif self['type'].lower() in ('hard', ):
673
                hard_reboot = True
674
            else:
675
                raise CLISyntaxError(
676
                    'Invalid reboot type %s' % self['type'],
677
                    importance=2, details=[
678
                        '--type values are either SOFT (default) or HARD'])
679

    
680
        r = self.client.reboot_server(int(server_id), hard_reboot)
681
        self._optional_output(r)
682

    
683
        if self['wait']:
684
            self._wait(server_id, 'REBOOT')
685

    
686
    def main(self, server_id):
687
        super(self.__class__, self)._run()
688
        self._run(server_id=server_id)
689

    
690

    
691
@command(server_cmds)
692
class server_start(_init_cyclades, _optional_output_cmd, _server_wait):
693
    """Start an existing virtual server"""
694

    
695
    arguments = dict(
696
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
697
    )
698

    
699
    @errors.generic.all
700
    @errors.cyclades.connection
701
    @errors.cyclades.server_id
702
    def _run(self, server_id):
703
        status = 'ACTIVE'
704
        if self['wait']:
705
            details = self.client.get_server_details(server_id)
706
            status = details['status']
707
            if status in ('ACTIVE', ):
708
                return
709

    
710
        r = self.client.start_server(int(server_id))
711
        self._optional_output(r)
712

    
713
        if self['wait']:
714
            self._wait(server_id, status)
715

    
716
    def main(self, server_id):
717
        super(self.__class__, self)._run()
718
        self._run(server_id=server_id)
719

    
720

    
721
@command(server_cmds)
722
class server_shutdown(_init_cyclades, _optional_output_cmd, _server_wait):
723
    """Shutdown an active virtual server"""
724

    
725
    arguments = dict(
726
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
727
    )
728

    
729
    @errors.generic.all
730
    @errors.cyclades.connection
731
    @errors.cyclades.server_id
732
    def _run(self, server_id):
733
        status = 'STOPPED'
734
        if self['wait']:
735
            details = self.client.get_server_details(server_id)
736
            status = details['status']
737
            if status in ('STOPPED', ):
738
                return
739

    
740
        r = self.client.shutdown_server(int(server_id))
741
        self._optional_output(r)
742

    
743
        if self['wait']:
744
            self._wait(server_id, status)
745

    
746
    def main(self, server_id):
747
        super(self.__class__, self)._run()
748
        self._run(server_id=server_id)
749

    
750

    
751
@command(server_cmds)
752
class server_nics(_init_cyclades):
753
    """DEPRECATED, use: [kamaki] server info SERVER_ID --nics"""
754

    
755
    def main(self, *args):
756
        raiseCLIError('DEPRECATED since v0.12', importance=3, details=[
757
            'Replaced by',
758
            '  [kamaki] server info <SERVER_ID> --nics'])
759

    
760

    
761
@command(server_cmds)
762
class server_console(_init_cyclades, _optional_json):
763
    """Create a VMC console and show connection information"""
764

    
765
    @errors.generic.all
766
    @errors.cyclades.connection
767
    @errors.cyclades.server_id
768
    def _run(self, server_id):
769
        self.error('The following credentials will be invalidated shortly')
770
        self._print(
771
            self.client.get_server_console(server_id), self.print_dict)
772

    
773
    def main(self, server_id):
774
        super(self.__class__, self)._run()
775
        self._run(server_id=server_id)
776

    
777

    
778
@command(server_cmds)
779
class server_rename(_init_cyclades, _optional_json):
780
    """DEPRECATED, use: [kamaki] server modify SERVER_ID --name=NEW_NAME"""
781

    
782
    def main(self, *args):
783
        raiseCLIError('DEPRECATED since v0.12', importance=3, details=[
784
            'Replaced by',
785
            '  [kamaki] server modify <SERVER_ID> --name=NEW_NAME'])
786

    
787

    
788
@command(server_cmds)
789
class server_stats(_init_cyclades, _optional_json):
790
    """DEPRECATED, use: [kamaki] server info SERVER_ID --stats"""
791

    
792
    def main(self, *args):
793
        raiseCLIError('DEPRECATED since v0.12', importance=3, details=[
794
            'Replaced by',
795
            '  [kamaki] server info <SERVER_ID> --stats'])
796

    
797

    
798
@command(server_cmds)
799
class server_wait(_init_cyclades, _server_wait):
800
    """Wait for server to change its status (default: BUILD)"""
801

    
802
    arguments = dict(
803
        timeout=IntArgument(
804
            'Wait limit in seconds (default: 60)', '--timeout', default=60),
805
        server_status=StatusArgument(
806
            'Status to wait for (%s, default: %s)' % (
807
                ', '.join(server_states), server_states[0]),
808
            '--status',
809
            valid_states=server_states)
810
    )
811

    
812
    @errors.generic.all
813
    @errors.cyclades.connection
814
    @errors.cyclades.server_id
815
    def _run(self, server_id, current_status):
816
        r = self.client.get_server_details(server_id)
817
        if r['status'].lower() == current_status.lower():
818
            self._wait(server_id, current_status, timeout=self['timeout'])
819
        else:
820
            self.error(
821
                'Server %s: Cannot wait for status %s, '
822
                'status is already %s' % (
823
                    server_id, current_status, r['status']))
824

    
825
    def main(self, server_id):
826
        super(self.__class__, self)._run()
827
        self._run(server_id=server_id, current_status=self['server_status'])
828

    
829

    
830
@command(flavor_cmds)
831
class flavor_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
832
    """List available hardware flavors"""
833

    
834
    PERMANENTS = ('id', 'name')
835

    
836
    arguments = dict(
837
        detail=FlagArgument('show detailed output', ('-l', '--details')),
838
        limit=IntArgument('limit # of listed flavors', ('-n', '--number')),
839
        more=FlagArgument(
840
            'output results in pages (-n to set items per page, default 10)',
841
            '--more'),
842
        enum=FlagArgument('Enumerate results', '--enumerate'),
843
        ram=ValueArgument('filter by ram', ('--ram')),
844
        vcpus=ValueArgument('filter by number of VCPUs', ('--vcpus')),
845
        disk=ValueArgument('filter by disk size in GB', ('--disk')),
846
        disk_template=ValueArgument(
847
            'filter by disk_templace', ('--disk-template'))
848
    )
849

    
850
    def _apply_common_filters(self, flavors):
851
        common_filters = dict()
852
        if self['ram']:
853
            common_filters['ram'] = self['ram']
854
        if self['vcpus']:
855
            common_filters['vcpus'] = self['vcpus']
856
        if self['disk']:
857
            common_filters['disk'] = self['disk']
858
        if self['disk_template']:
859
            common_filters['SNF:disk_template'] = self['disk_template']
860
        return filter_dicts_by_dict(flavors, common_filters)
861

    
862
    @errors.generic.all
863
    @errors.cyclades.connection
864
    def _run(self):
865
        withcommons = self['ram'] or self['vcpus'] or (
866
            self['disk'] or self['disk_template'])
867
        detail = self['detail'] or withcommons
868
        flavors = self.client.list_flavors(detail)
869
        flavors = self._filter_by_name(flavors)
870
        flavors = self._filter_by_id(flavors)
871
        if withcommons:
872
            flavors = self._apply_common_filters(flavors)
873
        if not (self['detail'] or (
874
                self['json_output'] or self['output_format'])):
875
            remove_from_items(flavors, 'links')
876
        if detail and not self['detail']:
877
            for flv in flavors:
878
                for key in set(flv).difference(self.PERMANENTS):
879
                    flv.pop(key)
880
        kwargs = dict(out=StringIO(), title=()) if self['more'] else {}
881
        self._print(
882
            flavors,
883
            with_redundancy=self['detail'], with_enumeration=self['enum'],
884
            **kwargs)
885
        if self['more']:
886
            pager(kwargs['out'].getvalue())
887

    
888
    def main(self):
889
        super(self.__class__, self)._run()
890
        self._run()
891

    
892

    
893
@command(flavor_cmds)
894
class flavor_info(_init_cyclades, _optional_json):
895
    """Detailed information on a hardware flavor
896
    To get a list of available flavors and flavor ids, try /flavor list
897
    """
898

    
899
    @errors.generic.all
900
    @errors.cyclades.connection
901
    @errors.cyclades.flavor_id
902
    def _run(self, flavor_id):
903
        self._print(
904
            self.client.get_flavor_details(int(flavor_id)), self.print_dict)
905

    
906
    def main(self, flavor_id):
907
        super(self.__class__, self)._run()
908
        self._run(flavor_id=flavor_id)
909

    
910

    
911
def _add_name(self, net):
912
        user_id, tenant_id, uuids = net['user_id'], net['tenant_id'], []
913
        if user_id:
914
            uuids.append(user_id)
915
        if tenant_id:
916
            uuids.append(tenant_id)
917
        if uuids:
918
            usernames = self._uuids2usernames(uuids)
919
            if user_id:
920
                net['user_id'] += ' (%s)' % usernames[user_id]
921
            if tenant_id:
922
                net['tenant_id'] += ' (%s)' % usernames[tenant_id]