Fix cosmetic issues in VIRL topologies
[csit.git] / resources / tools / virl / bin / start-testcase
1 #!/usr/bin/python
2
3 # Copyright (c) 2016 Cisco and/or its affiliates.
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
16 __author__ = 'ckoester@cisco.com'
17
18 import sys
19 import requests
20 import re
21 import os
22 import argparse
23 import tempfile
24 import shutil
25 import time
26 import paramiko
27
28 #
29 # Helper function to indent a text string
30 #
31 def indent(lines, amount, fillchar=' '):
32     padding = amount * fillchar
33     return padding + ('\n'+padding).join(lines.split('\n'))
34
35 #
36 # Main function.
37 # FIXME: Right now, this is really coded like a shell script, as one big
38 # function executed in sequence. This should be broken down into multiple
39 # functions.
40 #
41 def main():
42     #
43     # Verify CLI parameters and try to download our VPP image into a temporary
44     # file first
45     #
46     parser = argparse.ArgumentParser()
47     parser.add_argument("topology", help="the base topology to be started")
48     parser.add_argument("packages", help="Path to the VPP .deb(s) that " +
49                         "is/are to be installed", nargs='+')
50     parser.add_argument("-c", "--copy", help="Copy the .deb packages, " +
51                         "leaving the originals in place. Default is to " +
52                         "move them.", action='store_true')
53     parser.add_argument("-k", "--keep", help="Keep (do not delete) the " +
54                         "simulation in case of error", action='store_true')
55     parser.add_argument("-v", "--verbosity", action="count", default=0)
56     # FIXME: THe default value for the following line should not be a hardcoded
57     # address. We should determine it dynamically (e.g. IP address of first
58     # interface or whichever interface is tied to the flat network)
59     parser.add_argument("-nip", "--nfs-server-ip", help="NFS server (our) IP",
60                         default="10.30.51.28")
61     parser.add_argument("-ns", "--nfs-scratch-directory",
62                         help="Server location for NFS scratch diretory",
63                         default="/nfs/scratch")
64     parser.add_argument("-nc", "--nfs-common-directory",
65                         help="Server location for NFS common (read-only) " +
66                         "directory", default="/nfs/common")
67     parser.add_argument("-wc", "--wait-count",
68                         help="number of intervals to wait for simulation to " +
69                         "be ready", type=int, default=12)
70     parser.add_argument("-wt", "--wait-time",
71                         help="length of a single interval to wait for " +
72                         "simulation to be ready", type=int, default=5)
73     parser.add_argument("-vip", "--virl-ip",
74                         help="VIRL IP and Port (e.g. 127.0.0.1:19399)",
75                         default="127.0.0.1:19399")
76     parser.add_argument("-u", "--username", help="VIRL username",
77                         default="tb4-virl")
78     parser.add_argument("-p", "--password", help="VIRL password",
79                         default="Cisco1234")
80     parser.add_argument("-su", "--ssh-user", help="SSH username",
81                         default="cisco")
82     parser.add_argument("-spr", "--ssh-privkey", help="SSH private keyfile",
83                         default="/home/jenkins-in/.ssh/id_rsa_virl")
84     parser.add_argument("-spu", "--ssh-pubkey", help="SSH public keyfile",
85                         default="/home/jenkins-in/.ssh/id_rsa_virl.pub")
86     parser.add_argument("--topology-directory", help="Topology directory",
87                         default="/home/jenkins-in/testcase-infra/topologies")
88
89     args = parser.parse_args()
90
91     #
92     # Check if topology and template exist
93     #
94     if args.verbosity >= 2:
95         print "DEBUG: Running with topology {}".format(args.topology)
96
97     topology_virl_filename = os.path.join(args.topology_directory,
98                                           args.topology + ".virl")
99     topology_yaml_filename = os.path.join(args.topology_directory,
100                                           args.topology + ".yaml")
101
102     if not os.path.isfile(topology_virl_filename):
103         print "ERROR: Topology VIRL file {} does not exist".\
104             format(topology_virl_filename)
105         sys.exit(1)
106     if not os.path.isfile(topology_yaml_filename):
107         print "ERROR: Topology YAML file {} does not exist".\
108             format(topology_yaml_filename)
109         sys.exit(1)
110
111     #
112     # Check if VPP package exists
113     #
114     for package in args.packages:
115         if args.verbosity >= 2:
116             print "DEBUG: Checking if file {} exists".format(package)
117         if not os.path.isfile(package):
118             print "ERROR: Debian package {} does not exist.".format(package)
119             sys.exit(1)
120
121     #
122     # Start VIRL topology
123     #
124     if args.verbosity >= 1:
125         print "DEBUG: Starting VIRL topology"
126     temp_handle, temp_topology = tempfile.mkstemp()
127     with open(args.ssh_pubkey, 'r') as pubkey_file:
128         pub_key = pubkey_file.read().replace('\n', '')
129     with open(temp_topology, 'w') as new_file, \
130         open(topology_virl_filename, 'r') as old_file:
131         for line in old_file:
132             line = line.replace("  - VIRL-USER-SSH-PUBLIC-KEY", "  - "+pub_key)
133             line = line.replace("$$NFS_SERVER_SCRATCH$$", \
134                 args.nfs_server_ip+":"+args.nfs_scratch_directory)
135             line = line.replace("$$NFS_SERVER_COMMON$$", \
136                 args.nfs_server_ip+":"+args.nfs_common_directory)
137             new_file.write(line)
138     os.close(temp_handle)
139
140     try:
141         new_file = open(temp_topology, 'rb')
142         headers = {'Content-Type': 'text/xml'}
143         req = requests.post('http://' + args.virl_ip + '/simengine/rest/launch',
144                             headers=headers,
145                             auth=(args.username, args.password), data=new_file)
146         if args.verbosity >= 2:
147             print "DEBUG: - Response Code {}".format(req.status_code)
148         new_file.close()
149
150     except:
151         print "ERROR: Launching VIRL simulation - received invalid response"
152         print req
153         os.remove(temp_topology)
154         sys.exit(1)
155
156     if req.status_code != 200:
157         print "ERROR: Launching VIRL simulation - received status other " + \
158             "than 200 HTTP OK"
159         print "Status was: {} \n".format(req.status_code)
160         print "Response content was: "
161         print req.content
162         os.remove(temp_topology)
163         sys.exit(1)
164
165     # If we got here, we had a good response. The response content is the
166     # session ID.
167     session_id = req.content
168
169     #
170     # Create simulation scratch directory. Move topology file into that
171     # directory. Copy or move debian packages into that directory.
172     #
173     scratch_directory = os.path.join(args.nfs_scratch_directory, session_id)
174     os.mkdir(scratch_directory)
175     shutil.move(temp_topology, os.path.join(scratch_directory,
176                                             "virl_topology.virl"))
177     os.mkdir(os.path.join(scratch_directory, "vpp"))
178     for package in args.packages:
179         if args.copy:
180             shutil.copy(package, os.path.join(scratch_directory, "vpp",
181                                               os.path.basename(package)))
182         else:
183             shutil.move(package, os.path.join(scratch_directory, "vpp",
184                                               os.path.basename(package)))
185
186     #
187     # Wait for simulation to become active
188     #
189     if args.verbosity >= 1:
190         print "DEBUG: Waiting for simulation to become active"
191
192     sim_is_started = False
193     nodelist = []
194
195     count = args.wait_count
196     while (count > 0) and not sim_is_started:
197         time.sleep(args.wait_time)
198         count -= 1
199
200         req = requests.get('http://' + args.virl_ip + '/simengine/rest/nodes/' +
201                            session_id, auth=(args.username, args.password))
202         data = req.json()
203
204         active = 0
205         total = 0
206
207         # Flush the node list every time, keep the last one
208         nodelist = []
209
210         # Hosts are the keys of the inner dictionary
211         for key in data[session_id].keys():
212             if data[session_id][key]['management-proxy'] == "self":
213                 continue
214             nodelist.append(key)
215             total += 1
216             if data[session_id][key]['state'] == "ACTIVE":
217                 active += 1
218         if args.verbosity >= 2:
219             print "DEBUG: - Attempt {} out of {}, total {} hosts, {} active".\
220                 format(args.wait_count-count, args.wait_count, total, active)
221         if active == total:
222             sim_is_started = True
223
224     if not sim_is_started:
225         print "ERROR: Simulation started OK but devices never changed to " + \
226             "ACTIVE state"
227         print "Last VIRL response:"
228         print data
229         if not args.keep:
230             shutil.rmtree(scratch_directory)
231             req = requests.get('http://' + args.virl_ip +
232                                '/simengine/rest/stop/' + session_id,
233                                auth=(args.username, args.password))
234
235     if args.verbosity >= 2:
236         print "DEBUG: Nodes: " + ", ".join(nodelist)
237
238     #
239     # Fetch simulation's IPs and create files
240     # (ansible hosts file, topology YAML file)
241     #
242     req = requests.get('http://' + args.virl_ip +
243                        '/simengine/rest/interfaces/' + session_id +
244                        '?fetch-state=1', auth=(args.username, args.password))
245     data = req.json()
246
247     # Populate node addresses
248     nodeaddrs = {}
249     topology = {}
250     for key in nodelist:
251         nodetype = re.split('[0-9]', key)[0]
252         if not nodetype in nodeaddrs:
253             nodeaddrs[nodetype] = {}
254         nodeaddrs[nodetype][key] = re.split('\\/', \
255             data[session_id][key]['management']['ip-address'])[0]
256         if args.verbosity >= 2:
257             print "DEBUG: Node {} is of type {} and has management IP {}".\
258                 format(key, nodetype, nodeaddrs[nodetype][key])
259
260         topology[key] = {}
261         for key2 in data[session_id][key]:
262             topology[key]["nic-"+key2] = data[session_id][key][key2]
263             if 'ip-address' in topology[key]["nic-"+key2]:
264                 if topology[key]["nic-"+key2]['ip-address'] is not None:
265                     topology[key]["nic-"+key2]['ip-addr'] = re.split('\\/', \
266                         topology[key]["nic-"+key2]['ip-address'])[0]
267
268     # Write ansible file
269     ansiblehosts = open(os.path.join(scratch_directory, 'ansible-hosts'), 'w')
270     for key1 in nodeaddrs:
271         ansiblehosts.write("[{}]\n".format(key1))
272         for key2 in nodeaddrs[key1]:
273             ansiblehosts.write("{} hostname={}\n".format(nodeaddrs[key1][key2],
274                                                          key2))
275     ansiblehosts.close()
276
277     # Process topology YAML template
278     with open(args.ssh_privkey, 'r') as privkey_file:
279         priv_key = indent(privkey_file.read(), 6)
280
281     with open(os.path.join(scratch_directory, "topology.yaml"), 'w') as \
282         new_file, open(topology_yaml_filename, 'r') as old_file:
283         for line in old_file:
284             new_file.write(line.format(priv_key=priv_key, topology=topology))
285
286     #
287     # Wait for hosts to become reachable over SSH
288     #
289     if args.verbosity >= 1:
290         print "DEBUG: Waiting for hosts to become reachable using SSH"
291
292     missing = -1
293     count = args.wait_count
294     while (count > 0) and missing != 0:
295         time.sleep(args.wait_time)
296         count -= 1
297
298         missing = 0
299         for key in nodelist:
300             if not os.path.exists(os.path.join(scratch_directory, key)):
301                 missing += 1
302         if args.verbosity >= 2:
303             print "DEBUG: - Attempt {} out of {}, waiting for {} hosts".\
304                 format(args.wait_count-count, args.wait_count, missing)
305
306     if missing != 0:
307         print "ERROR: Simulation started OK but {} hosts ".format(missing) + \
308             "never mounted their NFS directory"
309         if not args.keep:
310             shutil.rmtree(scratch_directory)
311             req = requests.get('http://' + args.virl_ip +
312                                '/simengine/rest/stop/' + session_id,
313                                auth=(args.username, args.password))
314
315     #
316     # Upgrade VPP
317     #
318     if args.verbosity >= 1:
319         print "DEBUG: Uprading VPP"
320
321     for key1 in nodeaddrs:
322         if not key1 == 'tg':
323             for key2 in nodeaddrs[key1]:
324                 ipaddr = nodeaddrs[key1][key2]
325                 if args.verbosity >= 2:
326                     print "DEBUG: Upgrading VPP on node {}".format(ipaddr)
327                 paramiko.util.log_to_file(os.path.join(scratch_directory,
328                                                        "ssh.log"))
329                 client = paramiko.SSHClient()
330                 client.load_system_host_keys()
331                 client.load_host_keys("/dev/null")
332                 client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
333                 client.connect(ipaddr, username=args.ssh_user,
334                                key_filename=args.ssh_privkey)
335                 stdin, stdout, stderr = \
336                     client.exec_command('sudo dpkg -i /scratch/vpp/*deb')
337                 c_stdout = stdout.read()
338                 c_stderr = stderr.read()
339                 if args.verbosity >= 2:
340                     print "DEBUG: Command output was:"
341                     print c_stdout
342                     print "DEBUG: Command stderr was:"
343                     print c_stderr
344
345     #
346     # Write a file with timestamp to scratch directory. We can use this to track
347     # how long a simulation has been running.
348     #
349     with open(os.path.join(scratch_directory, 'start_time'), 'a') as \
350         timestampfile:
351         timestampfile.write('{}\n'.format(int(time.time())))
352
353     #
354     # Declare victory
355     #
356     if args.verbosity >= 1:
357         print "SESSION ID: {}".format(session_id)
358
359     print "{}".format(session_id)
360
361 if __name__ == "__main__":
362     sys.exit(main())