Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / cyclades_cli.py @ 8741c407

History | View | Annotate | Download (21.6 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 kamaki.cli import command
35
from kamaki.cli.command_tree import CommandTree
36
from kamaki.cli.utils import print_dict, print_list, print_items
37
from kamaki.cli.errors import raiseCLIError, CLISyntaxError
38
from kamaki.clients.cyclades import CycladesClient, ClientError
39
from kamaki.cli.argument import FlagArgument, ValueArgument, KeyValueArgument
40
from kamaki.cli.argument import ProgressBarArgument, DateArgument, IntArgument
41
from kamaki.cli.commands import _command_init, errors
42

    
43
from base64 import b64encode
44
from os.path import exists
45

    
46

    
47
server_cmds = CommandTree('server', 'Compute/Cyclades API server commands')
48
flavor_cmds = CommandTree('flavor', 'Compute/Cyclades API flavor commands')
49
network_cmds = CommandTree('network', 'Compute/Cyclades API network commands')
50
_commands = [server_cmds, flavor_cmds, network_cmds]
51

    
52

    
53
about_authentication = '\nUser Authentication:\
54
    \n* to check authentication: /astakos authenticate\
55
    \n* to set authentication token: /config set token <token>'
56

    
57
howto_personality = [
58
    'Defines a file to be injected to VMs personality.',
59
    'Personality value syntax: PATH,[SERVER_PATH,[OWNER,[GROUP,[MODE]]]]',
60
    '  PATH: of local file to be injected',
61
    '  SERVER_PATH: destination location inside server Image',
62
    '  OWNER: user id of destination file owner',
63
    '  GROUP: group id or name to own destination file',
64
    '  MODEL: permition in octal (e.g. 0777 or o+rwx)']
65

    
66

    
67
class _init_cyclades(_command_init):
68
    @errors.generic.all
69
    def _run(self, service='compute'):
70
        token = self.config.get(service, 'token')\
71
            or self.config.get('global', 'token')
72
        base_url = self.config.get(service, 'url')\
73
            or self.config.get('global', 'url')
74
        self.client = CycladesClient(base_url=base_url, token=token)
75

    
76
    def main(self):
77
        self._run()
78

    
79

    
80
@command(server_cmds)
81
class server_list(_init_cyclades):
82
    """List Virtual Machines accessible by user"""
83

    
84
    __doc__ += about_authentication
85

    
86
    arguments = dict(
87
        detail=FlagArgument('show detailed output', ('-l', '--details')),
88
        since=DateArgument(
89
            'show only items since date (\' d/m/Y H:M:S \')',
90
            '--since'),
91
        limit=IntArgument('limit number of listed VMs', ('-n', '--number')),
92
        more=FlagArgument(
93
            'output results in pages (-n to set items per page, default 10)',
94
            '--more')
95
    )
96

    
97
    def _make_results_pretty(self, servers):
98
        for server in servers:
99
            addr_dict = {}
100
            if 'attachments' in server:
101
                for addr in server['attachments']['values']:
102
                    ips = addr.pop('values', [])
103
                    for ip in ips:
104
                        addr['IPv%s' % ip['version']] = ip['addr']
105
                    if 'firewallProfile' in addr:
106
                        addr['firewall'] = addr.pop('firewallProfile')
107
                    addr_dict[addr.pop('id')] = addr
108
                server['attachments'] = addr_dict if addr_dict else None
109
            if 'metadata' in server:
110
                server['metadata'] = server['metadata']['values']
111

    
112
    @errors.generic.all
113
    @errors.cyclades.connection
114
    @errors.cyclades.date
115
    def _run(self):
116
        servers = self.client.list_servers(self['detail'], self['since'])
117
        if self['detail']:
118
            self._make_results_pretty(servers)
119

    
120
        if self['more']:
121
            print_items(
122
                servers,
123
                page_size=self['limit'] if self['limit'] else 10)
124
        else:
125
            print_items(
126
                servers[:self['limit'] if self['limit'] else len(servers)])
127

    
128
    def main(self):
129
        super(self.__class__, self)._run()
130
        self._run()
131

    
132

    
133
@command(server_cmds)
134
class server_info(_init_cyclades):
135
    """Detailed information on a Virtual Machine
136
    Contains:
137
    - name, id, status, create/update dates
138
    - network interfaces
139
    - metadata (e.g. os, superuser) and diagnostics
140
    - hardware flavor and os image ids
141
    """
142

    
143
    def _print(self, server):
144
        addr_dict = {}
145
        if 'attachments' in server:
146
            atts = server.pop('attachments')
147
            for addr in atts['values']:
148
                ips = addr.pop('values', [])
149
                for ip in ips:
150
                    addr['IPv%s' % ip['version']] = ip['addr']
151
                if 'firewallProfile' in addr:
152
                    addr['firewall'] = addr.pop('firewallProfile')
153
                addr_dict[addr.pop('id')] = addr
154
            server['attachments'] = addr_dict if addr_dict else None
155
        if 'metadata' in server:
156
            server['metadata'] = server['metadata']['values']
157
        print_dict(server, ident=1)
158

    
159
    @errors.generic.all
160
    @errors.cyclades.connection
161
    @errors.cyclades.server_id
162
    def _run(self, server_id):
163
        server = self.client.get_server_details(server_id)
164
        self._print(server)
165

    
166
    def main(self, server_id):
167
        super(self.__class__, self)._run()
168
        self._run(server_id=server_id)
169

    
170

    
171
class PersonalityArgument(KeyValueArgument):
172
    @property
173
    def value(self):
174
        return self._value if hasattr(self, '_value') else []
175

    
176
    @value.setter
177
    def value(self, newvalue):
178
        if newvalue == self.default:
179
            return self.value
180
        self._value = []
181
        for i, terms in enumerate(newvalue):
182
            termlist = terms.split(',')
183
            if len(termlist) > 5:
184
                msg = 'Wrong number of terms (should be 1 to 5)'
185
                raiseCLIError(CLISyntaxError(msg), details=howto_personality)
186
            path = termlist[0]
187
            if not exists(path):
188
                raiseCLIError(
189
                    None,
190
                    '--personality: File %s does not exist' % path,
191
                    importance=1,
192
                    details=howto_personality)
193
            self._value.append(dict(path=path))
194
            with open(path) as f:
195
                self._value[i]['contents'] = b64encode(f.read())
196
            try:
197
                self._value[i]['path'] = termlist[1]
198
                self._value[i]['owner'] = termlist[2]
199
                self._value[i]['group'] = termlist[3]
200
                self._value[i]['mode'] = termlist[4]
201
            except IndexError:
202
                pass
203

    
204

    
205
@command(server_cmds)
206
class server_create(_init_cyclades):
207
    """Create a server (aka Virtual Machine)
208
    Parameters:
209
    - name: (single quoted text)
210
    - flavor id: Hardware flavor. Pick one from: /flavor list
211
    - image id: OS images. Pick one from: /image list
212
    """
213

    
214
    arguments = dict(
215
        personality=PersonalityArgument(
216
            ' /// '.join(howto_personality),
217
            ('-p', '--personality'))
218
    )
219

    
220
    @errors.generic.all
221
    @errors.cyclades.connection
222
    @errors.plankton.id
223
    @errors.cyclades.flavor_id
224
    def _run(self, name, flavor_id, image_id):
225
        r = self.client.create_server(
226
            name,
227
            int(flavor_id),
228
            image_id,
229
            self['personality'])
230
        print_dict(r)
231

    
232
    def main(self, name, flavor_id, image_id):
233
        super(self.__class__, self)._run()
234
        self._run(name=name, flavor_id=flavor_id, image_id=image_id)
235

    
236

    
237
@command(server_cmds)
238
class server_rename(_init_cyclades):
239
    """Set/update a server (VM) name
240
    VM names are not unique, therefore multiple servers may share the same name
241
    """
242

    
243
    @errors.generic.all
244
    @errors.cyclades.connection
245
    @errors.cyclades.server_id
246
    def _run(self, server_id, new_name):
247
        self.client.update_server_name(int(server_id), new_name)
248

    
249
    def main(self, server_id, new_name):
250
        super(self.__class__, self)._run()
251
        self._run(server_id=server_id, new_name=new_name)
252

    
253

    
254
@command(server_cmds)
255
class server_delete(_init_cyclades):
256
    """Delete a server (VM)"""
257

    
258
    @errors.generic.all
259
    @errors.cyclades.connection
260
    @errors.cyclades.server_id
261
    def _run(self, server_id):
262
            self.client.delete_server(int(server_id))
263

    
264
    def main(self, server_id):
265
        super(self.__class__, self)._run()
266
        self._run(server_id=server_id)
267

    
268

    
269
@command(server_cmds)
270
class server_reboot(_init_cyclades):
271
    """Reboot a server (VM)"""
272

    
273
    arguments = dict(
274
        hard=FlagArgument('perform a hard reboot', ('-f', '--force'))
275
    )
276

    
277
    @errors.generic.all
278
    @errors.cyclades.connection
279
    @errors.cyclades.server_id
280
    def _run(self, server_id):
281
        self.client.reboot_server(int(server_id), self['hard'])
282

    
283
    def main(self, server_id):
284
        super(self.__class__, self)._run()
285
        self._run(server_id=server_id)
286

    
287

    
288
@command(server_cmds)
289
class server_start(_init_cyclades):
290
    """Start an existing server (VM)"""
291

    
292
    @errors.generic.all
293
    @errors.cyclades.connection
294
    @errors.cyclades.server_id
295
    def _run(self, server_id):
296
        self.client.start_server(int(server_id))
297

    
298
    def main(self, server_id):
299
        super(self.__class__, self)._run()
300
        self._run(server_id=server_id)
301

    
302

    
303
@command(server_cmds)
304
class server_shutdown(_init_cyclades):
305
    """Shutdown an active server (VM)"""
306

    
307
    @errors.generic.all
308
    @errors.cyclades.connection
309
    @errors.cyclades.server_id
310
    def _run(self, server_id):
311
        self.client.shutdown_server(int(server_id))
312

    
313
    def main(self, server_id):
314
        super(self.__class__, self)._run()
315
        self._run(server_id=server_id)
316

    
317

    
318
@command(server_cmds)
319
class server_console(_init_cyclades):
320
    """Get a VNC console to access an existing server (VM)
321
    Console connection information provided (at least):
322
    - host: (url or address) a VNC host
323
    - port: (int) the gateway to enter VM on host
324
    - password: for VNC authorization
325
    """
326

    
327
    @errors.generic.all
328
    @errors.cyclades.connection
329
    @errors.cyclades.server_id
330
    def _run(self, server_id):
331
        r = self.client.get_server_console(int(server_id))
332
        print_dict(r)
333

    
334
    def main(self, server_id):
335
        super(self.__class__, self)._run()
336
        self._run(server_id=server_id)
337

    
338

    
339
@command(server_cmds)
340
class server_firewall(_init_cyclades):
341
    """Set the server (VM) firewall profile on VMs public network
342
    Values for profile:
343
    - DISABLED: Shutdown firewall
344
    - ENABLED: Firewall in normal mode
345
    - PROTECTED: Firewall in secure mode
346
    """
347

    
348
    @errors.generic.all
349
    @errors.cyclades.connection
350
    @errors.cyclades.server_id
351
    @errors.cyclades.firewall
352
    def _run(self, server_id, profile):
353
        self.client.set_firewall_profile(
354
            server_id=int(server_id),
355
            profile=('%s' % profile).upper())
356

    
357
    def main(self, server_id, profile):
358
        super(self.__class__, self)._run()
359
        self._run(server_id=server_id, profile=profile)
360

    
361

    
362
@command(server_cmds)
363
class server_addr(_init_cyclades):
364
    """List the addresses of all network interfaces on a server (VM)"""
365

    
366
    @errors.generic.all
367
    @errors.cyclades.connection
368
    @errors.cyclades.server_id
369
    def _run(self, server_id):
370
        reply = self.client.list_server_nics(int(server_id))
371
        print_list(reply, with_enumeration=len(reply) > 1)
372

    
373
    def main(self, server_id):
374
        super(self.__class__, self)._run()
375
        self._run(server_id=server_id)
376

    
377

    
378
@command(server_cmds)
379
class server_meta(_init_cyclades):
380
    """Get a server's metadatum
381
    Metadata are formed as key:value pairs where key is used to retrieve them
382
    """
383

    
384
    @errors.generic.all
385
    @errors.cyclades.connection
386
    @errors.cyclades.server_id
387
    @errors.cyclades.metadata
388
    def _run(self, server_id, key=''):
389
        r = self.client.get_server_metadata(int(server_id), key)
390
        print_dict(r)
391

    
392
    def main(self, server_id, key=''):
393
        super(self.__class__, self)._run()
394
        self._run(server_id=server_id, key=key)
395

    
396

    
397
@command(server_cmds)
398
class server_setmeta(_init_cyclades):
399
    """set server (VM) metadata
400
    Metadata are formed as key:value pairs, both needed to set one
401
    """
402

    
403
    @errors.generic.all
404
    @errors.cyclades.connection
405
    @errors.cyclades.server_id
406
    def _run(self, server_id, key, val):
407
        metadata = {key: val}
408
        r = self.client.update_server_metadata(int(server_id), **metadata)
409
        print_dict(r)
410

    
411
    def main(self, server_id, key, val):
412
        super(self.__class__, self)._run()
413
        self._run(server_id=server_id, key=key, val=val)
414

    
415

    
416
@command(server_cmds)
417
class server_delmeta(_init_cyclades):
418
    """Delete server (VM) metadata"""
419

    
420
    @errors.generic.all
421
    @errors.cyclades.connection
422
    @errors.cyclades.server_id
423
    @errors.cyclades.metadata
424
    def _run(self, server_id, key):
425
        self.client.delete_server_metadata(int(server_id), key)
426

    
427
    def main(self, server_id, key):
428
        super(self.__class__, self)._run()
429
        self._run(server_id=server_id, key=key)
430

    
431

    
432
@command(server_cmds)
433
class server_stats(_init_cyclades):
434
    """Get server (VM) statistics"""
435

    
436
    @errors.generic.all
437
    @errors.cyclades.connection
438
    @errors.cyclades.server_id
439
    def _run(self, server_id):
440
        r = self.client.get_server_stats(int(server_id))
441
        print_dict(r, exclude=('serverRef',))
442

    
443
    def main(self, server_id):
444
        super(self.__class__, self)._run()
445
        self._run(server_id=server_id)
446

    
447

    
448
@command(server_cmds)
449
class server_wait(_init_cyclades):
450
    """Wait for server to finish [BUILD, STOPPED, REBOOT, ACTIVE]"""
451

    
452
    arguments = dict(
453
        progress_bar=ProgressBarArgument(
454
            'do not show progress bar',
455
            ('-N', '--no-progress-bar'),
456
            False
457
        )
458
    )
459

    
460
    @errors.generic.all
461
    @errors.cyclades.connection
462
    @errors.cyclades.server_id
463
    def _run(self, server_id, currect_status):
464
        (progress_bar, wait_cb) = self._safe_progress_bar(
465
            'Server %s still in %s mode' % (server_id, currect_status))
466

    
467
        try:
468
            new_mode = self.client.wait_server(
469
                server_id,
470
                currect_status,
471
                wait_cb=wait_cb)
472
        except Exception:
473
            self._safe_progress_bar_finish(progress_bar)
474
            raise
475
        finally:
476
            self._safe_progress_bar_finish(progress_bar)
477
        if new_mode:
478
            print('Server %s is now in %s mode' % (server_id, new_mode))
479
        else:
480
            raiseCLIError(None, 'Time out')
481

    
482
    def main(self, server_id, currect_status='BUILD'):
483
        super(self.__class__, self)._run()
484
        self._run(server_id=server_id, currect_status=currect_status)
485

    
486

    
487
@command(flavor_cmds)
488
class flavor_list(_init_cyclades):
489
    """List available hardware flavors"""
490

    
491
    arguments = dict(
492
        detail=FlagArgument('show detailed output', ('-l', '--details')),
493
        limit=IntArgument('limit # of listed flavors', ('-n', '--number')),
494
        more=FlagArgument(
495
            'output results in pages (-n to set items per page, default 10)',
496
            '--more')
497
    )
498

    
499
    @errors.generic.all
500
    @errors.cyclades.connection
501
    def _run(self):
502
        flavors = self.client.list_flavors(self['detail'])
503
        pg_size = 10 if self['more'] and not self['limit'] else self['limit']
504
        print_items(flavors, with_redundancy=self['detail'], page_size=pg_size)
505

    
506
    def main(self):
507
        super(self.__class__, self)._run()
508
        self._run()
509

    
510

    
511
@command(flavor_cmds)
512
class flavor_info(_init_cyclades):
513
    """Detailed information on a hardware flavor
514
    To get a list of available flavors and flavor ids, try /flavor list
515
    """
516

    
517
    @errors.generic.all
518
    @errors.cyclades.connection
519
    @errors.cyclades.flavor_id
520
    def _run(self, flavor_id):
521
        flavor = self.client.get_flavor_details(int(flavor_id))
522
        print_dict(flavor)
523

    
524
    def main(self, flavor_id):
525
        super(self.__class__, self)._run()
526
        self._run(flavor_id=flavor_id)
527

    
528

    
529
@command(network_cmds)
530
class network_info(_init_cyclades):
531
    """Detailed information on a network
532
    To get a list of available networks and network ids, try /network list
533
    """
534

    
535
    @classmethod
536
    def _make_result_pretty(self, net):
537
        if 'attachments' in net:
538
            att = net['attachments']['values']
539
            count = len(att)
540
            net['attachments'] = att if count else None
541

    
542
    @errors.generic.all
543
    @errors.cyclades.connection
544
    @errors.cyclades.network_id
545
    def _run(self, network_id):
546
        network = self.client.get_network_details(int(network_id))
547
        self._make_result_pretty(network)
548
        print_dict(network, exclude=('id'))
549

    
550
    def main(self, network_id):
551
        super(self.__class__, self)._run()
552
        self._run(network_id=network_id)
553

    
554

    
555
@command(network_cmds)
556
class network_list(_init_cyclades):
557
    """List networks"""
558

    
559
    arguments = dict(
560
        detail=FlagArgument('show detailed output', ('-l', '--details')),
561
        limit=IntArgument('limit # of listed networks', ('-n', '--number')),
562
        more=FlagArgument(
563
            'output results in pages (-n to set items per page, default 10)',
564
            '--more')
565
    )
566

    
567
    def _make_results_pretty(self, nets):
568
        for net in nets:
569
            network_info._make_result_pretty(net)
570

    
571
    @errors.generic.all
572
    @errors.cyclades.connection
573
    def _run(self):
574
        networks = self.client.list_networks(self['detail'])
575
        if self['detail']:
576
            self._make_results_pretty(networks)
577
        if self['more']:
578
            print_items(networks, page_size=self['limit'] or 10)
579
        elif self['limit']:
580
            print_items(networks[:self['limit']])
581
        else:
582
            print_items(networks)
583

    
584
    def main(self):
585
        super(self.__class__, self)._run()
586
        self._run()
587

    
588

    
589
@command(network_cmds)
590
class network_create(_init_cyclades):
591
    """Create an (unconnected) network"""
592

    
593
    arguments = dict(
594
        cidr=ValueArgument('explicitly set cidr', '--with-cidr'),
595
        gateway=ValueArgument('explicitly set gateway', '--with-gateway'),
596
        dhcp=FlagArgument('Use dhcp (default: off)', '--with-dhcp'),
597
        type=ValueArgument('explicitly set type', '--with-type')
598
    )
599

    
600
    @errors.generic.all
601
    @errors.cyclades.connection
602
    @errors.cyclades.network_max
603
    def _run(self, name):
604
        r = self.client.create_network(
605
            name,
606
            cidr=self['cidr'],
607
            gateway=self['gateway'],
608
            dhcp=self['dhcp'],
609
            type=self['type'])
610
        print_items([r])
611

    
612
    def main(self, name):
613
        super(self.__class__, self)._run()
614
        self._run(name)
615

    
616

    
617
@command(network_cmds)
618
class network_rename(_init_cyclades):
619
    """Set the name of a network"""
620

    
621
    @errors.generic.all
622
    @errors.cyclades.connection
623
    @errors.cyclades.network_id
624
    def _run(self, network_id, new_name):
625
        self.client.update_network_name(int(network_id), new_name)
626

    
627
    def main(self, network_id, new_name):
628
        super(self.__class__, self)._run()
629
        self._run(network_id=network_id, new_name=new_name)
630

    
631

    
632
@command(network_cmds)
633
class network_delete(_init_cyclades):
634
    """Delete a network"""
635

    
636
    @errors.generic.all
637
    @errors.cyclades.connection
638
    @errors.cyclades.network_id
639
    @errors.cyclades.network_in_use
640
    def _run(self, network_id):
641
        self.client.delete_network(int(network_id))
642

    
643
    def main(self, network_id):
644
        super(self.__class__, self)._run()
645
        self._run(network_id=network_id)
646

    
647

    
648
@command(network_cmds)
649
class network_connect(_init_cyclades):
650
    """Connect a server to a network"""
651

    
652
    @errors.generic.all
653
    @errors.cyclades.connection
654
    @errors.cyclades.server_id
655
    @errors.cyclades.network_id
656
    def _run(self, server_id, network_id):
657
        self.client.connect_server(int(server_id), int(network_id))
658

    
659
    def main(self, server_id, network_id):
660
        super(self.__class__, self)._run()
661
        self._run(server_id=server_id, network_id=network_id)
662

    
663

    
664
@command(network_cmds)
665
class network_disconnect(_init_cyclades):
666
    """Disconnect a nic that connects a server to a network
667
    Nic ids are listed as "attachments" in detailed network information
668
    To get detailed network information: /network info <network id>
669
    """
670

    
671
    @errors.cyclades.nic_format
672
    def _server_id_from_nic(self, nic_id):
673
        return nic_id.split('-')[1]
674

    
675
    @errors.generic.all
676
    @errors.cyclades.connection
677
    @errors.cyclades.server_id
678
    @errors.cyclades.nic_id
679
    def _run(self, nic_id, server_id):
680
        if not self.client.disconnect_server(server_id, nic_id):
681
            raise ClientError(
682
                'Network Interface %s not found on server %s' % (
683
                    nic_id,
684
                    server_id),
685
                status=404)
686

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