2 * Copyright (c) 2016 Cisco Systems, Inc. and others. All rights reserved.
4 * This program and the accompanying materials are made available under the
5 * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6 * and is available at http://www.eclipse.org/legal/epl-v10.html
9 package org.opendaylight.netconf.test.tool;
11 import static com.google.common.base.Preconditions.checkArgument;
13 import com.google.common.base.Preconditions;
14 import com.google.common.io.CharStreams;
15 import com.google.common.io.Files;
16 import java.io.BufferedReader;
18 import java.io.FileReader;
19 import java.io.IOException;
20 import java.io.InputStream;
21 import java.io.InputStreamReader;
22 import java.nio.charset.StandardCharsets;
23 import java.util.ArrayList;
24 import java.util.Arrays;
25 import java.util.Iterator;
26 import java.util.List;
27 import java.util.concurrent.TimeUnit;
28 import java.util.regex.Matcher;
29 import java.util.regex.Pattern;
30 import net.sourceforge.argparse4j.ArgumentParsers;
31 import net.sourceforge.argparse4j.annotation.Arg;
32 import net.sourceforge.argparse4j.inf.ArgumentParser;
33 import net.sourceforge.argparse4j.inf.ArgumentParserException;
35 public class TesttoolParameters {
37 private static final String HOST_KEY = "{HOST}";
38 private static final String PORT_KEY = "{PORT}";
39 private static final String TCP_ONLY = "{TCP_ONLY}";
40 private static final String ADDRESS_PORT = "{ADDRESS:PORT}";
41 private static final String dest = "http://{ADDRESS:PORT}/restconf/config/network-topology:network-topology/topology/topology-netconf/";
42 private static final Pattern YANG_FILENAME_PATTERN = Pattern.compile("(?<name>.*)@(?<revision>\\d{4}-\\d{2}-\\d{2})\\.yang");
43 private static final Pattern DATE_PATTERN = Pattern.compile("(\\d{4}-\\d{2}-\\d{2})");
45 private static final String RESOURCE = "/config-template.json";
46 @Arg(dest = "edit-content")
47 public File editContent;
50 @Arg(dest = "thread-amount")
51 public int threadAmount;
52 @Arg(dest = "throttle")
55 public ArrayList<String> auth;
56 @Arg(dest = "controller-destination")
57 public String controllerDestination;
58 @Arg(dest = "schemas-dir")
59 public File schemasDir;
60 @Arg(dest = "devices-count")
61 public int deviceCount;
62 @Arg(dest = "devices-per-port")
63 public int devicesPerPort;
64 @Arg(dest = "starting-port")
65 public int startingPort;
66 @Arg(dest = "generate-config-connection-timeout")
67 public int generateConfigsTimeout;
68 @Arg(dest = "generate-config-address")
69 public String generateConfigsAddress;
70 @Arg(dest = "distro-folder")
71 public File distroFolder;
72 @Arg(dest = "generate-configs-batch-size")
73 public int generateConfigBatchSize;
80 @Arg(dest = "notification-file")
81 public File notificationFile;
84 @Arg(dest = "initial-config-xml-file")
85 public File initialConfigXMLFile;
86 @Arg(dest = "time-out")
88 private InputStream stream;
93 @Arg(dest = "thread-pool-size")
94 public int threadPoolSize;
96 static ArgumentParser getParser() {
97 final ArgumentParser parser = ArgumentParsers.newArgumentParser("netconf testtool");
99 parser.description("netconf testtool");
101 parser.addArgument("--edit-content")
103 .dest("edit-content");
105 parser.addArgument("--async-requests")
110 parser.addArgument("--thread-amount")
113 .dest("thread-amount")
114 .help("The number of threads to use for configuring devices.");
116 parser.addArgument("--throttle")
119 .help("Maximum amount of async requests that can be open at a time, " +
120 "with mutltiple threads this gets divided among all threads")
123 parser.addArgument("--auth")
125 .help("Username and password for HTTP basic authentication in order username password.")
128 parser.addArgument("--controller-destination")
130 .help("Ip address and port of controller. Must be in following format <ip>:<port> " +
131 "if available it will be used for spawning netconf connectors via topology configuration as " +
132 "a part of URI. Example (http://<controller destination>/restconf/config/network-topology:network-topology/topology/topology-netconf/node/<node-id>)" +
133 "otherwise it will just start simulated devices and skip the execution of PUT requests")
134 .dest("controller-destination");
136 parser.addArgument("--device-count")
139 .help("Number of simulated netconf devices to spin. This is the number of actual ports open for the devices.")
140 .dest("devices-count");
142 parser.addArgument("--devices-per-port")
145 .help("Amount of config files generated per port to spoof more devices then are actually running")
146 .dest("devices-per-port");
148 parser.addArgument("--schemas-dir")
150 .help("Directory containing yang schemas to describe simulated devices. Some schemas e.g. netconf monitoring and inet types are included by default")
151 .dest("schemas-dir");
153 parser.addArgument("--notification-file")
155 .help("Xml file containing notifications that should be sent to clients after create subscription is called")
156 .dest("notification-file");
158 parser.addArgument("--initial-config-xml-file")
160 .help("Xml file containing initial simulatted configuration to be returned via get-config rpc")
161 .dest("initial-config-xml-file");
163 parser.addArgument("--starting-port")
166 .help("First port for simulated device. Each other device will have previous+1 port number")
167 .dest("starting-port");
169 parser.addArgument("--generate-config-connection-timeout")
171 .setDefault((int) TimeUnit.MINUTES.toMillis(30))
172 .help("Timeout to be generated in initial config files")
173 .dest("generate-config-connection-timeout");
175 parser.addArgument("--generate-config-address")
177 .setDefault("127.0.0.1")
178 .help("Address to be placed in generated configs")
179 .dest("generate-config-address");
181 parser.addArgument("--generate-configs-batch-size")
184 .help("Number of connector configs per generated file")
185 .dest("generate-configs-batch-size");
187 parser.addArgument("--distribution-folder")
189 .help("Directory where the karaf distribution for controller is located")
190 .dest("distro-folder");
192 parser.addArgument("--ssh")
195 .help("Whether to use ssh for transport or just pure tcp")
198 parser.addArgument("--exi")
201 .help("Whether to use exi to transport xml content")
204 parser.addArgument("--debug")
207 .help("Whether to use debug log level instead of INFO")
210 parser.addArgument("--md-sal")
213 .help("Whether to use md-sal datastore instead of default simulated datastore.")
216 parser.addArgument("--time-out")
219 .help("the maximum time in seconds for executing each PUT request")
222 parser.addArgument("-ip")
224 .setDefault("0.0.0.0")
225 .help("Ip address which will be used for creating a socket address." +
226 "It can either be a machine name, such as " +
227 "java.sun.com, or a textual representation of its IP address.")
230 parser.addArgument("--thread-pool-size")
233 .help("The number of threads to keep in the pool, when creating a device simulator. Even if they are idle.")
234 .dest("thread-pool-size");
239 public static TesttoolParameters parseArgs(final String[] args, final ArgumentParser parser) {
240 final TesttoolParameters opt = new TesttoolParameters();
242 parser.parseArgs(args, opt);
244 } catch (final ArgumentParserException e) {
245 parser.handleError(e);
252 private static String modifyMessage(final StringBuilder payloadBuilder, final int payloadPosition, final int size) {
254 return payloadBuilder.toString();
257 if (payloadPosition == 0) {
258 payloadBuilder.insert(payloadBuilder.toString().indexOf('{', 2), "[");
259 payloadBuilder.replace(payloadBuilder.length() - 1, payloadBuilder.length(), ",");
260 } else if (payloadPosition + 1 == size) {
261 payloadBuilder.delete(0, payloadBuilder.toString().indexOf(':') + 1);
262 payloadBuilder.insert(payloadBuilder.toString().indexOf('}', 2) + 1, "]");
264 payloadBuilder.delete(0, payloadBuilder.toString().indexOf(':') + 1);
265 payloadBuilder.replace(payloadBuilder.length() - 2, payloadBuilder.length() - 1, ",");
266 payloadBuilder.deleteCharAt(payloadBuilder.toString().lastIndexOf('}'));
268 return payloadBuilder.toString();
272 if (editContent == null) {
273 stream = TesttoolParameters.class.getResourceAsStream(RESOURCE);
275 Preconditions.checkArgument(!editContent.isDirectory(), "Edit content file is a dir");
276 Preconditions.checkArgument(editContent.canRead(), "Edit content file is unreadable");
279 if (controllerDestination != null) {
280 Preconditions.checkArgument(controllerDestination.contains(":"), "Controller Destination needs to be in a following format <ip>:<port>");
281 String[] parts = controllerDestination.split(Pattern.quote(":"));
282 Preconditions.checkArgument(Integer.parseInt(parts[1]) > 0, "Port =< 0");
285 checkArgument(deviceCount > 0, "Device count has to be > 0");
286 checkArgument(startingPort > 1023, "Starting port has to be > 1023");
287 checkArgument(devicesPerPort > 0, "Atleast one device per port needed");
289 if (schemasDir != null) {
290 checkArgument(schemasDir.exists(), "Schemas dir has to exist");
291 checkArgument(schemasDir.isDirectory(), "Schemas dir has to be a directory");
292 checkArgument(schemasDir.canRead(), "Schemas dir has to be readable");
294 final List<File> files = Arrays.asList(schemasDir.listFiles());
295 for (final File file : files) {
296 final Matcher matcher = YANG_FILENAME_PATTERN.matcher(file.getName());
297 if (!matcher.matches()) {
298 BufferedReader reader;
300 reader = new BufferedReader(new FileReader(file));
301 String line = reader.readLine();
302 while (!DATE_PATTERN.matcher(line).find()) {
303 line = reader.readLine();
305 Matcher m = DATE_PATTERN.matcher(line);
308 String moduleName = file.getAbsolutePath();
309 if (file.getName().endsWith(".yang")) {
310 moduleName = moduleName.substring(0, moduleName.length() - 5);
312 final String revision = m.group(1);
313 String correctName = moduleName + "@" + revision + ".yang";
314 File correctNameFile = new File(correctName);
315 file.renameTo(correctNameFile);
318 } catch (IOException e) {
326 public ArrayList<ArrayList<Execution.DestToPayload>> getThreadsPayloads(final List<Integer> openDevices) {
327 final String editContentString;
329 if (stream == null) {
330 editContentString = Files.toString(editContent, StandardCharsets.UTF_8);
332 editContentString = CharStreams.toString(new InputStreamReader(stream, StandardCharsets.UTF_8));
334 } catch (final IOException e) {
335 throw new IllegalArgumentException("Cannot read content of " + editContent);
339 Iterator<Integer> iterator;
341 final ArrayList<ArrayList<Execution.DestToPayload>> allThreadsPayloads = new ArrayList<>();
342 if (generateConfigBatchSize > 1) {
344 final int batchedRequests = openDevices.size() / generateConfigBatchSize;
345 final int batchedRequestsPerThread = batchedRequests / threadAmount;
346 final int leftoverBatchedRequests = (batchedRequests) % threadAmount;
347 final int leftoverRequests = openDevices.size() - (batchedRequests * generateConfigBatchSize);
349 final StringBuilder destBuilder = new StringBuilder(dest);
350 destBuilder.replace(destBuilder.indexOf(ADDRESS_PORT), destBuilder.indexOf(ADDRESS_PORT) + ADDRESS_PORT.length(), controllerDestination);
352 for (int l = 0; l < threadAmount; l++) {
353 from = l * (batchedRequests * batchedRequestsPerThread);
354 to = from + (batchedRequests * batchedRequestsPerThread);
355 iterator = openDevices.subList(from, to).iterator();
356 allThreadsPayloads.add(createBatchedPayloads(batchedRequestsPerThread, iterator, editContentString, destBuilder.toString()));
358 ArrayList<Execution.DestToPayload> payloads = null;
359 if (leftoverBatchedRequests > 0) {
360 from = threadAmount * (batchedRequests * batchedRequestsPerThread);
361 to = from + (batchedRequests * batchedRequestsPerThread);
362 iterator = openDevices.subList(from, to).iterator();
363 payloads = createBatchedPayloads(leftoverBatchedRequests, iterator, editContentString, destBuilder.toString());
367 for (int j = 0; j < leftoverRequests; j++) {
368 from = openDevices.size() - leftoverRequests;
369 to = openDevices.size();
370 iterator = openDevices.subList(from, to).iterator();
371 final StringBuilder payloadBuilder = new StringBuilder(prepareMessage(iterator.next(), editContentString));
372 payload += modifyMessage(payloadBuilder, j, leftoverRequests);
374 if (leftoverRequests > 0 || leftoverBatchedRequests > 0) {
376 if (payloads != null) {
377 payloads.add(new Execution.DestToPayload(destBuilder.toString(), payload));
379 allThreadsPayloads.add(payloads);
382 final int requestPerThreads = openDevices.size() / threadAmount;
383 final int leftoverRequests = openDevices.size() % threadAmount;
385 for (int i = 0; i < threadAmount; i++) {
386 from = i * requestPerThreads;
387 to = from + requestPerThreads;
388 iterator = openDevices.subList(from, to).iterator();
389 allThreadsPayloads.add(createPayloads(iterator, editContentString));
392 if (leftoverRequests > 0) {
393 from = (threadAmount) * requestPerThreads;
394 to = from + leftoverRequests;
395 iterator = openDevices.subList(from, to).iterator();
396 allThreadsPayloads.add(createPayloads(iterator, editContentString));
399 return allThreadsPayloads;
402 private String prepareMessage(final int openDevice, final String editContentString) {
403 StringBuilder messageBuilder = new StringBuilder(editContentString);
405 if (editContentString.contains(HOST_KEY)) {
406 messageBuilder.replace(messageBuilder.indexOf(HOST_KEY), messageBuilder.indexOf(HOST_KEY) + HOST_KEY.length(), generateConfigsAddress);
408 if (editContentString.contains(PORT_KEY)) {
409 while (messageBuilder.indexOf(PORT_KEY) != -1)
410 messageBuilder.replace(messageBuilder.indexOf(PORT_KEY), messageBuilder.indexOf(PORT_KEY) + PORT_KEY.length(), Integer.toString(openDevice));
412 if (editContentString.contains(TCP_ONLY)) {
413 messageBuilder.replace(messageBuilder.indexOf(TCP_ONLY), messageBuilder.indexOf(TCP_ONLY) + TCP_ONLY.length(), Boolean.toString(!ssh));
415 return messageBuilder.toString();
418 private ArrayList<Execution.DestToPayload> createPayloads(final Iterator<Integer> openDevices, final String editContentString) {
419 final ArrayList<Execution.DestToPayload> payloads = new ArrayList<>();
421 while (openDevices.hasNext()) {
422 final StringBuilder destBuilder = new StringBuilder(dest);
423 destBuilder.replace(destBuilder.indexOf(ADDRESS_PORT), destBuilder.indexOf(ADDRESS_PORT) + ADDRESS_PORT.length(), controllerDestination);
424 payloads.add(new Execution.DestToPayload(destBuilder.toString(), prepareMessage(openDevices.next(), editContentString)));
429 private ArrayList<Execution.DestToPayload> createBatchedPayloads(final int batchedRequestsCount, final Iterator<Integer> openDevices, final String editContentString,
430 final String destination) {
431 final ArrayList<Execution.DestToPayload> payloads = new ArrayList<>();
433 for (int i = 0; i < batchedRequestsCount; i++) {
435 for (int j = 0; j < generateConfigBatchSize; j++) {
436 final StringBuilder payloadBuilder = new StringBuilder(prepareMessage(openDevices.next(), editContentString));
437 payload += modifyMessage(payloadBuilder, j, generateConfigBatchSize);
439 payloads.add(new Execution.DestToPayload(destination, payload));
444 //TODO This may be more scalable enumerating parameters via reflection
446 public String toString() {
447 StringBuffer params = new StringBuffer("TesttoolParameters{");
448 params.append("edit-content='").append(editContent).append('\'');
449 params.append(", async='").append(async).append('\'');
450 params.append(", thread-amount='").append(threadAmount).append('\'');
451 params.append(", throttle='").append(throttle).append('\'');
452 params.append(", auth='").append(auth).append('\'');
453 params.append(", controller-destination='").append(controllerDestination).append('\'');
454 params.append(", schemas-dir='").append(schemasDir).append('\'');
455 params.append(", devices-count='").append(deviceCount).append('\'');
456 params.append(", devices-per-port='").append(devicesPerPort).append('\'');
457 params.append(", starting-port='").append(startingPort).append('\'');
458 params.append(", generate-config-connection-timeout='").append(generateConfigsTimeout).append('\'');
459 params.append(", generate-config-address='").append(generateConfigsAddress).append('\'');
460 params.append(", distro-folder='").append(distroFolder).append('\'');
461 params.append(", generate-configs-batch-size='").append(generateConfigBatchSize).append('\'');
462 params.append(", ssh='").append(ssh).append('\'');
463 params.append(", exi='").append(exi).append('\'');
464 params.append(", debug='").append(debug).append('\'');
465 params.append(", notification-file='").append(notificationFile).append('\'');
466 params.append(", md-sal='").append(mdSal).append('\'');
467 params.append(", initial-config-xml-file='").append(initialConfigXMLFile).append('\'');
468 params.append(", time-out='").append(timeOut).append('\'');
471 return params.toString();