Revision bcef3ac9

b/Changelog
54 54
    - server: firewall, metadata
55 55
- Add a _format_image_headers method and use it in image_register and get_meta
56 56
    for uniform image meta output [#3797]
57
- Rename meta-->metadata and remove values @lib [#3633]
58

  
57 59

  
58 60
Features:
59 61

  
b/kamaki/cli/commands/__init__.py
31 31
# interpreted as representing official policies, either expressed
32 32
# or implied, of GRNET S.A.command
33 33

  
34
from kamaki.logger import get_logger
34
from kamaki.cli.logger import get_logger
35 35
from kamaki.cli.utils import print_json, print_items
36 36
from kamaki.cli.argument import FlagArgument
37 37

  
b/kamaki/cli/commands/cyclades.py
98 98
        enum=FlagArgument('Enumerate results', '--enumerate')
99 99
    )
100 100

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

  
116 101
    @errors.generic.all
117 102
    @errors.cyclades.connection
118 103
    @errors.cyclades.date
119 104
    def _run(self):
120 105
        servers = self.client.list_servers(self['detail'], self['since'])
121 106

  
122
        if self['detail'] and not self['json_output']:
123
            self._make_results_pretty(servers)
124

  
125 107
        kwargs = dict(with_enumeration=self['enum'])
126 108
        if self['more']:
127 109
            kwargs['page_size'] = self['limit'] if self['limit'] else 10
......
144 126
    - hardware flavor and os image ids
145 127
    """
146 128

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

  
163 129
    @errors.generic.all
164 130
    @errors.cyclades.connection
165 131
    @errors.cyclades.server_id
166 132
    def _run(self, server_id):
167
        self._print(self.client.get_server_details(server_id), self._pretty)
133
        self._print(self.client.get_server_details(server_id), print_dict)
168 134

  
169 135
    def main(self, server_id):
170 136
        super(self.__class__, self)._run()
......
224 190
    @errors.plankton.id
225 191
    @errors.cyclades.flavor_id
226 192
    def _run(self, name, flavor_id, image_id):
227
        self._print([self.client.create_server(
228
            name, int(flavor_id), image_id, self['personality'])])
193
        self._print(
194
            self.client.create_server(
195
                name, int(flavor_id), image_id, self['personality']),
196
            print_dict)
229 197

  
230 198
    def main(self, name, flavor_id, image_id):
231 199
        super(self.__class__, self)._run()
......
328 296
    @errors.cyclades.connection
329 297
    @errors.cyclades.server_id
330 298
    def _run(self, server_id):
331
        self._print([self.client.get_server_console(int(server_id))])
299
        self._print(
300
            self.client.get_server_console(int(server_id)), print_dict)
332 301

  
333 302
    def main(self, server_id):
334 303
        super(self.__class__, self)._run()
......
413 382
    @errors.cyclades.metadata
414 383
    def _run(self, server_id, key=''):
415 384
        self._print(
416
            [self.client.get_server_metadata(int(server_id), key)], title=())
385
            self.client.get_server_metadata(int(server_id), key), print_dict)
417 386

  
418 387
    def main(self, server_id, key=''):
419 388
        super(self.__class__, self)._run()
......
448 417
                        '/server metadata set <server id>'
449 418
                        'key1=value1 key2=value2'])
450 419
        self._print(
451
            [self.client.update_server_metadata(int(server_id), **metadata)],
452
            title=())
420
            self.client.update_server_metadata(int(server_id), **metadata),
421
            print_dict)
453 422

  
454 423
    def main(self, server_id, *key_equals_val):
455 424
        super(self.__class__, self)._run()
......
481 450
    @errors.cyclades.connection
482 451
    @errors.cyclades.server_id
483 452
    def _run(self, server_id):
484
        self._print([self.client.get_server_stats(int(server_id))])
453
        self._print(self.client.get_server_stats(int(server_id)), print_dict)
485 454

  
486 455
    def main(self, server_id):
487 456
        super(self.__class__, self)._run()
......
566 535
    @errors.cyclades.connection
567 536
    @errors.cyclades.flavor_id
568 537
    def _run(self, flavor_id):
569
        self._print([self.client.get_flavor_details(int(flavor_id))])
538
        self._print(
539
            self.client.get_flavor_details(int(flavor_id)), print_dict)
570 540

  
571 541
    def main(self, flavor_id):
572 542
        super(self.__class__, self)._run()
......
579 549
    To get a list of available networks and network ids, try /network list
580 550
    """
581 551

  
582
    @classmethod
583
    def _make_result_pretty(self, net):
584
        if 'attachments' in net:
585
            att = net['attachments']['values']
586
            count = len(att)
587
            net['attachments'] = att if count else None
588

  
589 552
    @errors.generic.all
590 553
    @errors.cyclades.connection
591 554
    @errors.cyclades.network_id
592 555
    def _run(self, network_id):
593 556
        network = self.client.get_network_details(int(network_id))
594
        self._make_result_pretty(network)
595
        #print_dict(network, exclude=('id'))
596 557
        self._print(network, print_dict, exclude=('id'))
597 558

  
598 559
    def main(self, network_id):
......
613 574
        enum=FlagArgument('Enumerate results', '--enumerate')
614 575
    )
615 576

  
616
    def _make_results_pretty(self, nets):
617
        for net in nets:
618
            network_info._make_result_pretty(net)
619

  
620 577
    @errors.generic.all
621 578
    @errors.cyclades.connection
622 579
    def _run(self):
623 580
        networks = self.client.list_networks(self['detail'])
624
        if self['detail']:
625
            self._make_results_pretty(networks)
626 581
        kwargs = dict(with_enumeration=self['enum'])
627 582
        if self['more']:
628 583
            kwargs['page_size'] = self['limit'] or 10
......
654 609
    @errors.cyclades.connection
655 610
    @errors.cyclades.network_max
656 611
    def _run(self, name):
657
        self._print([self.client.create_network(
612
        self._print(self.client.create_network(
658 613
            name,
659 614
            cidr=self['cidr'],
660 615
            gateway=self['gateway'],
661 616
            dhcp=self['dhcp'],
662
            type=self['type'])])
617
            type=self['type']), print_dict)
663 618

  
664 619
    def main(self, name):
665 620
        super(self.__class__, self)._run()
b/kamaki/cli/commands/image.py
533 533
        enum=FlagArgument('Enumerate results', '--enumerate')
534 534
    )
535 535

  
536
    def _make_results_pretty(self, images):
537
        for img in images:
538
            if 'metadata' in img:
539
                img['metadata'] = img['metadata']['values']
540

  
541 536
    @errors.generic.all
542 537
    @errors.cyclades.connection
543 538
    def _run(self):
544 539
        images = self.client.list_images(self['detail'])
545
        if self['detail'] and not self['json_output']:
546
            self._make_results_pretty(images)
547 540
        kwargs = dict(with_enumeration=self['enum'])
548 541
        if self['more']:
549 542
            kwargs['page_size'] = self['limit'] or 10
550
        else:
543
        elif self['limit']:
551 544
            images = images[:self['limit']]
552 545
        self._print(images, **kwargs)
553 546

  
......
565 558
    @errors.plankton.id
566 559
    def _run(self, image_id):
567 560
        image = self.client.get_image_details(image_id)
568
        if (not self['json_output']) and 'metadata' in image:
569
            image['metadata'] = image['metadata']['values']
570
        self._print([image])
561
        self._print(image, print_dict)
571 562

  
572 563
    def main(self, image_id):
573 564
        super(self.__class__, self)._run()
b/kamaki/cli/errors.py
31 31
# interpreted as representing official policies, either expressed
32 32
# or implied, of GRNET S.A.
33 33

  
34
from kamaki.logger import get_logger
34
from kamaki.cli.logger import get_logger
35 35

  
36 36
log = get_logger('kamaki.cli')
37 37

  
b/kamaki/clients/compute/__init__.py
153 153
        """
154 154
        command = path4url('metadata', key)
155 155
        r = self.servers_get(server_id, command)
156
        return r.json['meta'] if key else r.json['metadata']
156
        return r.json['metadata']
157 157

  
158 158
    def create_server_metadata(self, server_id, key, val):
159 159
        """
......
165 165

  
166 166
        :returns: dict of updated key:val metadata
167 167
        """
168
        req = {'meta': {key: val}}
168
        req = {'metadata': {key: val}}
169 169
        r = self.servers_put(
170 170
            server_id,
171 171
            'metadata/' + key,
172 172
            json_data=req,
173 173
            success=201)
174
        return r.json['meta']
174
        return r.json['metadata']
175 175

  
176 176
    def update_server_metadata(self, server_id, **metadata):
177 177
        """
......
202 202
        """
203 203
        :param detail: (bool) detailed flavor info if set, short if not
204 204

  
205
        :returns: (dict) flavor info
205
        :returns: (list) flavor info
206 206
        """
207 207
        r = self.flavors_get(command='detail' if detail else '')
208 208
        return r.json['flavors']
......
258 258
        """
259 259
        command = path4url('metadata', key)
260 260
        r = self.images_get(image_id, command)
261
        return r.json['meta'] if key else r.json['metadata']
261
        return r.json['metadata']
262 262

  
263 263
    def create_image_metadata(self, image_id, key, val):
264 264
        """
......
270 270

  
271 271
        :returns: (dict) updated metadata
272 272
        """
273
        req = {'meta': {key: val}}
273
        req = {'metadata': {key: val}}
274 274
        r = self.images_put(image_id, 'metadata/' + key, json_data=req)
275
        return r.json['meta']
275
        return r.json['metadata']
276 276

  
277 277
    def update_image_metadata(self, image_id, **metadata):
278 278
        """
b/kamaki/clients/compute/test.py
62 62
    suspended=False,
63 63
    progress=0,
64 64
    id=31173,
65
    metadata=dict(values=dict(os="debian", users="root"))))
65
    metadata=dict(os="debian", users="root")))
66 66
img_recv = dict(image=dict(
67 67
    status="ACTIVE",
68 68
    updated="2013-02-26T11:10:14+00:00",
......
70 70
    created="2013-02-26T11:03:29+00:00",
71 71
    progress=100,
72 72
    id=img_ref,
73
    metadata=dict(values=dict(
73
    metadata=dict(
74 74
        partition_table="msdos",
75 75
        kernel="2.6.32",
76 76
        osfamily="linux",
......
79 79
        sortorder="1",
80 80
        os="debian",
81 81
        root_partition="1",
82
        description="Debian 6.0.7 (Squeeze) Base System"))))
83
vm_list = dict(servers=dict(values=[
82
        description="Debian 6.0.7 (Squeeze) Base System")))
83
vm_list = dict(servers=[
84 84
    dict(name='n1', id=1),
85
    dict(name='n2', id=2)]))
86
flavor_list = dict(flavors=dict(values=[
85
    dict(name='n2', id=2)])
86
flavor_list = dict(flavors=[
87 87
    dict(id=41, name="C1R1024D20"),
88 88
    dict(id=42, name="C1R1024D40"),
89
    dict(id=43, name="C1R1028D20")]))
90
img_list = dict(images=dict(values=[
89
    dict(id=43, name="C1R1028D20")])
90
img_list = dict(images=[
91 91
    dict(name="maelstrom", id="0fb03e45-7d5a-4515-bd4e-e6bbf6457f06"),
92 92
    dict(name="edx_saas", id="1357163d-5fd8-488e-a117-48734c526206"),
93 93
    dict(name="Debian_Wheezy_Base", id="1f8454f0-8e3e-4b6c-ab8e-5236b728dffe"),
......
95 95
    dict(name="Ubuntu Desktop", id="37bc522c-c479-4085-bfb9-464f9b9e2e31"),
96 96
    dict(name="Ubuntu 12.10", id="3a24fef9-1a8c-47d1-8f11-e07bd5e544fd"),
97 97
    dict(name="Debian Base", id="40ace203-6254-4e17-a5cb-518d55418a7d"),
98
    dict(name="ubuntu_bundled", id="5336e265-5c7c-4127-95cb-2bf832a79903")]))
98
    dict(name="ubuntu_bundled", id="5336e265-5c7c-4127-95cb-2bf832a79903")])
99 99

  
100 100

  
101 101
class FR(object):
......
282 282
            r = self.client.list_servers(detail)
283 283
            self.assertEqual(SG.mock_calls[-1], call(
284 284
                command='detail' if detail else ''))
285
            for i, vm in enumerate(vm_list['servers']['values']):
285
            for i, vm in enumerate(vm_list['servers']):
286 286
                self.assert_dicts_are_equal(r[i], vm)
287 287
            self.assertEqual(i + 1, len(r))
288 288

  
......
314 314
    def test_create_server_metadata(self, SP):
315 315
        vm_id = vm_recv['server']['id']
316 316
        metadata = dict(m1='v1', m2='v2', m3='v3')
317
        FR.json = dict(meta=vm_recv['server'])
317
        FR.json = dict(metadata=vm_recv['server'])
318 318
        for k, v in metadata.items():
319 319
            r = self.client.create_server_metadata(vm_id, k, v)
320 320
            self.assert_dicts_are_equal(r, vm_recv['server'])
321 321
            self.assertEqual(SP.mock_calls[-1], call(
322
                vm_id, 'meta/%s' % k,
323
                json_data=dict(meta={k: v}), success=201))
322
                vm_id, 'metadata/%s' % k,
323
                json_data=dict(metadata={k: v}), success=201))
324 324

  
325 325
    @patch('%s.servers_get' % compute_pkg, return_value=FR())
326 326
    def test_get_server_metadata(self, SG):
327 327
        vm_id = vm_recv['server']['id']
328 328
        metadata = dict(m1='v1', m2='v2', m3='v3')
329
        FR.json = dict(metadata=dict(values=metadata))
329
        FR.json = dict(metadata=metadata)
330 330
        r = self.client.get_server_metadata(vm_id)
331
        SG.assert_called_once_with(vm_id, '/meta')
331
        SG.assert_called_once_with(vm_id, '/metadata')
332 332
        self.assert_dicts_are_equal(r, metadata)
333 333

  
334 334
        for k, v in metadata.items():
335
            FR.json = dict(meta={k: v})
335
            FR.json = dict(metadata={k: v})
336 336
            r = self.client.get_server_metadata(vm_id, k)
337 337
            self.assert_dicts_are_equal(r, {k: v})
338
            self.assertEqual(SG.mock_calls[-1], call(vm_id, '/meta/%s' % k))
338
            self.assertEqual(
339
                SG.mock_calls[-1], call(vm_id, '/metadata/%s' % k))
339 340

  
340 341
    @patch('%s.servers_post' % compute_pkg, return_value=FR())
341 342
    def test_update_server_metadata(self, SP):
......
345 346
        r = self.client.update_server_metadata(vm_id, **metadata)
346 347
        self.assert_dicts_are_equal(r, metadata)
347 348
        SP.assert_called_once_with(
348
            vm_id, 'meta',
349
            vm_id, 'metadata',
349 350
            json_data=dict(metadata=metadata), success=201)
350 351

  
351 352
    @patch('%s.servers_delete' % compute_pkg, return_value=FR())
......
353 354
        vm_id = vm_recv['server']['id']
354 355
        key = 'metakey'
355 356
        self.client.delete_server_metadata(vm_id, key)
356
        SD.assert_called_once_with(vm_id, 'meta/' + key)
357
        SD.assert_called_once_with(vm_id, 'metadata/' + key)
357 358

  
358 359
    @patch('%s.flavors_get' % compute_pkg, return_value=FR())
359 360
    def test_list_flavors(self, FG):
......
361 362
        for cmd in ('', 'detail'):
362 363
            r = self.client.list_flavors(detail=(cmd == 'detail'))
363 364
            self.assertEqual(FG.mock_calls[-1], call(command=cmd))
364
            self.assert_dicts_are_equal(dict(values=r), flavor_list['flavors'])
365
            self.assertEqual(r, flavor_list['flavors'])
365 366

  
366 367
    @patch('%s.flavors_get' % compute_pkg, return_value=FR())
367 368
    def test_get_flavor_details(self, FG):
368
        FR.json = dict(flavor=flavor_list['flavors'])
369
        FR.json = dict(flavor=flavor_list['flavors'][0])
369 370
        r = self.client.get_flavor_details(fid)
370 371
        FG.assert_called_once_with(fid)
371
        self.assert_dicts_are_equal(r, flavor_list['flavors'])
372
        self.assert_dicts_are_equal(r, flavor_list['flavors'][0])
372 373

  
373 374
    @patch('%s.images_get' % compute_pkg, return_value=FR())
374 375
    def test_list_images(self, IG):
......
376 377
        for cmd in ('', 'detail'):
377 378
            r = self.client.list_images(detail=(cmd == 'detail'))
378 379
            self.assertEqual(IG.mock_calls[-1], call(command=cmd))
379
            expected = img_list['images']['values']
380
            expected = img_list['images']
380 381
            for i in range(len(r)):
381 382
                self.assert_dicts_are_equal(expected[i], r[i])
382 383

  
......
390 391
    @patch('%s.images_get' % compute_pkg, return_value=FR())
391 392
    def test_get_image_metadata(self, IG):
392 393
        for key in ('', '50m3k3y'):
393
            FR.json = dict(meta=img_recv['image']) if (
394
                key) else dict(metadata=dict(values=img_recv['image']))
394
            FR.json = dict(metadata=img_recv['image']) if (
395
                key) else dict(metadata=img_recv['image'])
395 396
            r = self.client.get_image_metadata(img_ref, key)
396 397
            self.assertEqual(IG.mock_calls[-1], call(
397 398
                '%s' % img_ref,
398
                '/meta%s' % (('/%s' % key) if key else '')))
399
                '/metadata%s' % (('/%s' % key) if key else '')))
399 400
            self.assert_dicts_are_equal(img_recv['image'], r)
400 401

  
401 402
    @patch('%s.servers_delete' % compute_pkg, return_value=FR())
......
412 413
    @patch('%s.images_put' % compute_pkg, return_value=FR())
413 414
    def test_create_image_metadata(self, IP):
414 415
        (key, val) = ('k1', 'v1')
415
        FR.json = dict(meta=img_recv['image'])
416
        FR.json = dict(metadata=img_recv['image'])
416 417
        r = self.client.create_image_metadata(img_ref, key, val)
417 418
        IP.assert_called_once_with(
418
            img_ref, 'meta/%s' % key,
419
            json_data=dict(meta={key: val}))
419
            img_ref, 'metadata/%s' % key,
420
            json_data=dict(metadata={key: val}))
420 421
        self.assert_dicts_are_equal(r, img_recv['image'])
421 422

  
422 423
    @patch('%s.images_post' % compute_pkg, return_value=FR())
......
425 426
        FR.json = dict(metadata=metadata)
426 427
        r = self.client.update_image_metadata(img_ref, **metadata)
427 428
        IP.assert_called_once_with(
428
            img_ref, 'meta',
429
            img_ref, 'metadata',
429 430
            json_data=dict(metadata=metadata))
430 431
        self.assert_dicts_are_equal(r, metadata)
431 432

  
......
433 434
    def test_delete_image_metadata(self, ID):
434 435
        key = 'metakey'
435 436
        self.client.delete_image_metadata(img_ref, key)
436
        ID.assert_called_once_with(img_ref, '/meta/%s' % key)
437
        ID.assert_called_once_with(img_ref, '/metadata/%s' % key)
437 438

  
438 439

  
439 440
if __name__ == '__main__':
b/kamaki/clients/cyclades/test.py
52 52
    suspended=False,
53 53
    progress=0,
54 54
    id=31173,
55
    metadata=dict(values=dict(os="debian", users="root"))))
56
vm_list = dict(servers=dict(values=[
55
    metadata=dict(os="debian", users="root")))
56
vm_list = dict(servers=[
57 57
    dict(name='n1', id=1),
58
    dict(name='n2', id=2)]))
58
    dict(name='n2', id=2)])
59 59
net_send = dict(network=dict(dhcp=False, name='someNet'))
60 60
net_recv = dict(network=dict(
61 61
    status="PENDING",
......
70 70
    cidr="192.168.1.0/24",
71 71
    type="MAC_FILTERED",
72 72
    gateway=None,
73
    attachments=dict(values=[dict(name='att1'), dict(name='att2')])))
74
net_list = dict(networks=dict(values=[
73
    attachments=[dict(name='att1'), dict(name='att2')]))
74
net_list = dict(networks=[
75 75
    dict(id=1, name='n1'),
76 76
    dict(id=2, name='n2'),
77
    dict(id=3, name='n3')]))
78
firewalls = dict(attachments=dict(values=[
79
    dict(firewallProfile='50m3_pr0f1L3', otherStuff='57uff')]))
77
    dict(id=3, name='n3')])
78
firewalls = dict(attachments=[
79
    dict(firewallProfile='50m3_pr0f1L3', otherStuff='57uff')])
80 80

  
81 81

  
82 82
class FR(object):
......
233 233
            self.assertEqual(SG.mock_calls[-1], call(
234 234
                command='detail' if detail else '',
235 235
                changes_since=since))
236
            expected = vm_list['servers']['values']
236
            expected = vm_list['servers']
237 237
            for i, vm in enumerate(r):
238 238
                self.assert_dicts_are_equal(vm, expected[i])
239 239
            self.assertEqual(i + 1, len(expected))
......
267 267

  
268 268
    def test_get_firewall_profile(self):
269 269
        vm_id = vm_recv['server']['id']
270
        v = firewalls['attachments']['values'][0]['firewallProfile']
270
        v = firewalls['attachments'][0]['firewallProfile']
271 271
        with patch.object(
272 272
                cyclades.CycladesClient, 'get_server_details',
273 273
                return_value=firewalls) as GSD:
......
285 285
    @patch('%s.servers_post' % cyclades_pkg, return_value=FR())
286 286
    def test_set_firewall_profile(self, SP):
287 287
        vm_id = vm_recv['server']['id']
288
        v = firewalls['attachments']['values'][0]['firewallProfile']
288
        v = firewalls['attachments'][0]['firewallProfile']
289 289
        self.client.set_firewall_profile(vm_id, v)
290 290
        SP.assert_called_once_with(
291 291
            vm_id, 'action',
......
354 354
    @patch('%s.servers_get' % cyclades_pkg, return_value=FR())
355 355
    def test_list_server_nics(self, SG):
356 356
        vm_id = vm_recv['server']['id']
357
        nics = dict(addresses=dict(values=[dict(id='nic1'), dict(id='nic2')]))
357
        nics = dict(addresses=[dict(id='nic1'), dict(id='nic2')])
358 358
        FR.json = nics
359 359
        r = self.client.list_server_nics(vm_id)
360 360
        SG.assert_called_once_with(vm_id, 'ips')
361
        expected = nics['addresses']['values']
361
        expected = nics['addresses']
362 362
        for i in range(len(r)):
363 363
            self.assert_dicts_are_equal(r[i], expected[i])
364 364
        self.assertEqual(i + 1, len(r))
......
366 366
    @patch('%s.networks_get' % cyclades_pkg, return_value=FR())
367 367
    def test_list_networks(self, NG):
368 368
        FR.json = net_list
369
        expected = net_list['networks']['values']
369
        expected = net_list['networks']
370 370
        for detail in ('', 'detail'):
371 371
            r = self.client.list_networks(detail=True if detail else False)
372 372
            self.assertEqual(NG.mock_calls[-1], call(command=detail))
......
380 380
        FR.json = net_recv
381 381
        r = self.client.list_network_nics(net_id)
382 382
        NG.assert_called_once_with(network_id=net_id)
383
        expected = net_recv['network']['attachments']['values']
383
        expected = net_recv['network']['attachments']
384 384
        for i in range(len(r)):
385 385
            self.assert_dicts_are_equal(r[i], expected[i])
386 386

  

Also available in: Unified diff