Statistics
| Branch: | Tag: | Revision:

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

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', 'Cyclades/Compute API server commands')
48
flavor_cmds = CommandTree('flavor', 'Cyclades/Compute API flavor commands')
49
network_cmds = CommandTree('network', 'Cyclades/Compute API network commands')
50
_commands = [server_cmds, flavor_cmds, network_cmds]
51

    
52

    
53
about_authentication = '\nUser Authentication:\
54
    \n* to check authentication: /user 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
        self._set_log_params()
76
        self._update_max_threads()
77

    
78
    def main(self):
79
        self._run()
80

    
81

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

    
86
    __doc__ += about_authentication
87

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

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

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

    
123
        if self['more']:
124
            print_items(
125
                servers,
126
                page_size=self['limit'] if self['limit'] else 10,
127
                with_enumeration=self['enum'])
128
        else:
129
            print_items(
130
                servers[:self['limit'] if self['limit'] else len(servers)],
131
                with_enumeration=self['enum'])
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
                msg = 'Wrong number of terms (should be 1 to 5)'
190
                raiseCLIError(CLISyntaxError(msg), details=howto_personality)
191
            path = termlist[0]
192
            if not exists(path):
193
                raiseCLIError(
194
                    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
            ('-p', '--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', '--force'))
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=('%s' % 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
            ('-N', '--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', '--details')),
498
        limit=IntArgument('limit # of listed flavors', ('-n', '--number')),
499
        more=FlagArgument(
500
            'output results in pages (-n to set items per page, default 10)',
501
            '--more'),
502
        enum=FlagArgument('Enumerate results', '--enumerate')
503
    )
504

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

    
516
    def main(self):
517
        super(self.__class__, self)._run()
518
        self._run()
519

    
520

    
521
@command(flavor_cmds)
522
class flavor_info(_init_cyclades):
523
    """Detailed information on a hardware flavor
524
    To get a list of available flavors and flavor ids, try /flavor list
525
    """
526

    
527
    @errors.generic.all
528
    @errors.cyclades.connection
529
    @errors.cyclades.flavor_id
530
    def _run(self, flavor_id):
531
        flavor = self.client.get_flavor_details(int(flavor_id))
532
        print_dict(flavor)
533

    
534
    def main(self, flavor_id):
535
        super(self.__class__, self)._run()
536
        self._run(flavor_id=flavor_id)
537

    
538

    
539
@command(network_cmds)
540
class network_info(_init_cyclades):
541
    """Detailed information on a network
542
    To get a list of available networks and network ids, try /network list
543
    """
544

    
545
    @classmethod
546
    def _make_result_pretty(self, net):
547
        if 'attachments' in net:
548
            att = net['attachments']['values']
549
            count = len(att)
550
            net['attachments'] = att if count else None
551

    
552
    @errors.generic.all
553
    @errors.cyclades.connection
554
    @errors.cyclades.network_id
555
    def _run(self, network_id):
556
        network = self.client.get_network_details(int(network_id))
557
        self._make_result_pretty(network)
558
        print_dict(network, exclude=('id'))
559

    
560
    def main(self, network_id):
561
        super(self.__class__, self)._run()
562
        self._run(network_id=network_id)
563

    
564

    
565
@command(network_cmds)
566
class network_list(_init_cyclades):
567
    """List networks"""
568

    
569
    arguments = dict(
570
        detail=FlagArgument('show detailed output', ('-l', '--details')),
571
        limit=IntArgument('limit # of listed networks', ('-n', '--number')),
572
        more=FlagArgument(
573
            'output results in pages (-n to set items per page, default 10)',
574
            '--more'),
575
        enum=FlagArgument('Enumerate results', '--enumerate')
576
    )
577

    
578
    def _make_results_pretty(self, nets):
579
        for net in nets:
580
            network_info._make_result_pretty(net)
581

    
582
    @errors.generic.all
583
    @errors.cyclades.connection
584
    def _run(self):
585
        networks = self.client.list_networks(self['detail'])
586
        if self['detail']:
587
            self._make_results_pretty(networks)
588
        if self['more']:
589
            print_items(
590
                networks,
591
                page_size=self['limit'] or 10, with_enumeration=self['enum'])
592
        elif self['limit']:
593
            print_items(
594
                networks[:self['limit']],
595
                with_enumeration=self['enum'])
596
        else:
597
            print_items(networks, with_enumeration=self['enum'])
598

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

    
603

    
604
@command(network_cmds)
605
class network_create(_init_cyclades):
606
    """Create an (unconnected) network"""
607

    
608
    arguments = dict(
609
        cidr=ValueArgument('explicitly set cidr', '--with-cidr'),
610
        gateway=ValueArgument('explicitly set gateway', '--with-gateway'),
611
        dhcp=FlagArgument('Use dhcp (default: off)', '--with-dhcp'),
612
        type=ValueArgument(
613
            'Valid network types are '
614
            'CUSTOM, IP_LESS_ROUTED, MAC_FILTERED (default), PHYSICAL_VLAN',
615
            '--with-type',
616
            default='MAC_FILTERED')
617
    )
618

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

    
631
    def main(self, name):
632
        super(self.__class__, self)._run()
633
        self._run(name)
634

    
635

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

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

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

    
650

    
651
@command(network_cmds)
652
class network_delete(_init_cyclades):
653
    """Delete a network"""
654

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

    
662
    def main(self, network_id):
663
        super(self.__class__, self)._run()
664
        self._run(network_id=network_id)
665

    
666

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

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

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

    
682

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

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

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

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