From: Moiz Raja Date: Wed, 24 Sep 2014 20:04:21 +0000 (-0700) Subject: BUG 2067 : Utility to automate the deployment of a cluster X-Git-Tag: release/helium-sr1~21^2 X-Git-Url: https://git.opendaylight.org/gerrit/gitweb?a=commitdiff_plain;h=51d9dc0d46b8d9b6dc81f6af88679d9f705e4b00;p=integration%2Ftest.git BUG 2067 : Utility to automate the deployment of a cluster First pass at automating the deployment of a cluster. To deploy a cluster do the following, - cd to the cluster-deployer directory - ./deploy.py --distribution= --hosts= --rootdir= --clean To get usage unformation on the script - ./deploy.py -h There are some pre-requisites for the script which are mentioned as documentation at the top of the script Change-Id: I1429f28ff4c6e565d44bfb1567f821ae2fb8454e Signed-off-by: Moiz Raja --- diff --git a/test/tools/cluster-deployer/deploy.py b/test/tools/cluster-deployer/deploy.py new file mode 100755 index 0000000000..a0f9a1407b --- /dev/null +++ b/test/tools/cluster-deployer/deploy.py @@ -0,0 +1,233 @@ +#!/usr/local/bin/python +# +# This script deploys a cluster +# ------------------------------------------- +# +# Pre-requisites +# - Python 2.7 +# - SSHLibrary (pip install robotframework-sshlibrary) +# - pystache (pip install pystache) +# - argparse (pip install argparse) +# +# The input that this script will take is as follows, +# +# - A comma separated list of ip addresses/hostnames for each host on which the distribution needs to be deployed +# - The replication factor to be used +# - The ssh username/password of the remote host(s). Note that this should be the same for each host +# - The name of the template to be used. +# Note that this template name should match the name of a template folder in the templates directory. +# The templates directory can be found in the same directory as this script. +# +# Here are the things it will do, +# +# - Copy over a distribution of opendaylight to the remote host +# - Create a timestamped directory on the remote host +# - Unzip the distribution to the timestamped directory +# - Copy over the template substituted configuration files to the appropriate location on the remote host +# - Create a symlink to the timestamped directory +# - Start karaf +# +# ------------------------------------------------------------------------------------------------------------- + +import argparse +from SSHLibrary import SSHLibrary +import time +import pystache +import os +import sys +import random + +parser = argparse.ArgumentParser(description='Cluster Deployer') +parser.add_argument("--distribution", default="", help="the absolute path of the distribution on the local host that needs to be deployed", required=True) +parser.add_argument("--rootdir", default="/root", help="the root directory on the remote host where the distribution is to be deployed", required=True) +parser.add_argument("--hosts", default="", help="a comma separated list of host names or ip addresses", required=True) +parser.add_argument("--clean", action="store_true", default=False, help="clean the deployment on the remote host") +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.") +parser.add_argument("--rf", default=3, type=int, help="replication factor. This is the number of replicas that should be created for each shard.") +parser.add_argument("--user", default="root", help="the SSH username for the remote host(s)") +parser.add_argument("--password", default="Ecp123", help="the SSH password for the remote host(s)") +args = parser.parse_args() + +# +# The RemoteHost class provides methods to do operations on a remote host +# +class RemoteHost: + def __init__(self, host, user, password, rootdir): + self.host = host + self.user = user + self.password = password + self.rootdir = rootdir + + def exec_cmd(self, command): + print "Executing command " + command + " on host " + self.host + lib = SSHLibrary() + lib.open_connection(self.host) + lib.login(username=self.user,password=self.password) + lib.execute_command(command) + lib.close_connection() + + + def mkdir(self, dir_name): + self.exec_cmd("mkdir -p " + dir_name) + + def copy_file(self, src, dest): + lib = SSHLibrary() + lib.open_connection(self.host) + lib.login(username=self.user, password=self.password) + print "Copying " + src + " to " + dest + " on " + self.host + lib.put_file(src, dest) + lib.close_connection() + + def kill_controller(self): + self.exec_cmd("ps axf | grep karaf | grep -v grep | awk '{print \"kill -9 \" $1}' | sh") + + def start_controller(self, dir_name): + self.exec_cmd(dir_name + "/odl/bin/start") + +# +# The TemplateRenderer provides methods to render a template +# +class TemplateRenderer: + def __init__(self, template): + self.cwd = os.getcwd() + self.template_root = self.cwd + "/templates/" + template + "/" + + def render(self, template_path, output_path, variables={}): + with open (self.template_root + template_path, "r") as myfile: + data=myfile.read() + + parsed = pystache.parse(u"%(data)s" % locals()) + renderer = pystache.Renderer() + + output = renderer.render(parsed, variables) + + with open (os.getcwd() + "/temp/" + output_path, "w") as myfile: + myfile.write(output) + return os.getcwd() + "/temp/" + output_path + +# +# The array_str method takes an array of strings and formats it into a string such that +# it can be used in an akka configuration file +# +def array_str(arr): + s = "[" + for x in range(0, len(arr)): + s = s + '"' + arr[x] + '"' + if x < (len(arr) - 1): + s = s + "," + s = s + "]" + return s + +# +# The Deployer deploys the controller to one host and configures it +# +class Deployer: + def __init__(self, host, member_no, template, user, password, rootdir, distribution, + dir_name, hosts, ds_seed_nodes, rpc_seed_nodes, replicas, clean=False): + self.host = host + self.member_no = member_no + self.template = template + self.user = user + self.password = password + self.rootdir = rootdir + self.clean = clean + self.distribution = distribution + self.dir_name = dir_name + self.hosts = hosts + self.ds_seed_nodes = ds_seed_nodes + self.rpc_seed_nodes = rpc_seed_nodes + self.replicas = replicas + + def deploy(self): + # Render all the templates + renderer = TemplateRenderer(self.template) + akka_conf = renderer.render("akka.conf.template", "akka.conf", + { + "HOST" : self.host, + "MEMBER_NAME" : "member-" + str(self.member_no), + "DS_SEED_NODES" : array_str(self.ds_seed_nodes), + "RPC_SEED_NODES" : array_str(self.rpc_seed_nodes) + } + ) + module_shards_conf = renderer.render("module-shards.conf.template", "module-shards.conf", self.replicas) + modules_conf = renderer.render("modules.conf.template", "modules.conf") + features_cfg = renderer.render("org.apache.karaf.features.cfg.template", "org.apache.karaf.features.cfg") + jolokia_xml = renderer.render("jolokia.xml.template", "jolokia.xml") + management_cfg = renderer.render("org.apache.karaf.management.cfg.template", "org.apache.karaf.management.cfg", {"HOST" : self.host}) + + # Connect to the remote host and start doing operations + remote = RemoteHost(self.host, self.user, self.password, self.rootdir) + remote.mkdir(self.dir_name) + + # Delete all the sub-directories under the deploy directory if the --clean flag is used + if(self.clean == True): + remote.exec_cmd("rm -rf " + self.rootdir + "/deploy/*") + + # Clean the m2 repository + remote.exec_cmd("rm -rf " + self.rootdir + "/.m2/repository") + + # Kill the controller if it's running + remote.kill_controller() + + # Copy the distribution to the host and unzip it + odl_file_path = self.dir_name + "/odl.zip" + remote.copy_file(self.distribution, odl_file_path) + remote.exec_cmd("unzip " + odl_file_path + " -d " + self.dir_name + "/") + + # Rename the distribution directory to odl + distribution_name = os.path.splitext(os.path.basename(self.distribution))[0] + remote.exec_cmd("mv " + self.dir_name + "/" + distribution_name + " " + self.dir_name + "/odl") + + # Copy all the generated files to the server + remote.mkdir(self.dir_name + "/odl/configuration/initial") + remote.copy_file(akka_conf, self.dir_name + "/odl/configuration/initial/") + remote.copy_file(module_shards_conf, self.dir_name + "/odl/configuration/initial/") + remote.copy_file(modules_conf, self.dir_name + "/odl/configuration/initial/") + remote.copy_file(features_cfg, self.dir_name + "/odl/etc/") + remote.copy_file(jolokia_xml, self.dir_name + "/odl/deploy/") + remote.copy_file(management_cfg, self.dir_name + "/odl/etc/") + + # Add symlink + remote.exec_cmd("ln -s " + self.dir_name + " " + args.rootdir + "/deploy/current") + + # Run karaf + remote.start_controller(self.dir_name) + + +def main(): + # Validate some input + if os.path.exists(args.distribution) == False: + print args.distribution + " is not a valid file" + sys.exit(1) + + if os.path.exists(os.getcwd() + "/templates/" + args.template) == False: + print args.template + " is not a valid template" + + # Prepare some 'global' variables + hosts = args.hosts.split(",") + time_stamp = time.time() + dir_name = args.rootdir + "/deploy/" + str(time_stamp) + distribution_name = os.path.splitext(os.path.basename(args.distribution))[0] + + ds_seed_nodes = [] + rpc_seed_nodes = [] + all_replicas = [] + replicas = {} + + for x in range(0, len(hosts)): + ds_seed_nodes.append("akka.tcp://opendaylight-cluster-data@" + hosts[x] + ":2550") + rpc_seed_nodes.append("akka.tcp://odl-cluster-rpc@" + hosts[x] + ":2551") + all_replicas.append("member-" + str(x+1)) + + + for x in range(0, 10): + if len(all_replicas) > args.rf: + replicas["REPLICAS_" + str(x+1)] = array_str(random.sample(all_replicas, args.rf)) + else: + replicas["REPLICAS_" + str(x+1)] = array_str(all_replicas) + + for x in range(0, len(hosts)): + 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() + +# Run the script +main() diff --git a/test/tools/cluster-deployer/temp/README b/test/tools/cluster-deployer/temp/README new file mode 100644 index 0000000000..dc2a9e1229 --- /dev/null +++ b/test/tools/cluster-deployer/temp/README @@ -0,0 +1 @@ +README - to ensure this directory exists \ No newline at end of file diff --git a/test/tools/cluster-deployer/templates/openflow/akka.conf.template b/test/tools/cluster-deployer/templates/openflow/akka.conf.template new file mode 100644 index 0000000000..738d538994 --- /dev/null +++ b/test/tools/cluster-deployer/templates/openflow/akka.conf.template @@ -0,0 +1,83 @@ + +odl-cluster-data { + bounded-mailbox { + mailbox-type = "org.opendaylight.controller.cluster.common.actor.MeteredBoundedMailbox" + mailbox-capacity = 1000 + mailbox-push-timeout-time = 100ms + } + + metric-capture-enabled = true + + akka { + loglevel = "INFO" + loggers = ["akka.event.slf4j.Slf4jLogger"] + + actor { + + provider = "akka.cluster.ClusterActorRefProvider" + serializers { + java = "akka.serialization.JavaSerializer" + proto = "akka.remote.serialization.ProtobufSerializer" + } + + serialization-bindings { + "com.google.protobuf.Message" = proto + + } + } + remote { + log-remote-lifecycle-events = off + netty.tcp { + hostname = "{{HOST}}" + port = 2550 + maximum-frame-size = 419430400 + send-buffer-size = 52428800 + receive-buffer-size = 52428800 + } + } + + cluster { + seed-nodes = {{{DS_SEED_NODES}}} + + auto-down-unreachable-after = 10s + + roles = [ + "{{MEMBER_NAME}}" + ] + + } + } +} + +odl-cluster-rpc { + bounded-mailbox { + mailbox-type = "org.opendaylight.controller.cluster.common.actor.MeteredBoundedMailbox" + mailbox-capacity = 1000 + mailbox-push-timeout-time = 100ms + } + + metric-capture-enabled = true + + akka { + loglevel = "INFO" + loggers = ["akka.event.slf4j.Slf4jLogger"] + + actor { + provider = "akka.cluster.ClusterActorRefProvider" + + } + remote { + log-remote-lifecycle-events = off + netty.tcp { + hostname = "{{HOST}}" + port = 2551 + } + } + + cluster { + seed-nodes = {{{RPC_SEED_NODES}}} + + auto-down-unreachable-after = 10s + } + } +} diff --git a/test/tools/cluster-deployer/templates/openflow/jolokia.xml.template b/test/tools/cluster-deployer/templates/openflow/jolokia.xml.template new file mode 100644 index 0000000000..5a3756048f --- /dev/null +++ b/test/tools/cluster-deployer/templates/openflow/jolokia.xml.template @@ -0,0 +1,5 @@ + + + mvn:org.jolokia/jolokia-osgi/1.1.5 + + \ No newline at end of file diff --git a/test/tools/cluster-deployer/templates/openflow/module-shards.conf.template b/test/tools/cluster-deployer/templates/openflow/module-shards.conf.template new file mode 100644 index 0000000000..540747a2ca --- /dev/null +++ b/test/tools/cluster-deployer/templates/openflow/module-shards.conf.template @@ -0,0 +1,65 @@ +# This file describes which shards live on which members +# The format for a module-shards is as follows, +# { +# name = "" +# shards = [ +# { +# name="" +# replicas = [ +# "" +# ] +# ] +# } +# +# For Helium we support only one shard per module. Beyond Helium +# we will support more than 1 +# The replicas section is a collection of member names. This information +# will be used to decide on which members replicas of a particular shard will be +# located. Once replication is integrated with the distributed data store then +# this section can have multiple entries. +# +# + + +module-shards = [ + { + name = "default" + shards = [ + { + name="default" + replicas = {{{REPLICAS_1}}} + + } + ] + }, + { + name = "topology" + shards = [ + { + name="topology" + replicas = {{{REPLICAS_2}}} + + } + ] + }, + { + name = "inventory" + shards = [ + { + name="inventory" + replicas = {{{REPLICAS_3}}} + + } + ] + }, + { + name = "toaster" + shards = [ + { + name="toaster" + replicas = {{{REPLICAS_4}}} + } + ] + } + +] diff --git a/test/tools/cluster-deployer/templates/openflow/modules.conf.template b/test/tools/cluster-deployer/templates/openflow/modules.conf.template new file mode 100644 index 0000000000..68347eeda9 --- /dev/null +++ b/test/tools/cluster-deployer/templates/openflow/modules.conf.template @@ -0,0 +1,32 @@ +# This file should describe all the modules that need to be placed in a separate shard +# The format of the configuration is as follows +# { +# name = "" +# namespace = "" +# shard-strategy = "module" +# } +# +# Note that at this time the only shard-strategy we support is module which basically +# will put all the data of a single module in two shards (one for config and one for +# operational data) + +modules = [ + { + name = "inventory" + namespace = "urn:opendaylight:inventory" + shard-strategy = "module" + }, + + { + name = "topology" + namespace = "urn:TBD:params:xml:ns:yang:network-topology" + shard-strategy = "module" + }, + + { + name = "toaster" + namespace = "http://netconfcentral.org/ns/toaster" + shard-strategy = "module" + } + +] diff --git a/test/tools/cluster-deployer/templates/openflow/org.apache.karaf.features.cfg.template b/test/tools/cluster-deployer/templates/openflow/org.apache.karaf.features.cfg.template new file mode 100644 index 0000000000..8b542ace5a --- /dev/null +++ b/test/tools/cluster-deployer/templates/openflow/org.apache.karaf.features.cfg.template @@ -0,0 +1,49 @@ +################################################################################ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +################################################################################ + +# +# Defines if the startlvl should be respected during feature startup. The default value is true. The default +# behavior for 2.x is false (!) for this property +# +# Be aware that this property is deprecated and will be removed in Karaf 4.0. So, if you need to +# set this to false, please use this only as a temporary solution! +# +#respectStartLvlDuringFeatureStartup=true + + +# +# Defines if the startlvl should be respected during feature uninstall. The default value is true. +# If true, means stop bundles respecting the descend order of start level in a certain feature. +# +#respectStartLvlDuringFeatureUninstall=true + +# +# Comma separated list of features repositories to register by default +# +featuresRepositories = mvn:org.apache.karaf.features/standard/3.0.1/xml/features,mvn:org.apache.karaf.features/enterprise/3.0.1/xml/features,mvn:org.ops4j.pax.web/pax-web-features/3.1.0/xml/features,mvn:org.apache.karaf.features/spring/3.0.1/xml/features,mvn:org.opendaylight.integration/features-integration/0.2.0-SNAPSHOT/xml/features + +# +# Comma separated list of features to install at startup +# +featuresBoot=config,standard,region,package,kar,ssh,management,odl-openflowplugin-flow-services,odl-restconf,odl-mdsal-clustering,feature-jolokia + +# +# Defines if the boot features are started in asynchronous mode (in a dedicated thread) +# +featuresBootAsynchronous=false diff --git a/test/tools/cluster-deployer/templates/openflow/org.apache.karaf.management.cfg.template b/test/tools/cluster-deployer/templates/openflow/org.apache.karaf.management.cfg.template new file mode 100644 index 0000000000..510eebd81e --- /dev/null +++ b/test/tools/cluster-deployer/templates/openflow/org.apache.karaf.management.cfg.template @@ -0,0 +1,63 @@ +################################################################################ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +################################################################################ + +# +# The properties in this file define the configuration of Apache Karaf's JMX Management +# + +# +# Port number for RMI registry connection +# +rmiRegistryPort = 1099 + +# +# Port number for RMI server connection +# +rmiServerPort = 44444 + +# +# Name of the JAAS realm used for authentication +# +jmxRealm = karaf + +# +# The service URL for the JMXConnectorServer +# +serviceUrl = service:jmx:rmi://{{HOST}}:${rmiServerPort}/jndi/rmi://{{HOST}}:${rmiRegistryPort}/karaf-${karaf.name} + +# +# Whether any threads started for the JMXConnectorServer should be started as daemon threads +# +daemon = true + +# +# Whether the JMXConnectorServer should be started in a separate thread +# +threaded = true + +# +# The ObjectName used to register the JMXConnectorServer +# +objectName = connector:name=rmi + +# +# Role name used for JMX access authorization +# If not set, this defaults to the ${karaf.admin.role} configured in etc/system.properties +# +# jmxRole=admin