Revision eb647cfe

b/kamaki/cli/argument/__init__.py
52 52
    This is the top-level Argument class. It is suggested to extent this
53 53
    class into more specific argument types.
54 54
    """
55
    lvalue_delimiter = '/'
55 56

  
56 57
    def __init__(self, arity, help=None, parsed_name=None, default=None):
57 58
        self.arity = int(arity)
......
86 87
            *self.parsed_name,
87 88
            dest=name, action=action, default=self.default, help=self.help)
88 89

  
90
    @property
91
    def lvalue(self):
92
        """A printable form of the left value when calling an argument e.g.,
93
        --left-value=right-value"""
94
        return (self.lvalue_delimiter or ' ').join(self.parsed_name or [])
95

  
89 96

  
90 97
class ConfigArgument(Argument):
91 98
    """Manage a kamaki configuration (file)"""
b/kamaki/cli/commands/__init__.py
263 263
        else:
264 264
            raise CLIInvalidArgument(
265 265
                'Invalid value %s for argument %s' % (
266
                    newvalue, '/'.join(self.parsed_name)),
266
                    newvalue, self.lvalue),
267 267
                details=['Valid output formats: %s' % ', '.join(self.formats)])
268 268

  
269 269

  
b/kamaki/cli/commands/cyclades.py
415 415
            'Connect server to network w. floating ip ( NETWORK_ID,IP )'
416 416
            '(can be repeated)',
417 417
            '--network-with-ip'),
418
        automatic_ip=FlagArgument(
419
            'Automatically assign an IP to the server', '--automatic-ip')
418 420
    )
419 421
    required = ('server_name', 'flavor_id', 'image_id')
420 422

  
421 423
    @errors.cyclades.cluster_size
422 424
    def _create_cluster(self, prefix, flavor_id, image_id, size):
423
        networks = [dict(network=netid) for netid in (
424
            self['network_id'] or [])] + (self['network_id_and_ip'] or [])
425
        if self['automatic_ip']:
426
            networks = []
427
        else:
428
            networks = [dict(network=netid) for netid in (
429
                (self['network_id'] or []) + (self['network_id_and_ip'] or [])
430
            )] or None
425 431
        servers = [dict(
426 432
            name='%s%s' % (prefix, i if size > 1 else ''),
427 433
            flavor_id=flavor_id,
......
473 479

  
474 480
    def main(self):
475 481
        super(self.__class__, self)._run()
482
        if self['automatic_ip'] and (
483
                self['network_id'] or self['network_id_and_ip']):
484
            raise CLIInvalidArgument('Invalid argument combination', details=[
485
                'Argument %s should not be combined with other' % (
486
                    self.arguments['automatic_ip'].lvalue),
487
                'network-related arguments i.e., %s or %s' % (
488
                    self.arguments['network_id'].lvalue,
489
                    self.arguments['network_id_and_ip'].lvalue)])
476 490
        self._run(
477 491
            name=self['server_name'],
478 492
            flavor_id=self['flavor_id'],
b/kamaki/cli/commands/pithos.py
377 377
        if self['publish'] and self['unpublish']:
378 378
            raise CLIInvalidArgument(
379 379
                'Arguments %s and %s cannot be used together' % (
380
                    '/'.join(self.arguments['publish'].parsed_name),
381
                    '/'.join(self.arguments['publish'].parsed_name)))
380
                    self.arguments['publish'].lvalue,
381
                    self.arguments['publish'].lvalue))
382 382
        if self['no_permissions'] and (
383 383
                self['uuid_for_read_permission'] or self[
384 384
                    'uuid_for_write_permission']):
385 385
            raise CLIInvalidArgument(
386
                '%s cannot be used with other permission arguments' % '/'.join(
387
                    self.arguments['no_permissions'].parsed_name))
386
                '%s cannot be used with other permission arguments' % (
387
                    self.arguments['no_permissions'].lvalue))
388 388
        self._run()
389 389

  
390 390

  
......
555 555
                                    self.dst_client.account,
556 556
                                    self.dst_client.container,
557 557
                                    dst_path),
558
                            'Use %s to transfer overwrite' % ('/'.join(
559
                                    self.arguments['force'].parsed_name))])
558
                            'Use %s to transfer overwrite' % (
559
                                    self.arguments['force'].lvalue)])
560 560
        else:
561 561
            #  One object transfer
562 562
            try:
......
570 570
                        'Missing specific path container %s' % self.container,
571 571
                        importance=2, details=[
572 572
                            'To transfer container contents %s' % (
573
                                '/'.join(self.arguments[
574
                                    'source_prefix'].parsed_name))])
573
                                self.arguments['source_prefix'].lvalue)])
575 574
                raise
576 575
            dst_path = self.dst_path or self.path
577 576
            dst_obj = dst_objects.get(dst_path or self.path, None)
......
589 588
                            self.container,
590 589
                            self.path),
591 590
                        'To recursively copy a directory, use',
592
                        '  %s' % ('/'.join(
593
                            self.arguments['source_prefix'].parsed_name)),
591
                        '  %s' % self.arguments['source_prefix'].lvalue,
594 592
                        'To create a file, use',
595 593
                        '  /file create  (general purpose)',
596 594
                        '  /file mkdir   (a directory object)'])
......
607 605
                                self.dst_client.account,
608 606
                                self.dst_client.container,
609 607
                                dst_path),
610
                        'Use %s to transfer overwrite' % ('/'.join(
611
                                self.arguments['force'].parsed_name))])
608
                        'Use %s to transfer overwrite' % (
609
                                self.arguments['force'].lvalue)])
612 610
        return pairs
613 611

  
614 612
    def _run(self, source_path_or_url, destination_path_or_url=''):
......
873 871
        if path.isdir(lpath):
874 872
            if not self['recursive']:
875 873
                raise CLIError('%s is a directory' % lpath, details=[
876
                    'Use %s to upload directories & contents' % '/'.join(
877
                        self.arguments['recursive'].parsed_name)])
874
                    'Use %s to upload directories & contents' % (
875
                        self.arguments['recursive'].lvalue)])
878 876
            robj = self.client.container_get(path=rpath)
879 877
            if not self['overwrite']:
880 878
                if robj.json:
......
1174 1172
                    elif path.exists(lpath):
1175 1173
                        raise CLIError(
1176 1174
                            'Cannot overwrite %s' % lpath,
1177
                            details=['To overwrite/resume, use  %s' % '/'.join(
1178
                                self.arguments['resume'].parsed_name)])
1175
                            details=['To overwrite/resume, use  %s' % (
1176
                                self.arguments['resume'].lvalue)])
1179 1177
                    else:
1180 1178
                        ret.append((opath, lpath, None))
1181 1179
            elif self.path:
1182 1180
                raise CLIError(
1183 1181
                    'Remote object /%s/%s is a directory' % (
1184 1182
                        self.container, local_path),
1185
                    details=['Use %s to download directories' % '/'.join(
1186
                        self.arguments['recursive'].parsed_name)])
1183
                    details=['Use %s to download directories' % (
1184
                        self.arguments['recursive'].lvalue)])
1187 1185
            else:
1188
                parsed_name = '/'.join(self.arguments['recursive'].parsed_name)
1186
                parsed_name = self.arguments['recursive'].lvalue
1189 1187
                raise CLIError(
1190 1188
                    'Cannot download container %s' % self.container,
1191 1189
                    details=[
......
1197 1195
            if path.exists(local_path) and not self['resume']:
1198 1196
                raise CLIError(
1199 1197
                    'Cannot overwrite local file %s' % (lpath),
1200
                    details=['To overwrite/resume, use  %s' % '/'.join(
1201
                        self.arguments['resume'].parsed_name)])
1198
                    details=['To overwrite/resume, use  %s' % (
1199
                        self.arguments['resume'].lvalue)])
1202 1200
            ret.append((rpath, local_path, self['resume']))
1203 1201
        for r, l, resume in ret:
1204 1202
            if r:
......
1528 1526
            delimiter, msg = '/', 'Empty and d%s' % msg[1:]
1529 1527
        elif num_of_contents:
1530 1528
            raise CLIError('Container %s is not empty' % container, details=[
1531
                'Use %s to delete non-empty containers' % '/'.join(
1532
                    self.arguments['recursive'].parsed_name)])
1529
                'Use %s to delete non-empty containers' % (
1530
                    self.arguments['recursive'].lvalue)])
1533 1531
        if self['yes'] or self.ask_user(msg):
1534 1532
            if num_of_contents:
1535 1533
                self.client.del_container(delimiter=delimiter)
......
1658 1656
        else:
1659 1657
            raise CLISyntaxError(
1660 1658
                'No valid users specified, use %s or %s' % (
1661
                    '/'.join(self.arguments['user_uuid'].parsed_name),
1662
                    '/'.join(self.arguments['username'].parsed_name)),
1659
                    self.arguments['user_uuid'].lvalue,
1660
                    self.arguments['username'].lvalue),
1663 1661
                details=[
1664 1662
                    'Check if a username or uuid is valid with',
1665 1663
                    '  user uuid2username', 'OR', '  user username2uuid'])
b/kamaki/clients/compute/__init__.py
128 128
        :param personality: a list of (file path, file contents) tuples,
129 129
            describing files to be injected into virtual server upon creation
130 130

  
131
        :param networks: (list of dicts) Networks to connect to, list this:
132
            "networks": [
133
                {"network": <network_uuid>},
134
                {"network": <network_uuid>, "fixed_ip": address},
135
                {"port": <port_id>}, ...]
136
            ATTENTION: Empty list is different to None. None means ' do not
137
            mention it', empty list means 'automatically get an ip'
138

  
131 139
        :returns: a dict with the new virtual server details
132 140

  
133 141
        :raises ClientError: wraps request errors
......
141 149
        if personality:
142 150
            req['server']['personality'] = personality
143 151

  
144
        if networks:
145
            req['server']['networks'] = networks
152
        if networks is not None:
153
            req['server']['networks'] = networks or []
146 154

  
147 155
        r = self.servers_post(
148 156
            json_data=req,
b/kamaki/clients/cyclades/__init__.py
61 61
                {"network": <network_uuid>},
62 62
                {"network": <network_uuid>, "fixed_ip": address},
63 63
                {"port": <port_id>}, ...]
64
            ATTENTION: Empty list is different to None. None means ' do not
65
            mention it', empty list means 'automatically get an ip'
64 66

  
65 67
        :returns: a dict with the new virtual server details
66 68

  
......
76 78

  
77 79
        return super(CycladesClient, self).create_server(
78 80
            name, flavor_id, image_id,
79
            metadata=metadata, personality=personality)
81
            metadata=metadata, personality=personality, networks=networks)
80 82

  
81 83
    def start_server(self, server_id):
82 84
        """Submit a startup request
......
157 159
        r = self.servers_stats_get(server_id)
158 160
        return r.json['stats']
159 161

  
160
    def list_networks(self, detail=False):
161
        """
162
        :param detail: (bool)
163

  
164
        :returns: (list) id,name if not detail else full info per network
165
        """
166
        detail = 'detail' if detail else ''
167
        r = self.networks_get(command=detail)
168
        return r.json['networks']
169

  
170
    def list_network_nics(self, network_id):
171
        """
172
        :param network_id: integer (str or int)
173

  
174
        :returns: (list)
175
        """
176
        r = self.networks_get(network_id=network_id)
177
        return r.json['network']['attachments']
178

  
179
    def create_network(
180
            self, name,
181
            cidr=None, gateway=None, type=None, dhcp=False):
182
        """
183
        :param name: (str)
184

  
185
        :param cidr: (str)
186

  
187
        :param geteway: (str)
188

  
189
        :param type: (str) if None, will use MAC_FILTERED as default
190
            Valid values: CUSTOM, IP_LESS_ROUTED, MAC_FILTERED, PHYSICAL_VLAN
191

  
192
        :param dhcp: (bool)
193

  
194
        :returns: (dict) network detailed info
195
        """
196
        net = dict(name=name)
197
        if cidr:
198
            net['cidr'] = cidr
199
        if gateway:
200
            net['gateway'] = gateway
201
        net['type'] = type or 'MAC_FILTERED'
202
        net['dhcp'] = True if dhcp else False
203
        req = dict(network=net)
204
        r = self.networks_post(json_data=req, success=202)
205
        return r.json['network']
206

  
207
    def get_network_details(self, network_id):
208
        """
209
        :param network_id: integer (str or int)
210

  
211
        :returns: (dict)
212
        """
213
        r = self.networks_get(network_id=network_id)
214
        return r.json['network']
215

  
216
    def update_network_name(self, network_id, new_name):
217
        """
218
        :param network_id: integer (str or int)
219

  
220
        :param new_name: (str)
221

  
222
        :returns: (dict) response headers
223
        """
224
        req = {'network': {'name': new_name}}
225
        r = self.networks_put(network_id=network_id, json_data=req)
226
        return r.headers
227

  
228
    def delete_network(self, network_id):
229
        """
230
        :param network_id: integer (str or int)
231

  
232
        :returns: (dict) response headers
233

  
234
        :raises ClientError: 421 Network in use
235
        """
236
        try:
237
            r = self.networks_delete(network_id)
238
            return r.headers
239
        except ClientError as err:
240
            if err.status == 421:
241
                err.details = [
242
                    'Network may be still connected to at least one server']
243
            raise
244

  
245
    def connect_server(self, server_id, network_id):
246
        """ Connect a server to a network
247

  
248
        :param server_id: integer (str or int)
249

  
250
        :param network_id: integer (str or int)
251

  
252
        :returns: (dict) response headers
253
        """
254
        req = {'add': {'serverRef': server_id}}
255
        r = self.networks_post(network_id, 'action', json_data=req)
256
        return r.headers
257

  
258
    def disconnect_server(self, server_id, nic_id):
259
        """
260
        :param server_id: integer (str or int)
261

  
262
        :param nic_id: (str)
263

  
264
        :returns: (int) the number of nics disconnected
265
        """
266
        vm_nets = self.list_server_nics(server_id)
267
        num_of_disconnections = 0
268
        for (nic_id, network_id) in [(
269
                net['id'],
270
                net['network_id']) for net in vm_nets if nic_id == net['id']]:
271
            req = {'remove': {'attachment': '%s' % nic_id}}
272
            self.networks_post(network_id, 'action', json_data=req)
273
            num_of_disconnections += 1
274
        return num_of_disconnections
275

  
276
    def disconnect_network_nics(self, netid):
277
        """
278
        :param netid: integer (str or int)
279
        """
280
        for nic in self.list_network_nics(netid):
281
            req = dict(remove=dict(attachment=nic))
282
            self.networks_post(netid, 'action', json_data=req)
283

  
284 162
    def wait_server(
285 163
            self, server_id,
286 164
            current_status='BUILD',
......
308 186
        return self._wait(
309 187
            server_id, current_status, get_status, delay, max_wait, wait_cb)
310 188

  
311
    def wait_network(
312
            self, net_id,
313
            current_status='PENDING', delay=1, max_wait=100, wait_cb=None):
314
        """Wait for network while its status is current_status
315

  
316
        :param net_id: integer (str or int)
317

  
318
        :param current_status: (str) PENDING | ACTIVE | DELETED
319

  
320
        :param delay: time interval between retries
321

  
322
        :max_wait: (int) timeout in secconds
323

  
324
        :param wait_cb: if set a progressbar is used to show progress
325

  
326
        :returns: (str) the new mode if succesfull, (bool) False if timed out
327
        """
328

  
329
        def get_status(self, net_id):
330
            r = self.get_network_details(net_id)
331
            return r['status'], None
332

  
333
        return self._wait(
334
            net_id, current_status, get_status, delay, max_wait, wait_cb)
335

  
336 189
    def wait_firewall(
337 190
            self, server_id,
338 191
            current_status='DISABLED', delay=1, max_wait=100, wait_cb=None):
......
358 211
            server_id, current_status, get_status, delay, max_wait, wait_cb)
359 212

  
360 213

  
361
class CycladesNetworkClient(NetworkClient, Waiter):
214
class CycladesNetworkClient(NetworkClient):
362 215
    """Cyclades Network API extentions"""
363 216

  
364 217
    network_types = (
......
379 232
        return r.json['network']
380 233

  
381 234
    def create_port(
382
            self, network_id, device_id,
383
            security_groups=None, name=None, fixed_ips=None):
384
        port = dict(network_id=network_id, device_id=device_id)
235
            self, network_id,
236
            device_id=None, security_groups=None, name=None, fixed_ips=None):
237
        port = dict(network_id=network_id)
238
        if device_id:
239
            port['device_id'] = device_id
385 240
        if security_groups:
386 241
            port['security_groups'] = security_groups
387 242
        if name:
388 243
            port['name'] = name
389
        for fixed_ip in fixed_ips:
244
        for fixed_ip in fixed_ips or []:
390 245
            diff = set(['subnet_id', 'ip_address']).difference(fixed_ip)
391 246
            if diff:
392 247
                raise ValueError(
......
396 251
        r = self.ports_post(json_data=dict(port=port), success=201)
397 252
        return r.json['port']
398 253

  
399
    def wait_network(
400
            self, net_id,
401
            current_status='PENDING', delay=1, max_wait=100, wait_cb=None):
254
    def create_floatingip(self, floating_network_id, floating_ip_address=''):
255
        return super(CycladesNetworkClient, self).create_floatingip(
256
            floating_network_id, floating_ip_address=floating_ip_address)
402 257

  
403
        def get_status(self, net_id):
404
            r = self.get_network_details(net_id)
405
            return r['status'], None
406

  
407
        return self._wait(
408
            net_id, current_status, get_status, delay, max_wait, wait_cb)
409

  
410
    def wait_port(
411
            self, port_id,
412
            current_status='PENDING', delay=1, max_wait=100, wait_cb=None):
413

  
414
        def get_status(self, net_id):
415
            r = self.get_port_details(port_id)
416
            return r['status'], None
417

  
418
        return self._wait(
419
            port_id, current_status, get_status, delay, max_wait, wait_cb)
258
    def update_floatingip(self, floating_network_id, floating_ip_address=''):
259
        """To nullify something optional, use None"""
260
        return super(CycladesNetworkClient, self).update_floatingip(
261
            floating_network_id, floating_ip_address=floating_ip_address)
b/kamaki/clients/network/__init__.py
31 31
# interpreted as representing official policies, either expressed
32 32
# or implied, of GRNET S.A.
33 33

  
34
from kamaki.clients import ClientError
34
from kamaki.clients import ClientError, Waiter
35 35
from kamaki.clients.network.rest_api import NetworkRestClient
36 36

  
37 37

  
38
class NetworkClient(NetworkRestClient):
38
class NetworkClient(NetworkRestClient, Waiter):
39 39
    """OpenStack Network API 2.0 client"""
40 40

  
41 41
    def list_networks(self):
......
362 362
    def delete_floatingip(self, floatingip_id):
363 363
        r = self.floatingips_delete(floatingip_id, success=204)
364 364
        return r.headers
365

  
366
    #  Wait methods
367

  
368
    def wait_network(
369
            self, net_id,
370
            current_status='PENDING', delay=1, max_wait=100, wait_cb=None):
371

  
372
        def get_status(self, net_id):
373
            r = self.get_network_details(net_id)
374
            return r['status'], None
375

  
376
        return self._wait(
377
            net_id, current_status, get_status, delay, max_wait, wait_cb)
378

  
379
    def wait_subnet(
380
            self, subnet_id,
381
            current_status='PENDING', delay=1, max_wait=100, wait_cb=None):
382

  
383
        def get_status(self, subnet_id):
384
            r = self.get_subnet_details(subnet_id)
385
            return r['status'], None
386

  
387
        return self._wait(
388
            subnet_id, current_status, get_status, delay, max_wait, wait_cb)
389

  
390
    def wait_port(
391
            self, port_id,
392
            current_status='PENDING', delay=1, max_wait=100, wait_cb=None):
393

  
394
        def get_status(self, net_id):
395
            r = self.get_port_details(port_id)
396
            return r['status'], None
397

  
398
        return self._wait(
399
            port_id, current_status, get_status, delay, max_wait, wait_cb)
400

  
401
    def wait_floatingip(
402
            self, floatingip_id,
403
            current_status='PENDING', delay=1, max_wait=100, wait_cb=None):
404

  
405
        def get_status(self, floatingip_id):
406
            r = self.get_network_details(floatingip_id)
407
            return r['status'], None
408

  
409
        return self._wait(
410
            floatingip_id,
411
            current_status, get_status, delay, max_wait, wait_cb)

Also available in: Unified diff