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