Statistics
| Branch: | Tag: | Revision:

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

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

    
50

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

    
56

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

    
61
howto_personality = [
62
    'Defines a file to be injected to virtual servers file system.',
63
    'syntax:  PATH,[SERVER_PATH,[OWNER,[GROUP,[MODE]]]]',
64
    '  PATH: local file to be injected (relative or absolute)',
65
    '  SERVER_PATH: destination location inside server Image',
66
    '  OWNER: virtual servers user id of the remote destination file',
67
    '  GROUP: virtual servers group id or name of the destination file',
68
    '  MODEL: permition in octal (e.g., 0777 or o+rwx)']
69

    
70

    
71
class _service_wait(object):
72

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

    
78
    def _wait(self, service, service_id, status_method, currect_status):
79
        (progress_bar, wait_cb) = self._safe_progress_bar(
80
            '%s %s: periodically check if status is %s' % (
81
                service, service_id, currect_status))
82

    
83
        try:
84
            new_mode = status_method(
85
                service_id, currect_status, wait_cb=wait_cb)
86
            if new_mode:
87
                self.error('\n%s %s: status is %s' % (
88
                    service, service_id, new_mode))
89
            else:
90
                self.error('\nTime out: %s %s still in %s' % (
91
                    service, service_id, currect_status))
92
        except KeyboardInterrupt:
93
            self.error('\n- canceled')
94
        finally:
95
            self._safe_progress_bar_finish(progress_bar)
96

    
97

    
98
class _server_wait(_service_wait):
99

    
100
    def _wait(self, server_id, currect_status):
101
        super(_server_wait, self)._wait(
102
            'Server', server_id, self.client.wait_server, currect_status)
103

    
104

    
105
class _network_wait(_service_wait):
106

    
107
    def _wait(self, net_id, currect_status):
108
        super(_network_wait, self)._wait(
109
            'Network', net_id, self.client.wait_network, currect_status)
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 self['json_output']:
238
            servers = self._add_user_name(servers)
239
        elif not (self['detail'] or self['json_output']):
240
            remove_from_items(servers, 'links')
241
        if detail and not self['detail']:
242
            for srv in servers:
243
                for key in set(srv).difference(self.PERMANENTS):
244
                    srv.pop(key)
245
        kwargs = dict(with_enumeration=self['enum'])
246
        if self['more']:
247
            kwargs['out'] = StringIO()
248
            kwargs['title'] = ()
249
        if self['limit']:
250
            servers = servers[:self['limit']]
251
        self._print(servers, **kwargs)
252
        if self['more']:
253
            pager(kwargs['out'].getvalue())
254

    
255
    def main(self):
256
        super(self.__class__, self)._run()
257
        self._run()
258

    
259

    
260
@command(server_cmds)
261
class server_info(_init_cyclades, _optional_json):
262
    """Detailed information on a Virtual Machine
263
    Contains:
264
    - name, id, status, create/update dates
265
    - network interfaces
266
    - metadata (e.g., os, superuser) and diagnostics
267
    - hardware flavor and os image ids
268
    """
269

    
270
    @errors.generic.all
271
    @errors.cyclades.connection
272
    @errors.cyclades.server_id
273
    def _run(self, server_id):
274
        vm = self.client.get_server_details(server_id)
275
        uuids = self._uuids2usernames([vm['user_id'], vm['tenant_id']])
276
        vm['user_id'] += ' (%s)' % uuids[vm['user_id']]
277
        vm['tenant_id'] += ' (%s)' % uuids[vm['tenant_id']]
278
        self._print(vm, self.print_dict)
279

    
280
    def main(self, server_id):
281
        super(self.__class__, self)._run()
282
        self._run(server_id=server_id)
283

    
284

    
285
class PersonalityArgument(KeyValueArgument):
286
    @property
287
    def value(self):
288
        return self._value if hasattr(self, '_value') else []
289

    
290
    @value.setter
291
    def value(self, newvalue):
292
        if newvalue == self.default:
293
            return self.value
294
        self._value = []
295
        for i, terms in enumerate(newvalue):
296
            termlist = terms.split(',')
297
            if len(termlist) > 5:
298
                msg = 'Wrong number of terms (should be 1 to 5)'
299
                raiseCLIError(CLISyntaxError(msg), details=howto_personality)
300
            path = termlist[0]
301
            if not exists(path):
302
                raiseCLIError(
303
                    None,
304
                    '--personality: File %s does not exist' % path,
305
                    importance=1, details=howto_personality)
306
            self._value.append(dict(path=path))
307
            with open(path) as f:
308
                self._value[i]['contents'] = b64encode(f.read())
309
            try:
310
                self._value[i]['path'] = termlist[1]
311
                self._value[i]['owner'] = termlist[2]
312
                self._value[i]['group'] = termlist[3]
313
                self._value[i]['mode'] = termlist[4]
314
            except IndexError:
315
                pass
316

    
317

    
318
@command(server_cmds)
319
class server_create(_init_cyclades, _optional_json, _server_wait):
320
    """Create a server (aka Virtual Machine)
321
    Parameters:
322
    - name: (single quoted text)
323
    - flavor id: Hardware flavor. Pick one from: /flavor list
324
    - image id: OS images. Pick one from: /image list
325
    """
326

    
327
    arguments = dict(
328
        personality=PersonalityArgument(
329
            (80 * ' ').join(howto_personality), ('-p', '--personality')),
330
        wait=FlagArgument('Wait server to build', ('-w', '--wait'))
331
    )
332

    
333
    @errors.generic.all
334
    @errors.cyclades.connection
335
    @errors.plankton.id
336
    @errors.cyclades.flavor_id
337
    def _run(self, name, flavor_id, image_id):
338
        r = self.client.create_server(
339
            name, int(flavor_id), image_id, personality=self['personality'])
340
        usernames = self._uuids2usernames([r['user_id'], r['tenant_id']])
341
        r['user_id'] += ' (%s)' % usernames[r['user_id']]
342
        r['tenant_id'] += ' (%s)' % usernames[r['tenant_id']]
343
        self._print(r, self.print_dict)
344
        if self['wait']:
345
            self._wait(r['id'], r['status'])
346

    
347
    def main(self, name, flavor_id, image_id):
348
        super(self.__class__, self)._run()
349
        self._run(name=name, flavor_id=flavor_id, image_id=image_id)
350

    
351

    
352
@command(server_cmds)
353
class server_rename(_init_cyclades, _optional_output_cmd):
354
    """Set/update a virtual server name
355
    virtual server names are not unique, therefore multiple servers may share
356
    the same name
357
    """
358

    
359
    @errors.generic.all
360
    @errors.cyclades.connection
361
    @errors.cyclades.server_id
362
    def _run(self, server_id, new_name):
363
        self._optional_output(
364
            self.client.update_server_name(int(server_id), new_name))
365

    
366
    def main(self, server_id, new_name):
367
        super(self.__class__, self)._run()
368
        self._run(server_id=server_id, new_name=new_name)
369

    
370

    
371
@command(server_cmds)
372
class server_delete(_init_cyclades, _optional_output_cmd, _server_wait):
373
    """Delete a virtual server"""
374

    
375
    arguments = dict(
376
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
377
    )
378

    
379
    @errors.generic.all
380
    @errors.cyclades.connection
381
    @errors.cyclades.server_id
382
    def _run(self, server_id):
383
            status = 'DELETED'
384
            if self['wait']:
385
                details = self.client.get_server_details(server_id)
386
                status = details['status']
387

    
388
            r = self.client.delete_server(int(server_id))
389
            self._optional_output(r)
390

    
391
            if self['wait']:
392
                self._wait(server_id, status)
393

    
394
    def main(self, server_id):
395
        super(self.__class__, self)._run()
396
        self._run(server_id=server_id)
397

    
398

    
399
@command(server_cmds)
400
class server_reboot(_init_cyclades, _optional_output_cmd, _server_wait):
401
    """Reboot a virtual server"""
402

    
403
    arguments = dict(
404
        hard=FlagArgument(
405
            'perform a hard reboot (deprecated)', ('-f', '--force')),
406
        type=ValueArgument('SOFT or HARD - default: SOFT', ('--type')),
407
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
408
    )
409

    
410
    @errors.generic.all
411
    @errors.cyclades.connection
412
    @errors.cyclades.server_id
413
    def _run(self, server_id):
414
        hard_reboot = self['hard']
415
        if hard_reboot:
416
            self.error(
417
                'WARNING: -f/--force will be deprecated in version 0.12\n'
418
                '\tIn the future, please use --type=hard instead')
419
        if self['type']:
420
            if self['type'].lower() in ('soft', ):
421
                hard_reboot = False
422
            elif self['type'].lower() in ('hard', ):
423
                hard_reboot = True
424
            else:
425
                raise CLISyntaxError(
426
                    'Invalid reboot type %s' % self['type'],
427
                    importance=2, details=[
428
                        '--type values are either SOFT (default) or HARD'])
429

    
430
        r = self.client.reboot_server(int(server_id), hard_reboot)
431
        self._optional_output(r)
432

    
433
        if self['wait']:
434
            self._wait(server_id, 'REBOOT')
435

    
436
    def main(self, server_id):
437
        super(self.__class__, self)._run()
438
        self._run(server_id=server_id)
439

    
440

    
441
@command(server_cmds)
442
class server_start(_init_cyclades, _optional_output_cmd, _server_wait):
443
    """Start an existing virtual server"""
444

    
445
    arguments = dict(
446
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
447
    )
448

    
449
    @errors.generic.all
450
    @errors.cyclades.connection
451
    @errors.cyclades.server_id
452
    def _run(self, server_id):
453
        status = 'ACTIVE'
454
        if self['wait']:
455
            details = self.client.get_server_details(server_id)
456
            status = details['status']
457
            if status in ('ACTIVE', ):
458
                return
459

    
460
        r = self.client.start_server(int(server_id))
461
        self._optional_output(r)
462

    
463
        if self['wait']:
464
            self._wait(server_id, status)
465

    
466
    def main(self, server_id):
467
        super(self.__class__, self)._run()
468
        self._run(server_id=server_id)
469

    
470

    
471
@command(server_cmds)
472
class server_shutdown(_init_cyclades, _optional_output_cmd, _server_wait):
473
    """Shutdown an active virtual server"""
474

    
475
    arguments = dict(
476
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
477
    )
478

    
479
    @errors.generic.all
480
    @errors.cyclades.connection
481
    @errors.cyclades.server_id
482
    def _run(self, server_id):
483
        status = 'STOPPED'
484
        if self['wait']:
485
            details = self.client.get_server_details(server_id)
486
            status = details['status']
487
            if status in ('STOPPED', ):
488
                return
489

    
490
        r = self.client.shutdown_server(int(server_id))
491
        self._optional_output(r)
492

    
493
        if self['wait']:
494
            self._wait(server_id, status)
495

    
496
    def main(self, server_id):
497
        super(self.__class__, self)._run()
498
        self._run(server_id=server_id)
499

    
500

    
501
@command(server_cmds)
502
class server_console(_init_cyclades, _optional_json):
503
    """Get a VNC console to access an existing virtual server
504
    Console connection information provided (at least):
505
    - host: (url or address) a VNC host
506
    - port: (int) the gateway to enter virtual server on host
507
    - password: for VNC authorization
508
    """
509

    
510
    @errors.generic.all
511
    @errors.cyclades.connection
512
    @errors.cyclades.server_id
513
    def _run(self, server_id):
514
        self._print(
515
            self.client.get_server_console(int(server_id)), self.print_dict)
516

    
517
    def main(self, server_id):
518
        super(self.__class__, self)._run()
519
        self._run(server_id=server_id)
520

    
521

    
522
@command(server_cmds)
523
class server_resize(_init_cyclades, _optional_output_cmd):
524
    """Set a different flavor for an existing server
525
    To get server ids and flavor ids:
526
    /server list
527
    /flavor list
528
    """
529

    
530
    @errors.generic.all
531
    @errors.cyclades.connection
532
    @errors.cyclades.server_id
533
    @errors.cyclades.flavor_id
534
    def _run(self, server_id, flavor_id):
535
        self._optional_output(self.client.resize_server(server_id, flavor_id))
536

    
537
    def main(self, server_id, flavor_id):
538
        super(self.__class__, self)._run()
539
        self._run(server_id=server_id, flavor_id=flavor_id)
540

    
541

    
542
@command(server_cmds)
543
class server_firewall(_init_cyclades):
544
    """Manage virtual server firewall profiles for public networks"""
545

    
546

    
547
@command(server_cmds)
548
class server_firewall_set(_init_cyclades, _optional_output_cmd):
549
    """Set the firewall profile on virtual server public network
550
    Values for profile:
551
    - DISABLED: Shutdown firewall
552
    - ENABLED: Firewall in normal mode
553
    - PROTECTED: Firewall in secure mode
554
    """
555

    
556
    @errors.generic.all
557
    @errors.cyclades.connection
558
    @errors.cyclades.server_id
559
    @errors.cyclades.firewall
560
    def _run(self, server_id, profile):
561
        self._optional_output(self.client.set_firewall_profile(
562
            server_id=int(server_id), profile=('%s' % profile).upper()))
563

    
564
    def main(self, server_id, profile):
565
        super(self.__class__, self)._run()
566
        self._run(server_id=server_id, profile=profile)
567

    
568

    
569
@command(server_cmds)
570
class server_firewall_get(_init_cyclades):
571
    """Get the firewall profile for a virtual servers' public network"""
572

    
573
    @errors.generic.all
574
    @errors.cyclades.connection
575
    @errors.cyclades.server_id
576
    def _run(self, server_id):
577
        self.writeln(self.client.get_firewall_profile(server_id))
578

    
579
    def main(self, server_id):
580
        super(self.__class__, self)._run()
581
        self._run(server_id=server_id)
582

    
583

    
584
@command(server_cmds)
585
class server_addr(_init_cyclades, _optional_json):
586
    """List the addresses of all network interfaces on a virtual server"""
587

    
588
    arguments = dict(
589
        enum=FlagArgument('Enumerate results', '--enumerate')
590
    )
591

    
592
    @errors.generic.all
593
    @errors.cyclades.connection
594
    @errors.cyclades.server_id
595
    def _run(self, server_id):
596
        reply = self.client.list_server_nics(int(server_id))
597
        self._print(reply, with_enumeration=self['enum'] and (reply) > 1)
598

    
599
    def main(self, server_id):
600
        super(self.__class__, self)._run()
601
        self._run(server_id=server_id)
602

    
603

    
604
@command(server_cmds)
605
class server_metadata(_init_cyclades):
606
    """Manage Server metadata (key:value pairs of server attributes)"""
607

    
608

    
609
@command(server_cmds)
610
class server_metadata_list(_init_cyclades, _optional_json):
611
    """Get server metadata"""
612

    
613
    @errors.generic.all
614
    @errors.cyclades.connection
615
    @errors.cyclades.server_id
616
    @errors.cyclades.metadata
617
    def _run(self, server_id, key=''):
618
        self._print(
619
            self.client.get_server_metadata(int(server_id), key),
620
            self.print_dict)
621

    
622
    def main(self, server_id, key=''):
623
        super(self.__class__, self)._run()
624
        self._run(server_id=server_id, key=key)
625

    
626

    
627
@command(server_cmds)
628
class server_metadata_set(_init_cyclades, _optional_json):
629
    """Set / update virtual server metadata
630
    Metadata should be given in key/value pairs in key=value format
631
    For example: /server metadata set <server id> key1=value1 key2=value2
632
    Old, unreferenced metadata will remain intact
633
    """
634

    
635
    @errors.generic.all
636
    @errors.cyclades.connection
637
    @errors.cyclades.server_id
638
    def _run(self, server_id, keyvals):
639
        assert keyvals, 'Please, add some metadata ( key=value)'
640
        metadata = dict()
641
        for keyval in keyvals:
642
            k, sep, v = keyval.partition('=')
643
            if sep and k:
644
                metadata[k] = v
645
            else:
646
                raiseCLIError(
647
                    'Invalid piece of metadata %s' % keyval,
648
                    importance=2, details=[
649
                        'Correct metadata format: key=val',
650
                        'For example:',
651
                        '/server metadata set <server id>'
652
                        'key1=value1 key2=value2'])
653
        self._print(
654
            self.client.update_server_metadata(int(server_id), **metadata),
655
            self.print_dict)
656

    
657
    def main(self, server_id, *key_equals_val):
658
        super(self.__class__, self)._run()
659
        self._run(server_id=server_id, keyvals=key_equals_val)
660

    
661

    
662
@command(server_cmds)
663
class server_metadata_delete(_init_cyclades, _optional_output_cmd):
664
    """Delete virtual server metadata"""
665

    
666
    @errors.generic.all
667
    @errors.cyclades.connection
668
    @errors.cyclades.server_id
669
    @errors.cyclades.metadata
670
    def _run(self, server_id, key):
671
        self._optional_output(
672
            self.client.delete_server_metadata(int(server_id), key))
673

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

    
678

    
679
@command(server_cmds)
680
class server_stats(_init_cyclades, _optional_json):
681
    """Get virtual server statistics"""
682

    
683
    @errors.generic.all
684
    @errors.cyclades.connection
685
    @errors.cyclades.server_id
686
    def _run(self, server_id):
687
        self._print(
688
            self.client.get_server_stats(int(server_id)), self.print_dict)
689

    
690
    def main(self, server_id):
691
        super(self.__class__, self)._run()
692
        self._run(server_id=server_id)
693

    
694

    
695
@command(server_cmds)
696
class server_wait(_init_cyclades, _server_wait):
697
    """Wait for server to finish [BUILD, STOPPED, REBOOT, ACTIVE]"""
698

    
699
    @errors.generic.all
700
    @errors.cyclades.connection
701
    @errors.cyclades.server_id
702
    def _run(self, server_id, currect_status):
703
        self._wait(server_id, currect_status)
704

    
705
    def main(self, server_id, currect_status='BUILD'):
706
        super(self.__class__, self)._run()
707
        self._run(server_id=server_id, currect_status=currect_status)
708

    
709

    
710
@command(flavor_cmds)
711
class flavor_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
712
    """List available hardware flavors"""
713

    
714
    PERMANENTS = ('id', 'name')
715

    
716
    arguments = dict(
717
        detail=FlagArgument('show detailed output', ('-l', '--details')),
718
        limit=IntArgument('limit # of listed flavors', ('-n', '--number')),
719
        more=FlagArgument(
720
            'output results in pages (-n to set items per page, default 10)',
721
            '--more'),
722
        enum=FlagArgument('Enumerate results', '--enumerate'),
723
        ram=ValueArgument('filter by ram', ('--ram')),
724
        vcpus=ValueArgument('filter by number of VCPUs', ('--vcpus')),
725
        disk=ValueArgument('filter by disk size in GB', ('--disk')),
726
        disk_template=ValueArgument(
727
            'filter by disk_templace', ('--disk-template'))
728
    )
729

    
730
    def _apply_common_filters(self, flavors):
731
        common_filters = dict()
732
        if self['ram']:
733
            common_filters['ram'] = self['ram']
734
        if self['vcpus']:
735
            common_filters['vcpus'] = self['vcpus']
736
        if self['disk']:
737
            common_filters['disk'] = self['disk']
738
        if self['disk_template']:
739
            common_filters['SNF:disk_template'] = self['disk_template']
740
        return filter_dicts_by_dict(flavors, common_filters)
741

    
742
    @errors.generic.all
743
    @errors.cyclades.connection
744
    def _run(self):
745
        withcommons = self['ram'] or self['vcpus'] or (
746
            self['disk'] or self['disk_template'])
747
        detail = self['detail'] or withcommons
748
        flavors = self.client.list_flavors(detail)
749
        flavors = self._filter_by_name(flavors)
750
        flavors = self._filter_by_id(flavors)
751
        if withcommons:
752
            flavors = self._apply_common_filters(flavors)
753
        if not (self['detail'] or self['json_output']):
754
            remove_from_items(flavors, 'links')
755
        if detail and not self['detail']:
756
            for flv in flavors:
757
                for key in set(flv).difference(self.PERMANENTS):
758
                    flv.pop(key)
759
        kwargs = dict(out=StringIO(), title=()) if self['more'] else {}
760
        self._print(
761
            flavors,
762
            with_redundancy=self['detail'], with_enumeration=self['enum'],
763
            **kwargs)
764
        if self['more']:
765
            pager(kwargs['out'].getvalue())
766

    
767
    def main(self):
768
        super(self.__class__, self)._run()
769
        self._run()
770

    
771

    
772
@command(flavor_cmds)
773
class flavor_info(_init_cyclades, _optional_json):
774
    """Detailed information on a hardware flavor
775
    To get a list of available flavors and flavor ids, try /flavor list
776
    """
777

    
778
    @errors.generic.all
779
    @errors.cyclades.connection
780
    @errors.cyclades.flavor_id
781
    def _run(self, flavor_id):
782
        self._print(
783
            self.client.get_flavor_details(int(flavor_id)), self.print_dict)
784

    
785
    def main(self, flavor_id):
786
        super(self.__class__, self)._run()
787
        self._run(flavor_id=flavor_id)
788

    
789

    
790
def _add_name(self, net):
791
        user_id, tenant_id, uuids = net['user_id'], net['tenant_id'], []
792
        if user_id:
793
            uuids.append(user_id)
794
        if tenant_id:
795
            uuids.append(tenant_id)
796
        if uuids:
797
            usernames = self._uuids2usernames(uuids)
798
            if user_id:
799
                net['user_id'] += ' (%s)' % usernames[user_id]
800
            if tenant_id:
801
                net['tenant_id'] += ' (%s)' % usernames[tenant_id]
802

    
803

    
804
@command(network_cmds)
805
class network_info(_init_cyclades, _optional_json):
806
    """Detailed information on a network
807
    To get a list of available networks and network ids, try /network list
808
    """
809

    
810
    @errors.generic.all
811
    @errors.cyclades.connection
812
    @errors.cyclades.network_id
813
    def _run(self, network_id):
814
        network = self.client.get_network_details(int(network_id))
815
        _add_name(self, network)
816
        self._print(network, self.print_dict, exclude=('id'))
817

    
818
    def main(self, network_id):
819
        super(self.__class__, self)._run()
820
        self._run(network_id=network_id)
821

    
822

    
823
@command(network_cmds)
824
class network_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
825
    """List networks"""
826

    
827
    PERMANENTS = ('id', 'name')
828

    
829
    arguments = dict(
830
        detail=FlagArgument('show detailed output', ('-l', '--details')),
831
        limit=IntArgument('limit # of listed networks', ('-n', '--number')),
832
        more=FlagArgument(
833
            'output results in pages (-n to set items per page, default 10)',
834
            '--more'),
835
        enum=FlagArgument('Enumerate results', '--enumerate'),
836
        status=ValueArgument('filter by status', ('--status')),
837
        public=FlagArgument('only public networks', ('--public')),
838
        private=FlagArgument('only private networks', ('--private')),
839
        dhcp=FlagArgument('show networks with dhcp', ('--with-dhcp')),
840
        no_dhcp=FlagArgument('show networks without dhcp', ('--without-dhcp')),
841
        user_id=ValueArgument('filter by user id', ('--user-id')),
842
        user_name=ValueArgument('filter by user name', ('--user-name')),
843
        gateway=ValueArgument('filter by gateway (IPv4)', ('--gateway')),
844
        gateway6=ValueArgument('filter by gateway (IPv6)', ('--gateway6')),
845
        cidr=ValueArgument('filter by cidr (IPv4)', ('--cidr')),
846
        cidr6=ValueArgument('filter by cidr (IPv6)', ('--cidr6')),
847
        type=ValueArgument('filter by type', ('--type')),
848
    )
849

    
850
    def _apply_common_filters(self, networks):
851
        common_filter = dict()
852
        if self['public']:
853
            if self['private']:
854
                return []
855
            common_filter['public'] = self['public']
856
        elif self['private']:
857
            common_filter['public'] = False
858
        if self['dhcp']:
859
            if self['no_dhcp']:
860
                return []
861
            common_filter['dhcp'] = True
862
        elif self['no_dhcp']:
863
            common_filter['dhcp'] = False
864
        if self['user_id'] or self['user_name']:
865
            uuid = self['user_id'] or self._username2uuid(self['user_name'])
866
            common_filter['user_id'] = uuid
867
        for term in ('status', 'gateway', 'gateway6', 'cidr', 'cidr6', 'type'):
868
            if self[term]:
869
                common_filter[term] = self[term]
870
        return filter_dicts_by_dict(networks, common_filter)
871

    
872
    def _add_name(self, networks, key='user_id'):
873
        uuids = self._uuids2usernames(
874
            list(set([net[key] for net in networks])))
875
        for net in networks:
876
            v = net.get(key, None)
877
            if v:
878
                net[key] += ' (%s)' % uuids[v]
879
        return networks
880

    
881
    @errors.generic.all
882
    @errors.cyclades.connection
883
    def _run(self):
884
        withcommons = False
885
        for term in (
886
                'status', 'public', 'private', 'user_id', 'user_name', 'type',
887
                'gateway', 'gateway6', 'cidr', 'cidr6', 'dhcp', 'no_dhcp'):
888
            if self[term]:
889
                withcommons = True
890
                break
891
        detail = self['detail'] or withcommons
892
        networks = self.client.list_networks(detail)
893
        networks = self._filter_by_name(networks)
894
        networks = self._filter_by_id(networks)
895
        if withcommons:
896
            networks = self._apply_common_filters(networks)
897
        if not (self['detail'] or self['json_output']):
898
            remove_from_items(networks, 'links')
899
        if detail and not self['detail']:
900
            for net in networks:
901
                for key in set(net).difference(self.PERMANENTS):
902
                    net.pop(key)
903
        if self['detail'] and not self['json_output']:
904
            self._add_name(networks)
905
            self._add_name(networks, 'tenant_id')
906
        kwargs = dict(with_enumeration=self['enum'])
907
        if self['more']:
908
            kwargs['out'] = StringIO()
909
            kwargs['title'] = ()
910
        if self['limit']:
911
            networks = networks[:self['limit']]
912
        self._print(networks, **kwargs)
913
        if self['more']:
914
            pager(kwargs['out'].getvalue())
915

    
916
    def main(self):
917
        super(self.__class__, self)._run()
918
        self._run()
919

    
920

    
921
@command(network_cmds)
922
class network_create(_init_cyclades, _optional_json, _network_wait):
923
    """Create an (unconnected) network"""
924

    
925
    arguments = dict(
926
        cidr=ValueArgument('explicitly set cidr', '--with-cidr'),
927
        gateway=ValueArgument('explicitly set gateway', '--with-gateway'),
928
        dhcp=FlagArgument('Use dhcp (default: off)', '--with-dhcp'),
929
        type=ValueArgument(
930
            'Valid network types are '
931
            'CUSTOM, IP_LESS_ROUTED, MAC_FILTERED (default), PHYSICAL_VLAN',
932
            '--with-type',
933
            default='MAC_FILTERED'),
934
        wait=FlagArgument('Wait network to build', ('-w', '--wait'))
935
    )
936

    
937
    @errors.generic.all
938
    @errors.cyclades.connection
939
    @errors.cyclades.network_max
940
    def _run(self, name):
941
        r = self.client.create_network(
942
            name,
943
            cidr=self['cidr'],
944
            gateway=self['gateway'],
945
            dhcp=self['dhcp'],
946
            type=self['type'])
947
        _add_name(self, r)
948
        self._print(r, self.print_dict)
949
        if self['wait']:
950
            self._wait(r['id'], 'PENDING')
951

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

    
956

    
957
@command(network_cmds)
958
class network_rename(_init_cyclades, _optional_output_cmd):
959
    """Set the name of a network"""
960

    
961
    @errors.generic.all
962
    @errors.cyclades.connection
963
    @errors.cyclades.network_id
964
    def _run(self, network_id, new_name):
965
        self._optional_output(
966
                self.client.update_network_name(int(network_id), new_name))
967

    
968
    def main(self, network_id, new_name):
969
        super(self.__class__, self)._run()
970
        self._run(network_id=network_id, new_name=new_name)
971

    
972

    
973
@command(network_cmds)
974
class network_delete(_init_cyclades, _optional_output_cmd, _network_wait):
975
    """Delete a network"""
976

    
977
    arguments = dict(
978
        wait=FlagArgument('Wait network to build', ('-w', '--wait'))
979
    )
980

    
981
    @errors.generic.all
982
    @errors.cyclades.connection
983
    @errors.cyclades.network_id
984
    @errors.cyclades.network_in_use
985
    def _run(self, network_id):
986
        status = 'DELETED'
987
        if self['wait']:
988
            r = self.client.get_network_details(network_id)
989
            status = r['status']
990
            if status in ('DELETED', ):
991
                return
992

    
993
        r = self.client.delete_network(int(network_id))
994
        self._optional_output(r)
995

    
996
        if self['wait']:
997
            self._wait(network_id, status)
998

    
999
    def main(self, network_id):
1000
        super(self.__class__, self)._run()
1001
        self._run(network_id=network_id)
1002

    
1003

    
1004
@command(network_cmds)
1005
class network_connect(_init_cyclades, _optional_output_cmd):
1006
    """Connect a server to a network"""
1007

    
1008
    @errors.generic.all
1009
    @errors.cyclades.connection
1010
    @errors.cyclades.server_id
1011
    @errors.cyclades.network_id
1012
    def _run(self, server_id, network_id):
1013
        self._optional_output(
1014
                self.client.connect_server(int(server_id), int(network_id)))
1015

    
1016
    def main(self, server_id, network_id):
1017
        super(self.__class__, self)._run()
1018
        self._run(server_id=server_id, network_id=network_id)
1019

    
1020

    
1021
@command(network_cmds)
1022
class network_disconnect(_init_cyclades):
1023
    """Disconnect a nic that connects a server to a network
1024
    Nic ids are listed as "attachments" in detailed network information
1025
    To get detailed network information: /network info <network id>
1026
    """
1027

    
1028
    @errors.cyclades.nic_format
1029
    def _server_id_from_nic(self, nic_id):
1030
        return nic_id.split('-')[1]
1031

    
1032
    @errors.generic.all
1033
    @errors.cyclades.connection
1034
    @errors.cyclades.server_id
1035
    @errors.cyclades.nic_id
1036
    def _run(self, nic_id, server_id):
1037
        num_of_disconnected = self.client.disconnect_server(server_id, nic_id)
1038
        if not num_of_disconnected:
1039
            raise ClientError(
1040
                'Network Interface %s not found on server %s' % (
1041
                    nic_id, server_id),
1042
                status=404)
1043
        print('Disconnected %s connections' % num_of_disconnected)
1044

    
1045
    def main(self, nic_id):
1046
        super(self.__class__, self)._run()
1047
        server_id = self._server_id_from_nic(nic_id=nic_id)
1048
        self._run(nic_id=nic_id, server_id=server_id)
1049

    
1050

    
1051
@command(network_cmds)
1052
class network_wait(_init_cyclades, _network_wait):
1053
    """Wait for server to finish [PENDING, ACTIVE, DELETED]"""
1054

    
1055
    @errors.generic.all
1056
    @errors.cyclades.connection
1057
    @errors.cyclades.network_id
1058
    def _run(self, network_id, currect_status):
1059
        self._wait(network_id, currect_status)
1060

    
1061
    def main(self, network_id, currect_status='PENDING'):
1062
        super(self.__class__, self)._run()
1063
        self._run(network_id=network_id, currect_status=currect_status)
1064

    
1065

    
1066
@command(server_cmds)
1067
class server_ip(_init_cyclades):
1068
    """Manage floating IPs for the servers"""
1069

    
1070

    
1071
@command(server_cmds)
1072
class server_ip_pools(_init_cyclades, _optional_json):
1073
    """List all floating pools of floating ips"""
1074

    
1075
    @errors.generic.all
1076
    @errors.cyclades.connection
1077
    def _run(self):
1078
        r = self.client.get_floating_ip_pools()
1079
        self._print(r if self['json_output'] else r['floating_ip_pools'])
1080

    
1081
    def main(self):
1082
        super(self.__class__, self)._run()
1083
        self._run()
1084

    
1085

    
1086
@command(server_cmds)
1087
class server_ip_list(_init_cyclades, _optional_json):
1088
    """List all floating ips"""
1089

    
1090
    @errors.generic.all
1091
    @errors.cyclades.connection
1092
    def _run(self):
1093
        r = self.client.get_floating_ips()
1094
        self._print(r if self['json_output'] else r['floating_ips'])
1095

    
1096
    def main(self):
1097
        super(self.__class__, self)._run()
1098
        self._run()
1099

    
1100

    
1101
@command(server_cmds)
1102
class server_ip_info(_init_cyclades, _optional_json):
1103
    """A floating IPs' details"""
1104

    
1105
    @errors.generic.all
1106
    @errors.cyclades.connection
1107
    def _run(self, ip):
1108
        self._print(self.client.get_floating_ip(ip), self.print_dict)
1109

    
1110
    def main(self, ip):
1111
        super(self.__class__, self)._run()
1112
        self._run(ip=ip)
1113

    
1114

    
1115
@command(server_cmds)
1116
class server_ip_create(_init_cyclades, _optional_json):
1117
    """Create a new floating IP"""
1118

    
1119
    arguments = dict(pool=ValueArgument('Source IP pool', ('--pool'), None))
1120

    
1121
    @errors.generic.all
1122
    @errors.cyclades.connection
1123
    def _run(self, ip=None):
1124
        self._print([self.client.alloc_floating_ip(self['pool'], ip)])
1125

    
1126
    def main(self, requested_address=None):
1127
        super(self.__class__, self)._run()
1128
        self._run(ip=requested_address)
1129

    
1130

    
1131
@command(server_cmds)
1132
class server_ip_delete(_init_cyclades, _optional_output_cmd):
1133
    """Delete a floating ip"""
1134

    
1135
    @errors.generic.all
1136
    @errors.cyclades.connection
1137
    def _run(self, ip):
1138
        self._optional_output(self.client.delete_floating_ip(ip))
1139

    
1140
    def main(self, ip):
1141
        super(self.__class__, self)._run()
1142
        self._run(ip=ip)
1143

    
1144

    
1145
@command(server_cmds)
1146
class server_ip_attach(_init_cyclades, _optional_output_cmd):
1147
    """Attach a floating ip to a server with server_id
1148
    """
1149

    
1150
    @errors.generic.all
1151
    @errors.cyclades.connection
1152
    @errors.cyclades.server_id
1153
    def _run(self, server_id, ip):
1154
        self._optional_output(self.client.attach_floating_ip(server_id, ip))
1155

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

    
1160

    
1161
@command(server_cmds)
1162
class server_ip_detach(_init_cyclades, _optional_output_cmd):
1163
    """Detach floating IP from server
1164
    """
1165

    
1166
    @errors.generic.all
1167
    @errors.cyclades.connection
1168
    @errors.cyclades.server_id
1169
    def _run(self, server_id, ip):
1170
        self._optional_output(self.client.detach_floating_ip(server_id, ip))
1171

    
1172
    def main(self, server_id, ip):
1173
        super(self.__class__, self)._run()
1174
        self._run(server_id=server_id, ip=ip)