Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / cyclades_cli.py @ c314fc10

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',
48
    'Compute/Cyclades API server commands')
49
flavor_cmds = CommandTree('flavor',
50
    'Compute/Cyclades API flavor commands')
51
image_cmds = CommandTree('image',
52
    'Compute/Cyclades or Glance API image commands')
53
network_cmds = CommandTree('network',
54
    'Compute/Cyclades API network commands')
55
_commands = [server_cmds, flavor_cmds, image_cmds, network_cmds]
56

    
57

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

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

    
71

    
72
class _init_cyclades(_command_init):
73
    @errors.generic.all
74
    def _run(self, service='compute'):
75
        token = self.config.get(service, 'token')\
76
            or self.config.get('global', 'token')
77
        base_url = self.config.get(service, 'url')\
78
            or self.config.get('global', 'url')
79
        self.client = CycladesClient(base_url=base_url, token=token)
80

    
81
    def main(self):
82
        self._run()
83

    
84

    
85
@command(server_cmds)
86
class server_list(_init_cyclades):
87
    """List Virtual Machines accessible by user"""
88

    
89
    __doc__ += about_authentication
90

    
91
    arguments = dict(
92
        detail=FlagArgument('show detailed output', '-l'),
93
        since=DateArgument(
94
            'show only items since date (\' d/m/Y H:M:S \')',
95
            '--since'),
96
        limit=IntArgument('limit the number of VMs to list', '-n'),
97
        more=FlagArgument(
98
            'output results in pages (-n to set items per page, default 10)',
99
            '--more')
100
    )
101

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

    
117
    @errors.generic.all
118
    @errors.cyclades.connection
119
    @errors.cyclades.date
120
    def _run(self):
121
        servers = self.client.list_servers(self['detail'], self['since'])
122
        if self['detail']:
123
            self._make_results_pretty(servers)
124

    
125
        if self['more']:
126
            print_items(
127
                servers,
128
                page_size=self['limit'] if self['limit'] else 10)
129
        else:
130
            print_items(
131
                servers[:self['limit'] if self['limit'] else len(servers)])
132

    
133
    def main(self):
134
        super(self.__class__, self)._run()
135
        self._run()
136

    
137

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

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

    
164
    @errors.generic.all
165
    @errors.cyclades.connection
166
    @errors.cyclades.server_id
167
    def _run(self, server_id):
168
        server = self.client.get_server_details(server_id)
169
        self._print(server)
170

    
171
    def main(self, server_id):
172
        super(self.__class__, self)._run()
173
        self._run(server_id=server_id)
174

    
175

    
176
class PersonalityArgument(KeyValueArgument):
177
    @property
178
    def value(self):
179
        return self._value if hasattr(self, '_value') else []
180

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

    
209

    
210
@command(server_cmds)
211
class server_create(_init_cyclades):
212
    """Create a server (aka Virtual Machine)
213
    Parameters:
214
    - name: (single quoted text)
215
    - flavor id: Hardware flavor. Pick one from: /flavor list
216
    - image id: OS images. Pick one from: /image list
217
    """
218

    
219
    arguments = dict(
220
        personality=PersonalityArgument(
221
            ' /// '.join(howto_personality),
222
            '--personality')
223
    )
224

    
225
    @errors.generic.all
226
    @errors.cyclades.connection
227
    @errors.plankton.id
228
    @errors.cyclades.flavor_id
229
    def _run(self, name, flavor_id, image_id):
230
        r = self.client.create_server(
231
            name,
232
            int(flavor_id),
233
            image_id,
234
            self['personality'])
235
        print_dict(r)
236

    
237
    def main(self, name, flavor_id, image_id):
238
        super(self.__class__, self)._run()
239
        self._run(name=name, flavor_id=flavor_id, image_id=image_id)
240

    
241

    
242
@command(server_cmds)
243
class server_rename(_init_cyclades):
244
    """Set/update a server (VM) name
245
    VM names are not unique, therefore multiple servers may share the same name
246
    """
247

    
248
    @errors.generic.all
249
    @errors.cyclades.connection
250
    @errors.cyclades.server_id
251
    def _run(self, server_id, new_name):
252
        self.client.update_server_name(int(server_id), new_name)
253

    
254
    def main(self, server_id, new_name):
255
        super(self.__class__, self)._run()
256
        self._run(server_id=server_id, new_name=new_name)
257

    
258

    
259
@command(server_cmds)
260
class server_delete(_init_cyclades):
261
    """Delete a server (VM)"""
262

    
263
    @errors.generic.all
264
    @errors.cyclades.connection
265
    @errors.cyclades.server_id
266
    def _run(self, server_id):
267
            self.client.delete_server(int(server_id))
268

    
269
    def main(self, server_id):
270
        super(self.__class__, self)._run()
271
        self._run(server_id=server_id)
272

    
273

    
274
@command(server_cmds)
275
class server_reboot(_init_cyclades):
276
    """Reboot a server (VM)"""
277

    
278
    arguments = dict(
279
        hard=FlagArgument('perform a hard reboot', '-f')
280
    )
281

    
282
    @errors.generic.all
283
    @errors.cyclades.connection
284
    @errors.cyclades.server_id
285
    def _run(self, server_id):
286
        self.client.reboot_server(int(server_id), self['hard'])
287

    
288
    def main(self, server_id):
289
        super(self.__class__, self)._run()
290
        self._run(server_id=server_id)
291

    
292

    
293
@command(server_cmds)
294
class server_start(_init_cyclades):
295
    """Start an existing server (VM)"""
296

    
297
    @errors.generic.all
298
    @errors.cyclades.connection
299
    @errors.cyclades.server_id
300
    def _run(self, server_id):
301
        self.client.start_server(int(server_id))
302

    
303
    def main(self, server_id):
304
        super(self.__class__, self)._run()
305
        self._run(server_id=server_id)
306

    
307

    
308
@command(server_cmds)
309
class server_shutdown(_init_cyclades):
310
    """Shutdown an active server (VM)"""
311

    
312
    @errors.generic.all
313
    @errors.cyclades.connection
314
    @errors.cyclades.server_id
315
    def _run(self, server_id):
316
        self.client.shutdown_server(int(server_id))
317

    
318
    def main(self, server_id):
319
        super(self.__class__, self)._run()
320
        self._run(server_id=server_id)
321

    
322

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

    
332
    @errors.generic.all
333
    @errors.cyclades.connection
334
    @errors.cyclades.server_id
335
    def _run(self, server_id):
336
        r = self.client.get_server_console(int(server_id))
337
        print_dict(r)
338

    
339
    def main(self, server_id):
340
        super(self.__class__, self)._run()
341
        self._run(server_id=server_id)
342

    
343

    
344
@command(server_cmds)
345
class server_firewall(_init_cyclades):
346
    """Set the server (VM) firewall profile on VMs public network
347
    Values for profile:
348
    - DISABLED: Shutdown firewall
349
    - ENABLED: Firewall in normal mode
350
    - PROTECTED: Firewall in secure mode
351
    """
352

    
353
    @errors.generic.all
354
    @errors.cyclades.connection
355
    @errors.cyclades.server_id
356
    @errors.cyclades.firewall
357
    def _run(self, server_id, profile):
358
        self.client.set_firewall_profile(
359
            server_id=int(server_id),
360
            profile=unicode(profile).upper())
361

    
362
    def main(self, server_id, profile):
363
        super(self.__class__, self)._run()
364
        self._run(server_id=server_id, profile=profile)
365

    
366

    
367
@command(server_cmds)
368
class server_addr(_init_cyclades):
369
    """List the addresses of all network interfaces on a server (VM)"""
370

    
371
    @errors.generic.all
372
    @errors.cyclades.connection
373
    @errors.cyclades.server_id
374
    def _run(self, server_id):
375
        reply = self.client.list_server_nics(int(server_id))
376
        print_list(reply, with_enumeration=len(reply) > 1)
377

    
378
    def main(self, server_id):
379
        super(self.__class__, self)._run()
380
        self._run(server_id=server_id)
381

    
382

    
383
@command(server_cmds)
384
class server_meta(_init_cyclades):
385
    """Get a server's metadatum
386
    Metadata are formed as key:value pairs where key is used to retrieve them
387
    """
388

    
389
    @errors.generic.all
390
    @errors.cyclades.connection
391
    @errors.cyclades.server_id
392
    @errors.cyclades.metadata
393
    def _run(self, server_id, key=''):
394
        r = self.client.get_server_metadata(int(server_id), key)
395
        print_dict(r)
396

    
397
    def main(self, server_id, key=''):
398
        super(self.__class__, self)._run()
399
        self._run(server_id=server_id, key=key)
400

    
401

    
402
@command(server_cmds)
403
class server_setmeta(_init_cyclades):
404
    """set server (VM) metadata
405
    Metadata are formed as key:value pairs, both needed to set one
406
    """
407

    
408
    @errors.generic.all
409
    @errors.cyclades.connection
410
    @errors.cyclades.server_id
411
    def _run(self, server_id, key, val):
412
        metadata = {key: val}
413
        r = self.client.update_server_metadata(int(server_id), **metadata)
414
        print_dict(r)
415

    
416
    def main(self, server_id, key, val):
417
        super(self.__class__, self)._run()
418
        self._run(server_id=server_id, key=key, val=val)
419

    
420

    
421
@command(server_cmds)
422
class server_delmeta(_init_cyclades):
423
    """Delete server (VM) metadata"""
424

    
425
    @errors.generic.all
426
    @errors.cyclades.connection
427
    @errors.cyclades.server_id
428
    @errors.cyclades.metadata
429
    def _run(self, server_id, key):
430
        self.client.delete_server_metadata(int(server_id), key)
431

    
432
    def main(self, server_id, key):
433
        super(self.__class__, self)._run()
434
        self._run(server_id=server_id, key=key)
435

    
436

    
437
@command(server_cmds)
438
class server_stats(_init_cyclades):
439
    """Get server (VM) statistics"""
440

    
441
    @errors.generic.all
442
    @errors.cyclades.connection
443
    @errors.cyclades.server_id
444
    def _run(self, server_id):
445
        r = self.client.get_server_stats(int(server_id))
446
        print_dict(r, exclude=('serverRef',))
447

    
448
    def main(self, server_id):
449
        super(self.__class__, self)._run()
450
        self._run(server_id=server_id)
451

    
452

    
453
@command(server_cmds)
454
class server_wait(_init_cyclades):
455
    """Wait for server to finish [BUILD, STOPPED, REBOOT, ACTIVE]"""
456

    
457
    arguments = dict(
458
        progress_bar=ProgressBarArgument(
459
            'do not show progress bar',
460
            '--no-progress-bar',
461
            False
462
        )
463
    )
464

    
465
    @errors.generic.all
466
    @errors.cyclades.connection
467
    @errors.cyclades.server_id
468
    def _run(self, server_id, currect_status):
469
        (progress_bar, wait_cb) = self._safe_progress_bar(
470
            'Server %s still in %s mode' % (server_id, currect_status))
471

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

    
487
    def main(self, server_id, currect_status='BUILD'):
488
        super(self.__class__, self)._run()
489
        self._run(server_id=server_id, currect_status=currect_status)
490

    
491

    
492
@command(flavor_cmds)
493
class flavor_list(_init_cyclades):
494
    """List available hardware flavors"""
495

    
496
    arguments = dict(
497
        detail=FlagArgument('show detailed output', '-l'),
498
        limit=IntArgument('limit the number of flavors to list', '-n'),
499
        more=FlagArgument(
500
        'output results in pages (-n to set items per page, default 10)',
501
        '--more')
502
    )
503

    
504
    @errors.generic.all
505
    @errors.cyclades.connection
506
    def _run(self):
507
        flavors = self.client.list_flavors(self['detail'])
508
        pg_size = 10 if self['more'] and not self['limit'] else self['limit']
509
        print_items(flavors, with_redundancy=self['detail'], page_size=pg_size)
510

    
511
    def main(self):
512
        super(self.__class__, self)._run()
513
        self._run()
514

    
515

    
516
@command(flavor_cmds)
517
class flavor_info(_init_cyclades):
518
    """Detailed information on a hardware flavor
519
    To get a list of available flavors and flavor ids, try /flavor list
520
    """
521

    
522
    @errors.generic.all
523
    @errors.cyclades.connection
524
    @errors.cyclades.flavor_id
525
    def _run(self, flavor_id):
526
        flavor = self.client.get_flavor_details(int(flavor_id))
527
        print_dict(flavor)
528

    
529
    def main(self, flavor_id):
530
        super(self.__class__, self)._run()
531
        self._run(flavor_id=flavor_id)
532

    
533

    
534
@command(network_cmds)
535
class network_info(_init_cyclades):
536
    """Detailed information on a network
537
    To get a list of available networks and network ids, try /network list
538
    """
539

    
540
    @classmethod
541
    def _make_result_pretty(self, net):
542
        if 'attachments' in net:
543
            att = net['attachments']['values']
544
            count = len(att)
545
            net['attachments'] = att if count else None
546

    
547
    @errors.generic.all
548
    @errors.cyclades.connection
549
    @errors.cyclades.network_id
550
    def _run(self, network_id):
551
        network = self.client.get_network_details(int(network_id))
552
        self._make_result_pretty(network)
553
        print_dict(network, exclude=('id'))
554

    
555
    def main(self, network_id):
556
        super(self.__class__, self)._run()
557
        self._run(network_id=network_id)
558

    
559

    
560
@command(network_cmds)
561
class network_list(_init_cyclades):
562
    """List networks"""
563

    
564
    arguments = dict(
565
        detail=FlagArgument('show detailed output', '-l'),
566
        limit=IntArgument('limit the number of networks in list', '-n'),
567
        more=FlagArgument(
568
            'output results in pages (-n to set items per page, default 10)',
569
            '--more')
570
    )
571

    
572
    def _make_results_pretty(self, nets):
573
        for net in nets:
574
            network_info._make_result_pretty(net)
575

    
576
    @errors.generic.all
577
    @errors.cyclades.connection
578
    def _run(self):
579
        networks = self.client.list_networks(self['detail'])
580
        if self['detail']:
581
            self._make_results_pretty(networks)
582
        if self['more']:
583
            print_items(networks,
584
                page_size=self['limit'] if self['limit'] else 10)
585
        elif self['limit']:
586
            print_items(networks[:self['limit']])
587
        else:
588
            print_items(networks)
589

    
590
    def main(self):
591
        super(self.__class__, self)._run()
592
        self._run()
593

    
594

    
595
@command(network_cmds)
596
class network_create(_init_cyclades):
597
    """Create an (unconnected) network"""
598

    
599
    arguments = dict(
600
        cidr=ValueArgument('explicitly set cidr', '--with-cidr'),
601
        gateway=ValueArgument('explicitly set gateway', '--with-gateway'),
602
        dhcp=ValueArgument('explicitly set dhcp', '--with-dhcp'),
603
        type=ValueArgument('explicitly set type', '--with-type')
604
    )
605

    
606
    @errors.generic.all
607
    @errors.cyclades.connection
608
    @errors.cyclades.network_max
609
    def _run(self, name):
610
        r = self.client.create_network(name,
611
            cidr=self['cidr'],
612
            gateway=self['gateway'],
613
            dhcp=self['dhcp'],
614
            type=self['type'])
615
        print_items([r])
616

    
617
    def main(self, name):
618
        super(self.__class__, self)._run()
619
        self._run(name)
620

    
621

    
622
@command(network_cmds)
623
class network_rename(_init_cyclades):
624
    """Set the name of a network"""
625

    
626
    @errors.generic.all
627
    @errors.cyclades.connection
628
    @errors.cyclades.network_id
629
    def _run(self, network_id, new_name):
630
        self.client.update_network_name(int(network_id), new_name)
631

    
632
    def main(self, network_id, new_name):
633
        super(self.__class__, self)._run()
634
        self._run(network_id=network_id, new_name=new_name)
635

    
636

    
637
@command(network_cmds)
638
class network_delete(_init_cyclades):
639
    """Delete a network"""
640

    
641
    @errors.generic.all
642
    @errors.cyclades.connection
643
    @errors.cyclades.network_id
644
    @errors.cyclades.network_in_use
645
    def _run(self, network_id):
646
        self.client.delete_network(int(network_id))
647

    
648
    def main(self, network_id):
649
        super(self.__class__, self)._run()
650
        self._run(network_id=network_id)
651

    
652

    
653
@command(network_cmds)
654
class network_connect(_init_cyclades):
655
    """Connect a server to a network"""
656

    
657
    @errors.generic.all
658
    @errors.cyclades.connection
659
    @errors.cyclades.server_id
660
    @errors.cyclades.network_id
661
    def _run(self, server_id, network_id):
662
        self.client.connect_server(int(server_id), int(network_id))
663

    
664
    def main(self, server_id, network_id):
665
        super(self.__class__, self)._run()
666
        self._run(server_id=server_id, network_id=network_id)
667

    
668

    
669
@command(network_cmds)
670
class network_disconnect(_init_cyclades):
671
    """Disconnect a nic that connects a server to a network
672
    Nic ids are listed as "attachments" in detailed network information
673
    To get detailed network information: /network info <network id>
674
    """
675

    
676
    @errors.cyclades.nic_format
677
    def _server_id_from_nic(self, nic_id):
678
        return nic_id.split('-')[1]
679

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

    
692
    def main(self, nic_id):
693
        super(self.__class__, self)._run()
694
        server_id = self._server_id_from_nic(nic_id=nic_id)
695
        self._run(nic_id=nic_id, server_id=server_id)