Bump pre-commit black to 22.1.0
[integration/test.git] / tools / clustering / cluster-deployer / deploy.py
1 #!/usr/bin/env 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
15 #   the distribution needs to be deployed
16 # - The replication factor to be used
17 # - The ssh username/password of the remote host(s). Note that this should be
18 #   the same for each host
19 # - The name of the template to be used.
20 #   Note that this template name should match the name of a template folder in
21 #   the templates directory.
22 #   The templates directory can be found in the same directory as this script.
23 #
24 # Here are the things it will do,
25 #
26 # - Copy over a distribution of opendaylight to the remote host
27 # - Create a timestamped directory on the remote host
28 # - Unzip the distribution to the timestamped directory
29 # - Copy over the template substituted configuration files to the appropriate
30 #   location on the remote host
31 # - Create a symlink to the timestamped directory
32 # - Start karaf
33 #
34 # ----------------------------------------------------------------------------
35
36 import argparse
37 import time
38 import pystache
39 import os
40 import sys
41 import random
42 import re
43 from remote_host import RemoteHost
44
45 parser = argparse.ArgumentParser(description="Cluster Deployer")
46 parser.add_argument(
47     "--distribution",
48     default="",
49     help="the absolute path of the distribution on the local "
50     "host that needs to be deployed. (Must contain "
51     'version in the form: "<#>.<#>.<#>-<name>", e.g. '
52     "0.2.0-SNAPSHOT)",
53     required=True,
54 )
55 parser.add_argument(
56     "--rootdir",
57     default="/root",
58     help="the root directory on the remote host where the "
59     "distribution is to be deployed",
60     required=True,
61 )
62 parser.add_argument(
63     "--hosts",
64     default="",
65     help="a comma separated list of " "host names or ip addresses",
66     required=True,
67 )
68 parser.add_argument(
69     "--clean",
70     action="store_true",
71     default=False,
72     help="clean the deployment on the remote host",
73 )
74 parser.add_argument(
75     "--template",
76     default="openflow",
77     help="the name of the template to be used. "
78     "This name should match a folder in the templates "
79     "directory.",
80 )
81 parser.add_argument(
82     "--rf",
83     default=3,
84     type=int,
85     help="replication factor. This is the number of replicas "
86     "that should be created for each shard.",
87 )
88 parser.add_argument(
89     "--user", default="root", help="the SSH username for the " "remote host(s)"
90 )
91 parser.add_argument(
92     "--password", default="Ecp123", help="the SSH password for the remote host(s)"
93 )
94 args = parser.parse_args()
95
96
97 #
98 # The TemplateRenderer provides methods to render a template
99 #
100 class TemplateRenderer:
101     def __init__(self, template):
102         self.cwd = os.getcwd()
103         self.template_root = self.cwd + "/templates/" + template + "/"
104
105     def render(self, template_path, output_path, variables=None):
106         if variables is None:
107             variables = {}
108
109         if os.path.exists(self.template_root + template_path) is False:
110             return
111
112         with open(self.template_root + template_path, "r") as myfile:
113             data = myfile.read()
114
115         parsed = pystache.parse("%(data)s" % locals())
116         renderer = pystache.Renderer()
117
118         output = renderer.render(parsed, variables)
119
120         with open(os.getcwd() + "/temp/" + output_path, "w") as myfile:
121             myfile.write(output)
122         return os.getcwd() + "/temp/" + output_path
123
124
125 #
126 # The array_str method takes an array of strings and formats it into a
127 #  string such that it can be used in an akka configuration file
128 #
129 def array_str(arr):
130     s = "["
131     for x in range(0, len(arr)):
132         s = s + '"' + arr[x] + '"'
133         if x < (len(arr) - 1):
134             s += ","
135     s += "]"
136     return s
137
138
139 #
140 # The Deployer deploys the controller to one host and configures it
141 #
142 class Deployer:
143     def __init__(
144         self,
145         host,
146         member_no,
147         template,
148         user,
149         password,
150         rootdir,
151         distribution,
152         dir_name,
153         hosts,
154         ds_seed_nodes,
155         rpc_seed_nodes,
156         replicas,
157         clean=False,
158     ):
159         self.host = host
160         self.member_no = member_no
161         self.template = template
162         self.user = user
163         self.password = password
164         self.rootdir = rootdir
165         self.clean = clean
166         self.distribution = distribution
167         self.dir_name = dir_name
168         self.hosts = hosts
169         self.ds_seed_nodes = ds_seed_nodes
170         self.rpc_seed_nodes = rpc_seed_nodes
171         self.replicas = replicas
172
173         # Connect to the remote host and start doing operations
174         self.remote = RemoteHost(self.host, self.user, self.password, self.rootdir)
175
176     def kill_controller(self):
177         self.remote.copy_file("kill_controller.sh", self.rootdir + "/")
178         self.remote.exec_cmd(self.rootdir + "/kill_controller.sh")
179
180     def deploy(self):
181         # Determine distribution version
182         distribution_name = os.path.splitext(os.path.basename(self.distribution))[0]
183         distribution_ver = re.search(
184             r"(\d+\.\d+\.\d+-\w+\Z)|"
185             r"(\d+\.\d+\.\d+-\w+)(-RC\d+\Z)|"
186             r"(\d+\.\d+\.\d+-\w+)(-RC\d+(\.\d+)\Z)|"
187             r"(\d+\.\d+\.\d+-\w+)(-SR\d+\Z)|"
188             r"(\d+\.\d+\.\d+-\w+)(-SR\d+(\.\d+)\Z)",
189             distribution_name,
190         )  # noqa
191
192         if distribution_ver is None:
193             print(
194                 "%s is not a valid distribution version."
195                 " (Must contain version in the form: "
196                 '"<#>.<#>.<#>-<name>" or "<#>.<#>.'
197                 '<#>-<name>-SR<#>" or "<#>.<#>.<#>'
198                 '-<name>-RC<#>", e.g. 0.2.0-SNAPSHOT)' % distribution_name
199             )
200             sys.exit(1)
201         distribution_ver = distribution_ver.group()
202
203         # Render all the templates
204         renderer = TemplateRenderer(self.template)
205         akka_conf = renderer.render(
206             "akka.conf.template",
207             "akka.conf",
208             {
209                 "HOST": self.host,
210                 "MEMBER_NAME": "member-" + str(self.member_no),
211                 "DS_SEED_NODES": array_str(self.ds_seed_nodes),
212                 "RPC_SEED_NODES": array_str(self.rpc_seed_nodes),
213             },
214         )
215         module_shards_conf = renderer.render(
216             "module-shards.conf.template", "module-shards.conf", self.replicas
217         )
218         modules_conf = renderer.render("modules.conf.template", "modules.conf")
219         features_cfg = renderer.render(
220             "org.apache.karaf.features.cfg.template",
221             "org.apache.karaf.features.cfg",
222             {"ODL_DISTRIBUTION": distribution_ver},
223         )
224         jolokia_xml = renderer.render("jolokia.xml.template", "jolokia.xml")
225         management_cfg = renderer.render(
226             "org.apache.karaf.management.cfg.template",
227             "org.apache.karaf.management.cfg",
228             {"HOST": self.host},
229         )
230         datastore_cfg = renderer.render(
231             "org.opendaylight.controller.cluster.datastore.cfg.template",
232             "org.opendaylight.controller.cluster.datastore.cfg",
233         )
234
235         # Delete all the sub-directories under the deploy directory if
236         # the --clean flag is used
237         if self.clean is True:
238             self.remote.exec_cmd("rm -rf " + self.rootdir + "/deploy/*")
239
240         # Create the deployment directory
241         self.remote.mkdir(self.dir_name)
242
243         # Clean the m2 repository
244         self.remote.exec_cmd("rm -rf " + self.rootdir + "/.m2/repository")
245
246         # Copy the distribution to the host and unzip it
247         odl_file_path = self.dir_name + "/odl.zip"
248         self.remote.copy_file(self.distribution, odl_file_path)
249         self.remote.exec_cmd("unzip -o " + odl_file_path + " -d " + self.dir_name + "/")
250
251         # Rename the distribution directory to odl
252         self.remote.exec_cmd(
253             "mv "
254             + self.dir_name
255             + "/"
256             + distribution_name
257             + " "
258             + self.dir_name
259             + "/odl"
260         )
261
262         # Copy all the generated files to the server
263         self.remote.mkdir(self.dir_name + "/odl/configuration/initial")
264         self.remote.copy_file(akka_conf, self.dir_name + "/odl/configuration/initial/")
265         self.remote.copy_file(
266             module_shards_conf, self.dir_name + "/odl/configuration/initial/"
267         )
268         self.remote.copy_file(
269             modules_conf, self.dir_name + "/odl/configuration/initial/"
270         )
271         self.remote.copy_file(features_cfg, self.dir_name + "/odl/etc/")
272         self.remote.copy_file(jolokia_xml, self.dir_name + "/odl/deploy/")
273         self.remote.copy_file(management_cfg, self.dir_name + "/odl/etc/")
274
275         if datastore_cfg is not None:
276             self.remote.copy_file(datastore_cfg, self.dir_name + "/odl/etc/")
277
278         # Add symlink
279         self.remote.exec_cmd(
280             "ln -sfn " + self.dir_name + " " + args.rootdir + "/deploy/current"
281         )
282
283         # Run karaf
284         self.remote.start_controller(self.dir_name)
285
286
287 def main():
288     # Validate some input
289     if os.path.exists(args.distribution) is False:
290         print("%s is not a valid file" % args.distribution)
291         sys.exit(1)
292
293     if os.path.exists(os.getcwd() + "/templates/" + args.template) is False:
294         print("%s is not a valid template" % args.template)
295
296     # Prepare some 'global' variables
297     hosts = args.hosts.split(",")
298     time_stamp = time.time()
299     dir_name = args.rootdir + "/deploy/" + str(time_stamp)
300
301     ds_seed_nodes = []
302     rpc_seed_nodes = []
303     all_replicas = []
304     replicas = {}
305
306     for x in range(0, len(hosts)):
307         ds_seed_nodes.append(
308             "akka.tcp://opendaylight-cluster-data@" + hosts[x] + ":2550"
309         )
310         rpc_seed_nodes.append("akka.tcp://odl-cluster-rpc@" + hosts[x] + ":2551")
311         all_replicas.append("member-" + str(x + 1))
312
313     for x in range(0, 10):
314         if len(all_replicas) > args.rf:
315             replicas["REPLICAS_" + str(x + 1)] = array_str(
316                 random.sample(all_replicas, args.rf)
317             )
318         else:
319             replicas["REPLICAS_" + str(x + 1)] = array_str(all_replicas)
320
321     deployers = []
322
323     for x in range(0, len(hosts)):
324         deployers.append(
325             Deployer(
326                 hosts[x],
327                 x + 1,
328                 args.template,
329                 args.user,
330                 args.password,
331                 args.rootdir,
332                 args.distribution,
333                 dir_name,
334                 hosts,
335                 ds_seed_nodes,
336                 rpc_seed_nodes,
337                 replicas,
338                 args.clean,
339             )
340         )
341
342     for x in range(0, len(hosts)):
343         deployers[x].kill_controller()
344
345     for x in range(0, len(hosts)):
346         deployers[x].deploy()
347
348
349 # Run the script
350 main()