Skip to content

Commit 797f4ae

Browse files
author
Sylvain Lebresne
committed
Initial commit
0 parents  commit 797f4ae

File tree

15 files changed

+1410
-0
lines changed

15 files changed

+1410
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
*.pyc

README

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
CCM (for Cassandra Cluster Manager)
2+
===================================
3+
4+
A script to create, launch and remove a Apache Cassandra cluster on localhost.
5+
6+
The goal of ccm is to make is easy to create, manage and destroy a small
7+
cluster on a local box. It is meant for quick testing on a Cassandra cluster.
8+
9+
10+
Install
11+
-------
12+
13+
As far as I know, this uses only standard python modules, so as long as you
14+
have python installed, you should be good to go. Simply clone this repository
15+
where you want.
16+
17+
Once cloned, you'll probably want to create a symbolic link to the ccm
18+
executable script somewhere in your path (The following examples assume that much).
19+
20+
ccm is only for cluster on localhost so if you want more than one node, you
21+
will likely need multiple loopback interface aliases. On mac os x for
22+
instance, you can create such aliases with
23+
sudo ifconfig lo0 alias 127.0.0.2 up
24+
sudo ifconfig lo0 alias 127.0.0.3 up
25+
...
26+
27+
I'll assume you have at least 127.0.0.1, 127.0.0.2 and 127.0.0.3 set up in
28+
the next section.
29+
30+
31+
Usage
32+
-----
33+
34+
ccm works from a Cassandra source tree. So in the following example, I assume
35+
that 'ccm' is in the path and that current directory is a Cassandra source
36+
directory (either 0.7 or trunk, this doesn't work with 0.6, though that could
37+
be added easily enough if there is some interest). It also assumes that
38+
Cassandra has been compiled (with 'ant build').
39+
40+
ccm work with the notion of a current cluster. To create a cluster and
41+
'switch' to it:
42+
> ccm create test
43+
44+
Then add some node:
45+
> ccm add node1 -i 127.0.0.1 -j 7100 -s
46+
> ccm add node2 -i 127.0.0.2 -j 7200 -s
47+
48+
This add 2 nodes on 127.0.0.1 and 127.0.0.2 using default thrift and storage
49+
port using jmx port 7100 and 7200 (JMX binds itself to all interfaces by
50+
default, so you want 2 separate ports here).
51+
Moreover, those are set as seeds ('-s' flag; you need at least one seed node).
52+
53+
You can then start the whole cluster:
54+
> ccm start
55+
56+
You can check that everything is working ok:
57+
> ccm node1 ring
58+
59+
which simply call nodetool ring on node1.
60+
61+
You can now bootstrap a new node and start it with:
62+
> ccm add node3 -i 127.0.0.3 -j 7300 -b
63+
> ccm node3 start
64+
65+
This will wait for node3 to be fully bootstrapped, so this will take around 90
66+
seconds. You can use --no-wait to avoid this.
67+
68+
ccm then provide a few conveniences, like flushing a full cluster:
69+
> ccm flush
70+
or a single node:
71+
> ccm node2 flush
72+
73+
You can watch the log file of a given node with:
74+
> ccm node1 showlog
75+
(this exec 'less' on the log file)
76+
77+
And you can remove the whole cluster with:
78+
> ccm remove
79+
80+
There is a bunch of other commands (some of nodetool command are provided, just so that
81+
you don't have to remember the IP addresses and port number). Just try 'ccm'
82+
to get a list of available command. Then each command options are documented:
83+
for instance 'ccm add -h' describe the option for 'ccm add'.
84+
85+
86+
Where are things stored
87+
-----------------------
88+
89+
By default, ccm store all the node data and configuration file under ~/.ccm/cluster_name/.
90+
This can be overriden using the --config-dir option with each command.
91+
92+
93+
Notes
94+
-----
95+
96+
I use this script almost daily for quick Cassandra test, but this is *not*
97+
heavily tested, so you have been warned. I do welcome suggestion however.
98+
99+
100+
Sylvain Lebresne <[email protected]>

__init__.py

Whitespace-only changes.

ccm

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
#!/usr/bin/python
2+
3+
import os, sys
4+
5+
L = os.path.realpath(__file__).split(os.path.sep)[:-1]
6+
root = os.path.sep.join(L)
7+
sys.path.append(os.path.join(root, 'cmds'))
8+
import command, common
9+
from cluster_cmds import *
10+
from cluster_cass_cmds import *
11+
from node_cmds import *
12+
from node_cass_cmds import *
13+
14+
def get_command(kind, cmd):
15+
cmd_name = kind.lower().capitalize() + cmd.lower().capitalize() + "Cmd"
16+
try:
17+
klass = globals()[cmd_name]
18+
except KeyError:
19+
return None
20+
if not issubclass(klass, command.Cmd):
21+
return None
22+
return klass()
23+
24+
def print_global_usage():
25+
print "Usage:"
26+
print " ccm <cluster_cmd> [options]"
27+
print " ccm <node_name> <node_cmd> [options]"
28+
print ""
29+
print "Where <node_name> is the name of a node of the current cluster, <cluster_cmd> is one of"
30+
for cmd_name in cluster_cmds():
31+
cmd = get_command("cluster", cmd_name)
32+
if not cmd:
33+
print "Internal error, unknown command {0}".format(cmd_name)
34+
exit(1)
35+
print " {0:14} {1}".format(cmd_name, cmd.description())
36+
print "and <node_cmd> is one of"
37+
for cmd_name in node_cmds():
38+
cmd = get_command("node", cmd_name)
39+
if not cmd:
40+
print "Internal error, unknown command {0}".format(cmd_name)
41+
exit(1)
42+
print " {0:14} {1}".format(cmd_name, cmd.description())
43+
exit(1)
44+
45+
if len(sys.argv) <= 1:
46+
print "Missing arguments"
47+
print_global_usage()
48+
49+
arg1 = sys.argv[1].lower()
50+
51+
if arg1 in cluster_cmds():
52+
kind = 'cluster'
53+
cmd = arg1
54+
cmd_args = sys.argv[2:]
55+
else:
56+
if len(sys.argv) <= 2:
57+
print "Missing arguments"
58+
print_global_usage()
59+
kind = 'node'
60+
node = arg1
61+
cmd = sys.argv[2]
62+
cmd_args = [node] + sys.argv[3:]
63+
64+
cmd = get_command(kind, cmd)
65+
if not cmd:
66+
print "Unknown node or command: {0}".format(arg1)
67+
exit(1)
68+
69+
parser = cmd.get_parser()
70+
71+
(options, args) = parser.parse_args(cmd_args)
72+
cmd.validate(parser, options, args)
73+
74+
cmd.run()

ccm_lib/__init__.py

Whitespace-only changes.

ccm_lib/cluster.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# ccm clusters
2+
3+
import common, yaml, os
4+
from node import Node
5+
6+
class Cluster():
7+
def __init__(self, path, name):
8+
self.name = name
9+
self.nodes = {}
10+
self.seeds = []
11+
self.path = path
12+
13+
def save(self):
14+
node_list = [ node.name for node in self.nodes.values() ]
15+
seed_list = [ node.name for node in self.seeds ]
16+
filename = os.path.join(self.path, self.name, 'cluster.conf')
17+
with open(filename, 'w') as f:
18+
yaml.dump({ 'name' : self.name, 'nodes' : node_list, 'seeds' : seed_list }, f)
19+
20+
@staticmethod
21+
def load(path, name):
22+
cluster_path = os.path.join(path, name)
23+
filename = os.path.join(cluster_path, 'cluster.conf')
24+
with open(filename, 'r') as f:
25+
data = yaml.load(f)
26+
try:
27+
cluster = Cluster(path, data['name'])
28+
node_list = data['nodes']
29+
seed_list = data['seeds']
30+
except KeyError as k:
31+
raise common.LoadError("Error Loading " + filename + ", missing property:" + k)
32+
33+
for node_name in node_list:
34+
cluster.nodes[node_name] = Node.load(cluster_path, node_name, cluster)
35+
for seed_name in seed_list:
36+
cluster.seeds.append(cluster.nodes[seed_name])
37+
return cluster
38+
39+
def add(self, node, is_seed):
40+
self.nodes[node.name] = node
41+
if is_seed:
42+
self.seeds.append(node)
43+
44+
def get_path(self):
45+
return os.path.join(self.path, self.name)
46+
47+
def get_seeds(self):
48+
return [ s.network_interfaces['storage'][0] for s in self.seeds ]
49+
50+
def show(self, verbose):
51+
if len(self.nodes.values()) == 0:
52+
print "No node in this cluster yet"
53+
return
54+
for node in self.nodes.values():
55+
if (verbose):
56+
node.show(show_cluster=False)
57+
print ""
58+
else:
59+
node.show(only_status=True)
60+
61+
# update_pids() should be called after this
62+
def start(self, cassandra_dir):
63+
started = []
64+
for node in self.nodes.values():
65+
if not node.is_running():
66+
p = node.start(cassandra_dir)
67+
started.append((node, p))
68+
return started
69+
70+
def update_pids(self, started):
71+
for node, p in started:
72+
try:
73+
node.update_pid(p)
74+
except StartError as e:
75+
print str(e)
76+
77+
def stop(self):
78+
not_running = []
79+
for node in self.nodes.values():
80+
if not node.stop():
81+
not_running.append(node)
82+
return not_running
83+
84+
85+
def nodetool(self, cassandra_dir, nodetool_cmd):
86+
for node in self.nodes.values():
87+
if node.is_running():
88+
node.nodetool(cassandra_dir, nodetool_cmd)

ccm_lib/common.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
#
2+
# Cassandra Cluster Management lib
3+
#
4+
5+
import os, common, shutil, re
6+
from cluster import Cluster
7+
from node import Node
8+
9+
USER_HOME = os.path.expanduser('~')
10+
11+
CASSANDRA_BIN_DIR= "bin"
12+
CASSANDRA_CONF_DIR= "conf"
13+
14+
CASSANDRA_CONF = "cassandra.yaml"
15+
LOG4J_CONF = "log4j-server.properties"
16+
CASSANDRA_ENV = "cassandra-env.sh"
17+
CASSANDRA_SH = "cassandra.in.sh"
18+
19+
class LoadError(Exception):
20+
pass
21+
22+
def get_default_path():
23+
default_path = os.path.join(USER_HOME, '.ccm')
24+
if not os.path.exists(default_path):
25+
os.mkdir(default_path)
26+
return default_path
27+
28+
def parse_interface(itf, default_port):
29+
i = itf.split(':')
30+
if len(i) == 1:
31+
return (i[0].strip(), default_port)
32+
elif len(i) == 2:
33+
return (i[0].strip(), int(i[1].strip()))
34+
else:
35+
raise ValueError("Invalid interface definition: " + itf)
36+
37+
def current_cluster_name(path):
38+
try:
39+
with open(os.path.join(path, 'CURRENT'), 'r') as f:
40+
return f.readline().strip()
41+
except IOError:
42+
return None
43+
44+
def load_current_cluster(path):
45+
name = current_cluster_name(path)
46+
if name is None:
47+
print 'No currently active cluster (use ccm cluster switch)'
48+
exit(1)
49+
try:
50+
return Cluster.load(path, name)
51+
except common.LoadError as e:
52+
print str(e)
53+
exit(1)
54+
55+
# may raise OSError if dir exists
56+
def create_cluster(path, name):
57+
dir_name = os.path.join(path, name)
58+
os.mkdir(dir_name)
59+
cluster = Cluster(path, name)
60+
cluster.save()
61+
return cluster
62+
63+
def switch_cluster(path, new_name):
64+
with open(os.path.join(path, 'CURRENT'), 'w') as f:
65+
f.write(new_name + '\n')
66+
67+
def replace_in_file(file, regexp, replace):
68+
replaces_in_file(file, [(regexp, replace)])
69+
70+
def replaces_in_file(file, replacement_list):
71+
rs = [ (re.compile(regexp), repl) for (regexp, repl) in replacement_list]
72+
file_tmp = file + ".tmp"
73+
with open(file, 'r') as f:
74+
with open(file_tmp, 'w') as f_tmp:
75+
for line in f:
76+
for r, replace in rs:
77+
match = r.search(line)
78+
if match:
79+
line = replace + "\n"
80+
f_tmp.write(line)
81+
shutil.move(file_tmp, file)
82+
83+
def make_cassandra_env(cassandra_dir, node_path):
84+
sh_file = os.path.join(CASSANDRA_BIN_DIR, CASSANDRA_SH)
85+
orig = os.path.join(cassandra_dir, sh_file)
86+
dst = os.path.join(node_path, sh_file)
87+
shutil.copy(orig, dst)
88+
replacements = [
89+
('CASSANDRA_HOME=', '\tCASSANDRA_HOME=%s' % cassandra_dir),
90+
('CASSANDRA_CONF=', '\tCASSANDRA_CONF=%s' % os.path.join(node_path, 'conf'))
91+
]
92+
common.replaces_in_file(dst, replacements)
93+
env = os.environ.copy()
94+
env['CASSANDRA_INCLUDE'] = os.path.join(dst)
95+
return env

0 commit comments

Comments
 (0)