Passed
Pull Request — master (#76)
by Antonio
02:01
created

build.main   F

Complexity

Total Complexity 101

Size/Duplication

Total Lines 579
Duplicated Lines 5.87 %

Test Coverage

Coverage 36.52%

Importance

Changes 0
Metric Value
eloc 389
dl 34
loc 579
ccs 126
cts 345
cp 0.3652
rs 2
c 0
b 0
f 0
wmc 101

47 Methods

Rating   Name   Duplication   Size   Complexity  
A Main.disable_switch() 0 8 2
A Main.disable_link() 0 9 2
A Main.enable_switch() 0 8 2
A Main.enable_interface() 17 17 3
A Main.add_switch_metadata() 0 12 2
A Main.get_interfaces() 0 10 3
A Main.delete_interface_metadata() 0 21 4
A Main.add_interface_metadata() 0 20 3
A Main.get_switches() 0 4 1
A Main.get_interface_metadata() 0 16 3
A Main.shutdown() 0 3 1
A Main.delete_link_metadata() 0 13 3
A Main.enable_link() 0 9 2
A Main.execute() 0 2 1
A Main.get_switch_metadata() 0 8 2
A Main.disable_interface() 17 17 3
A Main.get_links() 0 7 1
A Main.get_topology() 0 7 1
A Main.add_link_metadata() 0 12 2
A Main.delete_switch_metadata() 0 11 2
A Main.get_link_metadata() 0 7 2
A Main.verify_storehouse() 0 8 1
A Main.load_from_store() 0 8 2
A Main.request_retrieve_entities() 0 18 2
A Main._get_switches_dict() 0 4 1
B Main.update_instance_metadata() 0 19 8
A Main.setup() 0 9 1
A Main.update_instance() 0 8 2
A Main.save_metadata_on_store() 0 25 4
A Main._get_links_dict() 0 4 1
A Main._get_link_or_create() 0 9 3
A Main._get_link_from_interface() 0 6 3
A Main._get_topology_dict() 0 4 1
A Main.notify_port_created() 0 6 1
A Main.handle_interface_down() 0 9 1
A Main.notify_link_status_change() 0 9 2
B Main.handle_interface_link_up() 0 28 7
A Main.handle_interface_created() 0 4 1
A Main.notify_topology_update() 0 6 1
A Main.handle_new_switch() 0 12 1
A Main.handle_connection_lost() 0 12 2
A Main.handle_interface_link_down() 0 13 3
A Main.add_links() 0 14 1
A Main.handle_interface_up() 0 9 1
A Main.handle_interface_deleted() 0 4 1
A Main.notify_metadata_changes() 0 17 4
A Main._get_topology() 0 3 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complexity

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like build.main often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
"""Main module of kytos/topology Kytos Network Application.
2
3
Manage the network topology
4
"""
5 1
import time
6
7 1
from flask import jsonify, request
8
9 1
from kytos.core import KytosEvent, KytosNApp, log, rest
10 1
from kytos.core.helpers import listen_to
11 1
from kytos.core.interface import Interface
12 1
from kytos.core.link import Link
13 1
from kytos.core.switch import Switch
14 1
from napps.kytos.topology import settings
15 1
from napps.kytos.topology.models import Topology
16
17
18 1
class Main(KytosNApp):  # pylint: disable=too-many-public-methods
19
    """Main class of kytos/topology NApp.
20
21
    This class is the entry point for this napp.
22
    """
23
24 1
    def setup(self):
25
        """Initialize the NApp's links list."""
26 1
        self.links = {}
27 1
        self.store_items = {}
28 1
        self.link_up_timer = getattr(settings, 'LINK_UP_TIMER', 10)
29
30 1
        self.verify_storehouse('switches')
31 1
        self.verify_storehouse('interfaces')
32 1
        self.verify_storehouse('links')
33
34 1
    def execute(self):
35
        """Do nothing."""
36
37 1
    def shutdown(self):
38
        """Do nothing."""
39
        log.info('NApp kytos/topology shutting down.')
40
41 1
    def _get_link_or_create(self, endpoint_a, endpoint_b):
42
        new_link = Link(endpoint_a, endpoint_b)
43
44
        for link in self.links.values():
45
            if new_link == link:
46
                return link
47
48
        self.links[new_link.id] = new_link
49
        return new_link
50
51 1
    def _get_switches_dict(self):
52
        """Return a dictionary with the known switches."""
53
        return {'switches': {s.id: s.as_dict() for s in
54
                             self.controller.switches.values()}}
55
56 1
    def _get_links_dict(self):
57
        """Return a dictionary with the known links."""
58
        return {'links': {l.id: l.as_dict() for l in
59
                          self.links.values()}}
60
61 1
    def _get_topology_dict(self):
62
        """Return a dictionary with the known topology."""
63
        return {'topology': {**self._get_switches_dict(),
64
                             **self._get_links_dict()}}
65
66 1
    def _get_topology(self):
67
        """Return an object representing the topology."""
68 1
        return Topology(self.controller.switches, self.links)
69
70 1
    def _get_link_from_interface(self, interface):
71
        """Return the link of the interface, or None if it does not exist."""
72
        for link in self.links.values():
73
            if interface in (link.endpoint_a, link.endpoint_b):
74
                return link
75
        return None
76
77 1
    @rest('v3/')
78
    def get_topology(self):
79
        """Return the latest known topology.
80
81
        This topology is updated when there are network events.
82
        """
83
        return jsonify(self._get_topology_dict())
84
85
    # Switch related methods
86 1
    @rest('v3/switches')
87
    def get_switches(self):
88
        """Return a json with all the switches in the topology."""
89
        return jsonify(self._get_switches_dict())
90
91 1
    @rest('v3/switches/<dpid>/enable', methods=['POST'])
92
    def enable_switch(self, dpid):
93
        """Administratively enable a switch in the topology."""
94
        try:
95
            self.controller.switches[dpid].enable()
96
            return jsonify("Operation successful"), 201
97
        except KeyError:
98
            return jsonify("Switch not found"), 404
99
100 1
    @rest('v3/switches/<dpid>/disable', methods=['POST'])
101
    def disable_switch(self, dpid):
102
        """Administratively disable a switch in the topology."""
103
        try:
104
            self.controller.switches[dpid].disable()
105
            return jsonify("Operation successful"), 201
106
        except KeyError:
107
            return jsonify("Switch not found"), 404
108
109 1
    @rest('v3/switches/<dpid>/metadata')
110
    def get_switch_metadata(self, dpid):
111
        """Get metadata from a switch."""
112
        try:
113
            return jsonify({"metadata":
114
                            self.controller.switches[dpid].metadata}), 200
115
        except KeyError:
116
            return jsonify("Switch not found"), 404
117
118 1
    @rest('v3/switches/<dpid>/metadata', methods=['POST'])
119
    def add_switch_metadata(self, dpid):
120
        """Add metadata to a switch."""
121
        metadata = request.get_json()
122
        try:
123
            switch = self.controller.switches[dpid]
124
        except KeyError:
125
            return jsonify("Switch not found"), 404
126
127
        switch.extend_metadata(metadata)
128
        self.notify_metadata_changes(switch, 'added')
129
        return jsonify("Operation successful"), 201
130
131 1
    @rest('v3/switches/<dpid>/metadata/<key>', methods=['DELETE'])
132
    def delete_switch_metadata(self, dpid, key):
133
        """Delete metadata from a switch."""
134
        try:
135
            switch = self.controller.switches[dpid]
136
        except KeyError:
137
            return jsonify("Switch not found"), 404
138
139
        switch.remove_metadata(key)
140
        self.notify_metadata_changes(switch, 'removed')
141
        return jsonify("Operation successful"), 200
142
143
    # Interface related methods
144 1
    @rest('v3/interfaces')
145
    def get_interfaces(self):
146
        """Return a json with all the interfaces in the topology."""
147
        interfaces = {}
148
        switches = self._get_switches_dict()
149
        for switch in switches['switches'].values():
150
            for interface_id, interface in switch['interfaces'].items():
151
                interfaces[interface_id] = interface
152
153
        return jsonify({'interfaces': interfaces})
154
155 1 View Code Duplication
    @rest('v3/interfaces/<interface_id>/enable', methods=['POST'])
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
156
    def enable_interface(self, interface_id):
157
        """Administratively enable an interface in the topology."""
158
        switch_id = ":".join(interface_id.split(":")[:-1])
159
        interface_number = int(interface_id.split(":")[-1])
160
161
        try:
162
            switch = self.controller.switches[switch_id]
163
        except KeyError:
164
            return jsonify("Switch not found"), 404
165
166
        try:
167
            switch.interfaces[interface_number].enable()
168
        except KeyError:
169
            return jsonify("Interface not found"), 404
170
171
        return jsonify("Operation successful"), 201
172
173 1 View Code Duplication
    @rest('v3/interfaces/<interface_id>/disable', methods=['POST'])
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
174
    def disable_interface(self, interface_id):
175
        """Administratively disable an interface in the topology."""
176
        switch_id = ":".join(interface_id.split(":")[:-1])
177
        interface_number = int(interface_id.split(":")[-1])
178
179
        try:
180
            switch = self.controller.switches[switch_id]
181
        except KeyError:
182
            return jsonify("Switch not found"), 404
183
184
        try:
185
            switch.interfaces[interface_number].disable()
186
        except KeyError:
187
            return jsonify("Interface not found"), 404
188
189
        return jsonify("Operation successful"), 201
190
191 1
    @rest('v3/interfaces/<interface_id>/metadata')
192
    def get_interface_metadata(self, interface_id):
193
        """Get metadata from an interface."""
194
        switch_id = ":".join(interface_id.split(":")[:-1])
195
        interface_number = int(interface_id.split(":")[-1])
196
        try:
197
            switch = self.controller.switches[switch_id]
198
        except KeyError:
199
            return jsonify("Switch not found"), 404
200
201
        try:
202
            interface = switch.interfaces[interface_number]
203
        except KeyError:
204
            return jsonify("Interface not found"), 404
205
206
        return jsonify({"metadata": interface.metadata}), 200
207
208 1
    @rest('v3/interfaces/<interface_id>/metadata', methods=['POST'])
209
    def add_interface_metadata(self, interface_id):
210
        """Add metadata to an interface."""
211
        metadata = request.get_json()
212
213
        switch_id = ":".join(interface_id.split(":")[:-1])
214
        interface_number = int(interface_id.split(":")[-1])
215
        try:
216
            switch = self.controller.switches[switch_id]
217
        except KeyError:
218
            return jsonify("Switch not found"), 404
219
220
        try:
221
            interface = switch.interfaces[interface_number]
222
        except KeyError:
223
            return jsonify("Interface not found"), 404
224
225
        interface.extend_metadata(metadata)
226
        self.notify_metadata_changes(interface, 'added')
227
        return jsonify("Operation successful"), 201
228
229 1
    @rest('v3/interfaces/<interface_id>/metadata/<key>', methods=['DELETE'])
230
    def delete_interface_metadata(self, interface_id, key):
231
        """Delete metadata from an interface."""
232
        switch_id = ":".join(interface_id.split(":")[:-1])
233
        interface_number = int(interface_id.split(":")[-1])
234
235
        try:
236
            switch = self.controller.switches[switch_id]
237
        except KeyError:
238
            return jsonify("Switch not found"), 404
239
240
        try:
241
            interface = switch.interfaces[interface_number]
242
        except KeyError:
243
            return jsonify("Interface not found"), 404
244
245
        if interface.remove_metadata(key) is False:
246
            return jsonify("Metadata not found"), 404
247
248
        self.notify_metadata_changes(interface, 'removed')
249
        return jsonify("Operation successful"), 200
250
251
    # Link related methods
252 1
    @rest('v3/links')
253
    def get_links(self):
254
        """Return a json with all the links in the topology.
255
256
        Links are connections between interfaces.
257
        """
258
        return jsonify(self._get_links_dict()), 200
259
260 1
    @rest('v3/links/<link_id>/enable', methods=['POST'])
261
    def enable_link(self, link_id):
262
        """Administratively enable a link in the topology."""
263
        try:
264
            self.links[link_id].enable()
265
        except KeyError:
266
            return jsonify("Link not found"), 404
267
268
        return jsonify("Operation successful"), 201
269
270 1
    @rest('v3/links/<link_id>/disable', methods=['POST'])
271
    def disable_link(self, link_id):
272
        """Administratively disable a link in the topology."""
273
        try:
274
            self.links[link_id].disable()
275
        except KeyError:
276
            return jsonify("Link not found"), 404
277
278
        return jsonify("Operation successful"), 201
279
280 1
    @rest('v3/links/<link_id>/metadata')
281
    def get_link_metadata(self, link_id):
282
        """Get metadata from a link."""
283
        try:
284
            return jsonify({"metadata": self.links[link_id].metadata}), 200
285
        except KeyError:
286
            return jsonify("Link not found"), 404
287
288 1
    @rest('v3/links/<link_id>/metadata', methods=['POST'])
289
    def add_link_metadata(self, link_id):
290
        """Add metadata to a link."""
291
        metadata = request.get_json()
292
        try:
293
            link = self.links[link_id]
294
        except KeyError:
295
            return jsonify("Link not found"), 404
296
297
        link.extend_metadata(metadata)
298
        self.notify_metadata_changes(link, 'added')
299
        return jsonify("Operation successful"), 201
300
301 1
    @rest('v3/links/<link_id>/metadata/<key>', methods=['DELETE'])
302
    def delete_link_metadata(self, link_id, key):
303
        """Delete metadata from a link."""
304
        try:
305
            link = self.links[link_id]
306
        except KeyError:
307
            return jsonify("Link not found"), 404
308
309
        if link.remove_metadata(key) is False:
310
            return jsonify("Metadata not found"), 404
311
312
        self.notify_metadata_changes(link, 'removed')
313
        return jsonify("Operation successful"), 200
314
315 1
    @listen_to('.*.switch.(new|reconnected)')
316
    def handle_new_switch(self, event):
317
        """Create a new Device on the Topology.
318
319
        Handle the event of a new created switch and update the topology with
320
        this new device.
321
        """
322 1
        switch = event.content['switch']
323 1
        switch.activate()
324 1
        log.debug('Switch %s added to the Topology.', switch.id)
325 1
        self.notify_topology_update()
326 1
        self.update_instance_metadata(switch)
327
328 1
    @listen_to('.*.connection.lost')
329
    def handle_connection_lost(self, event):
330
        """Remove a Device from the topology.
331
332
        Remove the disconnected Device and every link that has one of its
333
        interfaces.
334
        """
335 1
        switch = event.content['source'].switch
336 1
        if switch:
337 1
            switch.deactivate()
338 1
            log.debug('Switch %s removed from the Topology.', switch.id)
339 1
            self.notify_topology_update()
340
341 1
    def handle_interface_up(self, event):
342
        """Update the topology based on a Port Modify event.
343
344
        The event notifies that an interface was changed to 'up'.
345
        """
346 1
        interface = event.content['interface']
347 1
        interface.activate()
348 1
        self.notify_topology_update()
349 1
        self.update_instance_metadata(interface)
350
351 1
    @listen_to('.*.switch.interface.created')
352
    def handle_interface_created(self, event):
353
        """Update the topology based on a Port Create event."""
354 1
        self.handle_interface_up(event)
355
356 1
    def handle_interface_down(self, event):
357
        """Update the topology based on a Port Modify event.
358
359
        The event notifies that an interface was changed to 'down'.
360
        """
361 1
        interface = event.content['interface']
362 1
        interface.deactivate()
363 1
        self.handle_interface_link_down(event)
364 1
        self.notify_topology_update()
365
366 1
    @listen_to('.*.switch.interface.deleted')
367
    def handle_interface_deleted(self, event):
368
        """Update the topology based on a Port Delete event."""
369 1
        self.handle_interface_down(event)
370
371 1
    @listen_to('.*.switch.interface.link_up')
372
    def handle_interface_link_up(self, event):
373
        """Update the topology based on a Port Modify event.
374
375
        The event notifies that an interface's link was changed to 'up'.
376
        """
377 1
        interface = event.content['interface']
378 1
        link = self._get_link_from_interface(interface)
379 1
        if not link:
380
            return
381 1
        if link.endpoint_a == interface:
382
            other_interface = link.endpoint_b
383
        else:
384
            other_interface = link.endpoint_a
385
        interface.activate()
386
        if other_interface.is_active() is False:
387
            return
388
        if link.is_active() is False:
389
            link.update_metadata('last_status_change', time.time())
390
            link.activate()
391
            time.sleep(self.link_up_timer)
392
            last_status_change = link.get_metadata('last_status_change')
393
            now = time.time()
394
            if link.is_active() and \
395
                    now - last_status_change >= self.link_up_timer:
396
                self.notify_topology_update()
397
                self.update_instance_metadata(interface.link)
398
                self.notify_link_status_change(link)
399
400 1
    @listen_to('.*.switch.interface.link_down')
401
    def handle_interface_link_down(self, event):
402
        """Update the topology based on a Port Modify event.
403
404
        The event notifies that an interface's link was changed to 'down'.
405
        """
406 1
        interface = event.content['interface']
407 1
        link = self._get_link_from_interface(interface)
408 1
        if link and link.is_active():
409 1
            link.deactivate()
410 1
            link.update_metadata('last_status_change', time.time())
411 1
            self.notify_topology_update()
412 1
            self.notify_link_status_change(link)
413
414 1
    @listen_to('.*.interface.is.nni')
415
    def add_links(self, event):
416
        """Update the topology with links related to the NNI interfaces."""
417 1
        interface_a = event.content['interface_a']
418 1
        interface_b = event.content['interface_b']
419
420 1
        link = self._get_link_or_create(interface_a, interface_b)
421 1
        interface_a.update_link(link)
422 1
        interface_b.update_link(link)
423
424 1
        interface_a.nni = True
425 1
        interface_b.nni = True
426
427 1
        self.notify_topology_update()
428
429
    # def add_host(self, event):
430
    #    """Update the topology with a new Host."""
431
432
    #    interface = event.content['port']
433
    #    mac = event.content['reachable_mac']
434
435
    #    host = Host(mac)
436
    #    link = self.topology.get_link(interface.id)
437
    #    if link is not None:
438
    #        return
439
440
    #    self.topology.add_link(interface.id, host.id)
441
    #    self.topology.add_device(host)
442
443
    #    if settings.DISPLAY_FULL_DUPLEX_LINKS:
444
    #        self.topology.add_link(host.id, interface.id)
445
446 1
    def notify_topology_update(self):
447
        """Send an event to notify about updates on the topology."""
448 1
        name = 'kytos/topology.updated'
449 1
        event = KytosEvent(name=name, content={'topology':
450
                                               self._get_topology()})
451 1
        self.controller.buffers.app.put(event)
452
453 1
    def notify_link_status_change(self, link):
454
        """Send an event to notify about a status change on a link."""
455 1
        name = 'kytos/topology.'
456 1
        if link.is_active():
457 1
            status = 'link_up'
458
        else:
459
            status = 'link_down'
460 1
        event = KytosEvent(name=name+status, content={'link': link})
461 1
        self.controller.buffers.app.put(event)
462
463 1
    def notify_metadata_changes(self, obj, action):
464
        """Send an event to notify about metadata changes."""
465 1
        if isinstance(obj, Switch):
466 1
            entity = 'switch'
467 1
            entities = 'switches'
468
        elif isinstance(obj, Interface):
469
            entity = 'interface'
470
            entities = 'interfaces'
471
        elif isinstance(obj, Link):
472
            entity = 'link'
473
            entities = 'links'
474
475 1
        name = f'kytos/topology.{entities}.metadata.{action}'
476 1
        event = KytosEvent(name=name, content={entity: obj,
0 ignored issues
show
introduced by
The variable entity does not seem to be defined for all execution paths.
Loading history...
477
                                               'metadata': obj.metadata})
478 1
        self.controller.buffers.app.put(event)
479 1
        log.debug(f'Metadata from {obj.id} was {action}.')
480
481 1
    @listen_to('.*.switch.port.created')
482
    def notify_port_created(self, original_event):
483
        """Notify when a port is created."""
484 1
        name = f'kytos/topology.port.created'
485 1
        event = KytosEvent(name=name, content=original_event.content)
486 1
        self.controller.buffers.app.put(event)
487
488 1
    @listen_to('kytos/topology.*.metadata.*')
489
    def save_metadata_on_store(self, event):
490
        """Send to storehouse the data updated."""
491
        name = 'kytos.storehouse.update'
492
        if 'switch' in event.content:
493
            store = self.store_items.get('switches')
494
            obj = event.content.get('switch')
495
            namespace = 'kytos.topology.switches.metadata'
496
        elif 'interface' in event.content:
497
            store = self.store_items.get('interfaces')
498
            obj = event.content.get('interface')
499
            namespace = 'kytos.topology.iterfaces.metadata'
500
        elif 'link' in event.content:
501
            store = self.store_items.get('links')
502
            obj = event.content.get('link')
503
            namespace = 'kytos.topology.links.metadata'
504
505
        store.data[obj.id] = obj.metadata
0 ignored issues
show
introduced by
The variable obj does not seem to be defined for all execution paths.
Loading history...
introduced by
The variable store does not seem to be defined for all execution paths.
Loading history...
506
        content = {'namespace': namespace,
0 ignored issues
show
introduced by
The variable namespace does not seem to be defined for all execution paths.
Loading history...
507
                   'box_id': store.box_id,
508
                   'data': store.data,
509
                   'callback': self.update_instance}
510
511
        event = KytosEvent(name=name, content=content)
512
        self.controller.buffers.app.put(event)
513
514 1
    @staticmethod
515
    def update_instance(event, _data, error):
516
        """Display in Kytos console if the data was updated."""
517
        entities = event.content.get('namespace', '').split('.')[-2]
518
        if error:
519
            log.error(f'Error trying to update storehouse {entities}.')
520
        else:
521
            log.debug(f'Storehouse update to entities: {entities}.')
522
523 1
    def verify_storehouse(self, entities):
524
        """Request a list of box saved by specific entity."""
525 1
        name = 'kytos.storehouse.list'
526 1
        content = {'namespace': f'kytos.topology.{entities}.metadata',
527
                   'callback': self.request_retrieve_entities}
528 1
        event = KytosEvent(name=name, content=content)
529 1
        self.controller.buffers.app.put(event)
530 1
        log.info(f'verify data in storehouse for {entities}.')
531
532 1
    def request_retrieve_entities(self, event, data, _error):
533
        """Create a box or retrieve an existent box from storehouse."""
534
        msg = ''
535
        content = {'namespace': event.content.get('namespace'),
536
                   'callback': self.load_from_store,
537
                   'data': {}}
538
539
        if not data:
540
            name = 'kytos.storehouse.create'
541
            msg = 'Create new box in storehouse'
542
        else:
543
            name = 'kytos.storehouse.retrieve'
544
            content['box_id'] = data[0]
545
            msg = 'Retrieve data from storeohouse.'
546
547
        event = KytosEvent(name=name, content=content)
548
        self.controller.buffers.app.put(event)
549
        log.debug(msg)
550
551 1
    def load_from_store(self, event, box, error):
552
        """Save the data retrived from storehouse."""
553
        entities = event.content.get('namespace', '').split('.')[-2]
554
        if error:
555
            log.error('Error while get a box from storehouse.')
556
        else:
557
            self.store_items[entities] = box
558
            log.debug('Data updated')
559
560 1
    def update_instance_metadata(self, obj):
561
        """Update object instance with saved metadata."""
562
        metadata = None
563
        if isinstance(obj, Interface):
564
            all_metadata = self.store_items.get('interfaces', None)
565
            if all_metadata:
566
                metadata = all_metadata.data.get(obj.id)
567
        elif isinstance(obj, Switch):
568
            all_metadata = self.store_items.get('switches', None)
569
            if all_metadata:
570
                metadata = all_metadata.data.get(obj.id)
571
        elif isinstance(obj, Link):
572
            all_metadata = self.store_items.get('links', None)
573
            if all_metadata:
574
                metadata = all_metadata.data.get(obj.id)
575
576
        if metadata:
577
            obj.extend_metadata(metadata)
578
            log.debug(f'Metadata to {obj.id} was updated')
579