Update testtool arguments
[netconf.git] / netconf / tools / netconf-testtool / src / main / java / org / opendaylight / netconf / test / tool / TesttoolParameters.java
1 /*
2  * Copyright (c) 2016 Cisco Systems, Inc. and others.  All rights reserved.
3  *
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
7  */
8 package org.opendaylight.netconf.test.tool;
9
10 import static com.google.common.base.Preconditions.checkArgument;
11 import static com.google.common.base.Preconditions.checkState;
12
13 import com.google.common.io.CharStreams;
14 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
15 import java.io.BufferedReader;
16 import java.io.File;
17 import java.io.FileReader;
18 import java.io.IOException;
19 import java.io.InputStream;
20 import java.io.InputStreamReader;
21 import java.lang.reflect.Field;
22 import java.nio.charset.StandardCharsets;
23 import java.nio.file.Files;
24 import java.nio.file.Paths;
25 import java.nio.file.StandardCopyOption;
26 import java.util.ArrayList;
27 import java.util.Arrays;
28 import java.util.Collections;
29 import java.util.Iterator;
30 import java.util.List;
31 import java.util.StringJoiner;
32 import java.util.concurrent.TimeUnit;
33 import java.util.regex.Matcher;
34 import java.util.regex.Pattern;
35 import net.sourceforge.argparse4j.ArgumentParsers;
36 import net.sourceforge.argparse4j.annotation.Arg;
37 import net.sourceforge.argparse4j.inf.ArgumentParser;
38 import net.sourceforge.argparse4j.inf.ArgumentParserException;
39 import org.opendaylight.yangtools.yang.common.YangConstants;
40
41 @SuppressFBWarnings({"DM_EXIT", "DM_DEFAULT_ENCODING"})
42 public class TesttoolParameters {
43
44     private static final String HOST_KEY = "{HOST}";
45     private static final String PORT_KEY = "{PORT}";
46     private static final String TCP_ONLY = "{TCP_ONLY}";
47     private static final String RESTCONF_NETCONF_TOPOLOGY_PATH_TEMPLATE =
48         "http://%s:%s/restconf/config/network-topology:network-topology/topology/topology-netconf/";
49     private static final Pattern YANG_FILENAME_PATTERN = Pattern
50         .compile("(?<name>.*)@(?<revision>\\d{4}-\\d{2}-\\d{2})\\.yang");
51     private static final Pattern REVISION_DATE_PATTERN = Pattern.compile("revision\\s+\"?(\\d{4}-\\d{2}-\\d{2})\"?");
52
53     private static final String RESOURCE = "/config-template.json";
54     @Arg(dest = "async")
55     public boolean async;
56     @Arg(dest = "thread-amount")
57     public int threadAmount;
58     @Arg(dest = "throttle")
59     public int throttle;
60     @Arg(dest = "controller-auth-username")
61     public String controllerAuthUsername;
62     @Arg(dest = "controller-auth-password")
63     public String controllerAuthPassword;
64     @Arg(dest = "controller-ip")
65     public String controllerIp;
66     @Arg(dest = "controller-port")
67     public Integer controllerPort;
68     @Arg(dest = "schemas-dir")
69     public File schemasDir;
70     @Arg(dest = "devices-count")
71     public int deviceCount;
72     @Arg(dest = "devices-per-port")
73     public int devicesPerPort;
74     @Arg(dest = "starting-port")
75     public int startingPort;
76     @Arg(dest = "generate-config-connection-timeout")
77     public int generateConfigsTimeout;
78     @Arg(dest = "generate-config-address")
79     public String generateConfigsAddress;
80     @Arg(dest = "distro-folder")
81     public File distroFolder;
82     @Arg(dest = "generate-configs-batch-size")
83     public int generateConfigBatchSize;
84     @Arg(dest = "ssh")
85     public boolean ssh;
86     @Arg(dest = "exi")
87     public boolean exi = true;
88     @Arg(dest = "debug")
89     public boolean debug;
90     @Arg(dest = "notification-file")
91     public File notificationFile;
92     @Arg(dest = "md-sal")
93     public boolean mdSal;
94     @Arg(dest = "initial-config-xml-file")
95     public File initialConfigXMLFile;
96     @Arg(dest = "time-out")
97     public long timeOut;
98     @Arg(dest = "ip")
99     public String ip;
100     @Arg(dest = "thread-pool-size")
101     public int threadPoolSize;
102     @Arg(dest = "rpc-config")
103     public File rpcConfig;
104
105     @SuppressWarnings("checkstyle:lineLength")
106     static ArgumentParser getParser() {
107         final ArgumentParser parser = ArgumentParsers.newArgumentParser("netconf testtool");
108
109         parser.description("netconf testtool");
110
111         parser.addArgument("--edit-content")
112                 .type(String.class)
113                 .dest("edit-content");
114
115         parser.addArgument("--async-requests")
116                 .type(Boolean.class)
117                 .setDefault(Boolean.FALSE)
118                 .dest("async");
119
120         parser.addArgument("--thread-amount")
121                 .type(Integer.class)
122                 .setDefault(1)
123                 .dest("thread-amount")
124                 .help("The number of threads to use for configuring devices.");
125
126         parser.addArgument("--throttle")
127                 .type(Integer.class)
128                 .setDefault(5000)
129                 .help("Maximum amount of async requests that can be open at a time, "
130                         + "with mutltiple threads this gets divided among all threads")
131                 .dest("throttle");
132
133         parser.addArgument("--controller-auth-username")
134                 .type(String.class)
135                 .setDefault("admin")
136                 .help("Username for HTTP basic authentication to destination controller.")
137                 .dest("controller-auth-username");
138
139         parser.addArgument("--controller-auth-password")
140                 .type(String.class)
141                 .setDefault("admin")
142                 .help("Password for HTTP basic authentication to destination controller.")
143                 .dest("controller-auth-password");
144
145         parser.addArgument("--controller-ip")
146                 .type(String.class)
147                 .help("Ip of controller if available it will be used for spawning netconf connectors via topology"
148                         + " configuration as a part of"
149                         + " URI(http://<controller-ip>:<controller-port>/restconf/config/...)"
150                         + " otherwise it will just start simulated devices and skip the execution of PUT requests")
151                 .dest("controller-ip");
152
153         parser.addArgument("--controller-port")
154                 .type(Integer.class)
155                 .help("Port of controller if available it will be used for spawning netconf connectors via topology "
156                         + "configuration as a part of"
157                         + " URI(http://<controller-ip>:<controller-port>/restconf/config/...) "
158                         + "otherwise it will just start simulated devices and skip the execution of PUT requests")
159                 .dest("controller-port");
160
161         parser.addArgument("--device-count")
162                 .type(Integer.class)
163                 .setDefault(1)
164                 .help("Number of simulated netconf devices to spin. This is the number of actual ports open for the devices.")
165                 .dest("devices-count");
166
167         parser.addArgument("--devices-per-port")
168                 .type(Integer.class)
169                 .setDefault(1)
170                 .help("Amount of config files generated per port to spoof more devices than are actually running")
171                 .dest("devices-per-port");
172
173         parser.addArgument("--schemas-dir")
174                 .type(File.class)
175                 .help("Directory containing yang schemas to describe simulated devices. Some schemas e.g. netconf monitoring and inet types are included by default")
176                 .dest("schemas-dir");
177
178         parser.addArgument("--notification-file")
179                 .type(File.class)
180                 .help("Xml file containing notifications that should be sent to clients after create subscription is called")
181                 .dest("notification-file");
182
183         parser.addArgument("--initial-config-xml-file")
184                 .type(File.class)
185                 .help("Xml file containing initial simulatted configuration to be returned via get-config rpc")
186                 .dest("initial-config-xml-file");
187
188         parser.addArgument("--starting-port")
189                 .type(Integer.class)
190                 .setDefault(17830)
191                 .help("First port for simulated device. Each other device will have previous+1 port number")
192                 .dest("starting-port");
193
194         parser.addArgument("--generate-config-connection-timeout")
195                 .type(Integer.class)
196                 .setDefault((int) TimeUnit.MINUTES.toMillis(30))
197                 .help("Timeout to be generated in initial config files")
198                 .dest("generate-config-connection-timeout");
199
200         parser.addArgument("--generate-config-address")
201                 .type(String.class)
202                 .setDefault("127.0.0.1")
203                 .help("Address to be placed in generated configs")
204                 .dest("generate-config-address");
205
206         parser.addArgument("--generate-configs-batch-size")
207                 .type(Integer.class)
208                 .setDefault(1)
209                 .help("Number of connector configs per generated file")
210                 .dest("generate-configs-batch-size");
211
212         parser.addArgument("--distribution-folder")
213                 .type(File.class)
214                 .help("Directory where the karaf distribution for controller is located")
215                 .dest("distro-folder");
216
217         parser.addArgument("--ssh")
218                 .type(Boolean.class)
219                 .setDefault(Boolean.TRUE)
220                 .help("Whether to use ssh for transport or just pure tcp")
221                 .dest("ssh");
222
223         parser.addArgument("--exi")
224                 .type(Boolean.class)
225                 .setDefault(Boolean.TRUE)
226                 .help("Whether to use exi to transport xml content")
227                 .dest("exi");
228
229         parser.addArgument("--debug")
230                 .type(Boolean.class)
231                 .setDefault(Boolean.FALSE)
232                 .help("Whether to use debug log level instead of INFO")
233                 .dest("debug");
234
235         parser.addArgument("--md-sal")
236                 .type(Boolean.class)
237                 .setDefault(Boolean.FALSE)
238                 .help("Whether to use md-sal datastore instead of default simulated datastore.")
239                 .dest("md-sal");
240
241         parser.addArgument("--time-out")
242                 .type(long.class)
243                 .setDefault(20)
244                 .help("the maximum time in seconds for executing each PUT request")
245                 .dest("time-out");
246
247         parser.addArgument("-ip")
248                 .type(String.class)
249                 .setDefault("0.0.0.0")
250                 .help("Ip address which will be used for creating a socket address."
251                         + "It can either be a machine name, such as "
252                         + "java.sun.com, or a textual representation of its IP address.")
253                 .dest("ip");
254
255         parser.addArgument("--thread-pool-size")
256                 .type(Integer.class)
257                 .setDefault(8)
258                 .help("The number of threads to keep in the pool, when creating a device simulator. Even if they are idle.")
259                 .dest("thread-pool-size");
260         parser.addArgument("--rpc-config")
261                 .type(File.class)
262                 .help("Rpc config file. It can be used to define custom rpc behavior, or override the default one."
263                     + "Usable for testing buggy device behavior.")
264                 .dest("rpc-config");
265
266         return parser;
267     }
268
269     public static TesttoolParameters parseArgs(final String[] args, final ArgumentParser parser) {
270         final TesttoolParameters opt = new TesttoolParameters();
271         try {
272             parser.parseArgs(args, opt);
273             return opt;
274         } catch (final ArgumentParserException e) {
275             parser.handleError(e);
276         }
277
278         System.exit(1);
279         return null;
280     }
281
282     private static String modifyMessage(final StringBuilder payloadBuilder, final int payloadPosition, final int size) {
283         if (size == 1) {
284             return payloadBuilder.toString();
285         }
286
287         if (payloadPosition == 0) {
288             payloadBuilder.insert(payloadBuilder.toString().indexOf('{', 2), "[");
289             payloadBuilder.replace(payloadBuilder.length() - 1, payloadBuilder.length(), ",");
290         } else if (payloadPosition + 1 == size) {
291             payloadBuilder.delete(0, payloadBuilder.toString().indexOf(':') + 1);
292             payloadBuilder.insert(payloadBuilder.toString().indexOf('}', 2) + 1, "]");
293         } else {
294             payloadBuilder.delete(0, payloadBuilder.toString().indexOf(':') + 1);
295             payloadBuilder.replace(payloadBuilder.length() - 2, payloadBuilder.length() - 1, ",");
296             payloadBuilder.deleteCharAt(payloadBuilder.toString().lastIndexOf('}'));
297         }
298         return payloadBuilder.toString();
299     }
300
301     @SuppressWarnings("checkstyle:regexpSinglelineJava")
302     void validate() {
303         if (controllerIp != null) {
304             //FIXME Ip validation
305             checkArgument(controllerPort != null, "Controller port is missing");
306             //FIXME Is there specific bound
307             checkArgument(controllerPort >= 0, "Controller port should be non-negative integer");
308             checkArgument(controllerPort < 65354, "Controller port should be less than 65354");
309         } else {
310             checkArgument(controllerPort == null, "Controller ip is missing");
311         }
312
313         checkArgument(deviceCount > 0, "Device count has to be > 0");
314         checkArgument(startingPort > 1023, "Starting port has to be > 1023");
315         checkArgument(devicesPerPort > 0, "Atleast one device per port needed");
316
317         if (schemasDir != null) {
318             checkArgument(schemasDir.exists(), "Schemas dir has to exist");
319             checkArgument(schemasDir.isDirectory(), "Schemas dir has to be a directory");
320             checkArgument(schemasDir.canRead(), "Schemas dir has to be readable");
321
322             final File[] filesArray = schemasDir.listFiles();
323             final List<File> files = filesArray != null ? Arrays.asList(filesArray) : Collections.emptyList();
324             for (final File file : files) {
325                 final Matcher matcher = YANG_FILENAME_PATTERN.matcher(file.getName());
326                 if (!matcher.matches()) {
327                     try {
328                         final String correctName = correctedName(file);
329                         if (correctName != null) {
330                             Files.move(file.toPath(), Paths.get(correctName), StandardCopyOption.ATOMIC_MOVE);
331                         }
332                     } catch (final IOException e) {
333                         // print error to console (test tool is running from console)
334                         e.printStackTrace();
335                     }
336                 }
337             }
338         }
339         if (rpcConfig != null) {
340             checkArgument(rpcConfig.exists(), "Rpc config file has to exist");
341             checkArgument(!rpcConfig.isDirectory(), "Rpc config file can't be a directory");
342             checkArgument(rpcConfig.canRead(), "Rpc config file to be readable");
343         }
344     }
345
346     private static String correctedName(final File file) throws IOException {
347         try (BufferedReader reader = new BufferedReader(new FileReader(file, StandardCharsets.UTF_8))) {
348             String line = reader.readLine();
349             while (line != null && !REVISION_DATE_PATTERN.matcher(line).find()) {
350                 line = reader.readLine();
351             }
352             if (line != null) {
353                 final Matcher m = REVISION_DATE_PATTERN.matcher(line);
354                 checkState(m.find(), "Revision pattern %s did not match line %s", REVISION_DATE_PATTERN, line);
355                 String moduleName = file.getAbsolutePath();
356                 if (file.getName().endsWith(YangConstants.RFC6020_YANG_FILE_EXTENSION)) {
357                     moduleName = moduleName.substring(0, moduleName.length() - 5);
358                 }
359
360                 return moduleName + "@" + m.group(1) + YangConstants.RFC6020_YANG_FILE_EXTENSION;
361             }
362         }
363         return null;
364     }
365
366
367     public ArrayList<ArrayList<Execution.DestToPayload>> getThreadsPayloads(final List<Integer> openDevices) {
368         final String editContentString;
369         try {
370             final InputStream stream = TesttoolParameters.class.getResourceAsStream(RESOURCE);
371             editContentString = CharStreams.toString(new InputStreamReader(stream, StandardCharsets.UTF_8));
372         } catch (final IOException e) {
373             throw new IllegalArgumentException("Cannot read content of " + RESOURCE, e);
374         }
375
376         int from;
377         int to;
378         Iterator<Integer> iterator;
379
380         final ArrayList<ArrayList<Execution.DestToPayload>> allThreadsPayloads = new ArrayList<>();
381         if (generateConfigBatchSize > 1) {
382
383             final int batchedRequests = openDevices.size() / generateConfigBatchSize;
384             final int batchedRequestsPerThread = batchedRequests / threadAmount;
385             final int leftoverBatchedRequests = batchedRequests % threadAmount;
386             final int leftoverRequests = openDevices.size() - batchedRequests * generateConfigBatchSize;
387
388             //FIXME Move this to validate() and rename it to init() or create init() and move there.
389             //FIXME Make it field.
390             final String restconfNetconfTopologyPath = String.format(RESTCONF_NETCONF_TOPOLOGY_PATH_TEMPLATE,
391                     controllerIp, controllerPort);
392
393             for (int l = 0; l < threadAmount; l++) {
394                 from = l * batchedRequests * batchedRequestsPerThread;
395                 to = from + batchedRequests * batchedRequestsPerThread;
396                 iterator = openDevices.subList(from, to).iterator();
397                 allThreadsPayloads.add(createBatchedPayloads(batchedRequestsPerThread, iterator, editContentString,
398                         restconfNetconfTopologyPath));
399             }
400             ArrayList<Execution.DestToPayload> payloads = null;
401             if (leftoverBatchedRequests > 0) {
402                 from = threadAmount * batchedRequests * batchedRequestsPerThread;
403                 to = from + batchedRequests * batchedRequestsPerThread;
404                 iterator = openDevices.subList(from, to).iterator();
405                 payloads = createBatchedPayloads(leftoverBatchedRequests, iterator, editContentString,
406                         restconfNetconfTopologyPath);
407             }
408             String payload = "";
409
410             for (int j = 0; j < leftoverRequests; j++) {
411                 from = openDevices.size() - leftoverRequests;
412                 to = openDevices.size();
413                 iterator = openDevices.subList(from, to).iterator();
414                 final StringBuilder payloadBuilder = new StringBuilder(
415                     prepareMessage(iterator.next(), editContentString));
416                 payload += modifyMessage(payloadBuilder, j, leftoverRequests);
417             }
418             if (leftoverRequests > 0 || leftoverBatchedRequests > 0) {
419
420                 if (payloads != null) {
421                     payloads.add(new Execution.DestToPayload(restconfNetconfTopologyPath, payload));
422                 }
423                 allThreadsPayloads.add(payloads);
424             }
425         } else {
426             final int requestPerThreads = openDevices.size() / threadAmount;
427             final int leftoverRequests = openDevices.size() % threadAmount;
428
429             for (int i = 0; i < threadAmount; i++) {
430                 from = i * requestPerThreads;
431                 to = from + requestPerThreads;
432                 iterator = openDevices.subList(from, to).iterator();
433                 allThreadsPayloads.add(createPayloads(iterator, editContentString));
434             }
435
436             if (leftoverRequests > 0) {
437                 from = threadAmount * requestPerThreads;
438                 to = from + leftoverRequests;
439                 iterator = openDevices.subList(from, to).iterator();
440                 allThreadsPayloads.add(createPayloads(iterator, editContentString));
441             }
442         }
443         return allThreadsPayloads;
444     }
445
446     private String prepareMessage(final int openDevice, final String editContentString) {
447         final StringBuilder messageBuilder = new StringBuilder(editContentString);
448
449         if (editContentString.contains(HOST_KEY)) {
450             messageBuilder.replace(messageBuilder.indexOf(HOST_KEY),
451                 messageBuilder.indexOf(HOST_KEY) + HOST_KEY.length(),
452                 generateConfigsAddress);
453         }
454         if (editContentString.contains(PORT_KEY)) {
455             while (messageBuilder.indexOf(PORT_KEY) != -1) {
456                 messageBuilder.replace(messageBuilder.indexOf(PORT_KEY),
457                     messageBuilder.indexOf(PORT_KEY) + PORT_KEY.length(),
458                     Integer.toString(openDevice));
459             }
460         }
461         if (editContentString.contains(TCP_ONLY)) {
462             messageBuilder.replace(messageBuilder.indexOf(TCP_ONLY),
463                 messageBuilder.indexOf(TCP_ONLY) + TCP_ONLY.length(),
464                 Boolean.toString(!ssh));
465         }
466         return messageBuilder.toString();
467     }
468
469     private ArrayList<Execution.DestToPayload> createPayloads(final Iterator<Integer> openDevices,
470                                                               final String editContentString) {
471         final ArrayList<Execution.DestToPayload> payloads = new ArrayList<>();
472
473         while (openDevices.hasNext()) {
474             //FIXME Move this to validate() and rename it to init() or create init() and move there.
475             //FIXME Make it field.
476             final String restconfNetconfTopologyPath = String.format(RESTCONF_NETCONF_TOPOLOGY_PATH_TEMPLATE,
477                     controllerIp, controllerPort);
478             payloads.add(new Execution.DestToPayload(
479                     restconfNetconfTopologyPath, prepareMessage(openDevices.next(), editContentString)));
480         }
481         return payloads;
482     }
483
484     private ArrayList<Execution.DestToPayload> createBatchedPayloads(final int batchedRequestsCount,
485             final Iterator<Integer> openDevices, final String editContentString, final String destination) {
486         final ArrayList<Execution.DestToPayload> payloads = new ArrayList<>();
487
488         for (int i = 0; i < batchedRequestsCount; i++) {
489             StringBuilder payload = new StringBuilder();
490             for (int j = 0; j < generateConfigBatchSize; j++) {
491                 final StringBuilder payloadBuilder = new StringBuilder(
492                     prepareMessage(openDevices.next(), editContentString));
493                 payload.append(modifyMessage(payloadBuilder, j, generateConfigBatchSize));
494             }
495             payloads.add(new Execution.DestToPayload(destination, payload.toString()));
496         }
497         return payloads;
498     }
499
500     @Override
501     public String toString() {
502         final List<Field> fields = Arrays.asList(this.getClass().getDeclaredFields());
503         final StringJoiner joiner = new StringJoiner(", \n", "TesttoolParameters{", "}\n");
504         fields.stream()
505                 .filter(field -> field.getAnnotation(Arg.class) != null)
506                 .map(this::getFieldString)
507                 .forEach(joiner::add);
508         return joiner.toString();
509     }
510
511     private String getFieldString(final Field field) {
512         try {
513             return field.getName() + "='" + field.get(this) + "'";
514         } catch (final IllegalAccessException e) {
515             return field.getName() + "= UNKNOWN";
516         }
517     }
518 }