cassandra_backup 0.0.6 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile CHANGED
@@ -1,3 +1,5 @@
1
1
  source :rubygems
2
+ gemspec
2
3
 
3
- gemspec
4
+ gem 'rake'
5
+ gem 'mocha'
@@ -6,16 +6,25 @@ CassandraBackup is a command line tool for dumping and restoring a Cassandra key
6
6
 
7
7
  Run cassandra_dump, specifying the keyspace:
8
8
 
9
- cassandra_dump depot_production > backup.json
9
+ cassandra_dump depot_production > backup.cql
10
10
 
11
11
  == Restoring a Keyspace
12
12
 
13
13
  Run cassandra_import, specifying the keyspace:
14
14
 
15
- cassandra_import depot_production < backup.json
15
+ cassandra_import depot_production < backup.cql
16
16
 
17
17
  == Options
18
18
 
19
- --servers:: A comma delimited list of servers. Defaults to 127.0.0.1:9160. (--servers 127.0.0.14:9169,127.0.1.19:9100)
20
- --version:: Set cassandra version. Defaults to 1.0. (--version 0.6)
21
- --columns:: The column families to dump. Defaults to all. (--columns Widgets --columns People)
19
+ --servers: A list of servers. Defaults to 127.0.0.1:9160. (--servers 127.0.0.14:9169,127.0.1.19:9100)
20
+ --version: Set cassandra version. Defaults to latest. (--version 1.0)
21
+ --tables: The tables to dump. Defaults to all. (--tables widgets,people)
22
+ --where: A condition to apply to all queries (--where="state='WA'")
23
+
24
+ == Notes on options
25
+
26
+ Options such as --servers and --tables support arrays in multiple formats. All of the following are the same:
27
+
28
+ --tables Widgets,People
29
+ --tables "Widgets People"
30
+ --tables Widgets --tables People
@@ -0,0 +1,12 @@
1
+ require 'bundler/setup'
2
+ require 'rake'
3
+ require 'rake/testtask'
4
+
5
+ task default: :test
6
+
7
+ Rake::TestTask.new(:test) do |t|
8
+ t.libs << 'lib'
9
+ t.libs << 'test'
10
+ t.pattern = 'test/cassandra_backup/**/*_test.rb'
11
+ t.verbose = true
12
+ end
@@ -1,4 +1,4 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  require File.expand_path('../../lib/cassandra_backup', __FILE__)
4
- CassandraBackup::Dumper.run!
4
+ CassandraBackup::Dumper.run_command CassandraBackup::Command.new(ARGV)
@@ -1,4 +1,4 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  require File.expand_path('../../lib/cassandra_backup', __FILE__)
4
- CassandraBackup::Importer.run!
4
+ CassandraBackup::Importer.run_command CassandraBackup::Command.new(ARGV)
@@ -2,7 +2,7 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = 'cassandra_backup'
5
- s.version = '0.0.6'
5
+ s.version = '0.1.0'
6
6
  s.description = 'Cassandra Backup'
7
7
  s.summary = 'Cassandra Backup'
8
8
  s.authors = ['Infogroup']
@@ -16,7 +16,5 @@ Gem::Specification.new do |s|
16
16
  s.files = `git ls-files`.split("\n")
17
17
  s.test_files = `git ls-files -- {test}/*`.split("\n")
18
18
 
19
- s.add_runtime_dependency('mcmire-cassandra', ">= 0.12.3")
20
- s.add_runtime_dependency('yajl-ruby')
21
- s.add_development_dependency('bundler')
19
+ s.add_dependency 'cassandra-cql'
22
20
  end
@@ -1,4 +1,6 @@
1
- require 'yajl'
2
-
1
+ require 'cassandra-cql'
2
+ require 'cassandra_backup/command'
3
+ require 'cassandra_backup/config'
4
+ require 'cassandra_backup/cqlsh'
3
5
  require 'cassandra_backup/dumper'
4
6
  require 'cassandra_backup/importer'
@@ -3,9 +3,9 @@ require 'optparse'
3
3
  module CassandraBackup
4
4
  class Command
5
5
 
6
- attr_accessor :options
6
+ attr_reader :options
7
7
  def initialize(args)
8
- self.options = {}
8
+ @options = {}
9
9
 
10
10
  if args.first
11
11
  options[:keyspace] = args.first
@@ -15,48 +15,33 @@ module CassandraBackup
15
15
 
16
16
  OptionParser.new do |opts|
17
17
  opts.banner = "Usage: keyspace [options]"
18
- opts.on('-s', '--servers SERVERS', 'Set server list. Default is 127.0.0.1:9160.') do |v|
19
- options[:servers] = v.split(/\s|,/)
18
+ opts.on('-s', '--servers servers', 'Set server list. Default is 127.0.0.1:9160.') do |v|
19
+ store_array_option :servers, v
20
20
  end
21
- opts.on('-v', '--version VERSION', 'Set cassandra version. Default is 1.0.') do |v|
21
+ opts.on('-c', '--tables tables', 'Set tables. Defaults to all') do |v|
22
+ store_array_option :tables, v
23
+ end
24
+ opts.on('-v', '--version VERSION', 'Set cassandra version. Defaults to latest.') do |v|
22
25
  options[:version] = v
23
26
  end
24
- opts.on('-c', '--columns Columns', 'Set column families. Defaults to all') do |v|
25
- (options[:columns] ||= []).concat v.split(/\s|,/)
27
+ opts.on('-w', '--where=\'where_conditions\'') do |v|
28
+ options[:where] = v
26
29
  end
27
30
  opts.on('-h', 'Show this help message.') do
28
31
  $stdout.puts opts; exit
29
32
  end
30
- opts.parse!(args)
31
- end
32
-
33
- require required_version
34
- end
35
-
36
- def keyspace
37
- options[:keyspace]
38
- end
39
33
 
40
- def servers
41
- if options[:servers]
42
- options[:servers]
43
- else
44
- ['127.0.0.1:9160']
34
+ opts.parse!(args)
45
35
  end
46
36
  end
47
37
 
48
- def columns
49
- if options[:columns]
50
- options[:columns]
51
- end
38
+ def config
39
+ CassandraBackup::Config.new(options)
52
40
  end
53
41
 
54
- def required_version
55
- if options[:version]
56
- "cassandra/#{options[:version]}"
57
- else
58
- "cassandra/1.0"
42
+ private
43
+ def store_array_option(name, value)
44
+ (options[name] ||= []).concat value.split(/\s|,/)
59
45
  end
60
- end
61
46
  end
62
47
  end
@@ -0,0 +1,53 @@
1
+ module CassandraBackup
2
+ class Config
3
+ attr_reader :options
4
+ def initialize(options = {})
5
+ @options = options
6
+
7
+ require_version!
8
+ end
9
+
10
+ def servers
11
+ if options[:servers]
12
+ options[:servers]
13
+ else
14
+ ['127.0.0.1:9160']
15
+ end
16
+ end
17
+
18
+ def input
19
+ options[:input] || STDIN
20
+ end
21
+
22
+ def output
23
+ options[:output] || STDOUT
24
+ end
25
+
26
+ def require_version!
27
+ if options[:version]
28
+ require "cassandra-cql/#{options[:version]}"
29
+ end
30
+ end
31
+
32
+ def keyspace
33
+ options[:keyspace]
34
+ end
35
+
36
+ def tables
37
+ options[:tables]
38
+ end
39
+
40
+ def where
41
+ options[:where]
42
+ end
43
+
44
+ def connection
45
+ if keyspace
46
+ options = {keyspace: keyspace}
47
+ else
48
+ options = {}
49
+ end
50
+ @connection ||= CassandraCQL::Database.new(servers, options)
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,27 @@
1
+ module CassandraBackup
2
+ class Cqlsh
3
+
4
+ attr_reader :config
5
+
6
+ def initialize(config)
7
+ @config = config
8
+ end
9
+
10
+ def exec(command)
11
+ `echo "#{command};" | #{cqlsh} -k #{keyspace} #{server}`.sub(/^(.*)$/, '').strip
12
+ end
13
+
14
+ private
15
+ def cqlsh
16
+ ENV['CQLSH'] || 'cqlsh'
17
+ end
18
+
19
+ def keyspace
20
+ config.keyspace
21
+ end
22
+
23
+ def server
24
+ config.servers.first.gsub(/:.*/, '')
25
+ end
26
+ end
27
+ end
@@ -1,26 +1,79 @@
1
- require 'cassandra_backup/backup_process'
2
-
3
1
  module CassandraBackup
4
- class Dumper < BackupProcess
2
+ class Dumper
3
+ def self.run_command(command)
4
+ new(command.config).run
5
+ end
6
+
7
+ attr_reader :config
8
+ attr_reader :cqlsh
9
+ attr_reader :primary_keys
10
+ def initialize(config)
11
+ @config = config
12
+ @cqlsh = CassandraBackup::Cqlsh.new(config)
13
+ @primary_keys = {}
14
+ end
15
+
5
16
  def run
6
- column_family_names.each do |column_family|
7
- connection.each(column_family) do |key, attributes|
8
- output_io.puts encoder.encode([column_family, key, attributes])
9
- end
17
+ table_names.each { |table_name| dump_table_schema(table_name) }
18
+ table_names.each { |table_name| dump_table_rows(table_name) }
19
+ end
20
+
21
+ def dump_table_schema(table_name)
22
+ table_description = cqlsh.exec("DESCRIBE COLUMNFAMILY #{table_name}")
23
+ capture_primary_key(table_name, table_description)
24
+ config.output.puts(table_description)
25
+ config.output.puts
26
+ end
27
+
28
+ def dump_table_rows(table_name)
29
+ primary_key = primary_key_for(table_name)
30
+ each_row(table_name) do |row|
31
+ next if row.keys.size == 1 && row.keys.first == primary_key
32
+
33
+ colummns = CassandraCQL::Statement.quote(row.keys)
34
+ values = CassandraCQL::Statement.quote(row.values.map { |v| CassandraCQL::Statement.escape(v) })
35
+
36
+ config.output.puts "INSERT INTO #{table_name} (#{colummns}) VALUES (#{values});"
10
37
  end
11
38
  end
12
39
 
40
+ def table_names
41
+ if config.tables
42
+ config.tables
43
+ else
44
+ cqlsh.exec('DESCRIBE COLUMNFAMILIES').split.sort
45
+ end
46
+ end
47
+
48
+ def execute(cql)
49
+ config.connection.execute(cql)
50
+ end
51
+
13
52
  private
14
- def column_family_names
15
- if command.columns
16
- command.columns
17
- else
18
- connection.column_families.keys - ['schema_migrations']
53
+ def primary_key_for(table_name)
54
+ primary_keys[table_name] || 'id'
55
+ end
56
+
57
+ def capture_primary_key(table_name, table_description)
58
+ if table_description =~ /^\s*(\w+).*PRIMARY KEY/
59
+ primary_keys[table_name] = $1
19
60
  end
20
61
  end
21
62
 
22
- def encoder
23
- @encoder ||= Yajl::Encoder.new
63
+ def each_row(table_name)
64
+ primary_key = primary_key_for(table_name)
65
+ cassandra_result = execute "SELECT * FROM #{table_name}"
66
+
67
+ while cassandra_result.rows > 0
68
+ last_fetch = nil
69
+ cassandra_result.fetch_hash do |hash|
70
+ yield(hash)
71
+ last_fetch = hash
72
+ end
73
+
74
+ where_cql = "'#{primary_key}' > '#{last_fetch[primary_key]}'"
75
+ cassandra_result = execute "SELECT * FROM #{table_name} WHERE #{where_cql}"
76
+ end
24
77
  end
25
78
  end
26
- end
79
+ end
@@ -1,17 +1,37 @@
1
- require 'cassandra_backup/backup_process'
2
-
3
1
  module CassandraBackup
4
- class Importer < BackupProcess
2
+ class Importer
3
+ def self.run_command(command)
4
+ new(command.config).run
5
+ end
6
+
7
+ attr_reader :config
8
+ def initialize(config)
9
+ @config = config
10
+ end
11
+
5
12
  def run
6
- parser.parse(input_io) do |column_family, key, attributes|
7
- p "inserting"
8
- connection.insert column_family, key, attributes
13
+ each_full_statement(config.input) do |cql|
14
+ execute cql
9
15
  end
10
16
  end
11
17
 
12
- private
13
- def parser
14
- @parser ||= Yajl::Parser.new
18
+ def execute(cql)
19
+ config.connection.execute(cql)
20
+ end
21
+
22
+ def each_full_statement(io)
23
+ current_cql = ''
24
+
25
+ io.each_line do |line|
26
+ next unless line =~ /[^[:space:]]/
27
+
28
+ current_cql << line.rstrip
29
+
30
+ if current_cql =~ /;$/
31
+ yield(current_cql)
32
+ current_cql = ''
33
+ end
15
34
  end
35
+ end
16
36
  end
17
37
  end
@@ -0,0 +1,33 @@
1
+ require 'helper'
2
+ require 'shellwords'
3
+
4
+ class CassandraBackup::CommandTest < MiniTest::Spec
5
+ def test_keyspace
6
+ assert_equal 'zombies', command('zombies').options[:keyspace]
7
+ end
8
+
9
+ def test_single_value_options
10
+ [:where, :version].each do |option|
11
+ assert_nil command('zombies').options[option]
12
+ assert_equal 'foo', command("zombies --#{option} foo").options[option]
13
+ end
14
+ end
15
+
16
+ def test_array_options
17
+ [:servers, :tables].each do |option|
18
+ assert_nil command('zombies').options[option]
19
+ assert_equal ['foo', 'bar'], command("zombies --#{option} \"foo bar\"").options[option]
20
+ assert_equal ['foo', 'bar'], command("zombies --#{option} foo,bar").options[option]
21
+ assert_equal ['foo', 'bar'], command("zombies --#{option} foo --#{option} bar").options[option]
22
+ end
23
+ end
24
+
25
+ def test_config
26
+ assert_kind_of CassandraBackup::Config, command('zombies').config
27
+ end
28
+
29
+ private
30
+ def command(line)
31
+ CassandraBackup::Command.new Shellwords.shellsplit(line)
32
+ end
33
+ end
@@ -0,0 +1,29 @@
1
+ require 'helper'
2
+
3
+ class CassandraBackup::ConfigTest < MiniTest::Spec
4
+ def test_servers
5
+ assert_equal ['127.0.0.1:9160'], config.servers
6
+ assert_equal ['foo'], config(servers: ['foo']).servers
7
+ end
8
+
9
+ def test_input
10
+ assert_equal STDIN, config.input
11
+ refute_equal STDIN, config(input: StringIO.new).input
12
+ end
13
+
14
+ def test_output
15
+ assert_equal STDOUT, config.output
16
+ refute_equal STDOUT, config(output: StringIO.new).output
17
+ end
18
+
19
+ def test_require_version
20
+ # expects(:require).with('cassandra-cql/1.0').once
21
+ # CassandraBackup::Config.new(version: '1.0')
22
+ end
23
+
24
+ private
25
+ def config(options = {})
26
+ CassandraBackup::Config.new(options)
27
+ end
28
+
29
+ end
@@ -0,0 +1,43 @@
1
+ require 'helper'
2
+
3
+ class CassandraBackup::DumperTest < MiniTest::Spec
4
+ def test_table_names
5
+ assert_equal ['presidents', 'senators'], create_dumper.table_names
6
+ assert_equal ['presidents'], create_dumper(tables: ['presidents']).table_names
7
+ end
8
+
9
+ def test_dump_table_schema
10
+ output = StringIO.new
11
+ dumper = create_dumper output: output
12
+
13
+ dumper.dump_table_schema('senators')
14
+
15
+ output.rewind
16
+ lines = output.readlines
17
+ assert_match /CREATE TABLE senators/, lines.first
18
+ assert_equal "\n", lines.last
19
+ assert_equal({"senators" => "id"}, dumper.primary_keys)
20
+ end
21
+
22
+ def test_dump_table_rows
23
+ output = StringIO.new
24
+ dumper = create_dumper output: output
25
+ dumper.execute "TRUNCATE senators"
26
+ dumper.execute "INSERT INTO senators ('id', 'name', 'state') VALUES ('abc', 'K''la', 'WA')"
27
+ dumper.execute "INSERT INTO senators ('id', 'name', 'state') VALUES ('xyz', 'Bob', 'NY')"
28
+
29
+ dumper.dump_table_rows('senators')
30
+
31
+ output.rewind
32
+ lines = output.readlines
33
+
34
+ assert_equal 2, lines.size
35
+ assert lines.include?("INSERT INTO senators ('id','name','state') VALUES ('abc','K''la','WA');\n")
36
+ assert lines.include?("INSERT INTO senators ('id','name','state') VALUES ('xyz','Bob','NY');\n")
37
+ end
38
+
39
+ private
40
+ def create_dumper(options = {})
41
+ CassandraBackup::Dumper.new CassandraBackup::Config.new(options.merge(keyspace: 'cassandra_dump_test'))
42
+ end
43
+ end
@@ -0,0 +1,40 @@
1
+ require 'helper'
2
+
3
+ class CassandraBackup::ImporterTest < MiniTest::Spec
4
+ def test_run
5
+ input = StringIO.new("INSERT INTO presidents (id, name) VALUES ('1st', 'George Washington');")
6
+ importer = create_importer input: input
7
+ importer.execute "TRUNCATE presidents"
8
+
9
+ importer.run
10
+
11
+ assert_equal 1, importer.execute("select * from presidents").rows
12
+ end
13
+
14
+ def test_each_full_statement
15
+ input = StringIO.new(
16
+ "INSERT INTO presidents (id, name) VALUES\n" +
17
+ " ('1st', 'George Washington');\n" +
18
+ " \n" +
19
+ "UPDATE presidents SET name='Zombie' WHERE id='1st';\n" +
20
+ "\n"
21
+ )
22
+ stmts = []
23
+
24
+ create_importer.each_full_statement(input) do |cql|
25
+ stmts << cql
26
+ end
27
+
28
+ expected = [
29
+ "INSERT INTO presidents (id, name) VALUES ('1st', 'George Washington');",
30
+ "UPDATE presidents SET name='Zombie' WHERE id='1st';"
31
+ ]
32
+ assert_equal expected, stmts
33
+ end
34
+
35
+ private
36
+
37
+ def create_importer(options = {})
38
+ CassandraBackup::Importer.new CassandraBackup::Config.new(options.merge(keyspace: 'cassandra_dump_test'))
39
+ end
40
+ end
@@ -0,0 +1,6 @@
1
+ require 'bundler/setup'
2
+ require 'minitest/autorun'
3
+ Bundler.require
4
+
5
+ require 'cassandra_backup'
6
+ require 'setup/connection'
@@ -0,0 +1,11 @@
1
+ CassandraBackup::Config.new.tap do |config|
2
+ begin
3
+ config.connection.execute("DROP KEYSPACE cassandra_dump_test")
4
+ rescue Exception => e
5
+ end
6
+ config.connection.execute("CREATE KEYSPACE cassandra_dump_test WITH strategy_class='org.apache.cassandra.locator.SimpleStrategy' AND strategy_options:replication_factor=1")
7
+ config.connection.execute("USE cassandra_dump_test")
8
+ config.connection.execute("CREATE TABLE presidents (id varchar PRIMARY KEY)")
9
+ config.connection.execute("CREATE TABLE senators (id varchar PRIMARY KEY, state varchar)")
10
+ config.connection.execute("CREATE INDEX senators_state_index ON senators(state)")
11
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cassandra_backup
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.6
4
+ version: 0.1.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,26 +9,10 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-06-19 00:00:00.000000000 Z
12
+ date: 2012-11-02 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
- name: mcmire-cassandra
16
- requirement: !ruby/object:Gem::Requirement
17
- none: false
18
- requirements:
19
- - - ! '>='
20
- - !ruby/object:Gem::Version
21
- version: 0.12.3
22
- type: :runtime
23
- prerelease: false
24
- version_requirements: !ruby/object:Gem::Requirement
25
- none: false
26
- requirements:
27
- - - ! '>='
28
- - !ruby/object:Gem::Version
29
- version: 0.12.3
30
- - !ruby/object:Gem::Dependency
31
- name: yajl-ruby
15
+ name: cassandra-cql
32
16
  requirement: !ruby/object:Gem::Requirement
33
17
  none: false
34
18
  requirements:
@@ -43,22 +27,6 @@ dependencies:
43
27
  - - ! '>='
44
28
  - !ruby/object:Gem::Version
45
29
  version: '0'
46
- - !ruby/object:Gem::Dependency
47
- name: bundler
48
- requirement: !ruby/object:Gem::Requirement
49
- none: false
50
- requirements:
51
- - - ! '>='
52
- - !ruby/object:Gem::Version
53
- version: '0'
54
- type: :development
55
- prerelease: false
56
- version_requirements: !ruby/object:Gem::Requirement
57
- none: false
58
- requirements:
59
- - - ! '>='
60
- - !ruby/object:Gem::Version
61
- version: '0'
62
30
  description: Cassandra Backup
63
31
  email: gems@gotime.com
64
32
  executables:
@@ -71,15 +39,22 @@ files:
71
39
  - .gitignore
72
40
  - Gemfile
73
41
  - README.rdoc
42
+ - Rakefile
74
43
  - bin/cassandra_dump
75
44
  - bin/cassandra_import
76
45
  - cassandra_backup.gemspec
77
46
  - lib/cassandra_backup.rb
78
- - lib/cassandra_backup/backup_process.rb
79
47
  - lib/cassandra_backup/command.rb
48
+ - lib/cassandra_backup/config.rb
49
+ - lib/cassandra_backup/cqlsh.rb
80
50
  - lib/cassandra_backup/dumper.rb
81
51
  - lib/cassandra_backup/importer.rb
82
- - test/test_helper.rb
52
+ - test/cassandra_backup/command_test.rb
53
+ - test/cassandra_backup/config_test.rb
54
+ - test/cassandra_backup/dumper_test.rb
55
+ - test/cassandra_backup/importer_test.rb
56
+ - test/helper.rb
57
+ - test/setup/connection.rb
83
58
  homepage: http://github.com/data-axle/cassandra_backup
84
59
  licenses: []
85
60
  post_install_message:
@@ -1,30 +0,0 @@
1
- require 'cassandra_backup/command'
2
-
3
- module CassandraBackup
4
- class BackupProcess
5
- def self.run!
6
- new(CassandraBackup::Command.new(ARGV)).run
7
- end
8
-
9
- attr_reader :command
10
- def initialize(command)
11
- @command = command
12
- end
13
-
14
- def run
15
- raise "inheriting class must implement"
16
- end
17
-
18
- def connection
19
- @connection ||= Cassandra.new(command.keyspace, command.servers)
20
- end
21
-
22
- def input_io
23
- $stdin
24
- end
25
-
26
- def output_io
27
- $stdout
28
- end
29
- end
30
- end
@@ -1,2 +0,0 @@
1
- # require 'bundler/setup'
2
- # Bundler.require