blob: 54f454eb40721ff0ed84edaf19de507a97891508 [file] [log] [blame]
Boyoung Jeong9e8faec2018-06-17 21:19:23 +09001/*
2 * Copyright 2018-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.openstacktelemetry.impl;
17
Jian Li0bbbb1c2018-06-22 22:01:17 +090018import com.google.common.collect.Sets;
Boyoung Jeong9e8faec2018-06-17 21:19:23 +090019import org.apache.commons.lang3.exception.ExceptionUtils;
20import org.apache.felix.scr.annotations.Activate;
21import org.apache.felix.scr.annotations.Component;
22import org.apache.felix.scr.annotations.Deactivate;
23import org.apache.felix.scr.annotations.Reference;
24import org.apache.felix.scr.annotations.ReferenceCardinality;
25import org.apache.felix.scr.annotations.Service;
Boyoung Jeong9e8faec2018-06-17 21:19:23 +090026import org.onlab.packet.IpAddress;
27import org.onlab.packet.MacAddress;
28import org.onlab.packet.VlanId;
29import org.onosproject.core.ApplicationId;
30import org.onosproject.core.CoreService;
31import org.onosproject.net.Device;
32import org.onosproject.net.DeviceId;
33import org.onosproject.net.Host;
34import org.onosproject.net.device.DeviceService;
35import org.onosproject.net.flow.DefaultFlowRule;
36import org.onosproject.net.flow.DefaultTrafficSelector;
37import org.onosproject.net.flow.DefaultTrafficTreatment;
38import org.onosproject.net.flow.FlowEntry;
39import org.onosproject.net.flow.FlowRule;
40import org.onosproject.net.flow.FlowRuleOperations;
41import org.onosproject.net.flow.FlowRuleOperationsContext;
42import org.onosproject.net.flow.FlowRuleService;
43import org.onosproject.net.flow.IndexTableId;
44import org.onosproject.net.flow.TrafficSelector;
45import org.onosproject.net.flow.TrafficTreatment;
Boyoung Jeong9e8faec2018-06-17 21:19:23 +090046import org.onosproject.net.flow.criteria.IPCriterion;
47import org.onosproject.net.flow.criteria.IPProtocolCriterion;
48import org.onosproject.net.flow.criteria.TcpPortCriterion;
49import org.onosproject.net.flow.criteria.UdpPortCriterion;
50import org.onosproject.net.host.HostService;
51import org.onosproject.openstacktelemetry.api.FlowInfo;
Jian Li0bbbb1c2018-06-22 22:01:17 +090052import org.onosproject.openstacktelemetry.api.OpenstackTelemetryService;
Boyoung Jeong9e8faec2018-06-17 21:19:23 +090053import org.onosproject.openstacktelemetry.api.StatsFlowRule;
54import org.onosproject.openstacktelemetry.api.StatsFlowRuleAdminService;
55import org.onosproject.openstacktelemetry.api.StatsInfo;
56import org.slf4j.Logger;
57import org.slf4j.LoggerFactory;
58
Boyoung Jeong9e8faec2018-06-17 21:19:23 +090059import java.util.Set;
60import java.util.Timer;
61import java.util.TimerTask;
62
Jian Li0bbbb1c2018-06-22 22:01:17 +090063import static org.onlab.packet.Ethernet.TYPE_IPV4;
64import static org.onlab.packet.IPv4.PROTOCOL_TCP;
65import static org.onlab.packet.IPv4.PROTOCOL_UDP;
66import static org.onosproject.net.flow.criteria.Criterion.Type.IPV4_DST;
67import static org.onosproject.net.flow.criteria.Criterion.Type.IPV4_SRC;
68import static org.onosproject.net.flow.criteria.Criterion.Type.IP_PROTO;
69import static org.onosproject.net.flow.criteria.Criterion.Type.TCP_DST;
70import static org.onosproject.net.flow.criteria.Criterion.Type.TCP_SRC;
71import static org.onosproject.net.flow.criteria.Criterion.Type.UDP_DST;
72import static org.onosproject.net.flow.criteria.Criterion.Type.UDP_SRC;
73import static org.onosproject.openstacknetworking.api.Constants.DHCP_ARP_TABLE;
74import static org.onosproject.openstacknetworking.api.Constants.FORWARDING_TABLE;
75import static org.onosproject.openstacknetworking.api.Constants.STAT_INBOUND_TABLE;
76import static org.onosproject.openstacknetworking.api.Constants.STAT_OUTBOUND_TABLE;
Boyoung Jeong9e8faec2018-06-17 21:19:23 +090077import static org.onosproject.openstacktelemetry.api.Constants.OPENSTACK_TELEMETRY_APP_ID;
78
Boyoung Jeong9e8faec2018-06-17 21:19:23 +090079/**
80 * Flow rule manager for network statistics of a VM.
81 */
82@Component(immediate = true)
83@Service
84public class StatsFlowRuleManager implements StatsFlowRuleAdminService {
85
86 private final Logger log = LoggerFactory.getLogger(getClass());
87
88 private static final byte FLOW_TYPE_SONA = 1; // VLAN
89
Jian Li0bbbb1c2018-06-22 22:01:17 +090090 private static final int MILLISECONDS = 1000;
Boyoung Jeong9e8faec2018-06-17 21:19:23 +090091 private static final int REFRESH_INTERVAL = 5;
92
93 private ApplicationId appId;
94
95 @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
96 protected CoreService coreService;
97
98 @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
99 protected FlowRuleService flowRuleService;
100
101 @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
102 protected HostService hostService;
103
Jian Li0bbbb1c2018-06-22 22:01:17 +0900104 @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
105 protected DeviceService deviceService;
106
107 @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
108 protected OpenstackTelemetryService telemetryService;
109
Boyoung Jeong9e8faec2018-06-17 21:19:23 +0900110 private Timer timer;
111 private TimerTask task;
Boyoung Jeong9e8faec2018-06-17 21:19:23 +0900112
Jian Li0bbbb1c2018-06-22 22:01:17 +0900113 private final Set<FlowInfo> gFlowInfoSet = Sets.newHashSet();
Boyoung Jeong9e8faec2018-06-17 21:19:23 +0900114 private int loopCount = 0;
115
116 private static final int SOURCE_ID = 1;
117 private static final int TARGET_ID = 2;
118 private static final int PRIORITY_BASE = 10000;
119 private static final int METRIC_PRIORITY_SOURCE = SOURCE_ID * PRIORITY_BASE;
120 private static final int METRIC_PRIORITY_TARGET = TARGET_ID * PRIORITY_BASE;
121
Jian Li0bbbb1c2018-06-22 22:01:17 +0900122 private static final MacAddress NO_HOST_MAC = MacAddress.valueOf("00:00:00:00:00:00");
Boyoung Jeong9e8faec2018-06-17 21:19:23 +0900123
124 public StatsFlowRuleManager() {
Boyoung Jeong9e8faec2018-06-17 21:19:23 +0900125 this.timer = new Timer("openstack-telemetry-sender");
126 }
127
128 @Activate
129 protected void activate() {
130 appId = coreService.registerApplication(OPENSTACK_TELEMETRY_APP_ID);
Jian Li0bbbb1c2018-06-22 22:01:17 +0900131
Boyoung Jeong9e8faec2018-06-17 21:19:23 +0900132 this.start();
Jian Li0bbbb1c2018-06-22 22:01:17 +0900133
134 log.info("Started");
Boyoung Jeong9e8faec2018-06-17 21:19:23 +0900135 }
136
137 @Deactivate
138 protected void deactivate() {
Jian Li0bbbb1c2018-06-22 22:01:17 +0900139 log.info("Stopped");
Boyoung Jeong9e8faec2018-06-17 21:19:23 +0900140 }
141
142 @Override
143 public void start() {
144 log.info("Start publishing thread");
Boyoung Jeong9e8faec2018-06-17 21:19:23 +0900145 task = new InternalTimerTask();
146 timer.scheduleAtFixedRate(task, MILLISECONDS * REFRESH_INTERVAL,
147 MILLISECONDS * REFRESH_INTERVAL);
148 }
149
150 @Override
151 public void stop() {
152 log.info("Stop data publishing thread");
153 task.cancel();
154 task = null;
155 }
156
Jian Li0bbbb1c2018-06-22 22:01:17 +0900157 @Override
158 public void createStatFlowRule(StatsFlowRule statsFlowRule) {
Boyoung Jeong9e8faec2018-06-17 21:19:23 +0900159
Jian Li0bbbb1c2018-06-22 22:01:17 +0900160 setStatFlowRule(statsFlowRule, true);
Boyoung Jeong9e8faec2018-06-17 21:19:23 +0900161
Jian Li0bbbb1c2018-06-22 22:01:17 +0900162 log.info("Install stat flow rule for SrcIp:{} DstIp:{}",
163 statsFlowRule.srcIpPrefix().toString(),
164 statsFlowRule.dstIpPrefix().toString());
165 }
166
167 @Override
168 public void deleteStatFlowRule(StatsFlowRule statsFlowRule) {
169 // FIXME: following code might not be necessary
170 flowRuleService.removeFlowRulesById(appId);
171
172 setStatFlowRule(statsFlowRule, false);
173
174 log.info("Remove stat flow rule for SrcIp:{} DstIp:{}",
175 statsFlowRule.srcIpPrefix().toString(),
176 statsFlowRule.dstIpPrefix().toString());
177 }
178
179 private void connectTables(DeviceId deviceId, int fromTable, int toTable,
180 StatsFlowRule statsFlowRule, int rulePriority,
181 boolean install) {
182
183 log.debug("Table Transition: {} -> {}", fromTable, toTable);
184 int srcPrefixLength = statsFlowRule.srcIpPrefix().prefixLength();
185 int dstPrefixLength = statsFlowRule.dstIpPrefix().prefixLength();
186 int prefixLength = rulePriority + srcPrefixLength + dstPrefixLength;
187 byte protocol = statsFlowRule.ipProtocol();
188
189 TrafficSelector.Builder selectorBuilder =
190 DefaultTrafficSelector.builder()
191 .matchEthType(TYPE_IPV4)
192 .matchIPSrc(statsFlowRule.srcIpPrefix())
193 .matchIPDst(statsFlowRule.dstIpPrefix());
194
195 if (protocol == PROTOCOL_TCP) {
196 selectorBuilder = selectorBuilder
197 .matchIPProtocol(statsFlowRule.ipProtocol())
198 .matchTcpSrc(statsFlowRule.srcTpPort())
199 .matchTcpDst(statsFlowRule.dstTpPort());
200
201 } else if (protocol == PROTOCOL_UDP) {
202 selectorBuilder = selectorBuilder
203 .matchIPProtocol(statsFlowRule.ipProtocol())
204 .matchUdpSrc(statsFlowRule.srcTpPort())
205 .matchUdpDst(statsFlowRule.dstTpPort());
206 } else {
207 log.warn("Unsupported protocol {}", statsFlowRule.ipProtocol());
Boyoung Jeong9e8faec2018-06-17 21:19:23 +0900208 }
Jian Li0bbbb1c2018-06-22 22:01:17 +0900209
210 TrafficTreatment.Builder treatmentBuilder = DefaultTrafficTreatment.builder();
211
212 treatmentBuilder.transition(toTable);
213
214 FlowRule flowRule = DefaultFlowRule.builder()
215 .forDevice(deviceId)
216 .withSelector(selectorBuilder.build())
217 .withTreatment(treatmentBuilder.build())
218 .withPriority(prefixLength)
219 .fromApp(appId)
220 .makePermanent()
221 .forTable(fromTable)
222 .build();
223
224 applyRule(flowRule, install);
Boyoung Jeong9e8faec2018-06-17 21:19:23 +0900225 }
226
227 /**
Jian Li0bbbb1c2018-06-22 22:01:17 +0900228 * Installs stats related flow rule to switch.
Boyoung Jeong9e8faec2018-06-17 21:19:23 +0900229 *
Jian Li0bbbb1c2018-06-22 22:01:17 +0900230 * @param flowRule flow rule
231 * @param install flag to install or not
Boyoung Jeong9e8faec2018-06-17 21:19:23 +0900232 */
233 private void applyRule(FlowRule flowRule, boolean install) {
Boyoung Jeong9e8faec2018-06-17 21:19:23 +0900234 FlowRuleOperations.Builder flowOpsBuilder = FlowRuleOperations.builder();
Jian Li0bbbb1c2018-06-22 22:01:17 +0900235 flowOpsBuilder = install ?
236 flowOpsBuilder.add(flowRule) : flowOpsBuilder.remove(flowRule);
Boyoung Jeong9e8faec2018-06-17 21:19:23 +0900237
238 flowRuleService.apply(flowOpsBuilder.build(new FlowRuleOperationsContext() {
239 @Override
240 public void onSuccess(FlowRuleOperations ops) {
241 log.debug("Provisioned vni or forwarding table: \n {}", ops.toString());
242 }
243
244 @Override
245 public void onError(FlowRuleOperations ops) {
246 log.debug("Failed to provision vni or forwarding table: \n {}", ops.toString());
247 }
248 }));
249 }
250
251 /**
Jian Li0bbbb1c2018-06-22 22:01:17 +0900252 * Gets a set of the flow infos.
Boyoung Jeong9e8faec2018-06-17 21:19:23 +0900253 *
Jian Li0bbbb1c2018-06-22 22:01:17 +0900254 * @return a set of flow infos
Boyoung Jeong9e8faec2018-06-17 21:19:23 +0900255 */
Jian Li0bbbb1c2018-06-22 22:01:17 +0900256 public Set<FlowInfo> getFlowInfo() {
257 Set<FlowInfo> flowInfos = Sets.newConcurrentHashSet();
Boyoung Jeong9e8faec2018-06-17 21:19:23 +0900258
Jian Li0bbbb1c2018-06-22 22:01:17 +0900259 // obtain all flow rule entries installed by telemetry app
260 for (FlowEntry entry : flowRuleService.getFlowEntriesById(appId)) {
261 FlowInfo.Builder fBuilder = new DefaultFlowInfo.DefaultBuilder();
262 TrafficSelector selector = entry.selector();
263
264 IPCriterion srcIp = (IPCriterion) selector.getCriterion(IPV4_SRC);
265 IPCriterion dstIp = (IPCriterion) selector.getCriterion(IPV4_DST);
266 IPProtocolCriterion ipProtocol =
267 (IPProtocolCriterion) selector.getCriterion(IP_PROTO);
268
269 log.debug("[FlowInfo] TableID:{} SRC_IP:{} DST_IP:{} Pkt:{} Byte:{}",
270 ((IndexTableId) entry.table()).id(),
271 srcIp.ip().toString(),
272 dstIp.ip().toString(),
273 entry.packets(),
274 entry.bytes());
275
276 fBuilder.withFlowType(FLOW_TYPE_SONA)
277 .withSrcIp(srcIp.ip())
278 .withDstIp(dstIp.ip())
279 .withProtocol((byte) ipProtocol.protocol());
280
281 if (ipProtocol.protocol() == PROTOCOL_TCP) {
282 TcpPortCriterion tcpSrc =
283 (TcpPortCriterion) selector.getCriterion(TCP_SRC);
284 TcpPortCriterion tcpDst =
285 (TcpPortCriterion) selector.getCriterion(TCP_DST);
286
287 log.debug("TCP SRC Port: {}, DST Port: {}",
288 tcpSrc.tcpPort().toInt(),
289 tcpDst.tcpPort().toInt());
290
291 fBuilder.withSrcPort(tcpSrc.tcpPort());
292 fBuilder.withDstPort(tcpDst.tcpPort());
293
294 } else if (ipProtocol.protocol() == PROTOCOL_UDP) {
295
296 UdpPortCriterion udpSrc =
297 (UdpPortCriterion) selector.getCriterion(UDP_SRC);
298 UdpPortCriterion udpDst =
299 (UdpPortCriterion) selector.getCriterion(UDP_DST);
300
301 log.debug("UDP SRC Port: {}, DST Port: {}",
302 udpSrc.udpPort().toInt(),
303 udpDst.udpPort().toInt());
304
305 fBuilder.withSrcPort(udpSrc.udpPort());
306 fBuilder.withDstPort(udpDst.udpPort());
307 } else {
308 log.debug("Other protocol: {}", ipProtocol.protocol());
Boyoung Jeong9e8faec2018-06-17 21:19:23 +0900309 }
Jian Li0bbbb1c2018-06-22 22:01:17 +0900310
311 fBuilder.withSrcMac(getMacAddress(srcIp.ip().address()))
312 .withDstMac(getMacAddress(dstIp.ip().address()))
313 .withInputInterfaceId(getInterfaceId(srcIp.ip().address()))
314 .withOutputInterfaceId(getInterfaceId(dstIp.ip().address()))
315 .withVlanId(getVlanId(srcIp.ip().address()))
316 .withDeviceId(entry.deviceId());
317
318 StatsInfo.Builder sBuilder = new DefaultStatsInfo.DefaultBuilder();
319
320 // TODO: need to collect error and drop packets stats
321 // TODO: need to make the refresh interval configurable
Jian Lide4ef402018-06-27 19:21:14 +0900322 sBuilder.withStartupTime(System.currentTimeMillis())
323 .withFstPktArrTime(System.currentTimeMillis())
324 .withLstPktOffset(REFRESH_INTERVAL * MILLISECONDS)
Jian Li0bbbb1c2018-06-22 22:01:17 +0900325 .withCurrAccPkts((int) entry.packets())
326 .withCurrAccBytes(entry.bytes())
327 .withErrorPkts((short) 0)
Jian Lide4ef402018-06-27 19:21:14 +0900328 .withDropPkts((short) 0);
Jian Li0bbbb1c2018-06-22 22:01:17 +0900329
330 fBuilder.withStatsInfo(sBuilder.build());
331
332 FlowInfo flowInfo = mergeFlowInfo(fBuilder.build(), fBuilder, sBuilder);
333
334 flowInfos.add(flowInfo);
335
336 log.debug("FlowInfo: \n{}", flowInfo.toString());
Boyoung Jeong9e8faec2018-06-17 21:19:23 +0900337 }
Jian Li0bbbb1c2018-06-22 22:01:17 +0900338
339 return flowInfos;
Boyoung Jeong9e8faec2018-06-17 21:19:23 +0900340 }
341
342 /**
Jian Li0bbbb1c2018-06-22 22:01:17 +0900343 * Merges old FlowInfo.StatsInfo and current FlowInfo.StatsInfo.
Boyoung Jeong9e8faec2018-06-17 21:19:23 +0900344 *
345 * @param flowInfo current FlowInfo object
346 * @param fBuilder Builder for FlowInfo
347 * @param sBuilder Builder for StatsInfo
348 * @return Merged FlowInfo object
349 */
350 private FlowInfo mergeFlowInfo(FlowInfo flowInfo,
351 FlowInfo.Builder fBuilder,
352 StatsInfo.Builder sBuilder) {
Jian Li0bbbb1c2018-06-22 22:01:17 +0900353 for (FlowInfo gFlowInfo : gFlowInfoSet) {
354 log.debug("Old FlowInfo:\n{}", gFlowInfo.toString());
355 if (gFlowInfo.roughEquals(flowInfo)) {
356
357 // Get old StatsInfo object and merge the value to current object.
358 StatsInfo oldStatsInfo = gFlowInfo.statsInfo();
359 sBuilder.withPrevAccPkts(oldStatsInfo.currAccPkts());
360 sBuilder.withPrevAccBytes(oldStatsInfo.currAccBytes());
361 FlowInfo newFlowInfo = fBuilder.withStatsInfo(sBuilder.build())
362 .build();
363
364 gFlowInfoSet.remove(gFlowInfo);
365 gFlowInfoSet.add(newFlowInfo);
366 log.info("Old FlowInfo found, Merge this {}", newFlowInfo.toString());
367 return newFlowInfo;
Boyoung Jeong9e8faec2018-06-17 21:19:23 +0900368 }
Boyoung Jeong9e8faec2018-06-17 21:19:23 +0900369 }
Jian Li0bbbb1c2018-06-22 22:01:17 +0900370
371 // No such record, then build the FlowInfo object and return this object.
372 log.info("No FlowInfo found, add new FlowInfo {}", flowInfo.toString());
373 FlowInfo newFlowInfo = fBuilder.withStatsInfo(sBuilder.build()).build();
374 gFlowInfoSet.add(newFlowInfo);
375 return newFlowInfo;
376 }
377
378 private void setStatFlowRule(StatsFlowRule statsFlowRule, boolean install) {
379 StatsFlowRule inverseFlowRule = DefaultStatsFlowRule.builder()
380 .srcIpPrefix(statsFlowRule.dstIpPrefix())
381 .dstIpPrefix(statsFlowRule.srcIpPrefix())
382 .ipProtocol(statsFlowRule.ipProtocol())
383 .srcTpPort(statsFlowRule.dstTpPort())
384 .dstTpPort(statsFlowRule.srcTpPort())
385 .build();
386
387 // FIXME: install stat flow rules for all devices for now
388 // need to query the device where the host with the given IP located
389 for (Device d : deviceService.getDevices()) {
390 if (d.type() == Device.Type.CONTROLLER) {
391 log.info("Not provide stats for 'CONTROLLER' ({})", d.id().toString());
392 continue;
393 }
394
395 connectTables(d.id(), STAT_INBOUND_TABLE, DHCP_ARP_TABLE,
396 statsFlowRule, METRIC_PRIORITY_SOURCE, install);
397 connectTables(d.id(), STAT_OUTBOUND_TABLE, FORWARDING_TABLE,
398 inverseFlowRule, METRIC_PRIORITY_TARGET, install);
399 }
Boyoung Jeong9e8faec2018-06-17 21:19:23 +0900400 }
401
402 /**
403 * Get VLAN ID with respect to IP Address.
404 *
405 * @param ipAddress IP Address of host
406 * @return VLAN ID
407 */
Jian Li0bbbb1c2018-06-22 22:01:17 +0900408 private VlanId getVlanId(IpAddress ipAddress) {
409 if (!hostService.getHostsByIp(ipAddress).isEmpty()) {
410 Host host = hostService.getHostsByIp(ipAddress).stream().findAny().get();
411 return host.vlan();
Boyoung Jeong9e8faec2018-06-17 21:19:23 +0900412 }
413 return VlanId.vlanId();
414 }
415
416 /**
417 * Get Interface ID of Switch which is connected to a host.
418 *
419 * @param ipAddress IP Address of host
420 * @return Interface ID of Switch
421 */
Jian Li0bbbb1c2018-06-22 22:01:17 +0900422 private int getInterfaceId(IpAddress ipAddress) {
423 if (!hostService.getHostsByIp(ipAddress).isEmpty()) {
424 Host host = hostService.getHostsByIp(ipAddress).stream().findAny().get();
425 return (int) host.location().port().toLong();
Boyoung Jeong9e8faec2018-06-17 21:19:23 +0900426 }
427 return -1;
428 }
429
430 /**
431 * Get MAC Address of host.
432 *
433 * @param ipAddress IP Address of host
434 * @return MAC Address of host
435 */
Jian Li0bbbb1c2018-06-22 22:01:17 +0900436 private MacAddress getMacAddress(IpAddress ipAddress) {
437 if (!hostService.getHostsByIp(ipAddress).isEmpty()) {
438 Host host = hostService.getHostsByIp(ipAddress).stream().findAny().get();
439 return host.mac();
Boyoung Jeong9e8faec2018-06-17 21:19:23 +0900440 }
Jian Li0bbbb1c2018-06-22 22:01:17 +0900441
Boyoung Jeong9e8faec2018-06-17 21:19:23 +0900442 return NO_HOST_MAC;
443 }
Jian Li0bbbb1c2018-06-22 22:01:17 +0900444
445 private class InternalTimerTask extends TimerTask {
446 @Override
447 public void run() {
448 log.debug("Timer Task Thread Starts ({})", loopCount++);
449 try {
450 telemetryService.publish(getFlowInfo());
451 } catch (Exception ex) {
452 log.error("Exception Stack:\n{}", ExceptionUtils.getStackTrace(ex));
453 }
454 }
455 }
Boyoung Jeong9e8faec2018-06-17 21:19:23 +0900456}