Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / cyclades_cli.py @ 5a673575

History | View | Annotate | Download (22.3 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 = '\n  User 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
def raise_if_connection_error(err, base_url='compute.url'):
73
    if err.status == 401:
74
        raiseCLIError(err, 'Authorization failed', details=[
75
            'Make sure a valid token is provided:',
76
            '  to check if the token is valid: /astakos authenticate',
77
            '  to set a token: /config set [.server.]token <token>',
78
            '  to get current token: /config get [server.]token'])
79
    elif err.status in range(-12, 200) + [403, 500]:
80
        raiseCLIError(err, details=[
81
            'Check if service is up or set to %s' % base_url,
82
            '  to get service url: /config get %s' % base_url,
83
            '  to set service url: /config set %s <URL>' % base_url]
84
        )
85

    
86

    
87
class _init_cyclades(_command_init):
88
    @errors.generic.all
89
    def _run(self, service='compute'):
90
        token = self.config.get(service, 'token')\
91
            or self.config.get('global', 'token')
92
        base_url = self.config.get(service, 'url')\
93
            or self.config.get('global', 'url')
94
        self.client = CycladesClient(base_url=base_url, token=token)
95

    
96
    def main(self):
97
        self._run()
98

    
99

    
100
@command(server_cmds)
101
class server_list(_init_cyclades):
102
    """List Virtual Machines accessible by user
103
    """
104

    
105
    __doc__ += about_authentication
106

    
107
    arguments = dict(
108
        detail=FlagArgument('show detailed output', '-l'),
109
        since=DateArgument(
110
            'show only items since date (\' d/m/Y H:M:S \')',
111
            '--since'),
112
        limit=IntArgument('limit the number of VMs to list', '-n'),
113
        more=FlagArgument(
114
            'output results in pages (-n to set items per page, default 10)',
115
            '--more')
116
    )
117

    
118
    def _make_results_pretty(self, servers):
119
        for server in servers:
120
            addr_dict = {}
121
            if 'attachments' in server:
122
                for addr in server['attachments']['values']:
123
                    ips = addr.pop('values', [])
124
                    for ip in ips:
125
                        addr['IPv%s' % ip['version']] = ip['addr']
126
                    if 'firewallProfile' in addr:
127
                        addr['firewall'] = addr.pop('firewallProfile')
128
                    addr_dict[addr.pop('id')] = addr
129
                server['attachments'] = addr_dict if addr_dict else None
130
            if 'metadata' in server:
131
                server['metadata'] = server['metadata']['values']
132

    
133
    @errors.generic.all
134
    @errors.cyclades.connection
135
    @errors.cyclades.date
136
    def _run(self):
137
        servers = self.client.list_servers(self['detail'], self['since'])
138
        if self['detail']:
139
            self._make_results_pretty(servers)
140

    
141
        if self['more']:
142
            print_items(
143
                servers,
144
                page_size=self['limit'] if self['limit'] else 10)
145
        else:
146
            print_items(
147
                servers[:self['limit'] if self['limit'] else len(servers)])
148

    
149
    def main(self):
150
        super(self.__class__, self)._run()
151
        self._run()
152

    
153

    
154
@command(server_cmds)
155
class server_info(_init_cyclades):
156
    """Detailed information on a Virtual Machine
157
    Contains:
158
    - name, id, status, create/update dates
159
    - network interfaces
160
    - metadata (e.g. os, superuser) and diagnostics
161
    - hardware flavor and os image ids
162
    """
163

    
164
    def _print(self, server):
165
        addr_dict = {}
166
        if 'attachments' in server:
167
            atts = server.pop('attachments')
168
            for addr in atts['values']:
169
                ips = addr.pop('values', [])
170
                for ip in ips:
171
                    addr['IPv%s' % ip['version']] = ip['addr']
172
                if 'firewallProfile' in addr:
173
                    addr['firewall'] = addr.pop('firewallProfile')
174
                addr_dict[addr.pop('id')] = addr
175
            server['attachments'] = addr_dict if addr_dict else None
176
        if 'metadata' in server:
177
            server['metadata'] = server['metadata']['values']
178
        print_dict(server, ident=1)
179

    
180
    @errors.generic.all
181
    @errors.cyclades.connection
182
    @errors.cyclades.server_id
183
    def _run(self, server_id):
184
        server = self.client.get_server_details(server_id)
185
        self._print(server)
186

    
187
    def main(self, server_id):
188
        super(self.__class__, self)._run()
189
        self._run(server_id=server_id)
190

    
191

    
192
class PersonalityArgument(KeyValueArgument):
193
    @property
194
    def value(self):
195
        return self._value if hasattr(self, '_value') else []
196

    
197
    @value.setter
198
    def value(self, newvalue):
199
        if newvalue == self.default:
200
            return self.value
201
        self._value = []
202
        for i, terms in enumerate(newvalue):
203
            termlist = terms.split(',')
204
            if len(termlist) > 5:
205
                raiseCLIError(
206
                CLISyntaxError('Wrong number of terms (should be 1 to 5)'),
207
                details=howto_personality)
208
            path = termlist[0]
209
            if not exists(path):
210
                raiseCLIError(None,
211
                    '--personality: File %s does not exist' % path,
212
                    importance=1,
213
                    details=howto_personality)
214
            self._value.append(dict(path=path))
215
            with open(path) as f:
216
                self._value[i]['contents'] = b64encode(f.read())
217
            try:
218
                self._value[i]['path'] = termlist[1]
219
                self._value[i]['owner'] = termlist[2]
220
                self._value[i]['group'] = termlist[3]
221
                self._value[i]['mode'] = termlist[4]
222
            except IndexError:
223
                pass
224

    
225

    
226
@command(server_cmds)
227
class server_create(_init_cyclades):
228
    """Create a server (aka Virtual Machine)
229
    Parameters:
230
    - name: (single quoted text)
231
    - flavor id: Hardware flavor. Pick one from: /flavor list
232
    - image id: OS images. Pick one from: /image list
233
    """
234

    
235
    arguments = dict(
236
        personality=PersonalityArgument(
237
            ' /// '.join(howto_personality),
238
            '--personality')
239
    )
240

    
241
    @errors.generic.all
242
    @errors.cyclades.connection
243
    @errors.plankton.id
244
    @errors.cyclades.flavor_id
245
    def _run(self, name, flavor_id, image_id):
246
        r = self.client.create_server(
247
            name,
248
            int(flavor_id),
249
            image_id,
250
            self['personality'])
251
        print_dict(r)
252

    
253
    def main(self, name, flavor_id, image_id):
254
        super(self.__class__, self)._run()
255
        self._run(name=name, flavor_id=flavor_id, image_id=image_id)
256

    
257

    
258
@command(server_cmds)
259
class server_rename(_init_cyclades):
260
    """Set/update a server (VM) name
261
    VM names are not unique, therefore multiple servers may share the same name
262
    """
263

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

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

    
274

    
275
@command(server_cmds)
276
class server_delete(_init_cyclades):
277
    """Delete a server (VM)"""
278

    
279
    @errors.generic.all
280
    @errors.cyclades.connection
281
    @errors.cyclades.server_id
282
    def _run(self, server_id):
283
            self.client.delete_server(int(server_id))
284

    
285
    def main(self, server_id):
286
        super(self.__class__, self)._run()
287
        self._run(server_id=server_id)
288

    
289

    
290
@command(server_cmds)
291
class server_reboot(_init_cyclades):
292
    """Reboot a server (VM)"""
293

    
294
    arguments = dict(
295
        hard=FlagArgument('perform a hard reboot', '-f')
296
    )
297

    
298
    @errors.generic.all
299
    @errors.cyclades.connection
300
    @errors.cyclades.server_id
301
    def _run(self, server_id):
302
        self.client.reboot_server(int(server_id), self['hard'])
303

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

    
308

    
309
@command(server_cmds)
310
class server_start(_init_cyclades):
311
    """Start an existing server (VM)"""
312

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

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

    
323

    
324
@command(server_cmds)
325
class server_shutdown(_init_cyclades):
326
    """Shutdown an active server (VM)"""
327

    
328
    @errors.generic.all
329
    @errors.cyclades.connection
330
    @errors.cyclades.server_id
331
    def _run(self, server_id):
332
        self.client.shutdown_server(int(server_id))
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_console(_init_cyclades):
341
    """Get a VNC console to access an existing server (VM)
342
    Console connection information provided (at least):
343
    - host: (url or address) a VNC host
344
    - port: (int) the gateway to enter VM on host
345
    - password: for VNC authorization
346
    """
347

    
348
    @errors.generic.all
349
    @errors.cyclades.connection
350
    @errors.cyclades.server_id
351
    def _run(self, server_id):
352
        r = self.client.get_server_console(int(server_id))
353
        print_dict(r)
354

    
355
    def main(self, server_id):
356
        super(self.__class__, self)._run()
357
        self._run(server_id=server_id)
358

    
359

    
360
@command(server_cmds)
361
class server_firewall(_init_cyclades):
362
    """Set the server (VM) firewall profile on VMs public network
363
    Values for profile:
364
    - DISABLED: Shutdown firewall
365
    - ENABLED: Firewall in normal mode
366
    - PROTECTED: Firewall in secure mode
367
    """
368

    
369
    @errors.generic.all
370
    @errors.cyclades.connection
371
    @errors.cyclades.server_id
372
    @errors.cyclades.firewall
373
    def _run(self, server_id, profile):
374
        self.client.set_firewall_profile(
375
            server_id=int(server_id),
376
            profile=unicode(profile).upper())
377

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

    
382

    
383
@command(server_cmds)
384
class server_addr(_init_cyclades):
385
    """List the addresses of all network interfaces on a server (VM)"""
386

    
387
    @errors.generic.all
388
    @errors.cyclades.connection
389
    @errors.cyclades.server_id
390
    def _run(self, server_id):
391
        reply = self.client.list_server_nics(int(server_id))
392
        print_list(reply, with_enumeration=len(reply) > 1)
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_meta(_init_cyclades):
401
    """Get a server's metadatum
402
    Metadata are formed as key:value pairs where key is used to retrieve them
403
    """
404

    
405
    @errors.generic.all
406
    @errors.cyclades.connection
407
    @errors.cyclades.server_id
408
    @errors.cyclades.metadata
409
    def _run(self, server_id, key=''):
410
        r = self.client.get_server_metadata(int(server_id), key)
411
        print_dict(r)
412

    
413
    def main(self, server_id, key=''):
414
        super(self.__class__, self)._run()
415
        self._run(server_id=server_id, key=key)
416

    
417

    
418
@command(server_cmds)
419
class server_setmeta(_init_cyclades):
420
    """set server (VM) metadata
421
    Metadata are formed as key:value pairs, both needed to set one
422
    """
423

    
424
    @errors.generic.all
425
    @errors.cyclades.connection
426
    @errors.cyclades.server_id
427
    def _run(self, server_id, key, val):
428
        metadata = {key: val}
429
        r = self.client.update_server_metadata(int(server_id), **metadata)
430
        print_dict(r)
431

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

    
436

    
437
@command(server_cmds)
438
class server_delmeta(_init_cyclades):
439
    """Delete server (VM) metadata"""
440

    
441
    @errors.generic.all
442
    @errors.cyclades.connection
443
    @errors.cyclades.server_id
444
    @errors.cyclades.metadata
445
    def _run(self, server_id, key):
446
        self.client.delete_server_metadata(int(server_id), key)
447

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

    
452

    
453
@command(server_cmds)
454
class server_stats(_init_cyclades):
455
    """Get server (VM) statistics"""
456

    
457
    @errors.generic.all
458
    @errors.cyclades.connection
459
    @errors.cyclades.server_id
460
    def _run(self, server_id):
461
        r = self.client.get_server_stats(int(server_id))
462
        print_dict(r, exclude=('serverRef',))
463

    
464
    def main(self, server_id):
465
        super(self.__class__, self)._run()
466
        self._run(server_id=server_id)
467

    
468

    
469
@command(server_cmds)
470
class server_wait(_init_cyclades):
471
    """Wait for server to finish [BUILD, STOPPED, REBOOT, ACTIVE]"""
472

    
473
    arguments = dict(
474
        progress_bar=ProgressBarArgument(
475
            'do not show progress bar',
476
            '--no-progress-bar',
477
            False
478
        )
479
    )
480

    
481
    @errors.generic.all
482
    @errors.cyclades.connection
483
    @errors.cyclades.server_id
484
    def _run(self, server_id, currect_status):
485
        (progress_bar, wait_cb) = self._safe_progress_bar(
486
            'Server %s still in %s mode' % (server_id, currect_status))
487

    
488
        try:
489
            new_mode = self.client.wait_server(
490
                server_id,
491
                currect_status,
492
                wait_cb=wait_cb)
493
        except Exception:
494
            self._safe_progress_bar_finish(progress_bar)
495
            raise
496
        finally:
497
            self._safe_progress_bar_finish(progress_bar)
498
        if new_mode:
499
            print('Server %s is now in %s mode' % (server_id, new_mode))
500
        else:
501
            raiseCLIError(None, 'Time out')
502

    
503
    def main(self, server_id, currect_status='BUILD'):
504
        super(self.__class__, self)._run()
505
        self._run(server_id=server_id, currect_status=currect_status)
506

    
507

    
508
@command(flavor_cmds)
509
class flavor_list(_init_cyclades):
510
    """List available hardware flavors"""
511

    
512
    arguments = dict(
513
        detail=FlagArgument('show detailed output', '-l'),
514
        limit=IntArgument('limit the number of flavors to list', '-n'),
515
        more=FlagArgument(
516
        'output results in pages (-n to set items per page, default 10)',
517
        '--more')
518
    )
519

    
520
    @errors.generic.all
521
    @errors.cyclades.connection
522
    def _run(self):
523
        flavors = self.client.list_flavors(self['detail'])
524
        pg_size = 10 if self['more'] and not self['limit'] else self['limit']
525
        print_items(flavors, with_redundancy=self['detail'], page_size=pg_size)
526

    
527
    def main(self):
528
        super(self.__class__, self)._run()
529
        self._run()
530

    
531

    
532
@command(flavor_cmds)
533
class flavor_info(_init_cyclades):
534
    """Detailed information on a hardware flavor
535
    To get a list of available flavors and flavor ids, try /flavor list
536
    """
537

    
538
    @errors.generic.all
539
    @errors.cyclades.connection
540
    @errors.cyclades.flavor_id
541
    def _run(self, flavor_id):
542
        flavor = self.client.get_flavor_details(int(flavor_id))
543
        print_dict(flavor)
544

    
545
    def main(self, flavor_id):
546
        super(self.__class__, self)._run()
547
        self._run(flavor_id=flavor_id)
548

    
549

    
550
@command(network_cmds)
551
class network_info(_init_cyclades):
552
    """Detailed information on a network
553
    To get a list of available networks and network ids, try /network list
554
    """
555

    
556
    @classmethod
557
    def _make_result_pretty(self, net):
558
        if 'attachments' in net:
559
            att = net['attachments']['values']
560
            count = len(att)
561
            net['attachments'] = att if count else None
562

    
563
    @errors.generic.all
564
    @errors.cyclades.connection
565
    @errors.cyclades.network_id
566
    def _run(self, network_id):
567
        network = self.client.get_network_details(int(network_id))
568
        self._make_result_pretty(network)
569
        print_dict(network, exclude=('id'))
570

    
571
    def main(self, network_id):
572
        super(self.__class__, self)._run()
573
        self._run(network_id=network_id)
574

    
575

    
576
@command(network_cmds)
577
class network_list(_init_cyclades):
578
    """List networks"""
579

    
580
    arguments = dict(
581
        detail=FlagArgument('show detailed output', '-l'),
582
        limit=IntArgument('limit the number of networks in list', '-n'),
583
        more=FlagArgument(
584
            'output results in pages (-n to set items per page, default 10)',
585
            '--more')
586
    )
587

    
588
    def _make_results_pretty(self, nets):
589
        for net in nets:
590
            network_info._make_result_pretty(net)
591

    
592
    @errors.generic.all
593
    @errors.cyclades.connection
594
    def _run(self):
595
        networks = self.client.list_networks(self['detail'])
596
        if self['detail']:
597
            self._make_results_pretty(networks)
598
        if self['more']:
599
            print_items(networks,
600
                page_size=self['limit'] if self['limit'] else 10)
601
        elif self['limit']:
602
            print_items(networks[:self['limit']])
603
        else:
604
            print_items(networks)
605

    
606
    def main(self):
607
        super(self.__class__, self)._run()
608
        self._run()
609

    
610

    
611
@command(network_cmds)
612
class network_create(_init_cyclades):
613
    """Create an (unconnected) network"""
614

    
615
    arguments = dict(
616
        cidr=ValueArgument('explicitly set cidr', '--with-cidr'),
617
        gateway=ValueArgument('explicitly set gateway', '--with-gateway'),
618
        dhcp=ValueArgument('explicitly set dhcp', '--with-dhcp'),
619
        type=ValueArgument('explicitly set type', '--with-type')
620
    )
621

    
622
    @errors.generic.all
623
    @errors.cyclades.connection
624
    @errors.cyclades.network_max
625
    def _run(self, name):
626
        r = self.client.create_network(name,
627
            cidr=self['cidr'],
628
            gateway=self['gateway'],
629
            dhcp=self['dhcp'],
630
            type=self['type'])
631
        print_items([r])
632

    
633
    def main(self, name):
634
        super(self.__class__, self)._run()
635
        self._run(name)
636

    
637

    
638
@command(network_cmds)
639
class network_rename(_init_cyclades):
640
    """Set the name of a network"""
641

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

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

    
652

    
653
@command(network_cmds)
654
class network_delete(_init_cyclades):
655
    """Delete a network"""
656

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

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

    
668

    
669
@command(network_cmds)
670
class network_connect(_init_cyclades):
671
    """Connect a server to a network"""
672

    
673
    @errors.generic.all
674
    @errors.cyclades.connection
675
    @errors.cyclades.server_id
676
    @errors.cyclades.network_id
677
    def _run(self, server_id, network_id):
678
        self.client.connect_server(int(server_id), int(network_id))
679

    
680
    def main(self, server_id, network_id):
681
        super(self.__class__, self)._run()
682
        self._run(server_id=server_id, network_id=network_id)
683

    
684

    
685
@command(network_cmds)
686
class network_disconnect(_init_cyclades):
687
    """Disconnect a nic that connects a server to a network
688
    Nic ids are listed as "attachments" in detailed network information
689
    To get detailed network information: /network info <network id>
690
    """
691

    
692
    @errors.cyclades.nic_format
693
    def _server_id_from_nic(self, nic_id):
694
        return nic_id.split('-')[1]
695

    
696
    @errors.generic.all
697
    @errors.cyclades.connection
698
    @errors.cyclades.server_id
699
    @errors.cyclades.nic_id
700
    def _run(self, nic_id, server_id):
701
        if not self.client.disconnect_server(server_id, nic_id):
702
            raise ClientError(
703
                'Network Interface %s not found on server %s' % (
704
                    nic_id,
705
                    server_id),
706
                status=404)
707

    
708
    def main(self, nic_id):
709
        super(self.__class__, self)._run()
710
        server_id = self._server_id_from_nic(nic_id=nic_id)
711
        self._run(nic_id=nic_id, server_id=server_id)