blob: 48a7a248dd3aa23304a92ba9ef4d28dba85df997 [file] [log] [blame]
Carmelo Casconefa421582018-09-13 10:05:57 -07001/*
2 * Copyright 2015-present Open Networking Foundation
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16package org.onosproject.inbandtelemetry.impl;
17
18import com.google.common.collect.Maps;
19import com.google.common.util.concurrent.Striped;
Carmelo Casconefa421582018-09-13 10:05:57 -070020import org.onlab.util.KryoNamespace;
21import org.onlab.util.SharedScheduledExecutors;
22import org.onosproject.core.ApplicationId;
23import org.onosproject.core.CoreService;
Yi Tseng0f1ffd12020-09-18 11:10:47 -070024import org.onosproject.net.behaviour.inbandtelemetry.IntReportConfig;
Carmelo Casconedefc74e2020-07-17 15:27:02 -070025import org.onosproject.net.behaviour.inbandtelemetry.IntMetadataType;
26import org.onosproject.net.behaviour.inbandtelemetry.IntDeviceConfig;
Carmelo Casconefa421582018-09-13 10:05:57 -070027import org.onosproject.inbandtelemetry.api.IntIntent;
28import org.onosproject.inbandtelemetry.api.IntIntentId;
Carmelo Casconedefc74e2020-07-17 15:27:02 -070029import org.onosproject.net.behaviour.inbandtelemetry.IntObjective;
30import org.onosproject.net.behaviour.inbandtelemetry.IntProgrammable;
Carmelo Casconefa421582018-09-13 10:05:57 -070031import org.onosproject.inbandtelemetry.api.IntService;
32import org.onosproject.mastership.MastershipService;
33import org.onosproject.net.ConnectPoint;
34import org.onosproject.net.Device;
35import org.onosproject.net.DeviceId;
36import org.onosproject.net.MastershipRole;
37import org.onosproject.net.PortNumber;
Yi Tseng0f1ffd12020-09-18 11:10:47 -070038import org.onosproject.net.config.ConfigFactory;
39import org.onosproject.net.config.NetworkConfigEvent;
40import org.onosproject.net.config.NetworkConfigListener;
41import org.onosproject.net.config.NetworkConfigRegistry;
42import org.onosproject.net.config.NetworkConfigService;
43import org.onosproject.net.config.basics.SubjectFactories;
Carmelo Casconefa421582018-09-13 10:05:57 -070044import org.onosproject.net.device.DeviceEvent;
45import org.onosproject.net.device.DeviceListener;
46import org.onosproject.net.device.DeviceService;
47import org.onosproject.net.host.HostEvent;
48import org.onosproject.net.host.HostListener;
49import org.onosproject.net.host.HostService;
50import org.onosproject.store.serializers.KryoNamespaces;
51import org.onosproject.store.service.AtomicIdGenerator;
52import org.onosproject.store.service.AtomicValue;
53import org.onosproject.store.service.AtomicValueEvent;
54import org.onosproject.store.service.AtomicValueEventListener;
55import org.onosproject.store.service.ConsistentMap;
56import org.onosproject.store.service.MapEvent;
57import org.onosproject.store.service.MapEventListener;
58import org.onosproject.store.service.Serializer;
59import org.onosproject.store.service.StorageService;
60import org.onosproject.store.service.Versioned;
Ray Milkeydb57f1c2018-10-09 10:39:29 -070061import org.osgi.service.component.annotations.Activate;
62import org.osgi.service.component.annotations.Component;
63import org.osgi.service.component.annotations.Deactivate;
64import org.osgi.service.component.annotations.Reference;
65import org.osgi.service.component.annotations.ReferenceCardinality;
Carmelo Casconefa421582018-09-13 10:05:57 -070066import org.slf4j.Logger;
67
68import java.util.Collection;
69import java.util.Map;
70import java.util.Optional;
71import java.util.Set;
72import java.util.concurrent.ConcurrentMap;
73import java.util.concurrent.ExecutionException;
74import java.util.concurrent.ScheduledFuture;
75import java.util.concurrent.TimeUnit;
76import java.util.concurrent.TimeoutException;
77import java.util.concurrent.locks.Lock;
78import java.util.stream.Collectors;
79
80import static com.google.common.base.Preconditions.checkNotNull;
81import static org.slf4j.LoggerFactory.getLogger;
82
83/**
84 * Simple implementation of IntService, for controlling INT-capable pipelines.
85 * <p>
86 * All INT intents are converted to an equivalent INT objective and applied to
87 * all SOURCE_SINK devices. A device is deemed SOURCE_SINK if it has at least
88 * one host attached.
89 * <p>
90 * The implementation listens for different types of events and when required it
91 * configures a device by cleaning-up any previous state and applying the new
92 * one.
93 */
Ray Milkeydb57f1c2018-10-09 10:39:29 -070094@Component(immediate = true, service = IntService.class)
Carmelo Casconefa421582018-09-13 10:05:57 -070095public class SimpleIntManager implements IntService {
96
97 private final Logger log = getLogger(getClass());
98
99 private static final int CONFIG_EVENT_DELAY = 5; // Seconds.
100
101 private static final String APP_NAME = "org.onosproject.inbandtelemetry";
102
Ray Milkeydb57f1c2018-10-09 10:39:29 -0700103 @Reference(cardinality = ReferenceCardinality.MANDATORY)
Carmelo Casconefa421582018-09-13 10:05:57 -0700104 private CoreService coreService;
105
Ray Milkeydb57f1c2018-10-09 10:39:29 -0700106 @Reference(cardinality = ReferenceCardinality.MANDATORY)
Carmelo Casconefa421582018-09-13 10:05:57 -0700107 private DeviceService deviceService;
108
Ray Milkeydb57f1c2018-10-09 10:39:29 -0700109 @Reference(cardinality = ReferenceCardinality.MANDATORY)
Carmelo Casconefa421582018-09-13 10:05:57 -0700110 private StorageService storageService;
111
Ray Milkeydb57f1c2018-10-09 10:39:29 -0700112 @Reference(cardinality = ReferenceCardinality.MANDATORY)
Carmelo Casconefa421582018-09-13 10:05:57 -0700113 private MastershipService mastershipService;
114
Ray Milkeydb57f1c2018-10-09 10:39:29 -0700115 @Reference(cardinality = ReferenceCardinality.MANDATORY)
Carmelo Casconefa421582018-09-13 10:05:57 -0700116 private HostService hostService;
117
Yi Tseng0f1ffd12020-09-18 11:10:47 -0700118 @Reference(cardinality = ReferenceCardinality.MANDATORY)
119 private NetworkConfigService netcfgService;
120
121 @Reference(cardinality = ReferenceCardinality.MANDATORY)
122 private NetworkConfigRegistry netcfgRegistry;
123
Carmelo Casconefa421582018-09-13 10:05:57 -0700124 private final Striped<Lock> deviceLocks = Striped.lock(10);
125
126 private final ConcurrentMap<DeviceId, ScheduledFuture<?>> scheduledDeviceTasks = Maps.newConcurrentMap();
127
128 // Distributed state.
129 private ConsistentMap<IntIntentId, IntIntent> intentMap;
130 private ConsistentMap<DeviceId, Long> devicesToConfigure;
Carmelo Casconedefc74e2020-07-17 15:27:02 -0700131 private AtomicValue<IntDeviceConfig> intConfig;
Carmelo Casconefa421582018-09-13 10:05:57 -0700132 private AtomicValue<Boolean> intStarted;
133 private AtomicIdGenerator intentIds;
134
135 // Event listeners.
136 private final InternalHostListener hostListener = new InternalHostListener();
137 private final InternalDeviceListener deviceListener = new InternalDeviceListener();
138 private final InternalIntentMapListener intentMapListener = new InternalIntentMapListener();
139 private final InternalIntConfigListener intConfigListener = new InternalIntConfigListener();
140 private final InternalIntStartedListener intStartedListener = new InternalIntStartedListener();
141 private final InternalDeviceToConfigureListener devicesToConfigureListener =
142 new InternalDeviceToConfigureListener();
Yi Tseng0f1ffd12020-09-18 11:10:47 -0700143 private final NetworkConfigListener appConfigListener = new IntAppConfigListener();
144
145 private final ConfigFactory<ApplicationId, IntReportConfig> intAppConfigFactory =
146 new ConfigFactory<>(SubjectFactories.APP_SUBJECT_FACTORY,
147 IntReportConfig.class, "report") {
148 @Override
149 public IntReportConfig createConfig() {
150 return new IntReportConfig();
151 }
152 };
Carmelo Casconefa421582018-09-13 10:05:57 -0700153
154 @Activate
155 public void activate() {
156
157 final ApplicationId appId = coreService.registerApplication(APP_NAME);
158
159 KryoNamespace.Builder serializer = KryoNamespace.newBuilder()
160 .register(KryoNamespaces.API)
161 .register(IntIntent.class)
162 .register(IntIntentId.class)
163 .register(IntDeviceRole.class)
164 .register(IntIntent.IntHeaderType.class)
Carmelo Casconedefc74e2020-07-17 15:27:02 -0700165 .register(IntMetadataType.class)
Carmelo Casconefa421582018-09-13 10:05:57 -0700166 .register(IntIntent.IntReportType.class)
167 .register(IntIntent.TelemetryMode.class)
Carmelo Casconedefc74e2020-07-17 15:27:02 -0700168 .register(IntDeviceConfig.class)
169 .register(IntDeviceConfig.TelemetrySpec.class);
Carmelo Casconefa421582018-09-13 10:05:57 -0700170
171 devicesToConfigure = storageService.<DeviceId, Long>consistentMapBuilder()
172 .withSerializer(Serializer.using(serializer.build()))
173 .withName("onos-int-devices-to-configure")
174 .withApplicationId(appId)
175 .withPurgeOnUninstall()
176 .build();
177 devicesToConfigure.addListener(devicesToConfigureListener);
178
179 intentMap = storageService.<IntIntentId, IntIntent>consistentMapBuilder()
180 .withSerializer(Serializer.using(serializer.build()))
181 .withName("onos-int-intents")
182 .withApplicationId(appId)
183 .withPurgeOnUninstall()
184 .build();
185 intentMap.addListener(intentMapListener);
186
187 intStarted = storageService.<Boolean>atomicValueBuilder()
188 .withSerializer(Serializer.using(serializer.build()))
189 .withName("onos-int-started")
190 .withApplicationId(appId)
191 .build()
192 .asAtomicValue();
193 intStarted.addListener(intStartedListener);
194
Carmelo Casconedefc74e2020-07-17 15:27:02 -0700195 intConfig = storageService.<IntDeviceConfig>atomicValueBuilder()
Carmelo Casconefa421582018-09-13 10:05:57 -0700196 .withSerializer(Serializer.using(serializer.build()))
197 .withName("onos-int-config")
198 .withApplicationId(appId)
199 .build()
200 .asAtomicValue();
201 intConfig.addListener(intConfigListener);
202
203 intentIds = storageService.getAtomicIdGenerator("int-intent-id-generator");
204
205 // Bootstrap config for already existing devices.
206 triggerAllDeviceConfigure();
207
208 hostService.addListener(hostListener);
209 deviceService.addListener(deviceListener);
210
Yi Tseng0f1ffd12020-09-18 11:10:47 -0700211 netcfgRegistry.registerConfigFactory(intAppConfigFactory);
212 netcfgService.addListener(appConfigListener);
213
Carmelo Casconefa421582018-09-13 10:05:57 -0700214 startInt();
215 log.info("Started", appId.id());
216 }
217
218 @Deactivate
219 public void deactivate() {
220 deviceService.removeListener(deviceListener);
221 hostService.removeListener(hostListener);
222 intentIds = null;
223 intConfig.removeListener(intConfigListener);
224 intConfig = null;
225 intStarted.removeListener(intStartedListener);
226 intStarted = null;
227 intentMap.removeListener(intentMapListener);
228 intentMap = null;
229 devicesToConfigure.removeListener(devicesToConfigureListener);
230 devicesToConfigure.destroy();
231 devicesToConfigure = null;
232 // Cancel tasks (if any).
233 scheduledDeviceTasks.values().forEach(f -> {
234 f.cancel(true);
235 if (!f.isDone()) {
236 try {
237 f.get(1, TimeUnit.SECONDS);
238 } catch (InterruptedException | ExecutionException | TimeoutException e) {
239 // Don't care, we are terminating the service anyways.
240 }
241 }
242 });
243 // Clean up INT rules from existing devices.
244 deviceService.getDevices().forEach(d -> cleanupDevice(d.id()));
245 log.info("Deactivated");
246 }
247
248 @Override
249 public void startInt() {
250 // Atomic value event will trigger device configure.
251 intStarted.set(true);
252 }
253
254 @Override
255 public void startInt(Set<DeviceId> deviceIds) {
256 log.warn("Starting INT for a subset of devices is not supported");
257 }
258
259 @Override
260 public void stopInt() {
261 // Atomic value event will trigger device configure.
262 intStarted.set(false);
263 }
264
265 @Override
266 public void stopInt(Set<DeviceId> deviceIds) {
267 log.warn("Stopping INT for a subset of devices is not supported");
268 }
269
270 @Override
Carmelo Casconedefc74e2020-07-17 15:27:02 -0700271 public void setConfig(IntDeviceConfig cfg) {
Carmelo Casconefa421582018-09-13 10:05:57 -0700272 checkNotNull(cfg);
273 // Atomic value event will trigger device configure.
274 intConfig.set(cfg);
275 }
276
277 @Override
Carmelo Casconedefc74e2020-07-17 15:27:02 -0700278 public IntDeviceConfig getConfig() {
Carmelo Casconefa421582018-09-13 10:05:57 -0700279 return intConfig.get();
280 }
281
282 @Override
283 public IntIntentId installIntIntent(IntIntent intent) {
284 checkNotNull(intent);
285 final Integer intentId = (int) intentIds.nextId();
286 final IntIntentId intIntentId = IntIntentId.valueOf(intentId);
287 // Intent map event will trigger device configure.
288 intentMap.put(intIntentId, intent);
289 return intIntentId;
290 }
291
292 @Override
293 public void removeIntIntent(IntIntentId intentId) {
294 checkNotNull(intentId);
295 // Intent map event will trigger device configure.
296 intentMap.remove(intentId).value();
297 }
298
299 @Override
300 public IntIntent getIntIntent(IntIntentId intentId) {
301 return Optional.ofNullable(intentMap.get(intentId).value()).orElse(null);
302 }
303
304 @Override
305 public Map<IntIntentId, IntIntent> getIntIntents() {
306 return intentMap.asJavaMap();
307 }
308
309 private boolean isConfigTaskValid(DeviceId deviceId, long creationTime) {
310 Versioned<?> versioned = devicesToConfigure.get(deviceId);
311 return versioned != null && versioned.creationTime() == creationTime;
312 }
313
314 private boolean isIntStarted() {
315 return intStarted.get();
316 }
317
318 private boolean isNotIntConfigured() {
319 return intConfig.get() == null;
320 }
321
322 private boolean isIntProgrammable(DeviceId deviceId) {
323 final Device device = deviceService.getDevice(deviceId);
324 return device != null && device.is(IntProgrammable.class);
325 }
326
327 private void triggerDeviceConfigure(DeviceId deviceId) {
328 if (isIntProgrammable(deviceId)) {
329 devicesToConfigure.put(deviceId, System.nanoTime());
330 }
331 }
332
333 private void triggerAllDeviceConfigure() {
334 deviceService.getDevices().forEach(d -> triggerDeviceConfigure(d.id()));
335 }
336
337 private void configDeviceTask(DeviceId deviceId, long creationTime) {
338 if (isConfigTaskValid(deviceId, creationTime)) {
339 // Task outdated.
340 return;
341 }
342 if (!deviceService.isAvailable(deviceId)) {
343 return;
344 }
345 final MastershipRole role = mastershipService.requestRoleForSync(deviceId);
346 if (!role.equals(MastershipRole.MASTER)) {
347 return;
348 }
349 deviceLocks.get(deviceId).lock();
350 try {
351 // Clean up first.
352 cleanupDevice(deviceId);
353 if (!configDevice(deviceId)) {
354 // Clean up if fails.
355 cleanupDevice(deviceId);
356 return;
357 }
358 devicesToConfigure.remove(deviceId);
359 } finally {
360 deviceLocks.get(deviceId).unlock();
361 }
362 }
363
364 private void cleanupDevice(DeviceId deviceId) {
365 final Device device = deviceService.getDevice(deviceId);
366 if (device == null || !device.is(IntProgrammable.class)) {
367 return;
368 }
369 device.as(IntProgrammable.class).cleanup();
370 }
371
372 private boolean configDevice(DeviceId deviceId) {
373 // Returns true if config was successful, false if not and a clean up is
374 // needed.
375 final Device device = deviceService.getDevice(deviceId);
376 if (device == null || !device.is(IntProgrammable.class)) {
377 return true;
378 }
379
380 if (isNotIntConfigured()) {
381 log.warn("Missing INT config, aborting programming of INT device {}", deviceId);
382 return true;
383 }
384
385 final boolean isEdge = !hostService.getConnectedHosts(deviceId).isEmpty();
386 final IntDeviceRole intDeviceRole = isEdge
387 ? IntDeviceRole.SOURCE_SINK
388 : IntDeviceRole.TRANSIT;
389
390 log.info("Started programming of INT device {} with role {}...",
391 deviceId, intDeviceRole);
392
393 final IntProgrammable intProg = device.as(IntProgrammable.class);
394
395 if (!isIntStarted()) {
396 // Leave device with no INT configuration.
397 return true;
398 }
399
400 if (!intProg.init()) {
401 log.warn("Unable to init INT pipeline on {}", deviceId);
402 return false;
403 }
404
405 if (intDeviceRole != IntDeviceRole.SOURCE_SINK) {
406 // Stop here, no more configuration needed for transit devices.
407 return true;
408 }
409
410 if (intProg.supportsFunctionality(IntProgrammable.IntFunctionality.SINK)) {
411 if (!intProg.setupIntConfig(intConfig.get())) {
412 log.warn("Unable to apply INT report config on {}", deviceId);
413 return false;
414 }
415 }
416
417 // Port configuration.
418 final Set<PortNumber> hostPorts = deviceService.getPorts(deviceId)
419 .stream()
420 .map(port -> new ConnectPoint(deviceId, port.number()))
421 .filter(cp -> !hostService.getConnectedHosts(cp).isEmpty())
422 .map(ConnectPoint::port)
423 .collect(Collectors.toSet());
424
425 for (PortNumber port : hostPorts) {
426 if (intProg.supportsFunctionality(IntProgrammable.IntFunctionality.SOURCE)) {
427 log.info("Setting port {}/{} as INT source port...", deviceId, port);
428 if (!intProg.setSourcePort(port)) {
429 log.warn("Unable to set INT source port {} on {}", port, deviceId);
430 return false;
431 }
432 }
433 if (intProg.supportsFunctionality(IntProgrammable.IntFunctionality.SINK)) {
434 log.info("Setting port {}/{} as INT sink port...", deviceId, port);
435 if (!intProg.setSinkPort(port)) {
436 log.warn("Unable to set INT sink port {} on {}", port, deviceId);
437 return false;
438 }
439 }
440 }
441
442 if (!intProg.supportsFunctionality(IntProgrammable.IntFunctionality.SOURCE)) {
443 // Stop here, no more configuration needed for sink devices.
444 return true;
445 }
446
447 // Apply intents.
448 // This is a trivial implementation where we simply get the
449 // corresponding INT objective from an intent and we apply to all source
450 // device.
451 final Collection<IntObjective> objectives = intentMap.values().stream()
452 .map(v -> getIntObjective(v.value()))
453 .collect(Collectors.toList());
454 int appliedCount = 0;
455 for (IntObjective objective : objectives) {
456 if (intProg.addIntObjective(objective)) {
457 appliedCount = appliedCount + 1;
458 }
459 }
460
461 log.info("Completed programming of {}, applied {} INT objectives of {} total",
462 deviceId, appliedCount, objectives.size());
463
464 return true;
465 }
466
467 private IntObjective getIntObjective(IntIntent intent) {
Carmelo Casconedefc74e2020-07-17 15:27:02 -0700468 // FIXME: we are ignore intent.headerType()
469 // what should we do with it?
Carmelo Casconefa421582018-09-13 10:05:57 -0700470 return new IntObjective.Builder()
471 .withSelector(intent.selector())
472 .withMetadataTypes(intent.metadataTypes())
Carmelo Casconefa421582018-09-13 10:05:57 -0700473 .build();
474 }
475
476 /* Event listeners which trigger device configuration. */
477
478 private class InternalHostListener implements HostListener {
479 @Override
480 public void event(HostEvent event) {
481 final DeviceId deviceId = event.subject().location().deviceId();
482 triggerDeviceConfigure(deviceId);
483 }
484 }
485
486 private class InternalDeviceListener implements DeviceListener {
487 @Override
488 public void event(DeviceEvent event) {
489 switch (event.type()) {
490 case DEVICE_ADDED:
491 case DEVICE_UPDATED:
492 case DEVICE_REMOVED:
493 case DEVICE_SUSPENDED:
494 case DEVICE_AVAILABILITY_CHANGED:
495 case PORT_ADDED:
496 case PORT_UPDATED:
497 case PORT_REMOVED:
498 triggerDeviceConfigure(event.subject().id());
499 return;
500 case PORT_STATS_UPDATED:
501 return;
502 default:
503 log.warn("Unknown device event type {}", event.type());
504 }
505 }
506 }
507
508 private class InternalIntentMapListener
509 implements MapEventListener<IntIntentId, IntIntent> {
510 @Override
511 public void event(MapEvent<IntIntentId, IntIntent> event) {
512 triggerAllDeviceConfigure();
513 }
514 }
515
516 private class InternalIntConfigListener
Carmelo Casconedefc74e2020-07-17 15:27:02 -0700517 implements AtomicValueEventListener<IntDeviceConfig> {
Carmelo Casconefa421582018-09-13 10:05:57 -0700518 @Override
Carmelo Casconedefc74e2020-07-17 15:27:02 -0700519 public void event(AtomicValueEvent<IntDeviceConfig> event) {
Carmelo Casconefa421582018-09-13 10:05:57 -0700520 triggerAllDeviceConfigure();
521 }
522 }
523
524 private class InternalIntStartedListener
525 implements AtomicValueEventListener<Boolean> {
526 @Override
527 public void event(AtomicValueEvent<Boolean> event) {
528 triggerAllDeviceConfigure();
529 }
530 }
531
532 private class InternalDeviceToConfigureListener
533 implements MapEventListener<DeviceId, Long> {
534 @Override
535 public void event(MapEvent<DeviceId, Long> event) {
536 if (event.type().equals(MapEvent.Type.REMOVE) ||
537 event.newValue() == null) {
538 return;
539 }
540 // Schedule task in the future. Wait for events for this device to
541 // stabilize.
542 final DeviceId deviceId = event.key();
543 final long creationTime = event.newValue().creationTime();
544 ScheduledFuture<?> newTask = SharedScheduledExecutors.newTimeout(
545 () -> configDeviceTask(deviceId, creationTime),
546 CONFIG_EVENT_DELAY, TimeUnit.SECONDS);
547 ScheduledFuture<?> oldTask = scheduledDeviceTasks.put(deviceId, newTask);
548 if (oldTask != null) {
549 oldTask.cancel(false);
550 }
551 }
552 }
Yi Tseng0f1ffd12020-09-18 11:10:47 -0700553
554 private class IntAppConfigListener implements NetworkConfigListener {
555
556 @Override
557 public void event(NetworkConfigEvent event) {
558 switch (event.type()) {
559 case CONFIG_ADDED:
560 case CONFIG_UPDATED:
561 event.config()
562 .map(config -> (IntReportConfig) config)
563 .ifPresent(config -> {
564 IntDeviceConfig intDeviceConfig = IntDeviceConfig.builder()
565 .withMinFlowHopLatencyChangeNs(config.minFlowHopLatencyChangeNs())
566 .withCollectorPort(config.collectorPort())
567 .withCollectorIp(config.collectorIp())
568 .enabled(true)
569 .build();
570 setConfig(intDeviceConfig);
571 });
572 break;
573 // TODO: Support removing INT config.
574 default:
575 break;
576 }
577 }
578
579 @Override
580 public boolean isRelevant(NetworkConfigEvent event) {
581 return event.configClass() == IntReportConfig.class;
582 }
583 }
Carmelo Casconefa421582018-09-13 10:05:57 -0700584}