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