BUG 2067 : Utility to automate the deployment of a cluster
[integration/test.git] / test / tools / cluster-deployer / deploy.py
1 #!/usr/local/bin/python
2 #
3 # This script deploys a cluster
4 # -------------------------------------------
5 #
6 # Pre-requisites
7 # - Python 2.7
8 # - SSHLibrary (pip install robotframework-sshlibrary)
9 # - pystache (pip install pystache)
10 # - argparse (pip install argparse)
11 #
12 # The input that this script will take is as follows,
13
14 # - A comma separated list of ip addresses/hostnames for each host on which the distribution needs to be deployed
15 # - The replication factor to be used
16 # - The ssh username/password of the remote host(s). Note that this should be the same for each host
17 # - The name of the template to be used. 
18 #   Note that this template name should match the name of a template folder in the templates directory.
19 #   The templates directory can be found in the same directory as this script.
20 #
21 # Here are the things it will do,
22 #
23 # - Copy over a distribution of opendaylight to the remote host
24 # - Create a timestamped directory on the remote host
25 # - Unzip the distribution to the timestamped directory
26 # - Copy over the template substituted configuration files to the appropriate location on the remote host
27 # - Create a symlink to the timestamped directory
28 # - Start karaf
29 #
30 # -------------------------------------------------------------------------------------------------------------
31
32 import argparse
33 from SSHLibrary import SSHLibrary
34 import time
35 import pystache
36 import os
37 import sys
38 import random
39
40 parser = argparse.ArgumentParser(description='Cluster Deployer')
41 parser.add_argument("--distribution", default="", help="the absolute path of the distribution on the local host that needs to be deployed", required=True)
42 parser.add_argument("--rootdir", default="/root", help="the root directory on the remote host where the distribution is to be deployed", required=True)
43 parser.add_argument("--hosts", default="", help="a comma separated list of host names or ip addresses", required=True)
44 parser.add_argument("--clean", action="store_true", default=False, help="clean the deployment on the remote host")
45 parser.add_argument("--template", default="openflow", help="the name of the template to be used. This name should match a folder in the templates directory.")
46 parser.add_argument("--rf", default=3, type=int, help="replication factor. This is the number of replicas that should be created for each shard.")
47 parser.add_argument("--user", default="root", help="the SSH username for the remote host(s)")
48 parser.add_argument("--password", default="Ecp123", help="the SSH password for the remote host(s)")
49 args = parser.parse_args()
50
51
52 # The RemoteHost class provides methods to do operations on a remote host
53 #
54 class RemoteHost:
55         def __init__(self, host, user, password, rootdir):
56                 self.host = host
57                 self.user = user
58                 self.password = password
59                 self.rootdir = rootdir
60
61         def exec_cmd(self, command):
62                 print "Executing command " + command + " on host " + self.host
63                 lib = SSHLibrary()
64                 lib.open_connection(self.host)
65                 lib.login(username=self.user,password=self.password)
66                 lib.execute_command(command)
67                 lib.close_connection()
68
69
70         def mkdir(self, dir_name):
71                 self.exec_cmd("mkdir -p " + dir_name)
72
73         def copy_file(self, src, dest):
74             lib = SSHLibrary()
75             lib.open_connection(self.host)
76             lib.login(username=self.user, password=self.password)
77             print "Copying " + src + " to " + dest + " on " + self.host
78             lib.put_file(src, dest)
79             lib.close_connection()
80
81         def kill_controller(self):
82             self.exec_cmd("ps axf | grep karaf | grep -v grep | awk '{print \"kill -9 \" $1}' | sh")
83
84         def start_controller(self, dir_name):
85                 self.exec_cmd(dir_name + "/odl/bin/start")
86
87 #
88 # The TemplateRenderer provides methods to render a template
89 #
90 class TemplateRenderer:
91     def __init__(self, template):
92             self.cwd = os.getcwd()
93             self.template_root = self.cwd + "/templates/" + template + "/"
94
95     def render(self, template_path, output_path, variables={}):
96         with open (self.template_root + template_path, "r") as myfile:
97             data=myfile.read()
98
99         parsed = pystache.parse(u"%(data)s" % locals())
100         renderer = pystache.Renderer()
101
102         output = renderer.render(parsed, variables)
103
104         with open (os.getcwd() + "/temp/" + output_path, "w") as myfile:
105                 myfile.write(output)
106         return os.getcwd() + "/temp/" + output_path
107
108 #
109 # The array_str method takes an array of strings and formats it into a string such that
110 # it can be used in an akka configuration file
111 #
112 def array_str(arr):
113     s = "["
114     for x in range(0, len(arr)):
115         s = s + '"' + arr[x] + '"'
116         if x < (len(arr) - 1):
117             s = s + ","
118     s = s + "]"
119     return s
120
121 #
122 # The Deployer deploys the controller to one host and configures it 
123 #
124 class Deployer:
125     def __init__(self, host, member_no, template, user, password, rootdir, distribution, 
126                 dir_name, hosts, ds_seed_nodes, rpc_seed_nodes, replicas, clean=False):
127         self.host = host
128         self.member_no = member_no
129         self.template = template
130         self.user = user
131         self.password = password
132         self.rootdir = rootdir
133         self.clean = clean
134         self.distribution = distribution
135         self.dir_name = dir_name
136         self.hosts = hosts
137         self.ds_seed_nodes = ds_seed_nodes
138         self.rpc_seed_nodes = rpc_seed_nodes
139         self.replicas = replicas
140
141     def deploy(self):
142         # Render all the templates
143         renderer = TemplateRenderer(self.template)
144         akka_conf = renderer.render("akka.conf.template", "akka.conf", 
145         {
146         "HOST" : self.host, 
147         "MEMBER_NAME" : "member-" + str(self.member_no), 
148         "DS_SEED_NODES" : array_str(self.ds_seed_nodes),
149         "RPC_SEED_NODES" : array_str(self.rpc_seed_nodes)
150         }
151         )
152         module_shards_conf = renderer.render("module-shards.conf.template", "module-shards.conf", self.replicas)
153         modules_conf = renderer.render("modules.conf.template", "modules.conf")
154         features_cfg = renderer.render("org.apache.karaf.features.cfg.template", "org.apache.karaf.features.cfg")
155         jolokia_xml = renderer.render("jolokia.xml.template", "jolokia.xml")
156         management_cfg = renderer.render("org.apache.karaf.management.cfg.template", "org.apache.karaf.management.cfg", {"HOST" : self.host})
157
158         # Connect to the remote host and start doing operations
159         remote = RemoteHost(self.host, self.user, self.password, self.rootdir)
160         remote.mkdir(self.dir_name)
161
162         # Delete all the sub-directories under the deploy directory if the --clean flag is used
163         if(self.clean == True):
164             remote.exec_cmd("rm -rf " + self.rootdir + "/deploy/*")
165         
166         # Clean the m2 repository
167         remote.exec_cmd("rm -rf " + self.rootdir + "/.m2/repository")
168
169         # Kill the controller if it's running
170         remote.kill_controller()
171
172         # Copy the distribution to the host and unzip it
173         odl_file_path = self.dir_name + "/odl.zip"
174         remote.copy_file(self.distribution, odl_file_path)
175         remote.exec_cmd("unzip " + odl_file_path + " -d " + self.dir_name + "/")
176
177         # Rename the distribution directory to odl
178         distribution_name = os.path.splitext(os.path.basename(self.distribution))[0]
179         remote.exec_cmd("mv " + self.dir_name + "/" + distribution_name + " " + self.dir_name + "/odl")
180
181         # Copy all the generated files to the server
182         remote.mkdir(self.dir_name + "/odl/configuration/initial")
183         remote.copy_file(akka_conf, self.dir_name + "/odl/configuration/initial/")
184         remote.copy_file(module_shards_conf, self.dir_name + "/odl/configuration/initial/")
185         remote.copy_file(modules_conf, self.dir_name + "/odl/configuration/initial/")
186         remote.copy_file(features_cfg, self.dir_name + "/odl/etc/")
187         remote.copy_file(jolokia_xml, self.dir_name + "/odl/deploy/")
188         remote.copy_file(management_cfg, self.dir_name + "/odl/etc/")
189
190         # Add symlink
191         remote.exec_cmd("ln -s " + self.dir_name + " " + args.rootdir + "/deploy/current")
192
193         # Run karaf
194         remote.start_controller(self.dir_name)      
195
196
197 def main():    
198     # Validate some input
199     if os.path.exists(args.distribution) == False:
200         print args.distribution + " is not a valid file"
201         sys.exit(1)
202
203     if os.path.exists(os.getcwd() + "/templates/" + args.template) == False:
204         print args.template + " is not a valid template"
205
206     # Prepare some 'global' variables
207     hosts = args.hosts.split(",")
208     time_stamp = time.time()
209     dir_name = args.rootdir + "/deploy/" + str(time_stamp)
210     distribution_name = os.path.splitext(os.path.basename(args.distribution))[0]
211
212     ds_seed_nodes = []
213     rpc_seed_nodes = []
214     all_replicas = []
215     replicas = {}
216
217     for x in range(0, len(hosts)):
218         ds_seed_nodes.append("akka.tcp://opendaylight-cluster-data@" + hosts[x] + ":2550")
219         rpc_seed_nodes.append("akka.tcp://odl-cluster-rpc@" + hosts[x] + ":2551")
220         all_replicas.append("member-" + str(x+1))
221
222
223     for x in range(0, 10):
224         if len(all_replicas) > args.rf:
225             replicas["REPLICAS_" + str(x+1)] = array_str(random.sample(all_replicas, args.rf))
226         else:
227             replicas["REPLICAS_" + str(x+1)] = array_str(all_replicas)
228
229     for x in range(0, len(hosts)):
230         Deployer(hosts[x], x+1, args.template, args.user, args.password, args.rootdir, args.distribution, dir_name, hosts, ds_seed_nodes, rpc_seed_nodes, replicas, args.clean).deploy()
231
232 # Run the script
233 main()